mirror of
https://github.com/SchildiChat/SchildiChat-android.git
synced 2024-11-21 17:05:39 +03:00
fix: update local file access permission
This commit is contained in:
parent
a1823b0f62
commit
33d09ecf40
14 changed files with 78 additions and 26 deletions
1
changelog.d/3616.bugfix
Normal file
1
changelog.d/3616.bugfix
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Fix crash when accessing a local file and permission is revoked.
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()) {
|
||||||
|
|
|
@ -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
|
||||||
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)
|
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) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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) {
|
// State must be in one of the sending states
|
||||||
room.sendService().cancelSend(action.eventId)
|
if (action.force || action.event.root.sendState.isSending()) {
|
||||||
return
|
room.sendService().cancelSend(action.event.eventId)
|
||||||
}
|
|
||||||
val targetEventId = action.eventId
|
val clearContent = action.event.root.getClearContent()
|
||||||
room.getTimelineEvent(targetEventId)?.let {
|
val messageContent = clearContent?.toModel<MessageContent>() as? MessageWithAttachmentContent
|
||||||
// State must be in one of the sending states
|
messageContent?.getFileUrl()?.takeIf { !it.isMxcUrl() }?.let {
|
||||||
if (!it.root.sendState.isSending()) {
|
_viewEvents.post(RoomDetailViewEvents.RevokeFilePermission(it.toUri()))
|
||||||
Timber.e("Cannot cancel message, it is not sending")
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
room.sendService().cancelSend(targetEventId)
|
} else {
|
||||||
|
Timber.e("Cannot cancel message, it is not sending")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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) :
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue