Add ability to install APK from directly from Element (#2381)

And cleanup `data class OpenFile`
This commit is contained in:
Benoit Marty 2021-04-28 15:55:21 +02:00
parent 195bc8e914
commit c40476aa94
7 changed files with 70 additions and 19 deletions

View file

@ -5,7 +5,7 @@ Features ✨:
-
Improvements 🙌:
-
- Add ability to install APK from directly from Element (#2381)
Bugfix 🐛:
- Message states cosmetic changes (#3007)

View file

@ -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/*"

View file

@ -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"

View file

@ -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?,

View file

@ -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())

View file

@ -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()

View file

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