Merge pull request #3238 from vector-im/feature/bma/android11
Android 11 fixes an other fixes for attachement
This commit is contained in:
commit
7beb483972
@ -5,13 +5,15 @@ Features ✨:
|
|||||||
-
|
-
|
||||||
|
|
||||||
Improvements 🙌:
|
Improvements 🙌:
|
||||||
-
|
- Add ability to install APK from directly from Element (#2381)
|
||||||
|
|
||||||
Bugfix 🐛:
|
Bugfix 🐛:
|
||||||
- Message states cosmetic changes (#3007)
|
- Message states cosmetic changes (#3007)
|
||||||
- Fix exception in rxSingle (#3180)
|
- Fix exception in rxSingle (#3180)
|
||||||
- Do not invite the current user when creating a room (#3123)
|
- Do not invite the current user when creating a room (#3123)
|
||||||
- Fix color issues when the system theme is changed (#2738)
|
- Fix color issues when the system theme is changed (#2738)
|
||||||
|
- Fix issues on Android 11 (#3067)
|
||||||
|
- Fix issue when opening encrypted files (#3186)
|
||||||
|
|
||||||
Translations 🗣:
|
Translations 🗣:
|
||||||
-
|
-
|
||||||
|
@ -29,14 +29,19 @@ import java.io.File
|
|||||||
*/
|
*/
|
||||||
interface FileService {
|
interface FileService {
|
||||||
|
|
||||||
enum class FileState {
|
sealed class FileState {
|
||||||
IN_CACHE,
|
/**
|
||||||
DOWNLOADING,
|
* The original file is in cache, but the decrypted files can be deleted for security reason.
|
||||||
UNKNOWN
|
* To decrypt the file again, call [downloadFile], the encrypted file will not be downloaded again
|
||||||
|
* @param decryptedFileInCache true if the decrypted file is available. Always true for clear files.
|
||||||
|
*/
|
||||||
|
data class InCache(val decryptedFileInCache: Boolean) : FileState()
|
||||||
|
object Downloading : FileState()
|
||||||
|
object Unknown : FileState()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Download a file.
|
* Download a file if necessary and ensure that if the file is encrypted, the file is decrypted.
|
||||||
* Result will be a decrypted file, stored in the cache folder. url parameter will be used to create unique filename to avoid name collision.
|
* Result will be a decrypted file, stored in the cache folder. url parameter will be used to create unique filename to avoid name collision.
|
||||||
*/
|
*/
|
||||||
suspend fun downloadFile(fileName: String,
|
suspend fun downloadFile(fileName: String,
|
||||||
|
@ -22,6 +22,7 @@ import org.matrix.android.sdk.api.extensions.orFalse
|
|||||||
object MimeTypes {
|
object MimeTypes {
|
||||||
const val Any: String = "*/*"
|
const val Any: String = "*/*"
|
||||||
const val OctetStream = "application/octet-stream"
|
const val OctetStream = "application/octet-stream"
|
||||||
|
const val Apk = "application/vnd.android.package-archive"
|
||||||
|
|
||||||
const val Images = "image/*"
|
const val Images = "image/*"
|
||||||
|
|
||||||
|
@ -219,7 +219,7 @@ internal class DefaultFileService @Inject constructor(
|
|||||||
fileName: String,
|
fileName: String,
|
||||||
mimeType: String?,
|
mimeType: String?,
|
||||||
elementToDecrypt: ElementToDecrypt?): Boolean {
|
elementToDecrypt: ElementToDecrypt?): Boolean {
|
||||||
return fileState(mxcUrl, fileName, mimeType, elementToDecrypt) == FileService.FileState.IN_CACHE
|
return fileState(mxcUrl, fileName, mimeType, elementToDecrypt) is FileService.FileState.InCache
|
||||||
}
|
}
|
||||||
|
|
||||||
internal data class CachedFiles(
|
internal data class CachedFiles(
|
||||||
@ -256,12 +256,17 @@ internal class DefaultFileService @Inject constructor(
|
|||||||
fileName: String,
|
fileName: String,
|
||||||
mimeType: String?,
|
mimeType: String?,
|
||||||
elementToDecrypt: ElementToDecrypt?): FileService.FileState {
|
elementToDecrypt: ElementToDecrypt?): FileService.FileState {
|
||||||
mxcUrl ?: return FileService.FileState.UNKNOWN
|
mxcUrl ?: return FileService.FileState.Unknown
|
||||||
if (getFiles(mxcUrl, fileName, mimeType, elementToDecrypt != null).file.exists()) return FileService.FileState.IN_CACHE
|
val files = getFiles(mxcUrl, fileName, mimeType, elementToDecrypt != null)
|
||||||
|
if (files.file.exists()) {
|
||||||
|
return FileService.FileState.InCache(
|
||||||
|
decryptedFileInCache = files.getClearFile().exists()
|
||||||
|
)
|
||||||
|
}
|
||||||
val isDownloading = synchronized(ongoing) {
|
val isDownloading = synchronized(ongoing) {
|
||||||
ongoing[mxcUrl] != null
|
ongoing[mxcUrl] != null
|
||||||
}
|
}
|
||||||
return if (isDownloading) FileService.FileState.DOWNLOADING else FileService.FileState.UNKNOWN
|
return if (isDownloading) FileService.FileState.Downloading else FileService.FileState.Unknown
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -28,6 +28,9 @@
|
|||||||
<!-- Needed for incoming calls -->
|
<!-- Needed for incoming calls -->
|
||||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
|
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
|
||||||
|
|
||||||
|
<!-- To be able to install APK from the application -->
|
||||||
|
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||||
|
|
||||||
<!-- Jitsi libs adds CALENDAR permissions, but we can remove them safely according to https://github.com/jitsi/jitsi-meet/issues/4068#issuecomment-480482481 -->
|
<!-- Jitsi libs adds CALENDAR permissions, but we can remove them safely according to https://github.com/jitsi/jitsi-meet/issues/4068#issuecomment-480482481 -->
|
||||||
<uses-permission
|
<uses-permission
|
||||||
android:name="android.permission.READ_CALENDAR"
|
android:name="android.permission.READ_CALENDAR"
|
||||||
@ -48,6 +51,22 @@
|
|||||||
android:name="android.hardware.camera.autofocus"
|
android:name="android.hardware.camera.autofocus"
|
||||||
android:required="false" />
|
android:required="false" />
|
||||||
|
|
||||||
|
<!-- Since Android 11, see https://developer.android.com/training/package-visibility -->
|
||||||
|
<queries>
|
||||||
|
<!-- To open URL in CustomTab (prefetch, etc.). It makes CustomTabsClient.getPackageName() work
|
||||||
|
see https://developer.android.com/training/package-visibility/use-cases#open-urls-custom-tabs -->
|
||||||
|
<intent>
|
||||||
|
<action android:name="android.support.customtabs.action.CustomTabsService" />
|
||||||
|
</intent>
|
||||||
|
|
||||||
|
<!-- The app can open attachments of any mime type
|
||||||
|
see https://developer.android.com/training/package-visibility/use-cases#open-a-file -->
|
||||||
|
<intent>
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
<data android:mimeType="*/*" />
|
||||||
|
</intent>
|
||||||
|
</queries>
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name=".VectorApplication"
|
android:name=".VectorApplication"
|
||||||
android:allowBackup="false"
|
android:allowBackup="false"
|
||||||
|
@ -29,6 +29,7 @@ import android.os.PowerManager
|
|||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.activity.result.ActivityResultLauncher
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import androidx.core.content.getSystemService
|
import androidx.core.content.getSystemService
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
@ -132,6 +133,17 @@ fun startAddGoogleAccountIntent(context: Context, activityResultLauncher: Activi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
|
fun startInstallFromSourceIntent(context: Context, activityResultLauncher: ActivityResultLauncher<Intent>) {
|
||||||
|
try {
|
||||||
|
val intent = Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES)
|
||||||
|
.setData(Uri.parse(String.format("package:%s", context.packageName)))
|
||||||
|
activityResultLauncher.launch(intent)
|
||||||
|
} catch (activityNotFoundException: ActivityNotFoundException) {
|
||||||
|
context.toast(R.string.error_no_external_application_found)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun startSharePlainTextIntent(fragment: Fragment,
|
fun startSharePlainTextIntent(fragment: Fragment,
|
||||||
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
||||||
chooserTitle: String?,
|
chooserTitle: String?,
|
||||||
|
@ -113,6 +113,7 @@ import im.vector.app.core.utils.registerForPermissionsResult
|
|||||||
import im.vector.app.core.utils.saveMedia
|
import im.vector.app.core.utils.saveMedia
|
||||||
import im.vector.app.core.utils.shareMedia
|
import im.vector.app.core.utils.shareMedia
|
||||||
import im.vector.app.core.utils.shareText
|
import im.vector.app.core.utils.shareText
|
||||||
|
import im.vector.app.core.utils.startInstallFromSourceIntent
|
||||||
import im.vector.app.core.utils.toast
|
import im.vector.app.core.utils.toast
|
||||||
import im.vector.app.databinding.DialogReportContentBinding
|
import im.vector.app.databinding.DialogReportContentBinding
|
||||||
import im.vector.app.databinding.FragmentRoomDetailBinding
|
import im.vector.app.databinding.FragmentRoomDetailBinding
|
||||||
@ -197,6 +198,7 @@ import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent
|
|||||||
import org.matrix.android.sdk.api.session.widgets.model.Widget
|
import org.matrix.android.sdk.api.session.widgets.model.Widget
|
||||||
import org.matrix.android.sdk.api.session.widgets.model.WidgetType
|
import org.matrix.android.sdk.api.session.widgets.model.WidgetType
|
||||||
import org.matrix.android.sdk.api.util.MatrixItem
|
import org.matrix.android.sdk.api.util.MatrixItem
|
||||||
|
import org.matrix.android.sdk.api.util.MimeTypes
|
||||||
import org.matrix.android.sdk.api.util.toMatrixItem
|
import org.matrix.android.sdk.api.util.toMatrixItem
|
||||||
import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent
|
import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent
|
||||||
import org.matrix.android.sdk.internal.crypto.model.event.WithHeldCode
|
import org.matrix.android.sdk.internal.crypto.model.event.WithHeldCode
|
||||||
@ -589,20 +591,53 @@ class RoomDetailFragment @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun startOpenFileIntent(action: RoomDetailViewEvents.OpenFile) {
|
private fun startOpenFileIntent(action: RoomDetailViewEvents.OpenFile) {
|
||||||
if (action.uri != null) {
|
if (action.mimeType == MimeTypes.Apk) {
|
||||||
val intent = Intent(Intent.ACTION_VIEW).apply {
|
installApk(action)
|
||||||
setDataAndTypeAndNormalize(action.uri, action.mimeType)
|
} else {
|
||||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_ACTIVITY_NEW_TASK)
|
openFile(action)
|
||||||
}
|
|
||||||
|
|
||||||
if (intent.resolveActivity(requireActivity().packageManager) != null) {
|
|
||||||
requireActivity().startActivity(intent)
|
|
||||||
} else {
|
|
||||||
requireActivity().toast(R.string.error_no_external_application_found)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun openFile(action: RoomDetailViewEvents.OpenFile) {
|
||||||
|
val intent = Intent(Intent.ACTION_VIEW).apply {
|
||||||
|
setDataAndTypeAndNormalize(action.uri, action.mimeType)
|
||||||
|
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (intent.resolveActivity(requireActivity().packageManager) != null) {
|
||||||
|
requireActivity().startActivity(intent)
|
||||||
|
} else {
|
||||||
|
requireActivity().toast(R.string.error_no_external_application_found)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun installApk(action: RoomDetailViewEvents.OpenFile) {
|
||||||
|
val safeContext = context ?: return
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
if (!safeContext.packageManager.canRequestPackageInstalls()) {
|
||||||
|
roomDetailViewModel.pendingEvent = action
|
||||||
|
startInstallFromSourceIntent(safeContext, installApkActivityResultLauncher)
|
||||||
|
} else {
|
||||||
|
openFile(action)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
openFile(action)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val installApkActivityResultLauncher = registerStartForActivityResult { activityResult ->
|
||||||
|
if (activityResult.resultCode == Activity.RESULT_OK) {
|
||||||
|
roomDetailViewModel.pendingEvent?.let {
|
||||||
|
if (it is RoomDetailViewEvents.OpenFile) {
|
||||||
|
openFile(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// User cancelled
|
||||||
|
}
|
||||||
|
roomDetailViewModel.pendingEvent = null
|
||||||
|
}
|
||||||
|
|
||||||
private fun displayPromptForIntegrationManager() {
|
private fun displayPromptForIntegrationManager() {
|
||||||
// The Sticker picker widget is not installed yet. Propose the user to install it
|
// The Sticker picker widget is not installed yet. Propose the user to install it
|
||||||
val builder = AlertDialog.Builder(requireContext())
|
val builder = AlertDialog.Builder(requireContext())
|
||||||
|
@ -67,9 +67,8 @@ sealed class RoomDetailViewEvents : VectorViewEvents {
|
|||||||
) : RoomDetailViewEvents()
|
) : RoomDetailViewEvents()
|
||||||
|
|
||||||
data class OpenFile(
|
data class OpenFile(
|
||||||
val mimeType: String?,
|
val uri: Uri,
|
||||||
val uri: Uri?,
|
val mimeType: String?
|
||||||
val throwable: Throwable?
|
|
||||||
) : RoomDetailViewEvents()
|
) : RoomDetailViewEvents()
|
||||||
|
|
||||||
abstract class SendMessageResult : RoomDetailViewEvents()
|
abstract class SendMessageResult : RoomDetailViewEvents()
|
||||||
|
@ -78,6 +78,7 @@ import org.matrix.android.sdk.api.session.events.model.isAttachmentMessage
|
|||||||
import org.matrix.android.sdk.api.session.events.model.isTextMessage
|
import org.matrix.android.sdk.api.session.events.model.isTextMessage
|
||||||
import org.matrix.android.sdk.api.session.events.model.toContent
|
import org.matrix.android.sdk.api.session.events.model.toContent
|
||||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||||
|
import org.matrix.android.sdk.api.session.file.FileService
|
||||||
import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities
|
import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities
|
||||||
import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState
|
import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState
|
||||||
import org.matrix.android.sdk.api.session.room.members.roomMemberQueryParams
|
import org.matrix.android.sdk.api.session.room.members.roomMemberQueryParams
|
||||||
@ -140,6 +141,9 @@ class RoomDetailViewModel @AssistedInject constructor(
|
|||||||
// Slot to keep a pending action during permission request
|
// Slot to keep a pending action during permission request
|
||||||
var pendingAction: RoomDetailAction? = null
|
var pendingAction: RoomDetailAction? = null
|
||||||
|
|
||||||
|
// Slot to keep a pending event during permission request
|
||||||
|
var pendingEvent: RoomDetailViewEvents? = null
|
||||||
|
|
||||||
private var trackUnreadMessages = AtomicBoolean(false)
|
private var trackUnreadMessages = AtomicBoolean(false)
|
||||||
private var mostRecentDisplayedEvent: TimelineEvent? = null
|
private var mostRecentDisplayedEvent: TimelineEvent? = null
|
||||||
|
|
||||||
@ -823,7 +827,7 @@ class RoomDetailViewModel @AssistedInject constructor(
|
|||||||
}
|
}
|
||||||
}.exhaustive
|
}.exhaustive
|
||||||
}
|
}
|
||||||
is SendMode.EDIT -> {
|
is SendMode.EDIT -> {
|
||||||
// is original event a reply?
|
// is original event a reply?
|
||||||
val inReplyTo = state.sendMode.timelineEvent.getRelationContent()?.inReplyTo?.eventId
|
val inReplyTo = state.sendMode.timelineEvent.getRelationContent()?.inReplyTo?.eventId
|
||||||
if (inReplyTo != null) {
|
if (inReplyTo != null) {
|
||||||
@ -846,7 +850,7 @@ class RoomDetailViewModel @AssistedInject constructor(
|
|||||||
_viewEvents.post(RoomDetailViewEvents.MessageSent)
|
_viewEvents.post(RoomDetailViewEvents.MessageSent)
|
||||||
popDraft()
|
popDraft()
|
||||||
}
|
}
|
||||||
is SendMode.QUOTE -> {
|
is SendMode.QUOTE -> {
|
||||||
val messageContent = state.sendMode.timelineEvent.getLastMessageContent()
|
val messageContent = state.sendMode.timelineEvent.getLastMessageContent()
|
||||||
val textMsg = messageContent?.body
|
val textMsg = messageContent?.body
|
||||||
|
|
||||||
@ -867,7 +871,7 @@ class RoomDetailViewModel @AssistedInject constructor(
|
|||||||
_viewEvents.post(RoomDetailViewEvents.MessageSent)
|
_viewEvents.post(RoomDetailViewEvents.MessageSent)
|
||||||
popDraft()
|
popDraft()
|
||||||
}
|
}
|
||||||
is SendMode.REPLY -> {
|
is SendMode.REPLY -> {
|
||||||
state.sendMode.timelineEvent.let {
|
state.sendMode.timelineEvent.let {
|
||||||
room.replyToMessage(it, action.text.toString(), action.autoMarkdown)
|
room.replyToMessage(it, action.text.toString(), action.autoMarkdown)
|
||||||
_viewEvents.post(RoomDetailViewEvents.MessageSent)
|
_viewEvents.post(RoomDetailViewEvents.MessageSent)
|
||||||
@ -1138,35 +1142,40 @@ class RoomDetailViewModel @AssistedInject constructor(
|
|||||||
val mxcUrl = action.messageFileContent.getFileUrl() ?: return
|
val mxcUrl = action.messageFileContent.getFileUrl() ?: return
|
||||||
val isLocalSendingFile = action.senderId == session.myUserId
|
val isLocalSendingFile = action.senderId == session.myUserId
|
||||||
&& mxcUrl.startsWith("content://")
|
&& mxcUrl.startsWith("content://")
|
||||||
val isDownloaded = session.fileService().isFileInCache(action.messageFileContent)
|
|
||||||
if (isLocalSendingFile) {
|
if (isLocalSendingFile) {
|
||||||
tryOrNull { Uri.parse(mxcUrl) }?.let {
|
tryOrNull { Uri.parse(mxcUrl) }?.let {
|
||||||
_viewEvents.post(RoomDetailViewEvents.OpenFile(
|
_viewEvents.post(RoomDetailViewEvents.OpenFile(
|
||||||
action.messageFileContent.mimeType,
|
|
||||||
it,
|
it,
|
||||||
null
|
action.messageFileContent.mimeType
|
||||||
))
|
|
||||||
}
|
|
||||||
} else if (isDownloaded) {
|
|
||||||
// we can open it
|
|
||||||
session.fileService().getTemporarySharableURI(action.messageFileContent)?.let { uri ->
|
|
||||||
_viewEvents.post(RoomDetailViewEvents.OpenFile(
|
|
||||||
action.messageFileContent.mimeType,
|
|
||||||
uri,
|
|
||||||
null
|
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val result = runCatching {
|
val fileState = session.fileService().fileState(action.messageFileContent)
|
||||||
session.fileService().downloadFile(messageContent = action.messageFileContent)
|
var canOpen = fileState is FileService.FileState.InCache && fileState.decryptedFileInCache
|
||||||
|
if (!canOpen) {
|
||||||
|
// First download, or download and decrypt, or decrypt from cache
|
||||||
|
val result = runCatching {
|
||||||
|
session.fileService().downloadFile(messageContent = action.messageFileContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
_viewEvents.post(RoomDetailViewEvents.DownloadFileState(
|
||||||
|
action.messageFileContent.mimeType,
|
||||||
|
result.getOrNull(),
|
||||||
|
result.exceptionOrNull()
|
||||||
|
))
|
||||||
|
canOpen = result.isSuccess
|
||||||
}
|
}
|
||||||
|
|
||||||
_viewEvents.post(RoomDetailViewEvents.DownloadFileState(
|
if (canOpen) {
|
||||||
action.messageFileContent.mimeType,
|
// We can now open the file
|
||||||
result.getOrNull(),
|
session.fileService().getTemporarySharableURI(action.messageFileContent)?.let { uri ->
|
||||||
result.exceptionOrNull()
|
_viewEvents.post(RoomDetailViewEvents.OpenFile(
|
||||||
))
|
uri,
|
||||||
|
action.messageFileContent.mimeType
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user