diff --git a/CHANGES.md b/CHANGES.md index 60eaed1461..7c31f37b0d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -5,6 +5,7 @@ Features ✨: - Improvements 🙌: + - Share image and other media from e2e rooms (#677) - Add support for `/plain` command (#12) - Detect spaces in password if user fail to login (#1038) - FTUE: do not display a different color when encrypting message when not in developer mode. @@ -12,6 +13,8 @@ Improvements 🙌: Bugfix 🐛: - Fix crash on attachment preview screen (#1088) + - "Share" option is not appearing in encrypted rooms for images (#1031) + - Set "image/jpeg" as MIME type of images instead of "image/jpg" (#1075) Translations 🗣: - diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle index 062b590acf..f980279a8d 100644 --- a/matrix-sdk-android/build.gradle +++ b/matrix-sdk-android/build.gradle @@ -126,7 +126,7 @@ dependencies { kapt 'dk.ilios:realmfieldnameshelper:1.1.1' // Work - implementation "androidx.work:work-runtime-ktx:2.3.0" + implementation "androidx.work:work-runtime-ktx:2.3.3" // FP implementation "io.arrow-kt:arrow-core:$arrow_version" diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/content/ContentAttachmentData.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/content/ContentAttachmentData.kt index 48dff4e56d..e32bb9f21f 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/content/ContentAttachmentData.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/content/ContentAttachmentData.kt @@ -31,7 +31,7 @@ data class ContentAttachmentData( val name: String? = null, val queryUri: String, val path: String, - val mimeType: String?, + private val mimeType: String?, val type: Type ) : Parcelable { @@ -41,4 +41,6 @@ data class ContentAttachmentData( AUDIO, VIDEO } + + fun getSafeMimeType() = if (mimeType == "image/jpg") "image/jpeg" else mimeType } 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 index 4d9cff3e92..32fb1a6ab0 100644 --- 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 @@ -34,7 +34,11 @@ interface FileService { /** * Download file in cache */ - FOR_INTERNAL_USE + FOR_INTERNAL_USE, + /** + * Download file in file provider path + */ + FOR_EXTERNAL_SHARE } /** 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 e9c6c71882..248e782a74 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 -) : MessageEncryptedContent +) : MessageWithAttachmentContent 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 7c635a401d..f770a2ccea 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 @@ -57,7 +57,7 @@ data class MessageFileContent( * 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 { +) : MessageWithAttachmentContent { fun getMimeType(): String { // Mimetype default to plain text, should not be used diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageImageInfoContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageImageInfoContent.kt index 9087a45b4c..be0b5c4bb7 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageImageInfoContent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageImageInfoContent.kt @@ -20,6 +20,6 @@ package im.vector.matrix.android.api.session.room.model.message /** * A content with image information */ -interface MessageImageInfoContent : MessageEncryptedContent { +interface MessageImageInfoContent : MessageWithAttachmentContent { val info: ImageInfo? } 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 4cf03a5ffd..88d2d72d15 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 -) : MessageEncryptedContent +) : MessageWithAttachmentContent 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/MessageWithAttachmentContent.kt similarity index 89% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageEncryptedContent.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageWithAttachmentContent.kt index 1d1d01c09c..9caf38013f 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/MessageWithAttachmentContent.kt @@ -21,7 +21,7 @@ import im.vector.matrix.android.internal.crypto.model.rest.EncryptedFileInfo /** * Interface for message which can contains an encrypted file */ -interface MessageEncryptedContent : MessageContent { +interface MessageWithAttachmentContent : MessageContent { /** * Required if the file is unencrypted. The URL (typically MXC URI) to the image. */ @@ -36,4 +36,4 @@ interface MessageEncryptedContent : MessageContent { /** * Get the url of the encrypted file or of the file */ -fun MessageEncryptedContent.getFileUrl() = encryptedFileInfo?.url ?: url +fun MessageWithAttachmentContent.getFileUrl() = encryptedFileInfo?.url ?: url diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/FileQualifiers.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/FileQualifiers.kt index dc36b02809..acd6703c77 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/FileQualifiers.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/FileQualifiers.kt @@ -25,3 +25,7 @@ annotation class SessionFilesDirectory @Qualifier @Retention(AnnotationRetention.RUNTIME) annotation class SessionCacheDirectory + +@Qualifier +@Retention(AnnotationRetention.RUNTIME) +annotation class CacheDirectory diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MatrixComponent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MatrixComponent.kt index c5b07ff4e8..e929016d4f 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MatrixComponent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MatrixComponent.kt @@ -32,6 +32,7 @@ import im.vector.matrix.android.internal.util.BackgroundDetectionObserver import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers import okhttp3.OkHttpClient import org.matrix.olm.OlmManager +import java.io.File @Component(modules = [MatrixModule::class, NetworkModule::class, AuthModule::class]) @MatrixScope @@ -52,6 +53,9 @@ internal interface MatrixComponent { fun resources(): Resources + @CacheDirectory + fun cacheDir(): File + fun olmManager(): OlmManager fun taskExecutor(): TaskExecutor diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MatrixModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MatrixModule.kt index 284cbfff88..0af22dd65a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MatrixModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MatrixModule.kt @@ -26,6 +26,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.android.asCoroutineDispatcher import kotlinx.coroutines.asCoroutineDispatcher import org.matrix.olm.OlmManager +import java.io.File import java.util.concurrent.Executors @Module @@ -49,6 +50,13 @@ internal object MatrixModule { return context.resources } + @JvmStatic + @Provides + @CacheDirectory + fun providesCacheDir(context: Context): File { + return context.cacheDir + } + @JvmStatic @Provides @MatrixScope 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 cf7e1b1d83..9eaea8cc34 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 @@ -24,11 +24,11 @@ import im.vector.matrix.android.api.session.file.FileService import im.vector.matrix.android.api.util.Cancelable import im.vector.matrix.android.internal.crypto.attachments.ElementToDecrypt import im.vector.matrix.android.internal.crypto.attachments.MXEncryptedAttachments +import im.vector.matrix.android.internal.di.CacheDirectory import im.vector.matrix.android.internal.di.SessionCacheDirectory import im.vector.matrix.android.internal.di.Unauthenticated import im.vector.matrix.android.internal.extensions.foldToCallback import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers -import im.vector.matrix.android.internal.util.md5 import im.vector.matrix.android.internal.util.toCancelable import im.vector.matrix.android.internal.util.writeToFile import kotlinx.coroutines.GlobalScope @@ -42,8 +42,10 @@ import java.io.IOException import javax.inject.Inject internal class DefaultFileService @Inject constructor( - @SessionCacheDirectory + @CacheDirectory private val cacheDirectory: File, + @SessionCacheDirectory + private val sessionCacheDirectory: File, private val contentUrlResolver: ContentUrlResolver, @Unauthenticated private val okHttpClient: OkHttpClient, @@ -62,60 +64,50 @@ internal class DefaultFileService @Inject constructor( return GlobalScope.launch(coroutineDispatchers.main) { withContext(coroutineDispatchers.io) { Try { - val folder = getFolder(downloadMode, id) - + val folder = File(sessionCacheDirectory, "MF") + if (!folder.exists()) { + folder.mkdirs() + } File(folder, fileName) }.flatMap { destFile -> - if (!destFile.exists() || downloadMode == FileService.DownloadMode.TO_EXPORT) { - Try { - val resolvedUrl = contentUrlResolver.resolveFullSize(url) ?: throw IllegalArgumentException("url is null") + if (!destFile.exists()) { + val resolvedUrl = contentUrlResolver.resolveFullSize(url) ?: return@flatMap Try.Failure(IllegalArgumentException("url is null")) - val request = Request.Builder() - .url(resolvedUrl) - .build() + val request = Request.Builder() + .url(resolvedUrl) + .build() - val response = okHttpClient.newCall(request).execute() - var inputStream = response.body?.byteStream() - Timber.v("Response size ${response.body?.contentLength()} - Stream available: ${inputStream?.available()}") - if (!response.isSuccessful - || inputStream == null) { - throw IOException() - } - - if (elementToDecrypt != null) { - Timber.v("## decrypt file") - inputStream = MXEncryptedAttachments.decryptAttachment(inputStream, elementToDecrypt) - ?: throw IllegalStateException("Decryption error") - } - - writeToFile(inputStream, destFile) - destFile + val response = okHttpClient.newCall(request).execute() + var inputStream = response.body?.byteStream() + Timber.v("Response size ${response.body?.contentLength()} - Stream available: ${inputStream?.available()}") + if (!response.isSuccessful || inputStream == null) { + return@flatMap Try.Failure(IOException()) } - } else { - Try.just(destFile) + + if (elementToDecrypt != null) { + Timber.v("## decrypt file") + inputStream = MXEncryptedAttachments.decryptAttachment(inputStream, elementToDecrypt) + ?: return@flatMap Try.Failure(IllegalStateException("Decryption error")) + } + + writeToFile(inputStream, destFile) } + + Try.just(copyFile(destFile, downloadMode)) } } .foldToCallback(callback) }.toCancelable() } - private fun getFolder(downloadMode: FileService.DownloadMode, id: String): File { + private fun copyFile(file: File, downloadMode: FileService.DownloadMode): File { return when (downloadMode) { - FileService.DownloadMode.FOR_INTERNAL_USE -> { - // Create dir tree (MF stands for Matrix File): - // //MF// - val tmpFolderSession = File(cacheDirectory, "MF") - File(tmpFolderSession, id.md5()) - } - FileService.DownloadMode.TO_EXPORT -> { - Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) - } + FileService.DownloadMode.TO_EXPORT -> + file.copyTo(File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), file.name), true) + FileService.DownloadMode.FOR_EXTERNAL_SHARE -> + file.copyTo(File(File(cacheDirectory, "ext_share"), file.name), true) + FileService.DownloadMode.FOR_INTERNAL_USE -> + file } - .also { folder -> - if (!folder.exists()) { - folder.mkdirs() - } - } } } 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 66a8341801..7a13d5f8a5 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 @@ -20,6 +20,7 @@ import android.os.Handler import android.os.Looper import im.vector.matrix.android.api.session.content.ContentUploadStateTracker import im.vector.matrix.android.internal.session.SessionScope +import timber.log.Timber import javax.inject.Inject @SessionScope @@ -33,7 +34,13 @@ internal class DefaultContentUploadStateTracker @Inject constructor() : ContentU val listeners = listeners.getOrPut(key) { ArrayList() } listeners.add(updateListener) val currentState = states[key] ?: ContentUploadStateTracker.State.Idle - mainHandler.post { updateListener.onUpdate(currentState) } + mainHandler.post { + try { + updateListener.onUpdate(currentState) + } catch (e: Exception) { + Timber.e(e, "## ContentUploadStateTracker.onUpdate() failed") + } + } } override fun untrack(key: String, updateListener: ContentUploadStateTracker.UpdateListener) { @@ -79,7 +86,13 @@ internal class DefaultContentUploadStateTracker @Inject constructor() : ContentU private fun updateState(key: String, state: ContentUploadStateTracker.State) { states[key] = state mainHandler.post { - listeners[key]?.forEach { it.onUpdate(state) } + listeners[key]?.forEach { + try { + it.onUpdate(state) + } catch (e: Exception) { + Timber.e(e, "## ContentUploadStateTracker.onUpdate() failed") + } + } } } } 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 94bdb11edb..1dde25fd78 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 @@ -58,7 +58,7 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter override val sessionId: String, val events: List, val attachment: ContentAttachmentData, - val isRoomEncrypted: Boolean, + val isEncrypted: Boolean, val compressBeforeSending: Boolean, override val lastFailureMessage: String? = null ) : SessionWorkerParams @@ -90,9 +90,11 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter Timber.e(e) notifyTracker(params) { contentUploadStateTracker.setFailure(it, e) } return Result.success( - WorkerParamsFactory.toData(params.copy( - lastFailureMessage = e.localizedMessage - )) + WorkerParamsFactory.toData( + params.copy( + lastFailureMessage = e.localizedMessage + ) + ) ) } .let { originalFile -> @@ -136,7 +138,7 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter } try { - val contentUploadResponse = if (params.isRoomEncrypted) { + val contentUploadResponse = if (params.isEncrypted) { Timber.v("Encrypt thumbnail") notifyTracker(params) { contentUploadStateTracker.setEncryptingThumbnail(it) } val encryptionResult = MXEncryptedAttachments.encryptAttachment(ByteArrayInputStream(thumbnailData.bytes), thumbnailData.mimeType) @@ -174,18 +176,18 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter var uploadedFileEncryptedFileInfo: EncryptedFileInfo? = null return try { - val contentUploadResponse = if (params.isRoomEncrypted) { + val contentUploadResponse = if (params.isEncrypted) { Timber.v("Encrypt file") notifyTracker(params) { contentUploadStateTracker.setEncrypting(it) } - val encryptionResult = MXEncryptedAttachments.encryptAttachment(FileInputStream(attachmentFile), attachment.mimeType) + val encryptionResult = MXEncryptedAttachments.encryptAttachment(FileInputStream(attachmentFile), attachment.getSafeMimeType()) uploadedFileEncryptedFileInfo = encryptionResult.encryptedFileInfo fileUploader .uploadByteArray(encryptionResult.encryptedByteArray, attachment.name, "application/octet-stream", progressListener) } else { fileUploader - .uploadFile(attachmentFile, attachment.name, attachment.mimeType, progressListener) + .uploadFile(attachmentFile, attachment.name, attachment.getSafeMimeType(), progressListener) } handleSuccess(params, @@ -226,7 +228,7 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter updateEvent(it, attachmentUrl, encryptedFileInfo, thumbnailUrl, thumbnailEncryptedFileInfo, newImageAttributes) } - val sendParams = MultipleEventSendingDispatcherWorker.Params(params.sessionId, updatedEvents, params.isRoomEncrypted) + val sendParams = MultipleEventSendingDispatcherWorker.Params(params.sessionId, updatedEvents, params.isEncrypted) return Result.success(WorkerParamsFactory.toData(sendParams)) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt index 51de36291d..198a65a2d3 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt @@ -261,7 +261,7 @@ internal class LocalEchoEventFactory @Inject constructor( msgType = MessageType.MSGTYPE_IMAGE, body = attachment.name ?: "image", info = ImageInfo( - mimeType = attachment.mimeType, + mimeType = attachment.getSafeMimeType(), width = width?.toInt() ?: 0, height = height?.toInt() ?: 0, size = attachment.size.toInt() @@ -293,7 +293,7 @@ internal class LocalEchoEventFactory @Inject constructor( msgType = MessageType.MSGTYPE_VIDEO, body = attachment.name ?: "video", videoInfo = VideoInfo( - mimeType = attachment.mimeType, + mimeType = attachment.getSafeMimeType(), width = width, height = height, size = attachment.size, @@ -312,7 +312,7 @@ internal class LocalEchoEventFactory @Inject constructor( msgType = MessageType.MSGTYPE_AUDIO, body = attachment.name ?: "audio", audioInfo = AudioInfo( - mimeType = attachment.mimeType?.takeIf { it.isNotBlank() } ?: "audio/mpeg", + mimeType = attachment.getSafeMimeType()?.takeIf { it.isNotBlank() } ?: "audio/mpeg", size = attachment.size ), url = attachment.path @@ -325,7 +325,7 @@ internal class LocalEchoEventFactory @Inject constructor( msgType = MessageType.MSGTYPE_FILE, body = attachment.name ?: "file", info = FileInfo( - mimeType = attachment.mimeType?.takeIf { it.isNotBlank() } + mimeType = attachment.getSafeMimeType()?.takeIf { it.isNotBlank() } ?: "application/octet-stream", size = attachment.size ), diff --git a/vector/build.gradle b/vector/build.gradle index 988712d87e..6d24a26838 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -296,7 +296,7 @@ dependencies { implementation 'com.airbnb.android:mvrx:1.3.0' // Work - implementation "androidx.work:work-runtime-ktx:2.3.0-beta02" + implementation "androidx.work:work-runtime-ktx:2.3.3" // Paging implementation "androidx.paging:paging-runtime-ktx:2.1.1" diff --git a/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentsHelper.kt b/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentsHelper.kt index c9ee1cb6e5..ba1197b787 100644 --- a/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentsHelper.kt +++ b/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentsHelper.kt @@ -27,6 +27,7 @@ import com.kbeanie.multipicker.api.Picker.PICK_IMAGE_CAMERA import com.kbeanie.multipicker.api.Picker.PICK_IMAGE_DEVICE import com.kbeanie.multipicker.core.ImagePickerImpl import com.kbeanie.multipicker.core.PickerManager +import com.kbeanie.multipicker.utils.IntentUtils import im.vector.matrix.android.BuildConfig import im.vector.matrix.android.api.session.content.ContentAttachmentData import im.vector.riotx.core.platform.Restorable @@ -176,13 +177,13 @@ class AttachmentsHelper private constructor(private val context: Context, fun handleShareIntent(intent: Intent): Boolean { val type = intent.resolveType(context) ?: return false if (type.startsWith("image")) { - imagePicker.submit(intent) + imagePicker.submit(IntentUtils.getPickerIntentForSharing(intent)) } else if (type.startsWith("video")) { - videoPicker.submit(intent) + videoPicker.submit(IntentUtils.getPickerIntentForSharing(intent)) } else if (type.startsWith("audio")) { - videoPicker.submit(intent) + videoPicker.submit(IntentUtils.getPickerIntentForSharing(intent)) } else if (type.startsWith("application") || type.startsWith("file") || type.startsWith("*")) { - filePicker.submit(intent) + filePicker.submit(IntentUtils.getPickerIntentForSharing(intent)) } else { return false } diff --git a/vector/src/main/java/im/vector/riotx/features/attachments/preview/Extensions.kt b/vector/src/main/java/im/vector/riotx/features/attachments/preview/Extensions.kt index 3bd47baa89..40fcc0aa92 100644 --- a/vector/src/main/java/im/vector/riotx/features/attachments/preview/Extensions.kt +++ b/vector/src/main/java/im/vector/riotx/features/attachments/preview/Extensions.kt @@ -23,6 +23,6 @@ import im.vector.matrix.android.api.session.content.ContentAttachmentData */ fun ContentAttachmentData.isEditable(): Boolean { return type == ContentAttachmentData.Type.IMAGE - && mimeType?.startsWith("image/") == true - && mimeType != "image/gif" + && getSafeMimeType()?.startsWith("image/") == true + && getSafeMimeType() != "image/gif" } 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 c2eb61b3ca..ad4e9694db 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 @@ -37,6 +37,7 @@ import androidx.annotation.StringRes import androidx.appcompat.app.AlertDialog import androidx.core.app.ActivityOptionsCompat import androidx.core.content.ContextCompat +import androidx.core.net.toUri import androidx.core.text.buildSpannedString import androidx.core.util.Pair import androidx.core.view.ViewCompat @@ -57,17 +58,17 @@ import com.airbnb.mvrx.Success import com.airbnb.mvrx.args import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState -import com.github.piasy.biv.BigImageViewer -import com.github.piasy.biv.loader.ImageLoader import com.google.android.material.checkbox.MaterialCheckBox import com.google.android.material.snackbar.Snackbar import com.google.android.material.textfield.TextInputEditText import com.google.android.material.textfield.TextInputLayout import com.jakewharton.rxbinding3.widget.textChanges +import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.permalinks.PermalinkFactory 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.Event +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.MessageAudioContent import im.vector.matrix.android.api.session.room.model.message.MessageContent @@ -77,12 +78,14 @@ import im.vector.matrix.android.api.session.room.model.message.MessageImageInfoC import im.vector.matrix.android.api.session.room.model.message.MessageTextContent import im.vector.matrix.android.api.session.room.model.message.MessageVerificationRequestContent import im.vector.matrix.android.api.session.room.model.message.MessageVideoContent +import im.vector.matrix.android.api.session.room.model.message.getFileUrl import im.vector.matrix.android.api.session.room.send.SendState import im.vector.matrix.android.api.session.room.timeline.Timeline import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent import im.vector.matrix.android.api.util.MatrixItem import im.vector.matrix.android.api.util.toMatrixItem +import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt import im.vector.riotx.R import im.vector.riotx.core.dialogs.withColoredButton import im.vector.riotx.core.epoxy.LayoutManagerStateRestorer @@ -93,6 +96,7 @@ import im.vector.riotx.core.extensions.setTextOrHide import im.vector.riotx.core.extensions.showKeyboard import im.vector.riotx.core.files.addEntryToDownloadManager import im.vector.riotx.core.glide.GlideApp +import im.vector.riotx.core.intent.getMimeTypeFromUri import im.vector.riotx.core.platform.VectorBaseFragment import im.vector.riotx.core.resources.ColorProvider import im.vector.riotx.core.ui.views.JumpToReadMarkerView @@ -1124,6 +1128,23 @@ class RoomDetailFragment @Inject constructor( roomDetailViewModel.handle(RoomDetailAction.EnterTrackingUnreadMessagesState) } + private fun onShareActionClicked(action: EventSharedAction.Share) { + session.downloadFile( + FileService.DownloadMode.FOR_EXTERNAL_SHARE, + action.eventId, + action.messageContent.body, + action.messageContent.getFileUrl(), + action.messageContent.encryptedFileInfo?.toElementToDecrypt(), + object : MatrixCallback { + override fun onSuccess(data: File) { + if (isAdded) { + shareMedia(requireContext(), data, getMimeTypeFromUri(requireContext(), data.toUri())) + } + } + } + ) + } + private fun handleActions(action: EventSharedAction) { when (action) { is EventSharedAction.OpenUserProfile -> { @@ -1145,32 +1166,7 @@ class RoomDetailFragment @Inject constructor( promptConfirmationToRedactEvent(action) } is EventSharedAction.Share -> { - // TODO current data communication is too limited - // Need to now the media type - // TODO bad, just POC - BigImageViewer.imageLoader().loadImage( - action.hashCode(), - Uri.parse(action.imageUrl), - object : ImageLoader.Callback { - override fun onFinish() {} - - override fun onSuccess(image: File?) { - if (image != null) { - shareMedia(requireContext(), image, "image/*") - } - } - - override fun onFail(error: Exception?) {} - - override fun onCacheHit(imageType: Int, image: File?) {} - - override fun onCacheMiss(imageType: Int, image: File?) {} - - override fun onProgress(progress: Int) {} - - override fun onStart() {} - } - ) + onShareActionClicked(action) } is EventSharedAction.ViewEditHistory -> { onEditedDecorationClicked(action.messageInformationData) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/EventSharedAction.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/EventSharedAction.kt index cba89d8481..b9e2ab2093 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/EventSharedAction.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/EventSharedAction.kt @@ -18,6 +18,7 @@ package im.vector.riotx.features.home.room.detail.timeline.action import androidx.annotation.DrawableRes import androidx.annotation.StringRes +import im.vector.matrix.android.api.session.room.model.message.MessageWithAttachmentContent import im.vector.riotx.R import im.vector.riotx.core.platform.VectorSharedAction import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData @@ -46,7 +47,7 @@ sealed class EventSharedAction(@StringRes val titleRes: Int, data class Reply(val eventId: String) : EventSharedAction(R.string.reply, R.drawable.ic_reply) - data class Share(val imageUrl: String) : + data class Share(val eventId: String, val messageContent: MessageWithAttachmentContent) : EventSharedAction(R.string.share, R.drawable.ic_share) data class Resend(val eventId: String) : diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsViewModel.kt index fa9bdbc29c..1fe1db27d7 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsViewModel.kt @@ -29,8 +29,8 @@ import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.isTextMessage import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.room.model.message.MessageContent +import im.vector.matrix.android.api.session.room.model.message.MessageWithAttachmentContent import im.vector.matrix.android.api.session.room.model.message.MessageFormat -import im.vector.matrix.android.api.session.room.model.message.MessageImageContent import im.vector.matrix.android.api.session.room.model.message.MessageTextContent import im.vector.matrix.android.api.session.room.model.message.MessageType import im.vector.matrix.android.api.session.room.model.message.MessageVerificationRequestContent @@ -260,13 +260,8 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted add(EventSharedAction.ViewEditHistory(informationData)) } - if (canShare(msgType)) { - if (messageContent is MessageImageContent) { - session.contentUrlResolver().resolveFullSize(messageContent.url)?.let { url -> - add(EventSharedAction.Share(url)) - } - } - // TODO + if (canShare(msgType) && messageContent is MessageWithAttachmentContent) { + add(EventSharedAction.Share(timelineEvent.eventId, messageContent)) } if (timelineEvent.root.sendState == SendState.SENT) { @@ -374,8 +369,9 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted return when (msgType) { MessageType.MSGTYPE_IMAGE, MessageType.MSGTYPE_AUDIO, - MessageType.MSGTYPE_VIDEO -> true - else -> false + MessageType.MSGTYPE_VIDEO, + MessageType.MSGTYPE_FILE -> true + else -> 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 e497d9ce04..737549d5b8 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 @@ -127,6 +127,15 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder: GlideApp .with(imageView) .load(resolvedUrl) + .apply { + if (mode == Mode.THUMBNAIL) { + error( + GlideApp + .with(imageView) + .load(contentUrlResolver.resolveFullSize(data.url)) + ) + } + } } } diff --git a/vector/src/main/java/im/vector/riotx/features/share/IncomingShareFragment.kt b/vector/src/main/java/im/vector/riotx/features/share/IncomingShareFragment.kt index eb0f5128ba..74821ab2fe 100644 --- a/vector/src/main/java/im/vector/riotx/features/share/IncomingShareFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/share/IncomingShareFragment.kt @@ -28,7 +28,6 @@ import androidx.appcompat.widget.SearchView import androidx.core.view.isVisible import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState -import com.kbeanie.multipicker.utils.IntentUtils import im.vector.matrix.android.api.session.content.ContentAttachmentData import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.riotx.R @@ -78,7 +77,7 @@ class IncomingShareFragment @Inject constructor( val intent = vectorBaseActivity.intent val isShareManaged = when (intent?.action) { Intent.ACTION_SEND -> { - var isShareManaged = attachmentsHelper.handleShareIntent(IntentUtils.getPickerIntentForSharing(intent)) + var isShareManaged = attachmentsHelper.handleShareIntent(intent) if (!isShareManaged) { isShareManaged = handleTextShare(intent) }