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
))
}
}