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 🙌:
|
||||
-
|
||||
- Add ability to install APK from directly from Element (#2381)
|
||||
|
||||
Bugfix 🐛:
|
||||
- Message states cosmetic changes (#3007)
|
||||
- Fix exception in rxSingle (#3180)
|
||||
- Do not invite the current user when creating a room (#3123)
|
||||
- Fix color issues when the system theme is changed (#2738)
|
||||
- Fix issues on Android 11 (#3067)
|
||||
- Fix issue when opening encrypted files (#3186)
|
||||
|
||||
Translations 🗣:
|
||||
-
|
||||
|
@ -29,14 +29,19 @@ import java.io.File
|
||||
*/
|
||||
interface FileService {
|
||||
|
||||
enum class FileState {
|
||||
IN_CACHE,
|
||||
DOWNLOADING,
|
||||
UNKNOWN
|
||||
sealed class FileState {
|
||||
/**
|
||||
* The original file is in cache, but the decrypted files can be deleted for security reason.
|
||||
* 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.
|
||||
*/
|
||||
suspend fun downloadFile(fileName: String,
|
||||
|
@ -22,6 +22,7 @@ import org.matrix.android.sdk.api.extensions.orFalse
|
||||
object MimeTypes {
|
||||
const val Any: String = "*/*"
|
||||
const val OctetStream = "application/octet-stream"
|
||||
const val Apk = "application/vnd.android.package-archive"
|
||||
|
||||
const val Images = "image/*"
|
||||
|
||||
|
@ -219,7 +219,7 @@ internal class DefaultFileService @Inject constructor(
|
||||
fileName: String,
|
||||
mimeType: String?,
|
||||
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(
|
||||
@ -256,12 +256,17 @@ internal class DefaultFileService @Inject constructor(
|
||||
fileName: String,
|
||||
mimeType: String?,
|
||||
elementToDecrypt: ElementToDecrypt?): FileService.FileState {
|
||||
mxcUrl ?: return FileService.FileState.UNKNOWN
|
||||
if (getFiles(mxcUrl, fileName, mimeType, elementToDecrypt != null).file.exists()) return FileService.FileState.IN_CACHE
|
||||
mxcUrl ?: return FileService.FileState.Unknown
|
||||
val files = getFiles(mxcUrl, fileName, mimeType, elementToDecrypt != null)
|
||||
if (files.file.exists()) {
|
||||
return FileService.FileState.InCache(
|
||||
decryptedFileInCache = files.getClearFile().exists()
|
||||
)
|
||||
}
|
||||
val isDownloading = synchronized(ongoing) {
|
||||
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 -->
|
||||
<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 -->
|
||||
<uses-permission
|
||||
android:name="android.permission.READ_CALENDAR"
|
||||
@ -48,6 +51,22 @@
|
||||
android:name="android.hardware.camera.autofocus"
|
||||
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
|
||||
android:name=".VectorApplication"
|
||||
android:allowBackup="false"
|
||||
|
@ -29,6 +29,7 @@ import android.os.PowerManager
|
||||
import android.provider.Settings
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.core.content.getSystemService
|
||||
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,
|
||||
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
||||
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.shareMedia
|
||||
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.databinding.DialogReportContentBinding
|
||||
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.WidgetType
|
||||
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.internal.crypto.model.event.EncryptedEventContent
|
||||
import org.matrix.android.sdk.internal.crypto.model.event.WithHeldCode
|
||||
@ -589,20 +591,53 @@ class RoomDetailFragment @Inject constructor(
|
||||
}
|
||||
|
||||
private fun startOpenFileIntent(action: RoomDetailViewEvents.OpenFile) {
|
||||
if (action.uri != null) {
|
||||
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)
|
||||
}
|
||||
if (action.mimeType == MimeTypes.Apk) {
|
||||
installApk(action)
|
||||
} else {
|
||||
openFile(action)
|
||||
}
|
||||
}
|
||||
|
||||
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() {
|
||||
// The Sticker picker widget is not installed yet. Propose the user to install it
|
||||
val builder = AlertDialog.Builder(requireContext())
|
||||
|
@ -67,9 +67,8 @@ sealed class RoomDetailViewEvents : VectorViewEvents {
|
||||
) : RoomDetailViewEvents()
|
||||
|
||||
data class OpenFile(
|
||||
val mimeType: String?,
|
||||
val uri: Uri?,
|
||||
val throwable: Throwable?
|
||||
val uri: Uri,
|
||||
val mimeType: String?
|
||||
) : 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.toContent
|
||||
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.room.members.ChangeMembershipState
|
||||
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
|
||||
var pendingAction: RoomDetailAction? = null
|
||||
|
||||
// Slot to keep a pending event during permission request
|
||||
var pendingEvent: RoomDetailViewEvents? = null
|
||||
|
||||
private var trackUnreadMessages = AtomicBoolean(false)
|
||||
private var mostRecentDisplayedEvent: TimelineEvent? = null
|
||||
|
||||
@ -823,7 +827,7 @@ class RoomDetailViewModel @AssistedInject constructor(
|
||||
}
|
||||
}.exhaustive
|
||||
}
|
||||
is SendMode.EDIT -> {
|
||||
is SendMode.EDIT -> {
|
||||
// is original event a reply?
|
||||
val inReplyTo = state.sendMode.timelineEvent.getRelationContent()?.inReplyTo?.eventId
|
||||
if (inReplyTo != null) {
|
||||
@ -846,7 +850,7 @@ class RoomDetailViewModel @AssistedInject constructor(
|
||||
_viewEvents.post(RoomDetailViewEvents.MessageSent)
|
||||
popDraft()
|
||||
}
|
||||
is SendMode.QUOTE -> {
|
||||
is SendMode.QUOTE -> {
|
||||
val messageContent = state.sendMode.timelineEvent.getLastMessageContent()
|
||||
val textMsg = messageContent?.body
|
||||
|
||||
@ -867,7 +871,7 @@ class RoomDetailViewModel @AssistedInject constructor(
|
||||
_viewEvents.post(RoomDetailViewEvents.MessageSent)
|
||||
popDraft()
|
||||
}
|
||||
is SendMode.REPLY -> {
|
||||
is SendMode.REPLY -> {
|
||||
state.sendMode.timelineEvent.let {
|
||||
room.replyToMessage(it, action.text.toString(), action.autoMarkdown)
|
||||
_viewEvents.post(RoomDetailViewEvents.MessageSent)
|
||||
@ -1138,35 +1142,40 @@ class RoomDetailViewModel @AssistedInject constructor(
|
||||
val mxcUrl = action.messageFileContent.getFileUrl() ?: return
|
||||
val isLocalSendingFile = action.senderId == session.myUserId
|
||||
&& mxcUrl.startsWith("content://")
|
||||
val isDownloaded = session.fileService().isFileInCache(action.messageFileContent)
|
||||
if (isLocalSendingFile) {
|
||||
tryOrNull { Uri.parse(mxcUrl) }?.let {
|
||||
_viewEvents.post(RoomDetailViewEvents.OpenFile(
|
||||
action.messageFileContent.mimeType,
|
||||
it,
|
||||
null
|
||||
))
|
||||
}
|
||||
} else if (isDownloaded) {
|
||||
// we can open it
|
||||
session.fileService().getTemporarySharableURI(action.messageFileContent)?.let { uri ->
|
||||
_viewEvents.post(RoomDetailViewEvents.OpenFile(
|
||||
action.messageFileContent.mimeType,
|
||||
uri,
|
||||
null
|
||||
action.messageFileContent.mimeType
|
||||
))
|
||||
}
|
||||
} else {
|
||||
viewModelScope.launch {
|
||||
val result = runCatching {
|
||||
session.fileService().downloadFile(messageContent = action.messageFileContent)
|
||||
val fileState = session.fileService().fileState(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(
|
||||
action.messageFileContent.mimeType,
|
||||
result.getOrNull(),
|
||||
result.exceptionOrNull()
|
||||
))
|
||||
if (canOpen) {
|
||||
// We can now open the file
|
||||
session.fileService().getTemporarySharableURI(action.messageFileContent)?.let { uri ->
|
||||
_viewEvents.post(RoomDetailViewEvents.OpenFile(
|
||||
uri,
|
||||
action.messageFileContent.mimeType
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user