-
-
Save iyotetsuya/8cbcf1d9eead49454e15a3dc22297425 to your computer and use it in GitHub Desktop.
Android 14 API Sample: PackageInstaller.Session.requestUserPreapproval
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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