fix: update local file access permission

This commit is contained in:
yostyle 2024-04-18 11:46:49 +02:00
parent a1823b0f62
commit 33d09ecf40
14 changed files with 78 additions and 26 deletions

1
changelog.d/3616.bugfix Normal file
View file

@ -0,0 +1 @@
Fix crash when accessing a local file and permission is revoked.

View file

@ -31,7 +31,7 @@ class AudioPicker : Picker<MultiPickerAudioType>() {
* Returns selected audio files or empty list if user did not select any files. * Returns selected audio files or empty list if user did not select any files.
*/ */
override fun getSelectedFiles(context: Context, data: Intent?): List<MultiPickerAudioType> { override fun getSelectedFiles(context: Context, data: Intent?): List<MultiPickerAudioType> {
return getSelectedUriList(data).mapNotNull { selectedUri -> return getSelectedUriList(context, data).mapNotNull { selectedUri ->
selectedUri.toMultiPickerAudioType(context) selectedUri.toMultiPickerAudioType(context)
} }
} }

View file

@ -41,7 +41,7 @@ class FilePicker : Picker<MultiPickerBaseType>() {
* Returns selected files or empty list if user did not select any files. * Returns selected files or empty list if user did not select any files.
*/ */
override fun getSelectedFiles(context: Context, data: Intent?): List<MultiPickerBaseType> { override fun getSelectedFiles(context: Context, data: Intent?): List<MultiPickerBaseType> {
return getSelectedUriList(data).mapNotNull { selectedUri -> return getSelectedUriList(context, data).mapNotNull { selectedUri ->
val type = context.contentResolver.getType(selectedUri) val type = context.contentResolver.getType(selectedUri)
when { when {

View file

@ -31,7 +31,7 @@ class ImagePicker : Picker<MultiPickerImageType>() {
* Returns selected image files or empty list if user did not select any files. * Returns selected image files or empty list if user did not select any files.
*/ */
override fun getSelectedFiles(context: Context, data: Intent?): List<MultiPickerImageType> { override fun getSelectedFiles(context: Context, data: Intent?): List<MultiPickerImageType> {
return getSelectedUriList(data).mapNotNull { selectedUri -> return getSelectedUriList(context, data).mapNotNull { selectedUri ->
selectedUri.toMultiPickerImageType(context) selectedUri.toMultiPickerImageType(context)
} }
} }

View file

@ -33,7 +33,7 @@ class MediaPicker : Picker<MultiPickerBaseMediaType>() {
* Returns selected image/video files or empty list if user did not select any files. * Returns selected image/video files or empty list if user did not select any files.
*/ */
override fun getSelectedFiles(context: Context, data: Intent?): List<MultiPickerBaseMediaType> { override fun getSelectedFiles(context: Context, data: Intent?): List<MultiPickerBaseMediaType> {
return getSelectedUriList(data).mapNotNull { selectedUri -> return getSelectedUriList(context, data).mapNotNull { selectedUri ->
val mimeType = context.contentResolver.getType(selectedUri) val mimeType = context.contentResolver.getType(selectedUri)
if (mimeType.isMimeTypeVideo()) { if (mimeType.isMimeTypeVideo()) {

View file

@ -16,6 +16,7 @@
package im.vector.lib.multipicker package im.vector.lib.multipicker
import android.content.ComponentName
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
@ -58,7 +59,17 @@ abstract class Picker<T> {
uriList.forEach { uriList.forEach {
for (resolveInfo in resInfoList) { for (resolveInfo in resInfoList) {
val packageName: String = resolveInfo.activityInfo.packageName val packageName: String = resolveInfo.activityInfo.packageName
// 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) 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) return getSelectedFiles(context, data)
@ -82,7 +93,7 @@ abstract class Picker<T> {
activityResultLauncher.launch(createIntent().apply { addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) }) activityResultLauncher.launch(createIntent().apply { addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) })
} }
protected fun getSelectedUriList(data: Intent?): List<Uri> { protected fun getSelectedUriList(context: Context, data: Intent?): List<Uri> {
val selectedUriList = mutableListOf<Uri>() val selectedUriList = mutableListOf<Uri>()
val dataUri = data?.data val dataUri = data?.data
val clipData = data?.clipData val clipData = data?.clipData
@ -104,6 +115,6 @@ abstract class Picker<T> {
} }
} }
} }
return selectedUriList return selectedUriList.onEach { context.grantUriPermission(context.applicationContext.packageName, it, Intent.FLAG_GRANT_READ_URI_PERMISSION) }
} }
} }

View file

@ -31,7 +31,7 @@ class VideoPicker : Picker<MultiPickerVideoType>() {
* Returns selected video files or empty list if user did not select any files. * Returns selected video files or empty list if user did not select any files.
*/ */
override fun getSelectedFiles(context: Context, data: Intent?): List<MultiPickerVideoType> { override fun getSelectedFiles(context: Context, data: Intent?): List<MultiPickerVideoType> {
return getSelectedUriList(data).mapNotNull { selectedUri -> return getSelectedUriList(context, data).mapNotNull { selectedUri ->
selectedUri.toMultiPickerVideoType(context) selectedUri.toMultiPickerVideoType(context)
} }
} }

View file

@ -17,8 +17,10 @@
package org.matrix.android.sdk.internal.session.content package org.matrix.android.sdk.internal.session.content
import android.content.Context import android.content.Context
import android.content.Intent
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import android.media.MediaMetadataRetriever import android.media.MediaMetadataRetriever
import android.os.Build
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
@ -115,7 +117,15 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
if (allCancelled) { if (allCancelled) {
// there is no point in uploading the image! // there is no point in uploading the image!
return Result.success(inputData) 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 val attachment = params.attachment
@ -396,6 +406,12 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
) )
return Result.success(WorkerParamsFactory.toData(sendParams)).also { return Result.success(WorkerParamsFactory.toData(sendParams)).also {
Timber.v("## handleSuccess $attachmentUrl, work is stopped $isStopped") 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)
}
} }
} }

View file

@ -52,7 +52,7 @@ sealed class RoomDetailAction : VectorViewModelAction {
data class ResendMessage(val eventId: String) : RoomDetailAction() data class ResendMessage(val eventId: String) : RoomDetailAction()
data class RemoveFailedEcho(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() data class VoteToPoll(val eventId: String, val optionKey: String) : RoomDetailAction()

View file

@ -65,6 +65,10 @@ sealed class RoomDetailViewEvents : VectorViewEvents {
val mimeType: String? val mimeType: String?
) : RoomDetailViewEvents() ) : RoomDetailViewEvents()
data class RevokeFilePermission(
val uri: Uri
) : RoomDetailViewEvents()
data class DisplayAndAcceptCall(val call: WebRtcCall) : RoomDetailViewEvents() data class DisplayAndAcceptCall(val call: WebRtcCall) : RoomDetailViewEvents()
object DisplayPromptForIntegrationManager : RoomDetailViewEvents() object DisplayPromptForIntegrationManager : RoomDetailViewEvents()

View file

@ -414,6 +414,7 @@ class TimelineFragment :
RoomDetailViewEvents.RoomReplacementStarted -> handleRoomReplacement() RoomDetailViewEvents.RoomReplacementStarted -> handleRoomReplacement()
RoomDetailViewEvents.OpenElementCallWidget -> handleOpenElementCallWidget() RoomDetailViewEvents.OpenElementCallWidget -> handleOpenElementCallWidget()
RoomDetailViewEvents.DisplayPromptToStopVoiceBroadcast -> displayPromptToStopVoiceBroadcast() RoomDetailViewEvents.DisplayPromptToStopVoiceBroadcast -> displayPromptToStopVoiceBroadcast()
is RoomDetailViewEvents.RevokeFilePermission -> revokeFilePermission(it)
} }
} }
@ -1571,14 +1572,14 @@ class TimelineFragment :
private fun handleCancelSend(action: EventSharedAction.Cancel) { private fun handleCancelSend(action: EventSharedAction.Cancel) {
if (action.force) { if (action.force) {
timelineViewModel.handle(RoomDetailAction.CancelSend(action.eventId, true)) timelineViewModel.handle(RoomDetailAction.CancelSend(action.event, true))
} else { } else {
MaterialAlertDialogBuilder(requireContext()) MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.dialog_title_confirmation) .setTitle(R.string.dialog_title_confirmation)
.setMessage(getString(R.string.event_status_cancel_sending_dialog_message)) .setMessage(getString(R.string.event_status_cancel_sending_dialog_message))
.setNegativeButton(R.string.no, null) .setNegativeButton(R.string.no, null)
.setPositiveButton(R.string.yes) { _, _ -> .setPositiveButton(R.string.yes) { _, _ ->
timelineViewModel.handle(RoomDetailAction.CancelSend(action.eventId, false)) timelineViewModel.handle(RoomDetailAction.CancelSend(action.event, false))
} }
.show() .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() { override fun onTapToReturnToCall() {
callManager.getCurrentCall()?.let { call -> callManager.getCurrentCall()?.let { call ->
VectorCallActivity.newIntent( VectorCallActivity.newIntent(

View file

@ -18,6 +18,7 @@ package im.vector.app.features.home.room.detail
import android.net.Uri import android.net.Uri
import androidx.annotation.IdRes import androidx.annotation.IdRes
import androidx.core.net.toUri
import androidx.lifecycle.asFlow import androidx.lifecycle.asFlow
import com.airbnb.mvrx.Async import com.airbnb.mvrx.Async
import com.airbnb.mvrx.Fail import com.airbnb.mvrx.Fail
@ -84,6 +85,7 @@ import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.matrix.android.sdk.api.MatrixPatterns 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.orFalse
import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.query.QueryStringValue 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.RoomMemberSummary
import org.matrix.android.sdk.api.session.room.model.RoomSummary 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.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.message.getFileUrl
import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent
import org.matrix.android.sdk.api.session.room.model.tombstone.RoomTombstoneContent 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) { private fun handleCancel(action: RoomDetailAction.CancelSend) {
if (room == null) return 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 // State must be in one of the sending states
if (!it.root.sendState.isSending()) { if (action.force || action.event.root.sendState.isSending()) {
Timber.e("Cannot cancel message, it is not sending") room.sendService().cancelSend(action.event.eventId)
return
val clearContent = action.event.root.getClearContent()
val messageContent = clearContent?.toModel<MessageContent>() 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")
} }
} }

View file

@ -23,6 +23,7 @@ import im.vector.app.core.platform.VectorSharedAction
import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData 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.MessageContent
import org.matrix.android.sdk.api.session.room.model.message.MessageWithAttachmentContent import org.matrix.android.sdk.api.session.room.model.message.MessageWithAttachmentContent
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
sealed class EventSharedAction( sealed class EventSharedAction(
@StringRes val titleRes: Int, @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) : 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) 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) EventSharedAction(R.string.action_cancel, R.drawable.ic_close_round)
data class ViewSource(val content: String) : data class ViewSource(val content: String) :

View file

@ -313,7 +313,7 @@ class MessageActionsViewModel @AssistedInject constructor(
private fun ArrayList<EventSharedAction>.addActionsForSendingState(timelineEvent: TimelineEvent) { private fun ArrayList<EventSharedAction>.addActionsForSendingState(timelineEvent: TimelineEvent) {
// TODO is uploading attachment? // TODO is uploading attachment?
if (canCancel(timelineEvent)) { 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) // If sent but not synced (synapse stuck at bottom bug)
// Still offer action to cancel (will only remove local echo) // Still offer action to cancel (will only remove local echo)
timelineEvent.root.eventId?.let { timelineEvent.root.eventId?.let {
add(EventSharedAction.Cancel(it, true)) add(EventSharedAction.Cancel(timelineEvent, true))
} }
// TODO Can be redacted // TODO Can be redacted