From 06ba478232b4b23c75685c915f6e915fc1c672a7 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 13 Feb 2020 17:00:16 +0100 Subject: [PATCH] Send files to several rooms at a time --- .../api/session/room/send/SendService.kt | 11 +- .../internal/di/WorkManagerProvider.kt | 8 +- .../internal/session/SessionComponent.kt | 21 ++-- .../session/content/UploadContentWorker.kt | 72 +++++++++--- .../room/relation/DefaultRelationService.kt | 4 +- .../session/room/send/DefaultSendService.kt | 103 ++++++++++++------ .../session/room/send/EncryptEventWorker.kt | 12 +- .../MultipleEventSendingDispatcherWorker.kt | 103 ++++++++++++++++++ .../session/room/send/SendEventWorker.kt | 22 ++-- .../home/room/detail/RoomDetailViewModel.kt | 4 +- .../features/share/IncomingShareViewModel.kt | 16 +-- 11 files changed, 284 insertions(+), 92 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/MultipleEventSendingDispatcherWorker.kt diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/SendService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/SendService.kt index b61f162359..5f32458dd2 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/SendService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/SendService.kt @@ -52,20 +52,27 @@ interface SendService { * Method to send a media asynchronously. * @param attachment the media to send * @param compressBeforeSending set to true to compress media before sending them + * @param roomIds set of roomIds to where the media will be sent. The current roomId will be add to this set if not present. + * It can be useful to send media to multiple room. It's safe to include the current roomId in this set * @return a [Cancelable] */ fun sendMedia(attachment: ContentAttachmentData, // TODO Change to a Compression Level Enum - compressBeforeSending: Boolean): Cancelable + compressBeforeSending: Boolean, + roomIds: Set): Cancelable /** * Method to send a list of media asynchronously. * @param attachments the list of media to send + * @param compressBeforeSending set to true to compress media before sending them + * @param roomIds set of roomIds to where the media will be sent. The current roomId will be add to this set if not present. + * It can be useful to send media to multiple room. It's safe to include the current roomId in this set * @return a [Cancelable] */ fun sendMedias(attachments: List, // TODO Change to a Compression Level Enum - compressBeforeSending: Boolean): Cancelable + compressBeforeSending: Boolean, + roomIds: Set): Cancelable /** * Send a poll to the room. diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/WorkManagerProvider.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/WorkManagerProvider.kt index 82091be697..5a0202719b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/WorkManagerProvider.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/WorkManagerProvider.kt @@ -17,7 +17,11 @@ package im.vector.matrix.android.internal.di import android.content.Context -import androidx.work.* +import androidx.work.Constraints +import androidx.work.ListenableWorker +import androidx.work.NetworkType +import androidx.work.OneTimeWorkRequestBuilder +import androidx.work.WorkManager import javax.inject.Inject internal class WorkManagerProvider @Inject constructor( @@ -54,5 +58,7 @@ internal class WorkManagerProvider @Inject constructor( val workConstraints = Constraints.Builder() .setRequiredNetworkType(NetworkType.CONNECTED) .build() + + const val BACKOFF_DELAY = 10_000L } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionComponent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionComponent.kt index 4a0e4424d0..1b07377fa1 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionComponent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionComponent.kt @@ -38,6 +38,7 @@ import im.vector.matrix.android.internal.session.pushers.PushersModule import im.vector.matrix.android.internal.session.room.RoomModule import im.vector.matrix.android.internal.session.room.relation.SendRelationWorker import im.vector.matrix.android.internal.session.room.send.EncryptEventWorker +import im.vector.matrix.android.internal.session.room.send.MultipleEventSendingDispatcherWorker import im.vector.matrix.android.internal.session.room.send.RedactEventWorker import im.vector.matrix.android.internal.session.room.send.SendEventWorker import im.vector.matrix.android.internal.session.signout.SignOutModule @@ -85,23 +86,25 @@ internal interface SessionComponent { fun taskExecutor(): TaskExecutor - fun inject(sendEventWorker: SendEventWorker) + fun inject(worker: SendEventWorker) - fun inject(sendEventWorker: SendRelationWorker) + fun inject(worker: SendRelationWorker) - fun inject(encryptEventWorker: EncryptEventWorker) + fun inject(worker: EncryptEventWorker) - fun inject(redactEventWorker: RedactEventWorker) + fun inject(worker: MultipleEventSendingDispatcherWorker) - fun inject(getGroupDataWorker: GetGroupDataWorker) + fun inject(worker: RedactEventWorker) - fun inject(uploadContentWorker: UploadContentWorker) + fun inject(worker: GetGroupDataWorker) - fun inject(syncWorker: SyncWorker) + fun inject(worker: UploadContentWorker) - fun inject(addHttpPusherWorker: AddHttpPusherWorker) + fun inject(worker: SyncWorker) - fun inject(sendVerificationMessageWorker: SendVerificationMessageWorker) + fun inject(worker: AddHttpPusherWorker) + + fun inject(worker: SendVerificationMessageWorker) @Component.Factory interface Factory { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/UploadContentWorker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/UploadContentWorker.kt index a7fa68f96f..334e1f5e5b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/UploadContentWorker.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/UploadContentWorker.kt @@ -24,11 +24,15 @@ import im.vector.matrix.android.api.session.content.ContentAttachmentData import im.vector.matrix.android.api.session.events.model.Event 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.* +import im.vector.matrix.android.api.session.room.model.message.MessageAudioContent +import im.vector.matrix.android.api.session.room.model.message.MessageContent +import im.vector.matrix.android.api.session.room.model.message.MessageFileContent +import im.vector.matrix.android.api.session.room.model.message.MessageImageContent +import im.vector.matrix.android.api.session.room.model.message.MessageVideoContent import im.vector.matrix.android.internal.crypto.attachments.MXEncryptedAttachments import im.vector.matrix.android.internal.crypto.model.rest.EncryptedFileInfo import im.vector.matrix.android.internal.network.ProgressRequestBody -import im.vector.matrix.android.internal.session.room.send.SendEventWorker +import im.vector.matrix.android.internal.session.room.send.MultipleEventSendingDispatcherWorker import im.vector.matrix.android.internal.worker.SessionWorkerParams import im.vector.matrix.android.internal.worker.WorkerParamsFactory import im.vector.matrix.android.internal.worker.getSessionComponent @@ -43,8 +47,7 @@ internal class UploadContentWorker(context: Context, params: WorkerParameters) : @JsonClass(generateAdapter = true) internal data class Params( override val sessionId: String, - val roomId: String, - val event: Event, + val events: List, val attachment: ContentAttachmentData, val isRoomEncrypted: Boolean, val compressBeforeSending: Boolean, @@ -68,14 +71,17 @@ internal class UploadContentWorker(context: Context, params: WorkerParameters) : val sessionComponent = getSessionComponent(params.sessionId) ?: return Result.success() sessionComponent.inject(this) - val eventId = params.event.eventId ?: return Result.success() val attachment = params.attachment val attachmentFile = try { File(attachment.path) } catch (e: Exception) { Timber.e(e) - contentUploadStateTracker.setFailure(params.event.eventId, e) + params.events + .mapNotNull { it.eventId } + .forEach { + contentUploadStateTracker.setFailure(it, e) + } return Result.success( WorkerParamsFactory.toData(params.copy( lastFailureMessage = e.localizedMessage @@ -91,14 +97,22 @@ internal class UploadContentWorker(context: Context, params: WorkerParameters) : ThumbnailExtractor.extractThumbnail(params.attachment)?.let { thumbnailData -> val thumbnailProgressListener = object : ProgressRequestBody.Listener { override fun onProgress(current: Long, total: Long) { - contentUploadStateTracker.setProgressThumbnail(eventId, current, total) + params.events + .mapNotNull { it.eventId } + .forEach { + contentUploadStateTracker.setProgressThumbnail(it, current, total) + } } } try { val contentUploadResponse = if (params.isRoomEncrypted) { Timber.v("Encrypt thumbnail") - contentUploadStateTracker.setEncryptingThumbnail(eventId) + params.events + .mapNotNull { it.eventId } + .forEach { + contentUploadStateTracker.setEncryptingThumbnail(it) + } val encryptionResult = MXEncryptedAttachments.encryptAttachment(ByteArrayInputStream(thumbnailData.bytes), thumbnailData.mimeType) uploadedThumbnailEncryptedFileInfo = encryptionResult.encryptedFileInfo fileUploader.uploadByteArray(encryptionResult.encryptedByteArray, @@ -121,11 +135,15 @@ internal class UploadContentWorker(context: Context, params: WorkerParameters) : val progressListener = object : ProgressRequestBody.Listener { override fun onProgress(current: Long, total: Long) { - if (isStopped) { - contentUploadStateTracker.setFailure(eventId, Throwable("Cancelled")) - } else { - contentUploadStateTracker.setProgress(eventId, current, total) - } + params.events + .mapNotNull { it.eventId } + .forEach { + if (isStopped) { + contentUploadStateTracker.setFailure(it, Throwable("Cancelled")) + } else { + contentUploadStateTracker.setProgress(it, current, total) + } + } } } @@ -134,7 +152,11 @@ internal class UploadContentWorker(context: Context, params: WorkerParameters) : return try { val contentUploadResponse = if (params.isRoomEncrypted) { Timber.v("Encrypt file") - contentUploadStateTracker.setEncrypting(eventId) + params.events + .mapNotNull { it.eventId } + .forEach { + contentUploadStateTracker.setEncrypting(it) + } val encryptionResult = MXEncryptedAttachments.encryptAttachment(FileInputStream(attachmentFile), attachment.mimeType) uploadedFileEncryptedFileInfo = encryptionResult.encryptedFileInfo @@ -154,7 +176,12 @@ internal class UploadContentWorker(context: Context, params: WorkerParameters) : } private fun handleFailure(params: Params, failure: Throwable): Result { - contentUploadStateTracker.setFailure(params.event.eventId!!, failure) + params.events + .mapNotNull { it.eventId } + .forEach { + contentUploadStateTracker.setFailure(it, failure) + } + return Result.success( WorkerParamsFactory.toData( params.copy( @@ -170,9 +197,18 @@ internal class UploadContentWorker(context: Context, params: WorkerParameters) : thumbnailUrl: String?, thumbnailEncryptedFileInfo: EncryptedFileInfo?): Result { Timber.v("handleSuccess $attachmentUrl, work is stopped $isStopped") - contentUploadStateTracker.setSuccess(params.event.eventId!!) - val event = updateEvent(params.event, attachmentUrl, encryptedFileInfo, thumbnailUrl, thumbnailEncryptedFileInfo) - val sendParams = SendEventWorker.Params(params.sessionId, params.roomId, event) + params.events + .mapNotNull { it.eventId } + .forEach { + contentUploadStateTracker.setSuccess(it) + } + + val updatedEvents = params.events + .map { + updateEvent(it, attachmentUrl, encryptedFileInfo, thumbnailUrl, thumbnailEncryptedFileInfo) + } + + val sendParams = MultipleEventSendingDispatcherWorker.Params(params.sessionId, updatedEvents, params.isRoomEncrypted) return Result.success(WorkerParamsFactory.toData(sendParams)) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/DefaultRelationService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/DefaultRelationService.kt index 0310020b5a..1f199322af 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/DefaultRelationService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/DefaultRelationService.kt @@ -196,13 +196,13 @@ internal class DefaultRelationService @AssistedInject constructor( private fun createEncryptEventWork(event: Event, keepKeys: List?): OneTimeWorkRequest { // Same parameter - val params = EncryptEventWorker.Params(sessionId, roomId, event, keepKeys) + val params = EncryptEventWorker.Params(sessionId, event, keepKeys) val sendWorkData = WorkerParamsFactory.toData(params) return timeLineSendEventWorkCommon.createWork(sendWorkData, true) } private fun createSendEventWork(event: Event, startChain: Boolean): OneTimeWorkRequest { - val sendContentWorkerParams = SendEventWorker.Params(sessionId, roomId, event) + val sendContentWorkerParams = SendEventWorker.Params(sessionId, event) val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams) return timeLineSendEventWorkCommon.createWork(sendWorkData, startChain) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt index da8ac7f2f7..05dc3098f1 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt @@ -22,7 +22,6 @@ import androidx.work.OneTimeWorkRequest import androidx.work.Operation import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject -import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.session.content.ContentAttachmentData import im.vector.matrix.android.api.session.crypto.CryptoService import im.vector.matrix.android.api.session.events.model.Event @@ -49,7 +48,6 @@ import java.util.concurrent.Executors import java.util.concurrent.TimeUnit private const val UPLOAD_WORK = "UPLOAD_WORK" -private const val BACKOFF_DELAY = 10_000L internal class DefaultSendService @AssistedInject constructor( @Assisted private val roomId: String, @@ -58,7 +56,6 @@ internal class DefaultSendService @AssistedInject constructor( @SessionId private val sessionId: String, private val localEchoEventFactory: LocalEchoEventFactory, private val cryptoService: CryptoService, - private val monarchy: Monarchy, private val taskExecutor: TaskExecutor, private val localEchoRepository: LocalEchoRepository ) : SendService { @@ -103,6 +100,7 @@ internal class DefaultSendService @AssistedInject constructor( return if (cryptoService.isRoomEncrypted(roomId)) { Timber.v("Send event in encrypted room") val encryptWork = createEncryptEventWork(event, true) + // Note that event will be replaced by the result of the previous work val sendWork = createSendEventWork(event, false) timelineSendEventWorkCommon.postSequentialWorks(roomId, encryptWork, sendWork) } else { @@ -111,9 +109,11 @@ internal class DefaultSendService @AssistedInject constructor( } } - override fun sendMedias(attachments: List, compressBeforeSending: Boolean): Cancelable { + override fun sendMedias(attachments: List, + compressBeforeSending: Boolean, + roomIds: Set): Cancelable { return attachments.mapTo(CancelableBag()) { - sendMedia(it, compressBeforeSending) + sendMedia(it, compressBeforeSending, roomIds) } } @@ -201,43 +201,66 @@ internal class DefaultSendService @AssistedInject constructor( } } - override fun sendMedia(attachment: ContentAttachmentData, compressBeforeSending: Boolean): Cancelable { + override fun sendMedia(attachment: ContentAttachmentData, + compressBeforeSending: Boolean, + roomIds: Set): Cancelable { // Create an event with the media file path - val event = localEchoEventFactory.createMediaEvent(roomId, attachment).also { - createLocalEcho(it) + // Ensure current roomId is included in the set + val allRoomIds = (roomIds + roomId).toList() + + // Create local echo for each room + val allLocalEchoes = allRoomIds.map { + localEchoEventFactory.createMediaEvent(it, attachment).also { event -> + createLocalEcho(event) + } } - return internalSendMedia(event, attachment, compressBeforeSending) + return internalSendMedia(allLocalEchoes, attachment, compressBeforeSending) } - private fun internalSendMedia(localEcho: Event, attachment: ContentAttachmentData, compressBeforeSending: Boolean): Cancelable { - val isRoomEncrypted = cryptoService.isRoomEncrypted(roomId) + /** + * We use the roomId of the local echo event + */ + private fun internalSendMedia(allLocalEchoes: List, attachment: ContentAttachmentData, compressBeforeSending: Boolean): Cancelable { + val splitLocalEchoes = allLocalEchoes.groupBy { cryptoService.isRoomEncrypted(it.roomId!!) } - val uploadWork = createUploadMediaWork(localEcho, attachment, isRoomEncrypted, compressBeforeSending, startChain = true) - val sendWork = createSendEventWork(localEcho, false) + val encryptedLocalEchoes = splitLocalEchoes[true].orEmpty() + val clearLocalEchoes = splitLocalEchoes[false].orEmpty() - if (isRoomEncrypted) { - val encryptWork = createEncryptEventWork(localEcho, false /*not start of chain, take input error*/) + val cancelableBag = CancelableBag() - val op: Operation = workManagerProvider.workManager + if (encryptedLocalEchoes.isNotEmpty()) { + val uploadWork = createUploadMediaWork(encryptedLocalEchoes, attachment, true, compressBeforeSending, startChain = true) + + val dispatcherWork = createMultipleEventDispatcherWork(true) + + val operation = workManagerProvider.workManager .beginUniqueWork(buildWorkName(UPLOAD_WORK), ExistingWorkPolicy.APPEND, uploadWork) - .then(encryptWork) - .then(sendWork) + .then(dispatcherWork) .enqueue() - op.result.addListener(Runnable { - if (op.result.isCancelled) { + operation.result.addListener(Runnable { + if (operation.result.isCancelled) { Timber.e("CHAIN WAS CANCELLED") - } else if (op.state.value is Operation.State.FAILURE) { + } else if (operation.state.value is Operation.State.FAILURE) { Timber.e("CHAIN DID FAIL") } }, workerFutureListenerExecutor) - } else { - workManagerProvider.workManager - .beginUniqueWork(buildWorkName(UPLOAD_WORK), ExistingWorkPolicy.APPEND, uploadWork) - .then(sendWork) - .enqueue() + + cancelableBag.add(CancelableWork(workManagerProvider.workManager, dispatcherWork.id)) } - return CancelableWork(workManagerProvider.workManager, sendWork.id) + if (clearLocalEchoes.isNotEmpty()) { + val uploadWork = createUploadMediaWork(clearLocalEchoes, attachment, false, compressBeforeSending, startChain = true) + val dispatcherWork = createMultipleEventDispatcherWork(false) + + workManagerProvider.workManager + .beginUniqueWork(buildWorkName(UPLOAD_WORK), ExistingWorkPolicy.APPEND, uploadWork) + .then(dispatcherWork) + .enqueue() + + cancelableBag.add(CancelableWork(workManagerProvider.workManager, dispatcherWork.id)) + } + + return cancelableBag } private fun createLocalEcho(event: Event) { @@ -250,19 +273,19 @@ internal class DefaultSendService @AssistedInject constructor( private fun createEncryptEventWork(event: Event, startChain: Boolean): OneTimeWorkRequest { // Same parameter - val params = EncryptEventWorker.Params(sessionId, roomId, event) + val params = EncryptEventWorker.Params(sessionId, event) val sendWorkData = WorkerParamsFactory.toData(params) return workManagerProvider.matrixOneTimeWorkRequestBuilder() .setConstraints(WorkManagerProvider.workConstraints) .setInputData(sendWorkData) .startChain(startChain) - .setBackoffCriteria(BackoffPolicy.LINEAR, BACKOFF_DELAY, TimeUnit.MILLISECONDS) + .setBackoffCriteria(BackoffPolicy.LINEAR, WorkManagerProvider.BACKOFF_DELAY, TimeUnit.MILLISECONDS) .build() } private fun createSendEventWork(event: Event, startChain: Boolean): OneTimeWorkRequest { - val sendContentWorkerParams = SendEventWorker.Params(sessionId, roomId, event) + val sendContentWorkerParams = SendEventWorker.Params(sessionId, event) val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams) return timelineSendEventWorkCommon.createWork(sendWorkData, startChain) @@ -277,19 +300,33 @@ internal class DefaultSendService @AssistedInject constructor( return timelineSendEventWorkCommon.createWork(redactWorkData, true) } - private fun createUploadMediaWork(event: Event, + private fun createUploadMediaWork(allLocalEchos: List, attachment: ContentAttachmentData, isRoomEncrypted: Boolean, compressBeforeSending: Boolean, startChain: Boolean): OneTimeWorkRequest { - val uploadMediaWorkerParams = UploadContentWorker.Params(sessionId, roomId, event, attachment, isRoomEncrypted, compressBeforeSending) + val uploadMediaWorkerParams = UploadContentWorker.Params(sessionId, allLocalEchos, attachment, isRoomEncrypted, compressBeforeSending) val uploadWorkData = WorkerParamsFactory.toData(uploadMediaWorkerParams) return workManagerProvider.matrixOneTimeWorkRequestBuilder() .setConstraints(WorkManagerProvider.workConstraints) .startChain(startChain) .setInputData(uploadWorkData) - .setBackoffCriteria(BackoffPolicy.LINEAR, BACKOFF_DELAY, TimeUnit.MILLISECONDS) + .setBackoffCriteria(BackoffPolicy.LINEAR, WorkManagerProvider.BACKOFF_DELAY, TimeUnit.MILLISECONDS) + .build() + } + + private fun createMultipleEventDispatcherWork(isRoomEncrypted: Boolean): OneTimeWorkRequest { + // the list of events will be replaced by the result of the media upload work + val params = MultipleEventSendingDispatcherWorker.Params(sessionId, emptyList(), isRoomEncrypted) + val workData = WorkerParamsFactory.toData(params) + + return workManagerProvider.matrixOneTimeWorkRequestBuilder() + // No constraint + // .setConstraints(WorkManagerProvider.workConstraints) + .startChain(false) + .setInputData(workData) + .setBackoffCriteria(BackoffPolicy.LINEAR, WorkManagerProvider.BACKOFF_DELAY, TimeUnit.MILLISECONDS) .build() } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/EncryptEventWorker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/EncryptEventWorker.kt index 21080d9037..72f5ee56b8 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/EncryptEventWorker.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/EncryptEventWorker.kt @@ -20,7 +20,6 @@ import android.content.Context import androidx.work.CoroutineWorker import androidx.work.WorkerParameters import com.squareup.moshi.JsonClass -import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.failure.Failure import im.vector.matrix.android.api.session.crypto.CryptoService import im.vector.matrix.android.api.session.events.model.Event @@ -39,9 +38,8 @@ internal class EncryptEventWorker(context: Context, params: WorkerParameters) @JsonClass(generateAdapter = true) internal data class Params( override val sessionId: String, - val roomId: String, val event: Event, - /**Do not encrypt these keys, keep them as is in encrypted content (e.g. m.relates_to)*/ + /** Do not encrypt these keys, keep them as is in encrypted content (e.g. m.relates_to) */ val keepKeys: List? = null, override val lastFailureMessage: String? = null ) : SessionWorkerParams @@ -53,7 +51,7 @@ internal class EncryptEventWorker(context: Context, params: WorkerParameters) Timber.v("Start Encrypt work") val params = WorkerParamsFactory.fromData(inputData) ?: return Result.success().also { - Timber.v("Work cancelled due to input error from parent") + Timber.e("Work cancelled due to input error from parent") } Timber.v("Start Encrypt work for event ${params.event.eventId}") @@ -80,7 +78,7 @@ internal class EncryptEventWorker(context: Context, params: WorkerParameters) var result: MXEncryptEventContentResult? = null try { result = awaitCallback { - crypto.encryptEventContent(localMutableContent, localEvent.type, params.roomId, it) + crypto.encryptEventContent(localMutableContent, localEvent.type, localEvent.roomId!!, it) } } catch (throwable: Throwable) { error = throwable @@ -98,7 +96,7 @@ internal class EncryptEventWorker(context: Context, params: WorkerParameters) type = safeResult.eventType, content = safeResult.eventContent ) - val nextWorkerParams = SendEventWorker.Params(params.sessionId, params.roomId, encryptedEvent) + val nextWorkerParams = SendEventWorker.Params(params.sessionId, encryptedEvent) return Result.success(WorkerParamsFactory.toData(nextWorkerParams)) } else { val sendState = when (error) { @@ -107,7 +105,7 @@ internal class EncryptEventWorker(context: Context, params: WorkerParameters) } localEchoUpdater.updateSendState(localEvent.eventId, sendState) // always return success, or the chain will be stuck for ever! - val nextWorkerParams = SendEventWorker.Params(params.sessionId, params.roomId, localEvent, error?.localizedMessage + val nextWorkerParams = SendEventWorker.Params(params.sessionId, localEvent, error?.localizedMessage ?: "Error") return Result.success(WorkerParamsFactory.toData(nextWorkerParams)) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/MultipleEventSendingDispatcherWorker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/MultipleEventSendingDispatcherWorker.kt new file mode 100644 index 0000000000..03db817dd6 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/MultipleEventSendingDispatcherWorker.kt @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2020 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.send + +import android.content.Context +import androidx.work.BackoffPolicy +import androidx.work.CoroutineWorker +import androidx.work.OneTimeWorkRequest +import androidx.work.WorkerParameters +import com.squareup.moshi.JsonClass +import im.vector.matrix.android.api.session.events.model.Event +import im.vector.matrix.android.internal.di.WorkManagerProvider +import im.vector.matrix.android.internal.session.room.timeline.TimelineSendEventWorkCommon +import im.vector.matrix.android.internal.worker.SessionWorkerParams +import im.vector.matrix.android.internal.worker.WorkerParamsFactory +import im.vector.matrix.android.internal.worker.getSessionComponent +import im.vector.matrix.android.internal.worker.startChain +import timber.log.Timber +import java.util.concurrent.TimeUnit +import javax.inject.Inject + +/** + * This worker creates a new work for each events passed in parameter + */ +internal class MultipleEventSendingDispatcherWorker(context: Context, params: WorkerParameters) + : CoroutineWorker(context, params) { + + @JsonClass(generateAdapter = true) + internal data class Params( + override val sessionId: String, + val events: List, + val isEncrypted: Boolean, + override val lastFailureMessage: String? = null + ) : SessionWorkerParams + + @Inject lateinit var workManagerProvider: WorkManagerProvider + @Inject lateinit var timelineSendEventWorkCommon: TimelineSendEventWorkCommon + + override suspend fun doWork(): Result { + Timber.v("Start dispatch sending multiple event work") + val params = WorkerParamsFactory.fromData(inputData) + ?: return Result.success().also { + Timber.e("Work cancelled due to input error from parent") + } + + if (params.lastFailureMessage != null) { + // Transmit the error + return Result.success(inputData) + } + + val sessionComponent = getSessionComponent(params.sessionId) ?: return Result.success() + sessionComponent.inject(this) + + // Create a work for every event + params.events.forEach { event -> + if (params.isEncrypted) { + Timber.v("Send event in encrypted room") + val encryptWork = createEncryptEventWork(params.sessionId, event, true) + // Note that event will be replaced by the result of the previous work + val sendWork = createSendEventWork(params.sessionId, event, false) + timelineSendEventWorkCommon.postSequentialWorks(event.roomId!!, encryptWork, sendWork) + } else { + val sendWork = createSendEventWork(params.sessionId, event, true) + timelineSendEventWorkCommon.postWork(event.roomId!!, sendWork) + } + } + + return Result.success() + } + + private fun createEncryptEventWork(sessionId: String, event: Event, startChain: Boolean): OneTimeWorkRequest { + val params = EncryptEventWorker.Params(sessionId, event) + val sendWorkData = WorkerParamsFactory.toData(params) + + return workManagerProvider.matrixOneTimeWorkRequestBuilder() + .setConstraints(WorkManagerProvider.workConstraints) + .setInputData(sendWorkData) + .startChain(startChain) + .setBackoffCriteria(BackoffPolicy.LINEAR, WorkManagerProvider.BACKOFF_DELAY, TimeUnit.MILLISECONDS) + .build() + } + + private fun createSendEventWork(sessionId: String, event: Event, startChain: Boolean): OneTimeWorkRequest { + val sendContentWorkerParams = SendEventWorker.Params(sessionId, event) + val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams) + + return timelineSendEventWorkCommon.createWork(sendWorkData, startChain) + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/SendEventWorker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/SendEventWorker.kt index 3d038a0c82..51c36afd61 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/SendEventWorker.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/SendEventWorker.kt @@ -30,6 +30,7 @@ import im.vector.matrix.android.internal.worker.SessionWorkerParams import im.vector.matrix.android.internal.worker.WorkerParamsFactory import im.vector.matrix.android.internal.worker.getSessionComponent import org.greenrobot.eventbus.EventBus +import timber.log.Timber import javax.inject.Inject internal class SendEventWorker(context: Context, @@ -39,7 +40,6 @@ internal class SendEventWorker(context: Context, @JsonClass(generateAdapter = true) internal data class Params( override val sessionId: String, - val roomId: String, val event: Event, override val lastFailureMessage: String? = null ) : SessionWorkerParams @@ -50,7 +50,9 @@ internal class SendEventWorker(context: Context, override suspend fun doWork(): Result { val params = WorkerParamsFactory.fromData(inputData) - ?: return Result.success() + ?: return Result.success().also { + Timber.e("Work cancelled due to input error from parent") + } val sessionComponent = getSessionComponent(params.sessionId) ?: return Result.success() sessionComponent.inject(this) @@ -66,7 +68,7 @@ internal class SendEventWorker(context: Context, return Result.success(inputData) } return try { - sendEvent(event.eventId, event.type, event.content, params.roomId) + sendEvent(event) Result.success() } catch (exception: Throwable) { if (exception.shouldBeRetried()) { @@ -79,16 +81,16 @@ internal class SendEventWorker(context: Context, } } - private suspend fun sendEvent(eventId: String, eventType: String, content: Content?, roomId: String) { - localEchoUpdater.updateSendState(eventId, SendState.SENDING) + private suspend fun sendEvent(event: Event) { + localEchoUpdater.updateSendState(event.eventId!!, SendState.SENDING) executeRequest(eventBus) { apiCall = roomAPI.send( - eventId, - roomId, - eventType, - content + event.eventId, + event.roomId!!, + event.type, + event.content ) } - localEchoUpdater.updateSendState(eventId, SendState.SENT) + localEchoUpdater.updateSendState(event.eventId, SendState.SENT) } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt index a2cb005858..1a4d3d0783 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt @@ -579,10 +579,10 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro if (maxUploadFileSize == HomeServerCapabilities.MAX_UPLOAD_FILE_SIZE_UNKNOWN) { // Unknown limitation - room.sendMedias(attachments, action.compressBeforeSending) + room.sendMedias(attachments, action.compressBeforeSending, emptySet()) } else { when (val tooBigFile = attachments.find { it.size > maxUploadFileSize }) { - null -> room.sendMedias(attachments, action.compressBeforeSending) + null -> room.sendMedias(attachments, action.compressBeforeSending, emptySet()) else -> _viewEvents.post(RoomDetailViewEvents.FileTooBigError( tooBigFile.name ?: tooBigFile.path, tooBigFile.size, diff --git a/vector/src/main/java/im/vector/riotx/features/share/IncomingShareViewModel.kt b/vector/src/main/java/im/vector/riotx/features/share/IncomingShareViewModel.kt index 2684828dec..eb5b1bda78 100644 --- a/vector/src/main/java/im/vector/riotx/features/share/IncomingShareViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/share/IncomingShareViewModel.kt @@ -135,19 +135,19 @@ class IncomingShareViewModel @AssistedInject constructor(@Assisted initialState: proposeMediaEdition: Boolean, compressMediaBeforeSending: Boolean) { if (!proposeMediaEdition) { - selectedRoomIds.forEach { roomId -> - val room = session.getRoom(roomId) - room?.sendMedias(attachmentData, compressMediaBeforeSending) - } + // Pick the first room to send the media + selectedRoomIds.firstOrNull() + ?.let { roomId -> session.getRoom(roomId) } + ?.sendMedias(attachmentData, compressMediaBeforeSending, selectedRoomIds) } else { val previewable = attachmentData.filterPreviewables() val nonPreviewable = attachmentData.filterNonPreviewables() if (nonPreviewable.isNotEmpty()) { // Send the non previewable attachment right now (?) - selectedRoomIds.forEach { roomId -> - val room = session.getRoom(roomId) - room?.sendMedias(nonPreviewable, compressMediaBeforeSending) - } + // Pick the first room to send the media + selectedRoomIds.firstOrNull() + ?.let { roomId -> session.getRoom(roomId) } + ?.sendMedias(nonPreviewable, compressMediaBeforeSending, selectedRoomIds) } if (previewable.isNotEmpty()) { // In case of multiple share of media, edit them first