diff --git a/changelog.d/3616.bugfix b/changelog.d/3616.bugfix new file mode 100644 index 0000000000..0e9d6f2e55 --- /dev/null +++ b/changelog.d/3616.bugfix @@ -0,0 +1 @@ +Fix crash when accessing a local file and permission is revoked. \ 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..f7cd0c7808 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,7 @@ 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 -> + return getSelectedUriList(context, data).mapNotNull { selectedUri -> selectedUri.toMultiPickerAudioType(context) } } 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..17c3a27e7b 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,7 @@ 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 -> + 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..4d8f3c205b 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,7 @@ 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 -> + 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..36d62198ff 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,7 @@ 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 -> + 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..3010c14994 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,17 @@ 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) + + // Replace implicit intent by an explicit to fix crash on some devices like Xiaomi. + // see https://juejin.cn/post/7031736325422186510 + 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 +93,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 +115,6 @@ abstract class Picker { } } } - return selectedUriList + return selectedUriList.onEach { context.grantUriPermission(context.applicationContext.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..89316f093f 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,7 @@ 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 -> + 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..6a6eeec12c 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,15 @@ 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") + + 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 +406,12 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter ) return Result.success(WorkerParamsFactory.toData(sendParams)).also { Timber.v("## handleSuccess $attachmentUrl, work is stopped $isStopped") + + 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 30bcf7f8eb..8961953168 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 1183951b45..ba7d0e6d60 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 @@ -65,6 +65,10 @@ sealed class RoomDetailViewEvents : VectorViewEvents { val mimeType: String? ) : RoomDetailViewEvents() + 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 f80855663f..43dfbf8bd4 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 @@ -414,6 +414,7 @@ class TimelineFragment : RoomDetailViewEvents.RoomReplacementStarted -> handleRoomReplacement() RoomDetailViewEvents.OpenElementCallWidget -> handleOpenElementCallWidget() RoomDetailViewEvents.DisplayPromptToStopVoiceBroadcast -> displayPromptToStopVoiceBroadcast() + is RoomDetailViewEvents.RevokeFilePermission -> revokeFilePermission(it) } } @@ -1571,14 +1572,14 @@ class TimelineFragment : private fun handleCancelSend(action: EventSharedAction.Cancel) { if (action.force) { - timelineViewModel.handle(RoomDetailAction.CancelSend(action.eventId, true)) + 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)) + timelineViewModel.handle(RoomDetailAction.CancelSend(action.event, false)) } .show() } @@ -2051,6 +2052,21 @@ class TimelineFragment : } } + 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 + ) + } + } + override fun onTapToReturnToCall() { callManager.getCurrentCall()?.let { call -> VectorCallActivity.newIntent( 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 0f396a4a8b..15b5b139de 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 @@ -84,6 +85,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 @@ -111,6 +113,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 @@ -1074,18 +1078,17 @@ 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 + // 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 18ff638390..7b9bdd6bc4 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 8809c4f0bf..aa8229d1bd 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,7 @@ class MessageActionsViewModel @AssistedInject constructor( private fun ArrayList.addActionsForSendingState(timelineEvent: TimelineEvent) { // TODO is uploading attachment? if (canCancel(timelineEvent)) { - add(EventSharedAction.Cancel(timelineEvent.eventId, false)) + add(EventSharedAction.Cancel(timelineEvent, false)) } } @@ -321,7 +321,7 @@ 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)) + add(EventSharedAction.Cancel(timelineEvent, true)) } // TODO Can be redacted