diff --git a/changelog.d/642.bugfix b/changelog.d/642.bugfix new file mode 100644 index 0000000000..f932845d0b --- /dev/null +++ b/changelog.d/642.bugfix @@ -0,0 +1 @@ +Correction des crashs à l'ouverture des fichiers locaux. \ No newline at end of file diff --git a/changelog.d/766.bugfix b/changelog.d/766.bugfix new file mode 100644 index 0000000000..f932845d0b --- /dev/null +++ b/changelog.d/766.bugfix @@ -0,0 +1 @@ +Correction des crashs à l'ouverture des fichiers locaux. \ No newline at end of file diff --git a/changelog.d/851.bugfix b/changelog.d/851.bugfix new file mode 100644 index 0000000000..f932845d0b --- /dev/null +++ b/changelog.d/851.bugfix @@ -0,0 +1 @@ +Correction des crashs à l'ouverture des fichiers locaux. \ No newline at end of file diff --git a/changelog.d/911.bugfix b/changelog.d/911.bugfix new file mode 100644 index 0000000000..f932845d0b --- /dev/null +++ b/changelog.d/911.bugfix @@ -0,0 +1 @@ +Correction des crashs à l'ouverture des fichiers locaux. \ No newline at end of file diff --git a/library/multipicker/src/main/java/im/vector/lib/multipicker/AudioPicker.kt b/library/multipicker/src/main/java/im/vector/lib/multipicker/AudioPicker.kt index 3d6fdb96fc..7ca9a2550f 100644 --- a/library/multipicker/src/main/java/im/vector/lib/multipicker/AudioPicker.kt +++ b/library/multipicker/src/main/java/im/vector/lib/multipicker/AudioPicker.kt @@ -31,7 +31,8 @@ class AudioPicker : Picker() { * Returns selected audio files or empty list if user did not select any files. */ override fun getSelectedFiles(context: Context, data: Intent?): List { - return getSelectedUriList(data).mapNotNull { selectedUri -> + // Tchap: Grant permission to access the selected file. + return getSelectedUriList(context, data).mapNotNull { selectedUri -> selectedUri.toMultiPickerAudioType(context) } } diff --git a/library/multipicker/src/main/java/im/vector/lib/multipicker/CameraPicker.kt b/library/multipicker/src/main/java/im/vector/lib/multipicker/CameraPicker.kt index c4a2ebbea9..4472334b16 100644 --- a/library/multipicker/src/main/java/im/vector/lib/multipicker/CameraPicker.kt +++ b/library/multipicker/src/main/java/im/vector/lib/multipicker/CameraPicker.kt @@ -40,6 +40,7 @@ class CameraPicker { val photoUri = createPhotoUri(context) val intent = createIntent().apply { putExtra(MediaStore.EXTRA_OUTPUT, photoUri) + addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) } activityResultLauncher.launch(intent) return photoUri diff --git a/library/multipicker/src/main/java/im/vector/lib/multipicker/FilePicker.kt b/library/multipicker/src/main/java/im/vector/lib/multipicker/FilePicker.kt index 928fdf894c..021260fd00 100644 --- a/library/multipicker/src/main/java/im/vector/lib/multipicker/FilePicker.kt +++ b/library/multipicker/src/main/java/im/vector/lib/multipicker/FilePicker.kt @@ -41,7 +41,8 @@ class FilePicker : Picker() { * Returns selected files or empty list if user did not select any files. */ override fun getSelectedFiles(context: Context, data: Intent?): List { - return getSelectedUriList(data).mapNotNull { selectedUri -> + // Tchap: Grant permission to access the selected file. + return getSelectedUriList(context, data).mapNotNull { selectedUri -> val type = context.contentResolver.getType(selectedUri) when { diff --git a/library/multipicker/src/main/java/im/vector/lib/multipicker/ImagePicker.kt b/library/multipicker/src/main/java/im/vector/lib/multipicker/ImagePicker.kt index bc5a13558a..87b03a0fb8 100644 --- a/library/multipicker/src/main/java/im/vector/lib/multipicker/ImagePicker.kt +++ b/library/multipicker/src/main/java/im/vector/lib/multipicker/ImagePicker.kt @@ -31,7 +31,8 @@ class ImagePicker : Picker() { * Returns selected image files or empty list if user did not select any files. */ override fun getSelectedFiles(context: Context, data: Intent?): List { - return getSelectedUriList(data).mapNotNull { selectedUri -> + // Tchap: Grant permission to access the selected file. + return getSelectedUriList(context, data).mapNotNull { selectedUri -> selectedUri.toMultiPickerImageType(context) } } diff --git a/library/multipicker/src/main/java/im/vector/lib/multipicker/MediaPicker.kt b/library/multipicker/src/main/java/im/vector/lib/multipicker/MediaPicker.kt index 82d0e358df..b76ce3bc53 100644 --- a/library/multipicker/src/main/java/im/vector/lib/multipicker/MediaPicker.kt +++ b/library/multipicker/src/main/java/im/vector/lib/multipicker/MediaPicker.kt @@ -33,7 +33,8 @@ class MediaPicker : Picker() { * Returns selected image/video files or empty list if user did not select any files. */ override fun getSelectedFiles(context: Context, data: Intent?): List { - return getSelectedUriList(data).mapNotNull { selectedUri -> + // Tchap: Grant permission to access the selected file. + return getSelectedUriList(context, data).mapNotNull { selectedUri -> val mimeType = context.contentResolver.getType(selectedUri) if (mimeType.isMimeTypeVideo()) { diff --git a/library/multipicker/src/main/java/im/vector/lib/multipicker/Picker.kt b/library/multipicker/src/main/java/im/vector/lib/multipicker/Picker.kt index 1cfcba505f..d03cbd7fe1 100644 --- a/library/multipicker/src/main/java/im/vector/lib/multipicker/Picker.kt +++ b/library/multipicker/src/main/java/im/vector/lib/multipicker/Picker.kt @@ -16,6 +16,7 @@ package im.vector.lib.multipicker +import android.content.ComponentName import android.content.Context import android.content.Intent import android.content.pm.PackageManager @@ -58,7 +59,15 @@ abstract class Picker { uriList.forEach { for (resolveInfo in resInfoList) { val packageName: String = resolveInfo.activityInfo.packageName - context.grantUriPermission(packageName, it, Intent.FLAG_GRANT_READ_URI_PERMISSION) + // Tchap: Replace implicit intent by an explicit to fix crash on some devices like Xiaomi. + try { + context.grantUriPermission(packageName, it, Intent.FLAG_GRANT_READ_URI_PERMISSION) + } catch (e: Exception) { + continue + } + data.action = null + data.component = ComponentName(packageName, resolveInfo.activityInfo.name) + break } } return getSelectedFiles(context, data) @@ -82,7 +91,7 @@ abstract class Picker { activityResultLauncher.launch(createIntent().apply { addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) }) } - protected fun getSelectedUriList(data: Intent?): List { + protected fun getSelectedUriList(context: Context, data: Intent?): List { val selectedUriList = mutableListOf() val dataUri = data?.data val clipData = data?.clipData @@ -104,6 +113,8 @@ abstract class Picker { } } } - return selectedUriList + // Tchap: Grant permission to access the selected file. + val packageName = context.applicationContext.packageName + return selectedUriList.onEach { context.grantUriPermission(packageName, it, Intent.FLAG_GRANT_READ_URI_PERMISSION) } } } diff --git a/library/multipicker/src/main/java/im/vector/lib/multipicker/VideoPicker.kt b/library/multipicker/src/main/java/im/vector/lib/multipicker/VideoPicker.kt index 89bb1af6aa..37d287ed3c 100644 --- a/library/multipicker/src/main/java/im/vector/lib/multipicker/VideoPicker.kt +++ b/library/multipicker/src/main/java/im/vector/lib/multipicker/VideoPicker.kt @@ -31,7 +31,8 @@ class VideoPicker : Picker() { * Returns selected video files or empty list if user did not select any files. */ override fun getSelectedFiles(context: Context, data: Intent?): List { - return getSelectedUriList(data).mapNotNull { selectedUri -> + // Tchap: Grant permission to access the selected file. + return getSelectedUriList(context, data).mapNotNull { selectedUri -> selectedUri.toMultiPickerVideoType(context) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt index 3dd440737a..2f816e15e1 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt @@ -17,8 +17,10 @@ package org.matrix.android.sdk.internal.session.content import android.content.Context +import android.content.Intent import android.graphics.BitmapFactory import android.media.MediaMetadataRetriever +import android.os.Build import androidx.core.net.toUri import androidx.work.WorkerParameters import com.squareup.moshi.JsonClass @@ -115,7 +117,16 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter if (allCancelled) { // there is no point in uploading the image! return Result.success(inputData) - .also { Timber.e("## Send: Work cancelled by user") } + .also { + Timber.e("## Send: Work cancelled by user") + + // Tchap: Revoke read permission to the local file. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + context.revokeUriPermission(context.packageName, params.attachment.queryUri, Intent.FLAG_GRANT_READ_URI_PERMISSION) + } else { + context.revokeUriPermission(params.attachment.queryUri, Intent.FLAG_GRANT_READ_URI_PERMISSION) + } + } } val attachment = params.attachment @@ -396,6 +407,13 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter ) return Result.success(WorkerParamsFactory.toData(sendParams)).also { Timber.v("## handleSuccess $attachmentUrl, work is stopped $isStopped") + + // Tchap: Revoke read permission to the local file. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + context.revokeUriPermission(context.packageName, params.attachment.queryUri, Intent.FLAG_GRANT_READ_URI_PERMISSION) + } else { + context.revokeUriPermission(params.attachment.queryUri, Intent.FLAG_GRANT_READ_URI_PERMISSION) + } } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt index c1e64f5f01..5987dd89ba 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt @@ -52,7 +52,7 @@ sealed class RoomDetailAction : VectorViewModelAction { data class ResendMessage(val eventId: String) : RoomDetailAction() data class RemoveFailedEcho(val eventId: String) : RoomDetailAction() - data class CancelSend(val eventId: String, val force: Boolean) : RoomDetailAction() + data class CancelSend(val event: TimelineEvent, val force: Boolean) : RoomDetailAction() data class VoteToPoll(val eventId: String, val optionKey: String) : RoomDetailAction() diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewEvents.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewEvents.kt index 0799c3657b..886f601947 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewEvents.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewEvents.kt @@ -66,6 +66,11 @@ sealed class RoomDetailViewEvents : VectorViewEvents { val mimeType: String? ) : RoomDetailViewEvents() + // Tchap: Revoke read permission to the local file. + data class RevokeFilePermission( + val uri: Uri + ) : RoomDetailViewEvents() + data class DisplayAndAcceptCall(val call: WebRtcCall) : RoomDetailViewEvents() object DisplayPromptForIntegrationManager : RoomDetailViewEvents() diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt index d284e9d054..67acfb8dae 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt @@ -383,6 +383,7 @@ class TimelineFragment : timelineViewModel.observeViewEvents { when (it) { + is RoomDetailViewEvents.RevokeFilePermission -> revokeFilePermission(it) is RoomDetailViewEvents.SendCallFeedback -> bugReporter.openBugReportScreen(requireActivity(), ReportType.VOIP) is RoomDetailViewEvents.Failure -> displayErrorMessage(it) is RoomDetailViewEvents.OnNewTimelineEvents -> scrollOnNewMessageCallback.addNewTimelineEventIds(it.eventIds) @@ -547,6 +548,22 @@ class TimelineFragment : ) } + // Tchap: Revoke read permission to the local file. + private fun revokeFilePermission(revokeFilePermission: RoomDetailViewEvents.RevokeFilePermission) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + requireContext().revokeUriPermission( + requireContext().applicationContext.packageName, + revokeFilePermission.uri, + Intent.FLAG_GRANT_READ_URI_PERMISSION + ) + } else { + requireContext().revokeUriPermission( + revokeFilePermission.uri, + Intent.FLAG_GRANT_READ_URI_PERMISSION + ) + } + } + private fun displayErrorMessage(error: RoomDetailViewEvents.Failure) { if (error.showInDialog) displayErrorDialog(error.throwable) else showErrorInSnackbar(error.throwable) } @@ -1578,14 +1595,16 @@ class TimelineFragment : private fun handleCancelSend(action: EventSharedAction.Cancel) { if (action.force) { - timelineViewModel.handle(RoomDetailAction.CancelSend(action.eventId, true)) + // Tchap: Revoke read permission to the local file. + timelineViewModel.handle(RoomDetailAction.CancelSend(action.event, true)) } else { MaterialAlertDialogBuilder(requireContext()) .setTitle(R.string.dialog_title_confirmation) .setMessage(getString(R.string.event_status_cancel_sending_dialog_message)) .setNegativeButton(R.string.no, null) .setPositiveButton(R.string.yes) { _, _ -> - timelineViewModel.handle(RoomDetailAction.CancelSend(action.eventId, false)) + // Tchap: Revoke read permission to the local file. + timelineViewModel.handle(RoomDetailAction.CancelSend(action.event, false)) } .show() } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt index 3d1f9eb316..2c3d948d24 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt @@ -18,6 +18,7 @@ package im.vector.app.features.home.room.detail import android.net.Uri import androidx.annotation.IdRes +import androidx.core.net.toUri import androidx.lifecycle.asFlow import com.airbnb.mvrx.Async import com.airbnb.mvrx.Fail @@ -85,6 +86,7 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.matrix.android.sdk.api.MatrixPatterns +import org.matrix.android.sdk.api.MatrixUrls.isMxcUrl import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.query.QueryStringValue @@ -112,6 +114,8 @@ import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.localecho.RoomLocalEcho +import org.matrix.android.sdk.api.session.room.model.message.MessageContent +import org.matrix.android.sdk.api.session.room.model.message.MessageWithAttachmentContent import org.matrix.android.sdk.api.session.room.model.message.getFileUrl import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent import org.matrix.android.sdk.api.session.room.model.tombstone.RoomTombstoneContent @@ -1097,18 +1101,18 @@ class TimelineViewModel @AssistedInject constructor( private fun handleCancel(action: RoomDetailAction.CancelSend) { if (room == null) return - if (action.force) { - room.sendService().cancelSend(action.eventId) - return - } - val targetEventId = action.eventId - room.getTimelineEvent(targetEventId)?.let { - // State must be in one of the sending states - if (!it.root.sendState.isSending()) { - Timber.e("Cannot cancel message, it is not sending") - return + // Tchap: Revoke read permission to the local file. + // State must be in one of the sending states + if (action.force || action.event.root.sendState.isSending()) { + room.sendService().cancelSend(action.event.eventId) + + val clearContent = action.event.root.getClearContent() + val messageContent = clearContent?.toModel() as? MessageWithAttachmentContent + messageContent?.getFileUrl()?.takeIf { !it.isMxcUrl() }?.let { + _viewEvents.post(RoomDetailViewEvents.RevokeFilePermission(it.toUri())) } - room.sendService().cancelSend(targetEventId) + } else { + Timber.e("Cannot cancel message, it is not sending") } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/EventSharedAction.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/EventSharedAction.kt index 7bf9f536f2..b6af0245aa 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/EventSharedAction.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/EventSharedAction.kt @@ -23,6 +23,7 @@ import im.vector.app.core.platform.VectorSharedAction import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData import org.matrix.android.sdk.api.session.room.model.message.MessageContent import org.matrix.android.sdk.api.session.room.model.message.MessageWithAttachmentContent +import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent sealed class EventSharedAction( @StringRes val titleRes: Int, @@ -71,7 +72,7 @@ sealed class EventSharedAction( data class Redact(val eventId: String, val askForReason: Boolean, val dialogTitleRes: Int, val dialogDescriptionRes: Int) : EventSharedAction(R.string.message_action_item_redact, R.drawable.ic_delete, true) - data class Cancel(val eventId: String, val force: Boolean) : + data class Cancel(val event: TimelineEvent, val force: Boolean) : EventSharedAction(R.string.action_cancel, R.drawable.ic_close_round) data class ViewSource(val content: String) : diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt index ccc015e0be..82864dbe2f 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt @@ -313,7 +313,8 @@ class MessageActionsViewModel @AssistedInject constructor( private fun ArrayList.addActionsForSendingState(timelineEvent: TimelineEvent) { // TODO is uploading attachment? if (canCancel(timelineEvent)) { - add(EventSharedAction.Cancel(timelineEvent.eventId, false)) + // Tchap: Revoke read permission to the local file. + add(EventSharedAction.Cancel(timelineEvent, false)) } } @@ -321,7 +322,8 @@ class MessageActionsViewModel @AssistedInject constructor( // If sent but not synced (synapse stuck at bottom bug) // Still offer action to cancel (will only remove local echo) timelineEvent.root.eventId?.let { - add(EventSharedAction.Cancel(it, true)) + // Tchap: Revoke read permission to the local file. + add(EventSharedAction.Cancel(timelineEvent, true)) } // TODO Can be redacted