Share images from clear and encrypted rooms.

This commit is contained in:
onurays 2020-03-03 10:39:24 +01:00
parent 41b4f412c4
commit 5f14516dec
10 changed files with 69 additions and 80 deletions

View File

@ -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
}
/**

View File

@ -25,10 +25,10 @@ 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.SessionCacheDirectory
import im.vector.matrix.android.internal.di.SessionFilesDirectory
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
@ -44,6 +44,8 @@ import javax.inject.Inject
internal class DefaultFileService @Inject constructor(
@SessionCacheDirectory
private val cacheDirectory: File,
@SessionFilesDirectory
private val filesDirectory: File,
private val contentUrlResolver: ContentUrlResolver,
@Unauthenticated
private val okHttpClient: OkHttpClient,
@ -62,60 +64,47 @@ internal class DefaultFileService @Inject constructor(
return GlobalScope.launch(coroutineDispatchers.main) {
withContext(coroutineDispatchers.io) {
Try {
val folder = getFolder(downloadMode, id)
val folder = File(cacheDirectory, "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) ?: throw 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)
?: throw 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):
// <cache>/<sessionId>/MF/<md5(id)>/
val tmpFolderSession = File(cacheDirectory, "MF")
File(tmpFolderSession, id.md5())
}
FileService.DownloadMode.TO_EXPORT -> {
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
}
FileService.DownloadMode.TO_EXPORT -> file.copyTo(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), true)
FileService.DownloadMode.FOR_INTERNAL_USE -> file.copyTo(File(filesDirectory, "ext_share"), true)
FileService.DownloadMode.FOR_EXTERNAL_SHARE -> file
}
.also { folder ->
if (!folder.exists()) {
folder.mkdirs()
}
}
}
}

View File

@ -205,7 +205,10 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
return Result.success(
WorkerParamsFactory.toData(
params.copy(
MultipleEventSendingDispatcherWorker.Params(
sessionId = params.sessionId,
events = params.events,
isEncrypted = params.isRoomEncrypted,
lastFailureMessage = failure.localizedMessage
)
)

View File

@ -233,7 +233,7 @@ internal class DefaultSendService @AssistedInject constructor(
val dispatcherWork = createMultipleEventDispatcherWork(isRoomEncrypted)
workManagerProvider.workManager
.beginUniqueWork(buildWorkName(UPLOAD_WORK), ExistingWorkPolicy.APPEND, uploadWork)
.beginWith(uploadWork)
.then(dispatcherWork)
.enqueue()
.also { operation ->

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.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
}

View File

@ -57,17 +57,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 +77,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
@ -1145,30 +1147,18 @@ 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/*")
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, "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() {}
}
)
}

View File

@ -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.MessageImageContent
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: MessageImageContent) :
EventSharedAction(R.string.share, R.drawable.ic_share)
data class Resend(val eventId: String) :

View File

@ -262,11 +262,9 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
if (canShare(msgType)) {
if (messageContent is MessageImageContent) {
session.contentUrlResolver().resolveFullSize(messageContent.url)?.let { url ->
add(EventSharedAction.Share(url))
}
add(EventSharedAction.Share(timelineEvent.eventId, messageContent))
}
// TODO
// TODO Support other media types
}
if (timelineEvent.root.sendState == SendState.SENT) {

View File

@ -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)
}

View File

@ -3,4 +3,8 @@
<cache-path
name="shared"
path="/" />
<files-path
name="ext_share"
path="ext_share/" />
</paths>