diff --git a/CHANGES.md b/CHANGES.md index 2574ea07b5..85819b4604 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -5,7 +5,7 @@ Features ✨: - Improvements 🙌: - - + - Add ability to install APK from directly from Element (#2381) Bugfix 🐛: - Message states cosmetic changes (#3007) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MimeTypes.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MimeTypes.kt index c74999b4ab..182b37f2ad 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MimeTypes.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MimeTypes.kt @@ -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/*" diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml index 6c6ea46626..9322adbe04 100644 --- a/vector/src/main/AndroidManifest.xml +++ b/vector/src/main/AndroidManifest.xml @@ -28,6 +28,9 @@ + + + ) { + 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?, chooserTitle: String?, diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt index b7e2e189d3..13e9fb18b0 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt @@ -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()) 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 9f801e7272..b326700d5e 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 @@ -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() diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt index f4d3c8138c..e21428921f 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt @@ -141,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 @@ -1142,9 +1145,8 @@ class RoomDetailViewModel @AssistedInject constructor( if (isLocalSendingFile) { tryOrNull { Uri.parse(mxcUrl) }?.let { _viewEvents.post(RoomDetailViewEvents.OpenFile( - action.messageFileContent.mimeType, it, - null + action.messageFileContent.mimeType )) } } else { @@ -1169,9 +1171,8 @@ class RoomDetailViewModel @AssistedInject constructor( // We can now open the file session.fileService().getTemporarySharableURI(action.messageFileContent)?.let { uri -> _viewEvents.post(RoomDetailViewEvents.OpenFile( - action.messageFileContent.mimeType, uri, - null + action.messageFileContent.mimeType )) } }