Merge pull request #1432 from vector-im/feature/bma/installApk
Install apk from the app - REQUEST_INSTALL_PACKAGES
This commit is contained in:
commit
f607b557ff
9 changed files with 99 additions and 22 deletions
|
|
@ -16,6 +16,7 @@
|
|||
|
||||
package io.element.android.features.messages.impl.media.local
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.ContentResolver
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
|
|
@ -24,17 +25,25 @@ import android.net.Uri
|
|||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.provider.MediaStore
|
||||
import androidx.activity.compose.ManagedActivityResultLauncher
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.ActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.core.net.toFile
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.libraries.androidutils.system.startInstallFromSourceIntent
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
import io.element.android.libraries.core.meta.BuildMeta
|
||||
import io.element.android.libraries.core.mimetype.MimeTypes
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import io.element.android.libraries.di.ApplicationContext
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
|
|
@ -50,10 +59,27 @@ class AndroidLocalMediaActions @Inject constructor(
|
|||
) : LocalMediaActions {
|
||||
|
||||
private var activityContext: Context? = null
|
||||
private var apkInstallLauncher: ManagedActivityResultLauncher<Intent, ActivityResult>? = null
|
||||
private var pendingMedia: LocalMedia? = null
|
||||
|
||||
@Composable
|
||||
override fun Configure() {
|
||||
val context = LocalContext.current
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
apkInstallLauncher = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.StartActivityForResult(),
|
||||
) { activityResult ->
|
||||
if (activityResult.resultCode == Activity.RESULT_OK) {
|
||||
pendingMedia?.let {
|
||||
coroutineScope.launch {
|
||||
openFile(it)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// User cancelled
|
||||
}
|
||||
pendingMedia = null
|
||||
}
|
||||
return DisposableEffect(Unit) {
|
||||
activityContext = context
|
||||
onDispose {
|
||||
|
|
@ -99,11 +125,20 @@ class AndroidLocalMediaActions @Inject constructor(
|
|||
override suspend fun open(localMedia: LocalMedia): Result<Unit> = withContext(coroutineDispatchers.io) {
|
||||
require(localMedia.uri.scheme == ContentResolver.SCHEME_FILE)
|
||||
runCatching {
|
||||
val openMediaIntent = Intent(Intent.ACTION_VIEW)
|
||||
.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
.setDataAndType(localMedia.toShareableUri(), localMedia.info.mimeType)
|
||||
withContext(coroutineDispatchers.main) {
|
||||
activityContext!!.startActivity(openMediaIntent)
|
||||
when (localMedia.info.mimeType) {
|
||||
MimeTypes.Apk -> {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
if (activityContext?.packageManager?.canRequestPackageInstalls() == false) {
|
||||
pendingMedia = localMedia
|
||||
activityContext?.startInstallFromSourceIntent(apkInstallLauncher!!).let { }
|
||||
} else {
|
||||
openFile(localMedia)
|
||||
}
|
||||
} else {
|
||||
openFile(localMedia)
|
||||
}
|
||||
}
|
||||
else -> openFile(localMedia)
|
||||
}
|
||||
}.onSuccess {
|
||||
Timber.v("Open media succeed")
|
||||
|
|
@ -112,6 +147,15 @@ class AndroidLocalMediaActions @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private suspend fun openFile(localMedia: LocalMedia) {
|
||||
val openMediaIntent = Intent(Intent.ACTION_VIEW)
|
||||
.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
.setDataAndType(localMedia.toShareableUri(), localMedia.info.mimeType)
|
||||
withContext(coroutineDispatchers.main) {
|
||||
activityContext?.startActivity(openMediaIntent)
|
||||
}
|
||||
}
|
||||
|
||||
private fun LocalMedia.toShareableUri(): Uri {
|
||||
val mediaAsFile = this.toFile()
|
||||
val authority = "${buildMeta.applicationId}.fileprovider"
|
||||
|
|
|
|||
|
|
@ -47,11 +47,13 @@ import androidx.compose.ui.tooling.preview.Preview
|
|||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import coil.compose.AsyncImage
|
||||
import io.element.android.features.messages.impl.R
|
||||
import io.element.android.features.messages.impl.media.local.LocalMedia
|
||||
import io.element.android.features.messages.impl.media.local.LocalMediaView
|
||||
import io.element.android.features.messages.impl.media.local.MediaInfo
|
||||
import io.element.android.features.messages.impl.media.local.rememberLocalMediaViewState
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.core.mimetype.MimeTypes
|
||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||
import io.element.android.libraries.designsystem.components.dialogs.RetryDialog
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||
|
|
@ -92,6 +94,7 @@ fun MediaViewerView(
|
|||
topBar = {
|
||||
MediaViewerTopBar(
|
||||
actionsEnabled = state.downloadedMedia is Async.Success,
|
||||
mimeType = state.mediaInfo.mimeType,
|
||||
onBackPressed = onBackPressed,
|
||||
eventSink = state.eventSink
|
||||
)
|
||||
|
|
@ -162,6 +165,7 @@ private fun rememberShowProgress(downloadedMedia: Async<LocalMedia>): Boolean {
|
|||
@Composable
|
||||
private fun MediaViewerTopBar(
|
||||
actionsEnabled: Boolean,
|
||||
mimeType: String,
|
||||
onBackPressed: () -> Unit,
|
||||
eventSink: (MediaViewerEvents) -> Unit,
|
||||
) {
|
||||
|
|
@ -175,10 +179,16 @@ private fun MediaViewerTopBar(
|
|||
eventSink(MediaViewerEvents.OpenWith)
|
||||
},
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.OpenInNew,
|
||||
contentDescription = stringResource(id = CommonStrings.action_open_with)
|
||||
)
|
||||
when (mimeType) {
|
||||
MimeTypes.Apk -> Icon(
|
||||
resourceId = R.drawable.ic_apk_install,
|
||||
contentDescription = stringResource(id = CommonStrings.common_install_apk_android)
|
||||
)
|
||||
else -> Icon(
|
||||
imageVector = Icons.Default.OpenInNew,
|
||||
contentDescription = stringResource(id = CommonStrings.action_open_with)
|
||||
)
|
||||
}
|
||||
}
|
||||
IconButton(
|
||||
enabled = actionsEnabled,
|
||||
|
|
|
|||
|
|
@ -109,14 +109,17 @@ class TimelineItemContentMessageFactory @Inject constructor(
|
|||
formattedFileSize = fileSizeFormatter.format(messageType.info?.size ?: 0),
|
||||
fileExtension = fileExtensionExtractor.extractFromName(messageType.body)
|
||||
)
|
||||
is FileMessageType -> TimelineItemFileContent(
|
||||
body = messageType.body,
|
||||
thumbnailSource = messageType.info?.thumbnailSource,
|
||||
fileSource = messageType.source,
|
||||
mimeType = messageType.info?.mimetype ?: MimeTypes.OctetStream,
|
||||
formattedFileSize = fileSizeFormatter.format(messageType.info?.size ?: 0),
|
||||
fileExtension = fileExtensionExtractor.extractFromName(messageType.body)
|
||||
)
|
||||
is FileMessageType -> {
|
||||
val fileExtension = fileExtensionExtractor.extractFromName(messageType.body)
|
||||
TimelineItemFileContent(
|
||||
body = messageType.body,
|
||||
thumbnailSource = messageType.info?.thumbnailSource,
|
||||
fileSource = messageType.source,
|
||||
mimeType = messageType.info?.mimetype ?: MimeTypes.fromFileExtension(fileExtension),
|
||||
formattedFileSize = fileSizeFormatter.format(messageType.info?.size ?: 0),
|
||||
fileExtension = fileExtension
|
||||
)
|
||||
}
|
||||
is NoticeMessageType -> TimelineItemNoticeContent(
|
||||
body = messageType.body,
|
||||
htmlDocument = messageType.formatted?.toHtmlDocument(),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M160,880Q127,880 103.5,856.5Q80,833 80,800L80,160Q80,127 103.5,103.5Q127,80 160,80L480,80L720,320L720,490L640,490L640,360L440,360L440,160L160,160Q160,160 160,160Q160,160 160,160L160,800Q160,800 160,800Q160,800 160,800L600,800L600,880L160,880ZM160,800L160,490L160,490L160,360L160,160L160,160Q160,160 160,160Q160,160 160,160L160,800Q160,800 160,800Q160,800 160,800L160,800ZM200,760Q204,711 230,670Q256,629 298,605L260,537Q260,536 264,522Q269,520 273.5,520Q278,520 280,525L319,595Q339,587 359,582.5Q379,578 400,578Q421,578 441,582.5Q461,587 481,595L520,525Q520,525 535,521Q540,523 541,528Q542,533 540,537L502,605Q544,629 570,670Q596,711 600,760L200,760ZM310,700Q318,700 324,694Q330,688 330,680Q330,672 324,666Q318,660 310,660Q302,660 296,666Q290,672 290,680Q290,688 296,694Q302,700 310,700ZM490,700Q498,700 504,694Q510,688 510,680Q510,672 504,666Q498,660 490,660Q482,660 476,666Q470,672 470,680Q470,688 476,694Q482,700 490,700ZM800,880L640,720L696,663L760,726L760,560L840,560L840,726L904,663L960,720L800,880Z"/>
|
||||
</vector>
|
||||
|
|
@ -51,4 +51,12 @@ object MimeTypes {
|
|||
fun String?.isMimeTypeFile() = this?.startsWith("file/").orFalse()
|
||||
fun String?.isMimeTypeText() = this?.startsWith("text/").orFalse()
|
||||
fun String?.isMimeTypeAny() = this?.startsWith("*/").orFalse()
|
||||
|
||||
fun fromFileExtension(fileExtension: String): String {
|
||||
return when (fileExtension.lowercase()) {
|
||||
"apk" -> Apk
|
||||
"pdf" -> Pdf
|
||||
else -> OctetStream
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@
|
|||
package io.element.android.libraries.matrix.impl.media
|
||||
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
import io.element.android.libraries.core.mimetype.MimeTypes
|
||||
import io.element.android.libraries.matrix.api.media.MatrixMediaLoader
|
||||
import io.element.android.libraries.matrix.api.media.MediaFile
|
||||
import io.element.android.libraries.matrix.api.media.MediaSource
|
||||
|
|
@ -77,7 +78,7 @@ class RustMediaLoader(
|
|||
val mediaFile = innerClient.getMediaFile(
|
||||
mediaSource = mediaSource,
|
||||
body = body,
|
||||
mimeType = mimeType ?: "application/octet-stream",
|
||||
mimeType = mimeType ?: MimeTypes.OctetStream,
|
||||
tempDir = cacheDirectory.path,
|
||||
)
|
||||
RustMediaFile(mediaFile)
|
||||
|
|
|
|||
|
|
@ -94,6 +94,7 @@
|
|||
<string name="common_gif">"GIF"</string>
|
||||
<string name="common_image">"Image"</string>
|
||||
<string name="common_in_reply_to">"In reply to %1$s"</string>
|
||||
<string name="common_install_apk_android">"Install APK"</string>
|
||||
<string name="common_invite_unknown_profile">"This Matrix ID can\'t be found, so the invite might not be received."</string>
|
||||
<string name="common_leaving_room">"Leaving room"</string>
|
||||
<string name="common_link_copied_to_clipboard">"Link copied to clipboard"</string>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:15ec8227e84c01b8e9acd5271e379d989c8e4ed72b57c87d252a992a0d3f1a1a
|
||||
size 15702
|
||||
oid sha256:bd2a831abc63de6366f2a4fbac653d551e29fbbbb698f5dc3ae51de04dbf6138
|
||||
size 15941
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:08c11b272ebe9b4d8f9b76841b60a519d08a9e5f37970c66b78bd36319bcd53f
|
||||
size 15782
|
||||
oid sha256:6b7dea2e1df15375ae07db4e6cf7b2a14a26c0ff1f2cbb11efdaea263d954550
|
||||
size 16067
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue