Stop per-app download notification chatter during installs

The download worker posted a separate per-app notification for every
internal phase transition, keyed by package hash and distinct from the
ongoing foreground progress notification. In practice this meant:

- a blank, non-dismissable notification while "purchasing" (no branch
  rendered it), and
- a transient "download complete" notice for every app that was then
  immediately replaced by the install-success notification.

During a bulk update this produced a burst of redundant notifications.

Suppress the purchasing/verifying phases (the ongoing progress
notification already conveys activity) and only surface completion when
the user must act on it (apps that can't be installed silently get a
tap-to-install notice). Silently-installable apps now go straight to a
single "installed" notification.

Also clear any stale per-app notification on these phases and on
cancellation, fixing a leak where a cancelled download left its last
notification behind, and refresh the foreground notification to an
"Installing" state so the user sees a clean downloading -> installing
progression instead of a download bar stuck at 100%.
This commit is contained in:
Rahul Patel
2026-05-30 17:57:50 +05:30
parent 012f4dcc26
commit 9bd5e3f8e7
2 changed files with 36 additions and 1 deletions

View File

@@ -252,6 +252,10 @@ class DownloadWorker @AssistedInject constructor(
private suspend fun onSuccess(): Result { private suspend fun onSuccess(): Result {
return withContext(NonCancellable) { return withContext(NonCancellable) {
return@withContext try { return@withContext try {
// Update the ongoing foreground notification to reflect the install phase,
// so the user sees a clean "Downloading -> Installing" progression instead of
// a stale download bar lingering at 100%.
notifyStatus(DownloadStatus.INSTALLING, isProgress = true)
appInstaller.getPreferredInstaller(notifyOnFallback = true).install(download) appInstaller.getPreferredInstaller(notifyOnFallback = true).install(download)
Result.success() Result.success()
} catch (exception: Exception) { } catch (exception: Exception) {
@@ -537,13 +541,35 @@ class DownloadWorker @AssistedInject constructor(
downloadDao.updateStatus(download.packageName, status) downloadDao.updateStatus(download.packageName, status)
when (status) { when (status) {
// Internal phases the user doesn't need a separate notification for: the ongoing
// foreground progress notification already conveys that work is in progress.
// Clear any stale per-app notification (e.g. a prior failure being retried) so it
// doesn't linger.
DownloadStatus.PURCHASING,
DownloadStatus.VERIFYING, DownloadStatus.VERIFYING,
DownloadStatus.CANCELLED -> return DownloadStatus.CANCELLED -> {
notificationManager.cancel(download.packageName.hashCode())
return
}
DownloadStatus.COMPLETED -> { DownloadStatus.COMPLETED -> {
// Mark progress as 100 manually to avoid race conditions // Mark progress as 100 manually to avoid race conditions
download.progress = 100 download.progress = 100
downloadDao.updateProgress(download.packageName, 100, 0, 0) downloadDao.updateProgress(download.packageName, 100, 0, 0)
// Silently-installable apps install automatically and get a single
// "installed" notification afterwards, so a separate "download complete"
// notice is just noise. Only surface completion when the user must act on it
// (tap to install).
val needsUserAction = !AppInstaller.canInstallSilently(
context,
download.packageName,
download.targetSdk
)
if (!needsUserAction) {
notificationManager.cancel(download.packageName.hashCode())
return
}
} }
else -> {} else -> {}

View File

@@ -208,6 +208,15 @@ object NotificationUtil {
) )
} }
DownloadStatus.INSTALLING -> {
builder.setSmallIcon(android.R.drawable.stat_sys_download_done)
builder.setContentText(context.getString(R.string.status_installing))
builder.setOngoing(true)
builder.setCategory(Notification.CATEGORY_PROGRESS)
builder.setProgress(100, 100, true)
builder.foregroundServiceBehavior = NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE
}
else -> {} else -> {}
} }
return builder.build() return builder.build()