Improve install notifications, improve DefaultInstaller, misc. clean-up

Installer notifications have their own channel, their tags have been
fixed, and the timeout has been properly set instead of using sleep.

Ensured that DefaultInstaller's sessions use unique file names. Also
improved error handling by including broken pipes and by preventing
post-copy operations if an error has occurred.

Some cleaning up has been done in Common, DownloadService, and
SyncService. A few changes have been cherry-picked from master.
This commit is contained in:
Matthew Crossman
2022-01-04 20:26:07 +11:00
parent e1fc3c656a
commit 375ab23edb
6 changed files with 112 additions and 117 deletions

View File

@ -11,10 +11,12 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.File
import java.io.FileNotFoundException
import java.io.IOException
class DefaultInstaller(context: Context) : BaseInstaller(context) {
private val sessionInstaller = context.packageManager.packageInstaller
private val packageManager = context.packageManager
private val sessionInstaller = packageManager.packageInstaller
private val intent = Intent(context, InstallerService::class.java)
companion object {
@ -48,27 +50,48 @@ class DefaultInstaller(context: Context) : BaseInstaller(context) {
override suspend fun uninstall(packageName: String) = mDefaultUninstaller(packageName)
private fun mDefaultInstaller(cacheFile: File) {
// clean up inactive sessions
sessionInstaller.mySessions
.filter { session -> !session.isActive }
.forEach { session -> sessionInstaller.abandonSession(session.sessionId) }
// start new session
val id = sessionInstaller.createSession(sessionParams)
val session = sessionInstaller.openSession(id)
// get package name
val packageInfo = packageManager.getPackageArchiveInfo(cacheFile.absolutePath, 0)
val packageName = packageInfo?.packageName ?: "unknown-package"
// error flags
var hasErrors = false
session.use { activeSession ->
activeSession.openWrite("package", 0, cacheFile.length()).use { packageStream ->
activeSession.openWrite(packageName, 0, cacheFile.length()).use { packageStream ->
try {
cacheFile.inputStream().use { fileStream ->
fileStream.copyTo(packageStream)
}
} catch (error: FileNotFoundException) {
Log.w("DefaultInstaller", "Cache file for DefaultInstaller does not seem to exist.")
} catch (e: FileNotFoundException) {
Log.w(
"DefaultInstaller",
"Cache file for DefaultInstaller does not seem to exist."
)
hasErrors = true
} catch (e: IOException) {
Log.w(
"DefaultInstaller",
"Failed to perform cache to package copy due to a bad pipe."
)
hasErrors = true
}
}
val pendingIntent = PendingIntent.getService(context, id, intent, flags)
session.commit(pendingIntent.intentSender)
}
cacheFile.delete()
if (!hasErrors) {
session.commit(PendingIntent.getService(context, id, intent, flags).intentSender)
cacheFile.delete()
}
}
private suspend fun mDefaultUninstaller(packageName: String) {

View File

@ -1,16 +1,17 @@
package com.looker.droidify.installer
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.Service
import android.content.Intent
import android.content.pm.PackageInstaller
import android.content.pm.PackageManager
import android.net.Uri
import android.os.IBinder
import android.view.ContextThemeWrapper
import androidx.core.app.NotificationCompat
import com.looker.droidify.Common.NOTIFICATION_CHANNEL_DOWNLOADING
import com.looker.droidify.Common.NOTIFICATION_ID_DOWNLOADING
import com.looker.droidify.Common.NOTIFICATION_CHANNEL_INSTALLER
import com.looker.droidify.Common.NOTIFICATION_ID_INSTALLER
import com.looker.droidify.R
import com.looker.droidify.MainActivity
import com.looker.droidify.utility.Utils
@ -27,6 +28,20 @@ class InstallerService : Service() {
const val KEY_ACTION = "installerAction"
const val KEY_APP_NAME = "appName"
const val ACTION_UNINSTALL = "uninstall"
private const val INSTALLED_NOTIFICATION_TIMEOUT: Long = 10000
private const val NOTIFICATION_TAG_PREFIX = "install-"
}
override fun onCreate() {
super.onCreate()
if (Android.sdk(26)) {
NotificationChannel(
NOTIFICATION_CHANNEL_INSTALLER,
getString(R.string.syncing), NotificationManager.IMPORTANCE_LOW
)
.let(notificationManager::createNotificationChannel)
}
}
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
@ -61,12 +76,18 @@ class InstallerService : Service() {
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 sessionId = intent.getIntExtra(PackageInstaller.EXTRA_SESSION_ID, -1)
// get package information from session
val sessionInstaller = this.packageManager.packageInstaller
val session = if (sessionId > 0) sessionInstaller.getSessionInfo(sessionId) else null
val name = session?.appPackageName ?: 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 = intent.getStringExtra(KEY_APP_NAME)
val appLabel = session?.appLabel ?: intent.getStringExtra(KEY_APP_NAME)
?: try {
if (name != null) packageManager.getApplicationLabel(
packageManager.getApplicationInfo(
@ -78,11 +99,11 @@ class InstallerService : Service() {
null
}
val notificationTag = "download-$name"
val notificationTag = "${NOTIFICATION_TAG_PREFIX}$name"
// start building
val builder = NotificationCompat
.Builder(this, NOTIFICATION_CHANNEL_DOWNLOADING)
.Builder(this, NOTIFICATION_CHANNEL_INSTALLER)
.setAutoCancel(true)
.setColor(
ContextThemeWrapper(this, R.style.Theme_Main_Light)
@ -93,7 +114,7 @@ class InstallerService : Service() {
PackageInstaller.STATUS_PENDING_USER_ACTION -> {
// request user action with "downloaded" notification that triggers a working prompt
notificationManager.notify(
notificationTag, NOTIFICATION_ID_DOWNLOADING, builder
notificationTag, NOTIFICATION_ID_INSTALLER, builder
.setSmallIcon(android.R.drawable.stat_sys_download_done)
.setContentIntent(installIntent(intent))
.setContentTitle(getString(R.string.downloaded_FORMAT, appLabel))
@ -104,20 +125,19 @@ class InstallerService : Service() {
PackageInstaller.STATUS_SUCCESS -> {
if (installerAction == ACTION_UNINSTALL)
// remove any notification for this app
notificationManager.cancel(notificationTag, NOTIFICATION_ID_DOWNLOADING)
notificationManager.cancel(notificationTag, NOTIFICATION_ID_INSTALLER)
else {
val notification = builder
.setSmallIcon(android.R.drawable.stat_sys_download_done)
.setContentTitle(getString(R.string.installed))
.setContentText(appLabel)
.setTimeoutAfter(INSTALLED_NOTIFICATION_TIMEOUT)
.build()
notificationManager.notify(
notificationTag,
NOTIFICATION_ID_DOWNLOADING,
NOTIFICATION_ID_INSTALLER,
notification
)
Thread.sleep(5000)
notificationManager.cancel(notificationTag, NOTIFICATION_ID_DOWNLOADING)
}
}
PackageInstaller.STATUS_FAILURE_ABORTED -> {
@ -132,7 +152,7 @@ class InstallerService : Service() {
.build()
notificationManager.notify(
notificationTag,
NOTIFICATION_ID_DOWNLOADING,
NOTIFICATION_ID_INSTALLER,
notification
)
}
@ -163,7 +183,6 @@ class InstallerService : Service() {
0,
Intent(this, MainActivity::class.java)
.setAction(MainActivity.ACTION_INSTALL)
.setData(Uri.parse("package:$name"))
.putExtra(Intent.EXTRA_INTENT, promptIntent)
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
if (Android.sdk(23)) PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE