Share images from clear and encrypted rooms.
This commit is contained in:
parent
41b4f412c4
commit
5f14516dec
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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.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.SessionCacheDirectory
|
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.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
|
||||||
|
@ -44,6 +44,8 @@ import javax.inject.Inject
|
||||||
internal class DefaultFileService @Inject constructor(
|
internal class DefaultFileService @Inject constructor(
|
||||||
@SessionCacheDirectory
|
@SessionCacheDirectory
|
||||||
private val cacheDirectory: File,
|
private val cacheDirectory: File,
|
||||||
|
@SessionFilesDirectory
|
||||||
|
private val filesDirectory: File,
|
||||||
private val contentUrlResolver: ContentUrlResolver,
|
private val contentUrlResolver: ContentUrlResolver,
|
||||||
@Unauthenticated
|
@Unauthenticated
|
||||||
private val okHttpClient: OkHttpClient,
|
private val okHttpClient: OkHttpClient,
|
||||||
|
@ -62,60 +64,47 @@ 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(cacheDirectory, "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) ?: throw 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)
|
||||||
|
?: throw 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 -> file.copyTo(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), true)
|
||||||
// Create dir tree (MF stands for Matrix File):
|
FileService.DownloadMode.FOR_INTERNAL_USE -> file.copyTo(File(filesDirectory, "ext_share"), true)
|
||||||
// <cache>/<sessionId>/MF/<md5(id)>/
|
FileService.DownloadMode.FOR_EXTERNAL_SHARE -> file
|
||||||
val tmpFolderSession = File(cacheDirectory, "MF")
|
|
||||||
File(tmpFolderSession, id.md5())
|
|
||||||
}
|
|
||||||
FileService.DownloadMode.TO_EXPORT -> {
|
|
||||||
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.also { folder ->
|
|
||||||
if (!folder.exists()) {
|
|
||||||
folder.mkdirs()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -205,7 +205,10 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
|
||||||
|
|
||||||
return Result.success(
|
return Result.success(
|
||||||
WorkerParamsFactory.toData(
|
WorkerParamsFactory.toData(
|
||||||
params.copy(
|
MultipleEventSendingDispatcherWorker.Params(
|
||||||
|
sessionId = params.sessionId,
|
||||||
|
events = params.events,
|
||||||
|
isEncrypted = params.isRoomEncrypted,
|
||||||
lastFailureMessage = failure.localizedMessage
|
lastFailureMessage = failure.localizedMessage
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
|
@ -233,7 +233,7 @@ internal class DefaultSendService @AssistedInject constructor(
|
||||||
val dispatcherWork = createMultipleEventDispatcherWork(isRoomEncrypted)
|
val dispatcherWork = createMultipleEventDispatcherWork(isRoomEncrypted)
|
||||||
|
|
||||||
workManagerProvider.workManager
|
workManagerProvider.workManager
|
||||||
.beginUniqueWork(buildWorkName(UPLOAD_WORK), ExistingWorkPolicy.APPEND, uploadWork)
|
.beginWith(uploadWork)
|
||||||
.then(dispatcherWork)
|
.then(dispatcherWork)
|
||||||
.enqueue()
|
.enqueue()
|
||||||
.also { operation ->
|
.also { operation ->
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -57,17 +57,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 +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.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
|
||||||
|
@ -1145,30 +1147,18 @@ class RoomDetailFragment @Inject constructor(
|
||||||
promptConfirmationToRedactEvent(action)
|
promptConfirmationToRedactEvent(action)
|
||||||
}
|
}
|
||||||
is EventSharedAction.Share -> {
|
is EventSharedAction.Share -> {
|
||||||
// TODO current data communication is too limited
|
session.downloadFile(
|
||||||
// Need to now the media type
|
FileService.DownloadMode.FOR_EXTERNAL_SHARE,
|
||||||
// TODO bad, just POC
|
action.eventId,
|
||||||
BigImageViewer.imageLoader().loadImage(
|
action.messageContent.body,
|
||||||
action.hashCode(),
|
action.messageContent.getFileUrl(),
|
||||||
Uri.parse(action.imageUrl),
|
action.messageContent.encryptedFileInfo?.toElementToDecrypt(),
|
||||||
object : ImageLoader.Callback {
|
object : MatrixCallback<File> {
|
||||||
override fun onFinish() {}
|
override fun onSuccess(data: File) {
|
||||||
|
if (isAdded) {
|
||||||
override fun onSuccess(image: File?) {
|
shareMedia(requireContext(), data, "image/*")
|
||||||
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() {}
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.MessageImageContent
|
||||||
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: MessageImageContent) :
|
||||||
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) :
|
||||||
|
|
|
@ -262,11 +262,9 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
|
||||||
|
|
||||||
if (canShare(msgType)) {
|
if (canShare(msgType)) {
|
||||||
if (messageContent is MessageImageContent) {
|
if (messageContent is MessageImageContent) {
|
||||||
session.contentUrlResolver().resolveFullSize(messageContent.url)?.let { url ->
|
add(EventSharedAction.Share(timelineEvent.eventId, messageContent))
|
||||||
add(EventSharedAction.Share(url))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// TODO
|
// TODO Support other media types
|
||||||
}
|
}
|
||||||
|
|
||||||
if (timelineEvent.root.sendState == SendState.SENT) {
|
if (timelineEvent.root.sendState == SendState.SENT) {
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,4 +3,8 @@
|
||||||
<cache-path
|
<cache-path
|
||||||
name="shared"
|
name="shared"
|
||||||
path="/" />
|
path="/" />
|
||||||
|
|
||||||
|
<files-path
|
||||||
|
name="ext_share"
|
||||||
|
path="ext_share/" />
|
||||||
</paths>
|
</paths>
|
Loading…
Reference in New Issue