Skip to content

Instantly share code, notes, and snippets.

@iyotetsuya
Created March 17, 2023 00:54
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save iyotetsuya/8cbcf1d9eead49454e15a3dc22297425 to your computer and use it in GitHub Desktop.
Save iyotetsuya/8cbcf1d9eead49454e15a3dc22297425 to your computer and use it in GitHub Desktop.
Android 14 API Sample: PackageInstaller.Session.requestUserPreapproval
package com.iyotetsuya.android14playgrounds
import android.app.Application
import android.app.PendingIntent
import android.content.Intent
import android.content.IntentSender
import android.content.pm.PackageInstaller
import android.graphics.BitmapFactory
import android.icu.util.ULocale
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.IOException
class InstallViewModel(private val application: Application) : AndroidViewModel(application) {
private lateinit var session: PackageInstaller.Session
private lateinit var statusReceiver: IntentSender
data class ViewState(val message: String?)
private val _viewState = MutableStateFlow(ViewState(null))
val viewState = _viewState.asStateFlow()
@RequiresApi(34)
fun installApp(askFirst: Boolean) {
try {
val packageManager = getApplication<Application>().packageManager
val context = getApplication<Application>().applicationContext
val resources = getApplication<Application>().resources
val packageInstaller = packageManager.packageInstaller
val params = PackageInstaller.SessionParams(
PackageInstaller.SessionParams.MODE_FULL_INSTALL
)
val sessionId = packageInstaller.createSession(params)
session = packageInstaller.openSession(sessionId)
val intent = Intent(context, PackageInstallerActivity::class.java)
intent.action = PACKAGE_INSTALLED_ACTION
val pendingIntent =
PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_MUTABLE)
statusReceiver = pendingIntent.intentSender
if (askFirst) {
val bitmap =
BitmapFactory.decodeResource(resources, R.drawable.ic_launcher_foreground)
val detail = PackageInstaller
.PreapprovalDetails.Builder()
.setLocale(ULocale.JAPAN)
.setIcon(bitmap)
.setPackageName(PACKAGE_NAME)
.setLabel("Copilot")
.build()
session.requestUserPreapproval(detail, statusReceiver)
} else {
downloadApp()
}
} catch (e: IOException) {
throw java.lang.RuntimeException("Couldn't install package", e)
} catch (e: java.lang.RuntimeException) {
session.abandon()
throw e
}
}
fun downloadApp() {
_viewState.value = _viewState.value.copy(message = "App Downloading")
viewModelScope.launch(Dispatchers.IO) {
addApkToInstallSession(session)
session.commit(statusReceiver)
withContext(Dispatchers.Main) {
_viewState.value = _viewState.value.copy(message = "App Downloaded.")
}
}
}
@Throws(IOException::class)
private fun addApkToInstallSession(session: PackageInstaller.Session) {
Log.v(TAG, "addApkToInstallSession start")
session.openWrite("package", 0, -1).use { packageInSession ->
application.assets.open(FILE_NAME).use { `is` ->
val buffer = ByteArray(10)
var n: Int
while (`is`.read(buffer).also { n = it } >= 0) {
packageInSession.write(buffer, 0, n)
}
}
Log.v(TAG, "addApkToInstallSession done")
}
}
companion object {
const val PACKAGE_INSTALLED_ACTION =
"com.iyotetsuya.android14playgrounds.SESSION_API_PACKAGE_INSTALLED"
private val TAG = InstallViewModel::class.java.simpleName
private const val FILE_NAME = "app-debug.apk"
private const val PACKAGE_NAME = "com.example.copilot"
}
}
package com.iyotetsuya.android14playgrounds
import android.content.Intent
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
import android.content.pm.PackageInstaller
import android.os.Bundle
import android.util.Log
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.annotation.RequiresApi
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.lifecycleScope
import com.iyotetsuya.android14playgrounds.ui.theme.Android14PlaygroundsTheme
import kotlinx.coroutines.launch
@RequiresApi(34)
class PackageInstallerActivity : ComponentActivity() {
private val viewModel: InstallViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
Android14PlaygroundsTheme {
Box(Modifier.fillMaxSize()) {
Text(
modifier = Modifier
.padding(4.dp)
.align(Alignment.TopStart),
text = "Package Installer Activity"
)
Column(
Modifier.align(Alignment.Center)
) {
Button(onClick = { viewModel.installApp(false) }) {
Text(text = "Download -> ASK -> Install")
}
Button(onClick = { viewModel.installApp(true) }) {
Text(text = "Ask -> Download -> Install")
}
Button(onClick = { openApp(packageName) }) {
Text(text = "Open App")
}
}
}
}
}
lifecycleScope.launch {
viewModel.viewState.collect {
it.message?.let { message ->
showToast(message)
}
}
}
}
private fun openApp(packageName: String) {
val intent = Intent(Intent.ACTION_MAIN)
intent.addCategory(Intent.CATEGORY_LAUNCHER)
intent.setPackage(packageName)
intent.addFlags(FLAG_ACTIVITY_NEW_TASK)
intent.setClassName(packageName, "$packageName.MainActivity")
try {
startActivity(intent)
} catch (e: Exception) {
e.printStackTrace()
}
}
override fun onNewIntent(intent: Intent) {
Log.v(TAG, "intent: $intent")
val extrasBundle = intent.extras
Log.v(TAG, "extrasBundle: $extrasBundle")
val extraIntent = intent.getParcelableExtra(Intent.EXTRA_INTENT, Intent::class.java)
Log.v(TAG, "extraIntent: $extraIntent")
if (InstallViewModel.PACKAGE_INSTALLED_ACTION == intent.action) {
val status = extrasBundle?.getInt(PackageInstaller.EXTRA_STATUS) ?: -1
val message = extrasBundle?.getString(PackageInstaller.EXTRA_STATUS_MESSAGE) ?: ""
val isPreApproval =
extrasBundle?.getBoolean(PackageInstaller.EXTRA_PRE_APPROVAL) ?: false
when (status) {
PackageInstaller.STATUS_PENDING_USER_ACTION -> {
extraIntent?.addFlags(FLAG_ACTIVITY_NEW_TASK)
startActivity(extraIntent)
Log.v(TAG, "STATUS_PENDING_USER_ACTION")
}
PackageInstaller.STATUS_SUCCESS -> {
Log.v(TAG, "STATUS_SUCCESS")
if (isPreApproval) {
viewModel.downloadApp()
} else {
showToast("App Installed.")
}
}
PackageInstaller.STATUS_FAILURE,
PackageInstaller.STATUS_FAILURE_ABORTED,
PackageInstaller.STATUS_FAILURE_BLOCKED,
PackageInstaller.STATUS_FAILURE_CONFLICT,
PackageInstaller.STATUS_FAILURE_INCOMPATIBLE,
PackageInstaller.STATUS_FAILURE_INVALID,
PackageInstaller.STATUS_FAILURE_STORAGE -> {
showToast("Install failed! $status, $message")
Log.v(TAG, "Install failed! $status, $message")
}
else -> {
showToast("Unrecognized status received from installer: $status")
Log.v(TAG, "Unrecognized status received from installer: $status")
}
}
}
}
private fun showToast(message: String) {
Toast.makeText(
this@PackageInstallerActivity,
message,
Toast.LENGTH_SHORT
).show()
}
companion object {
private val TAG = PackageInstallerActivity::class.java.simpleName
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment