diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle index 90bdf02243..5be3330ed8 100644 --- a/matrix-sdk-android/build.gradle +++ b/matrix-sdk-android/build.gradle @@ -115,7 +115,7 @@ dependencies { def coroutines_version = "1.3.8" def markwon_version = '3.1.0' def daggerVersion = '2.25.4' - def work_version = '2.3.3' + def work_version = '2.4.0' def retrofit_version = '2.6.2' implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/CancelSendTracker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/CancelSendTracker.kt new file mode 100644 index 0000000000..fb7145c7c0 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/CancelSendTracker.kt @@ -0,0 +1,61 @@ +/* + * 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 im.vector.matrix.android.internal.session.SessionScope +import javax.inject.Inject + +/** + * We cannot use work manager cancellation mechanism because cancelling a work will just ignore + * any follow up send that was already queued. + * We use this class to track cancel requests, the workers will look for this to check for cancellation request + * and just ignore the work request and continue by returning success. + * + * Known limitation, for now requests are not persisted + */ +@SessionScope +class CancelSendTracker @Inject constructor() { + + data class Request( + val localId: String, + val roomId: String + ) + + private val cancellingRequests = ArrayList() + + fun markLocalEchoForCancel(eventId: String, roomId: String) { + synchronized(cancellingRequests) { + cancellingRequests.add(Request(eventId, roomId)) + } + } + + fun isCancelRequestedFor(eventId: String?, roomId: String?): Boolean { + val index = synchronized(cancellingRequests) { + cancellingRequests.indexOfFirst { it.localId == eventId && it.roomId == roomId } + } + return index != -1 + } + + fun markCancelled(eventId: String, roomId: String) { + synchronized(cancellingRequests) { + val index = cancellingRequests.indexOfFirst { it.localId == eventId && it.roomId == roomId } + if (index != -1) { + cancellingRequests.removeAt(index) + } + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt index f287017b02..33eefb4182 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt @@ -55,6 +55,7 @@ sealed class RoomDetailAction : VectorViewModelAction { data class ResendMessage(val eventId: String) : RoomDetailAction() data class RemoveFailedEcho(val eventId: String) : RoomDetailAction() + data class CancelSend(val eventId: String) : RoomDetailAction() data class ReplyToOptions(val eventId: String, val optionIndex: Int, val optionValue: String) : RoomDetailAction() diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt index 040a5191e4..b2790e0b47 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt @@ -1617,6 +1617,9 @@ class RoomDetailFragment @Inject constructor( is EventSharedAction.Remove -> { roomDetailViewModel.handle(RoomDetailAction.RemoveFailedEcho(action.eventId)) } + is EventSharedAction.Cancel -> { + roomDetailViewModel.handle(RoomDetailAction.CancelSend(action.eventId)) + } is EventSharedAction.ReportContentSpam -> { roomDetailViewModel.handle(RoomDetailAction.ReportContent( action.eventId, action.senderId, "This message is spam", spam = true)) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt index 4bad9c6ed0..799c9a615e 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt @@ -19,6 +19,7 @@ package im.vector.app.features.home.room.detail import android.net.Uri import androidx.annotation.IdRes import androidx.lifecycle.viewModelScope +import androidx.work.WorkManager import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.Success @@ -282,6 +283,7 @@ class RoomDetailViewModel @AssistedInject constructor( is RoomDetailAction.AddJitsiWidget -> handleAddJitsiConference(action) is RoomDetailAction.RemoveWidget -> handleDeleteWidget(action.widgetId) is RoomDetailAction.EnsureNativeWidgetAllowed -> handleCheckWidgetAllowed(action) + is RoomDetailAction.CancelSend -> handleCancel(action) }.exhaustive } @@ -1051,9 +1053,9 @@ class RoomDetailViewModel @AssistedInject constructor( return } when { - it.root.isTextMessage() -> room.resendTextMessage(it) - it.root.isImageMessage() -> room.resendMediaMessage(it) - else -> { + it.root.isTextMessage() -> room.resendTextMessage(it) + it.root.isAttachmentMessage() -> room.resendMediaMessage(it) + else -> { // TODO } } @@ -1072,6 +1074,18 @@ class RoomDetailViewModel @AssistedInject constructor( } } + private fun handleCancel(action: RoomDetailAction.CancelSend) { + val targetEventId = action.eventId + room.getTimeLineEvent(targetEventId)?.let { + // State must be UNDELIVERED or Failed + if (!it.root.sendState.isSending()) { + Timber.e("Cannot resend message, it is not failed, Cancel first") + return + } + room.cancelSend(action.eventId) + } + } + private fun handleClearSendQueue() { room.clearSendingQueue() } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt index b51f41d573..789025c538 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt @@ -230,6 +230,14 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted add(EventSharedAction.Resend(eventId)) } add(EventSharedAction.Remove(eventId)) + if (vectorPreferences.developerMode()) { + add(EventSharedAction.ViewSource(timelineEvent.root.toContentStringWithIndent())) + if (timelineEvent.isEncrypted() && timelineEvent.root.mxDecryptionResult != null) { + val decryptedContent = timelineEvent.root.toClearContentStringWithIndent() + ?: stringProvider.getString(R.string.encryption_information_decryption_error) + add(EventSharedAction.ViewDecryptedSource(decryptedContent)) + } + } } else if (timelineEvent.root.sendState.isSending()) { // TODO is uploading attachment? if (canCancel(timelineEvent)) { @@ -321,7 +329,7 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted } private fun canCancel(@Suppress("UNUSED_PARAMETER") event: TimelineEvent): Boolean { - return false + return true } private fun canReply(event: TimelineEvent, messageContent: MessageContent?, actionPermissions: ActionPermissions): Boolean { @@ -365,7 +373,9 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted } private fun canRetry(event: TimelineEvent, actionPermissions: ActionPermissions): Boolean { - return event.root.sendState.hasFailed() && event.root.isTextMessage() && actionPermissions.canSendMessage + return event.root.sendState.hasFailed() + && actionPermissions.canSendMessage + && (event.root.isAttachmentMessage() || event.root.isTextMessage()) } private fun canViewReactions(event: TimelineEvent): Boolean {