From e149ee53ded374c1ebb08e610ad9cf875a06df17 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 8 Jul 2019 11:55:31 +0200 Subject: [PATCH 01/16] Fix bad mime type for encrypted thumbnail --- .../android/internal/session/content/UploadContentWorker.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/UploadContentWorker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/UploadContentWorker.kt index 2597ef4e..90d15cc3 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/UploadContentWorker.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/UploadContentWorker.kt @@ -86,7 +86,7 @@ internal class UploadContentWorker(context: Context, params: WorkerParameters) : uploadedThumbnailEncryptedFileInfo = encryptionResult.encryptedFileInfo fileUploader - .uploadByteArray(encryptionResult.encryptedByteArray, "thumb_${attachment.name}", thumbnailData.mimeType) + .uploadByteArray(encryptionResult.encryptedByteArray, "thumb_${attachment.name}", "application/octet-stream") } else { fileUploader .uploadByteArray(thumbnailData.bytes, "thumb_${attachment.name}", thumbnailData.mimeType) From 829e8da8dc8cde42e594b603e75193446b63840d Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 8 Jul 2019 13:31:33 +0200 Subject: [PATCH 02/16] lastFailureMessage is val, not var --- .../android/internal/session/content/UploadContentWorker.kt | 2 +- .../matrix/android/internal/session/group/GetGroupDataWorker.kt | 2 +- .../internal/session/room/relation/SendRelationWorker.kt | 2 +- .../android/internal/session/room/send/EncryptEventWorker.kt | 2 +- .../android/internal/session/room/send/RedactEventWorker.kt | 2 +- .../android/internal/session/room/send/SendEventWorker.kt | 2 +- .../matrix/android/internal/worker/SessionWorkerParams.kt | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/UploadContentWorker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/UploadContentWorker.kt index 90d15cc3..5a8688ee 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/UploadContentWorker.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/UploadContentWorker.kt @@ -48,7 +48,7 @@ internal class UploadContentWorker(context: Context, params: WorkerParameters) : val event: Event, val attachment: ContentAttachmentData, val isRoomEncrypted: Boolean, - override var lastFailureMessage: String? = null + override val lastFailureMessage: String? = null ) : SessionWorkerParams @Inject lateinit var fileUploader: FileUploader diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/GetGroupDataWorker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/GetGroupDataWorker.kt index a619b83a..081739cb 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/GetGroupDataWorker.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/GetGroupDataWorker.kt @@ -32,7 +32,7 @@ internal class GetGroupDataWorker(context: Context, params: WorkerParameters) : internal data class Params( override val userId: String, val groupIds: List, - override var lastFailureMessage: String? = null + override val lastFailureMessage: String? = null ) : SessionWorkerParams @Inject lateinit var getGroupDataTask: GetGroupDataTask diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/SendRelationWorker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/SendRelationWorker.kt index a04c7b3e..81a888ee 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/SendRelationWorker.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/SendRelationWorker.kt @@ -40,7 +40,7 @@ internal class SendRelationWorker(context: Context, params: WorkerParameters) : val roomId: String, val event: Event, val relationType: String? = null, - override var lastFailureMessage: String? + override val lastFailureMessage: String? ) : SessionWorkerParams @Inject lateinit var roomAPI: RoomAPI diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/EncryptEventWorker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/EncryptEventWorker.kt index 44046a1d..118fa7cc 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/EncryptEventWorker.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/EncryptEventWorker.kt @@ -42,7 +42,7 @@ internal class EncryptEventWorker(context: Context, params: WorkerParameters) val event: Event, /**Do not encrypt these keys, keep them as is in encrypted content (e.g. m.relates_to)*/ val keepKeys: List? = null, - override var lastFailureMessage: String? = null + override val lastFailureMessage: String? = null ) : SessionWorkerParams @Inject lateinit var crypto: CryptoService diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/RedactEventWorker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/RedactEventWorker.kt index 38e0e23b..5acc16e1 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/RedactEventWorker.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/RedactEventWorker.kt @@ -36,7 +36,7 @@ internal class RedactEventWorker(context: Context, params: WorkerParameters) : C val roomId: String, val eventId: String, val reason: String?, - override var lastFailureMessage: String? = null + override val lastFailureMessage: String? = null ) : SessionWorkerParams @Inject lateinit var roomAPI: RoomAPI diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/SendEventWorker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/SendEventWorker.kt index 315ea457..05cd56e3 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/SendEventWorker.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/SendEventWorker.kt @@ -39,7 +39,7 @@ internal class SendEventWorker constructor(context: Context, params: WorkerParam override val userId: String, val roomId: String, val event: Event, - override var lastFailureMessage: String? = null + override val lastFailureMessage: String? = null ) : SessionWorkerParams @Inject lateinit var localEchoUpdater: LocalEchoUpdater diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/worker/SessionWorkerParams.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/worker/SessionWorkerParams.kt index 99fe3142..0c53a3ef 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/worker/SessionWorkerParams.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/worker/SessionWorkerParams.kt @@ -20,5 +20,5 @@ interface SessionWorkerParams { val userId: String // Null is no error occurs. When chaining Workers, first step is to check that there is no lastFailureMessage from the previous workers - var lastFailureMessage: String? + val lastFailureMessage: String? } From 20999655085f96b13331230c694c9e9169aee67b Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 8 Jul 2019 13:49:24 +0200 Subject: [PATCH 03/16] Avoid returning Result.failure() from appendable worker. --- .../attachments/MXEncryptedAttachments.kt | 27 +++++----- .../session/content/UploadContentWorker.kt | 53 +++++++++---------- 2 files changed, 38 insertions(+), 42 deletions(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/attachments/MXEncryptedAttachments.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/attachments/MXEncryptedAttachments.kt index 7fe82421..02c8931f 100755 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/attachments/MXEncryptedAttachments.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/attachments/MXEncryptedAttachments.kt @@ -18,6 +18,7 @@ package im.vector.matrix.android.internal.crypto.attachments import android.text.TextUtils import android.util.Base64 +import arrow.core.Try import im.vector.matrix.android.internal.crypto.model.rest.EncryptedFileInfo import im.vector.matrix.android.internal.crypto.model.rest.EncryptedFileKey import timber.log.Timber @@ -51,7 +52,7 @@ object MXEncryptedAttachments { * @param mimetype the mime type * @return the encryption file info */ - fun encryptAttachment(attachmentStream: InputStream, mimetype: String): EncryptionResult? { + fun encryptAttachment(attachmentStream: InputStream, mimetype: String): Try { val t0 = System.currentTimeMillis() val secureRandom = SecureRandom() @@ -115,23 +116,21 @@ object MXEncryptedAttachments { encryptedByteArray = outStream.toByteArray() ) - outStream.close() - Timber.v("Encrypt in " + (System.currentTimeMillis() - t0) + " ms") - return result + return Try.just(result) } catch (oom: OutOfMemoryError) { - Timber.e(oom, "## encryptAttachment failed " + oom.message) + Timber.e(oom, "## encryptAttachment failed") + return Try.Failure(oom) } catch (e: Exception) { - Timber.e(e, "## encryptAttachment failed " + e.message) + Timber.e(e, "## encryptAttachment failed") + return Try.Failure(e) + } finally { + try { + outStream.close() + } catch (e: Exception) { + Timber.e(e, "## encryptAttachment() : fail to close outStream") + } } - - try { - outStream.close() - } catch (e: Exception) { - Timber.e(e, "## encryptAttachment() : fail to close outStream") - } - - return null } /** diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/UploadContentWorker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/UploadContentWorker.kt index 5a8688ee..fd6b15f3 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/UploadContentWorker.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/UploadContentWorker.kt @@ -69,24 +69,30 @@ internal class UploadContentWorker(context: Context, params: WorkerParameters) : val eventId = params.event.eventId ?: return Result.success() val attachment = params.attachment - val isRoomEncrypted = params.isRoomEncrypted + val attachmentFile = try { + File(attachment.path) + } catch (e: Exception) { + Timber.e(e) + return Result.success( + WorkerParamsFactory.toData(params.copy( + lastFailureMessage = e.localizedMessage + )) + ) + } - - val thumbnailData = ThumbnailExtractor.extractThumbnail(params.attachment) - val attachmentFile = createAttachmentFile(attachment) ?: return Result.failure() var uploadedThumbnailUrl: String? = null var uploadedThumbnailEncryptedFileInfo: EncryptedFileInfo? = null - if (thumbnailData != null) { - val contentUploadResponse = if (isRoomEncrypted) { + ThumbnailExtractor.extractThumbnail(params.attachment)?.let { thumbnailData -> + val contentUploadResponse = if (params.isRoomEncrypted) { Timber.v("Encrypt thumbnail") - val encryptionResult = MXEncryptedAttachments.encryptAttachment(ByteArrayInputStream(thumbnailData.bytes), thumbnailData.mimeType) - ?: return Result.failure() + MXEncryptedAttachments.encryptAttachment(ByteArrayInputStream(thumbnailData.bytes), thumbnailData.mimeType) + .flatMap { encryptionResult -> + uploadedThumbnailEncryptedFileInfo = encryptionResult.encryptedFileInfo - uploadedThumbnailEncryptedFileInfo = encryptionResult.encryptedFileInfo - - fileUploader - .uploadByteArray(encryptionResult.encryptedByteArray, "thumb_${attachment.name}", "application/octet-stream") + fileUploader + .uploadByteArray(encryptionResult.encryptedByteArray, "thumb_${attachment.name}", "application/octet-stream") + } } else { fileUploader .uploadByteArray(thumbnailData.bytes, "thumb_${attachment.name}", thumbnailData.mimeType) @@ -107,16 +113,16 @@ internal class UploadContentWorker(context: Context, params: WorkerParameters) : var uploadedFileEncryptedFileInfo: EncryptedFileInfo? = null - val contentUploadResponse = if (isRoomEncrypted) { + val contentUploadResponse = if (params.isRoomEncrypted) { Timber.v("Encrypt file") - val encryptionResult = MXEncryptedAttachments.encryptAttachment(FileInputStream(attachmentFile), attachment.mimeType) - ?: return Result.failure() + MXEncryptedAttachments.encryptAttachment(FileInputStream(attachmentFile), attachment.mimeType) + .flatMap { encryptionResult -> + uploadedFileEncryptedFileInfo = encryptionResult.encryptedFileInfo - uploadedFileEncryptedFileInfo = encryptionResult.encryptedFileInfo - - fileUploader - .uploadByteArray(encryptionResult.encryptedByteArray, attachment.name, "application/octet-stream", progressListener) + fileUploader + .uploadByteArray(encryptionResult.encryptedByteArray, attachment.name, "application/octet-stream", progressListener) + } } else { fileUploader .uploadFile(attachmentFile, attachment.name, attachment.mimeType, progressListener) @@ -129,15 +135,6 @@ internal class UploadContentWorker(context: Context, params: WorkerParameters) : ) } - private fun createAttachmentFile(attachment: ContentAttachmentData): File? { - return try { - File(attachment.path) - } catch (e: Exception) { - Timber.e(e) - null - } - } - private fun handleFailure(params: Params, failure: Throwable): Result { contentUploadStateTracker.setFailure(params.event.eventId!!) return Result.success( From d24ce27903036ffd801c1f019d7605a9961f35f8 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 8 Jul 2019 13:52:25 +0200 Subject: [PATCH 04/16] Add missing call to contentUploadStateTracker.setFailure --- .../android/internal/session/content/UploadContentWorker.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/UploadContentWorker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/UploadContentWorker.kt index fd6b15f3..37f57e4b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/UploadContentWorker.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/UploadContentWorker.kt @@ -73,6 +73,7 @@ internal class UploadContentWorker(context: Context, params: WorkerParameters) : File(attachment.path) } catch (e: Exception) { Timber.e(e) + contentUploadStateTracker.setFailure(params.event.eventId) return Result.success( WorkerParamsFactory.toData(params.copy( lastFailureMessage = e.localizedMessage From 8a5612be3dca636ea9c7cb99e041aa3394a9d5d0 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 8 Jul 2019 14:31:19 +0200 Subject: [PATCH 05/16] Send file: improve UI feedback --- .../content/ContentUploadStateTracker.kt | 7 ++- .../DefaultContentUploadStateTracker.kt | 19 +++++- .../session/content/UploadContentWorker.kt | 22 +++++-- .../helper/ContentUploadStateTrackerBinder.kt | 58 ++++++++++++++----- vector/src/main/res/values/strings_riotX.xml | 6 ++ 5 files changed, 90 insertions(+), 22 deletions(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/content/ContentUploadStateTracker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/content/ContentUploadStateTracker.kt index 9211371d..83074525 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/content/ContentUploadStateTracker.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/content/ContentUploadStateTracker.kt @@ -28,10 +28,11 @@ interface ContentUploadStateTracker { sealed class State { object Idle : State() + object EncryptingThumbnail : State() + data class ProgressThumbnailData(val current: Long, val total: Long) : State() + object Encrypting : State() data class ProgressData(val current: Long, val total: Long) : State() object Success : State() - object Failure : State() + data class Failure(val throwable: Throwable) : State() } - - } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/DefaultContentUploadStateTracker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/DefaultContentUploadStateTracker.kt index fbd7983d..391a90f1 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/DefaultContentUploadStateTracker.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/DefaultContentUploadStateTracker.kt @@ -43,8 +43,8 @@ internal class DefaultContentUploadStateTracker @Inject constructor() : ContentU } } - internal fun setFailure(key: String) { - val failure = ContentUploadStateTracker.State.Failure + internal fun setFailure(key: String, throwable: Throwable) { + val failure = ContentUploadStateTracker.State.Failure(throwable) updateState(key, failure) } @@ -53,6 +53,21 @@ internal class DefaultContentUploadStateTracker @Inject constructor() : ContentU updateState(key, success) } + internal fun setEncryptingThumbnail(key: String) { + val progressData = ContentUploadStateTracker.State.EncryptingThumbnail + updateState(key, progressData) + } + + internal fun setProgressThumbnail(key: String, current: Long, total: Long) { + val progressData = ContentUploadStateTracker.State.ProgressThumbnailData(current, total) + updateState(key, progressData) + } + + internal fun setEncrypting(key: String) { + val progressData = ContentUploadStateTracker.State.Encrypting + updateState(key, progressData) + } + internal fun setProgress(key: String, current: Long, total: Long) { val progressData = ContentUploadStateTracker.State.ProgressData(current, total) updateState(key, progressData) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/UploadContentWorker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/UploadContentWorker.kt index 37f57e4b..0a2aca99 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/UploadContentWorker.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/UploadContentWorker.kt @@ -73,7 +73,7 @@ internal class UploadContentWorker(context: Context, params: WorkerParameters) : File(attachment.path) } catch (e: Exception) { Timber.e(e) - contentUploadStateTracker.setFailure(params.event.eventId) + contentUploadStateTracker.setFailure(params.event.eventId, e) return Result.success( WorkerParamsFactory.toData(params.copy( lastFailureMessage = e.localizedMessage @@ -85,18 +85,31 @@ internal class UploadContentWorker(context: Context, params: WorkerParameters) : var uploadedThumbnailEncryptedFileInfo: EncryptedFileInfo? = null ThumbnailExtractor.extractThumbnail(params.attachment)?.let { thumbnailData -> + val thumbnailProgressListener = object : ProgressRequestBody.Listener { + override fun onProgress(current: Long, total: Long) { + contentUploadStateTracker.setProgressThumbnail(eventId, current, total) + } + } + val contentUploadResponse = if (params.isRoomEncrypted) { Timber.v("Encrypt thumbnail") + contentUploadStateTracker.setEncryptingThumbnail(eventId) MXEncryptedAttachments.encryptAttachment(ByteArrayInputStream(thumbnailData.bytes), thumbnailData.mimeType) .flatMap { encryptionResult -> uploadedThumbnailEncryptedFileInfo = encryptionResult.encryptedFileInfo fileUploader - .uploadByteArray(encryptionResult.encryptedByteArray, "thumb_${attachment.name}", "application/octet-stream") + .uploadByteArray(encryptionResult.encryptedByteArray, + "thumb_${attachment.name}", + "application/octet-stream", + thumbnailProgressListener) } } else { fileUploader - .uploadByteArray(thumbnailData.bytes, "thumb_${attachment.name}", thumbnailData.mimeType) + .uploadByteArray(thumbnailData.bytes, + "thumb_${attachment.name}", + thumbnailData.mimeType, + thumbnailProgressListener) } contentUploadResponse @@ -116,6 +129,7 @@ internal class UploadContentWorker(context: Context, params: WorkerParameters) : val contentUploadResponse = if (params.isRoomEncrypted) { Timber.v("Encrypt file") + contentUploadStateTracker.setEncrypting(eventId) MXEncryptedAttachments.encryptAttachment(FileInputStream(attachmentFile), attachment.mimeType) .flatMap { encryptionResult -> @@ -137,7 +151,7 @@ internal class UploadContentWorker(context: Context, params: WorkerParameters) : } private fun handleFailure(params: Params, failure: Throwable): Result { - contentUploadStateTracker.setFailure(params.event.eventId!!) + contentUploadStateTracker.setFailure(params.event.eventId!!, failure) return Result.success( WorkerParamsFactory.toData( params.copy( diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/ContentUploadStateTrackerBinder.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/ContentUploadStateTrackerBinder.kt index fa4f05f1..a431d409 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/ContentUploadStateTrackerBinder.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/ContentUploadStateTrackerBinder.kt @@ -16,12 +16,12 @@ package im.vector.riotx.features.home.room.detail.timeline.helper -import android.content.Context import android.text.format.Formatter import android.view.View import android.view.ViewGroup import android.widget.ProgressBar import android.widget.TextView +import androidx.core.view.isVisible import im.vector.matrix.android.api.session.content.ContentUploadStateTracker import im.vector.riotx.R import im.vector.riotx.core.di.ActiveSessionHolder @@ -61,10 +61,13 @@ private class ContentMediaProgressUpdater(private val progressLayout: ViewGroup, override fun onUpdate(state: ContentUploadStateTracker.State) { when (state) { - is ContentUploadStateTracker.State.Idle -> handleIdle(state) - is ContentUploadStateTracker.State.Failure -> handleFailure(state) - is ContentUploadStateTracker.State.Success -> handleSuccess(state) - is ContentUploadStateTracker.State.ProgressData -> handleProgress(state) + is ContentUploadStateTracker.State.Idle -> handleIdle(state) + is ContentUploadStateTracker.State.EncryptingThumbnail -> handleEncryptingThumbnail(state) + is ContentUploadStateTracker.State.ProgressThumbnailData -> handleProgressThumbnail(state) + is ContentUploadStateTracker.State.Encrypting -> handleEncrypting(state) + is ContentUploadStateTracker.State.ProgressData -> handleProgress(state) + is ContentUploadStateTracker.State.Failure -> handleFailure(state) + is ContentUploadStateTracker.State.Success -> handleSuccess(state) } } @@ -74,32 +77,61 @@ private class ContentMediaProgressUpdater(private val progressLayout: ViewGroup, progressLayout.visibility = View.VISIBLE val progressBar = progressLayout.findViewById(R.id.mediaProgressBar) val progressTextView = progressLayout.findViewById(R.id.mediaProgressTextView) + progressBar?.isVisible = true + progressBar?.isIndeterminate = true progressBar?.progress = 0 - progressTextView?.text = formatStats(progressLayout.context, 0L, file.length()) + progressTextView?.text = progressLayout.context.getString(R.string.send_file_step_idle) } else { progressLayout.visibility = View.GONE } } - private fun handleFailure(state: ContentUploadStateTracker.State.Failure) { - + private fun handleEncryptingThumbnail(state: ContentUploadStateTracker.State.EncryptingThumbnail) { + _handleEncrypting(R.string.send_file_step_encrypting_thumbnail) } - private fun handleSuccess(state: ContentUploadStateTracker.State.Success) { + private fun handleProgressThumbnail(state: ContentUploadStateTracker.State.ProgressThumbnailData) { + _handleProgress(R.string.send_file_step_sending_thumbnail, state.current, state.total) + } + private fun handleEncrypting(state: ContentUploadStateTracker.State.Encrypting) { + _handleEncrypting(R.string.send_file_step_encrypting_file) } private fun handleProgress(state: ContentUploadStateTracker.State.ProgressData) { + _handleProgress(R.string.send_file_step_sending_file, state.current, state.total) + } + + private fun _handleEncrypting(resId: Int) { progressLayout.visibility = View.VISIBLE - val percent = 100L * (state.current.toFloat() / state.total.toFloat()) val progressBar = progressLayout.findViewById(R.id.mediaProgressBar) val progressTextView = progressLayout.findViewById(R.id.mediaProgressTextView) + progressBar?.isIndeterminate = true + progressTextView?.text = progressLayout.context.getString(resId) + } + + private fun _handleProgress(resId: Int, current: Long, total: Long) { + progressLayout.visibility = View.VISIBLE + val percent = 100L * (current.toFloat() / total.toFloat()) + val progressBar = progressLayout.findViewById(R.id.mediaProgressBar) + val progressTextView = progressLayout.findViewById(R.id.mediaProgressTextView) + progressBar?.isVisible = true + progressBar?.isIndeterminate = false progressBar?.progress = percent.toInt() - progressTextView?.text = formatStats(progressLayout.context, state.current, state.total) + progressTextView?.text = progressLayout.context.getString(resId, + Formatter.formatShortFileSize(progressLayout.context, current), + Formatter.formatShortFileSize(progressLayout.context, total)) } - private fun formatStats(context: Context, current: Long, total: Long): String { - return "${Formatter.formatShortFileSize(context, current)} / ${Formatter.formatShortFileSize(context, total)}" + 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 = state.throwable.localizedMessage } + private fun handleSuccess(state: ContentUploadStateTracker.State.Success) { + // Nothing to do + } } diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml index 1539d84d..50b1cd83 100644 --- a/vector/src/main/res/values/strings_riotX.xml +++ b/vector/src/main/res/values/strings_riotX.xml @@ -5,5 +5,11 @@ Direct Messages + Waiting… + Encrypting thumbnail… + Sending thumbnail (%1$s / %2$s) + Encrypting file… + Sending file (%1$s / %2$s) + \ No newline at end of file From ea7768674687fac0c313ecd0f520bed0a23d925e Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 8 Jul 2019 14:46:14 +0200 Subject: [PATCH 06/16] Send file: cleanup --- .../content/ContentUploadStateTracker.kt | 4 +- .../DefaultContentUploadStateTracker.kt | 4 +- .../helper/ContentUploadStateTrackerBinder.kt | 53 +++++++++---------- 3 files changed, 28 insertions(+), 33 deletions(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/content/ContentUploadStateTracker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/content/ContentUploadStateTracker.kt index 83074525..26273ebb 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/content/ContentUploadStateTracker.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/content/ContentUploadStateTracker.kt @@ -29,9 +29,9 @@ interface ContentUploadStateTracker { sealed class State { object Idle : State() object EncryptingThumbnail : State() - data class ProgressThumbnailData(val current: Long, val total: Long) : State() + data class UploadingThumbnail(val current: Long, val total: Long) : State() object Encrypting : State() - data class ProgressData(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/im/vector/matrix/android/internal/session/content/DefaultContentUploadStateTracker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/DefaultContentUploadStateTracker.kt index 391a90f1..4bb58fe7 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/DefaultContentUploadStateTracker.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/DefaultContentUploadStateTracker.kt @@ -59,7 +59,7 @@ internal class DefaultContentUploadStateTracker @Inject constructor() : ContentU } internal fun setProgressThumbnail(key: String, current: Long, total: Long) { - val progressData = ContentUploadStateTracker.State.ProgressThumbnailData(current, total) + val progressData = ContentUploadStateTracker.State.UploadingThumbnail(current, total) updateState(key, progressData) } @@ -69,7 +69,7 @@ internal class DefaultContentUploadStateTracker @Inject constructor() : ContentU } internal fun setProgress(key: String, current: Long, total: Long) { - val progressData = ContentUploadStateTracker.State.ProgressData(current, total) + val progressData = ContentUploadStateTracker.State.Uploading(current, total) updateState(key, progressData) } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/ContentUploadStateTrackerBinder.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/ContentUploadStateTrackerBinder.kt index a431d409..5df8d38b 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/ContentUploadStateTrackerBinder.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/ContentUploadStateTrackerBinder.kt @@ -26,7 +26,6 @@ import im.vector.matrix.android.api.session.content.ContentUploadStateTracker import im.vector.riotx.R import im.vector.riotx.core.di.ActiveSessionHolder import im.vector.riotx.features.media.ImageContentRenderer -import java.io.File import javax.inject.Inject class ContentUploadStateTrackerBinder @Inject constructor(private val activeSessionHolder: ActiveSessionHolder) { @@ -61,48 +60,43 @@ private class ContentMediaProgressUpdater(private val progressLayout: ViewGroup, override fun onUpdate(state: ContentUploadStateTracker.State) { when (state) { - is ContentUploadStateTracker.State.Idle -> handleIdle(state) - is ContentUploadStateTracker.State.EncryptingThumbnail -> handleEncryptingThumbnail(state) - is ContentUploadStateTracker.State.ProgressThumbnailData -> handleProgressThumbnail(state) - is ContentUploadStateTracker.State.Encrypting -> handleEncrypting(state) - is ContentUploadStateTracker.State.ProgressData -> handleProgress(state) - is ContentUploadStateTracker.State.Failure -> handleFailure(state) - is ContentUploadStateTracker.State.Success -> handleSuccess(state) + is ContentUploadStateTracker.State.Idle -> handleIdle(state) + is ContentUploadStateTracker.State.EncryptingThumbnail -> handleEncryptingThumbnail(state) + 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.Success -> handleSuccess(state) } } private fun handleIdle(state: ContentUploadStateTracker.State.Idle) { - if (mediaData.isLocalFile()) { - val file = File(mediaData.url) - progressLayout.visibility = View.VISIBLE - val progressBar = progressLayout.findViewById(R.id.mediaProgressBar) - val progressTextView = progressLayout.findViewById(R.id.mediaProgressTextView) - progressBar?.isVisible = true - progressBar?.isIndeterminate = true - progressBar?.progress = 0 - progressTextView?.text = progressLayout.context.getString(R.string.send_file_step_idle) - } else { - progressLayout.visibility = View.GONE - } + progressLayout.visibility = View.VISIBLE + val progressBar = progressLayout.findViewById(R.id.mediaProgressBar) + val progressTextView = progressLayout.findViewById(R.id.mediaProgressTextView) + progressBar?.isVisible = true + progressBar?.isIndeterminate = true + progressBar?.progress = 0 + progressTextView?.text = progressLayout.context.getString(R.string.send_file_step_idle) } private fun handleEncryptingThumbnail(state: ContentUploadStateTracker.State.EncryptingThumbnail) { - _handleEncrypting(R.string.send_file_step_encrypting_thumbnail) + doHandleEncrypting(R.string.send_file_step_encrypting_thumbnail) } - private fun handleProgressThumbnail(state: ContentUploadStateTracker.State.ProgressThumbnailData) { - _handleProgress(R.string.send_file_step_sending_thumbnail, state.current, state.total) + private fun handleProgressThumbnail(state: ContentUploadStateTracker.State.UploadingThumbnail) { + doHandleProgress(R.string.send_file_step_sending_thumbnail, state.current, state.total) } private fun handleEncrypting(state: ContentUploadStateTracker.State.Encrypting) { - _handleEncrypting(R.string.send_file_step_encrypting_file) + doHandleEncrypting(R.string.send_file_step_encrypting_file) } - private fun handleProgress(state: ContentUploadStateTracker.State.ProgressData) { - _handleProgress(R.string.send_file_step_sending_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 _handleEncrypting(resId: Int) { + private fun doHandleEncrypting(resId: Int) { progressLayout.visibility = View.VISIBLE val progressBar = progressLayout.findViewById(R.id.mediaProgressBar) val progressTextView = progressLayout.findViewById(R.id.mediaProgressTextView) @@ -110,7 +104,7 @@ private class ContentMediaProgressUpdater(private val progressLayout: ViewGroup, progressTextView?.text = progressLayout.context.getString(resId) } - private fun _handleProgress(resId: Int, current: Long, total: Long) { + private fun doHandleProgress(resId: Int, current: Long, total: Long) { progressLayout.visibility = View.VISIBLE val percent = 100L * (current.toFloat() / total.toFloat()) val progressBar = progressLayout.findViewById(R.id.mediaProgressBar) @@ -128,10 +122,11 @@ private class ContentMediaProgressUpdater(private val progressLayout: ViewGroup, val progressBar = progressLayout.findViewById(R.id.mediaProgressBar) val progressTextView = progressLayout.findViewById(R.id.mediaProgressTextView) progressBar?.isVisible = false + // TODO Red text progressTextView?.text = state.throwable.localizedMessage } private fun handleSuccess(state: ContentUploadStateTracker.State.Success) { - // Nothing to do + progressLayout.visibility = View.GONE } } From c13ab621874bed52e41f6510286044c4e60296e7 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 8 Jul 2019 15:37:39 +0200 Subject: [PATCH 07/16] Fix issue when sending video in encrypted room --- .../android/internal/session/content/UploadContentWorker.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/UploadContentWorker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/UploadContentWorker.kt index 0a2aca99..8e1a0281 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/UploadContentWorker.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/UploadContentWorker.kt @@ -202,9 +202,10 @@ internal class UploadContentWorker(context: Context, params: WorkerParameters) : thumbnailEncryptedFileInfo: EncryptedFileInfo?): MessageVideoContent { return copy( url = if (encryptedFileInfo == null) url else null, + encryptedFileInfo = encryptedFileInfo?.copy(url = url), videoInfo = videoInfo?.copy( thumbnailUrl = if (thumbnailEncryptedFileInfo == null) thumbnailUrl else null, - thumbnailFile = thumbnailEncryptedFileInfo?.copy(url = url) + thumbnailFile = thumbnailEncryptedFileInfo?.copy(url = thumbnailUrl) ) ) } From 1b82ed5abbd961aa4bb1883a2016cb8427b7e617 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 8 Jul 2019 15:41:03 +0200 Subject: [PATCH 08/16] Fix regression --- .../helper/ContentUploadStateTrackerBinder.kt | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/ContentUploadStateTrackerBinder.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/ContentUploadStateTrackerBinder.kt index 5df8d38b..688cac3d 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/ContentUploadStateTrackerBinder.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/ContentUploadStateTrackerBinder.kt @@ -71,13 +71,17 @@ private class ContentMediaProgressUpdater(private val progressLayout: ViewGroup, } private fun handleIdle(state: ContentUploadStateTracker.State.Idle) { - progressLayout.visibility = View.VISIBLE - val progressBar = progressLayout.findViewById(R.id.mediaProgressBar) - val progressTextView = progressLayout.findViewById(R.id.mediaProgressTextView) - progressBar?.isVisible = true - progressBar?.isIndeterminate = true - progressBar?.progress = 0 - progressTextView?.text = progressLayout.context.getString(R.string.send_file_step_idle) + if (mediaData.isLocalFile()) { + progressLayout.isVisible = true + val progressBar = progressLayout.findViewById(R.id.mediaProgressBar) + val progressTextView = progressLayout.findViewById(R.id.mediaProgressTextView) + progressBar?.isVisible = true + progressBar?.isIndeterminate = true + progressBar?.progress = 0 + progressTextView?.text = progressLayout.context.getString(R.string.send_file_step_idle) + } else { + progressLayout.isVisible = false + } } private fun handleEncryptingThumbnail(state: ContentUploadStateTracker.State.EncryptingThumbnail) { From 12bd85e0a99e18a10a5ed8a40d1e44f9f71f9b1d Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 8 Jul 2019 17:07:21 +0200 Subject: [PATCH 09/16] Decrypt video file --- .../api/session/crypto/CryptoService.kt | 9 ++ .../android/internal/crypto/CryptoManager.kt | 8 ++ .../android/internal/crypto/FileDecryptor.kt | 99 +++++++++++++++++++ .../attachments/MXEncryptedAttachments.kt | 3 +- .../matrix/android/internal/util/FileSaver.kt | 37 +++++++ .../im/vector/riotx/core/files/FileSaver.kt | 1 + .../timeline/factory/MessageItemFactory.kt | 57 +++++------ .../features/media/ImageContentRenderer.kt | 1 - .../features/media/VideoContentRenderer.kt | 75 ++++++++++++-- .../media/VideoMediaViewerActivity.kt | 11 +-- .../layout/activity_video_media_viewer.xml | 28 +++++- 11 files changed, 280 insertions(+), 49 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/FileDecryptor.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/FileSaver.kt diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/CryptoService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/CryptoService.kt index 0397b514..06880ceb 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/CryptoService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/CryptoService.kt @@ -26,12 +26,14 @@ import im.vector.matrix.android.api.session.events.model.Content import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.internal.crypto.MXEventDecryptionResult import im.vector.matrix.android.internal.crypto.NewSessionListener +import im.vector.matrix.android.internal.crypto.attachments.ElementToDecrypt import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo import im.vector.matrix.android.internal.crypto.model.MXEncryptEventContentResult import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody +import java.io.File interface CryptoService { @@ -103,6 +105,13 @@ interface CryptoService { fun decryptEventAsync(event: Event, timeline: String, callback: MatrixCallback) + /** + * Decrypt a file. + * Result will be a decrypted file, stored in the cache folder. id parameter will be used to create a sub folder to avoid name collision. + * You can pass the eventId + */ + fun decryptFile(id: String, filename: String, url: String, elementToDecrypt: ElementToDecrypt, callback: MatrixCallback) + fun getEncryptionAlgorithm(roomId: String): String? fun shouldEncryptForInvitedMembers(roomId: String): Boolean diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/CryptoManager.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/CryptoManager.kt index 667f1446..99911749 100755 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/CryptoManager.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/CryptoManager.kt @@ -48,6 +48,7 @@ import im.vector.matrix.android.internal.crypto.actions.SetDeviceVerificationAct import im.vector.matrix.android.internal.crypto.algorithms.IMXEncrypting import im.vector.matrix.android.internal.crypto.algorithms.megolm.MXMegolmEncryptionFactory import im.vector.matrix.android.internal.crypto.algorithms.olm.MXOlmEncryptionFactory +import im.vector.matrix.android.internal.crypto.attachments.ElementToDecrypt import im.vector.matrix.android.internal.crypto.keysbackup.KeysBackup import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo @@ -78,6 +79,7 @@ import im.vector.matrix.android.internal.util.fetchCopied import kotlinx.coroutines.* import org.matrix.olm.OlmManager import timber.log.Timber +import java.io.File import java.util.* import java.util.concurrent.atomic.AtomicBoolean import javax.inject.Inject @@ -112,6 +114,8 @@ internal class CryptoManager @Inject constructor( private val keysBackup: KeysBackup, // private val objectSigner: ObjectSigner, + // File decryptor + private val fileDecryptor: FileDecryptor, // private val oneTimeKeysUploader: OneTimeKeysUploader, // @@ -607,6 +611,10 @@ internal class CryptoManager @Inject constructor( } } + override fun decryptFile(id: String, filename: String, url: String, elementToDecrypt: ElementToDecrypt, callback: MatrixCallback) { + fileDecryptor.decryptFile(id, filename, url, elementToDecrypt, callback) + } + /** * Decrypt an event * diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/FileDecryptor.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/FileDecryptor.kt new file mode 100644 index 00000000..bcdd36f8 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/FileDecryptor.kt @@ -0,0 +1,99 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.crypto + +import android.content.Context +import arrow.core.Try +import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.auth.data.SessionParams +import im.vector.matrix.android.api.session.content.ContentUrlResolver +import im.vector.matrix.android.internal.crypto.attachments.ElementToDecrypt +import im.vector.matrix.android.internal.crypto.attachments.MXEncryptedAttachments +import im.vector.matrix.android.internal.extensions.foldToCallback +import im.vector.matrix.android.internal.session.SessionScope +import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers +import im.vector.matrix.android.internal.util.md5 +import im.vector.matrix.android.internal.util.writeToFile +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import okhttp3.OkHttpClient +import okhttp3.Request +import timber.log.Timber +import java.io.File +import java.io.IOException +import javax.inject.Inject + +@SessionScope +internal class FileDecryptor @Inject constructor(private val context: Context, + private val sessionParams: SessionParams, + private val contentUrlResolver: ContentUrlResolver, + private val coroutineDispatchers: MatrixCoroutineDispatchers) { + + val okHttpClient = OkHttpClient() + + fun decryptFile(id: String, + fileName: String, + url: String, + elementToDecrypt: ElementToDecrypt, + callback: MatrixCallback) { + GlobalScope.launch(coroutineDispatchers.main) { + withContext(coroutineDispatchers.io) { + Try { + // Create dir tree: + // /DF/// + val tmpFolderRoot = File(context.cacheDir, "DF") + val tmpFolderUser = File(tmpFolderRoot, sessionParams.credentials.userId.md5()) + val tmpFolder = File(tmpFolderUser, id.md5()) + + if (!tmpFolder.exists()) { + tmpFolder.mkdirs() + } + + File(tmpFolder, fileName) + }.map { destFile -> + if (!destFile.exists()) { + Try { + Timber.v("## decrypt file") + + val resolvedUrl = contentUrlResolver.resolveFullSize(url) ?: throw IllegalArgumentException("url is null") + + val request = Request.Builder() + .url(resolvedUrl) + .build() + + val response = okHttpClient.newCall(request).execute() + val inputStream = response.body()?.byteStream() + Timber.v("Response size ${response.body()?.contentLength()} - Stream available: ${inputStream?.available()}") + if (!response.isSuccessful) { + throw IOException() + } + + MXEncryptedAttachments.decryptAttachment(inputStream, elementToDecrypt) ?: throw IllegalStateException("Decryption error") + } + .map { inputStream -> + writeToFile(inputStream, destFile) + } + } + + destFile + } + } + .foldToCallback(callback) + } + } +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/attachments/MXEncryptedAttachments.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/attachments/MXEncryptedAttachments.kt index 02c8931f..d8c62709 100755 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/attachments/MXEncryptedAttachments.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/attachments/MXEncryptedAttachments.kt @@ -16,7 +16,6 @@ package im.vector.matrix.android.internal.crypto.attachments -import android.text.TextUtils import android.util.Base64 import arrow.core.Try import im.vector.matrix.android.internal.crypto.model.rest.EncryptedFileInfo @@ -198,7 +197,7 @@ object MXEncryptedAttachments { val currentDigestValue = base64ToUnpaddedBase64(Base64.encodeToString(messageDigest.digest(), Base64.DEFAULT)) - if (!TextUtils.equals(elementToDecrypt.sha256, currentDigestValue)) { + if (elementToDecrypt.sha256 != currentDigestValue) { Timber.e("## decryptAttachment() : Digest value mismatch") outStream.close() return null diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/FileSaver.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/FileSaver.kt new file mode 100644 index 00000000..9654f5e0 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/FileSaver.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.util + +import androidx.annotation.WorkerThread +import okio.Okio +import java.io.File +import java.io.InputStream + +/** + * Save an input stream to a file with Okio + */ +@WorkerThread +fun writeToFile(inputStream: InputStream, outputFile: File) { + val source = Okio.buffer(Okio.source(inputStream)) + val sink = Okio.buffer(Okio.sink(outputFile)) + + source.use { input -> + sink.use { output -> + output.writeAll(input) + } + } +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/core/files/FileSaver.kt b/vector/src/main/java/im/vector/riotx/core/files/FileSaver.kt index 23798587..d6b0056c 100644 --- a/vector/src/main/java/im/vector/riotx/core/files/FileSaver.kt +++ b/vector/src/main/java/im/vector/riotx/core/files/FileSaver.kt @@ -23,6 +23,7 @@ import arrow.core.Try import okio.Okio import timber.log.Timber import java.io.File +import java.io.InputStream /** * Save a string to a file with Okio diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt index 92ec26dc..17328ca0 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt @@ -29,14 +29,7 @@ import im.vector.matrix.android.api.permalinks.MatrixPermalinkSpan import im.vector.matrix.android.api.session.events.model.RelationType import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.room.model.EditAggregatedSummary -import im.vector.matrix.android.api.session.room.model.message.MessageAudioContent -import im.vector.matrix.android.api.session.room.model.message.MessageContent -import im.vector.matrix.android.api.session.room.model.message.MessageEmoteContent -import im.vector.matrix.android.api.session.room.model.message.MessageFileContent -import im.vector.matrix.android.api.session.room.model.message.MessageImageContent -import im.vector.matrix.android.api.session.room.model.message.MessageNoticeContent -import im.vector.matrix.android.api.session.room.model.message.MessageTextContent -import im.vector.matrix.android.api.session.room.model.message.MessageVideoContent +import im.vector.matrix.android.api.session.room.model.message.* import im.vector.matrix.android.api.session.room.send.SendState import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt @@ -87,9 +80,9 @@ class MessageItemFactory @Inject constructor( val messageContent: MessageContent = event.annotations?.editSummary?.aggregatedContent?.toModel() - ?: event.root.getClearContent().toModel() - ?: //Malformed content, we should echo something on screen - return DefaultItem_().text(stringProvider.getString(R.string.malformed_message)) + ?: event.root.getClearContent().toModel() + ?: //Malformed content, we should echo something on screen + return DefaultItem_().text(stringProvider.getString(R.string.malformed_message)) if (messageContent.relatesTo?.type == RelationType.REPLACE) { // ignore replace event, the targeted id is already edited @@ -99,16 +92,16 @@ class MessageItemFactory @Inject constructor( // val ev = all.toModel() return when (messageContent) { is MessageEmoteContent -> buildEmoteMessageItem(messageContent, - informationData, - event.annotations?.editSummary, - highlight, - callback) + informationData, + event.annotations?.editSummary, + highlight, + callback) is MessageTextContent -> buildTextMessageItem(event.sendState, - messageContent, - informationData, - event.annotations?.editSummary, - highlight, - callback + messageContent, + informationData, + event.annotations?.editSummary, + highlight, + callback ) is MessageImageContent -> buildImageMessageItem(messageContent, informationData, highlight, callback) is MessageNoticeContent -> buildNoticeMessageItem(messageContent, informationData, highlight, callback) @@ -142,7 +135,7 @@ class MessageItemFactory @Inject constructor( })) .longClickListener { view -> return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view) - ?: false + ?: false } } @@ -165,7 +158,7 @@ class MessageItemFactory @Inject constructor( })) .longClickListener { view -> return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view) - ?: false + ?: false } .clickListener( DebouncedClickListener(View.OnClickListener { _ -> @@ -218,7 +211,7 @@ class MessageItemFactory @Inject constructor( })) .longClickListener { view -> return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view) - ?: false + ?: false } } @@ -239,8 +232,10 @@ class MessageItemFactory @Inject constructor( ) val videoData = VideoContentRenderer.Data( + eventId = informationData.eventId, filename = messageContent.body, - videoUrl = messageContent.url, + url = messageContent.encryptedFileInfo?.url ?: messageContent.url, + elementToDecrypt = messageContent.encryptedFileInfo?.toElementToDecrypt(), thumbnailMediaData = thumbnailData ) @@ -262,7 +257,7 @@ class MessageItemFactory @Inject constructor( .clickListener { view -> callback?.onVideoMessageClicked(messageContent, videoData, view) } .longClickListener { view -> return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view) - ?: false + ?: false } } @@ -302,7 +297,7 @@ class MessageItemFactory @Inject constructor( })) .longClickListener { view -> return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view) - ?: false + ?: false } } @@ -334,9 +329,9 @@ class MessageItemFactory @Inject constructor( //nop } }, - editStart, - editEnd, - Spanned.SPAN_INCLUSIVE_EXCLUSIVE) + editStart, + editEnd, + Spanned.SPAN_INCLUSIVE_EXCLUSIVE) return spannable } @@ -372,7 +367,7 @@ class MessageItemFactory @Inject constructor( })) .longClickListener { view -> return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view) - ?: false + ?: false } } @@ -408,7 +403,7 @@ class MessageItemFactory @Inject constructor( })) .longClickListener { view -> return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view) - ?: false + ?: false } } diff --git a/vector/src/main/java/im/vector/riotx/features/media/ImageContentRenderer.kt b/vector/src/main/java/im/vector/riotx/features/media/ImageContentRenderer.kt index 412043e8..4d68d869 100644 --- a/vector/src/main/java/im/vector/riotx/features/media/ImageContentRenderer.kt +++ b/vector/src/main/java/im/vector/riotx/features/media/ImageContentRenderer.kt @@ -100,7 +100,6 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder: return } - // TODO DECRYPT_FILE Decrypt file imageView.showImage( Uri.parse(thumbnail), Uri.parse(fullSize) diff --git a/vector/src/main/java/im/vector/riotx/features/media/VideoContentRenderer.kt b/vector/src/main/java/im/vector/riotx/features/media/VideoContentRenderer.kt index 5cba0084..c369f775 100644 --- a/vector/src/main/java/im/vector/riotx/features/media/VideoContentRenderer.kt +++ b/vector/src/main/java/im/vector/riotx/features/media/VideoContentRenderer.kt @@ -18,26 +18,87 @@ package im.vector.riotx.features.media import android.os.Parcelable import android.widget.ImageView +import android.widget.ProgressBar +import android.widget.TextView import android.widget.VideoView +import androidx.core.view.isVisible +import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.internal.crypto.attachments.ElementToDecrypt +import im.vector.riotx.R import im.vector.riotx.core.di.ActiveSessionHolder +import im.vector.riotx.core.error.ErrorFormatter import kotlinx.android.parcel.Parcelize +import timber.log.Timber +import java.io.File import javax.inject.Inject -class VideoContentRenderer @Inject constructor(private val activeSessionHolder: ActiveSessionHolder){ +class VideoContentRenderer @Inject constructor(private val activeSessionHolder: ActiveSessionHolder, + private val errorFormatter: ErrorFormatter) { - // TODO DECRYPT_FILE Encrypted data @Parcelize data class Data( + val eventId: String, val filename: String, - val videoUrl: String?, + val url: String?, + val elementToDecrypt: ElementToDecrypt?, val thumbnailMediaData: ImageContentRenderer.Data ) : Parcelable - fun render(data: Data, thumbnailView: ImageView, videoView: VideoView) { + fun render(data: Data, + thumbnailView: ImageView, + loadingView: ProgressBar, + videoView: VideoView, + errorView: TextView) { val contentUrlResolver = activeSessionHolder.getActiveSession().contentUrlResolver() - val resolvedUrl = contentUrlResolver.resolveFullSize(data.videoUrl) - videoView.setVideoPath(resolvedUrl) - videoView.start() + + if (data.elementToDecrypt != null) { + Timber.v("Decrypt video") + videoView.isVisible = false + + if (data.url == null) { + loadingView.isVisible = false + errorView.isVisible = true + errorView.setText(R.string.unknown_error) + } else { + thumbnailView.isVisible = true + loadingView.isVisible = true + + activeSessionHolder.getActiveSession() + .decryptFile(data.eventId, + data.filename, + data.url, + data.elementToDecrypt, + object : MatrixCallback { + override fun onSuccess(data: File) { + thumbnailView.isVisible = false + loadingView.isVisible = false + videoView.isVisible = true + + videoView.setVideoPath(data.path) + videoView.start() + } + + override fun onFailure(failure: Throwable) { + loadingView.isVisible = false + errorView.isVisible = true + errorView.text = errorFormatter.toHumanReadable(failure) + } + }) + } + } else { + thumbnailView.isVisible = false + loadingView.isVisible = false + + val resolvedUrl = contentUrlResolver.resolveFullSize(data.url) + + if (resolvedUrl == null) { + errorView.isVisible = true + errorView.setText(R.string.unknown_error) + } else { + videoView.setVideoPath(resolvedUrl) + videoView.start() + } + } } } \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/media/VideoMediaViewerActivity.kt b/vector/src/main/java/im/vector/riotx/features/media/VideoMediaViewerActivity.kt index e8e4f213..093e257c 100644 --- a/vector/src/main/java/im/vector/riotx/features/media/VideoMediaViewerActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/media/VideoMediaViewerActivity.kt @@ -28,6 +28,7 @@ import javax.inject.Inject class VideoMediaViewerActivity : VectorBaseActivity() { + @Inject lateinit var imageContentRenderer: ImageContentRenderer @Inject lateinit var videoContentRenderer: VideoContentRenderer override fun injectWith(injector: ScreenComponent) { @@ -38,12 +39,10 @@ class VideoMediaViewerActivity : VectorBaseActivity() { super.onCreate(savedInstanceState) setContentView(im.vector.riotx.R.layout.activity_video_media_viewer) val mediaData = intent.getParcelableExtra(EXTRA_MEDIA_DATA) - if (mediaData.videoUrl.isNullOrEmpty()) { - finish() - } else { - configureToolbar(videoMediaViewerToolbar, mediaData) - videoContentRenderer.render(mediaData, videoMediaViewerThumbnailView, videoMediaViewerVideoView) - } + + configureToolbar(videoMediaViewerToolbar, mediaData) + imageContentRenderer.render(mediaData.thumbnailMediaData, ImageContentRenderer.Mode.FULL_SIZE, videoMediaViewerThumbnailView) + videoContentRenderer.render(mediaData, videoMediaViewerThumbnailView, videoMediaViewerLoading, videoMediaViewerVideoView, videoMediaViewerErrorView) } private fun configureToolbar(toolbar: Toolbar, mediaData: VideoContentRenderer.Data) { diff --git a/vector/src/main/res/layout/activity_video_media_viewer.xml b/vector/src/main/res/layout/activity_video_media_viewer.xml index e1f243b5..f21e6daf 100644 --- a/vector/src/main/res/layout/activity_video_media_viewer.xml +++ b/vector/src/main/res/layout/activity_video_media_viewer.xml @@ -15,6 +15,7 @@ --> @@ -32,12 +33,35 @@ + android:layout_height="match_parent" + android:visibility="gone" + tools:visibility="visible" /> + + + android:layout_height="match_parent" + android:visibility="gone" /> + + From a07f8b615e83e1d28238f56de64fa2893372d73c Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 8 Jul 2019 19:06:17 +0200 Subject: [PATCH 10/16] Download file - WIP --- .../matrix/android/api/session/Session.kt | 2 + .../api/session/crypto/CryptoService.kt | 7 -- .../android/api/session/file/FileService.kt | 52 ++++++++++++++ .../android/internal/crypto/CryptoManager.kt | 6 -- .../DefaultFileService.kt} | 72 ++++++++++++------- .../internal/session/DefaultSession.kt | 24 ++++--- .../internal/session/room/RoomModule.kt | 4 ++ .../home/room/detail/RoomDetailActions.kt | 2 + .../home/room/detail/RoomDetailFragment.kt | 25 ++++--- .../home/room/detail/RoomDetailViewModel.kt | 49 +++++++++++++ .../timeline/TimelineEventController.kt | 2 +- .../timeline/factory/MessageItemFactory.kt | 2 +- .../features/media/VideoContentRenderer.kt | 5 +- vector/src/main/res/values/strings_riotX.xml | 3 + 14 files changed, 193 insertions(+), 62 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/file/FileService.kt rename matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/{crypto/FileDecryptor.kt => session/DefaultFileService.kt} (54%) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/Session.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/Session.kt index 864a9d6f..3621c6ad 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/Session.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/Session.kt @@ -24,6 +24,7 @@ import im.vector.matrix.android.api.session.cache.CacheService import im.vector.matrix.android.api.session.content.ContentUploadStateTracker import im.vector.matrix.android.api.session.content.ContentUrlResolver import im.vector.matrix.android.api.session.crypto.CryptoService +import im.vector.matrix.android.api.session.file.FileService import im.vector.matrix.android.api.session.group.GroupService import im.vector.matrix.android.api.session.pushers.PushersService import im.vector.matrix.android.api.session.room.RoomDirectoryService @@ -46,6 +47,7 @@ interface Session : CacheService, SignOutService, FilterService, + FileService, PushRuleService, PushersService { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/CryptoService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/CryptoService.kt index 06880ceb..8e933426 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/CryptoService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/CryptoService.kt @@ -105,13 +105,6 @@ interface CryptoService { fun decryptEventAsync(event: Event, timeline: String, callback: MatrixCallback) - /** - * Decrypt a file. - * Result will be a decrypted file, stored in the cache folder. id parameter will be used to create a sub folder to avoid name collision. - * You can pass the eventId - */ - fun decryptFile(id: String, filename: String, url: String, elementToDecrypt: ElementToDecrypt, callback: MatrixCallback) - fun getEncryptionAlgorithm(roomId: String): String? fun shouldEncryptForInvitedMembers(roomId: String): Boolean diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/file/FileService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/file/FileService.kt new file mode 100644 index 00000000..296b7e15 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/file/FileService.kt @@ -0,0 +1,52 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.api.session.file + +import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.internal.crypto.attachments.ElementToDecrypt +import java.io.File + + +/** + * This interface defines methods to get files. + */ +interface FileService { + + enum class DownloadMode { + /** + * Download file in external storage + */ + TO_EXPORT, + /** + * Download file in cache + */ + FOR_INTERNAL_USE + } + + /** + * Download a file. + * Result will be a decrypted file, stored in the cache folder. id parameter will be used to create a sub folder to avoid name collision. + * You can pass the eventId + */ + fun downloadFile( + downloadMode: DownloadMode, + id: String, + fileName: String, + url: String?, + elementToDecrypt: ElementToDecrypt?, + callback: MatrixCallback) +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/CryptoManager.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/CryptoManager.kt index 99911749..342c7ee5 100755 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/CryptoManager.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/CryptoManager.kt @@ -114,8 +114,6 @@ internal class CryptoManager @Inject constructor( private val keysBackup: KeysBackup, // private val objectSigner: ObjectSigner, - // File decryptor - private val fileDecryptor: FileDecryptor, // private val oneTimeKeysUploader: OneTimeKeysUploader, // @@ -611,10 +609,6 @@ internal class CryptoManager @Inject constructor( } } - override fun decryptFile(id: String, filename: String, url: String, elementToDecrypt: ElementToDecrypt, callback: MatrixCallback) { - fileDecryptor.decryptFile(id, filename, url, elementToDecrypt, callback) - } - /** * Decrypt an event * diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/FileDecryptor.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultFileService.kt similarity index 54% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/FileDecryptor.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultFileService.kt index bcdd36f8..b23f56bc 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/FileDecryptor.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultFileService.kt @@ -14,17 +14,18 @@ * limitations under the License. */ -package im.vector.matrix.android.internal.crypto +package im.vector.matrix.android.internal.session import android.content.Context +import android.os.Environment import arrow.core.Try import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.auth.data.SessionParams import im.vector.matrix.android.api.session.content.ContentUrlResolver +import im.vector.matrix.android.api.session.file.FileService import im.vector.matrix.android.internal.crypto.attachments.ElementToDecrypt import im.vector.matrix.android.internal.crypto.attachments.MXEncryptedAttachments import im.vector.matrix.android.internal.extensions.foldToCallback -import im.vector.matrix.android.internal.session.SessionScope import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers import im.vector.matrix.android.internal.util.md5 import im.vector.matrix.android.internal.util.writeToFile @@ -38,33 +39,29 @@ import java.io.File import java.io.IOException import javax.inject.Inject -@SessionScope -internal class FileDecryptor @Inject constructor(private val context: Context, - private val sessionParams: SessionParams, - private val contentUrlResolver: ContentUrlResolver, - private val coroutineDispatchers: MatrixCoroutineDispatchers) { +internal class DefaultFileService @Inject constructor(private val context: Context, + private val sessionParams: SessionParams, + private val contentUrlResolver: ContentUrlResolver, + private val coroutineDispatchers: MatrixCoroutineDispatchers) : FileService { val okHttpClient = OkHttpClient() - fun decryptFile(id: String, - fileName: String, - url: String, - elementToDecrypt: ElementToDecrypt, - callback: MatrixCallback) { + /** + * Download file in the cache folder, and eventually decrypt it + * TODO implement clear file, to delete "MF" + */ + override fun downloadFile(downloadMode: FileService.DownloadMode, + id: String, + fileName: String, + url: String?, + elementToDecrypt: ElementToDecrypt?, + callback: MatrixCallback) { GlobalScope.launch(coroutineDispatchers.main) { withContext(coroutineDispatchers.io) { Try { - // Create dir tree: - // /DF/// - val tmpFolderRoot = File(context.cacheDir, "DF") - val tmpFolderUser = File(tmpFolderRoot, sessionParams.credentials.userId.md5()) - val tmpFolder = File(tmpFolderUser, id.md5()) + val folder = getFolder(downloadMode, id) - if (!tmpFolder.exists()) { - tmpFolder.mkdirs() - } - - File(tmpFolder, fileName) + File(folder, fileName) }.map { destFile -> if (!destFile.exists()) { Try { @@ -79,11 +76,16 @@ internal class FileDecryptor @Inject constructor(private val context: Context, val response = okHttpClient.newCall(request).execute() val inputStream = response.body()?.byteStream() Timber.v("Response size ${response.body()?.contentLength()} - Stream available: ${inputStream?.available()}") - if (!response.isSuccessful) { + if (!response.isSuccessful + || inputStream == null) { throw IOException() } - MXEncryptedAttachments.decryptAttachment(inputStream, elementToDecrypt) ?: throw IllegalStateException("Decryption error") + if (elementToDecrypt != null) { + MXEncryptedAttachments.decryptAttachment(inputStream, elementToDecrypt) ?: throw IllegalStateException("Decryption error") + } else { + inputStream + } } .map { inputStream -> writeToFile(inputStream, destFile) @@ -96,4 +98,24 @@ internal class FileDecryptor @Inject constructor(private val context: Context, .foldToCallback(callback) } } -} \ No newline at end of file + + private fun getFolder(downloadMode: FileService.DownloadMode, id: String): File { + return when (downloadMode) { + FileService.DownloadMode.FOR_INTERNAL_USE -> { + // Create dir tree (MF stands for Matrix File): + // /MF/// + val tmpFolderRoot = File(context.cacheDir, "MF") + val tmpFolderUser = File(tmpFolderRoot, sessionParams.credentials.userId.md5()) + File(tmpFolderUser, id.md5()) + } + FileService.DownloadMode.TO_EXPORT -> { + Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) + } + } + .also { folder -> + if (!folder.exists()) { + folder.mkdirs() + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt index 32311853..72fdadfc 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt @@ -21,7 +21,6 @@ import android.os.Looper import androidx.annotation.MainThread import androidx.lifecycle.LiveData import androidx.work.WorkManager -import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.auth.data.SessionParams import im.vector.matrix.android.api.pushrules.PushRuleService @@ -30,6 +29,7 @@ import im.vector.matrix.android.api.session.cache.CacheService import im.vector.matrix.android.api.session.content.ContentUploadStateTracker import im.vector.matrix.android.api.session.content.ContentUrlResolver import im.vector.matrix.android.api.session.crypto.CryptoService +import im.vector.matrix.android.api.session.file.FileService import im.vector.matrix.android.api.session.group.GroupService import im.vector.matrix.android.api.session.pushers.PushersService import im.vector.matrix.android.api.session.room.RoomDirectoryService @@ -61,20 +61,22 @@ internal class DefaultSession @Inject constructor(override val sessionParams: Se private val pushRuleService: PushRuleService, private val pushersService: PushersService, private val cryptoService: CryptoManager, + private val fileService: FileService, private val syncThread: SyncThread, private val contentUrlResolver: ContentUrlResolver, private val contentUploadProgressTracker: ContentUploadStateTracker) : Session, - RoomService by roomService, - RoomDirectoryService by roomDirectoryService, - GroupService by groupService, - UserService by userService, - CryptoService by cryptoService, - CacheService by cacheService, - SignOutService by signOutService, - FilterService by filterService, - PushRuleService by pushRuleService, - PushersService by pushersService { + RoomService by roomService, + RoomDirectoryService by roomDirectoryService, + GroupService by groupService, + UserService by userService, + CryptoService by cryptoService, + CacheService by cacheService, + SignOutService by signOutService, + FilterService by filterService, + FileService by fileService, + PushRuleService by pushRuleService, + PushersService by pushersService { private var isOpen = false diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt index 9cf6ac69..09322e6a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt @@ -19,6 +19,7 @@ package im.vector.matrix.android.internal.session.room import dagger.Binds import dagger.Module import dagger.Provides +import im.vector.matrix.android.api.session.file.FileService import im.vector.matrix.android.api.session.room.RoomDirectoryService import im.vector.matrix.android.api.session.room.RoomService import im.vector.matrix.android.api.session.room.members.MembershipService @@ -27,6 +28,7 @@ import im.vector.matrix.android.api.session.room.read.ReadService import im.vector.matrix.android.api.session.room.send.SendService import im.vector.matrix.android.api.session.room.state.StateService import im.vector.matrix.android.api.session.room.timeline.TimelineService +import im.vector.matrix.android.internal.session.DefaultFileService import im.vector.matrix.android.internal.session.SessionScope import im.vector.matrix.android.internal.session.room.create.CreateRoomTask import im.vector.matrix.android.internal.session.room.create.DefaultCreateRoomTask @@ -138,4 +140,6 @@ internal abstract class RoomModule { @Binds abstract fun bindTimelineService(timelineService: DefaultTimelineService): TimelineService + @Binds + abstract fun bindFileService(fileService: DefaultFileService): FileService } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailActions.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailActions.kt index df5cfc96..d52b16ca 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailActions.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailActions.kt @@ -18,6 +18,7 @@ package im.vector.riotx.features.home.room.detail import com.jaiselrahman.filepicker.model.MediaFile import im.vector.matrix.android.api.session.room.model.EditAggregatedSummary +import im.vector.matrix.android.api.session.room.model.message.MessageFileContent import im.vector.matrix.android.api.session.room.timeline.Timeline import im.vector.matrix.android.api.session.room.timeline.TimelineEvent @@ -33,6 +34,7 @@ sealed class RoomDetailActions { data class UpdateQuickReactAction(val targetEventId: String, val selectedReaction: String, val add: Boolean) : RoomDetailActions() data class ShowEditHistoryAction(val event: String, val editAggregatedSummary: EditAggregatedSummary) : RoomDetailActions() data class NavigateToEvent(val eventId: String, val position: Int?) : RoomDetailActions() + data class DownloadFile(val eventId: String, val messageFileContent: MessageFileContent) : RoomDetailActions() object AcceptInvite : RoomDetailActions() object RejectInvite : RoomDetailActions() diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt index 72c17f63..ed137f24 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt @@ -63,19 +63,14 @@ import im.vector.riotx.R import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.dialogs.DialogListItem import im.vector.riotx.core.epoxy.LayoutManagerStateRestorer +import im.vector.riotx.core.error.ErrorFormatter import im.vector.riotx.core.extensions.hideKeyboard import im.vector.riotx.core.extensions.observeEvent import im.vector.riotx.core.extensions.setTextOrHide +import im.vector.riotx.core.files.addEntryToDownloadManager import im.vector.riotx.core.glide.GlideApp import im.vector.riotx.core.platform.VectorBaseFragment -import im.vector.riotx.core.utils.PERMISSIONS_FOR_TAKING_PHOTO -import im.vector.riotx.core.utils.PERMISSION_REQUEST_CODE_LAUNCH_CAMERA -import im.vector.riotx.core.utils.PERMISSION_REQUEST_CODE_LAUNCH_NATIVE_CAMERA -import im.vector.riotx.core.utils.PERMISSION_REQUEST_CODE_LAUNCH_NATIVE_VIDEO_CAMERA -import im.vector.riotx.core.utils.checkPermissions -import im.vector.riotx.core.utils.copyToClipboard -import im.vector.riotx.core.utils.openCamera -import im.vector.riotx.core.utils.shareMedia +import im.vector.riotx.core.utils.* import im.vector.riotx.features.autocomplete.command.AutocompleteCommandPresenter import im.vector.riotx.features.autocomplete.command.CommandAutocompletePolicy import im.vector.riotx.features.autocomplete.user.AutocompleteUserPresenter @@ -180,6 +175,7 @@ class RoomDetailFragment : @Inject lateinit var notificationDrawerManager: NotificationDrawerManager @Inject lateinit var roomDetailViewModelFactory: RoomDetailViewModel.Factory @Inject lateinit var textComposerViewModelFactory: TextComposerViewModel.Factory + @Inject lateinit var errorFormatter: ErrorFormatter private lateinit var scrollOnNewMessageCallback: ScrollOnNewMessageCallback private lateinit var scrollOnHighlightedEventCallback: ScrollOnHighlightedEventCallback @@ -220,6 +216,15 @@ class RoomDetailFragment : scrollOnHighlightedEventCallback.scheduleScrollTo(it) } + roomDetailViewModel.downloadedFileEvent.observeEvent(this) { downloadFileState -> + if (downloadFileState.throwable != null) { + requireActivity().toast(errorFormatter.toHumanReadable(downloadFileState.throwable)) + } else if (downloadFileState.file != null) { + requireActivity().toast(getString(R.string.downloaded_file, downloadFileState.file.path)) + addEntryToDownloadManager(requireContext(), downloadFileState.file, downloadFileState.mimeType) + } + } + roomDetailViewModel.selectSubscribe( RoomDetailViewState::sendMode, RoomDetailViewState::selectedEvent, @@ -615,8 +620,8 @@ class RoomDetailFragment : startActivity(intent) } - override fun onFileMessageClicked(messageFileContent: MessageFileContent) { - vectorBaseActivity.notImplemented("open file") + override fun onFileMessageClicked(eventId: String, messageFileContent: MessageFileContent) { + roomDetailViewModel.process(RoomDetailActions.DownloadFile(eventId, messageFileContent)) } override fun onAudioMessageClicked(messageAudioContent: MessageAudioContent) { diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt index 83dcb415..0c1b4523 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt @@ -16,6 +16,7 @@ package im.vector.riotx.features.home.room.detail +import android.content.ClipDescription import android.net.Uri import android.text.TextUtils import androidx.lifecycle.LiveData @@ -31,10 +32,12 @@ import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.content.ContentAttachmentData import im.vector.matrix.android.api.session.events.model.toModel +import im.vector.matrix.android.api.session.file.FileService import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.api.session.room.model.message.MessageContent import im.vector.matrix.android.api.session.room.model.message.MessageType import im.vector.matrix.android.api.session.room.timeline.TimelineEvent +import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt import im.vector.matrix.rx.rx import im.vector.riotx.R import im.vector.riotx.core.intent.getFilenameFromUri @@ -50,6 +53,7 @@ import io.reactivex.rxkotlin.subscribeBy import org.commonmark.parser.Parser import org.commonmark.renderer.html.HtmlRenderer import timber.log.Timber +import java.io.File import java.text.SimpleDateFormat import java.util.* import java.util.concurrent.TimeUnit @@ -113,6 +117,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro is RoomDetailActions.EnterEditMode -> handleEditAction(action) is RoomDetailActions.EnterQuoteMode -> handleQuoteAction(action) is RoomDetailActions.EnterReplyMode -> handleReplyAction(action) + is RoomDetailActions.DownloadFile -> handleDownloadFile(action) is RoomDetailActions.NavigateToEvent -> handleNavigateToEvent(action) else -> Timber.e("Unhandled Action: $action") } @@ -149,6 +154,10 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro val navigateToEvent: LiveData> get() = _navigateToEvent + private val _downloadedFileEvent = MutableLiveData>() + val downloadedFileEvent: LiveData> + get() = _downloadedFileEvent + // PRIVATE METHODS ***************************************************************************** @@ -433,6 +442,46 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro } } + data class DownloadFileState( + val mimeType: String, + val file: File?, + val throwable: Throwable? + ) + + private fun handleDownloadFile(action: RoomDetailActions.DownloadFile) { + session.downloadFile( + FileService.DownloadMode.TO_EXPORT, + action.eventId, + action.messageFileContent.filename ?: "file.dat", + action.messageFileContent.url, + action.messageFileContent.encryptedFileInfo?.toElementToDecrypt(), + object : MatrixCallback { + override fun onSuccess(data: File) { + _downloadedFileEvent.postValue(LiveEvent(DownloadFileState( + // Mimetype default to plain text, should not be used + action.messageFileContent.encryptedFileInfo?.mimetype + ?: action.messageFileContent.info?.mimeType + ?: ClipDescription.MIMETYPE_TEXT_PLAIN, + data, + null + ))) + } + + override fun onFailure(failure: Throwable) { + _downloadedFileEvent.postValue(LiveEvent(DownloadFileState( + // Mimetype default to plain text, should not be used + action.messageFileContent.encryptedFileInfo?.mimetype + ?: action.messageFileContent.info?.mimeType + ?: ClipDescription.MIMETYPE_TEXT_PLAIN, + null, + failure + ))) + } + }) + + } + + private fun handleNavigateToEvent(action: RoomDetailActions.NavigateToEvent) { val targetEventId = action.eventId diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt index ec1ea87e..0ecfa216 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt @@ -57,7 +57,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Tim fun onEncryptedMessageClicked(informationData: MessageInformationData, view: View) fun onImageMessageClicked(messageImageContent: MessageImageContent, mediaData: ImageContentRenderer.Data, view: View) fun onVideoMessageClicked(messageVideoContent: MessageVideoContent, mediaData: VideoContentRenderer.Data, view: View) - fun onFileMessageClicked(messageFileContent: MessageFileContent) + fun onFileMessageClicked(eventId: String, messageFileContent: MessageFileContent) fun onAudioMessageClicked(messageAudioContent: MessageAudioContent) fun onEditedDecorationClicked(informationData: MessageInformationData, editAggregatedSummary: EditAggregatedSummary?) } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt index 17328ca0..d2289380 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt @@ -162,7 +162,7 @@ class MessageItemFactory @Inject constructor( } .clickListener( DebouncedClickListener(View.OnClickListener { _ -> - callback?.onFileMessageClicked(messageContent) + callback?.onFileMessageClicked(informationData.eventId, messageContent) })) } diff --git a/vector/src/main/java/im/vector/riotx/features/media/VideoContentRenderer.kt b/vector/src/main/java/im/vector/riotx/features/media/VideoContentRenderer.kt index c369f775..80114676 100644 --- a/vector/src/main/java/im/vector/riotx/features/media/VideoContentRenderer.kt +++ b/vector/src/main/java/im/vector/riotx/features/media/VideoContentRenderer.kt @@ -23,6 +23,7 @@ import android.widget.TextView import android.widget.VideoView import androidx.core.view.isVisible import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.session.file.FileService import im.vector.matrix.android.internal.crypto.attachments.ElementToDecrypt import im.vector.riotx.R import im.vector.riotx.core.di.ActiveSessionHolder @@ -64,7 +65,9 @@ class VideoContentRenderer @Inject constructor(private val activeSessionHolder: loadingView.isVisible = true activeSessionHolder.getActiveSession() - .decryptFile(data.eventId, + .downloadFile( + FileService.DownloadMode.FOR_INTERNAL_USE, + data.eventId, data.filename, data.url, data.elementToDecrypt, diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml index 50b1cd83..8919438a 100644 --- a/vector/src/main/res/values/strings_riotX.xml +++ b/vector/src/main/res/values/strings_riotX.xml @@ -11,5 +11,8 @@ Encrypting file… Sending file (%1$s / %2$s) + Downloading file %1$s… + File %1$s has been downloaded! + \ No newline at end of file From b0c939866f07e59f5addc462eb100e77af7a9701 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 9 Jul 2019 10:33:23 +0200 Subject: [PATCH 11/16] Download file - typo --- .../room/model/message/MessageAudioContent.kt | 2 +- ...yptedContent.kt => MessageEncryptedContent.kt} | 2 +- .../room/model/message/MessageFileContent.kt | 15 ++++++++++++++- .../room/model/message/MessageImageContent.kt | 2 +- .../room/model/message/MessageVideoContent.kt | 2 +- .../home/room/detail/RoomDetailViewModel.kt | 13 +++---------- 6 files changed, 21 insertions(+), 15 deletions(-) rename matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/{MessageEncyptedContent.kt => MessageEncryptedContent.kt} (94%) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageAudioContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageAudioContent.kt index 7a9ccf7a..6987804c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageAudioContent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageAudioContent.kt @@ -51,4 +51,4 @@ data class MessageAudioContent( * Required if the file is encrypted. Information on the encrypted file, as specified in End-to-end encryption. */ @Json(name = "file") override val encryptedFileInfo: EncryptedFileInfo? = null -) : MessageEncyptedContent \ No newline at end of file +) : MessageEncryptedContent diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageEncyptedContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageEncryptedContent.kt similarity index 94% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageEncyptedContent.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageEncryptedContent.kt index 3a98701c..88123be3 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageEncyptedContent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageEncryptedContent.kt @@ -22,6 +22,6 @@ import im.vector.matrix.android.internal.crypto.model.rest.EncryptedFileInfo /** * Interface for message which can contains encrypted data */ -interface MessageEncyptedContent : MessageContent { +interface MessageEncryptedContent : MessageContent { val encryptedFileInfo: EncryptedFileInfo? } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageFileContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageFileContent.kt index 1b7f1798..f29a6fa9 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageFileContent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageFileContent.kt @@ -16,6 +16,7 @@ package im.vector.matrix.android.api.session.room.model.message +import android.content.ClipDescription import com.squareup.moshi.Json import com.squareup.moshi.JsonClass import im.vector.matrix.android.api.session.events.model.Content @@ -53,4 +54,16 @@ data class MessageFileContent( @Json(name = "m.new_content") override val newContent: Content? = null, @Json(name = "file") override val encryptedFileInfo: EncryptedFileInfo? = null -) : MessageEncyptedContent \ No newline at end of file +) : MessageEncryptedContent { + + fun getMimeType(): String { + // Mimetype default to plain text, should not be used + return encryptedFileInfo?.mimetype + ?: info?.mimeType + ?: ClipDescription.MIMETYPE_TEXT_PLAIN + } + + fun getFileName(): String { + return filename ?: body + } +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageImageContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageImageContent.kt index 50feb484..15ee19c4 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageImageContent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageImageContent.kt @@ -52,4 +52,4 @@ data class MessageImageContent( * Required if the file is encrypted. Information on the encrypted file, as specified in End-to-end encryption. */ @Json(name = "file") override val encryptedFileInfo: EncryptedFileInfo? = null -) : MessageEncyptedContent \ No newline at end of file +) : MessageEncryptedContent \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageVideoContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageVideoContent.kt index 11845d4d..e741e2f2 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageVideoContent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageVideoContent.kt @@ -51,4 +51,4 @@ data class MessageVideoContent( * Required if the file is encrypted. Information on the encrypted file, as specified in End-to-end encryption. */ @Json(name = "file") override val encryptedFileInfo: EncryptedFileInfo? = null -) : MessageEncyptedContent \ No newline at end of file +) : MessageEncryptedContent \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt index 0c1b4523..336179a6 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt @@ -16,7 +16,6 @@ package im.vector.riotx.features.home.room.detail -import android.content.ClipDescription import android.net.Uri import android.text.TextUtils import androidx.lifecycle.LiveData @@ -452,16 +451,13 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro session.downloadFile( FileService.DownloadMode.TO_EXPORT, action.eventId, - action.messageFileContent.filename ?: "file.dat", + action.messageFileContent.getFileName(), action.messageFileContent.url, action.messageFileContent.encryptedFileInfo?.toElementToDecrypt(), object : MatrixCallback { override fun onSuccess(data: File) { _downloadedFileEvent.postValue(LiveEvent(DownloadFileState( - // Mimetype default to plain text, should not be used - action.messageFileContent.encryptedFileInfo?.mimetype - ?: action.messageFileContent.info?.mimeType - ?: ClipDescription.MIMETYPE_TEXT_PLAIN, + action.messageFileContent.getMimeType(), data, null ))) @@ -469,10 +465,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro override fun onFailure(failure: Throwable) { _downloadedFileEvent.postValue(LiveEvent(DownloadFileState( - // Mimetype default to plain text, should not be used - action.messageFileContent.encryptedFileInfo?.mimetype - ?: action.messageFileContent.info?.mimeType - ?: ClipDescription.MIMETYPE_TEXT_PLAIN, + action.messageFileContent.getMimeType(), null, failure ))) From d7b2371854f779c192fd0bef5b680f4745ce3c96 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 9 Jul 2019 10:42:20 +0200 Subject: [PATCH 12/16] Add long click listener to file items --- .../home/room/detail/timeline/factory/MessageItemFactory.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt index d2289380..7a2d0836 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt @@ -164,6 +164,10 @@ class MessageItemFactory @Inject constructor( DebouncedClickListener(View.OnClickListener { _ -> callback?.onFileMessageClicked(informationData.eventId, messageContent) })) + .longClickListener { view -> + return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view) + ?: false + } } private fun buildNotHandledMessageItem(messageContent: MessageContent, highlight: Boolean): DefaultItem? { From 058e7153a13fd161d821b78f6a9c7cf93a0856fb Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 9 Jul 2019 11:05:25 +0200 Subject: [PATCH 13/16] Fix bug --- .../android/internal/session/DefaultFileService.kt | 12 ++++++------ .../features/home/room/detail/RoomDetailViewModel.kt | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultFileService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultFileService.kt index b23f56bc..0f1554ea 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultFileService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultFileService.kt @@ -62,11 +62,9 @@ internal class DefaultFileService @Inject constructor(private val context: Conte val folder = getFolder(downloadMode, id) File(folder, fileName) - }.map { destFile -> - if (!destFile.exists()) { + }.flatMap { destFile -> + if (!destFile.exists() || downloadMode == FileService.DownloadMode.TO_EXPORT) { Try { - Timber.v("## decrypt file") - val resolvedUrl = contentUrlResolver.resolveFullSize(url) ?: throw IllegalArgumentException("url is null") val request = Request.Builder() @@ -82,6 +80,7 @@ internal class DefaultFileService @Inject constructor(private val context: Conte } if (elementToDecrypt != null) { + Timber.v("## decrypt file") MXEncryptedAttachments.decryptAttachment(inputStream, elementToDecrypt) ?: throw IllegalStateException("Decryption error") } else { inputStream @@ -89,10 +88,11 @@ internal class DefaultFileService @Inject constructor(private val context: Conte } .map { inputStream -> writeToFile(inputStream, destFile) + destFile } + } else { + Try.just(destFile) } - - destFile } } .foldToCallback(callback) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt index 336179a6..d8b03cc8 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt @@ -452,7 +452,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro FileService.DownloadMode.TO_EXPORT, action.eventId, action.messageFileContent.getFileName(), - action.messageFileContent.url, + action.messageFileContent.encryptedFileInfo?.url ?: action.messageFileContent.url, action.messageFileContent.encryptedFileInfo?.toElementToDecrypt(), object : MatrixCallback { override fun onSuccess(data: File) { From 9a4eb8e9a46ed84c70f0b703cdf30e6b471022c0 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 9 Jul 2019 11:20:00 +0200 Subject: [PATCH 14/16] add getFileUrl extension --- .../room/model/message/MessageAudioContent.kt | 2 +- .../room/model/message/MessageEncryptedContent.kt | 14 ++++++++++++-- .../room/model/message/MessageFileContent.kt | 4 ++-- .../room/model/message/MessageImageContent.kt | 4 ++-- .../room/model/message/MessageVideoContent.kt | 4 ++-- .../home/room/detail/RoomDetailViewModel.kt | 2 +- .../detail/timeline/factory/MessageItemFactory.kt | 4 ++-- 7 files changed, 22 insertions(+), 12 deletions(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageAudioContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageAudioContent.kt index 6987804c..624d827d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageAudioContent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageAudioContent.kt @@ -42,7 +42,7 @@ data class MessageAudioContent( /** * Required. Required if the file is not encrypted. The URL (typically MXC URI) to the audio clip. */ - @Json(name = "url") val url: String? = null, + @Json(name = "url") override val url: String? = null, @Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null, @Json(name = "m.new_content") override val newContent: Content? = null, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageEncryptedContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageEncryptedContent.kt index 88123be3..125c18bb 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageEncryptedContent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageEncryptedContent.kt @@ -20,8 +20,18 @@ import im.vector.matrix.android.internal.crypto.model.rest.EncryptedFileInfo /** - * Interface for message which can contains encrypted data + * Interface for message which can contains an encrypted file */ interface MessageEncryptedContent : MessageContent { + /** + * Required. Required if the file is unencrypted. The URL (typically MXC URI) to the image. + */ + val url: String? + val encryptedFileInfo: EncryptedFileInfo? -} \ No newline at end of file +} + +/** + * Get the url of the encrypted file or of the file + */ +fun MessageEncryptedContent.getFileUrl() = encryptedFileInfo?.url ?: url diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageFileContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageFileContent.kt index f29a6fa9..ac70f5bf 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageFileContent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageFileContent.kt @@ -48,7 +48,7 @@ data class MessageFileContent( /** * Required. Required if the file is unencrypted. The URL (typically MXC URI) to the file. */ - @Json(name = "url") val url: String? = null, + @Json(name = "url") override val url: String? = null, @Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null, @Json(name = "m.new_content") override val newContent: Content? = null, @@ -66,4 +66,4 @@ data class MessageFileContent( fun getFileName(): String { return filename ?: body } -} \ No newline at end of file +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageImageContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageImageContent.kt index 15ee19c4..107a8b27 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageImageContent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageImageContent.kt @@ -43,7 +43,7 @@ data class MessageImageContent( /** * Required. Required if the file is unencrypted. The URL (typically MXC URI) to the image. */ - @Json(name = "url") val url: String? = null, + @Json(name = "url") override val url: String? = null, @Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null, @Json(name = "m.new_content") override val newContent: Content? = null, @@ -52,4 +52,4 @@ data class MessageImageContent( * Required if the file is encrypted. Information on the encrypted file, as specified in End-to-end encryption. */ @Json(name = "file") override val encryptedFileInfo: EncryptedFileInfo? = null -) : MessageEncryptedContent \ No newline at end of file +) : MessageEncryptedContent diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageVideoContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageVideoContent.kt index e741e2f2..1c84b3e7 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageVideoContent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageVideoContent.kt @@ -42,7 +42,7 @@ data class MessageVideoContent( /** * Required. Required if the file is unencrypted. The URL (typically MXC URI) to the video clip. */ - @Json(name = "url") val url: String? = null, + @Json(name = "url") override val url: String? = null, @Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null, @Json(name = "m.new_content") override val newContent: Content? = null, @@ -51,4 +51,4 @@ data class MessageVideoContent( * Required if the file is encrypted. Information on the encrypted file, as specified in End-to-end encryption. */ @Json(name = "file") override val encryptedFileInfo: EncryptedFileInfo? = null -) : MessageEncryptedContent \ No newline at end of file +) : MessageEncryptedContent diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt index d8b03cc8..d51b63d8 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt @@ -452,7 +452,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro FileService.DownloadMode.TO_EXPORT, action.eventId, action.messageFileContent.getFileName(), - action.messageFileContent.encryptedFileInfo?.url ?: action.messageFileContent.url, + action.messageFileContent.getSafeUrl(), action.messageFileContent.encryptedFileInfo?.toElementToDecrypt(), object : MatrixCallback { override fun onSuccess(data: File) { diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt index 7a2d0836..889df1b5 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt @@ -185,7 +185,7 @@ class MessageItemFactory @Inject constructor( val (maxWidth, maxHeight) = timelineMediaSizeProvider.getMaxSize() val data = ImageContentRenderer.Data( filename = messageContent.body, - url = messageContent.encryptedFileInfo?.url ?: messageContent.url, + url = messageContent.getFileUrl(), elementToDecrypt = messageContent.encryptedFileInfo?.toElementToDecrypt(), height = messageContent.info?.height, maxHeight = maxHeight, @@ -238,7 +238,7 @@ class MessageItemFactory @Inject constructor( val videoData = VideoContentRenderer.Data( eventId = informationData.eventId, filename = messageContent.body, - url = messageContent.encryptedFileInfo?.url ?: messageContent.url, + url = messageContent.getFileUrl(), elementToDecrypt = messageContent.encryptedFileInfo?.toElementToDecrypt(), thumbnailMediaData = thumbnailData ) From 5dc83d64c1197d6ebebd2a88ac794e21170dee4b Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 9 Jul 2019 14:48:57 +0200 Subject: [PATCH 15/16] Fix compilation issue --- .../riotx/features/home/room/detail/RoomDetailViewModel.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt index d51b63d8..69c35b38 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt @@ -35,6 +35,7 @@ import im.vector.matrix.android.api.session.file.FileService import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.api.session.room.model.message.MessageContent import im.vector.matrix.android.api.session.room.model.message.MessageType +import im.vector.matrix.android.api.session.room.model.message.getFileUrl import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt import im.vector.matrix.rx.rx @@ -452,7 +453,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro FileService.DownloadMode.TO_EXPORT, action.eventId, action.messageFileContent.getFileName(), - action.messageFileContent.getSafeUrl(), + action.messageFileContent.getFileUrl(), action.messageFileContent.encryptedFileInfo?.toElementToDecrypt(), object : MatrixCallback { override fun onSuccess(data: File) { From ba589e7961156202da146b25f8cdb9d1b99b1122 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 9 Jul 2019 14:52:48 +0200 Subject: [PATCH 16/16] Add missing permission request --- .../riotx/core/utils/PermissionsTools.kt | 1 + .../setup/KeysBackupSetupActivity.kt | 65 +++++++++++-------- .../home/room/detail/RoomDetailFragment.kt | 21 +++++- .../home/room/detail/RoomDetailViewModel.kt | 3 + 4 files changed, 61 insertions(+), 29 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/core/utils/PermissionsTools.kt b/vector/src/main/java/im/vector/riotx/core/utils/PermissionsTools.kt index 01ca16bf..03cf248b 100644 --- a/vector/src/main/java/im/vector/riotx/core/utils/PermissionsTools.kt +++ b/vector/src/main/java/im/vector/riotx/core/utils/PermissionsTools.kt @@ -66,6 +66,7 @@ const val PERMISSION_REQUEST_CODE_AUDIO_CALL = 571 const val PERMISSION_REQUEST_CODE_VIDEO_CALL = 572 const val PERMISSION_REQUEST_CODE_EXPORT_KEYS = 573 const val PERMISSION_REQUEST_CODE_CHANGE_AVATAR = 574 +const val PERMISSION_REQUEST_CODE_DOWNLOAD_FILE = 575 /** * Log the used permissions statuses. diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupActivity.kt b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupActivity.kt index 77780016..0677224f 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupActivity.kt @@ -27,7 +27,7 @@ import im.vector.riotx.R import im.vector.riotx.core.dialogs.ExportKeysDialog import im.vector.riotx.core.extensions.observeEvent import im.vector.riotx.core.platform.SimpleFragmentActivity -import im.vector.riotx.core.utils.toast +import im.vector.riotx.core.utils.* import im.vector.riotx.features.crypto.keys.KeysExporter class KeysBackupSetupActivity : SimpleFragmentActivity() { @@ -132,39 +132,48 @@ class KeysBackupSetupActivity : SimpleFragmentActivity() { } private fun exportKeysManually() { - ExportKeysDialog().show(this, object : ExportKeysDialog.ExportKeyDialogListener { - override fun onPassphrase(passphrase: String) { - showWaitingView() + if (checkPermissions(PERMISSIONS_FOR_WRITING_FILES, this, PERMISSION_REQUEST_CODE_EXPORT_KEYS)) { + ExportKeysDialog().show(this, object : ExportKeysDialog.ExportKeyDialogListener { + override fun onPassphrase(passphrase: String) { + showWaitingView() - KeysExporter(session) - .export(this@KeysBackupSetupActivity, - passphrase, - object : MatrixCallback { + KeysExporter(session) + .export(this@KeysBackupSetupActivity, + passphrase, + object : MatrixCallback { - override fun onSuccess(data: String) { - hideWaitingView() + override fun onSuccess(data: String) { + hideWaitingView() - AlertDialog.Builder(this@KeysBackupSetupActivity) - .setMessage(getString(R.string.encryption_export_saved_as, data)) - .setCancelable(false) - .setPositiveButton(R.string.ok) { dialog, which -> - val resultIntent = Intent() - resultIntent.putExtra(MANUAL_EXPORT, true) - setResult(RESULT_OK, resultIntent) - finish() - } - .show() - } + AlertDialog.Builder(this@KeysBackupSetupActivity) + .setMessage(getString(R.string.encryption_export_saved_as, data)) + .setCancelable(false) + .setPositiveButton(R.string.ok) { dialog, which -> + val resultIntent = Intent() + resultIntent.putExtra(MANUAL_EXPORT, true) + setResult(RESULT_OK, resultIntent) + finish() + } + .show() + } - override fun onFailure(failure: Throwable) { - toast(failure.localizedMessage) - hideWaitingView() - } - }) - } - }) + override fun onFailure(failure: Throwable) { + toast(failure.localizedMessage) + hideWaitingView() + } + }) + } + }) + } } + override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { + if (allGranted(grantResults)) { + if (requestCode == PERMISSION_REQUEST_CODE_EXPORT_KEYS) { + exportKeysManually() + } + } + } override fun onBackPressed() { if (viewModel.shouldPromptOnBack) { diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt index ed137f24..1439a1e9 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt @@ -621,7 +621,26 @@ class RoomDetailFragment : } override fun onFileMessageClicked(eventId: String, messageFileContent: MessageFileContent) { - roomDetailViewModel.process(RoomDetailActions.DownloadFile(eventId, messageFileContent)) + val action = RoomDetailActions.DownloadFile(eventId, messageFileContent) + // We need WRITE_EXTERNAL permission + if (checkPermissions(PERMISSIONS_FOR_WRITING_FILES, this, PERMISSION_REQUEST_CODE_DOWNLOAD_FILE)) { + roomDetailViewModel.process(action) + } else { + roomDetailViewModel.pendingAction = action + } + } + + override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { + if (allGranted(grantResults)) { + if (requestCode == PERMISSION_REQUEST_CODE_DOWNLOAD_FILE) { + val action = roomDetailViewModel.pendingAction + + if (action != null) { + roomDetailViewModel.pendingAction = null + roomDetailViewModel.process(action) + } + } + } } override fun onAudioMessageClicked(messageAudioContent: MessageAudioContent) { diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt index 69c35b38..9e0fda91 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt @@ -75,6 +75,9 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro } private var timeline = room.createTimeline(eventId, allowedTypes) + // Slot to keep a pending action during permission request + var pendingAction: RoomDetailActions? = null + @AssistedInject.Factory interface Factory { fun create(initialState: RoomDetailViewState): RoomDetailViewModel