Rewrite DefaultInstaller to use PackageInstaller

This commit is contained in:
Matthew Crossman 2021-11-24 10:38:29 +11:00 committed by machiav3lli
parent 7bd4785d80
commit 87f19ddc21
3 changed files with 166 additions and 28 deletions

View File

@ -9,6 +9,7 @@
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
<uses-permission android:name="android.permission.UPDATE_PACKAGES_WITHOUT_USER_ACTION" />
<application
android:name="com.looker.droidify.MainApplication"
@ -89,6 +90,10 @@
<receiver android:name="com.looker.droidify.service.DownloadService$Receiver" />
<service
android:name="com.looker.droidify.installer.InstallerService"
android:exported="false" />
<provider
android:name="com.looker.droidify.content.Cache$Provider"
android:authorities="${applicationId}.provider.cache"

View File

@ -1,8 +1,9 @@
package com.looker.droidify.installer
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.content.pm.PackageInstaller.SessionParams
import com.looker.droidify.content.Cache
import com.looker.droidify.utility.extension.android.Android
import kotlinx.coroutines.Dispatchers
@ -25,38 +26,48 @@ class DefaultInstaller(context: Context) : BaseInstaller(context) {
override suspend fun uninstall(packageName: String) = mDefaultUninstaller(packageName)
private suspend fun mDefaultInstaller(cacheFile: File) {
val (uri, flags) = if (Android.sdk(24)) {
Pair(
Cache.getReleaseUri(context, cacheFile.name),
Intent.FLAG_GRANT_READ_URI_PERMISSION
)
} else {
Pair(Uri.fromFile(cacheFile), 0)
private fun mDefaultInstaller(cacheFile: File) {
val sessionInstaller = context.packageManager.packageInstaller
val sessionParams =
SessionParams(SessionParams.MODE_FULL_INSTALL)
if (Android.sdk(31)) {
sessionParams.setRequireUserAction(SessionParams.USER_ACTION_NOT_REQUIRED)
}
// TODO Handle deprecation
@Suppress("DEPRECATION")
withContext(Dispatchers.IO) {
context.startActivity(
Intent(Intent.ACTION_INSTALL_PACKAGE)
.setDataAndType(uri, "application/vnd.android.package-archive")
.setFlags(flags)
)
val id = sessionInstaller.createSession(sessionParams)
val session = sessionInstaller.openSession(id)
session.use { activeSession ->
activeSession.openWrite("package", 0, cacheFile.length()).use { packageStream ->
cacheFile.inputStream().use { fileStream ->
fileStream.copyTo(packageStream)
}
}
val intent = Intent(context, InstallerService::class.java)
val flags = if (Android.sdk(31)) PendingIntent.FLAG_MUTABLE else 0
val pendingIntent = PendingIntent.getService(context, id, intent, flags)
session.commit(pendingIntent.intentSender)
}
}
private suspend fun mDefaultUninstaller(packageName: String) {
val uri = Uri.fromParts("package", packageName, null)
val intent = Intent()
intent.data = uri
@Suppress("DEPRECATION")
if (Android.sdk(28)) {
intent.action = Intent.ACTION_DELETE
} else {
intent.action = Intent.ACTION_UNINSTALL_PACKAGE
intent.putExtra(Intent.EXTRA_RETURN_RESULT, true)
}
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
withContext(Dispatchers.IO) { context.startActivity(intent) }
val sessionInstaller = context.packageManager.packageInstaller
val intent = Intent(context, InstallerService::class.java)
intent.putExtra(InstallerService.KEY_ACTION, InstallerService.ACTION_UNINSTALL)
val flags = if (Android.sdk(31)) PendingIntent.FLAG_MUTABLE else 0
val pendingIntent = PendingIntent.getService(context, -1, intent, flags)
withContext(Dispatchers.IO) {
sessionInstaller.uninstall(packageName, pendingIntent.intentSender)
}
}
}

View File

@ -0,0 +1,122 @@
package com.looker.droidify.installer
import android.app.Service
import android.content.Intent
import android.content.pm.PackageInstaller
import android.content.pm.PackageManager
import android.os.IBinder
import android.view.ContextThemeWrapper
import androidx.core.app.NotificationCompat
import com.looker.droidify.Common
import com.looker.droidify.R
import com.looker.droidify.utility.extension.android.notificationManager
import com.looker.droidify.utility.extension.resources.getColorFromAttr
/**
* Runs during or after a PackageInstaller session in order to handle completion, failure, or
* interruptions requiring user intervention (e.g. "Install Unknown Apps" permission requests).
*/
class InstallerService : Service() {
companion object {
const val KEY_ACTION = "installerAction"
const val ACTION_UNINSTALL = "uninstall"
}
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
val status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, -1)
if (status == PackageInstaller.STATUS_PENDING_USER_ACTION) {
// prompts user to enable unknown source
val promptIntent: Intent? = intent.getParcelableExtra(Intent.EXTRA_INTENT)
promptIntent?.let {
it.putExtra(Intent.EXTRA_NOT_UNKNOWN_SOURCE, true)
it.putExtra(Intent.EXTRA_INSTALLER_PACKAGE_NAME, "com.android.vending")
it.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
startActivity(it)
}
} else {
notifyStatus(intent)
}
stopSelf()
return START_NOT_STICKY
}
/**
* Notifies user of installer outcome.
*/
private fun notifyStatus(intent: Intent) {
// unpack from intent
val status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, -1)
val name = intent.getStringExtra(PackageInstaller.EXTRA_PACKAGE_NAME)
val message = intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE)
val installerAction = intent.getStringExtra(KEY_ACTION)
// get application name for notifications
val appLabel = try {
if (name != null) packageManager.getApplicationLabel(
packageManager.getApplicationInfo(
name,
PackageManager.GET_META_DATA
)
) else null
} catch (_: Exception) {
null
}
val notificationTag = "download-$name"
// start building
val builder = NotificationCompat
.Builder(this, Common.NOTIFICATION_CHANNEL_DOWNLOADING)
.setAutoCancel(true)
.setColor(
ContextThemeWrapper(this, R.style.Theme_Main_Light)
.getColorFromAttr(android.R.attr.colorAccent).defaultColor
)
when (status) {
PackageInstaller.STATUS_SUCCESS -> {
if (installerAction == ACTION_UNINSTALL)
// remove any notification for this app
notificationManager.cancel(notificationTag, Common.NOTIFICATION_ID_DOWNLOADING)
else {
val notification = builder
.setSmallIcon(android.R.drawable.stat_sys_download_done)
.setContentTitle(getString(R.string.installed))
.setContentText(appLabel)
.build()
notificationManager.notify(
notificationTag,
Common.NOTIFICATION_ID_DOWNLOADING,
notification
)
}
}
PackageInstaller.STATUS_FAILURE_ABORTED -> {
// do nothing if user cancels
}
else -> {
// problem occurred when installing/uninstalling package
val notification = builder
.setSmallIcon(android.R.drawable.stat_notify_error)
.setContentTitle(getString(R.string.unknown_error_DESC))
.setContentText(message)
.build()
notificationManager.notify(
notificationTag,
Common.NOTIFICATION_ID_DOWNLOADING,
notification
)
}
}
}
override fun onBind(intent: Intent?): IBinder? {
return null
}
}