Skip to content

Instantly share code, notes, and snippets.

@Skeptick
Last active July 24, 2025 15:08
Show Gist options
  • Save Skeptick/1016eba0f81a3d57ecb2b3917be75abd to your computer and use it in GitHub Desktop.
Save Skeptick/1016eba0f81a3d57ecb2b3917be75abd to your computer and use it in GitHub Desktop.
App Installation througt PackageInstaller
import android.app.PendingIntent
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.IntentSender
import android.content.pm.PackageInstaller
import android.os.Build
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.platform.LocalContext
import androidx.core.content.ContextCompat
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File
import java.io.FileInputStream
private const val ActionInstallCommit = "INSTALL_COMMIT"
private val IntentFlags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
PendingIntent.FLAG_ALLOW_UNSAFE_IMPLICIT_INTENT or PendingIntent.FLAG_MUTABLE
} else {
PendingIntent.FLAG_MUTABLE
}
@Composable
actual fun rememberAppInstallLauncher(onResult: (AppInstallationResult) -> Unit): (uri: String) -> Unit {
val context = LocalContext.current
val coroutineScope = rememberCoroutineScope()
val actualOnResult by rememberUpdatedState(onResult)
val installerResultReceiver = remember { InstallerResultReceiver(actualOnResult) }
DisposableEffect(context) {
context.registerInstallerResultReceiver(installerResultReceiver)
onDispose {
context.unregisterReceiver(installerResultReceiver)
}
}
return remember {
{ uri ->
coroutineScope.launch {
val file = File(uri)
val packageInstaller = context.packageManager.packageInstaller
val params = PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL)
val sessionId = packageInstaller.createSession(params)
val session = packageInstaller.openSession(sessionId, file)
val intentSender = context.createInstallerResultIntentSender(sessionId)
session.commit(intentSender)
session.close()
}
}
}
}
private suspend fun PackageInstaller.openSession(id: Int, file: File): PackageInstaller.Session =
withContext(Dispatchers.IO) {
openSession(id).apply {
FileInputStream(file).use { inputStream ->
openWrite("package.apk", 0, file.length()).use { outputStream ->
inputStream.copyTo(outputStream)
fsync(outputStream)
}
}
}
}
private fun Context.registerInstallerResultReceiver(receiver: BroadcastReceiver) {
ContextCompat.registerReceiver(
/* context = */ this,
/* receiver = */ receiver,
/* filter = */ IntentFilter(ActionInstallCommit),
/* flags = */ ContextCompat.RECEIVER_EXPORTED
)
}
private fun Context.createInstallerResultIntentSender(sessionId: Int): IntentSender {
val pendingIntent = PendingIntent.getBroadcast(
/* context = */ this,
/* requestCode = */ sessionId,
/* intent = */ Intent(ActionInstallCommit),
/* flags = */ IntentFlags
)
return pendingIntent.intentSender
}
private class InstallerResultReceiver(private val onResult: (AppInstallationResult) -> Unit) : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (intent.action != ActionInstallCommit) return
when (intent.getIntExtra(PackageInstaller.EXTRA_STATUS, -999)) {
PackageInstaller.STATUS_PENDING_USER_ACTION -> when (context.tryShowInstallDialog(intent)) {
true -> onResult(AppInstallationResult.Awaiting)
false -> onResult(AppInstallationResult.Failure)
}
PackageInstaller.STATUS_SUCCESS -> onResult(AppInstallationResult.Success)
PackageInstaller.STATUS_FAILURE -> onResult(AppInstallationResult.Failure)
PackageInstaller.STATUS_FAILURE_BLOCKED -> onResult(AppInstallationResult.Blocked)
PackageInstaller.STATUS_FAILURE_ABORTED -> onResult(AppInstallationResult.Cancelled)
PackageInstaller.STATUS_FAILURE_INVALID -> onResult(AppInstallationResult.Invalid)
PackageInstaller.STATUS_FAILURE_CONFLICT -> onResult(AppInstallationResult.Conflict)
PackageInstaller.STATUS_FAILURE_STORAGE -> onResult(AppInstallationResult.NoStorage)
PackageInstaller.STATUS_FAILURE_INCOMPATIBLE -> onResult(AppInstallationResult.Incompatible)
PackageInstaller.STATUS_FAILURE_TIMEOUT -> onResult(AppInstallationResult.Timeout)
else -> onResult(AppInstallationResult.Failure)
}
}
private fun Context.tryShowInstallDialog(intent: Intent): Boolean {
val confirmationIntent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
intent.getParcelableExtra(Intent.EXTRA_INTENT, Intent::class.java)
} else {
@Suppress("DEPRECATION")
intent.getParcelableExtra(Intent.EXTRA_INTENT)
}
confirmationIntent?.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
return runCatching { startActivity(confirmationIntent) }.isSuccess
}
}
enum class AppInstallationResult {
Awaiting,
Success,
Failure,
Blocked,
Cancelled,
Invalid,
Conflict,
NoStorage,
Incompatible,
Timeout
}
@Composable
expect fun rememberAppInstallLauncher(onResult: (AppInstallationResult) -> Unit): (uri: String) -> Unit
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment