forked from GitHub-Mirror/riotX-android
Media: start to play with uploading media
This commit is contained in:
parent
96a67a44ac
commit
18591d0287
@ -1,6 +1,7 @@
|
||||
apply plugin: 'com.android.library'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlin-kapt'
|
||||
apply plugin: 'kotlin-android-extensions'
|
||||
apply plugin: 'realm-android'
|
||||
apply plugin: 'okreplay'
|
||||
|
||||
@ -19,6 +20,10 @@ repositories {
|
||||
jcenter()
|
||||
}
|
||||
|
||||
androidExtensions {
|
||||
experimental = true
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion 28
|
||||
testOptions.unitTests.includeAndroidResources = true
|
||||
|
@ -19,6 +19,7 @@ package im.vector.matrix.android.api.session.room.send
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.session.events.model.Event
|
||||
import im.vector.matrix.android.api.util.Cancelable
|
||||
import im.vector.matrix.android.internal.session.room.media.MediaAttachment
|
||||
|
||||
/**
|
||||
* This interface defines methods to send events in a room. It's implemented at the room level.
|
||||
@ -33,5 +34,6 @@ interface SendService {
|
||||
*/
|
||||
fun sendTextMessage(text: String, callback: MatrixCallback<Event>): Cancelable
|
||||
|
||||
fun sendMedia(attachment: MediaAttachment, callback: MatrixCallback<Event>): Cancelable
|
||||
|
||||
}
|
@ -21,7 +21,7 @@ import im.vector.matrix.android.api.session.content.ContentUrlResolver
|
||||
|
||||
|
||||
private const val MATRIX_CONTENT_URI_SCHEME = "mxc://"
|
||||
private const val URI_PREFIX_CONTENT_API = "/_matrix/media/v1/"
|
||||
internal const val URI_PREFIX_CONTENT_API = "/_matrix/media/v1/"
|
||||
|
||||
internal class DefaultContentUrlResolver(private val homeServerConnectionConfig: HomeServerConnectionConfig) : ContentUrlResolver {
|
||||
|
||||
|
@ -17,7 +17,6 @@
|
||||
package im.vector.matrix.android.internal.session.room
|
||||
|
||||
import com.zhuinden.monarchy.Monarchy
|
||||
import im.vector.matrix.android.api.auth.data.Credentials
|
||||
import im.vector.matrix.android.api.session.room.Room
|
||||
import im.vector.matrix.android.internal.session.room.members.LoadRoomMembersTask
|
||||
import im.vector.matrix.android.internal.session.room.members.RoomMemberExtractor
|
||||
@ -33,7 +32,6 @@ import im.vector.matrix.android.internal.task.TaskExecutor
|
||||
|
||||
internal class RoomFactory(private val loadRoomMembersTask: LoadRoomMembersTask,
|
||||
private val monarchy: Monarchy,
|
||||
private val credentials: Credentials,
|
||||
private val paginationTask: PaginationTask,
|
||||
private val contextOfEventTask: GetContextOfEventTask,
|
||||
private val setReadMarkersTask: SetReadMarkersTask,
|
||||
|
@ -17,6 +17,7 @@
|
||||
package im.vector.matrix.android.internal.session.room
|
||||
|
||||
import im.vector.matrix.android.internal.session.DefaultSession
|
||||
import im.vector.matrix.android.internal.session.room.media.MediaUploader
|
||||
import im.vector.matrix.android.internal.session.room.members.DefaultLoadRoomMembersTask
|
||||
import im.vector.matrix.android.internal.session.room.members.LoadRoomMembersTask
|
||||
import im.vector.matrix.android.internal.session.room.read.DefaultSetReadMarkersTask
|
||||
@ -61,7 +62,11 @@ class RoomModule {
|
||||
}
|
||||
|
||||
scope(DefaultSession.SCOPE) {
|
||||
RoomFactory(get(), get(), get(), get(), get(), get(), get(), get())
|
||||
MediaUploader(get(), get())
|
||||
}
|
||||
|
||||
scope(DefaultSession.SCOPE) {
|
||||
RoomFactory(get(), get(), get(), get(), get(), get(), get())
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,36 @@
|
||||
/*
|
||||
*
|
||||
* * Copyright 2019 New Vector Ltd
|
||||
* *
|
||||
* * Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* * you may not use this file except in compliance with the License.
|
||||
* * You may obtain a copy of the License at
|
||||
* *
|
||||
* * http://www.apache.org/licenses/LICENSE-2.0
|
||||
* *
|
||||
* * Unless required by applicable law or agreed to in writing, software
|
||||
* * distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* * See the License for the specific language governing permissions and
|
||||
* * limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.session.room.media
|
||||
|
||||
import android.net.Uri
|
||||
import android.os.Parcelable
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class MediaAttachment(
|
||||
val size: Long = 0,
|
||||
val duration: Long = 0,
|
||||
val date: Long = 0,
|
||||
val height: Long = 0,
|
||||
val width: Long = 0,
|
||||
val name: String? = null,
|
||||
val thumbnail: Uri? = null,
|
||||
val path: String? = null,
|
||||
val mimeType: String? = null
|
||||
) : Parcelable
|
@ -0,0 +1,60 @@
|
||||
/*
|
||||
*
|
||||
* * Copyright 2019 New Vector Ltd
|
||||
* *
|
||||
* * Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* * you may not use this file except in compliance with the License.
|
||||
* * You may obtain a copy of the License at
|
||||
* *
|
||||
* * http://www.apache.org/licenses/LICENSE-2.0
|
||||
* *
|
||||
* * Unless required by applicable law or agreed to in writing, software
|
||||
* * distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* * See the License for the specific language governing permissions and
|
||||
* * limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.session.room.media
|
||||
|
||||
import arrow.core.Try
|
||||
import im.vector.matrix.android.api.auth.data.SessionParams
|
||||
import im.vector.matrix.android.internal.session.content.URI_PREFIX_CONTENT_API
|
||||
import okhttp3.MediaType
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
|
||||
|
||||
internal class MediaUploader(private val okHttpClient: OkHttpClient,
|
||||
private val sessionParams: SessionParams) {
|
||||
|
||||
fun uploadFile(attachment: MediaAttachment): Try<String> {
|
||||
if (attachment.path == null || attachment.mimeType == null) {
|
||||
return Try.raise(RuntimeException())
|
||||
}
|
||||
val urlString = sessionParams.homeServerConnectionConfig.homeServerUri.toString() + URI_PREFIX_CONTENT_API + "upload"
|
||||
val file = File(attachment.path)
|
||||
|
||||
// create RequestBody instance from file
|
||||
val requestFile = RequestBody.create(
|
||||
MediaType.parse(attachment.mimeType),
|
||||
file
|
||||
)
|
||||
val request = Request.Builder()
|
||||
.url(urlString)
|
||||
.post(requestFile)
|
||||
.build()
|
||||
|
||||
return okHttpClient.newCall(request).execute().use { response ->
|
||||
if (response.isSuccessful) {
|
||||
Try.raise(IOException(""))
|
||||
} else {
|
||||
Try.just(response.message())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
/*
|
||||
*
|
||||
* * Copyright 2019 New Vector Ltd
|
||||
* *
|
||||
* * Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* * you may not use this file except in compliance with the License.
|
||||
* * You may obtain a copy of the License at
|
||||
* *
|
||||
* * http://www.apache.org/licenses/LICENSE-2.0
|
||||
* *
|
||||
* * Unless required by applicable law or agreed to in writing, software
|
||||
* * distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* * See the License for the specific language governing permissions and
|
||||
* * limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.session.room.media
|
||||
|
||||
import android.content.Context
|
||||
import androidx.work.Worker
|
||||
import androidx.work.WorkerParameters
|
||||
import com.squareup.moshi.JsonClass
|
||||
import im.vector.matrix.android.internal.di.MatrixKoinComponent
|
||||
import im.vector.matrix.android.internal.util.WorkerParamsFactory
|
||||
import org.koin.standalone.inject
|
||||
|
||||
internal class UploadMediaWorker(context: Context, params: WorkerParameters)
|
||||
: Worker(context, params), MatrixKoinComponent {
|
||||
|
||||
private val mediaUploader by inject<MediaUploader>()
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
internal data class Params(
|
||||
val attachment: MediaAttachment
|
||||
)
|
||||
|
||||
override fun doWork(): Result {
|
||||
val params = WorkerParamsFactory.fromData<Params>(inputData)
|
||||
?: return Result.failure()
|
||||
|
||||
return mediaUploader
|
||||
.uploadFile(params.attachment)
|
||||
.fold({ Result.retry() }, { Result.success() })
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -25,6 +25,8 @@ import im.vector.matrix.android.api.util.Cancelable
|
||||
import im.vector.matrix.android.internal.database.helper.add
|
||||
import im.vector.matrix.android.internal.database.model.ChunkEntity
|
||||
import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoom
|
||||
import im.vector.matrix.android.internal.session.room.media.MediaAttachment
|
||||
import im.vector.matrix.android.internal.session.room.media.UploadMediaWorker
|
||||
import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection
|
||||
import im.vector.matrix.android.internal.util.CancelableWork
|
||||
import im.vector.matrix.android.internal.util.WorkerParamsFactory
|
||||
@ -32,39 +34,70 @@ import im.vector.matrix.android.internal.util.tryTransactionAsync
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
private const val SEND_WORK = "SEND_WORK"
|
||||
private const val BACKOFF_DELAY = 10_000L
|
||||
private val WORK_CONSTRAINTS = Constraints.Builder()
|
||||
.setRequiredNetworkType(NetworkType.CONNECTED)
|
||||
.build()
|
||||
|
||||
internal class DefaultSendService(private val roomId: String,
|
||||
private val eventFactory: EventFactory,
|
||||
private val monarchy: Monarchy) : SendService {
|
||||
|
||||
private val sendConstraints = Constraints.Builder()
|
||||
.setRequiredNetworkType(NetworkType.CONNECTED)
|
||||
.build()
|
||||
|
||||
override fun sendTextMessage(text: String, callback: MatrixCallback<Event>): Cancelable {
|
||||
val event = eventFactory.createTextEvent(roomId, text)
|
||||
|
||||
monarchy.tryTransactionAsync { realm ->
|
||||
val chunkEntity = ChunkEntity.findLastLiveChunkFromRoom(realm, roomId)
|
||||
?: return@tryTransactionAsync
|
||||
chunkEntity.add(roomId, event, PaginationDirection.FORWARDS)
|
||||
}
|
||||
|
||||
val sendContentWorkerParams = SendEventWorker.Params(roomId, event)
|
||||
val workData = WorkerParamsFactory.toData(sendContentWorkerParams)
|
||||
|
||||
val sendWork = OneTimeWorkRequestBuilder<SendEventWorker>()
|
||||
.setConstraints(sendConstraints)
|
||||
.setInputData(workData)
|
||||
.setBackoffCriteria(BackoffPolicy.LINEAR, 10_000, TimeUnit.MILLISECONDS)
|
||||
.build()
|
||||
|
||||
saveLiveEvent(event)
|
||||
val sendWork = createSendEventWork(event)
|
||||
WorkManager.getInstance()
|
||||
.beginUniqueWork(SEND_WORK, ExistingWorkPolicy.APPEND, sendWork)
|
||||
.enqueue()
|
||||
|
||||
return CancelableWork(sendWork.id)
|
||||
}
|
||||
|
||||
override fun sendMedia(attachment: MediaAttachment, callback: MatrixCallback<Event>): Cancelable {
|
||||
// Create an event with the media file path
|
||||
val event = eventFactory.createImageEvent(roomId, attachment)
|
||||
saveLiveEvent(event)
|
||||
|
||||
val uploadWork = createUploadMediaWork(attachment)
|
||||
val sendWork = createSendEventWork(event)
|
||||
|
||||
WorkManager.getInstance()
|
||||
.beginUniqueWork(SEND_WORK, ExistingWorkPolicy.APPEND, uploadWork)
|
||||
.then(sendWork)
|
||||
.enqueue()
|
||||
return CancelableWork(sendWork.id)
|
||||
}
|
||||
|
||||
private fun saveLiveEvent(event: Event) {
|
||||
monarchy.tryTransactionAsync { realm ->
|
||||
val chunkEntity = ChunkEntity.findLastLiveChunkFromRoom(realm, roomId)
|
||||
?: return@tryTransactionAsync
|
||||
chunkEntity.add(roomId, event, PaginationDirection.FORWARDS)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createSendEventWork(event: Event): OneTimeWorkRequest {
|
||||
val sendContentWorkerParams = SendEventWorker.Params(roomId, event)
|
||||
val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams)
|
||||
|
||||
return OneTimeWorkRequestBuilder<SendEventWorker>()
|
||||
.setConstraints(WORK_CONSTRAINTS)
|
||||
.setInputData(sendWorkData)
|
||||
.setBackoffCriteria(BackoffPolicy.LINEAR, BACKOFF_DELAY, TimeUnit.MILLISECONDS)
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun createUploadMediaWork(attachment: MediaAttachment): OneTimeWorkRequest {
|
||||
val uploadMediaWorkerParams = UploadMediaWorker.Params(attachment)
|
||||
val uploadWorkData = WorkerParamsFactory.toData(uploadMediaWorkerParams)
|
||||
|
||||
return OneTimeWorkRequestBuilder<UploadMediaWorker>()
|
||||
.setConstraints(WORK_CONSTRAINTS)
|
||||
.setInputData(uploadWorkData)
|
||||
.setBackoffCriteria(BackoffPolicy.LINEAR, BACKOFF_DELAY, TimeUnit.MILLISECONDS)
|
||||
.build()
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -17,27 +17,45 @@
|
||||
package im.vector.matrix.android.internal.session.room.send
|
||||
|
||||
import im.vector.matrix.android.api.auth.data.Credentials
|
||||
import im.vector.matrix.android.api.session.events.model.Content
|
||||
import im.vector.matrix.android.api.session.events.model.Event
|
||||
import im.vector.matrix.android.api.session.events.model.EventType
|
||||
import im.vector.matrix.android.api.session.events.model.toContent
|
||||
import im.vector.matrix.android.api.session.events.model.toModel
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageImageContent
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageTextContent
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageType
|
||||
import im.vector.matrix.android.internal.di.MoshiProvider
|
||||
import im.vector.matrix.android.internal.session.room.media.MediaAttachment
|
||||
|
||||
internal class EventFactory(private val credentials: Credentials) {
|
||||
|
||||
private val moshi = MoshiProvider.providesMoshi()
|
||||
|
||||
fun createTextEvent(roomId: String, text: String): Event {
|
||||
val content = MessageTextContent(type = MessageType.MSGTYPE_TEXT, body = text)
|
||||
return createEvent(roomId, content)
|
||||
}
|
||||
|
||||
fun createImageEvent(roomId: String, attachment: MediaAttachment): Event {
|
||||
val content = MessageImageContent(
|
||||
type = MessageType.MSGTYPE_IMAGE,
|
||||
body = attachment.name ?: "image",
|
||||
url = attachment.path
|
||||
)
|
||||
return createEvent(roomId, content)
|
||||
}
|
||||
|
||||
fun updateImageEvent(event: Event, url: String): Event {
|
||||
val imageContent = event.content.toModel<MessageImageContent>() ?: return event
|
||||
val updatedContent = imageContent.copy(url = url)
|
||||
return event.copy(content = updatedContent.toContent())
|
||||
}
|
||||
|
||||
fun createEvent(roomId: String, content: Any? = null): Event {
|
||||
return Event(
|
||||
roomId = roomId,
|
||||
originServerTs = dummyOriginServerTs(),
|
||||
sender = credentials.userId,
|
||||
eventId = dummyEventId(roomId),
|
||||
type = EventType.MESSAGE,
|
||||
content = toContent(content)
|
||||
content = content.toContent()
|
||||
)
|
||||
}
|
||||
|
||||
@ -48,13 +66,4 @@ internal class EventFactory(private val credentials: Credentials) {
|
||||
private fun dummyEventId(roomId: String): String {
|
||||
return roomId + "-" + dummyOriginServerTs()
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
private inline fun <reified T> toContent(data: T?): Content? {
|
||||
val moshiAdapter = moshi.adapter(T::class.java)
|
||||
val jsonValue = moshiAdapter.toJsonValue(data)
|
||||
return jsonValue as? Content?
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -149,7 +149,7 @@ dependencies {
|
||||
def coroutines_version = "1.0.1"
|
||||
def markwon_version = '3.0.0-SNAPSHOT'
|
||||
def big_image_viewer_version = '1.5.6'
|
||||
def glide_version = '4.8.0'
|
||||
def glide_version = '4.9.0'
|
||||
|
||||
implementation project(":matrix-sdk-android")
|
||||
implementation project(":matrix-sdk-android-rx")
|
||||
@ -209,6 +209,9 @@ dependencies {
|
||||
implementation "com.github.bumptech.glide:glide:$glide_version"
|
||||
kapt "com.github.bumptech.glide:compiler:$glide_version"
|
||||
|
||||
implementation 'com.github.jaiselrahman:FilePicker:1.2.0'
|
||||
|
||||
|
||||
// DI
|
||||
implementation "org.koin:koin-android:$koin_version"
|
||||
implementation "org.koin:koin-android-scope:$koin_version"
|
||||
|
@ -0,0 +1,240 @@
|
||||
/*
|
||||
*
|
||||
* * Copyright 2019 New Vector Ltd
|
||||
* *
|
||||
* * Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* * you may not use this file except in compliance with the License.
|
||||
* * You may obtain a copy of the License at
|
||||
* *
|
||||
* * http://www.apache.org/licenses/LICENSE-2.0
|
||||
* *
|
||||
* * Unless required by applicable law or agreed to in writing, software
|
||||
* * distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* * See the License for the specific language governing permissions and
|
||||
* * limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package im.vector.riotredesign.core.utils
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.provider.Browser
|
||||
import android.provider.MediaStore
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.fragment.app.Fragment
|
||||
import im.vector.riotredesign.BuildConfig
|
||||
import im.vector.riotredesign.R
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Open a url in the internet browser of the system
|
||||
*/
|
||||
fun openUrlInExternalBrowser(context: Context, url: String?) {
|
||||
url?.let {
|
||||
openUrlInExternalBrowser(context, Uri.parse(it))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Open a uri in the internet browser of the system
|
||||
*/
|
||||
fun openUrlInExternalBrowser(context: Context, uri: Uri?) {
|
||||
uri?.let {
|
||||
val browserIntent = Intent(Intent.ACTION_VIEW, it).apply {
|
||||
putExtra(Browser.EXTRA_APPLICATION_ID, context.packageName)
|
||||
}
|
||||
|
||||
try {
|
||||
context.startActivity(browserIntent)
|
||||
} catch (activityNotFoundException: ActivityNotFoundException) {
|
||||
context.toast(R.string.error_no_external_application_found)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Open sound recorder external application
|
||||
*/
|
||||
fun openSoundRecorder(activity: Activity, requestCode: Int) {
|
||||
val recordSoundIntent = Intent(MediaStore.Audio.Media.RECORD_SOUND_ACTION)
|
||||
|
||||
// Create chooser
|
||||
val chooserIntent = Intent.createChooser(recordSoundIntent, activity.getString(R.string.go_on_with))
|
||||
|
||||
try {
|
||||
activity.startActivityForResult(chooserIntent, requestCode)
|
||||
} catch (activityNotFoundException: ActivityNotFoundException) {
|
||||
activity.toast(R.string.error_no_external_application_found)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Open file selection activity
|
||||
*/
|
||||
fun openFileSelection(activity: Activity,
|
||||
fragment: Fragment?,
|
||||
allowMultipleSelection: Boolean,
|
||||
requestCode: Int) {
|
||||
val fileIntent = Intent(Intent.ACTION_GET_CONTENT)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
|
||||
fileIntent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, allowMultipleSelection)
|
||||
}
|
||||
|
||||
fileIntent.addCategory(Intent.CATEGORY_OPENABLE)
|
||||
fileIntent.type = "*/*"
|
||||
|
||||
try {
|
||||
fragment
|
||||
?.startActivityForResult(fileIntent, requestCode)
|
||||
?: run {
|
||||
activity.startActivityForResult(fileIntent, requestCode)
|
||||
}
|
||||
} catch (activityNotFoundException: ActivityNotFoundException) {
|
||||
activity.toast(R.string.error_no_external_application_found)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Open external video recorder
|
||||
*/
|
||||
fun openVideoRecorder(activity: Activity, requestCode: Int) {
|
||||
val captureIntent = Intent(MediaStore.ACTION_VIDEO_CAPTURE)
|
||||
|
||||
// lowest quality
|
||||
captureIntent.putExtra(MediaStore.EXTRA_VIDEO_QUALITY, 0)
|
||||
|
||||
try {
|
||||
activity.startActivityForResult(captureIntent, requestCode)
|
||||
} catch (activityNotFoundException: ActivityNotFoundException) {
|
||||
activity.toast(R.string.error_no_external_application_found)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Open external camera
|
||||
* @return the latest taken picture camera uri
|
||||
*/
|
||||
fun openCamera(activity: Activity, titlePrefix: String, requestCode: Int): String? {
|
||||
val captureIntent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
|
||||
|
||||
// the following is a fix for buggy 2.x devices
|
||||
val date = Date()
|
||||
val formatter = SimpleDateFormat("yyyyMMddHHmmss", Locale.US)
|
||||
val values = ContentValues()
|
||||
values.put(MediaStore.Images.Media.TITLE, titlePrefix + formatter.format(date))
|
||||
// The Galaxy S not only requires the name of the file to output the image to, but will also not
|
||||
// set the mime type of the picture it just took (!!!). We assume that the Galaxy S takes image/jpegs
|
||||
// so the attachment uploader doesn't freak out about there being no mimetype in the content database.
|
||||
values.put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg")
|
||||
var dummyUri: Uri? = null
|
||||
try {
|
||||
dummyUri = activity.contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values)
|
||||
|
||||
if (null == dummyUri) {
|
||||
Timber.e("Cannot use the external storage media to save image")
|
||||
}
|
||||
} catch (uoe: UnsupportedOperationException) {
|
||||
Timber.e(uoe, "Unable to insert camera URI into MediaStore.Images.Media.EXTERNAL_CONTENT_URI " +
|
||||
"no SD card? Attempting to insert into device storage.")
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "Unable to insert camera URI into MediaStore.Images.Media.EXTERNAL_CONTENT_URI. $e")
|
||||
}
|
||||
|
||||
if (null == dummyUri) {
|
||||
try {
|
||||
dummyUri = activity.contentResolver.insert(MediaStore.Images.Media.INTERNAL_CONTENT_URI, values)
|
||||
if (null == dummyUri) {
|
||||
Timber.e("Cannot use the internal storage to save media to save image")
|
||||
}
|
||||
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "Unable to insert camera URI into internal storage. Giving up. $e")
|
||||
}
|
||||
}
|
||||
|
||||
if (dummyUri != null) {
|
||||
captureIntent.putExtra(MediaStore.EXTRA_OUTPUT, dummyUri)
|
||||
Timber.d("trying to take a photo on " + dummyUri.toString())
|
||||
} else {
|
||||
Timber.d("trying to take a photo with no predefined uri")
|
||||
}
|
||||
|
||||
// Store the dummy URI which will be set to a placeholder location. When all is lost on Samsung devices,
|
||||
// this will point to the data we're looking for.
|
||||
// Because Activities tend to use a single MediaProvider for all their intents, this field will only be the
|
||||
// *latest* TAKE_PICTURE Uri. This is deemed acceptable as the normal flow is to create the intent then immediately
|
||||
// fire it, meaning onActivityResult/getUri will be the next thing called, not another createIntentFor.
|
||||
val result = if (dummyUri == null) null else dummyUri.toString()
|
||||
|
||||
try {
|
||||
activity.startActivityForResult(captureIntent, requestCode)
|
||||
|
||||
return result
|
||||
} catch (activityNotFoundException: ActivityNotFoundException) {
|
||||
activity.toast(R.string.error_no_external_application_found)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Send an email to address with optional subject and message
|
||||
*/
|
||||
fun sendMailTo(address: String, subject: String? = null, message: String? = null, activity: Activity) {
|
||||
val intent = Intent(Intent.ACTION_SENDTO, Uri.fromParts(
|
||||
"mailto", address, null))
|
||||
intent.putExtra(Intent.EXTRA_SUBJECT, subject)
|
||||
intent.putExtra(Intent.EXTRA_TEXT, message)
|
||||
|
||||
try {
|
||||
activity.startActivity(intent)
|
||||
} catch (activityNotFoundException: ActivityNotFoundException) {
|
||||
activity.toast(R.string.error_no_external_application_found)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Open an arbitrary uri
|
||||
*/
|
||||
fun openUri(activity: Activity, uri: String) {
|
||||
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(uri))
|
||||
|
||||
try {
|
||||
activity.startActivity(intent)
|
||||
} catch (activityNotFoundException: ActivityNotFoundException) {
|
||||
activity.toast(R.string.error_no_external_application_found)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send media to a third party application.
|
||||
*
|
||||
* @param activity the activity
|
||||
* @param savedMediaPath the media path
|
||||
* @param mimeType the media mime type.
|
||||
*/
|
||||
fun openMedia(activity: Activity, savedMediaPath: String, mimeType: String) {
|
||||
val file = File(savedMediaPath)
|
||||
val uri = FileProvider.getUriForFile(activity, BuildConfig.APPLICATION_ID + ".fileProvider", file)
|
||||
|
||||
val intent = Intent(Intent.ACTION_VIEW).apply {
|
||||
setDataAndType(uri, mimeType)
|
||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
}
|
||||
|
||||
try {
|
||||
activity.startActivity(intent)
|
||||
} catch (activityNotFoundException: ActivityNotFoundException) {
|
||||
activity.toast(R.string.error_no_external_application_found)
|
||||
}
|
||||
}
|
@ -16,12 +16,14 @@
|
||||
|
||||
package im.vector.riotredesign.features.home.room.detail
|
||||
|
||||
import com.jaiselrahman.filepicker.model.MediaFile
|
||||
import im.vector.matrix.android.api.session.room.timeline.Timeline
|
||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||
|
||||
sealed class RoomDetailActions {
|
||||
|
||||
data class SendMessage(val text: String) : RoomDetailActions()
|
||||
data class SendMedia(val mediaFiles: List<MediaFile>) : RoomDetailActions()
|
||||
object IsDisplayed : RoomDetailActions()
|
||||
data class EventDisplayed(val event: TimelineEvent) : RoomDetailActions()
|
||||
data class LoadMore(val direction: Timeline.Direction) : RoomDetailActions()
|
||||
|
@ -16,6 +16,8 @@
|
||||
|
||||
package im.vector.riotredesign.features.home.room.detail
|
||||
|
||||
import android.app.Activity.RESULT_OK
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import android.view.LayoutInflater
|
||||
@ -26,6 +28,9 @@ import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.airbnb.epoxy.EpoxyVisibilityTracker
|
||||
import com.airbnb.mvrx.fragmentViewModel
|
||||
import com.jaiselrahman.filepicker.activity.FilePickerActivity
|
||||
import com.jaiselrahman.filepicker.config.Configurations
|
||||
import com.jaiselrahman.filepicker.model.MediaFile
|
||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||
import im.vector.riotredesign.R
|
||||
import im.vector.riotredesign.core.dialogs.DialogListItem
|
||||
@ -33,11 +38,7 @@ import im.vector.riotredesign.core.dialogs.DialogSendItemAdapter
|
||||
import im.vector.riotredesign.core.epoxy.LayoutManagerStateRestorer
|
||||
import im.vector.riotredesign.core.platform.RiotFragment
|
||||
import im.vector.riotredesign.core.platform.ToolbarConfigurable
|
||||
import im.vector.riotredesign.core.utils.PERMISSIONS_FOR_TAKING_PHOTO
|
||||
import im.vector.riotredesign.core.utils.PERMISSION_REQUEST_CODE_LAUNCH_CAMERA
|
||||
import im.vector.riotredesign.core.utils.PERMISSION_REQUEST_CODE_LAUNCH_NATIVE_CAMERA
|
||||
import im.vector.riotredesign.core.utils.PERMISSION_REQUEST_CODE_LAUNCH_NATIVE_VIDEO_CAMERA
|
||||
import im.vector.riotredesign.core.utils.checkPermissions
|
||||
import im.vector.riotredesign.core.utils.*
|
||||
import im.vector.riotredesign.features.home.AvatarRenderer
|
||||
import im.vector.riotredesign.features.home.HomeModule
|
||||
import im.vector.riotredesign.features.home.HomePermalinkHandler
|
||||
@ -51,6 +52,7 @@ import org.koin.android.ext.android.inject
|
||||
import org.koin.android.scope.ext.android.bindScope
|
||||
import org.koin.android.scope.ext.android.getOrCreateScope
|
||||
import org.koin.core.parameter.parametersOf
|
||||
import timber.log.Timber
|
||||
|
||||
|
||||
@Parcelize
|
||||
@ -60,6 +62,10 @@ data class RoomDetailArgs(
|
||||
) : Parcelable
|
||||
|
||||
|
||||
private const val CAMERA_VALUE_TITLE = "attachment"
|
||||
private const val REQUEST_FILES_REQUEST_CODE = 0
|
||||
private const val TAKE_IMAGE_REQUEST_CODE = 1
|
||||
|
||||
class RoomDetailFragment : RiotFragment(), TimelineEventController.Callback {
|
||||
|
||||
companion object {
|
||||
@ -91,6 +97,16 @@ class RoomDetailFragment : RiotFragment(), TimelineEventController.Callback {
|
||||
roomDetailViewModel.subscribe { renderState(it) }
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
if (resultCode == RESULT_OK && data != null) {
|
||||
when (requestCode) {
|
||||
REQUEST_FILES_REQUEST_CODE, TAKE_IMAGE_REQUEST_CODE -> handleMediaIntent(data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
roomDetailViewModel.process(RoomDetailActions.IsDisplayed)
|
||||
@ -150,7 +166,7 @@ class RoomDetailFragment : RiotFragment(), TimelineEventController.Callback {
|
||||
*/
|
||||
|
||||
// Send sticker
|
||||
items.add(DialogListItem.SendSticker)
|
||||
//items.add(DialogListItem.SendSticker)
|
||||
// Camera
|
||||
|
||||
//if (PreferencesManager.useNativeCamera(this)) {
|
||||
@ -161,18 +177,24 @@ class RoomDetailFragment : RiotFragment(), TimelineEventController.Callback {
|
||||
// }
|
||||
val adapter = DialogSendItemAdapter(requireContext(), items)
|
||||
AlertDialog.Builder(requireContext())
|
||||
.setAdapter(adapter, { dialog, which ->
|
||||
onSendChoiceClicked(items[which])
|
||||
})
|
||||
.setAdapter(adapter) { _, position ->
|
||||
onSendChoiceClicked(items[position])
|
||||
}
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun onSendChoiceClicked(dialogListItem: DialogListItem) {
|
||||
Timber.v("On send choice clicked: $dialogListItem")
|
||||
when (dialogListItem) {
|
||||
is DialogListItem.SendFile -> {
|
||||
//launchFileSelectionIntent()
|
||||
val intent = Intent(requireContext(), FilePickerActivity::class.java)
|
||||
intent.putExtra(FilePickerActivity.CONFIGS, Configurations.Builder()
|
||||
.setCheckPermission(true)
|
||||
.setSkipZeroSizeFiles(true)
|
||||
.build())
|
||||
startActivityForResult(intent, REQUEST_FILES_REQUEST_CODE)
|
||||
}
|
||||
is DialogListItem.SendVoice -> {
|
||||
//launchAudioRecorderIntent()
|
||||
@ -184,7 +206,7 @@ class RoomDetailFragment : RiotFragment(), TimelineEventController.Callback {
|
||||
// launchCamera()
|
||||
}
|
||||
is DialogListItem.TakePhoto -> if (checkPermissions(PERMISSIONS_FOR_TAKING_PHOTO, requireActivity(), PERMISSION_REQUEST_CODE_LAUNCH_NATIVE_CAMERA)) {
|
||||
// launchNativeCamera()
|
||||
openCamera(requireActivity(), CAMERA_VALUE_TITLE, TAKE_IMAGE_REQUEST_CODE)
|
||||
}
|
||||
is DialogListItem.TakeVideo -> if (checkPermissions(PERMISSIONS_FOR_TAKING_PHOTO, requireActivity(), PERMISSION_REQUEST_CODE_LAUNCH_NATIVE_VIDEO_CAMERA)) {
|
||||
// launchNativeVideoRecorder()
|
||||
@ -192,6 +214,11 @@ class RoomDetailFragment : RiotFragment(), TimelineEventController.Callback {
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleMediaIntent(data: Intent) {
|
||||
val files: ArrayList<MediaFile> = data.getParcelableArrayListExtra(FilePickerActivity.MEDIA_FILES)
|
||||
roomDetailViewModel.process(RoomDetailActions.SendMedia(files))
|
||||
}
|
||||
|
||||
private fun renderState(state: RoomDetailViewState) {
|
||||
renderRoomSummary(state)
|
||||
timelineEventController.setTimeline(state.timeline)
|
||||
|
@ -22,6 +22,7 @@ import com.jakewharton.rxrelay2.BehaviorRelay
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.session.Session
|
||||
import im.vector.matrix.android.api.session.events.model.Event
|
||||
import im.vector.matrix.android.internal.session.room.media.MediaAttachment
|
||||
import im.vector.matrix.rx.rx
|
||||
import im.vector.riotredesign.core.platform.RiotViewModel
|
||||
import im.vector.riotredesign.features.home.room.VisibleRoomStore
|
||||
@ -64,6 +65,7 @@ class RoomDetailViewModel(initialState: RoomDetailViewState,
|
||||
fun process(action: RoomDetailActions) {
|
||||
when (action) {
|
||||
is RoomDetailActions.SendMessage -> handleSendMessage(action)
|
||||
is RoomDetailActions.SendMedia -> handleSendMedia(action)
|
||||
is RoomDetailActions.IsDisplayed -> handleIsDisplayed()
|
||||
is RoomDetailActions.EventDisplayed -> handleEventDisplayed(action)
|
||||
is RoomDetailActions.LoadMore -> handleLoadMore(action)
|
||||
@ -76,6 +78,25 @@ class RoomDetailViewModel(initialState: RoomDetailViewState,
|
||||
room.sendTextMessage(action.text, callback = object : MatrixCallback<Event> {})
|
||||
}
|
||||
|
||||
private fun handleSendMedia(action: RoomDetailActions.SendMedia) {
|
||||
val attachment = action.mediaFiles.firstOrNull()
|
||||
?.let {
|
||||
MediaAttachment(
|
||||
it.size,
|
||||
it.duration,
|
||||
it.date,
|
||||
it.height,
|
||||
it.width,
|
||||
it.name,
|
||||
it.thumbnail,
|
||||
it.path,
|
||||
it.mimeType
|
||||
)
|
||||
}
|
||||
?: return
|
||||
room.sendMedia(attachment, callback = object : MatrixCallback<Event> {})
|
||||
}
|
||||
|
||||
private fun handleEventDisplayed(action: RoomDetailActions.EventDisplayed) {
|
||||
displayedEventsObservable.accept(action)
|
||||
}
|
||||
|
@ -53,7 +53,9 @@ object MediaContentRenderer {
|
||||
val resolvedUrl = when (mode) {
|
||||
Mode.FULL_SIZE -> contentUrlResolver.resolveFullSize(data.url)
|
||||
Mode.THUMBNAIL -> contentUrlResolver.resolveThumbnail(data.url, width, height, ContentUrlResolver.ThumbnailMethod.SCALE)
|
||||
} ?: return
|
||||
}
|
||||
//Fallback to base url
|
||||
?: data.url
|
||||
|
||||
GlideApp
|
||||
.with(imageView)
|
||||
@ -68,8 +70,8 @@ object MediaContentRenderer {
|
||||
val fullSize = contentUrlResolver.resolveFullSize(data.url)
|
||||
val thumbnail = contentUrlResolver.resolveThumbnail(data.url, width, height, ContentUrlResolver.ThumbnailMethod.SCALE)
|
||||
imageView.showImage(
|
||||
Uri.parse(thumbnail),
|
||||
Uri.parse(fullSize)
|
||||
Uri.parse(thumbnail ?: data.url),
|
||||
Uri.parse(fullSize ?: data.url)
|
||||
)
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user