Skip to content

Commit f607b55

Browse files
authored
Merge pull request #1432 from vector-im/feature/bma/installApk
Install apk from the app - REQUEST_INSTALL_PACKAGES
2 parents 6e422cc + 3eb1123 commit f607b55

File tree

9 files changed

+99
-22
lines changed

9 files changed

+99
-22
lines changed

features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/AndroidLocalMediaActions.kt

Lines changed: 49 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
package io.element.android.features.messages.impl.media.local
1818

19+
import android.app.Activity
1920
import android.content.ContentResolver
2021
import android.content.ContentValues
2122
import android.content.Context
@@ -24,17 +25,25 @@ import android.net.Uri
2425
import android.os.Build
2526
import android.os.Environment
2627
import android.provider.MediaStore
28+
import androidx.activity.compose.ManagedActivityResultLauncher
29+
import androidx.activity.compose.rememberLauncherForActivityResult
30+
import androidx.activity.result.ActivityResult
31+
import androidx.activity.result.contract.ActivityResultContracts
2732
import androidx.annotation.RequiresApi
2833
import androidx.compose.runtime.Composable
2934
import androidx.compose.runtime.DisposableEffect
35+
import androidx.compose.runtime.rememberCoroutineScope
3036
import androidx.compose.ui.platform.LocalContext
3137
import androidx.core.content.FileProvider
3238
import androidx.core.net.toFile
3339
import com.squareup.anvil.annotations.ContributesBinding
40+
import io.element.android.libraries.androidutils.system.startInstallFromSourceIntent
3441
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
3542
import io.element.android.libraries.core.meta.BuildMeta
43+
import io.element.android.libraries.core.mimetype.MimeTypes
3644
import io.element.android.libraries.di.AppScope
3745
import io.element.android.libraries.di.ApplicationContext
46+
import kotlinx.coroutines.launch
3847
import kotlinx.coroutines.withContext
3948
import timber.log.Timber
4049
import java.io.File
@@ -50,10 +59,27 @@ class AndroidLocalMediaActions @Inject constructor(
5059
) : LocalMediaActions {
5160

5261
private var activityContext: Context? = null
62+
private var apkInstallLauncher: ManagedActivityResultLauncher<Intent, ActivityResult>? = null
63+
private var pendingMedia: LocalMedia? = null
5364

5465
@Composable
5566
override fun Configure() {
5667
val context = LocalContext.current
68+
val coroutineScope = rememberCoroutineScope()
69+
apkInstallLauncher = rememberLauncherForActivityResult(
70+
contract = ActivityResultContracts.StartActivityForResult(),
71+
) { activityResult ->
72+
if (activityResult.resultCode == Activity.RESULT_OK) {
73+
pendingMedia?.let {
74+
coroutineScope.launch {
75+
openFile(it)
76+
}
77+
}
78+
} else {
79+
// User cancelled
80+
}
81+
pendingMedia = null
82+
}
5783
return DisposableEffect(Unit) {
5884
activityContext = context
5985
onDispose {
@@ -99,11 +125,20 @@ class AndroidLocalMediaActions @Inject constructor(
99125
override suspend fun open(localMedia: LocalMedia): Result<Unit> = withContext(coroutineDispatchers.io) {
100126
require(localMedia.uri.scheme == ContentResolver.SCHEME_FILE)
101127
runCatching {
102-
val openMediaIntent = Intent(Intent.ACTION_VIEW)
103-
.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
104-
.setDataAndType(localMedia.toShareableUri(), localMedia.info.mimeType)
105-
withContext(coroutineDispatchers.main) {
106-
activityContext!!.startActivity(openMediaIntent)
128+
when (localMedia.info.mimeType) {
129+
MimeTypes.Apk -> {
130+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
131+
if (activityContext?.packageManager?.canRequestPackageInstalls() == false) {
132+
pendingMedia = localMedia
133+
activityContext?.startInstallFromSourceIntent(apkInstallLauncher!!).let { }
134+
} else {
135+
openFile(localMedia)
136+
}
137+
} else {
138+
openFile(localMedia)
139+
}
140+
}
141+
else -> openFile(localMedia)
107142
}
108143
}.onSuccess {
109144
Timber.v("Open media succeed")
@@ -112,6 +147,15 @@ class AndroidLocalMediaActions @Inject constructor(
112147
}
113148
}
114149

150+
private suspend fun openFile(localMedia: LocalMedia) {
151+
val openMediaIntent = Intent(Intent.ACTION_VIEW)
152+
.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
153+
.setDataAndType(localMedia.toShareableUri(), localMedia.info.mimeType)
154+
withContext(coroutineDispatchers.main) {
155+
activityContext?.startActivity(openMediaIntent)
156+
}
157+
}
158+
115159
private fun LocalMedia.toShareableUri(): Uri {
116160
val mediaAsFile = this.toFile()
117161
val authority = "${buildMeta.applicationId}.fileprovider"

features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerView.kt

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,11 +47,13 @@ import androidx.compose.ui.tooling.preview.Preview
4747
import androidx.compose.ui.tooling.preview.PreviewParameter
4848
import androidx.compose.ui.unit.dp
4949
import coil.compose.AsyncImage
50+
import io.element.android.features.messages.impl.R
5051
import io.element.android.features.messages.impl.media.local.LocalMedia
5152
import io.element.android.features.messages.impl.media.local.LocalMediaView
5253
import io.element.android.features.messages.impl.media.local.MediaInfo
5354
import io.element.android.features.messages.impl.media.local.rememberLocalMediaViewState
5455
import io.element.android.libraries.architecture.Async
56+
import io.element.android.libraries.core.mimetype.MimeTypes
5557
import io.element.android.libraries.designsystem.components.button.BackButton
5658
import io.element.android.libraries.designsystem.components.dialogs.RetryDialog
5759
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
@@ -92,6 +94,7 @@ fun MediaViewerView(
9294
topBar = {
9395
MediaViewerTopBar(
9496
actionsEnabled = state.downloadedMedia is Async.Success,
97+
mimeType = state.mediaInfo.mimeType,
9598
onBackPressed = onBackPressed,
9699
eventSink = state.eventSink
97100
)
@@ -162,6 +165,7 @@ private fun rememberShowProgress(downloadedMedia: Async<LocalMedia>): Boolean {
162165
@Composable
163166
private fun MediaViewerTopBar(
164167
actionsEnabled: Boolean,
168+
mimeType: String,
165169
onBackPressed: () -> Unit,
166170
eventSink: (MediaViewerEvents) -> Unit,
167171
) {
@@ -175,10 +179,16 @@ private fun MediaViewerTopBar(
175179
eventSink(MediaViewerEvents.OpenWith)
176180
},
177181
) {
178-
Icon(
179-
imageVector = Icons.Default.OpenInNew,
180-
contentDescription = stringResource(id = CommonStrings.action_open_with)
181-
)
182+
when (mimeType) {
183+
MimeTypes.Apk -> Icon(
184+
resourceId = R.drawable.ic_apk_install,
185+
contentDescription = stringResource(id = CommonStrings.common_install_apk_android)
186+
)
187+
else -> Icon(
188+
imageVector = Icons.Default.OpenInNew,
189+
contentDescription = stringResource(id = CommonStrings.action_open_with)
190+
)
191+
}
182192
}
183193
IconButton(
184194
enabled = actionsEnabled,

features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactory.kt

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -109,14 +109,17 @@ class TimelineItemContentMessageFactory @Inject constructor(
109109
formattedFileSize = fileSizeFormatter.format(messageType.info?.size ?: 0),
110110
fileExtension = fileExtensionExtractor.extractFromName(messageType.body)
111111
)
112-
is FileMessageType -> TimelineItemFileContent(
113-
body = messageType.body,
114-
thumbnailSource = messageType.info?.thumbnailSource,
115-
fileSource = messageType.source,
116-
mimeType = messageType.info?.mimetype ?: MimeTypes.OctetStream,
117-
formattedFileSize = fileSizeFormatter.format(messageType.info?.size ?: 0),
118-
fileExtension = fileExtensionExtractor.extractFromName(messageType.body)
119-
)
112+
is FileMessageType -> {
113+
val fileExtension = fileExtensionExtractor.extractFromName(messageType.body)
114+
TimelineItemFileContent(
115+
body = messageType.body,
116+
thumbnailSource = messageType.info?.thumbnailSource,
117+
fileSource = messageType.source,
118+
mimeType = messageType.info?.mimetype ?: MimeTypes.fromFileExtension(fileExtension),
119+
formattedFileSize = fileSizeFormatter.format(messageType.info?.size ?: 0),
120+
fileExtension = fileExtension
121+
)
122+
}
120123
is NoticeMessageType -> TimelineItemNoticeContent(
121124
body = messageType.body,
122125
htmlDocument = messageType.formatted?.toHtmlDocument(),
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<vector xmlns:android="http://schemas.android.com/apk/res/android"
2+
android:width="24dp"
3+
android:height="24dp"
4+
android:viewportWidth="960"
5+
android:viewportHeight="960"
6+
android:tint="?attr/colorControlNormal">
7+
<path
8+
android:fillColor="@android:color/white"
9+
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"/>
10+
</vector>

libraries/core/src/main/kotlin/io/element/android/libraries/core/mimetype/MimeTypes.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,4 +51,12 @@ object MimeTypes {
5151
fun String?.isMimeTypeFile() = this?.startsWith("file/").orFalse()
5252
fun String?.isMimeTypeText() = this?.startsWith("text/").orFalse()
5353
fun String?.isMimeTypeAny() = this?.startsWith("*/").orFalse()
54+
55+
fun fromFileExtension(fileExtension: String): String {
56+
return when (fileExtension.lowercase()) {
57+
"apk" -> Apk
58+
"pdf" -> Pdf
59+
else -> OctetStream
60+
}
61+
}
5462
}

libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/RustMediaLoader.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package io.element.android.libraries.matrix.impl.media
1818

1919
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
20+
import io.element.android.libraries.core.mimetype.MimeTypes
2021
import io.element.android.libraries.matrix.api.media.MatrixMediaLoader
2122
import io.element.android.libraries.matrix.api.media.MediaFile
2223
import io.element.android.libraries.matrix.api.media.MediaSource
@@ -77,7 +78,7 @@ class RustMediaLoader(
7778
val mediaFile = innerClient.getMediaFile(
7879
mediaSource = mediaSource,
7980
body = body,
80-
mimeType = mimeType ?: "application/octet-stream",
81+
mimeType = mimeType ?: MimeTypes.OctetStream,
8182
tempDir = cacheDirectory.path,
8283
)
8384
RustMediaFile(mediaFile)

libraries/ui-strings/src/main/res/values/localazy.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@
9494
<string name="common_gif">"GIF"</string>
9595
<string name="common_image">"Image"</string>
9696
<string name="common_in_reply_to">"In reply to %1$s"</string>
97+
<string name="common_install_apk_android">"Install APK"</string>
9798
<string name="common_invite_unknown_profile">"This Matrix ID can\'t be found, so the invite might not be received."</string>
9899
<string name="common_leaving_room">"Leaving room"</string>
99100
<string name="common_link_copied_to_clipboard">"Link copied to clipboard"</string>
Lines changed: 2 additions & 2 deletions
Loading
Lines changed: 2 additions & 2 deletions
Loading

0 commit comments

Comments
 (0)