Merge pull request #1099 from vector-im/feature/fix_share_image

Share images from clear and encrypted rooms.
This commit is contained in:
Benoit Marty 2020-03-06 14:27:47 +01:00 committed by GitHub
commit b929a2f185
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 149 additions and 115 deletions

View File

@ -5,6 +5,7 @@ Features ✨:
- -
Improvements 🙌: Improvements 🙌:
- Share image and other media from e2e rooms (#677)
- Add support for `/plain` command (#12) - Add support for `/plain` command (#12)
- Detect spaces in password if user fail to login (#1038) - 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. - FTUE: do not display a different color when encrypting message when not in developer mode.
@ -12,6 +13,8 @@ Improvements 🙌:
Bugfix 🐛: Bugfix 🐛:
- Fix crash on attachment preview screen (#1088) - 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 🗣: Translations 🗣:
- -

View File

@ -126,7 +126,7 @@ dependencies {
kapt 'dk.ilios:realmfieldnameshelper:1.1.1' kapt 'dk.ilios:realmfieldnameshelper:1.1.1'
// Work // Work
implementation "androidx.work:work-runtime-ktx:2.3.0" implementation "androidx.work:work-runtime-ktx:2.3.3"
// FP // FP
implementation "io.arrow-kt:arrow-core:$arrow_version" implementation "io.arrow-kt:arrow-core:$arrow_version"

View File

@ -31,7 +31,7 @@ data class ContentAttachmentData(
val name: String? = null, val name: String? = null,
val queryUri: String, val queryUri: String,
val path: String, val path: String,
val mimeType: String?, private val mimeType: String?,
val type: Type val type: Type
) : Parcelable { ) : Parcelable {
@ -41,4 +41,6 @@ data class ContentAttachmentData(
AUDIO, AUDIO,
VIDEO VIDEO
} }
fun getSafeMimeType() = if (mimeType == "image/jpg") "image/jpeg" else mimeType
} }

View File

@ -34,7 +34,11 @@ interface FileService {
/** /**
* Download file in cache * Download file in cache
*/ */
FOR_INTERNAL_USE FOR_INTERNAL_USE,
/**
* Download file in file provider path
*/
FOR_EXTERNAL_SHARE
} }
/** /**

View File

@ -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. * 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 @Json(name = "file") override val encryptedFileInfo: EncryptedFileInfo? = null
) : MessageEncryptedContent ) : MessageWithAttachmentContent

View File

@ -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. * 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 @Json(name = "file") override val encryptedFileInfo: EncryptedFileInfo? = null
) : MessageEncryptedContent { ) : MessageWithAttachmentContent {
fun getMimeType(): String { fun getMimeType(): String {
// Mimetype default to plain text, should not be used // Mimetype default to plain text, should not be used

View File

@ -20,6 +20,6 @@ package im.vector.matrix.android.api.session.room.model.message
/** /**
* A content with image information * A content with image information
*/ */
interface MessageImageInfoContent : MessageEncryptedContent { interface MessageImageInfoContent : MessageWithAttachmentContent {
val info: ImageInfo? val info: ImageInfo?
} }

View File

@ -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. * 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 @Json(name = "file") override val encryptedFileInfo: EncryptedFileInfo? = null
) : MessageEncryptedContent ) : MessageWithAttachmentContent

View File

@ -21,7 +21,7 @@ import im.vector.matrix.android.internal.crypto.model.rest.EncryptedFileInfo
/** /**
* Interface for message which can contains an encrypted file * 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. * 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 * Get the url of the encrypted file or of the file
*/ */
fun MessageEncryptedContent.getFileUrl() = encryptedFileInfo?.url ?: url fun MessageWithAttachmentContent.getFileUrl() = encryptedFileInfo?.url ?: url

View File

@ -25,3 +25,7 @@ annotation class SessionFilesDirectory
@Qualifier @Qualifier
@Retention(AnnotationRetention.RUNTIME) @Retention(AnnotationRetention.RUNTIME)
annotation class SessionCacheDirectory annotation class SessionCacheDirectory
@Qualifier
@Retention(AnnotationRetention.RUNTIME)
annotation class CacheDirectory

View File

@ -32,6 +32,7 @@ import im.vector.matrix.android.internal.util.BackgroundDetectionObserver
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import org.matrix.olm.OlmManager import org.matrix.olm.OlmManager
import java.io.File
@Component(modules = [MatrixModule::class, NetworkModule::class, AuthModule::class]) @Component(modules = [MatrixModule::class, NetworkModule::class, AuthModule::class])
@MatrixScope @MatrixScope
@ -52,6 +53,9 @@ internal interface MatrixComponent {
fun resources(): Resources fun resources(): Resources
@CacheDirectory
fun cacheDir(): File
fun olmManager(): OlmManager fun olmManager(): OlmManager
fun taskExecutor(): TaskExecutor fun taskExecutor(): TaskExecutor

View File

@ -26,6 +26,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.android.asCoroutineDispatcher import kotlinx.coroutines.android.asCoroutineDispatcher
import kotlinx.coroutines.asCoroutineDispatcher import kotlinx.coroutines.asCoroutineDispatcher
import org.matrix.olm.OlmManager import org.matrix.olm.OlmManager
import java.io.File
import java.util.concurrent.Executors import java.util.concurrent.Executors
@Module @Module
@ -49,6 +50,13 @@ internal object MatrixModule {
return context.resources return context.resources
} }
@JvmStatic
@Provides
@CacheDirectory
fun providesCacheDir(context: Context): File {
return context.cacheDir
}
@JvmStatic @JvmStatic
@Provides @Provides
@MatrixScope @MatrixScope

View File

@ -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.api.util.Cancelable
import im.vector.matrix.android.internal.crypto.attachments.ElementToDecrypt import im.vector.matrix.android.internal.crypto.attachments.ElementToDecrypt
import im.vector.matrix.android.internal.crypto.attachments.MXEncryptedAttachments 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.SessionCacheDirectory
import im.vector.matrix.android.internal.di.Unauthenticated import im.vector.matrix.android.internal.di.Unauthenticated
import im.vector.matrix.android.internal.extensions.foldToCallback import im.vector.matrix.android.internal.extensions.foldToCallback
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers 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.toCancelable
import im.vector.matrix.android.internal.util.writeToFile import im.vector.matrix.android.internal.util.writeToFile
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
@ -42,8 +42,10 @@ import java.io.IOException
import javax.inject.Inject import javax.inject.Inject
internal class DefaultFileService @Inject constructor( internal class DefaultFileService @Inject constructor(
@SessionCacheDirectory @CacheDirectory
private val cacheDirectory: File, private val cacheDirectory: File,
@SessionCacheDirectory
private val sessionCacheDirectory: File,
private val contentUrlResolver: ContentUrlResolver, private val contentUrlResolver: ContentUrlResolver,
@Unauthenticated @Unauthenticated
private val okHttpClient: OkHttpClient, private val okHttpClient: OkHttpClient,
@ -62,60 +64,50 @@ internal class DefaultFileService @Inject constructor(
return GlobalScope.launch(coroutineDispatchers.main) { return GlobalScope.launch(coroutineDispatchers.main) {
withContext(coroutineDispatchers.io) { withContext(coroutineDispatchers.io) {
Try { Try {
val folder = getFolder(downloadMode, id) val folder = File(sessionCacheDirectory, "MF")
if (!folder.exists()) {
folder.mkdirs()
}
File(folder, fileName) File(folder, fileName)
}.flatMap { destFile -> }.flatMap { destFile ->
if (!destFile.exists() || downloadMode == FileService.DownloadMode.TO_EXPORT) { if (!destFile.exists()) {
Try { val resolvedUrl = contentUrlResolver.resolveFullSize(url) ?: return@flatMap Try.Failure(IllegalArgumentException("url is null"))
val resolvedUrl = contentUrlResolver.resolveFullSize(url) ?: throw IllegalArgumentException("url is null")
val request = Request.Builder() val request = Request.Builder()
.url(resolvedUrl) .url(resolvedUrl)
.build() .build()
val response = okHttpClient.newCall(request).execute() val response = okHttpClient.newCall(request).execute()
var inputStream = response.body?.byteStream() var inputStream = response.body?.byteStream()
Timber.v("Response size ${response.body?.contentLength()} - Stream available: ${inputStream?.available()}") Timber.v("Response size ${response.body?.contentLength()} - Stream available: ${inputStream?.available()}")
if (!response.isSuccessful if (!response.isSuccessful || inputStream == null) {
|| inputStream == null) { return@flatMap Try.Failure(IOException())
throw IOException()
}
if (elementToDecrypt != null) {
Timber.v("## decrypt file")
inputStream = MXEncryptedAttachments.decryptAttachment(inputStream, elementToDecrypt)
?: throw IllegalStateException("Decryption error")
}
writeToFile(inputStream, destFile)
destFile
} }
} 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) .foldToCallback(callback)
}.toCancelable() }.toCancelable()
} }
private fun getFolder(downloadMode: FileService.DownloadMode, id: String): File { private fun copyFile(file: File, downloadMode: FileService.DownloadMode): File {
return when (downloadMode) { return when (downloadMode) {
FileService.DownloadMode.FOR_INTERNAL_USE -> { FileService.DownloadMode.TO_EXPORT ->
// Create dir tree (MF stands for Matrix File): file.copyTo(File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), file.name), true)
// <cache>/<sessionId>/MF/<md5(id)>/ FileService.DownloadMode.FOR_EXTERNAL_SHARE ->
val tmpFolderSession = File(cacheDirectory, "MF") file.copyTo(File(File(cacheDirectory, "ext_share"), file.name), true)
File(tmpFolderSession, id.md5()) FileService.DownloadMode.FOR_INTERNAL_USE ->
} file
FileService.DownloadMode.TO_EXPORT -> {
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
}
} }
.also { folder ->
if (!folder.exists()) {
folder.mkdirs()
}
}
} }
} }

View File

@ -20,6 +20,7 @@ import android.os.Handler
import android.os.Looper import android.os.Looper
import im.vector.matrix.android.api.session.content.ContentUploadStateTracker import im.vector.matrix.android.api.session.content.ContentUploadStateTracker
import im.vector.matrix.android.internal.session.SessionScope import im.vector.matrix.android.internal.session.SessionScope
import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@SessionScope @SessionScope
@ -33,7 +34,13 @@ internal class DefaultContentUploadStateTracker @Inject constructor() : ContentU
val listeners = listeners.getOrPut(key) { ArrayList() } val listeners = listeners.getOrPut(key) { ArrayList() }
listeners.add(updateListener) listeners.add(updateListener)
val currentState = states[key] ?: ContentUploadStateTracker.State.Idle 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) { 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) { private fun updateState(key: String, state: ContentUploadStateTracker.State) {
states[key] = state states[key] = state
mainHandler.post { mainHandler.post {
listeners[key]?.forEach { it.onUpdate(state) } listeners[key]?.forEach {
try {
it.onUpdate(state)
} catch (e: Exception) {
Timber.e(e, "## ContentUploadStateTracker.onUpdate() failed")
}
}
} }
} }
} }

View File

@ -58,7 +58,7 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
override val sessionId: String, override val sessionId: String,
val events: List<Event>, val events: List<Event>,
val attachment: ContentAttachmentData, val attachment: ContentAttachmentData,
val isRoomEncrypted: Boolean, val isEncrypted: Boolean,
val compressBeforeSending: Boolean, val compressBeforeSending: Boolean,
override val lastFailureMessage: String? = null override val lastFailureMessage: String? = null
) : SessionWorkerParams ) : SessionWorkerParams
@ -90,9 +90,11 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
Timber.e(e) Timber.e(e)
notifyTracker(params) { contentUploadStateTracker.setFailure(it, e) } notifyTracker(params) { contentUploadStateTracker.setFailure(it, e) }
return Result.success( return Result.success(
WorkerParamsFactory.toData(params.copy( WorkerParamsFactory.toData(
lastFailureMessage = e.localizedMessage params.copy(
)) lastFailureMessage = e.localizedMessage
)
)
) )
} }
.let { originalFile -> .let { originalFile ->
@ -136,7 +138,7 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
} }
try { try {
val contentUploadResponse = if (params.isRoomEncrypted) { val contentUploadResponse = if (params.isEncrypted) {
Timber.v("Encrypt thumbnail") Timber.v("Encrypt thumbnail")
notifyTracker(params) { contentUploadStateTracker.setEncryptingThumbnail(it) } notifyTracker(params) { contentUploadStateTracker.setEncryptingThumbnail(it) }
val encryptionResult = MXEncryptedAttachments.encryptAttachment(ByteArrayInputStream(thumbnailData.bytes), thumbnailData.mimeType) 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 var uploadedFileEncryptedFileInfo: EncryptedFileInfo? = null
return try { return try {
val contentUploadResponse = if (params.isRoomEncrypted) { val contentUploadResponse = if (params.isEncrypted) {
Timber.v("Encrypt file") Timber.v("Encrypt file")
notifyTracker(params) { contentUploadStateTracker.setEncrypting(it) } notifyTracker(params) { contentUploadStateTracker.setEncrypting(it) }
val encryptionResult = MXEncryptedAttachments.encryptAttachment(FileInputStream(attachmentFile), attachment.mimeType) val encryptionResult = MXEncryptedAttachments.encryptAttachment(FileInputStream(attachmentFile), attachment.getSafeMimeType())
uploadedFileEncryptedFileInfo = encryptionResult.encryptedFileInfo uploadedFileEncryptedFileInfo = encryptionResult.encryptedFileInfo
fileUploader fileUploader
.uploadByteArray(encryptionResult.encryptedByteArray, attachment.name, "application/octet-stream", progressListener) .uploadByteArray(encryptionResult.encryptedByteArray, attachment.name, "application/octet-stream", progressListener)
} else { } else {
fileUploader fileUploader
.uploadFile(attachmentFile, attachment.name, attachment.mimeType, progressListener) .uploadFile(attachmentFile, attachment.name, attachment.getSafeMimeType(), progressListener)
} }
handleSuccess(params, handleSuccess(params,
@ -226,7 +228,7 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
updateEvent(it, attachmentUrl, encryptedFileInfo, thumbnailUrl, thumbnailEncryptedFileInfo, newImageAttributes) 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)) return Result.success(WorkerParamsFactory.toData(sendParams))
} }

View File

@ -261,7 +261,7 @@ internal class LocalEchoEventFactory @Inject constructor(
msgType = MessageType.MSGTYPE_IMAGE, msgType = MessageType.MSGTYPE_IMAGE,
body = attachment.name ?: "image", body = attachment.name ?: "image",
info = ImageInfo( info = ImageInfo(
mimeType = attachment.mimeType, mimeType = attachment.getSafeMimeType(),
width = width?.toInt() ?: 0, width = width?.toInt() ?: 0,
height = height?.toInt() ?: 0, height = height?.toInt() ?: 0,
size = attachment.size.toInt() size = attachment.size.toInt()
@ -293,7 +293,7 @@ internal class LocalEchoEventFactory @Inject constructor(
msgType = MessageType.MSGTYPE_VIDEO, msgType = MessageType.MSGTYPE_VIDEO,
body = attachment.name ?: "video", body = attachment.name ?: "video",
videoInfo = VideoInfo( videoInfo = VideoInfo(
mimeType = attachment.mimeType, mimeType = attachment.getSafeMimeType(),
width = width, width = width,
height = height, height = height,
size = attachment.size, size = attachment.size,
@ -312,7 +312,7 @@ internal class LocalEchoEventFactory @Inject constructor(
msgType = MessageType.MSGTYPE_AUDIO, msgType = MessageType.MSGTYPE_AUDIO,
body = attachment.name ?: "audio", body = attachment.name ?: "audio",
audioInfo = AudioInfo( audioInfo = AudioInfo(
mimeType = attachment.mimeType?.takeIf { it.isNotBlank() } ?: "audio/mpeg", mimeType = attachment.getSafeMimeType()?.takeIf { it.isNotBlank() } ?: "audio/mpeg",
size = attachment.size size = attachment.size
), ),
url = attachment.path url = attachment.path
@ -325,7 +325,7 @@ internal class LocalEchoEventFactory @Inject constructor(
msgType = MessageType.MSGTYPE_FILE, msgType = MessageType.MSGTYPE_FILE,
body = attachment.name ?: "file", body = attachment.name ?: "file",
info = FileInfo( info = FileInfo(
mimeType = attachment.mimeType?.takeIf { it.isNotBlank() } mimeType = attachment.getSafeMimeType()?.takeIf { it.isNotBlank() }
?: "application/octet-stream", ?: "application/octet-stream",
size = attachment.size size = attachment.size
), ),

View File

@ -296,7 +296,7 @@ dependencies {
implementation 'com.airbnb.android:mvrx:1.3.0' implementation 'com.airbnb.android:mvrx:1.3.0'
// Work // Work
implementation "androidx.work:work-runtime-ktx:2.3.0-beta02" implementation "androidx.work:work-runtime-ktx:2.3.3"
// Paging // Paging
implementation "androidx.paging:paging-runtime-ktx:2.1.1" implementation "androidx.paging:paging-runtime-ktx:2.1.1"

View File

@ -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.api.Picker.PICK_IMAGE_DEVICE
import com.kbeanie.multipicker.core.ImagePickerImpl import com.kbeanie.multipicker.core.ImagePickerImpl
import com.kbeanie.multipicker.core.PickerManager import com.kbeanie.multipicker.core.PickerManager
import com.kbeanie.multipicker.utils.IntentUtils
import im.vector.matrix.android.BuildConfig import im.vector.matrix.android.BuildConfig
import im.vector.matrix.android.api.session.content.ContentAttachmentData import im.vector.matrix.android.api.session.content.ContentAttachmentData
import im.vector.riotx.core.platform.Restorable import im.vector.riotx.core.platform.Restorable
@ -176,13 +177,13 @@ class AttachmentsHelper private constructor(private val context: Context,
fun handleShareIntent(intent: Intent): Boolean { fun handleShareIntent(intent: Intent): Boolean {
val type = intent.resolveType(context) ?: return false val type = intent.resolveType(context) ?: return false
if (type.startsWith("image")) { if (type.startsWith("image")) {
imagePicker.submit(intent) imagePicker.submit(IntentUtils.getPickerIntentForSharing(intent))
} else if (type.startsWith("video")) { } else if (type.startsWith("video")) {
videoPicker.submit(intent) videoPicker.submit(IntentUtils.getPickerIntentForSharing(intent))
} else if (type.startsWith("audio")) { } else if (type.startsWith("audio")) {
videoPicker.submit(intent) videoPicker.submit(IntentUtils.getPickerIntentForSharing(intent))
} else if (type.startsWith("application") || type.startsWith("file") || type.startsWith("*")) { } else if (type.startsWith("application") || type.startsWith("file") || type.startsWith("*")) {
filePicker.submit(intent) filePicker.submit(IntentUtils.getPickerIntentForSharing(intent))
} else { } else {
return false return false
} }

View File

@ -23,6 +23,6 @@ import im.vector.matrix.android.api.session.content.ContentAttachmentData
*/ */
fun ContentAttachmentData.isEditable(): Boolean { fun ContentAttachmentData.isEditable(): Boolean {
return type == ContentAttachmentData.Type.IMAGE return type == ContentAttachmentData.Type.IMAGE
&& mimeType?.startsWith("image/") == true && getSafeMimeType()?.startsWith("image/") == true
&& mimeType != "image/gif" && getSafeMimeType() != "image/gif"
} }

View File

@ -37,6 +37,7 @@ import androidx.annotation.StringRes
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.core.app.ActivityOptionsCompat import androidx.core.app.ActivityOptionsCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.net.toUri
import androidx.core.text.buildSpannedString import androidx.core.text.buildSpannedString
import androidx.core.util.Pair import androidx.core.util.Pair
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
@ -57,17 +58,17 @@ import com.airbnb.mvrx.Success
import com.airbnb.mvrx.args import com.airbnb.mvrx.args
import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState 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.checkbox.MaterialCheckBox
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.google.android.material.textfield.TextInputEditText import com.google.android.material.textfield.TextInputEditText
import com.google.android.material.textfield.TextInputLayout import com.google.android.material.textfield.TextInputLayout
import com.jakewharton.rxbinding3.widget.textChanges 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.permalinks.PermalinkFactory
import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.content.ContentAttachmentData import im.vector.matrix.android.api.session.content.ContentAttachmentData
import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.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.Membership
import im.vector.matrix.android.api.session.room.model.message.MessageAudioContent 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.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.MessageTextContent
import im.vector.matrix.android.api.session.room.model.message.MessageVerificationRequestContent 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.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.send.SendState
import im.vector.matrix.android.api.session.room.timeline.Timeline 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.TimelineEvent
import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent 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.MatrixItem
import im.vector.matrix.android.api.util.toMatrixItem 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.R
import im.vector.riotx.core.dialogs.withColoredButton import im.vector.riotx.core.dialogs.withColoredButton
import im.vector.riotx.core.epoxy.LayoutManagerStateRestorer 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.extensions.showKeyboard
import im.vector.riotx.core.files.addEntryToDownloadManager import im.vector.riotx.core.files.addEntryToDownloadManager
import im.vector.riotx.core.glide.GlideApp 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.platform.VectorBaseFragment
import im.vector.riotx.core.resources.ColorProvider import im.vector.riotx.core.resources.ColorProvider
import im.vector.riotx.core.ui.views.JumpToReadMarkerView import im.vector.riotx.core.ui.views.JumpToReadMarkerView
@ -1124,6 +1128,23 @@ class RoomDetailFragment @Inject constructor(
roomDetailViewModel.handle(RoomDetailAction.EnterTrackingUnreadMessagesState) 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<File> {
override fun onSuccess(data: File) {
if (isAdded) {
shareMedia(requireContext(), data, getMimeTypeFromUri(requireContext(), data.toUri()))
}
}
}
)
}
private fun handleActions(action: EventSharedAction) { private fun handleActions(action: EventSharedAction) {
when (action) { when (action) {
is EventSharedAction.OpenUserProfile -> { is EventSharedAction.OpenUserProfile -> {
@ -1145,32 +1166,7 @@ class RoomDetailFragment @Inject constructor(
promptConfirmationToRedactEvent(action) promptConfirmationToRedactEvent(action)
} }
is EventSharedAction.Share -> { is EventSharedAction.Share -> {
// TODO current data communication is too limited onShareActionClicked(action)
// 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() {}
}
)
} }
is EventSharedAction.ViewEditHistory -> { is EventSharedAction.ViewEditHistory -> {
onEditedDecorationClicked(action.messageInformationData) onEditedDecorationClicked(action.messageInformationData)

View File

@ -18,6 +18,7 @@ package im.vector.riotx.features.home.room.detail.timeline.action
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
import androidx.annotation.StringRes import androidx.annotation.StringRes
import im.vector.matrix.android.api.session.room.model.message.MessageWithAttachmentContent
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.platform.VectorSharedAction import im.vector.riotx.core.platform.VectorSharedAction
import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData 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) : data class Reply(val eventId: String) :
EventSharedAction(R.string.reply, R.drawable.ic_reply) 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) EventSharedAction(R.string.share, R.drawable.ic_share)
data class Resend(val eventId: String) : data class Resend(val eventId: String) :

View File

@ -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.isTextMessage
import im.vector.matrix.android.api.session.events.model.toModel 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.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.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.MessageTextContent
import im.vector.matrix.android.api.session.room.model.message.MessageType import im.vector.matrix.android.api.session.room.model.message.MessageType
import im.vector.matrix.android.api.session.room.model.message.MessageVerificationRequestContent import im.vector.matrix.android.api.session.room.model.message.MessageVerificationRequestContent
@ -260,13 +260,8 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
add(EventSharedAction.ViewEditHistory(informationData)) add(EventSharedAction.ViewEditHistory(informationData))
} }
if (canShare(msgType)) { if (canShare(msgType) && messageContent is MessageWithAttachmentContent) {
if (messageContent is MessageImageContent) { add(EventSharedAction.Share(timelineEvent.eventId, messageContent))
session.contentUrlResolver().resolveFullSize(messageContent.url)?.let { url ->
add(EventSharedAction.Share(url))
}
}
// TODO
} }
if (timelineEvent.root.sendState == SendState.SENT) { if (timelineEvent.root.sendState == SendState.SENT) {
@ -374,8 +369,9 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
return when (msgType) { return when (msgType) {
MessageType.MSGTYPE_IMAGE, MessageType.MSGTYPE_IMAGE,
MessageType.MSGTYPE_AUDIO, MessageType.MSGTYPE_AUDIO,
MessageType.MSGTYPE_VIDEO -> true MessageType.MSGTYPE_VIDEO,
else -> false MessageType.MSGTYPE_FILE -> true
else -> false
} }
} }
} }

View File

@ -127,6 +127,15 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder:
GlideApp GlideApp
.with(imageView) .with(imageView)
.load(resolvedUrl) .load(resolvedUrl)
.apply {
if (mode == Mode.THUMBNAIL) {
error(
GlideApp
.with(imageView)
.load(contentUrlResolver.resolveFullSize(data.url))
)
}
}
} }
} }

View File

@ -28,7 +28,6 @@ import androidx.appcompat.widget.SearchView
import androidx.core.view.isVisible import androidx.core.view.isVisible
import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState 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.content.ContentAttachmentData
import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.riotx.R import im.vector.riotx.R
@ -78,7 +77,7 @@ class IncomingShareFragment @Inject constructor(
val intent = vectorBaseActivity.intent val intent = vectorBaseActivity.intent
val isShareManaged = when (intent?.action) { val isShareManaged = when (intent?.action) {
Intent.ACTION_SEND -> { Intent.ACTION_SEND -> {
var isShareManaged = attachmentsHelper.handleShareIntent(IntentUtils.getPickerIntentForSharing(intent)) var isShareManaged = attachmentsHelper.handleShareIntent(intent)
if (!isShareManaged) { if (!isShareManaged) {
isShareManaged = handleTextShare(intent) isShareManaged = handleTextShare(intent)
} }