From 87f19ddc21b5e974e460ff992d76c524244f8307 Mon Sep 17 00:00:00 2001 From: Matthew Crossman Date: Wed, 24 Nov 2021 10:38:29 +1100 Subject: [PATCH] Rewrite DefaultInstaller to use PackageInstaller --- src/main/AndroidManifest.xml | 5 + .../droidify/installer/DefaultInstaller.kt | 67 ++++++---- .../droidify/installer/InstallerService.kt | 122 ++++++++++++++++++ 3 files changed, 166 insertions(+), 28 deletions(-) create mode 100644 src/main/kotlin/com/looker/droidify/installer/InstallerService.kt diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml index ee35783a..61249d74 100644 --- a/src/main/AndroidManifest.xml +++ b/src/main/AndroidManifest.xml @@ -9,6 +9,7 @@ + + + + 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) + 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) } - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - withContext(Dispatchers.IO) { context.startActivity(intent) } } } \ No newline at end of file diff --git a/src/main/kotlin/com/looker/droidify/installer/InstallerService.kt b/src/main/kotlin/com/looker/droidify/installer/InstallerService.kt new file mode 100644 index 00000000..88f48cb5 --- /dev/null +++ b/src/main/kotlin/com/looker/droidify/installer/InstallerService.kt @@ -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 + } + + +} \ No newline at end of file