From 11c8da3717c1552defcd6f3df58a91a60c779f6a Mon Sep 17 00:00:00 2001 From: Valere Date: Wed, 5 Aug 2020 12:29:57 +0200 Subject: [PATCH 01/32] Improve upload/dl mem for big files + report ecryption progress --- .../MatrixDigestCheckInputStream.kt | 65 +++++++++++++++++++ .../app/core/glide/VectorGlideModelLoader.kt | 6 +- .../helper/ContentUploadStateTrackerBinder.kt | 14 ++-- 3 files changed, 78 insertions(+), 7 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/attachments/MatrixDigestCheckInputStream.kt diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/attachments/MatrixDigestCheckInputStream.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/attachments/MatrixDigestCheckInputStream.kt new file mode 100644 index 0000000000..2a6ec59f5f --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/attachments/MatrixDigestCheckInputStream.kt @@ -0,0 +1,65 @@ +/* + * 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.crypto.attachments + +import android.util.Base64 +import java.io.FilterInputStream +import java.io.IOException +import java.io.InputStream +import java.security.MessageDigest + +class MatrixDigestCheckInputStream(`in`: InputStream?, val expectedDigest: String) : FilterInputStream(`in`) { + + val digest = MessageDigest.getInstance("SHA-256") + + @Throws(IOException::class) + override fun read(): Int { + val b = `in`.read() + if (b >= 0) { + digest.update(b.toByte()) + } + + if (b == -1) { + ensureDigest() + } + return b + } + + @Throws(IOException::class) + override fun read( + b: ByteArray, + off: Int, + len: Int): Int { + val n = `in`.read(b, off, len) + if (n > 0) { + digest.update(b, off, n) + } + + if (n == -1) { + ensureDigest() + } + return n + } + + @Throws(IOException::class) + private fun ensureDigest() { + val currentDigestValue = MXEncryptedAttachments.base64ToUnpaddedBase64(Base64.encodeToString(digest.digest(), Base64.DEFAULT)) + if (currentDigestValue != expectedDigest) { + throw IOException("Bad digest") + } + } +} diff --git a/vector/src/main/java/im/vector/app/core/glide/VectorGlideModelLoader.kt b/vector/src/main/java/im/vector/app/core/glide/VectorGlideModelLoader.kt index 9ac8a4d3bc..8447557fe2 100644 --- a/vector/src/main/java/im/vector/app/core/glide/VectorGlideModelLoader.kt +++ b/vector/src/main/java/im/vector/app/core/glide/VectorGlideModelLoader.kt @@ -84,10 +84,14 @@ class VectorGlideDataFetcher(private val activeSessionHolder: ActiveSessionHolde override fun cancel() { if (stream != null) { try { + // This is often called on main thread, and this could be a network Stream.. + // on close will throw android.os.NetworkOnMainThreadException, so we catch throwable stream?.close() // interrupts decode if any stream = null - } catch (ignore: IOException) { + } catch (ignore: Throwable) { Timber.e(ignore) + } finally { + stream = null } } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/ContentUploadStateTrackerBinder.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/ContentUploadStateTrackerBinder.kt index f149b440e0..0bd59bf2fc 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/ContentUploadStateTrackerBinder.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/ContentUploadStateTrackerBinder.kt @@ -75,7 +75,7 @@ private class ContentMediaProgressUpdater(private val progressLayout: ViewGroup, is ContentUploadStateTracker.State.Idle -> handleIdle() is ContentUploadStateTracker.State.EncryptingThumbnail -> handleEncryptingThumbnail() is ContentUploadStateTracker.State.UploadingThumbnail -> handleProgressThumbnail(state) - is ContentUploadStateTracker.State.Encrypting -> handleEncrypting() + is ContentUploadStateTracker.State.Encrypting -> handleEncrypting(state) is ContentUploadStateTracker.State.Uploading -> handleProgress(state) is ContentUploadStateTracker.State.Failure -> handleFailure(state) is ContentUploadStateTracker.State.Success -> handleSuccess() @@ -98,26 +98,28 @@ private class ContentMediaProgressUpdater(private val progressLayout: ViewGroup, } private fun handleEncryptingThumbnail() { - doHandleEncrypting(R.string.send_file_step_encrypting_thumbnail) + doHandleEncrypting(R.string.send_file_step_encrypting_thumbnail, 0, 0) } private fun handleProgressThumbnail(state: ContentUploadStateTracker.State.UploadingThumbnail) { doHandleProgress(R.string.send_file_step_sending_thumbnail, state.current, state.total) } - private fun handleEncrypting() { - doHandleEncrypting(R.string.send_file_step_encrypting_file) + private fun handleEncrypting(state: ContentUploadStateTracker.State.Encrypting) { + doHandleEncrypting(R.string.send_file_step_encrypting_file, state.current, state.total) } private fun handleProgress(state: ContentUploadStateTracker.State.Uploading) { doHandleProgress(R.string.send_file_step_sending_file, state.current, state.total) } - private fun doHandleEncrypting(resId: Int) { + private fun doHandleEncrypting(resId: Int, current: Long, total: Long) { progressLayout.visibility = View.VISIBLE + val percent = if (total > 0) (100L * (current.toFloat() / total.toFloat())) else 0f val progressBar = progressLayout.findViewById(R.id.mediaProgressBar) val progressTextView = progressLayout.findViewById(R.id.mediaProgressTextView) - progressBar?.isIndeterminate = true + progressBar?.isIndeterminate = false + progressBar?.progress = percent.toInt() progressTextView?.text = progressLayout.context.getString(resId) progressTextView?.setTextColor(messageColorProvider.getMessageTextColor(SendState.ENCRYPTING)) } From 299cd9ced3cdb6e8dad24ce83e7f39cebbfb7039 Mon Sep 17 00:00:00 2001 From: Valere Date: Wed, 5 Aug 2020 12:30:57 +0200 Subject: [PATCH 02/32] Fix / preview/edit was shown for movies and gif --- .../vector/app/features/attachments/ContentAttachmentData.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/app/features/attachments/ContentAttachmentData.kt b/vector/src/main/java/im/vector/app/features/attachments/ContentAttachmentData.kt index b2e46b1abf..bd13c0dac4 100644 --- a/vector/src/main/java/im/vector/app/features/attachments/ContentAttachmentData.kt +++ b/vector/src/main/java/im/vector/app/features/attachments/ContentAttachmentData.kt @@ -19,7 +19,9 @@ package im.vector.app.features.attachments import org.matrix.android.sdk.api.session.content.ContentAttachmentData fun ContentAttachmentData.isPreviewable(): Boolean { - return type == ContentAttachmentData.Type.IMAGE || type == ContentAttachmentData.Type.VIDEO + // For now the preview only supports still image + return type == ContentAttachmentData.Type.IMAGE + && listOf("image/jpeg", "image/png", "image/jpg").contains(getSafeMimeType() ?: "") } data class GroupedContentAttachmentData( From caf0ac1c9f599d5ad0362cc603027807fce8c695 Mon Sep 17 00:00:00 2001 From: Valere Date: Fri, 7 Aug 2020 16:48:30 +0200 Subject: [PATCH 03/32] Add event sending indicator for attachment --- .../room/detail/timeline/item/MessageFileItem.kt | 4 ++++ .../detail/timeline/item/MessageImageVideoItem.kt | 7 +++++++ .../res/layout/item_timeline_event_file_stub.xml | 15 ++++++++++++++- .../item_timeline_event_media_message_stub.xml | 9 +++++++++ 4 files changed, 34 insertions(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageFileItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageFileItem.kt index b215fa5dd5..aa07a65c17 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageFileItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageFileItem.kt @@ -28,6 +28,7 @@ import com.airbnb.epoxy.EpoxyModelClass import im.vector.app.R import im.vector.app.features.home.room.detail.timeline.helper.ContentDownloadStateTrackerBinder import im.vector.app.features.home.room.detail.timeline.helper.ContentUploadStateTrackerBinder +import im.vector.matrix.android.api.session.room.send.SendState @EpoxyModelClass(layout = R.layout.item_timeline_event_base) abstract class MessageFileItem : AbsMessageItem() { @@ -86,6 +87,8 @@ abstract class MessageFileItem : AbsMessageItem() { holder.fileImageWrapper.setOnClickListener(attributes.itemClickListener) holder.fileImageWrapper.setOnLongClickListener(attributes.itemLongClickListener) holder.filenameView.paintFlags = (holder.filenameView.paintFlags or Paint.UNDERLINE_TEXT_FLAG) + + holder.eventSendingIndicator.isVisible = attributes.informationData.sendState == SendState.SENDING || attributes.informationData.sendState == SendState.ENCRYPTING } override fun unbind(holder: Holder) { @@ -103,6 +106,7 @@ abstract class MessageFileItem : AbsMessageItem() { val fileImageWrapper by bind(R.id.messageFileImageView) val fileDownloadProgress by bind(R.id.messageFileProgressbar) val filenameView by bind(R.id.messageFilenameView) + val eventSendingIndicator by bind(R.id.eventSendingIndicator) } companion object { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageImageVideoItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageImageVideoItem.kt index e8a840918f..0962aba312 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageImageVideoItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageImageVideoItem.kt @@ -19,6 +19,8 @@ package im.vector.app.features.home.room.detail.timeline.item import android.view.View import android.view.ViewGroup import android.widget.ImageView +import android.widget.ProgressBar +import android.widget.TextView import androidx.core.view.ViewCompat import androidx.core.view.isVisible import com.airbnb.epoxy.EpoxyAttribute @@ -27,6 +29,7 @@ import im.vector.app.R import im.vector.app.core.glide.GlideApp import im.vector.app.features.home.room.detail.timeline.helper.ContentUploadStateTrackerBinder import im.vector.app.features.media.ImageContentRenderer +import im.vector.matrix.android.api.session.room.send.SendState @EpoxyModelClass(layout = R.layout.item_timeline_event_base) abstract class MessageImageVideoItem : AbsMessageItem() { @@ -60,6 +63,8 @@ abstract class MessageImageVideoItem : AbsMessageItem(R.id.messageMediaUploadProgressLayout) val imageView by bind(R.id.messageThumbnailView) @@ -79,6 +85,7 @@ abstract class MessageImageVideoItem : AbsMessageItem(R.id.messageContentMedia) val failedToSendIndicator by bind(R.id.messageFailToSendIndicator) + val eventSendingIndicator by bind(R.id.eventSendingIndicator) } companion object { diff --git a/vector/src/main/res/layout/item_timeline_event_file_stub.xml b/vector/src/main/res/layout/item_timeline_event_file_stub.xml index a67d14168b..efd85fc31f 100644 --- a/vector/src/main/res/layout/item_timeline_event_file_stub.xml +++ b/vector/src/main/res/layout/item_timeline_event_file_stub.xml @@ -42,7 +42,9 @@ + + + + + Date: Fri, 7 Aug 2020 16:50:57 +0200 Subject: [PATCH 04/32] Use file service in glide loader (avoid re-dl after send) --- .../app/core/glide/VectorGlideModelLoader.kt | 64 +++++++++++++------ 1 file changed, 44 insertions(+), 20 deletions(-) diff --git a/vector/src/main/java/im/vector/app/core/glide/VectorGlideModelLoader.kt b/vector/src/main/java/im/vector/app/core/glide/VectorGlideModelLoader.kt index 8447557fe2..19147cc4a1 100644 --- a/vector/src/main/java/im/vector/app/core/glide/VectorGlideModelLoader.kt +++ b/vector/src/main/java/im/vector/app/core/glide/VectorGlideModelLoader.kt @@ -33,6 +33,8 @@ import timber.log.Timber import java.io.File import java.io.IOException import java.io.InputStream +import java.lang.Exception +import java.lang.IllegalArgumentException class VectorGlideModelLoaderFactory(private val activeSessionHolder: ActiveSessionHolder) : ModelLoaderFactory { @@ -89,7 +91,7 @@ class VectorGlideDataFetcher(private val activeSessionHolder: ActiveSessionHolde stream?.close() // interrupts decode if any stream = null } catch (ignore: Throwable) { - Timber.e(ignore) + Timber.e("Failed to close stream ${ignore.localizedMessage}") } finally { stream = null } @@ -103,26 +105,48 @@ class VectorGlideDataFetcher(private val activeSessionHolder: ActiveSessionHolde callback.onDataReady(initialFile.inputStream()) return } - val contentUrlResolver = activeSessionHolder.getActiveSession().contentUrlResolver() - val url = contentUrlResolver.resolveFullSize(data.url) - ?: return +// val contentUrlResolver = activeSessionHolder.getActiveSession().contentUrlResolver() - val request = Request.Builder() - .url(url) - .build() + val fileService = activeSessionHolder.getSafeActiveSession()?.fileService() ?: return Unit.also { + callback.onLoadFailed(IllegalArgumentException("No File service")) + } + // Use the file vector service, will avoid flickering and redownload after upload + fileService.downloadFile( + downloadMode = FileService.DownloadMode.FOR_INTERNAL_USE, + mimeType = data.mimeType, + id = data.eventId, + url = data.url, + fileName = data.filename, + elementToDecrypt = data.elementToDecrypt, + callback = object: MatrixCallback { + override fun onSuccess(data: File) { + callback.onDataReady(data.inputStream()) + } - val response = client.newCall(request).execute() - val inputStream = response.body?.byteStream() - Timber.v("Response size ${response.body?.contentLength()} - Stream available: ${inputStream?.available()}") - if (!response.isSuccessful) { - callback.onLoadFailed(IOException("Unexpected code $response")) - return - } - stream = if (data.elementToDecrypt != null && data.elementToDecrypt.k.isNotBlank()) { - Matrix.decryptStream(inputStream, data.elementToDecrypt) - } else { - inputStream - } - callback.onDataReady(stream) + override fun onFailure(failure: Throwable) { + callback.onLoadFailed(failure as? Exception ?: IOException(failure.localizedMessage)) + } + } + ) +// val url = contentUrlResolver.resolveFullSize(data.url) +// ?: return +// +// val request = Request.Builder() +// .url(url) +// .build() +// +// val response = client.newCall(request).execute() +// val inputStream = response.body?.byteStream() +// Timber.v("Response size ${response.body?.contentLength()} - Stream available: ${inputStream?.available()}") +// if (!response.isSuccessful) { +// callback.onLoadFailed(IOException("Unexpected code $response")) +// return +// } +// stream = if (data.elementToDecrypt != null && data.elementToDecrypt.k.isNotBlank()) { +// Matrix.decryptStream(inputStream, data.elementToDecrypt) +// } else { +// inputStream +// } +// callback.onDataReady(stream) } } From 8b8855d2d5a7d063ee74a26ecdb43cb78ec9f9ff Mon Sep 17 00:00:00 2001 From: Valere Date: Fri, 7 Aug 2020 16:51:33 +0200 Subject: [PATCH 05/32] FIx / Audio icon not shown after download --- .../detail/timeline/factory/MessageItemFactory.kt | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt index cce3248551..97bc693f26 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt @@ -187,10 +187,18 @@ class MessageItemFactory @Inject constructor( informationData: MessageInformationData, highlight: Boolean, attributes: AbsMessageItem.Attributes): MessageFileItem? { + val fileUrl = messageContent.getFileUrl()?.let { + if (informationData.sentByMe && !informationData.sendState.isSent()) { + it + } else { + it.takeIf { it.startsWith("mxc://") } + } + } ?: "" return MessageFileItem_() .attributes(attributes) - .izLocalFile(messageContent.getFileUrl().isLocalFile()) - .mxcUrl(messageContent.getFileUrl() ?: "") + .izLocalFile(fileUrl.isLocalFile()) + .izDownloaded(session.fileService().isFileInCache(fileUrl, messageContent.mimeType)) + .mxcUrl(fileUrl) .contentUploadStateTrackerBinder(contentUploadStateTrackerBinder) .contentDownloadStateTrackerBinder(contentDownloadStateTrackerBinder) .highlighted(highlight) From a888e1e80ed1146becb3f78904cd1a8793c3d117 Mon Sep 17 00:00:00 2001 From: Valere Date: Fri, 7 Aug 2020 16:54:39 +0200 Subject: [PATCH 06/32] Support cancel sending and resend event with attachments Avoid auto retry for medium and big files --- matrix-sdk-android/build.gradle | 2 +- .../session/room/send/CancelSendTracker.kt | 61 +++++++++++++++++++ .../home/room/detail/RoomDetailAction.kt | 1 + .../home/room/detail/RoomDetailFragment.kt | 3 + .../home/room/detail/RoomDetailViewModel.kt | 20 +++++- .../action/MessageActionsViewModel.kt | 14 ++++- 6 files changed, 95 insertions(+), 6 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/CancelSendTracker.kt 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 { From 9d2ea19d7d5aa3f52280f55bfdf3c73a236e0b14 Mon Sep 17 00:00:00 2001 From: Valere Date: Fri, 7 Aug 2020 16:54:55 +0200 Subject: [PATCH 07/32] Upgrade to worker 2.4.0 --- vector/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vector/build.gradle b/vector/build.gradle index e55ad31ef3..52ff0dd23c 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -274,7 +274,7 @@ dependencies { def moshi_version = '1.8.0' def daggerVersion = '2.25.4' def autofill_version = "1.0.0" - def work_version = '2.3.4' + def work_version = '2.4.0' def arch_version = '2.1.0' def lifecycle_version = '2.2.0' From 31eccf5f1c9e044c4d166cdfaed28a96ed326e0a Mon Sep 17 00:00:00 2001 From: Valere Date: Fri, 7 Aug 2020 17:05:33 +0200 Subject: [PATCH 08/32] Cleaning --- .../java/im/vector/app/core/glide/VectorGlideModelLoader.kt | 1 - .../vector/app/features/home/room/detail/RoomDetailViewModel.kt | 1 - .../home/room/detail/timeline/item/MessageImageVideoItem.kt | 2 -- 3 files changed, 4 deletions(-) diff --git a/vector/src/main/java/im/vector/app/core/glide/VectorGlideModelLoader.kt b/vector/src/main/java/im/vector/app/core/glide/VectorGlideModelLoader.kt index 19147cc4a1..a35ed5a7bb 100644 --- a/vector/src/main/java/im/vector/app/core/glide/VectorGlideModelLoader.kt +++ b/vector/src/main/java/im/vector/app/core/glide/VectorGlideModelLoader.kt @@ -28,7 +28,6 @@ import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.features.media.ImageContentRenderer import org.matrix.android.sdk.api.Matrix import okhttp3.OkHttpClient -import okhttp3.Request import timber.log.Timber import java.io.File import java.io.IOException 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 799c9a615e..7a50894a44 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,7 +19,6 @@ 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 diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageImageVideoItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageImageVideoItem.kt index 0962aba312..0ad61ce874 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageImageVideoItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageImageVideoItem.kt @@ -20,7 +20,6 @@ import android.view.View import android.view.ViewGroup import android.widget.ImageView import android.widget.ProgressBar -import android.widget.TextView import androidx.core.view.ViewCompat import androidx.core.view.isVisible import com.airbnb.epoxy.EpoxyAttribute @@ -77,7 +76,6 @@ abstract class MessageImageVideoItem : AbsMessageItem(R.id.messageMediaUploadProgressLayout) val imageView by bind(R.id.messageThumbnailView) From 5f76f182f6cc9c08ebdc379af3cddbe9401b09fa Mon Sep 17 00:00:00 2001 From: Valere Date: Mon, 10 Aug 2020 14:59:31 +0200 Subject: [PATCH 09/32] Fix clear glide in recycler view --- .../java/im/vector/app/core/ui/views/ReadReceiptsView.kt | 5 ++--- .../java/im/vector/app/features/home/AvatarRenderer.kt | 5 +++++ .../home/room/detail/timeline/item/AbsBaseMessageItem.kt | 2 +- .../home/room/detail/timeline/item/AbsMessageItem.kt | 1 + .../home/room/detail/timeline/item/DefaultItem.kt | 6 ++++++ .../room/detail/timeline/item/MessageImageVideoItem.kt | 1 + .../home/room/detail/timeline/item/NoticeItem.kt | 6 ++++++ .../app/features/home/room/list/RoomSummaryItem.kt | 1 + .../im/vector/app/features/media/ImageContentRenderer.kt | 9 +++++++++ 9 files changed, 32 insertions(+), 4 deletions(-) diff --git a/vector/src/main/java/im/vector/app/core/ui/views/ReadReceiptsView.kt b/vector/src/main/java/im/vector/app/core/ui/views/ReadReceiptsView.kt index 2ce3396d0c..b1ef746ee7 100644 --- a/vector/src/main/java/im/vector/app/core/ui/views/ReadReceiptsView.kt +++ b/vector/src/main/java/im/vector/app/core/ui/views/ReadReceiptsView.kt @@ -23,7 +23,6 @@ import android.widget.ImageView import android.widget.LinearLayout import androidx.core.view.isVisible import im.vector.app.R -import im.vector.app.core.glide.GlideApp import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.room.detail.timeline.item.ReadReceiptData import im.vector.app.features.home.room.detail.timeline.item.toMatrixItem @@ -113,9 +112,9 @@ class ReadReceiptsView @JvmOverloads constructor( } } - fun unbind() { + fun unbind(avatarRenderer: AvatarRenderer?) { receiptAvatars.forEach { - GlideApp.with(context.applicationContext).clear(it) + avatarRenderer?.clear(it) } isVisible = false } diff --git a/vector/src/main/java/im/vector/app/features/home/AvatarRenderer.kt b/vector/src/main/java/im/vector/app/features/home/AvatarRenderer.kt index f53fa77791..5e1433a6fa 100644 --- a/vector/src/main/java/im/vector/app/features/home/AvatarRenderer.kt +++ b/vector/src/main/java/im/vector/app/features/home/AvatarRenderer.kt @@ -56,6 +56,11 @@ class AvatarRenderer @Inject constructor(private val activeSessionHolder: Active DrawableImageViewTarget(imageView)) } + fun clear(imageView: ImageView) { + // It can be called after recycler view is destroyed, just silently catch + tryThis { GlideApp.with(imageView).clear(imageView) } + } + @UiThread fun render(matrixItem: MatrixItem, imageView: ImageView, glideRequests: GlideRequests) { render(imageView.context, diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsBaseMessageItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsBaseMessageItem.kt index 1aec2db9a4..631cd9ff66 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsBaseMessageItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsBaseMessageItem.kt @@ -110,7 +110,7 @@ abstract class AbsBaseMessageItem : BaseEventItem override fun unbind(holder: H) { holder.reactionsContainer.setOnLongClickListener(null) - holder.readReceiptsView.unbind() + holder.readReceiptsView.unbind(baseAttributes.avatarRenderer) super.unbind(holder) } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageItem.kt index fa216c15a4..7c2a6286b9 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageItem.kt @@ -77,6 +77,7 @@ abstract class AbsMessageItem : AbsBaseMessageItem } override fun unbind(holder: H) { + attributes.avatarRenderer.clear(holder.avatarImageView) holder.avatarImageView.setOnClickListener(null) holder.avatarImageView.setOnLongClickListener(null) holder.memberNameView.setOnClickListener(null) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/DefaultItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/DefaultItem.kt index e82c4191b9..8cd3c95141 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/DefaultItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/DefaultItem.kt @@ -44,6 +44,12 @@ abstract class DefaultItem : BaseEventItem() { holder.readReceiptsView.render(attributes.informationData.readReceipts, attributes.avatarRenderer, _readReceiptsClickListener) } + override fun unbind(holder: Holder) { + attributes.avatarRenderer.clear(holder.avatarImageView) + holder.readReceiptsView.unbind(attributes.avatarRenderer) + super.unbind(holder) + } + override fun getEventIds(): List { return listOf(attributes.informationData.eventId) } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageImageVideoItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageImageVideoItem.kt index 0ad61ce874..be3d276037 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageImageVideoItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageImageVideoItem.kt @@ -68,6 +68,7 @@ abstract class MessageImageVideoItem : AbsMessageItem() { } } + override fun unbind(holder: Holder) { + attributes.avatarRenderer.clear(holder.avatarImageView) + holder.readReceiptsView.unbind(attributes.avatarRenderer) + super.unbind(holder) + } + override fun getEventIds(): List { return listOf(attributes.informationData.eventId) } diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItem.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItem.kt index ce572ed96d..4cac11af21 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItem.kt @@ -82,6 +82,7 @@ abstract class RoomSummaryItem : VectorEpoxyModel() { override fun unbind(holder: Holder) { holder.rootView.setOnClickListener(null) holder.rootView.setOnLongClickListener(null) + avatarRenderer.clear(holder.avatarImageView) super.unbind(holder) } diff --git a/vector/src/main/java/im/vector/app/features/media/ImageContentRenderer.kt b/vector/src/main/java/im/vector/app/features/media/ImageContentRenderer.kt index 2976f4b4b1..a7d666184d 100644 --- a/vector/src/main/java/im/vector/app/features/media/ImageContentRenderer.kt +++ b/vector/src/main/java/im/vector/app/features/media/ImageContentRenderer.kt @@ -103,6 +103,15 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder: .into(imageView) } + fun clear(imageView: ImageView) { + // It can be called after recycler view is destroyed, just silently catch + // We'd better keep ref to requestManager, but we don't have it + tryThis { + GlideApp + .with(imageView).clear(imageView) + } + } + fun render(data: Data, contextView: View, target: CustomViewTarget<*, Drawable>) { val req = if (data.elementToDecrypt != null) { // Encrypted image From d6f96e3d64a3655889a7d2a377dc26146821a7d8 Mon Sep 17 00:00:00 2001 From: Valere Date: Mon, 10 Aug 2020 15:42:00 +0200 Subject: [PATCH 10/32] Fix test + cleaning --- .../features/home/room/detail/timeline/item/MessageFileItem.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageFileItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageFileItem.kt index aa07a65c17..37885d0751 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageFileItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageFileItem.kt @@ -88,7 +88,8 @@ abstract class MessageFileItem : AbsMessageItem() { holder.fileImageWrapper.setOnLongClickListener(attributes.itemLongClickListener) holder.filenameView.paintFlags = (holder.filenameView.paintFlags or Paint.UNDERLINE_TEXT_FLAG) - holder.eventSendingIndicator.isVisible = attributes.informationData.sendState == SendState.SENDING || attributes.informationData.sendState == SendState.ENCRYPTING + holder.eventSendingIndicator.isVisible = attributes.informationData.sendState == SendState.SENDING + || attributes.informationData.sendState == SendState.ENCRYPTING } override fun unbind(holder: Holder) { From cc57a73f233107be314a2ed64247c9b86f379143 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 11 Aug 2020 17:33:40 +0200 Subject: [PATCH 11/32] Cleanup and split long lines --- .../home/room/detail/timeline/item/MessageFileItem.kt | 4 +--- .../room/detail/timeline/item/MessageImageVideoItem.kt | 2 +- .../src/main/res/layout/item_timeline_event_file_stub.xml | 8 ++++---- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageFileItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageFileItem.kt index 37885d0751..1f3c6147ae 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageFileItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageFileItem.kt @@ -28,7 +28,6 @@ import com.airbnb.epoxy.EpoxyModelClass import im.vector.app.R import im.vector.app.features.home.room.detail.timeline.helper.ContentDownloadStateTrackerBinder import im.vector.app.features.home.room.detail.timeline.helper.ContentUploadStateTrackerBinder -import im.vector.matrix.android.api.session.room.send.SendState @EpoxyModelClass(layout = R.layout.item_timeline_event_base) abstract class MessageFileItem : AbsMessageItem() { @@ -88,8 +87,7 @@ abstract class MessageFileItem : AbsMessageItem() { holder.fileImageWrapper.setOnLongClickListener(attributes.itemLongClickListener) holder.filenameView.paintFlags = (holder.filenameView.paintFlags or Paint.UNDERLINE_TEXT_FLAG) - holder.eventSendingIndicator.isVisible = attributes.informationData.sendState == SendState.SENDING - || attributes.informationData.sendState == SendState.ENCRYPTING + holder.eventSendingIndicator.isVisible = attributes.informationData.sendState.isInProgress() } override fun unbind(holder: Holder) { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageImageVideoItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageImageVideoItem.kt index be3d276037..53946e4bf6 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageImageVideoItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageImageVideoItem.kt @@ -63,7 +63,7 @@ abstract class MessageImageVideoItem : AbsMessageItem @@ -62,9 +62,9 @@ android:layout_width="16dp" android:layout_height="16dp" android:visibility="gone" - tools:visibility="visible" app:layout_constraintStart_toEndOf="@id/messageFilenameView" - app:layout_constraintTop_toTopOf="@id/messageFilenameView" /> + app:layout_constraintTop_toTopOf="@id/messageFilenameView" + tools:visibility="visible" /> Date: Mon, 17 Aug 2020 23:33:33 +0200 Subject: [PATCH 12/32] Rebase post matrix sdk package renaming --- .../crypto/AttachmentEncryptionTest.kt | 4 +- .../content/ContentUploadStateTracker.kt | 2 +- .../sdk/api/session/events/model/Event.kt | 18 ++ .../sdk/api/session/room/send/SendService.kt | 2 + .../sdk/api/session/room/send/SendState.kt | 5 +- .../attachments/MXEncryptedAttachments.kt | 261 +++++++++++++----- .../MatrixDigestCheckInputStream.kt | 2 +- .../internal/network/ProgressRequestBody.kt | 4 + .../internal/session/DefaultFileService.kt | 16 +- .../DefaultContentUploadStateTracker.kt | 4 +- .../internal/session/content/FileUploader.kt | 48 +++- .../session/content/UploadContentWorker.kt | 172 +++++++----- .../DefaultContentDownloadStateTracker.kt | 22 +- .../session/room/send/CancelSendTracker.kt | 4 +- .../session/room/send/DefaultSendService.kt | 130 ++++++--- .../session/room/send/EncryptEventWorker.kt | 9 +- .../session/room/send/LocalEchoRepository.kt | 37 +-- .../MultipleEventSendingDispatcherWorker.kt | 9 +- .../session/room/send/RoomEventSender.kt | 7 +- .../session/room/send/SendEventWorker.kt | 24 +- .../session/room/timeline/DefaultTimeline.kt | 1 + .../timeline/TimelineSendEventWorkCommon.kt | 2 +- .../app/core/glide/VectorGlideModelLoader.kt | 3 +- .../app/features/home/AvatarRenderer.kt | 3 +- .../home/room/detail/RoomDetailViewModel.kt | 2 +- .../action/MessageActionsViewModel.kt | 1 + .../timeline/item/MessageImageVideoItem.kt | 1 - .../features/media/ImageContentRenderer.kt | 1 + 28 files changed, 563 insertions(+), 231 deletions(-) rename matrix-sdk-android/src/main/java/{im/vector/matrix/android => org/matrix/android/sdk}/internal/crypto/attachments/MatrixDigestCheckInputStream.kt (96%) rename matrix-sdk-android/src/main/java/{im/vector/matrix/android => org/matrix/android/sdk}/internal/session/room/send/CancelSendTracker.kt (94%) diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/AttachmentEncryptionTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/AttachmentEncryptionTest.kt index 80e7b6dbbb..476f67218b 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/AttachmentEncryptionTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/AttachmentEncryptionTest.kt @@ -29,6 +29,8 @@ import org.junit.runners.MethodSorters import org.matrix.android.sdk.internal.crypto.attachments.MXEncryptedAttachments import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileInfo import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileKey +import org.matrix.android.sdk.internal.crypto.attachments.toElementToDecrypt +import java.io.ByteArrayInputStream import java.io.InputStream /** @@ -52,7 +54,7 @@ class AttachmentEncryptionTest { memoryFile.inputStream } - val decryptedStream = MXEncryptedAttachments.decryptAttachment(inputStream, encryptedFileInfo) + val decryptedStream = MXEncryptedAttachments.decryptAttachment(inputStream, encryptedFileInfo.toElementToDecrypt()!!) assertNotNull(decryptedStream) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/content/ContentUploadStateTracker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/content/ContentUploadStateTracker.kt index a29e7110e2..a216770939 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/content/ContentUploadStateTracker.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/content/ContentUploadStateTracker.kt @@ -33,7 +33,7 @@ interface ContentUploadStateTracker { object Idle : State() object EncryptingThumbnail : State() data class UploadingThumbnail(val current: Long, val total: Long) : State() - object Encrypting : State() + data class Encrypting(val current: Long, val total: Long) : State() data class Uploading(val current: Long, val total: Long) : State() object Success : State() data class Failure(val throwable: Throwable) : State() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt index fdd3e66703..1068b92019 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt @@ -239,6 +239,14 @@ fun Event.isVideoMessage(): Boolean { } } +fun Event.isAudioMessage(): Boolean { + return getClearType() == EventType.MESSAGE + && when (getClearContent()?.toModel()?.msgType) { + MessageType.MSGTYPE_AUDIO -> true + else -> false + } +} + fun Event.isFileMessage(): Boolean { return getClearType() == EventType.MESSAGE && when (getClearContent()?.toModel()?.msgType) { @@ -246,6 +254,16 @@ fun Event.isFileMessage(): Boolean { else -> false } } +fun Event.isAttachmentMessage(): Boolean { + return getClearType() == EventType.MESSAGE + && when (getClearContent()?.toModel()?.msgType) { + MessageType.MSGTYPE_IMAGE, + MessageType.MSGTYPE_AUDIO, + MessageType.MSGTYPE_VIDEO, + MessageType.MSGTYPE_FILE -> true + else -> false + } +} fun Event.getRelationContent(): RelationDefaultContent? { return if (isEncrypted()) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendService.kt index e84b75d0af..de85438b1c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendService.kt @@ -126,6 +126,8 @@ interface SendService { fun clearSendingQueue() + fun cancelSend(eventId: String) + /** * Resend all failed messages one by one (and keep order) */ diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendState.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendState.kt index f0dd2f3025..be8849b20e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendState.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendState.kt @@ -37,7 +37,8 @@ enum class SendState { internal companion object { val HAS_FAILED_STATES = listOf(UNDELIVERED, FAILED_UNKNOWN_DEVICES) val IS_SENT_STATES = listOf(SENT, SYNCED) - val IS_SENDING_STATES = listOf(UNSENT, ENCRYPTING, SENDING) + val IS_PROGRESSING_STATES = listOf(ENCRYPTING, SENDING) + val IS_SENDING_STATES = IS_PROGRESSING_STATES + UNSENT val PENDING_STATES = IS_SENDING_STATES + HAS_FAILED_STATES } @@ -45,5 +46,7 @@ enum class SendState { fun hasFailed() = HAS_FAILED_STATES.contains(this) + fun isInProgress() = IS_PROGRESSING_STATES.contains(this) + fun isSending() = IS_SENDING_STATES.contains(this) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/attachments/MXEncryptedAttachments.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/attachments/MXEncryptedAttachments.kt index 9e1ef19b3a..68fce9462b 100755 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/attachments/MXEncryptedAttachments.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/attachments/MXEncryptedAttachments.kt @@ -22,10 +22,13 @@ import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileInfo import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileKey import timber.log.Timber import java.io.ByteArrayOutputStream +import java.io.File import java.io.InputStream +import java.io.OutputStream import java.security.MessageDigest import java.security.SecureRandom import javax.crypto.Cipher +import javax.crypto.CipherInputStream import javax.crypto.spec.IvParameterSpec import javax.crypto.spec.SecretKeySpec @@ -35,8 +38,121 @@ internal object MXEncryptedAttachments { private const val SECRET_KEY_SPEC_ALGORITHM = "AES" private const val MESSAGE_DIGEST_ALGORITHM = "SHA-256" + fun encrypt(clearStream: InputStream, mimetype: String?, outputFile: File, progress: ((current: Int, total: Int) -> Unit)): EncryptedFileInfo { + val t0 = System.currentTimeMillis() + val secureRandom = SecureRandom() + val initVectorBytes = ByteArray(16) { 0.toByte() } + + val ivRandomPart = ByteArray(8) + secureRandom.nextBytes(ivRandomPart) + + System.arraycopy(ivRandomPart, 0, initVectorBytes, 0, ivRandomPart.size) + + val key = ByteArray(32) + secureRandom.nextBytes(key) + + val messageDigest = MessageDigest.getInstance(MESSAGE_DIGEST_ALGORITHM) + + outputFile.outputStream().use { outputStream -> + val encryptCipher = Cipher.getInstance(CIPHER_ALGORITHM) + val secretKeySpec = SecretKeySpec(key, SECRET_KEY_SPEC_ALGORITHM) + val ivParameterSpec = IvParameterSpec(initVectorBytes) + encryptCipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec) + + val data = ByteArray(CRYPTO_BUFFER_SIZE) + var read: Int + var encodedBytes: ByteArray + clearStream.use { inputStream -> + val estimatedSize = inputStream.available() + progress.invoke(0, estimatedSize) + read = inputStream.read(data) + var totalRead = read + while (read != -1) { + progress.invoke(totalRead, estimatedSize) + encodedBytes = encryptCipher.update(data, 0, read) + messageDigest.update(encodedBytes, 0, encodedBytes.size) + outputStream.write(encodedBytes) + read = inputStream.read(data) + totalRead += read + } + } + + // encrypt the latest chunk + encodedBytes = encryptCipher.doFinal() + messageDigest.update(encodedBytes, 0, encodedBytes.size) + outputStream.write(encodedBytes) + } + + return EncryptedFileInfo( + url = null, + mimetype = mimetype, + key = EncryptedFileKey( + alg = "A256CTR", + ext = true, + key_ops = listOf("encrypt", "decrypt"), + kty = "oct", + k = base64ToBase64Url(Base64.encodeToString(key, Base64.DEFAULT)) + ), + iv = Base64.encodeToString(initVectorBytes, Base64.DEFAULT).replace("\n", "").replace("=", ""), + hashes = mapOf("sha256" to base64ToUnpaddedBase64(Base64.encodeToString(messageDigest.digest(), Base64.DEFAULT))), + v = "v2" + ) + .also { Timber.v("Encrypt in ${System.currentTimeMillis() - t0}ms") } + } + +// fun cipherInputStream(attachmentStream: InputStream, mimetype: String?): Pair { +// val secureRandom = SecureRandom() +// +// // generate a random iv key +// // Half of the IV is random, the lower order bits are zeroed +// // such that the counter never wraps. +// // See https://github.com/matrix-org/matrix-ios-kit/blob/3dc0d8e46b4deb6669ed44f72ad79be56471354c/MatrixKit/Models/Room/MXEncryptedAttachments.m#L75 +// val initVectorBytes = ByteArray(16) { 0.toByte() } +// +// val ivRandomPart = ByteArray(8) +// secureRandom.nextBytes(ivRandomPart) +// +// System.arraycopy(ivRandomPart, 0, initVectorBytes, 0, ivRandomPart.size) +// +// val key = ByteArray(32) +// secureRandom.nextBytes(key) +// +// val encryptCipher = Cipher.getInstance(CIPHER_ALGORITHM) +// val secretKeySpec = SecretKeySpec(key, SECRET_KEY_SPEC_ALGORITHM) +// val ivParameterSpec = IvParameterSpec(initVectorBytes) +// encryptCipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec) +// +// val cipherInputStream = CipherInputStream(attachmentStream, encryptCipher) +// +// // Could it be possible to get the digest on the fly instead of +// val info = EncryptedFileInfo( +// url = null, +// mimetype = mimetype, +// key = EncryptedFileKey( +// alg = "A256CTR", +// ext = true, +// key_ops = listOf("encrypt", "decrypt"), +// kty = "oct", +// k = base64ToBase64Url(Base64.encodeToString(key, Base64.DEFAULT)) +// ), +// iv = Base64.encodeToString(initVectorBytes, Base64.DEFAULT).replace("\n", "").replace("=", ""), +// //hashes = mapOf("sha256" to base64ToUnpaddedBase64(Base64.encodeToString(messageDigest.digest(), Base64.DEFAULT))), +// v = "v2" +// ) +// +// val messageDigest = MessageDigest.getInstance(MESSAGE_DIGEST_ALGORITHM) +// return DigestInputStream(cipherInputStream, messageDigest) to info +// } +// +// fun updateInfoWithDigest(digestInputStream: DigestInputStream, info: EncryptedFileInfo): EncryptedFileInfo { +// return info.copy( +// hashes = mapOf("sha256" to base64ToUnpaddedBase64(Base64.encodeToString(digestInputStream.messageDigest.digest(), Base64.DEFAULT))) +// ) +// } + /*** * Encrypt an attachment stream. + * DO NOT USE for big files, it will load all in memory * @param attachmentStream the attachment stream. Will be closed after this method call. * @param mimetype the mime type * @return the encryption file info @@ -59,14 +175,14 @@ internal object MXEncryptedAttachments { val key = ByteArray(32) secureRandom.nextBytes(key) - ByteArrayOutputStream().use { outputStream -> + val messageDigest = MessageDigest.getInstance(MESSAGE_DIGEST_ALGORITHM) + val byteArrayOutputStream = ByteArrayOutputStream() + byteArrayOutputStream.use { outputStream -> val encryptCipher = Cipher.getInstance(CIPHER_ALGORITHM) val secretKeySpec = SecretKeySpec(key, SECRET_KEY_SPEC_ALGORITHM) val ivParameterSpec = IvParameterSpec(initVectorBytes) encryptCipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec) - val messageDigest = MessageDigest.getInstance(MESSAGE_DIGEST_ALGORITHM) - val data = ByteArray(CRYPTO_BUFFER_SIZE) var read: Int var encodedBytes: ByteArray @@ -85,26 +201,26 @@ internal object MXEncryptedAttachments { encodedBytes = encryptCipher.doFinal() messageDigest.update(encodedBytes, 0, encodedBytes.size) outputStream.write(encodedBytes) - - return EncryptionResult( - encryptedFileInfo = EncryptedFileInfo( - url = null, - mimetype = mimetype, - key = EncryptedFileKey( - alg = "A256CTR", - ext = true, - keyOps = listOf("encrypt", "decrypt"), - kty = "oct", - k = base64ToBase64Url(Base64.encodeToString(key, Base64.DEFAULT)) - ), - iv = Base64.encodeToString(initVectorBytes, Base64.DEFAULT).replace("\n", "").replace("=", ""), - hashes = mapOf("sha256" to base64ToUnpaddedBase64(Base64.encodeToString(messageDigest.digest(), Base64.DEFAULT))), - v = "v2" - ), - encryptedByteArray = outputStream.toByteArray() - ) - .also { Timber.v("Encrypt in ${System.currentTimeMillis() - t0}ms") } } + + return EncryptionResult( + encryptedFileInfo = EncryptedFileInfo( + url = null, + mimetype = mimetype, + key = EncryptedFileKey( + alg = "A256CTR", + ext = true, + key_ops = listOf("encrypt", "decrypt"), + kty = "oct", + k = base64ToBase64Url(Base64.encodeToString(key, Base64.DEFAULT)) + ), + iv = Base64.encodeToString(initVectorBytes, Base64.DEFAULT).replace("\n", "").replace("=", ""), + hashes = mapOf("sha256" to base64ToUnpaddedBase64(Base64.encodeToString(messageDigest.digest(), Base64.DEFAULT))), + v = "v2" + ), + encryptedByteArray = byteArrayOutputStream.toByteArray() + ) + .also { Timber.v("Encrypt in ${System.currentTimeMillis() - t0}ms") } } /** @@ -114,15 +230,23 @@ internal object MXEncryptedAttachments { * @param encryptedFileInfo the encryption file info * @return the decrypted attachment stream */ - fun decryptAttachment(attachmentStream: InputStream?, encryptedFileInfo: EncryptedFileInfo?): InputStream? { - if (encryptedFileInfo?.isValid() != true) { - Timber.e("## decryptAttachment() : some fields are not defined, or invalid key fields") + fun decryptAttachment(attachmentStream: InputStream?, elementToDecrypt: ElementToDecrypt): InputStream? { + try { + val digestCheckInputStream = MatrixDigestCheckInputStream(attachmentStream, elementToDecrypt.sha256) + + val key = Base64.decode(base64UrlToBase64(elementToDecrypt.k), Base64.DEFAULT) + val initVectorBytes = Base64.decode(elementToDecrypt.iv, Base64.DEFAULT) + + val decryptCipher = Cipher.getInstance(CIPHER_ALGORITHM) + val secretKeySpec = SecretKeySpec(key, SECRET_KEY_SPEC_ALGORITHM) + val ivParameterSpec = IvParameterSpec(initVectorBytes) + decryptCipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec) + + return CipherInputStream(digestCheckInputStream, decryptCipher) + } catch (failure: Throwable) { + Timber.e(failure, "## decryptAttachment() : failed to create stream") return null } - - val elementToDecrypt = encryptedFileInfo.toElementToDecrypt() - - return decryptAttachment(attachmentStream, elementToDecrypt) } /** @@ -132,62 +256,59 @@ internal object MXEncryptedAttachments { * @param elementToDecrypt the elementToDecrypt info * @return the decrypted attachment stream */ - fun decryptAttachment(attachmentStream: InputStream?, elementToDecrypt: ElementToDecrypt?): InputStream? { + fun decryptAttachment(attachmentStream: InputStream?, elementToDecrypt: ElementToDecrypt?, outputStream: OutputStream): Boolean { // sanity checks if (null == attachmentStream || elementToDecrypt == null) { Timber.e("## decryptAttachment() : null stream") - return null + return false } val t0 = System.currentTimeMillis() - ByteArrayOutputStream().use { outputStream -> - try { - val key = Base64.decode(base64UrlToBase64(elementToDecrypt.k), Base64.DEFAULT) - val initVectorBytes = Base64.decode(elementToDecrypt.iv, Base64.DEFAULT) + try { + val key = Base64.decode(base64UrlToBase64(elementToDecrypt.k), Base64.DEFAULT) + val initVectorBytes = Base64.decode(elementToDecrypt.iv, Base64.DEFAULT) - val decryptCipher = Cipher.getInstance(CIPHER_ALGORITHM) - val secretKeySpec = SecretKeySpec(key, SECRET_KEY_SPEC_ALGORITHM) - val ivParameterSpec = IvParameterSpec(initVectorBytes) - decryptCipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec) + val decryptCipher = Cipher.getInstance(CIPHER_ALGORITHM) + val secretKeySpec = SecretKeySpec(key, SECRET_KEY_SPEC_ALGORITHM) + val ivParameterSpec = IvParameterSpec(initVectorBytes) + decryptCipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec) - val messageDigest = MessageDigest.getInstance(MESSAGE_DIGEST_ALGORITHM) + val messageDigest = MessageDigest.getInstance(MESSAGE_DIGEST_ALGORITHM) - var read: Int - val data = ByteArray(CRYPTO_BUFFER_SIZE) - var decodedBytes: ByteArray + var read: Int + val data = ByteArray(CRYPTO_BUFFER_SIZE) + var decodedBytes: ByteArray - attachmentStream.use { inputStream -> + attachmentStream.use { inputStream -> + read = inputStream.read(data) + while (read != -1) { + messageDigest.update(data, 0, read) + decodedBytes = decryptCipher.update(data, 0, read) + outputStream.write(decodedBytes) read = inputStream.read(data) - while (read != -1) { - messageDigest.update(data, 0, read) - decodedBytes = decryptCipher.update(data, 0, read) - outputStream.write(decodedBytes) - read = inputStream.read(data) - } } - - // decrypt the last chunk - decodedBytes = decryptCipher.doFinal() - outputStream.write(decodedBytes) - - val currentDigestValue = base64ToUnpaddedBase64(Base64.encodeToString(messageDigest.digest(), Base64.DEFAULT)) - - if (elementToDecrypt.sha256 != currentDigestValue) { - Timber.e("## decryptAttachment() : Digest value mismatch") - return null - } - - return outputStream.toByteArray().inputStream() - .also { Timber.v("Decrypt in ${System.currentTimeMillis() - t0}ms") } - } catch (oom: OutOfMemoryError) { - Timber.e(oom, "## decryptAttachment() failed: OOM") - } catch (e: Exception) { - Timber.e(e, "## decryptAttachment() failed") } + + // decrypt the last chunk + decodedBytes = decryptCipher.doFinal() + outputStream.write(decodedBytes) + + val currentDigestValue = base64ToUnpaddedBase64(Base64.encodeToString(messageDigest.digest(), Base64.DEFAULT)) + + if (elementToDecrypt.sha256 != currentDigestValue) { + Timber.e("## decryptAttachment() : Digest value mismatch") + return false + } + + return true.also { Timber.v("Decrypt in ${System.currentTimeMillis() - t0}ms") } + } catch (oom: OutOfMemoryError) { + Timber.e(oom, "## decryptAttachment() failed: OOM") + } catch (e: Exception) { + Timber.e(e, "## decryptAttachment() failed") } - return null + return false } /** @@ -206,7 +327,7 @@ internal object MXEncryptedAttachments { .replace("=", "") } - private fun base64ToUnpaddedBase64(base64: String): String { + internal fun base64ToUnpaddedBase64(base64: String): String { return base64.replace("\n".toRegex(), "") .replace("=", "") } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/attachments/MatrixDigestCheckInputStream.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/attachments/MatrixDigestCheckInputStream.kt similarity index 96% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/attachments/MatrixDigestCheckInputStream.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/attachments/MatrixDigestCheckInputStream.kt index 2a6ec59f5f..01de479ff5 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/attachments/MatrixDigestCheckInputStream.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/attachments/MatrixDigestCheckInputStream.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package im.vector.matrix.android.internal.crypto.attachments +package org.matrix.android.sdk.internal.crypto.attachments import android.util.Base64 import java.io.FilterInputStream diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ProgressRequestBody.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ProgressRequestBody.kt index 7ce260e54e..addc5b7205 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ProgressRequestBody.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ProgressRequestBody.kt @@ -35,6 +35,10 @@ internal class ProgressRequestBody(private val delegate: RequestBody, return delegate.contentType() } + override fun isOneShot() = delegate.isOneShot() + + override fun isDuplex() = delegate.isDuplex() + override fun contentLength(): Long { try { return delegate.contentLength() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultFileService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultFileService.kt index 97ebe943ec..aa4114c8c2 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultFileService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultFileService.kt @@ -143,20 +143,22 @@ internal class DefaultFileService @Inject constructor( Timber.v("Response size ${response.body?.contentLength()} - Stream available: ${!source.exhausted()}") if (elementToDecrypt != null) { - Timber.v("## decrypt file") - val decryptedStream = MXEncryptedAttachments.decryptAttachment(source.inputStream(), elementToDecrypt) + Timber.v("## FileService: decrypt file") + val decryptSuccess = MXEncryptedAttachments.decryptAttachment( + source.inputStream(), + elementToDecrypt, + destFile.outputStream().buffered() + ) response.close() - if (decryptedStream == null) { + if (!decryptSuccess) { return@flatMap Try.Failure(IllegalStateException("Decryption error")) - } else { - decryptedStream.use { - writeToFile(decryptedStream, destFile) - } } } else { writeToFile(source.inputStream(), destFile) response.close() } + } else { + Timber.v("## FileService: cache hit for $url") } Try.just(copyFile(destFile, downloadMode)) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/DefaultContentUploadStateTracker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/DefaultContentUploadStateTracker.kt index aa8b98ae62..951c24ccb7 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/DefaultContentUploadStateTracker.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/DefaultContentUploadStateTracker.kt @@ -74,8 +74,8 @@ internal class DefaultContentUploadStateTracker @Inject constructor() : ContentU updateState(key, progressData) } - internal fun setEncrypting(key: String) { - val progressData = ContentUploadStateTracker.State.Encrypting + internal fun setEncrypting(key: String, current: Long, total: Long) { + val progressData = ContentUploadStateTracker.State.Encrypting(current, total) updateState(key, progressData) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/FileUploader.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/FileUploader.kt index 5e5380fce1..d52794a5c0 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/FileUploader.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/FileUploader.kt @@ -23,12 +23,14 @@ import com.squareup.moshi.Moshi import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import okhttp3.HttpUrl.Companion.toHttpUrlOrNull +import okhttp3.MediaType import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.RequestBody -import okhttp3.RequestBody.Companion.asRequestBody import okhttp3.RequestBody.Companion.toRequestBody +import okio.BufferedSink +import okio.source import org.greenrobot.eventbus.EventBus import org.matrix.android.sdk.api.session.content.ContentUrlResolver import org.matrix.android.sdk.internal.di.Authenticated @@ -38,6 +40,7 @@ import org.matrix.android.sdk.internal.network.toFailure import java.io.File import java.io.FileNotFoundException import java.io.IOException +import java.io.InputStream import javax.inject.Inject internal class FileUploader @Inject constructor(@Authenticated @@ -54,7 +57,21 @@ internal class FileUploader @Inject constructor(@Authenticated filename: String?, mimeType: String?, progressListener: ProgressRequestBody.Listener? = null): ContentUploadResponse { - val uploadBody = file.asRequestBody(mimeType?.toMediaTypeOrNull()) + val uploadBody = object : RequestBody() { + override fun contentLength() = file.length() + + // Disable okhttp auto resend for 'large files' + override fun isOneShot() = contentLength() == 0L || contentLength() >= 1_000_000 + + override fun contentType(): MediaType? { + return mimeType?.toMediaTypeOrNull() + } + + override fun writeTo(sink: BufferedSink) { + file.source().use { sink.writeAll(it) } + } + } + return upload(uploadBody, filename, progressListener) } @@ -66,6 +83,28 @@ internal class FileUploader @Inject constructor(@Authenticated return upload(uploadBody, filename, progressListener) } + suspend fun uploadInputStream(inputStream: InputStream, + filename: String?, + mimeType: String?, + progressListener: ProgressRequestBody.Listener? = null): ContentUploadResponse { + val length = inputStream.available().toLong() + val uploadBody = object : RequestBody() { + override fun contentLength() = length + + // Disable okhttp auto resend for 'large files' + override fun isOneShot() = contentLength() == 0L || contentLength() >= 1_000_000 + + override fun contentType(): MediaType? { + return mimeType?.toMediaTypeOrNull() + } + + override fun writeTo(sink: BufferedSink) { + inputStream.source().use { sink.writeAll(it) } + } + } + return upload(uploadBody, filename, progressListener) + } + suspend fun uploadFromUri(uri: Uri, filename: String?, mimeType: String?, @@ -73,10 +112,7 @@ internal class FileUploader @Inject constructor(@Authenticated return withContext(Dispatchers.IO) { val inputStream = context.contentResolver.openInputStream(uri) ?: throw FileNotFoundException() - inputStream.use { - uploadByteArray(it.readBytes(), filename, mimeType, progressListener) - } - } + return uploadInputStream(inputStream, filename, mimeType, progressListener) } private suspend fun upload(uploadBody: RequestBody, filename: String?, progressListener: ProgressRequestBody.Listener?): ContentUploadResponse { val urlBuilder = uploadUrl.toHttpUrlOrNull()?.newBuilder() ?: throw RuntimeException() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt index 720269404f..8ad1945f91 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt @@ -24,6 +24,7 @@ import androidx.work.WorkerParameters import com.squareup.moshi.JsonClass import id.zelory.compressor.Compressor import id.zelory.compressor.constraint.default +import org.matrix.android.sdk.api.extensions.tryThis import org.matrix.android.sdk.api.session.content.ContentAttachmentData import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.toContent @@ -37,12 +38,15 @@ import org.matrix.android.sdk.internal.crypto.attachments.MXEncryptedAttachments import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileInfo import org.matrix.android.sdk.internal.network.ProgressRequestBody import org.matrix.android.sdk.internal.session.DefaultFileService +import org.matrix.android.sdk.internal.session.room.send.CancelSendTracker import org.matrix.android.sdk.internal.session.room.send.MultipleEventSendingDispatcherWorker import org.matrix.android.sdk.internal.worker.SessionWorkerParams import org.matrix.android.sdk.internal.worker.WorkerParamsFactory import org.matrix.android.sdk.internal.worker.getSessionComponent import timber.log.Timber import java.io.File +import java.io.FileOutputStream +import java.io.InputStream import java.util.UUID import javax.inject.Inject @@ -71,6 +75,7 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter @Inject lateinit var fileUploader: FileUploader @Inject lateinit var contentUploadStateTracker: DefaultContentUploadStateTracker @Inject lateinit var fileService: DefaultFileService + @Inject lateinit var cancelSendTracker: CancelSendTracker override suspend fun doWork(): Result { val params = WorkerParamsFactory.fromData(inputData) @@ -102,6 +107,13 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter var newImageAttributes: NewImageAttributes? = null + val allCancelled = params.events.all { cancelSendTracker.isCancelRequestedFor(it.eventId, it.roomId) } + if (allCancelled) { + // there is no point in uploading the image! + return Result.success(inputData) + .also { Timber.e("## Send: Work cancelled by user") } + } + try { val inputStream = context.contentResolver.openInputStream(attachment.queryUri) ?: return Result.success( @@ -112,16 +124,16 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter ) ) - inputStream.use { - var uploadedThumbnailUrl: String? = null - var uploadedThumbnailEncryptedFileInfo: EncryptedFileInfo? = null +// inputStream.use { + var uploadedThumbnailUrl: String? = null + var uploadedThumbnailEncryptedFileInfo: EncryptedFileInfo? = null - ThumbnailExtractor.extractThumbnail(context, params.attachment)?.let { thumbnailData -> - val thumbnailProgressListener = object : ProgressRequestBody.Listener { - override fun onProgress(current: Long, total: Long) { - notifyTracker(params) { contentUploadStateTracker.setProgressThumbnail(it, current, total) } - } + ThumbnailExtractor.extractThumbnail(context, params.attachment)?.let { thumbnailData -> + val thumbnailProgressListener = object : ProgressRequestBody.Listener { + override fun onProgress(current: Long, total: Long) { + notifyTracker(params) { contentUploadStateTracker.setProgressThumbnail(it, current, total) } } + } try { val contentUploadResponse = if (params.isEncrypted) { @@ -140,27 +152,30 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter thumbnailProgressListener) } - uploadedThumbnailUrl = contentUploadResponse.contentUri - } catch (t: Throwable) { - Timber.e(t, "Thumbnail update failed") - } + uploadedThumbnailUrl = contentUploadResponse.contentUri + } catch (t: Throwable) { + Timber.e(t, "Thumbnail update failed") } + } - val progressListener = object : ProgressRequestBody.Listener { - override fun onProgress(current: Long, total: Long) { - notifyTracker(params) { - if (isStopped) { - contentUploadStateTracker.setFailure(it, Throwable("Cancelled")) - } else { - contentUploadStateTracker.setProgress(it, current, total) - } + val progressListener = object : ProgressRequestBody.Listener { + override fun onProgress(current: Long, total: Long) { + notifyTracker(params) { + if (isStopped) { + contentUploadStateTracker.setFailure(it, Throwable("Cancelled")) + } else { + contentUploadStateTracker.setProgress(it, current, total) } } } + } - var uploadedFileEncryptedFileInfo: EncryptedFileInfo? = null + var uploadedFileEncryptedFileInfo: EncryptedFileInfo? = null - return try { + return try { + var modifiedStream: InputStream + + if (attachment.type == ContentAttachmentData.Type.IMAGE && params.compressBeforeSending) { // Compressor library works with File instead of Uri for now. Since Scoped Storage doesn't allow us to access files directly, we should // copy it to a cache folder by using InputStream and OutputStream. // https://github.com/zetbaitsu/Compressor/pull/150 @@ -178,58 +193,86 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter inputStream.copyTo(outputStream) } - if (attachment.type == ContentAttachmentData.Type.IMAGE && params.compressBeforeSending) { - cacheFile = Compressor.compress(context, cacheFile) { - default( - width = MAX_IMAGE_SIZE, - height = MAX_IMAGE_SIZE - ) - }.also { compressedFile -> - val options = BitmapFactory.Options().apply { inJustDecodeBounds = true } - BitmapFactory.decodeFile(compressedFile.absolutePath, options) - val fileSize = compressedFile.length().toInt() - newImageAttributes = NewImageAttributes( - options.outWidth, - options.outHeight, - fileSize - ) + val compressedFile = Compressor.compress(context, cacheFile) { + default( + width = MAX_IMAGE_SIZE, + height = MAX_IMAGE_SIZE + ) + } + + val options = BitmapFactory.Options().apply { inJustDecodeBounds = true } + BitmapFactory.decodeFile(compressedFile.absolutePath, options) + val fileSize = compressedFile.length().toInt() + newImageAttributes = NewImageAttributes( + options.outWidth, + options.outHeight, + fileSize + ) + modifiedStream = compressedFile.inputStream() + } else { + // Unfortunatly the original stream is not always able to provide content length + // by passing by a temp copy it's working (better experience for upload progress..) + modifiedStream = if (tryThis { inputStream.available() } ?: 0 <= 0) { + val tmp = File.createTempFile(UUID.randomUUID().toString(), null, context.cacheDir) + tmp.outputStream().use { + inputStream.copyTo(it) } - } + tmp.inputStream() + } else inputStream + } - val contentUploadResponse = if (params.isEncrypted) { - Timber.v("Encrypt file") - notifyTracker(params) { contentUploadStateTracker.setEncrypting(it) } + val contentUploadResponse = if (params.isEncrypted) { + Timber.v("## FileService: Encrypt file") - val encryptionResult = MXEncryptedAttachments.encryptAttachment(cacheFile.inputStream(), attachment.getSafeMimeType()) - uploadedFileEncryptedFileInfo = encryptionResult.encryptedFileInfo + val tmpEncrypted = File.createTempFile(UUID.randomUUID().toString(), null, context.cacheDir) - fileUploader - .uploadByteArray(encryptionResult.encryptedByteArray, attachment.name, "application/octet-stream", progressListener) - } else { - fileUploader - .uploadFile(cacheFile, attachment.name, attachment.getSafeMimeType(), progressListener) - } + uploadedFileEncryptedFileInfo = + MXEncryptedAttachments.encrypt(modifiedStream, attachment.getSafeMimeType(), tmpEncrypted) { read, total -> + notifyTracker(params) { + contentUploadStateTracker.setEncrypting(it, read.toLong(), total.toLong()) + } + } - // If it's a file update the file service so that it does not redownload? - if (params.attachment.type == ContentAttachmentData.Type.FILE) { + Timber.v("## FileService: Uploading file") + + fileUploader + .uploadFile(tmpEncrypted, attachment.name, "application/octet-stream", progressListener) + .also { + // we can delete? + tryThis { tmpEncrypted.delete() } + } + } else { + Timber.v("## FileService: Clear file") + fileUploader + .uploadInputStream(modifiedStream, attachment.name, attachment.getSafeMimeType(), progressListener) + } + + // If it's a file update the file service so that it does not redownload? +// if (params.attachment.type == ContentAttachmentData.Type.FILE) { + Timber.v("## FileService: Update cache storage for ${contentUploadResponse.contentUri}") + try { context.contentResolver.openInputStream(attachment.queryUri)?.let { fileService.storeDataFor(contentUploadResponse.contentUri, params.attachment.getSafeMimeType(), it) } + Timber.v("## FileService: cache storage updated") + } catch (failure: Throwable) { + Timber.e(failure, "## FileService: Failed to update fileservice cache") } +// } - handleSuccess(params, - contentUploadResponse.contentUri, - uploadedFileEncryptedFileInfo, - uploadedThumbnailUrl, - uploadedThumbnailEncryptedFileInfo, - newImageAttributes) - } catch (t: Throwable) { - Timber.e(t) - handleFailure(params, t) - } + handleSuccess(params, + contentUploadResponse.contentUri, + uploadedFileEncryptedFileInfo, + uploadedThumbnailUrl, + uploadedThumbnailEncryptedFileInfo, + newImageAttributes) + } catch (t: Throwable) { + Timber.e(t, "## FileService: ERROR ${t.localizedMessage}") + handleFailure(params, t) } +// } } catch (e: Exception) { - Timber.e(e) + Timber.e(e, "## FileService: ERROR") notifyTracker(params) { contentUploadStateTracker.setFailure(it, e) } return Result.success( WorkerParamsFactory.toData( @@ -259,7 +302,6 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter thumbnailUrl: String?, thumbnailEncryptedFileInfo: EncryptedFileInfo?, newImageAttributes: NewImageAttributes?): Result { - Timber.v("handleSuccess $attachmentUrl, work is stopped $isStopped") notifyTracker(params) { contentUploadStateTracker.setSuccess(it) } val updatedEvents = params.events @@ -268,7 +310,9 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter } val sendParams = MultipleEventSendingDispatcherWorker.Params(params.sessionId, updatedEvents, params.isEncrypted) - return Result.success(WorkerParamsFactory.toData(sendParams)) + return Result.success(WorkerParamsFactory.toData(sendParams)).also { + Timber.v("## handleSuccess $attachmentUrl, work is stopped $isStopped") + } } private fun updateEvent(event: Event, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/download/DefaultContentDownloadStateTracker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/download/DefaultContentDownloadStateTracker.kt index 295a829b08..c4ba95af84 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/download/DefaultContentDownloadStateTracker.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/download/DefaultContentDownloadStateTracker.kt @@ -61,19 +61,23 @@ internal class DefaultContentDownloadStateTracker @Inject constructor() : Progre // private fun URL.toKey() = toString() override fun update(url: String, bytesRead: Long, contentLength: Long, done: Boolean) { - Timber.v("## DL Progress url:$url read:$bytesRead total:$contentLength done:$done") - if (done) { - updateState(url, ContentDownloadStateTracker.State.Success) - } else { - updateState(url, ContentDownloadStateTracker.State.Downloading(bytesRead, contentLength, contentLength == -1L)) + mainHandler.post { + Timber.v("## DL Progress url:$url read:$bytesRead total:$contentLength done:$done") + if (done) { + updateState(url, ContentDownloadStateTracker.State.Success) + } else { + updateState(url, ContentDownloadStateTracker.State.Downloading(bytesRead, contentLength, contentLength == -1L)) + } } } override fun error(url: String, errorCode: Int) { - Timber.v("## DL Progress Error code:$errorCode") - updateState(url, ContentDownloadStateTracker.State.Failure(errorCode)) - listeners[url]?.forEach { - tryThis { it.onDownloadStateUpdate(ContentDownloadStateTracker.State.Failure(errorCode)) } + mainHandler.post { + Timber.v("## DL Progress Error code:$errorCode") + updateState(url, ContentDownloadStateTracker.State.Failure(errorCode)) + listeners[url]?.forEach { + tryThis { it.onDownloadStateUpdate(ContentDownloadStateTracker.State.Failure(errorCode)) } + } } } 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/org/matrix/android/sdk/internal/session/room/send/CancelSendTracker.kt similarity index 94% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/CancelSendTracker.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/CancelSendTracker.kt index fb7145c7c0..fe3aad245a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/CancelSendTracker.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/CancelSendTracker.kt @@ -14,9 +14,9 @@ * limitations under the License. */ -package im.vector.matrix.android.internal.session.room.send +package org.matrix.android.sdk.internal.session.room.send -import im.vector.matrix.android.internal.session.SessionScope +import org.matrix.android.sdk.internal.session.SessionScope import javax.inject.Inject /** diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt index d6fa6775ee..eca0778401 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt @@ -17,6 +17,7 @@ package org.matrix.android.sdk.internal.session.room.send +import android.net.Uri import androidx.work.BackoffPolicy import androidx.work.ExistingWorkPolicy import androidx.work.OneTimeWorkRequest @@ -26,7 +27,6 @@ import com.squareup.inject.assisted.AssistedInject import org.matrix.android.sdk.api.session.content.ContentAttachmentData import org.matrix.android.sdk.api.session.crypto.CryptoService import org.matrix.android.sdk.api.session.events.model.Event -import org.matrix.android.sdk.api.session.events.model.isImageMessage import org.matrix.android.sdk.api.session.events.model.isTextMessage import org.matrix.android.sdk.api.session.room.model.message.OptionItem import org.matrix.android.sdk.api.session.room.send.SendService @@ -45,6 +45,15 @@ import org.matrix.android.sdk.internal.worker.AlwaysSuccessfulWorker import org.matrix.android.sdk.internal.worker.WorkerParamsFactory import org.matrix.android.sdk.internal.worker.startChain import kotlinx.coroutines.launch +import org.matrix.android.sdk.api.session.events.model.isAttachmentMessage +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.room.model.message.MessageAudioContent +import org.matrix.android.sdk.api.session.room.model.message.MessageContent +import org.matrix.android.sdk.api.session.room.model.message.MessageFileContent +import org.matrix.android.sdk.api.session.room.model.message.MessageImageContent +import org.matrix.android.sdk.api.session.room.model.message.MessageVideoContent +import org.matrix.android.sdk.api.session.room.model.message.MessageWithAttachmentContent +import org.matrix.android.sdk.api.session.room.model.message.getFileUrl import timber.log.Timber import java.util.concurrent.Executors import java.util.concurrent.TimeUnit @@ -60,7 +69,8 @@ internal class DefaultSendService @AssistedInject constructor( private val cryptoService: CryptoService, private val taskExecutor: TaskExecutor, private val localEchoRepository: LocalEchoRepository, - private val roomEventSender: RoomEventSender + private val roomEventSender: RoomEventSender, + private val cancelSendTracker: CancelSendTracker ) : SendService { @AssistedInject.Factory @@ -136,36 +146,72 @@ internal class DefaultSendService @AssistedInject constructor( } override fun resendMediaMessage(localEcho: TimelineEvent): Cancelable? { - if (localEcho.root.isImageMessage() && localEcho.root.sendState.hasFailed()) { + if (localEcho.root.sendState.hasFailed()) { // TODO this need a refactoring of attachement sending -// val clearContent = localEcho.root.getClearContent() -// val messageContent = clearContent?.toModel() ?: return null -// when (messageContent.type) { -// MessageType.MSGTYPE_IMAGE -> { -// val imageContent = clearContent.toModel() ?: return null -// val url = imageContent.url ?: return null -// if (url.startsWith("mxc://")) { -// //TODO -// } else { -// //The image has not yet been sent -// val attachmentData = ContentAttachmentData( -// size = imageContent.info!!.size.toLong(), -// mimeType = imageContent.info.mimeType!!, -// width = imageContent.info.width.toLong(), -// height = imageContent.info.height.toLong(), -// name = imageContent.body, -// path = imageContent.url, -// type = ContentAttachmentData.Type.IMAGE -// ) -// monarchy.runTransactionSync { -// EventEntity.where(it,eventId = localEcho.root.eventId ?: "").findFirst()?.let { -// it.sendState = SendState.UNSENT -// } -// } -// return internalSendMedia(localEcho.root,attachmentData) -// } -// } -// } + val clearContent = localEcho.root.getClearContent() + val messageContent = clearContent?.toModel() as? MessageWithAttachmentContent ?: return null + + val url = messageContent.getFileUrl() ?: return null + if (url.startsWith("mxc://")) { + // We need to resend only the message as the attachment is ok + localEchoRepository.updateSendState(localEcho.eventId, SendState.UNSENT) + return sendEvent(localEcho.root) + } + // we need to resend the media + + when (messageContent) { + is MessageImageContent -> { + // The image has not yet been sent + val attachmentData = ContentAttachmentData( + size = messageContent.info!!.size.toLong(), + mimeType = messageContent.info.mimeType!!, + width = messageContent.info.width.toLong(), + height = messageContent.info.height.toLong(), + name = messageContent.body, + queryUri = Uri.parse(messageContent.url), + type = ContentAttachmentData.Type.IMAGE + ) + localEchoRepository.updateSendState(localEcho.eventId, SendState.UNSENT) + return internalSendMedia(listOf(localEcho.root), attachmentData, true) + } + is MessageVideoContent -> { + val attachmentData = ContentAttachmentData( + size = messageContent.videoInfo?.size ?: 0L, + mimeType = messageContent.mimeType, + width = messageContent.videoInfo?.width?.toLong(), + height = messageContent.videoInfo?.height?.toLong(), + duration = messageContent.videoInfo?.duration?.toLong(), + name = messageContent.body, + queryUri = Uri.parse(messageContent.url), + type = ContentAttachmentData.Type.VIDEO + ) + localEchoRepository.updateSendState(localEcho.eventId, SendState.UNSENT) + return internalSendMedia(listOf(localEcho.root), attachmentData, true) + } + is MessageFileContent -> { + val attachmentData = ContentAttachmentData( + size = messageContent.info!!.size, + mimeType = messageContent.info.mimeType!!, + name = messageContent.body, + queryUri = Uri.parse(messageContent.url), + type = ContentAttachmentData.Type.FILE + ) + localEchoRepository.updateSendState(localEcho.eventId, SendState.UNSENT) + return internalSendMedia(listOf(localEcho.root), attachmentData, true) + } + is MessageAudioContent -> { + val attachmentData = ContentAttachmentData( + size = messageContent.audioInfo?.size ?: 0, + duration = messageContent.audioInfo?.duration?.toLong() ?: 0L, + mimeType = messageContent.audioInfo?.mimeType, + name = messageContent.body, + queryUri = Uri.parse(messageContent.url), + type = ContentAttachmentData.Type.AUDIO + ) + localEchoRepository.updateSendState(localEcho.eventId, SendState.UNSENT) + return internalSendMedia(listOf(localEcho.root), attachmentData, true) + } + } return null } return null @@ -196,16 +242,34 @@ internal class DefaultSendService @AssistedInject constructor( } } + override fun cancelSend(eventId: String) { + cancelSendTracker.markLocalEchoForCancel(eventId, roomId) + taskExecutor.executorScope.launch { + localEchoRepository.deleteFailedEcho(roomId, eventId) + } + } + override fun resendAllFailedMessages() { taskExecutor.executorScope.launch { val eventsToResend = localEchoRepository.getAllFailedEventsToResend(roomId) eventsToResend.forEach { - sendEvent(it) + if (it.root.isTextMessage()) { + resendTextMessage(it) + } else if (it.root.isAttachmentMessage()) { + resendMediaMessage(it) + } } - localEchoRepository.updateSendState(roomId, eventsToResend.mapNotNull { it.eventId }, SendState.UNSENT) + localEchoRepository.updateSendState(roomId, eventsToResend.map { it.eventId }, SendState.UNSENT) } } +// override fun failAllPendingMessages() { +// taskExecutor.executorScope.launch { +// val eventsToResend = localEchoRepository.getAllEventsWithStates(roomId, SendState.PENDING_STATES) +// localEchoRepository.updateSendState(roomId, eventsToResend.map { it.eventId }, SendState.UNDELIVERED) +// } +// } + override fun sendMedia(attachment: ContentAttachmentData, compressBeforeSending: Boolean, roomIds: Set): Cancelable { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/EncryptEventWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/EncryptEventWorker.kt index f878df52b2..6b2a2ab115 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/EncryptEventWorker.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/EncryptEventWorker.kt @@ -54,6 +54,7 @@ internal class EncryptEventWorker(context: Context, params: WorkerParameters) @Inject lateinit var crypto: CryptoService @Inject lateinit var localEchoRepository: LocalEchoRepository + @Inject lateinit var cancelSendTracker: CancelSendTracker override suspend fun doWork(): Result { Timber.v("Start Encrypt work") @@ -61,7 +62,7 @@ internal class EncryptEventWorker(context: Context, params: WorkerParameters) ?: return Result.success() .also { Timber.e("Unable to parse work parameters") } - Timber.v("Start Encrypt work for event ${params.event.eventId}") + Timber.v("## SendEvent: Start Encrypt work for event ${params.event.eventId}") if (params.lastFailureMessage != null) { // Transmit the error return Result.success(inputData) @@ -75,6 +76,12 @@ internal class EncryptEventWorker(context: Context, params: WorkerParameters) if (localEvent.eventId == null) { return Result.success() } + + if (cancelSendTracker.isCancelRequestedFor(localEvent.eventId, localEvent.roomId)) { + return Result.success() + .also { Timber.e("## SendEvent: Event sending has been cancelled ${localEvent.eventId}") } + } + localEchoRepository.updateSendState(localEvent.eventId, SendState.ENCRYPTING) val localMutableContent = localEvent.content?.toMutableMap() ?: mutableMapOf() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoRepository.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoRepository.kt index a9859136ad..b3188883c0 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoRepository.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoRepository.kt @@ -30,7 +30,6 @@ import org.matrix.android.sdk.internal.crypto.MXEventDecryptionResult import org.matrix.android.sdk.internal.database.helper.nextId import org.matrix.android.sdk.internal.database.mapper.ContentMapper import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper -import org.matrix.android.sdk.internal.database.mapper.asDomain import org.matrix.android.sdk.internal.database.mapper.toEntity import org.matrix.android.sdk.internal.database.model.EventEntity import org.matrix.android.sdk.internal.database.model.EventInsertEntity @@ -88,7 +87,7 @@ internal class LocalEchoRepository @Inject constructor(@SessionDatabase private } fun updateSendState(eventId: String, sendState: SendState) { - Timber.v("Update local state of $eventId to ${sendState.name}") + Timber.v("## SendEvent: [${System.currentTimeMillis()}] Update local state of $eventId to ${sendState.name}") monarchy.writeAsync { realm -> val sendingEventEntity = EventEntity.where(realm, eventId).findFirst() if (sendingEventEntity != null) { @@ -114,9 +113,13 @@ internal class LocalEchoRepository @Inject constructor(@SessionDatabase private } suspend fun deleteFailedEcho(roomId: String, localEcho: TimelineEvent) { + deleteFailedEcho(roomId, localEcho.eventId) + } + + suspend fun deleteFailedEcho(roomId: String, eventId: String?) { monarchy.awaitTransaction { realm -> - TimelineEventEntity.where(realm, roomId = roomId, eventId = localEcho.root.eventId ?: "").findFirst()?.deleteFromRealm() - EventEntity.where(realm, eventId = localEcho.root.eventId ?: "").findFirst()?.deleteFromRealm() + TimelineEventEntity.where(realm, roomId = roomId, eventId = eventId ?: "").findFirst()?.deleteFromRealm() + EventEntity.where(realm, eventId = eventId ?: "").findFirst()?.deleteFromRealm() roomSummaryUpdater.updateSendingInformation(realm, roomId) } } @@ -142,45 +145,47 @@ internal class LocalEchoRepository @Inject constructor(@SessionDatabase private } } - fun getAllFailedEventsToResend(roomId: String): List { + fun getAllFailedEventsToResend(roomId: String): List { + return getAllEventsWithStates(roomId, SendState.HAS_FAILED_STATES) + } + + fun getAllEventsWithStates(roomId: String, states : List): List { return Realm.getInstance(monarchy.realmConfiguration).use { realm -> TimelineEventEntity - .findAllInRoomWithSendStates(realm, roomId, SendState.HAS_FAILED_STATES) + .findAllInRoomWithSendStates(realm, roomId, states) .sortedByDescending { it.displayIndex } - .mapNotNull { it.root?.asDomain() } + .mapNotNull { it?.let { timelineEventMapper.map(it) } } .filter { event -> - when (event.getClearType()) { + when (event.root.getClearType()) { EventType.MESSAGE, EventType.REDACTION, EventType.REACTION -> { - val content = event.getClearContent().toModel() + val content = event.root.getClearContent().toModel() if (content != null) { when (content.msgType) { MessageType.MSGTYPE_EMOTE, MessageType.MSGTYPE_NOTICE, MessageType.MSGTYPE_LOCATION, - MessageType.MSGTYPE_TEXT -> { - true - } + MessageType.MSGTYPE_TEXT, MessageType.MSGTYPE_FILE, MessageType.MSGTYPE_VIDEO, MessageType.MSGTYPE_IMAGE, MessageType.MSGTYPE_AUDIO -> { // need to resend the attachment - false + true } else -> { - Timber.e("Cannot resend message ${event.type} / ${content.msgType}") + Timber.e("Cannot resend message ${event.root.getClearType()} / ${content.msgType}") false } } } else { - Timber.e("Unsupported message to resend ${event.type}") + Timber.e("Unsupported message to resend ${event.root.getClearType()}") false } } else -> { - Timber.e("Unsupported message to resend ${event.type}") + Timber.e("Unsupported message to resend ${event.root.getClearType()}") false } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/MultipleEventSendingDispatcherWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/MultipleEventSendingDispatcherWorker.kt index ead2dc9377..8e8d24c227 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/MultipleEventSendingDispatcherWorker.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/MultipleEventSendingDispatcherWorker.kt @@ -58,7 +58,7 @@ internal class MultipleEventSendingDispatcherWorker(context: Context, params: Wo @Inject lateinit var localEchoRepository: LocalEchoRepository override suspend fun doWork(): Result { - Timber.v("Start dispatch sending multiple event work") + Timber.v("## SendEvent: Start dispatch sending multiple event work") val params = WorkerParamsFactory.fromData(inputData) ?: return Result.success() .also { Timber.e("Unable to parse work parameters") } @@ -72,18 +72,21 @@ internal class MultipleEventSendingDispatcherWorker(context: Context, params: Wo } // Transmit the error if needed? return Result.success(inputData) - .also { Timber.e("Work cancelled due to input error from parent") } + .also { Timber.e("## SendEvent: Work cancelled due to input error from parent ${params.lastFailureMessage}") } } // Create a work for every event params.events.forEach { event -> if (params.isEncrypted) { - Timber.v("Send event in encrypted room") + localEchoRepository.updateSendState(event.eventId ?: "", SendState.ENCRYPTING) + Timber.v("## SendEvent: [${System.currentTimeMillis()}] Schedule encrypt and send event ${event.eventId}") 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 { + localEchoRepository.updateSendState(event.eventId ?: "", SendState.SENDING) + Timber.v("## SendEvent: [${System.currentTimeMillis()}] Schedule send event ${event.eventId}") val sendWork = createSendEventWork(params.sessionId, event, true) timelineSendEventWorkCommon.postWork(event.roomId!!, sendWork) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/RoomEventSender.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/RoomEventSender.kt index e46adeb9c1..6085459a08 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/RoomEventSender.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/RoomEventSender.kt @@ -39,13 +39,16 @@ internal class RoomEventSender @Inject constructor( ) { fun sendEvent(event: Event): Cancelable { // Encrypted room handling - return if (cryptoService.isRoomEncrypted(event.roomId ?: "")) { - Timber.v("Send event in encrypted room") + return if (cryptoService.isRoomEncrypted(event.roomId ?: "") + && !event.isEncrypted() // In case of resend where it's already encrypted so skip to send + ) { + Timber.v("## SendEvent: [${System.currentTimeMillis()}] Schedule encrypt and send event ${event.eventId}") 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(event.roomId ?: "", encryptWork, sendWork) } else { + Timber.v("## SendEvent: [${System.currentTimeMillis()}] Schedule send event ${event.eventId}") val sendWork = createSendEventWork(event, true) timelineSendEventWorkCommon.postWork(event.roomId ?: "", sendWork) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/SendEventWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/SendEventWorker.kt index 5da14f0a41..2d1819288d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/SendEventWorker.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/SendEventWorker.kt @@ -34,7 +34,7 @@ import org.matrix.android.sdk.internal.worker.getSessionComponent import timber.log.Timber import javax.inject.Inject -private const val MAX_NUMBER_OF_RETRY_BEFORE_FAILING = 3 +// private const val MAX_NUMBER_OF_RETRY_BEFORE_FAILING = 3 /** * Possible previous worker: [EncryptEventWorker] or first worker @@ -56,12 +56,12 @@ internal class SendEventWorker(context: Context, @Inject lateinit var localEchoRepository: LocalEchoRepository @Inject lateinit var roomAPI: RoomAPI @Inject lateinit var eventBus: EventBus + @Inject lateinit var cancelSendTracker: CancelSendTracker override suspend fun doWork(): Result { val params = WorkerParamsFactory.fromData(inputData) ?: return Result.success() - .also { Timber.e("Unable to parse work parameters") } - + .also { Timber.e("## SendEvent: Unable to parse work parameters") } val sessionComponent = getSessionComponent(params.sessionId) ?: return Result.success() sessionComponent.inject(this) @@ -75,22 +75,32 @@ internal class SendEventWorker(context: Context, .also { Timber.e("Work cancelled due to bad input data") } } + if (cancelSendTracker.isCancelRequestedFor(params.eventId, params.roomId)) { + return Result.success() + .also { + cancelSendTracker.markCancelled(params.eventId, params.roomId) + Timber.e("## SendEvent: Event sending has been cancelled ${params.eventId}") + } + } + if (params.lastFailureMessage != null) { localEchoRepository.updateSendState(event.eventId, SendState.UNDELIVERED) // Transmit the error return Result.success(inputData) .also { Timber.e("Work cancelled due to input error from parent") } } + + Timber.v("## SendEvent: [${System.currentTimeMillis()}] Send event ${params.eventId}") return try { sendEvent(event.eventId, event.roomId, event.type, event.content) Result.success() } catch (exception: Throwable) { - // It does start from 0, we want it to stop if it fails the third time - val currentAttemptCount = runAttemptCount + 1 - if (currentAttemptCount >= MAX_NUMBER_OF_RETRY_BEFORE_FAILING || !exception.shouldBeRetried()) { - localEchoRepository.updateSendState(event.eventId, SendState.UNDELIVERED) + if (/*currentAttemptCount >= MAX_NUMBER_OF_RETRY_BEFORE_FAILING ||**/ !exception.shouldBeRetried()) { + Timber.e("## SendEvent: [${System.currentTimeMillis()}] Send event Failed cannot retry ${params.eventId} > ${exception.localizedMessage}") + localEchoRepository.updateSendState(params.eventId, SendState.UNDELIVERED) return Result.success() } else { + Timber.e("## SendEvent: [${System.currentTimeMillis()}] Send event Failed schedule retry ${params.eventId} > ${exception.localizedMessage}") Result.retry() } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt index b4c32c045e..a569b775a4 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt @@ -115,6 +115,7 @@ internal class DefaultTimeline( if (!results.isLoaded || !results.isValid) { return@OrderedRealmCollectionChangeListener } + Timber.v("## SendEvent: [${System.currentTimeMillis()}] DB update for room $roomId") handleUpdates(results, changeSet) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineSendEventWorkCommon.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineSendEventWorkCommon.kt index d3124b68ca..3bc6a85cfb 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineSendEventWorkCommon.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineSendEventWorkCommon.kt @@ -57,7 +57,7 @@ internal class TimelineSendEventWorkCommon @Inject constructor( } } - fun postWork(roomId: String, workRequest: OneTimeWorkRequest, policy: ExistingWorkPolicy = ExistingWorkPolicy.APPEND): Cancelable { + fun postWork(roomId: String, workRequest: OneTimeWorkRequest, policy: ExistingWorkPolicy = ExistingWorkPolicy.APPEND_OR_REPLACE): Cancelable { workManagerProvider.workManager .beginUniqueWork(buildWorkName(roomId), policy, workRequest) .enqueue() diff --git a/vector/src/main/java/im/vector/app/core/glide/VectorGlideModelLoader.kt b/vector/src/main/java/im/vector/app/core/glide/VectorGlideModelLoader.kt index a35ed5a7bb..2a17c2ca1b 100644 --- a/vector/src/main/java/im/vector/app/core/glide/VectorGlideModelLoader.kt +++ b/vector/src/main/java/im/vector/app/core/glide/VectorGlideModelLoader.kt @@ -26,8 +26,9 @@ import com.bumptech.glide.load.model.MultiModelLoaderFactory import com.bumptech.glide.signature.ObjectKey import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.features.media.ImageContentRenderer -import org.matrix.android.sdk.api.Matrix import okhttp3.OkHttpClient +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.session.file.FileService import timber.log.Timber import java.io.File import java.io.IOException diff --git a/vector/src/main/java/im/vector/app/features/home/AvatarRenderer.kt b/vector/src/main/java/im/vector/app/features/home/AvatarRenderer.kt index 5e1433a6fa..e15f1e90cb 100644 --- a/vector/src/main/java/im/vector/app/features/home/AvatarRenderer.kt +++ b/vector/src/main/java/im/vector/app/features/home/AvatarRenderer.kt @@ -34,6 +34,7 @@ import im.vector.app.core.glide.GlideApp import im.vector.app.core.glide.GlideRequest import im.vector.app.core.glide.GlideRequests import im.vector.app.core.utils.getColorFromUserId +import org.matrix.android.sdk.api.extensions.tryThis import org.matrix.android.sdk.api.session.content.ContentUrlResolver import org.matrix.android.sdk.api.util.MatrixItem import javax.inject.Inject @@ -58,7 +59,7 @@ class AvatarRenderer @Inject constructor(private val activeSessionHolder: Active fun clear(imageView: ImageView) { // It can be called after recycler view is destroyed, just silently catch - tryThis { GlideApp.with(imageView).clear(imageView) } + tryThis { GlideApp.with(imageView).clear(imageView) } } @UiThread 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 7a50894a44..f29ff4c330 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 @@ -61,7 +61,7 @@ import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.crypto.MXCryptoError import org.matrix.android.sdk.api.session.events.model.EventType -import org.matrix.android.sdk.api.session.events.model.isImageMessage +import org.matrix.android.sdk.api.session.events.model.isAttachmentMessage import org.matrix.android.sdk.api.session.events.model.isTextMessage import org.matrix.android.sdk.api.session.events.model.toContent import org.matrix.android.sdk.api.session.events.model.toModel 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 789025c538..80d968670a 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 @@ -35,6 +35,7 @@ import im.vector.app.features.settings.VectorPreferences import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupState import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.isAttachmentMessage import org.matrix.android.sdk.api.session.events.model.isTextMessage import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.room.model.message.MessageContent diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageImageVideoItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageImageVideoItem.kt index 53946e4bf6..87c7e17fd5 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageImageVideoItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageImageVideoItem.kt @@ -28,7 +28,6 @@ import im.vector.app.R import im.vector.app.core.glide.GlideApp import im.vector.app.features.home.room.detail.timeline.helper.ContentUploadStateTrackerBinder import im.vector.app.features.media.ImageContentRenderer -import im.vector.matrix.android.api.session.room.send.SendState @EpoxyModelClass(layout = R.layout.item_timeline_event_base) abstract class MessageImageVideoItem : AbsMessageItem() { diff --git a/vector/src/main/java/im/vector/app/features/media/ImageContentRenderer.kt b/vector/src/main/java/im/vector/app/features/media/ImageContentRenderer.kt index a7d666184d..0c709f98af 100644 --- a/vector/src/main/java/im/vector/app/features/media/ImageContentRenderer.kt +++ b/vector/src/main/java/im/vector/app/features/media/ImageContentRenderer.kt @@ -39,6 +39,7 @@ import im.vector.app.core.utils.isLocalFile import org.matrix.android.sdk.api.session.content.ContentUrlResolver import org.matrix.android.sdk.internal.crypto.attachments.ElementToDecrypt import kotlinx.android.parcel.Parcelize +import org.matrix.android.sdk.api.extensions.tryThis import timber.log.Timber import java.io.File import javax.inject.Inject From 4e7790966f96b0666f39dddea7b8c51d2a7996ab Mon Sep 17 00:00:00 2001 From: Valere Date: Mon, 17 Aug 2020 23:56:43 +0200 Subject: [PATCH 13/32] Always use temp file before sending --- .../session/content/UploadContentWorker.kt | 60 +++++++------------ 1 file changed, 20 insertions(+), 40 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt index 8ad1945f91..12ded1666b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt @@ -45,7 +45,6 @@ import org.matrix.android.sdk.internal.worker.WorkerParamsFactory import org.matrix.android.sdk.internal.worker.getSessionComponent import timber.log.Timber import java.io.File -import java.io.FileOutputStream import java.io.InputStream import java.util.UUID import javax.inject.Inject @@ -124,6 +123,12 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter ) ) + // always use a temporary file, it guaranties that we could report progress on upload and simplifies the flows + val workingFile = File.createTempFile(UUID.randomUUID().toString(), null, context.cacheDir) + workingFile.outputStream().use { + inputStream.copyTo(it) + } + // inputStream.use { var uploadedThumbnailUrl: String? = null var uploadedThumbnailEncryptedFileInfo: EncryptedFileInfo? = null @@ -173,27 +178,14 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter var uploadedFileEncryptedFileInfo: EncryptedFileInfo? = null return try { - var modifiedStream: InputStream + val streamToUpload: InputStream if (attachment.type == ContentAttachmentData.Type.IMAGE && params.compressBeforeSending) { // Compressor library works with File instead of Uri for now. Since Scoped Storage doesn't allow us to access files directly, we should // copy it to a cache folder by using InputStream and OutputStream. // https://github.com/zetbaitsu/Compressor/pull/150 // As soon as the above PR is merged, we can use attachment.queryUri instead of creating a cacheFile. - var cacheFile = File.createTempFile(attachment.name ?: UUID.randomUUID().toString(), ".jpg", context.cacheDir) - cacheFile.parentFile?.mkdirs() - if (cacheFile.exists()) { - cacheFile.delete() - } - cacheFile.createNewFile() - cacheFile.deleteOnExit() - - val outputStream = cacheFile.outputStream() - outputStream.use { - inputStream.copyTo(outputStream) - } - - val compressedFile = Compressor.compress(context, cacheFile) { + val compressedFile = Compressor.compress(context, workingFile) { default( width = MAX_IMAGE_SIZE, height = MAX_IMAGE_SIZE @@ -208,17 +200,9 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter options.outHeight, fileSize ) - modifiedStream = compressedFile.inputStream() + streamToUpload = compressedFile.inputStream() } else { - // Unfortunatly the original stream is not always able to provide content length - // by passing by a temp copy it's working (better experience for upload progress..) - modifiedStream = if (tryThis { inputStream.available() } ?: 0 <= 0) { - val tmp = File.createTempFile(UUID.randomUUID().toString(), null, context.cacheDir) - tmp.outputStream().use { - inputStream.copyTo(it) - } - tmp.inputStream() - } else inputStream + streamToUpload = workingFile.inputStream() } val contentUploadResponse = if (params.isEncrypted) { @@ -227,7 +211,7 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter val tmpEncrypted = File.createTempFile(UUID.randomUUID().toString(), null, context.cacheDir) uploadedFileEncryptedFileInfo = - MXEncryptedAttachments.encrypt(modifiedStream, attachment.getSafeMimeType(), tmpEncrypted) { read, total -> + MXEncryptedAttachments.encrypt(streamToUpload, attachment.getSafeMimeType(), tmpEncrypted) { read, total -> notifyTracker(params) { contentUploadStateTracker.setEncrypting(it, read.toLong(), total.toLong()) } @@ -244,21 +228,18 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter } else { Timber.v("## FileService: Clear file") fileUploader - .uploadInputStream(modifiedStream, attachment.name, attachment.getSafeMimeType(), progressListener) + .uploadInputStream(streamToUpload, attachment.name, attachment.getSafeMimeType(), progressListener) } - // If it's a file update the file service so that it does not redownload? -// if (params.attachment.type == ContentAttachmentData.Type.FILE) { - Timber.v("## FileService: Update cache storage for ${contentUploadResponse.contentUri}") - try { - context.contentResolver.openInputStream(attachment.queryUri)?.let { - fileService.storeDataFor(contentUploadResponse.contentUri, params.attachment.getSafeMimeType(), it) - } - Timber.v("## FileService: cache storage updated") - } catch (failure: Throwable) { - Timber.e(failure, "## FileService: Failed to update fileservice cache") + Timber.v("## FileService: Update cache storage for ${contentUploadResponse.contentUri}") + try { + context.contentResolver.openInputStream(attachment.queryUri)?.let { + fileService.storeDataFor(contentUploadResponse.contentUri, params.attachment.getSafeMimeType(), it) } -// } + Timber.v("## FileService: cache storage updated") + } catch (failure: Throwable) { + Timber.e(failure, "## FileService: Failed to update fileservice cache") + } handleSuccess(params, contentUploadResponse.contentUri, @@ -270,7 +251,6 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter Timber.e(t, "## FileService: ERROR ${t.localizedMessage}") handleFailure(params, t) } -// } } catch (e: Exception) { Timber.e(e, "## FileService: ERROR") notifyTracker(params) { contentUploadStateTracker.setFailure(it, e) } From 55dcba6f3689b0e4c2377923889542e1f7ec8d83 Mon Sep 17 00:00:00 2001 From: Valere Date: Tue, 18 Aug 2020 23:56:22 +0200 Subject: [PATCH 14/32] Fix progress of message with attachment --- .../helper/ContentUploadStateTrackerBinder.kt | 13 +++++++++---- .../room/detail/timeline/item/MessageFileItem.kt | 8 +++++++- .../detail/timeline/item/MessageImageVideoItem.kt | 8 +++++++- 3 files changed, 23 insertions(+), 6 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/ContentUploadStateTrackerBinder.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/ContentUploadStateTrackerBinder.kt index 0bd59bf2fc..8216d36ac9 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/ContentUploadStateTrackerBinder.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/ContentUploadStateTrackerBinder.kt @@ -77,7 +77,7 @@ private class ContentMediaProgressUpdater(private val progressLayout: ViewGroup, is ContentUploadStateTracker.State.UploadingThumbnail -> handleProgressThumbnail(state) is ContentUploadStateTracker.State.Encrypting -> handleEncrypting(state) is ContentUploadStateTracker.State.Uploading -> handleProgress(state) - is ContentUploadStateTracker.State.Failure -> handleFailure(state) + is ContentUploadStateTracker.State.Failure -> handleFailure(/*state*/) is ContentUploadStateTracker.State.Success -> handleSuccess() } } @@ -120,6 +120,7 @@ private class ContentMediaProgressUpdater(private val progressLayout: ViewGroup, val progressTextView = progressLayout.findViewById(R.id.mediaProgressTextView) progressBar?.isIndeterminate = false progressBar?.progress = percent.toInt() + progressTextView.isVisible = true progressTextView?.text = progressLayout.context.getString(resId) progressTextView?.setTextColor(messageColorProvider.getMessageTextColor(SendState.ENCRYPTING)) } @@ -132,19 +133,23 @@ private class ContentMediaProgressUpdater(private val progressLayout: ViewGroup, progressBar?.isVisible = true progressBar?.isIndeterminate = false progressBar?.progress = percent.toInt() + progressTextView.isVisible = true progressTextView?.text = progressLayout.context.getString(resId, TextUtils.formatFileSize(progressLayout.context, current, true), TextUtils.formatFileSize(progressLayout.context, total, true)) progressTextView?.setTextColor(messageColorProvider.getMessageTextColor(SendState.SENDING)) } - private fun handleFailure(state: ContentUploadStateTracker.State.Failure) { + private fun handleFailure(/*state: ContentUploadStateTracker.State.Failure*/) { progressLayout.visibility = View.VISIBLE val progressBar = progressLayout.findViewById(R.id.mediaProgressBar) val progressTextView = progressLayout.findViewById(R.id.mediaProgressTextView) progressBar?.isVisible = false - progressTextView?.text = errorFormatter.toHumanReadable(state.throwable) - progressTextView?.setTextColor(messageColorProvider.getMessageTextColor(SendState.UNDELIVERED)) + // Do not show the message it's too technical for users, and unfortunate when upload is cancelled + // in the middle by turning airplane mode for example + progressTextView.isVisible = false +// progressTextView?.text = errorFormatter.toHumanReadable(state.throwable) +// progressTextView?.setTextColor(messageColorProvider.getMessageTextColor(SendState.UNDELIVERED)) } private fun handleSuccess() { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageFileItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageFileItem.kt index 1f3c6147ae..5160a7cd84 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageFileItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageFileItem.kt @@ -28,6 +28,7 @@ import com.airbnb.epoxy.EpoxyModelClass import im.vector.app.R import im.vector.app.features.home.room.detail.timeline.helper.ContentDownloadStateTrackerBinder import im.vector.app.features.home.room.detail.timeline.helper.ContentUploadStateTrackerBinder +import org.matrix.android.sdk.api.session.room.send.SendState @EpoxyModelClass(layout = R.layout.item_timeline_event_base) abstract class MessageFileItem : AbsMessageItem() { @@ -87,7 +88,12 @@ abstract class MessageFileItem : AbsMessageItem() { holder.fileImageWrapper.setOnLongClickListener(attributes.itemLongClickListener) holder.filenameView.paintFlags = (holder.filenameView.paintFlags or Paint.UNDERLINE_TEXT_FLAG) - holder.eventSendingIndicator.isVisible = attributes.informationData.sendState.isInProgress() + holder.eventSendingIndicator.isVisible = when (attributes.informationData.sendState) { + SendState.UNSENT, + SendState.ENCRYPTING, + SendState.SENDING -> true + else -> false + } } override fun unbind(holder: Holder) { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageImageVideoItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageImageVideoItem.kt index 87c7e17fd5..9ada087207 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageImageVideoItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageImageVideoItem.kt @@ -28,6 +28,7 @@ import im.vector.app.R import im.vector.app.core.glide.GlideApp import im.vector.app.features.home.room.detail.timeline.helper.ContentUploadStateTrackerBinder import im.vector.app.features.media.ImageContentRenderer +import org.matrix.android.sdk.api.session.room.send.SendState @EpoxyModelClass(layout = R.layout.item_timeline_event_base) abstract class MessageImageVideoItem : AbsMessageItem() { @@ -62,7 +63,12 @@ abstract class MessageImageVideoItem : AbsMessageItem true + else -> false + } } override fun unbind(holder: Holder) { From dd09c4a72dd17e49d5a33d05f8a5e78d6a6bfb50 Mon Sep 17 00:00:00 2001 From: Valere Date: Mon, 31 Aug 2020 09:23:29 +0200 Subject: [PATCH 15/32] post rebase fix --- .../android/sdk/internal/crypto/AttachmentEncryptionTest.kt | 1 - .../android/sdk/internal/session/content/FileUploader.kt | 6 +++--- .../sdk/internal/session/room/send/SendEventWorker.kt | 6 +++--- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/AttachmentEncryptionTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/AttachmentEncryptionTest.kt index 476f67218b..ebaa235a2b 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/AttachmentEncryptionTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/AttachmentEncryptionTest.kt @@ -30,7 +30,6 @@ import org.matrix.android.sdk.internal.crypto.attachments.MXEncryptedAttachments import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileInfo import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileKey import org.matrix.android.sdk.internal.crypto.attachments.toElementToDecrypt -import java.io.ByteArrayInputStream import java.io.InputStream /** diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/FileUploader.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/FileUploader.kt index d52794a5c0..18d3558f4b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/FileUploader.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/FileUploader.kt @@ -109,9 +109,9 @@ internal class FileUploader @Inject constructor(@Authenticated filename: String?, mimeType: String?, progressListener: ProgressRequestBody.Listener? = null): ContentUploadResponse { - return withContext(Dispatchers.IO) { - val inputStream = context.contentResolver.openInputStream(uri) ?: throw FileNotFoundException() - + val inputStream = withContext(Dispatchers.IO) { + context.contentResolver.openInputStream(uri) + } ?: throw FileNotFoundException() return uploadInputStream(inputStream, filename, mimeType, progressListener) } private suspend fun upload(uploadBody: RequestBody, filename: String?, progressListener: ProgressRequestBody.Listener?): ContentUploadResponse { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/SendEventWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/SendEventWorker.kt index 2d1819288d..16acde7d16 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/SendEventWorker.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/SendEventWorker.kt @@ -75,10 +75,10 @@ internal class SendEventWorker(context: Context, .also { Timber.e("Work cancelled due to bad input data") } } - if (cancelSendTracker.isCancelRequestedFor(params.eventId, params.roomId)) { + if (cancelSendTracker.isCancelRequestedFor(params.eventId, event.roomId)) { return Result.success() .also { - cancelSendTracker.markCancelled(params.eventId, params.roomId) + cancelSendTracker.markCancelled(event.eventId, event.roomId) Timber.e("## SendEvent: Event sending has been cancelled ${params.eventId}") } } @@ -97,7 +97,7 @@ internal class SendEventWorker(context: Context, } catch (exception: Throwable) { if (/*currentAttemptCount >= MAX_NUMBER_OF_RETRY_BEFORE_FAILING ||**/ !exception.shouldBeRetried()) { Timber.e("## SendEvent: [${System.currentTimeMillis()}] Send event Failed cannot retry ${params.eventId} > ${exception.localizedMessage}") - localEchoRepository.updateSendState(params.eventId, SendState.UNDELIVERED) + localEchoRepository.updateSendState(event.eventId, SendState.UNDELIVERED) return Result.success() } else { Timber.e("## SendEvent: [${System.currentTimeMillis()}] Send event Failed schedule retry ${params.eventId} > ${exception.localizedMessage}") From bf4f86952450bf315028d9df59adfc16e23e42a0 Mon Sep 17 00:00:00 2001 From: Valere Date: Mon, 31 Aug 2020 16:25:40 +0200 Subject: [PATCH 16/32] rebase fix --- .../sdk/internal/crypto/attachments/MXEncryptedAttachments.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/attachments/MXEncryptedAttachments.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/attachments/MXEncryptedAttachments.kt index 68fce9462b..5672d195cc 100755 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/attachments/MXEncryptedAttachments.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/attachments/MXEncryptedAttachments.kt @@ -89,7 +89,7 @@ internal object MXEncryptedAttachments { key = EncryptedFileKey( alg = "A256CTR", ext = true, - key_ops = listOf("encrypt", "decrypt"), + keyOps = listOf("encrypt", "decrypt"), kty = "oct", k = base64ToBase64Url(Base64.encodeToString(key, Base64.DEFAULT)) ), @@ -210,7 +210,7 @@ internal object MXEncryptedAttachments { key = EncryptedFileKey( alg = "A256CTR", ext = true, - key_ops = listOf("encrypt", "decrypt"), + keyOps = listOf("encrypt", "decrypt"), kty = "oct", k = base64ToBase64Url(Base64.encodeToString(key, Base64.DEFAULT)) ), From 28081aa7d282ba542c1bd0d76a5c82f8050bccad Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 2 Sep 2020 12:03:03 +0200 Subject: [PATCH 17/32] Cleanup: rename parameters, make some fields private, add Javadoc, fix copy paste error --- .../android/sdk/api/session/room/send/SendService.kt | 6 ++++++ .../crypto/attachments/MatrixDigestCheckInputStream.kt | 7 +++++-- .../app/features/home/room/detail/RoomDetailViewModel.kt | 6 +++--- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendService.kt index de85438b1c..fd596de328 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendService.kt @@ -124,8 +124,14 @@ interface SendService { */ fun deleteFailedEcho(localEcho: TimelineEvent) + /** + * Delete all the events in one of the sending states + */ fun clearSendingQueue() + /** + * Cancel sending a specific event. It has to be in one of the sending states + */ fun cancelSend(eventId: String) /** diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/attachments/MatrixDigestCheckInputStream.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/attachments/MatrixDigestCheckInputStream.kt index 01de479ff5..9ae4703e05 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/attachments/MatrixDigestCheckInputStream.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/attachments/MatrixDigestCheckInputStream.kt @@ -22,9 +22,12 @@ import java.io.IOException import java.io.InputStream import java.security.MessageDigest -class MatrixDigestCheckInputStream(`in`: InputStream?, val expectedDigest: String) : FilterInputStream(`in`) { +class MatrixDigestCheckInputStream( + inputStream: InputStream?, + private val expectedDigest: String +) : FilterInputStream(inputStream) { - val digest = MessageDigest.getInstance("SHA-256") + private val digest = MessageDigest.getInstance("SHA-256") @Throws(IOException::class) override fun read(): Int { 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 f29ff4c330..ddb21b9f2f 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 @@ -1076,12 +1076,12 @@ class RoomDetailViewModel @AssistedInject constructor( private fun handleCancel(action: RoomDetailAction.CancelSend) { val targetEventId = action.eventId room.getTimeLineEvent(targetEventId)?.let { - // State must be UNDELIVERED or Failed + // State must be in one of the sending states if (!it.root.sendState.isSending()) { - Timber.e("Cannot resend message, it is not failed, Cancel first") + Timber.e("Cannot cancel message, it is not sending") return } - room.cancelSend(action.eventId) + room.cancelSend(targetEventId) } } From 76c79f9f75678f51b57a8e00a6100d5c0d5f3b94 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 2 Sep 2020 12:06:21 +0200 Subject: [PATCH 18/32] Move Base64 methods to a dedicated file --- .../attachments/MXEncryptedAttachments.kt | 24 ++---------- .../MatrixDigestCheckInputStream.kt | 3 +- .../identity/IdentityBulkLookupTask.kt | 2 +- .../android/sdk/internal/util/Base64.kt | 39 +++++++++++++++++++ 4 files changed, 45 insertions(+), 23 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/Base64.kt diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/attachments/MXEncryptedAttachments.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/attachments/MXEncryptedAttachments.kt index 5672d195cc..d159bcd512 100755 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/attachments/MXEncryptedAttachments.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/attachments/MXEncryptedAttachments.kt @@ -20,6 +20,9 @@ package org.matrix.android.sdk.internal.crypto.attachments import android.util.Base64 import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileInfo import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileKey +import org.matrix.android.sdk.internal.util.base64ToBase64Url +import org.matrix.android.sdk.internal.util.base64ToUnpaddedBase64 +import org.matrix.android.sdk.internal.util.base64UrlToBase64 import timber.log.Timber import java.io.ByteArrayOutputStream import java.io.File @@ -310,25 +313,4 @@ internal object MXEncryptedAttachments { return false } - - /** - * Base64 URL conversion methods - */ - - private fun base64UrlToBase64(base64Url: String): String { - return base64Url.replace('-', '+') - .replace('_', '/') - } - - internal fun base64ToBase64Url(base64: String): String { - return base64.replace("\n".toRegex(), "") - .replace("\\+".toRegex(), "-") - .replace('/', '_') - .replace("=", "") - } - - internal fun base64ToUnpaddedBase64(base64: String): String { - return base64.replace("\n".toRegex(), "") - .replace("=", "") - } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/attachments/MatrixDigestCheckInputStream.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/attachments/MatrixDigestCheckInputStream.kt index 9ae4703e05..7ca5158f64 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/attachments/MatrixDigestCheckInputStream.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/attachments/MatrixDigestCheckInputStream.kt @@ -17,6 +17,7 @@ package org.matrix.android.sdk.internal.crypto.attachments import android.util.Base64 +import org.matrix.android.sdk.internal.util.base64ToUnpaddedBase64 import java.io.FilterInputStream import java.io.IOException import java.io.InputStream @@ -60,7 +61,7 @@ class MatrixDigestCheckInputStream( @Throws(IOException::class) private fun ensureDigest() { - val currentDigestValue = MXEncryptedAttachments.base64ToUnpaddedBase64(Base64.encodeToString(digest.digest(), Base64.DEFAULT)) + val currentDigestValue = base64ToUnpaddedBase64(Base64.encodeToString(digest.digest(), Base64.DEFAULT)) if (currentDigestValue != expectedDigest) { throw IOException("Bad digest") } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityBulkLookupTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityBulkLookupTask.kt index ac33c2666f..45d7d48a18 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityBulkLookupTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityBulkLookupTask.kt @@ -23,7 +23,6 @@ import org.matrix.android.sdk.api.session.identity.FoundThreePid import org.matrix.android.sdk.api.session.identity.IdentityServiceError import org.matrix.android.sdk.api.session.identity.ThreePid import org.matrix.android.sdk.api.session.identity.toMedium -import org.matrix.android.sdk.internal.crypto.attachments.MXEncryptedAttachments.base64ToBase64Url import org.matrix.android.sdk.internal.crypto.tools.withOlmUtility import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.network.executeRequest @@ -32,6 +31,7 @@ import org.matrix.android.sdk.internal.session.identity.model.IdentityHashDetail import org.matrix.android.sdk.internal.session.identity.model.IdentityLookUpParams import org.matrix.android.sdk.internal.session.identity.model.IdentityLookUpResponse import org.matrix.android.sdk.internal.task.Task +import org.matrix.android.sdk.internal.util.base64ToBase64Url import java.util.Locale import javax.inject.Inject diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/Base64.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/Base64.kt new file mode 100644 index 0000000000..76e24c4e31 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/Base64.kt @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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 org.matrix.android.sdk.internal.util + +/** + * Base64 URL conversion methods + */ + +internal fun base64UrlToBase64(base64Url: String): String { + return base64Url.replace('-', '+') + .replace('_', '/') +} + +internal fun base64ToBase64Url(base64: String): String { + return base64.replace("\n".toRegex(), "") + .replace("\\+".toRegex(), "-") + .replace('/', '_') + .replace("=", "") +} + +internal fun base64ToUnpaddedBase64(base64: String): String { + return base64.replace("\n".toRegex(), "") + .replace("=", "") +} From 6d24aa75d072e806363d07a0a27b2840f81377fe Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 2 Sep 2020 12:25:50 +0200 Subject: [PATCH 19/32] Format file (no other change) --- .../internal/session/content/FileUploader.kt | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/FileUploader.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/FileUploader.kt index 18d3558f4b..0ba9bf3541 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/FileUploader.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/FileUploader.kt @@ -57,11 +57,11 @@ internal class FileUploader @Inject constructor(@Authenticated filename: String?, mimeType: String?, progressListener: ProgressRequestBody.Listener? = null): ContentUploadResponse { - val uploadBody = object : RequestBody() { + val uploadBody = object : RequestBody() { override fun contentLength() = file.length() // Disable okhttp auto resend for 'large files' - override fun isOneShot() = contentLength() == 0L || contentLength() >= 1_000_000 + override fun isOneShot() = contentLength() == 0L || contentLength() >= 1_000_000 override fun contentType(): MediaType? { return mimeType?.toMediaTypeOrNull() @@ -84,22 +84,22 @@ internal class FileUploader @Inject constructor(@Authenticated } suspend fun uploadInputStream(inputStream: InputStream, - filename: String?, - mimeType: String?, - progressListener: ProgressRequestBody.Listener? = null): ContentUploadResponse { + filename: String?, + mimeType: String?, + progressListener: ProgressRequestBody.Listener? = null): ContentUploadResponse { val length = inputStream.available().toLong() - val uploadBody = object : RequestBody() { + val uploadBody = object : RequestBody() { override fun contentLength() = length // Disable okhttp auto resend for 'large files' - override fun isOneShot() = contentLength() == 0L || contentLength() >= 1_000_000 + override fun isOneShot() = contentLength() == 0L || contentLength() >= 1_000_000 override fun contentType(): MediaType? { - return mimeType?.toMediaTypeOrNull() + return mimeType?.toMediaTypeOrNull() } override fun writeTo(sink: BufferedSink) { - inputStream.source().use { sink.writeAll(it) } + inputStream.source().use { sink.writeAll(it) } } } return upload(uploadBody, filename, progressListener) @@ -109,11 +109,12 @@ internal class FileUploader @Inject constructor(@Authenticated filename: String?, mimeType: String?, progressListener: ProgressRequestBody.Listener? = null): ContentUploadResponse { - val inputStream = withContext(Dispatchers.IO) { + val inputStream = withContext(Dispatchers.IO) { context.contentResolver.openInputStream(uri) } ?: throw FileNotFoundException() return uploadInputStream(inputStream, filename, mimeType, progressListener) } + private suspend fun upload(uploadBody: RequestBody, filename: String?, progressListener: ProgressRequestBody.Listener?): ContentUploadResponse { val urlBuilder = uploadUrl.toHttpUrlOrNull()?.newBuilder() ?: throw RuntimeException() From 53744982f00b4cb6055ca71eafd68072616783ec Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 2 Sep 2020 12:29:18 +0200 Subject: [PATCH 20/32] Update Javadoc --- .../internal/crypto/attachments/MXEncryptedAttachments.kt | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/attachments/MXEncryptedAttachments.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/attachments/MXEncryptedAttachments.kt index d159bcd512..df1d39c250 100755 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/attachments/MXEncryptedAttachments.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/attachments/MXEncryptedAttachments.kt @@ -229,8 +229,8 @@ internal object MXEncryptedAttachments { /** * Decrypt an attachment * - * @param attachmentStream the attachment stream. Will be closed after this method call. - * @param encryptedFileInfo the encryption file info + * @param attachmentStream the attachment stream. Will be closed after this method call. + * @param elementToDecrypt the element to decrypt the file * @return the decrypted attachment stream */ fun decryptAttachment(attachmentStream: InputStream?, elementToDecrypt: ElementToDecrypt): InputStream? { @@ -257,7 +257,8 @@ internal object MXEncryptedAttachments { * * @param attachmentStream the attachment stream. Will be closed after this method call. * @param elementToDecrypt the elementToDecrypt info - * @return the decrypted attachment stream + * @param outputStream the outputStream where the decrypted attachment will be write. + * @return true in case of success, false in case of error */ fun decryptAttachment(attachmentStream: InputStream?, elementToDecrypt: ElementToDecrypt?, outputStream: OutputStream): Boolean { // sanity checks From 95219c7934f4edaf5db1b93a5f0273ef7923330a Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 2 Sep 2020 12:29:39 +0200 Subject: [PATCH 21/32] typo --- .../session/room/send/DefaultSendService.kt | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt index eca0778401..e20ab79ee6 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt @@ -24,11 +24,21 @@ import androidx.work.OneTimeWorkRequest import androidx.work.Operation import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject +import kotlinx.coroutines.launch import org.matrix.android.sdk.api.session.content.ContentAttachmentData import org.matrix.android.sdk.api.session.crypto.CryptoService import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.isAttachmentMessage import org.matrix.android.sdk.api.session.events.model.isTextMessage +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.room.model.message.MessageAudioContent +import org.matrix.android.sdk.api.session.room.model.message.MessageContent +import org.matrix.android.sdk.api.session.room.model.message.MessageFileContent +import org.matrix.android.sdk.api.session.room.model.message.MessageImageContent +import org.matrix.android.sdk.api.session.room.model.message.MessageVideoContent +import org.matrix.android.sdk.api.session.room.model.message.MessageWithAttachmentContent import org.matrix.android.sdk.api.session.room.model.message.OptionItem +import org.matrix.android.sdk.api.session.room.model.message.getFileUrl import org.matrix.android.sdk.api.session.room.send.SendService import org.matrix.android.sdk.api.session.room.send.SendState import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent @@ -44,16 +54,6 @@ import org.matrix.android.sdk.internal.util.CancelableWork import org.matrix.android.sdk.internal.worker.AlwaysSuccessfulWorker import org.matrix.android.sdk.internal.worker.WorkerParamsFactory import org.matrix.android.sdk.internal.worker.startChain -import kotlinx.coroutines.launch -import org.matrix.android.sdk.api.session.events.model.isAttachmentMessage -import org.matrix.android.sdk.api.session.events.model.toModel -import org.matrix.android.sdk.api.session.room.model.message.MessageAudioContent -import org.matrix.android.sdk.api.session.room.model.message.MessageContent -import org.matrix.android.sdk.api.session.room.model.message.MessageFileContent -import org.matrix.android.sdk.api.session.room.model.message.MessageImageContent -import org.matrix.android.sdk.api.session.room.model.message.MessageVideoContent -import org.matrix.android.sdk.api.session.room.model.message.MessageWithAttachmentContent -import org.matrix.android.sdk.api.session.room.model.message.getFileUrl import timber.log.Timber import java.util.concurrent.Executors import java.util.concurrent.TimeUnit @@ -147,7 +147,7 @@ internal class DefaultSendService @AssistedInject constructor( override fun resendMediaMessage(localEcho: TimelineEvent): Cancelable? { if (localEcho.root.sendState.hasFailed()) { - // TODO this need a refactoring of attachement sending + // TODO this need a refactoring of attachment sending val clearContent = localEcho.root.getClearContent() val messageContent = clearContent?.toModel() as? MessageWithAttachmentContent ?: return null From e5e67fbcbbf83c90e9d902bc720a929792e5a391 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 2 Sep 2020 12:29:53 +0200 Subject: [PATCH 22/32] Internal class and Copyright --- .../sdk/internal/session/room/send/CancelSendTracker.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/CancelSendTracker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/CancelSendTracker.kt index fe3aad245a..0b79b93cf6 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/CancelSendTracker.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/CancelSendTracker.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,7 +29,7 @@ import javax.inject.Inject * Known limitation, for now requests are not persisted */ @SessionScope -class CancelSendTracker @Inject constructor() { +internal class CancelSendTracker @Inject constructor() { data class Request( val localId: String, From 7c33bf2742eac0e314b4642b7002619e19c77cf8 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 2 Sep 2020 12:31:22 +0200 Subject: [PATCH 23/32] Remove Done TODO --- .../android/sdk/internal/session/room/send/DefaultSendService.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt index e20ab79ee6..4ac3beb432 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt @@ -147,7 +147,6 @@ internal class DefaultSendService @AssistedInject constructor( override fun resendMediaMessage(localEcho: TimelineEvent): Cancelable? { if (localEcho.root.sendState.hasFailed()) { - // TODO this need a refactoring of attachment sending val clearContent = localEcho.root.getClearContent() val messageContent = clearContent?.toModel() as? MessageWithAttachmentContent ?: return null From 93cb6bd26ed13ed2e8baab85ba8f7b873ee6d1d1 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 2 Sep 2020 12:37:06 +0200 Subject: [PATCH 24/32] Avoid null type --- .../sdk/api/session/room/send/SendService.kt | 4 +-- .../session/room/send/DefaultSendService.kt | 29 ++++++++++--------- 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendService.kt index fd596de328..b8e536cb33 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendService.kt @@ -110,13 +110,13 @@ interface SendService { * Schedule this message to be resent * @param localEcho the unsent local echo */ - fun resendTextMessage(localEcho: TimelineEvent): Cancelable? + fun resendTextMessage(localEcho: TimelineEvent): Cancelable /** * Schedule this message to be resent * @param localEcho the unsent local echo */ - fun resendMediaMessage(localEcho: TimelineEvent): Cancelable? + fun resendMediaMessage(localEcho: TimelineEvent): Cancelable /** * Remove this failed message from the timeline diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt index 4ac3beb432..95cd1c699c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt @@ -45,6 +45,7 @@ import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.matrix.android.sdk.api.util.Cancelable import org.matrix.android.sdk.api.util.CancelableBag import org.matrix.android.sdk.api.util.JsonDict +import org.matrix.android.sdk.api.util.NoOpCancellable import org.matrix.android.sdk.internal.di.SessionId import org.matrix.android.sdk.internal.di.WorkManagerProvider import org.matrix.android.sdk.internal.session.content.UploadContentWorker @@ -137,28 +138,28 @@ internal class DefaultSendService @AssistedInject constructor( .let { timelineSendEventWorkCommon.postWork(roomId, it) } } - override fun resendTextMessage(localEcho: TimelineEvent): Cancelable? { + override fun resendTextMessage(localEcho: TimelineEvent): Cancelable { if (localEcho.root.isTextMessage() && localEcho.root.sendState.hasFailed()) { localEchoRepository.updateSendState(localEcho.eventId, SendState.UNSENT) return sendEvent(localEcho.root) } - return null + return NoOpCancellable } - override fun resendMediaMessage(localEcho: TimelineEvent): Cancelable? { + override fun resendMediaMessage(localEcho: TimelineEvent): Cancelable { if (localEcho.root.sendState.hasFailed()) { val clearContent = localEcho.root.getClearContent() - val messageContent = clearContent?.toModel() as? MessageWithAttachmentContent ?: return null + val messageContent = clearContent?.toModel() as? MessageWithAttachmentContent ?: return NoOpCancellable - val url = messageContent.getFileUrl() ?: return null + val url = messageContent.getFileUrl() ?: return NoOpCancellable if (url.startsWith("mxc://")) { // We need to resend only the message as the attachment is ok localEchoRepository.updateSendState(localEcho.eventId, SendState.UNSENT) return sendEvent(localEcho.root) } - // we need to resend the media - when (messageContent) { + // we need to resend the media + return when (messageContent) { is MessageImageContent -> { // The image has not yet been sent val attachmentData = ContentAttachmentData( @@ -171,7 +172,7 @@ internal class DefaultSendService @AssistedInject constructor( type = ContentAttachmentData.Type.IMAGE ) localEchoRepository.updateSendState(localEcho.eventId, SendState.UNSENT) - return internalSendMedia(listOf(localEcho.root), attachmentData, true) + internalSendMedia(listOf(localEcho.root), attachmentData, true) } is MessageVideoContent -> { val attachmentData = ContentAttachmentData( @@ -185,9 +186,9 @@ internal class DefaultSendService @AssistedInject constructor( type = ContentAttachmentData.Type.VIDEO ) localEchoRepository.updateSendState(localEcho.eventId, SendState.UNSENT) - return internalSendMedia(listOf(localEcho.root), attachmentData, true) + internalSendMedia(listOf(localEcho.root), attachmentData, true) } - is MessageFileContent -> { + is MessageFileContent -> { val attachmentData = ContentAttachmentData( size = messageContent.info!!.size, mimeType = messageContent.info.mimeType!!, @@ -196,7 +197,7 @@ internal class DefaultSendService @AssistedInject constructor( type = ContentAttachmentData.Type.FILE ) localEchoRepository.updateSendState(localEcho.eventId, SendState.UNSENT) - return internalSendMedia(listOf(localEcho.root), attachmentData, true) + internalSendMedia(listOf(localEcho.root), attachmentData, true) } is MessageAudioContent -> { val attachmentData = ContentAttachmentData( @@ -208,12 +209,12 @@ internal class DefaultSendService @AssistedInject constructor( type = ContentAttachmentData.Type.AUDIO ) localEchoRepository.updateSendState(localEcho.eventId, SendState.UNSENT) - return internalSendMedia(listOf(localEcho.root), attachmentData, true) + internalSendMedia(listOf(localEcho.root), attachmentData, true) } + else -> NoOpCancellable } - return null } - return null + return NoOpCancellable } override fun deleteFailedEcho(localEcho: TimelineEvent) { From 4ef1f9c4d9f310e6686282610925902b30835959 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 2 Sep 2020 12:45:05 +0200 Subject: [PATCH 25/32] Avoid copy paste of code --- .../action/MessageActionsViewModel.kt | 25 +++++++++---------- 1 file changed, 12 insertions(+), 13 deletions(-) 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 80d968670a..a49b74c243 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 @@ -51,6 +51,7 @@ import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent import org.matrix.android.sdk.api.session.room.timeline.hasBeenEdited import org.matrix.android.sdk.rx.rx import org.matrix.android.sdk.rx.unwrap +import java.util.ArrayList /** * Information related to an event and used to display preview in contextual bottom sheet. @@ -232,12 +233,7 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted } 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)) - } + addViewSourceItems(timelineEvent) } } else if (timelineEvent.root.sendState.isSending()) { // TODO is uploading attachment? @@ -307,13 +303,7 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted add(EventSharedAction.ReRequestKey(timelineEvent.eventId)) } } - - 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)) - } + addViewSourceItems(timelineEvent) } add(EventSharedAction.CopyPermalink(eventId)) if (session.myUserId != timelineEvent.root.senderId) { @@ -329,6 +319,15 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted } } + private fun ArrayList.addViewSourceItems(timelineEvent: TimelineEvent) { + 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)) + } + } + private fun canCancel(@Suppress("UNUSED_PARAMETER") event: TimelineEvent): Boolean { return true } From 3b8c61a87ea793085455c4a8b0ce2569c1d6bc11 Mon Sep 17 00:00:00 2001 From: Valere Date: Wed, 2 Sep 2020 16:06:48 +0200 Subject: [PATCH 26/32] FIx / interceptors and stream closed --- .../internal/network/ProgressRequestBody.kt | 11 ++--- .../internal/session/content/FileUploader.kt | 40 ++++++------------- .../session/content/UploadContentWorker.kt | 10 ++--- 3 files changed, 21 insertions(+), 40 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ProgressRequestBody.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ProgressRequestBody.kt index addc5b7205..98dec301ee 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ProgressRequestBody.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ProgressRequestBody.kt @@ -24,6 +24,7 @@ import okio.BufferedSink import okio.ForwardingSink import okio.Sink import okio.buffer +import org.matrix.android.sdk.api.extensions.tryThis import java.io.IOException internal class ProgressRequestBody(private val delegate: RequestBody, @@ -39,15 +40,9 @@ internal class ProgressRequestBody(private val delegate: RequestBody, override fun isDuplex() = delegate.isDuplex() - override fun contentLength(): Long { - try { - return delegate.contentLength() - } catch (e: IOException) { - e.printStackTrace() - } + val length = tryThis { delegate.contentLength() } ?: -1 - return -1 - } + override fun contentLength() = length @Throws(IOException::class) override fun writeTo(sink: BufferedSink) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/FileUploader.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/FileUploader.kt index 18d3558f4b..4ddf394b00 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/FileUploader.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/FileUploader.kt @@ -32,6 +32,7 @@ import okhttp3.RequestBody.Companion.toRequestBody import okio.BufferedSink import okio.source import org.greenrobot.eventbus.EventBus +import org.matrix.android.sdk.api.extensions.tryThis import org.matrix.android.sdk.api.session.content.ContentUrlResolver import org.matrix.android.sdk.internal.di.Authenticated import org.matrix.android.sdk.internal.network.ProgressRequestBody @@ -40,7 +41,7 @@ import org.matrix.android.sdk.internal.network.toFailure import java.io.File import java.io.FileNotFoundException import java.io.IOException -import java.io.InputStream +import java.util.UUID import javax.inject.Inject internal class FileUploader @Inject constructor(@Authenticated @@ -57,11 +58,11 @@ internal class FileUploader @Inject constructor(@Authenticated filename: String?, mimeType: String?, progressListener: ProgressRequestBody.Listener? = null): ContentUploadResponse { - val uploadBody = object : RequestBody() { + val uploadBody = object : RequestBody() { override fun contentLength() = file.length() // Disable okhttp auto resend for 'large files' - override fun isOneShot() = contentLength() == 0L || contentLength() >= 1_000_000 + override fun isOneShot() = contentLength() == 0L || contentLength() >= 1_000_000 override fun contentType(): MediaType? { return mimeType?.toMediaTypeOrNull() @@ -83,37 +84,22 @@ internal class FileUploader @Inject constructor(@Authenticated return upload(uploadBody, filename, progressListener) } - suspend fun uploadInputStream(inputStream: InputStream, - filename: String?, - mimeType: String?, - progressListener: ProgressRequestBody.Listener? = null): ContentUploadResponse { - val length = inputStream.available().toLong() - val uploadBody = object : RequestBody() { - override fun contentLength() = length - - // Disable okhttp auto resend for 'large files' - override fun isOneShot() = contentLength() == 0L || contentLength() >= 1_000_000 - - override fun contentType(): MediaType? { - return mimeType?.toMediaTypeOrNull() - } - - override fun writeTo(sink: BufferedSink) { - inputStream.source().use { sink.writeAll(it) } - } - } - return upload(uploadBody, filename, progressListener) - } - suspend fun uploadFromUri(uri: Uri, filename: String?, mimeType: String?, progressListener: ProgressRequestBody.Listener? = null): ContentUploadResponse { - val inputStream = withContext(Dispatchers.IO) { + val inputStream = withContext(Dispatchers.IO) { context.contentResolver.openInputStream(uri) } ?: throw FileNotFoundException() - return uploadInputStream(inputStream, filename, mimeType, progressListener) + val workingFile = File.createTempFile(UUID.randomUUID().toString(), null, context.cacheDir) + workingFile.outputStream().use { + inputStream.copyTo(it) + } + return uploadFile(workingFile, filename, mimeType, progressListener).also { + tryThis { workingFile.delete() } + } } + private suspend fun upload(uploadBody: RequestBody, filename: String?, progressListener: ProgressRequestBody.Listener?): ContentUploadResponse { val urlBuilder = uploadUrl.toHttpUrlOrNull()?.newBuilder() ?: throw RuntimeException() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt index 12ded1666b..53ca720f6a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt @@ -178,7 +178,7 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter var uploadedFileEncryptedFileInfo: EncryptedFileInfo? = null return try { - val streamToUpload: InputStream + val fileToUplaod: File if (attachment.type == ContentAttachmentData.Type.IMAGE && params.compressBeforeSending) { // Compressor library works with File instead of Uri for now. Since Scoped Storage doesn't allow us to access files directly, we should @@ -200,9 +200,9 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter options.outHeight, fileSize ) - streamToUpload = compressedFile.inputStream() + fileToUplaod = compressedFile } else { - streamToUpload = workingFile.inputStream() + fileToUplaod = workingFile } val contentUploadResponse = if (params.isEncrypted) { @@ -211,7 +211,7 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter val tmpEncrypted = File.createTempFile(UUID.randomUUID().toString(), null, context.cacheDir) uploadedFileEncryptedFileInfo = - MXEncryptedAttachments.encrypt(streamToUpload, attachment.getSafeMimeType(), tmpEncrypted) { read, total -> + MXEncryptedAttachments.encrypt(fileToUplaod.inputStream(), attachment.getSafeMimeType(), tmpEncrypted) { read, total -> notifyTracker(params) { contentUploadStateTracker.setEncrypting(it, read.toLong(), total.toLong()) } @@ -228,7 +228,7 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter } else { Timber.v("## FileService: Clear file") fileUploader - .uploadInputStream(streamToUpload, attachment.name, attachment.getSafeMimeType(), progressListener) + .uploadFile(fileToUplaod, attachment.name, attachment.getSafeMimeType(), progressListener) } Timber.v("## FileService: Update cache storage for ${contentUploadResponse.contentUri}") From 8c801ae07889ae5b90e6f0a305e9b3bf243bc699 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 2 Sep 2020 19:01:26 +0200 Subject: [PATCH 27/32] API change: encrypted files are now decrypted internally, no need to expose decryptStream() anymore --- .../java/org/matrix/android/sdk/api/Matrix.kt | 9 +------ .../java/org/matrix/android/sdk/api/Matrix.kt | 7 ----- .../attachments/MXEncryptedAttachments.kt | 27 ------------------- .../session/content/UploadContentWorker.kt | 1 - 4 files changed, 1 insertion(+), 43 deletions(-) diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/api/Matrix.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/api/Matrix.kt index df359f2adc..df26bb1227 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/api/Matrix.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/api/Matrix.kt @@ -23,15 +23,12 @@ import androidx.work.WorkManager import com.zhuinden.monarchy.Monarchy import org.matrix.android.sdk.BuildConfig import org.matrix.android.sdk.api.auth.AuthenticationService -import org.matrix.android.sdk.common.DaggerTestMatrixComponent import org.matrix.android.sdk.api.legacy.LegacySessionImporter +import org.matrix.android.sdk.common.DaggerTestMatrixComponent import org.matrix.android.sdk.internal.SessionManager -import org.matrix.android.sdk.internal.crypto.attachments.ElementToDecrypt -import org.matrix.android.sdk.internal.crypto.attachments.MXEncryptedAttachments import org.matrix.android.sdk.internal.network.UserAgentHolder import org.matrix.android.sdk.internal.util.BackgroundDetectionObserver import org.matrix.olm.OlmManager -import java.io.InputStream import java.util.concurrent.Executors import java.util.concurrent.atomic.AtomicBoolean import javax.inject.Inject @@ -96,9 +93,5 @@ class Matrix private constructor(context: Context, matrixConfiguration: MatrixCo fun getSdkVersion(): String { return BuildConfig.VERSION_NAME + " (" + BuildConfig.GIT_SDK_REVISION + ")" } - - fun decryptStream(inputStream: InputStream?, elementToDecrypt: ElementToDecrypt): InputStream? { - return MXEncryptedAttachments.decryptAttachment(inputStream, elementToDecrypt) - } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/Matrix.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/Matrix.kt index 6cd003ddae..aafefa2048 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/Matrix.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/Matrix.kt @@ -26,13 +26,10 @@ import org.matrix.android.sdk.BuildConfig import org.matrix.android.sdk.api.auth.AuthenticationService import org.matrix.android.sdk.api.legacy.LegacySessionImporter import org.matrix.android.sdk.internal.SessionManager -import org.matrix.android.sdk.internal.crypto.attachments.ElementToDecrypt -import org.matrix.android.sdk.internal.crypto.attachments.MXEncryptedAttachments import org.matrix.android.sdk.internal.di.DaggerMatrixComponent import org.matrix.android.sdk.internal.network.UserAgentHolder import org.matrix.android.sdk.internal.util.BackgroundDetectionObserver import org.matrix.olm.OlmManager -import java.io.InputStream import java.util.concurrent.Executors import java.util.concurrent.atomic.AtomicBoolean import javax.inject.Inject @@ -97,9 +94,5 @@ class Matrix private constructor(context: Context, matrixConfiguration: MatrixCo fun getSdkVersion(): String { return BuildConfig.VERSION_NAME + " (" + BuildConfig.GIT_SDK_REVISION + ")" } - - fun decryptStream(inputStream: InputStream?, elementToDecrypt: ElementToDecrypt): InputStream? { - return MXEncryptedAttachments.decryptAttachment(inputStream, elementToDecrypt) - } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/attachments/MXEncryptedAttachments.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/attachments/MXEncryptedAttachments.kt index df1d39c250..11d5b4796a 100755 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/attachments/MXEncryptedAttachments.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/attachments/MXEncryptedAttachments.kt @@ -31,7 +31,6 @@ import java.io.OutputStream import java.security.MessageDigest import java.security.SecureRandom import javax.crypto.Cipher -import javax.crypto.CipherInputStream import javax.crypto.spec.IvParameterSpec import javax.crypto.spec.SecretKeySpec @@ -226,32 +225,6 @@ internal object MXEncryptedAttachments { .also { Timber.v("Encrypt in ${System.currentTimeMillis() - t0}ms") } } - /** - * Decrypt an attachment - * - * @param attachmentStream the attachment stream. Will be closed after this method call. - * @param elementToDecrypt the element to decrypt the file - * @return the decrypted attachment stream - */ - fun decryptAttachment(attachmentStream: InputStream?, elementToDecrypt: ElementToDecrypt): InputStream? { - try { - val digestCheckInputStream = MatrixDigestCheckInputStream(attachmentStream, elementToDecrypt.sha256) - - val key = Base64.decode(base64UrlToBase64(elementToDecrypt.k), Base64.DEFAULT) - val initVectorBytes = Base64.decode(elementToDecrypt.iv, Base64.DEFAULT) - - val decryptCipher = Cipher.getInstance(CIPHER_ALGORITHM) - val secretKeySpec = SecretKeySpec(key, SECRET_KEY_SPEC_ALGORITHM) - val ivParameterSpec = IvParameterSpec(initVectorBytes) - decryptCipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec) - - return CipherInputStream(digestCheckInputStream, decryptCipher) - } catch (failure: Throwable) { - Timber.e(failure, "## decryptAttachment() : failed to create stream") - return null - } - } - /** * Decrypt an attachment * diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt index 53ca720f6a..015ad3a1e4 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt @@ -45,7 +45,6 @@ import org.matrix.android.sdk.internal.worker.WorkerParamsFactory import org.matrix.android.sdk.internal.worker.getSessionComponent import timber.log.Timber import java.io.File -import java.io.InputStream import java.util.UUID import javax.inject.Inject From e02b9b7736fb94ba9a800689fadbc4c313a4be4b Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 2 Sep 2020 19:02:36 +0200 Subject: [PATCH 28/32] Changelog --- CHANGES.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES.md b/CHANGES.md index 1ffb6bcad0..c728fce85d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -7,6 +7,7 @@ Features ✨: Improvements 🙌: - You can now join room through permalink and within room directory search - Add long click gesture to copy userId, user display name, room name, room topic and room alias (#1774) + - Fix several issues when uploading bug files (#1889) Bugfix 🐛: - Display name not shown under Settings/General (#1926) From 8103081e0edad3d627de060cb1c3dba87d21c6b5 Mon Sep 17 00:00:00 2001 From: Valere Date: Thu, 3 Sep 2020 09:31:53 +0200 Subject: [PATCH 29/32] Fix / Support open and view of sending attachment --- .../lib/attachmentviewer/VideoLoaderTarget.kt | 17 ++++++-- .../lib/attachmentviewer/VideoViewHolder.kt | 7 +++ .../home/room/detail/RoomDetailAction.kt | 2 +- .../home/room/detail/RoomDetailFragment.kt | 2 +- .../home/room/detail/RoomDetailViewModel.kt | 13 +++++- .../features/media/BaseAttachmentProvider.kt | 43 ++++++++++--------- .../features/media/ImageContentRenderer.kt | 21 ++++++--- .../media/RoomEventsAttachmentProvider.kt | 10 +++-- .../features/media/VideoContentRenderer.kt | 13 +++++- 9 files changed, 91 insertions(+), 37 deletions(-) diff --git a/attachment-viewer/src/main/java/im/vector/lib/attachmentviewer/VideoLoaderTarget.kt b/attachment-viewer/src/main/java/im/vector/lib/attachmentviewer/VideoLoaderTarget.kt index 78f46a320f..9936ae3cba 100644 --- a/attachment-viewer/src/main/java/im/vector/lib/attachmentviewer/VideoLoaderTarget.kt +++ b/attachment-viewer/src/main/java/im/vector/lib/attachmentviewer/VideoLoaderTarget.kt @@ -34,7 +34,8 @@ interface VideoLoaderTarget { fun onVideoFileLoading(uid: String) fun onVideoFileLoadFailed(uid: String) - fun onVideoFileReady(uid: String, file: File) + fun onVideoURLReady(uid: String, file: File) + fun onVideoURLReady(uid: String, path: String) } internal class DefaultVideoLoaderTarget(val holder: VideoViewHolder, private val contextView: ImageView) : VideoLoaderTarget { @@ -66,11 +67,21 @@ internal class DefaultVideoLoaderTarget(val holder: VideoViewHolder, private val holder.videoFileLoadError() } - override fun onVideoFileReady(uid: String, file: File) { + override fun onVideoURLReady(uid: String, file: File) { if (holder.boundResourceUid != uid) return + arrangeForVideoReady() + holder.videoReady(file) + } + + override fun onVideoURLReady(uid: String, contentPath: String) { + if (holder.boundResourceUid != uid) return + arrangeForVideoReady() + holder.videoReady(contentPath) + } + + private fun arrangeForVideoReady() { holder.thumbnailImage.isVisible = false holder.loaderProgressBar.isVisible = false holder.videoView.isVisible = true - holder.videoReady(file) } } diff --git a/attachment-viewer/src/main/java/im/vector/lib/attachmentviewer/VideoViewHolder.kt b/attachment-viewer/src/main/java/im/vector/lib/attachmentviewer/VideoViewHolder.kt index 32f449d6fe..1899a828be 100644 --- a/attachment-viewer/src/main/java/im/vector/lib/attachmentviewer/VideoViewHolder.kt +++ b/attachment-viewer/src/main/java/im/vector/lib/attachmentviewer/VideoViewHolder.kt @@ -65,6 +65,13 @@ class VideoViewHolder constructor(itemView: View) : } } + fun videoReady(path: String) { + mVideoPath = path + if (isSelected) { + startPlaying() + } + } + fun videoFileLoadError() { } 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 33eefb4182..4bdf2e7e57 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 @@ -40,7 +40,7 @@ sealed class RoomDetailAction : VectorViewModelAction { data class UpdateQuickReactAction(val targetEventId: String, val selectedReaction: String, val add: Boolean) : RoomDetailAction() data class NavigateToEvent(val eventId: String, val highlight: Boolean) : RoomDetailAction() object MarkAllAsRead : RoomDetailAction() - data class DownloadOrOpen(val eventId: String, val messageFileContent: MessageWithAttachmentContent) : RoomDetailAction() + data class DownloadOrOpen(val eventId: String, val senderId: String?, val messageFileContent: MessageWithAttachmentContent) : RoomDetailAction() data class HandleTombstoneEvent(val event: Event) : RoomDetailAction() object AcceptInvite : RoomDetailAction() object RejectInvite : 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 b2790e0b47..7635c3042d 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 @@ -1423,7 +1423,7 @@ class RoomDetailFragment @Inject constructor( roomDetailViewModel.handle(RoomDetailAction.ResumeVerification(informationData.eventId, null)) } is MessageWithAttachmentContent -> { - val action = RoomDetailAction.DownloadOrOpen(informationData.eventId, messageContent) + val action = RoomDetailAction.DownloadOrOpen(informationData.eventId, informationData.senderId, messageContent) roomDetailViewModel.handle(action) } is EncryptedEventContent -> { 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 ddb21b9f2f..cbf57a86f1 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 @@ -57,6 +57,7 @@ import org.commonmark.renderer.html.HtmlRenderer import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.MatrixPatterns import org.matrix.android.sdk.api.NoOpMatrixCallback +import org.matrix.android.sdk.api.extensions.tryThis import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.crypto.MXCryptoError @@ -990,8 +991,18 @@ class RoomDetailViewModel @AssistedInject constructor( private fun handleOpenOrDownloadFile(action: RoomDetailAction.DownloadOrOpen) { val mxcUrl = action.messageFileContent.getFileUrl() + val isLocalSendingFile = action.senderId == session.myUserId + && action.messageFileContent.getFileUrl()?.startsWith("content://") ?: false val isDownloaded = mxcUrl?.let { session.fileService().isFileInCache(it, action.messageFileContent.mimeType) } ?: false - if (isDownloaded) { + if (isLocalSendingFile) { + tryThis { Uri.parse(mxcUrl) }?.let { + _viewEvents.post(RoomDetailViewEvents.OpenFile( + action.messageFileContent.mimeType, + it, + null + )) + } + } else if (isDownloaded) { // we can open it session.fileService().getTemporarySharableURI(mxcUrl!!, action.messageFileContent.mimeType)?.let { uri -> _viewEvents.post(RoomDetailViewEvents.OpenFile( diff --git a/vector/src/main/java/im/vector/app/features/media/BaseAttachmentProvider.kt b/vector/src/main/java/im/vector/app/features/media/BaseAttachmentProvider.kt index 966ee985f0..c3516308c6 100644 --- a/vector/src/main/java/im/vector/app/features/media/BaseAttachmentProvider.kt +++ b/vector/src/main/java/im/vector/app/features/media/BaseAttachmentProvider.kt @@ -27,6 +27,7 @@ import im.vector.lib.attachmentviewer.AttachmentSourceProvider import im.vector.lib.attachmentviewer.ImageLoaderTarget import im.vector.lib.attachmentviewer.VideoLoaderTarget import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.session.file.FileService import java.io.File @@ -101,11 +102,7 @@ abstract class BaseAttachmentProvider(val imageContentRenderer: ImageContentRend override fun loadVideo(target: VideoLoaderTarget, info: AttachmentInfo.Video) { val data = info.data as? VideoContentRenderer.Data ?: return -// videoContentRenderer.render(data, -// holder.thumbnailImage, -// holder.loaderProgressBar, -// holder.videoView, -// holder.errorTextView) + imageContentRenderer.render(data.thumbnailMediaData, target.contextView(), object : CustomViewTarget(target.contextView()) { override fun onLoadFailed(errorDrawable: Drawable?) { target.onThumbnailLoadFailed(info.uid, errorDrawable) @@ -120,24 +117,28 @@ abstract class BaseAttachmentProvider(val imageContentRenderer: ImageContentRend } }) - target.onVideoFileLoading(info.uid) - fileService.downloadFile( - downloadMode = FileService.DownloadMode.FOR_INTERNAL_USE, - id = data.eventId, - mimeType = data.mimeType, - elementToDecrypt = data.elementToDecrypt, - fileName = data.filename, - url = data.url, - callback = object : MatrixCallback { - override fun onSuccess(data: File) { - target.onVideoFileReady(info.uid, data) - } + if (data.url?.startsWith("content://").orFalse() && data.allowNonMxcUrls) { + target.onVideoURLReady(info.uid, data.url!!) + } else { + target.onVideoFileLoading(info.uid) + fileService.downloadFile( + downloadMode = FileService.DownloadMode.FOR_INTERNAL_USE, + id = data.eventId, + mimeType = data.mimeType, + elementToDecrypt = data.elementToDecrypt, + fileName = data.filename, + url = data.url, + callback = object : MatrixCallback { + override fun onSuccess(data: File) { + target.onVideoURLReady(info.uid, data) + } - override fun onFailure(failure: Throwable) { - target.onVideoFileLoadFailed(info.uid) + override fun onFailure(failure: Throwable) { + target.onVideoFileLoadFailed(info.uid) + } } - } - ) + ) + } } override fun clear(id: String) { diff --git a/vector/src/main/java/im/vector/app/features/media/ImageContentRenderer.kt b/vector/src/main/java/im/vector/app/features/media/ImageContentRenderer.kt index 0c709f98af..28cead29df 100644 --- a/vector/src/main/java/im/vector/app/features/media/ImageContentRenderer.kt +++ b/vector/src/main/java/im/vector/app/features/media/ImageContentRenderer.kt @@ -51,6 +51,8 @@ interface AttachmentData : Parcelable { val mimeType: String? val url: String? val elementToDecrypt: ElementToDecrypt? + // If true will load non mxc url, be careful to set it only for images sent by you + val allowNonMxcUrls: Boolean } class ImageContentRenderer @Inject constructor(private val activeSessionHolder: ActiveSessionHolder, @@ -66,7 +68,9 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder: val height: Int?, val maxHeight: Int, val width: Int?, - val maxWidth: Int + val maxWidth: Int, + // If true will load non mxc url, be careful to set it only for images sent by you + override val allowNonMxcUrls: Boolean = false ) : AttachmentData { fun isLocalFile() = url.isLocalFile() @@ -121,7 +125,8 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder: .load(data) } else { // Clear image - val resolvedUrl = activeSessionHolder.getActiveSession().contentUrlResolver().resolveFullSize(data.url) + val resolvedUrl = resolveUrl(data) + ?: data.url.takeIf { data.allowNonMxcUrls } GlideApp .with(contextView) .load(resolvedUrl) @@ -175,7 +180,7 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder: .load(data) } else { // Clear image - val resolvedUrl = activeSessionHolder.getActiveSession().contentUrlResolver().resolveFullSize(data.url) + val resolvedUrl = resolveUrl(data) GlideApp .with(imageView) .load(resolvedUrl) @@ -215,7 +220,7 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder: val contentUrlResolver = activeSessionHolder.getActiveSession().contentUrlResolver() val resolvedUrl = when (mode) { Mode.FULL_SIZE, - Mode.STICKER -> contentUrlResolver.resolveFullSize(data.url) + Mode.STICKER -> resolveUrl(data) Mode.THUMBNAIL -> contentUrlResolver.resolveThumbnail(data.url, size.width, size.height, ContentUrlResolver.ThumbnailMethod.SCALE) } // Fallback to base url @@ -229,7 +234,7 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder: error( GlideApp .with(imageView) - .load(contentUrlResolver.resolveFullSize(data.url)) + .load(resolveUrl(data)) ) } } @@ -242,7 +247,7 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder: val (width, height) = processSize(data, Mode.THUMBNAIL) val contentUrlResolver = activeSessionHolder.getActiveSession().contentUrlResolver() - val fullSize = contentUrlResolver.resolveFullSize(data.url) + val fullSize = resolveUrl(data) val thumbnail = contentUrlResolver.resolveThumbnail(data.url, width, height, ContentUrlResolver.ThumbnailMethod.SCALE) if (fullSize.isNullOrBlank() || thumbnail.isNullOrBlank()) { @@ -262,6 +267,10 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder: ) } + private fun resolveUrl(data: Data) = + (activeSessionHolder.getActiveSession().contentUrlResolver().resolveFullSize(data.url) + ?: data.url?.takeIf { data.isLocalFile() && data.allowNonMxcUrls }) + private fun processSize(data: Data, mode: Mode): Size { val maxImageWidth = data.maxWidth val maxImageHeight = data.maxHeight diff --git a/vector/src/main/java/im/vector/app/features/media/RoomEventsAttachmentProvider.kt b/vector/src/main/java/im/vector/app/features/media/RoomEventsAttachmentProvider.kt index b4fa38eefc..e5f0f481bf 100644 --- a/vector/src/main/java/im/vector/app/features/media/RoomEventsAttachmentProvider.kt +++ b/vector/src/main/java/im/vector/app/features/media/RoomEventsAttachmentProvider.kt @@ -63,7 +63,9 @@ class RoomEventsAttachmentProvider( maxHeight = -1, maxWidth = -1, width = null, - height = null + height = null, + allowNonMxcUrls = it.root.sendState.isSending() + ) if (content.mimeType == "image/gif") { AttachmentInfo.AnimatedImage( @@ -89,7 +91,8 @@ class RoomEventsAttachmentProvider( height = content.videoInfo?.height, maxHeight = -1, width = content.videoInfo?.width, - maxWidth = -1 + maxWidth = -1, + allowNonMxcUrls = it.root.sendState.isSending() ) val data = VideoContentRenderer.Data( eventId = it.eventId, @@ -97,7 +100,8 @@ class RoomEventsAttachmentProvider( mimeType = content.mimeType, url = content.getFileUrl(), elementToDecrypt = content.encryptedFileInfo?.toElementToDecrypt(), - thumbnailMediaData = thumbnailData + thumbnailMediaData = thumbnailData, + allowNonMxcUrls = it.root.sendState.isSending() ) AttachmentInfo.Video( uid = it.eventId, diff --git a/vector/src/main/java/im/vector/app/features/media/VideoContentRenderer.kt b/vector/src/main/java/im/vector/app/features/media/VideoContentRenderer.kt index 45fc238e93..4eb14592e0 100644 --- a/vector/src/main/java/im/vector/app/features/media/VideoContentRenderer.kt +++ b/vector/src/main/java/im/vector/app/features/media/VideoContentRenderer.kt @@ -24,12 +24,14 @@ import androidx.core.view.isVisible import im.vector.app.R import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.error.ErrorFormatter +import im.vector.app.core.utils.isLocalFile import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.session.file.FileService import org.matrix.android.sdk.internal.crypto.attachments.ElementToDecrypt import kotlinx.android.parcel.Parcelize import timber.log.Timber import java.io.File +import java.net.URLEncoder import javax.inject.Inject class VideoContentRenderer @Inject constructor(private val activeSessionHolder: ActiveSessionHolder, @@ -42,7 +44,9 @@ class VideoContentRenderer @Inject constructor(private val activeSessionHolder: override val mimeType: String?, override val url: String?, override val elementToDecrypt: ElementToDecrypt?, - val thumbnailMediaData: ImageContentRenderer.Data + val thumbnailMediaData: ImageContentRenderer.Data, + // If true will load non mxc url, be careful to set it only for video sent by you + override val allowNonMxcUrls: Boolean = false ) : AttachmentData fun render(data: Data, @@ -60,6 +64,12 @@ class VideoContentRenderer @Inject constructor(private val activeSessionHolder: loadingView.isVisible = false errorView.isVisible = true errorView.setText(R.string.unknown_error) + } else if (data.url.isLocalFile() && data.allowNonMxcUrls) { + thumbnailView.isVisible = false + loadingView.isVisible = false + videoView.isVisible = true + videoView.setVideoPath(URLEncoder.encode(data.url, Charsets.US_ASCII.displayName())) + videoView.start() } else { thumbnailView.isVisible = true loadingView.isVisible = true @@ -91,6 +101,7 @@ class VideoContentRenderer @Inject constructor(private val activeSessionHolder: } } else { val resolvedUrl = contentUrlResolver.resolveFullSize(data.url) + ?: data.url?.takeIf { data.url.isLocalFile() && data.allowNonMxcUrls } if (resolvedUrl == null) { thumbnailView.isVisible = false From 8340d5e71f809bc9b7c24ca765ebce1f2e484895 Mon Sep 17 00:00:00 2001 From: Valere Date: Thu, 3 Sep 2020 09:38:40 +0200 Subject: [PATCH 30/32] Fix tests --- .../internal/crypto/AttachmentEncryptionTest.kt | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/AttachmentEncryptionTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/AttachmentEncryptionTest.kt index ebaa235a2b..f4a48eb367 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/AttachmentEncryptionTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/AttachmentEncryptionTest.kt @@ -30,6 +30,7 @@ import org.matrix.android.sdk.internal.crypto.attachments.MXEncryptedAttachments import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileInfo import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileKey import org.matrix.android.sdk.internal.crypto.attachments.toElementToDecrypt +import java.io.ByteArrayOutputStream import java.io.InputStream /** @@ -53,17 +54,14 @@ class AttachmentEncryptionTest { memoryFile.inputStream } - val decryptedStream = MXEncryptedAttachments.decryptAttachment(inputStream, encryptedFileInfo.toElementToDecrypt()!!) + val decryptedStream = ByteArrayOutputStream() + val result = MXEncryptedAttachments.decryptAttachment(inputStream, encryptedFileInfo.toElementToDecrypt()!!, decryptedStream) - assertNotNull(decryptedStream) + assert(result) - val buffer = ByteArray(100) + val toByteArray = decryptedStream.toByteArray() - val len = decryptedStream!!.read(buffer) - - decryptedStream.close() - - return Base64.encodeToString(buffer, 0, len, Base64.DEFAULT).replace("\n".toRegex(), "").replace("=".toRegex(), "") + return Base64.encodeToString(toByteArray, 0, toByteArray.size, Base64.DEFAULT).replace("\n".toRegex(), "").replace("=".toRegex(), "") } @Test From 7c638798c76fabb29036591e75d4f07579121403 Mon Sep 17 00:00:00 2001 From: Valere Date: Thu, 3 Sep 2020 14:53:13 +0200 Subject: [PATCH 31/32] Code review --- .../im/vector/lib/attachmentviewer/VideoLoaderTarget.kt | 8 ++++---- .../sdk/internal/crypto/AttachmentEncryptionTest.kt | 1 - .../app/features/home/room/detail/RoomDetailViewModel.kt | 2 +- .../vector/app/features/media/BaseAttachmentProvider.kt | 7 +++---- .../im/vector/app/features/media/ImageContentRenderer.kt | 2 +- 5 files changed, 9 insertions(+), 11 deletions(-) diff --git a/attachment-viewer/src/main/java/im/vector/lib/attachmentviewer/VideoLoaderTarget.kt b/attachment-viewer/src/main/java/im/vector/lib/attachmentviewer/VideoLoaderTarget.kt index 9936ae3cba..996c4c65f4 100644 --- a/attachment-viewer/src/main/java/im/vector/lib/attachmentviewer/VideoLoaderTarget.kt +++ b/attachment-viewer/src/main/java/im/vector/lib/attachmentviewer/VideoLoaderTarget.kt @@ -34,7 +34,7 @@ interface VideoLoaderTarget { fun onVideoFileLoading(uid: String) fun onVideoFileLoadFailed(uid: String) - fun onVideoURLReady(uid: String, file: File) + fun onVideoFileReady(uid: String, file: File) fun onVideoURLReady(uid: String, path: String) } @@ -67,16 +67,16 @@ internal class DefaultVideoLoaderTarget(val holder: VideoViewHolder, private val holder.videoFileLoadError() } - override fun onVideoURLReady(uid: String, file: File) { + override fun onVideoFileReady(uid: String, file: File) { if (holder.boundResourceUid != uid) return arrangeForVideoReady() holder.videoReady(file) } - override fun onVideoURLReady(uid: String, contentPath: String) { + override fun onVideoURLReady(uid: String, path: String) { if (holder.boundResourceUid != uid) return arrangeForVideoReady() - holder.videoReady(contentPath) + holder.videoReady(path) } private fun arrangeForVideoReady() { diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/AttachmentEncryptionTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/AttachmentEncryptionTest.kt index f4a48eb367..1e109f11ae 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/AttachmentEncryptionTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/AttachmentEncryptionTest.kt @@ -21,7 +21,6 @@ import android.util.Base64 import androidx.test.ext.junit.runners.AndroidJUnit4 import org.junit.Assert.assertEquals import org.junit.Assert.assertNotEquals -import org.junit.Assert.assertNotNull import org.junit.FixMethodOrder import org.junit.Test import org.junit.runner.RunWith 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 cbf57a86f1..0120cbb2bb 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 @@ -992,7 +992,7 @@ class RoomDetailViewModel @AssistedInject constructor( private fun handleOpenOrDownloadFile(action: RoomDetailAction.DownloadOrOpen) { val mxcUrl = action.messageFileContent.getFileUrl() val isLocalSendingFile = action.senderId == session.myUserId - && action.messageFileContent.getFileUrl()?.startsWith("content://") ?: false + && mxcUrl?.startsWith("content://") ?: false val isDownloaded = mxcUrl?.let { session.fileService().isFileInCache(it, action.messageFileContent.mimeType) } ?: false if (isLocalSendingFile) { tryThis { Uri.parse(mxcUrl) }?.let { diff --git a/vector/src/main/java/im/vector/app/features/media/BaseAttachmentProvider.kt b/vector/src/main/java/im/vector/app/features/media/BaseAttachmentProvider.kt index c3516308c6..3846e56ecf 100644 --- a/vector/src/main/java/im/vector/app/features/media/BaseAttachmentProvider.kt +++ b/vector/src/main/java/im/vector/app/features/media/BaseAttachmentProvider.kt @@ -27,7 +27,6 @@ import im.vector.lib.attachmentviewer.AttachmentSourceProvider import im.vector.lib.attachmentviewer.ImageLoaderTarget import im.vector.lib.attachmentviewer.VideoLoaderTarget import org.matrix.android.sdk.api.MatrixCallback -import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.session.file.FileService import java.io.File @@ -117,8 +116,8 @@ abstract class BaseAttachmentProvider(val imageContentRenderer: ImageContentRend } }) - if (data.url?.startsWith("content://").orFalse() && data.allowNonMxcUrls) { - target.onVideoURLReady(info.uid, data.url!!) + if (data.url?.startsWith("content://") == true && data.allowNonMxcUrls) { + target.onVideoURLReady(info.uid, data.url) } else { target.onVideoFileLoading(info.uid) fileService.downloadFile( @@ -130,7 +129,7 @@ abstract class BaseAttachmentProvider(val imageContentRenderer: ImageContentRend url = data.url, callback = object : MatrixCallback { override fun onSuccess(data: File) { - target.onVideoURLReady(info.uid, data) + target.onVideoFileReady(info.uid, data) } override fun onFailure(failure: Throwable) { diff --git a/vector/src/main/java/im/vector/app/features/media/ImageContentRenderer.kt b/vector/src/main/java/im/vector/app/features/media/ImageContentRenderer.kt index 28cead29df..968f02a065 100644 --- a/vector/src/main/java/im/vector/app/features/media/ImageContentRenderer.kt +++ b/vector/src/main/java/im/vector/app/features/media/ImageContentRenderer.kt @@ -51,7 +51,7 @@ interface AttachmentData : Parcelable { val mimeType: String? val url: String? val elementToDecrypt: ElementToDecrypt? - // If true will load non mxc url, be careful to set it only for images sent by you + // If true will load non mxc url, be careful to set it only for attachments sent by you val allowNonMxcUrls: Boolean } From 0c39495e3fcb894d390fc20b8698ca5b75a1230c Mon Sep 17 00:00:00 2001 From: Valere Date: Thu, 3 Sep 2020 15:03:07 +0200 Subject: [PATCH 32/32] FIx / unneeded code --- .../java/im/vector/app/features/media/ImageContentRenderer.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/vector/src/main/java/im/vector/app/features/media/ImageContentRenderer.kt b/vector/src/main/java/im/vector/app/features/media/ImageContentRenderer.kt index 968f02a065..5c43b25d51 100644 --- a/vector/src/main/java/im/vector/app/features/media/ImageContentRenderer.kt +++ b/vector/src/main/java/im/vector/app/features/media/ImageContentRenderer.kt @@ -126,7 +126,6 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder: } else { // Clear image val resolvedUrl = resolveUrl(data) - ?: data.url.takeIf { data.allowNonMxcUrls } GlideApp .with(contextView) .load(resolvedUrl)