diff --git a/AGENTS.md b/AGENTS.md index 5b40036..445cc42 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -935,6 +935,20 @@ mutable at runtime from the phone. `autoCancel`, so the tap that opens the app takes it away, and the app is where the truth about that build is -- which is the cheap answer to a case that costs a stale line in the shade. + **There are two channels, and the difference is the sound.** Work in + progress is `IMPORTANCE_LOW`, because a build is an ongoing report of + something somebody just pressed a button for and the screen they + pressed it on already says the same thing; the row waiting to be + installed is `IMPORTANCE_DEFAULT`, because it is the arrival of + something nobody was watching for and a silent one is a button nobody + is told about. Two channels rather than one raised, because Android lets + an app only ever *lower* an existing channel's importance -- raising + the one that shipped would do nothing at all on a phone that had + already run this app, and the whole fix would be somebody finding it + in Android's settings. The waiting row also undoes the + `setOnlyAlertOnce` every other row wants: it replaces the running row + it grew out of, same tag and same id, and an update to a notification + posted alert-once is silent however loud its channel is. `POST_NOTIFICATIONS` is asked for at startup beside the local-network one, and refusing it costs the watching rather than the work: the service still runs, and there is simply nothing to draw. The type is diff --git a/README.md b/README.md index c18906a..9165b11 100644 --- a/README.md +++ b/README.md @@ -135,7 +135,9 @@ A build that finishes while you are somewhere else leaves its notification up with an **Install** button on it, because Android will not let the app put the installer on screen from the background. Pressing it installs, and the notification goes away by itself — as the rest do when their work is -over. +over. That one arrives with a sound, and the progress ones do not: they +are two channels, *Ready to install* and *Work in progress*, so either can +be turned down on its own. ### Updating this server itself diff --git a/app/androidApp/src/main/kotlin/com/example/devupdater/WorkNotice.kt b/app/androidApp/src/main/kotlin/com/example/devupdater/WorkNotice.kt index eb96f26..54ccfc1 100644 --- a/app/androidApp/src/main/kotlin/com/example/devupdater/WorkNotice.kt +++ b/app/androidApp/src/main/kotlin/com/example/devupdater/WorkNotice.kt @@ -186,7 +186,7 @@ object WorkNotice { /** What the shade should show for [items], against what it is showing now. */ private fun draw(context: Context, items: List) { - createChannel(context) + createChannels(context) // Notifications refused is not a reason to stop: the service is // what keeps a build alive, and that runs either way -- there is // simply nothing to draw. Asked rather than left to notify(), @@ -224,12 +224,13 @@ object WorkNotice { val rows = (if (running.size > 1) running else emptyList()) + waiting rows.forEach { item -> if (drawn[item.tag] == item) return@forEach - val into = base(context) + val inFlight = item.kind is WorkKind.Running + val into = base(context, if (inFlight) CHANNEL_ID else READY_CHANNEL_ID) // Running rows are grouped under the summary above. One // waiting to be installed is not: the group is the service's // and outlives neither it nor, at one row, its own collapsing // -- and a collapsed group hides the button this row is for. - if (item.kind is WorkKind.Running) into.setGroup(GROUP_KEY) + if (inFlight) into.setGroup(GROUP_KEY) notifications.notify(item.tag, ROW_ID, row(context, item, into)) } val tags = rows.map { it.tag }.toSet() @@ -256,6 +257,9 @@ private const val GROUP_KEY = "work" private const val CHANNEL_ID = "work" +/** The other channel: a build that has arrived, which is the one thing here that makes a sound. */ +private const val READY_CHANNEL_ID = "ready" + /** * How many steps a determinate bar has. * @@ -297,6 +301,11 @@ private fun row(context: Context, item: WorkItem, into: NotificationCompat.Build // app itself may not start that screen from the background. is WorkKind.Waiting -> into + // Undone here alone: this row replaces the running one + // it grew out of, same tag and same id, and an update to a + // notification posted alert-once is silent however loud + // its channel is. + .setOnlyAlertOnce(false) .setAutoCancel(true) .addAction( R.drawable.ic_updating, @@ -317,7 +326,7 @@ private fun row(context: Context, item: WorkItem, into: NotificationCompat.Build */ private fun foregroundNotice(context: Context, running: List): Notification { val builder = - base(context) + base(context, CHANNEL_ID) .setOngoing(true) .setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE) // Only between the last thing finishing and this service stopping, @@ -336,8 +345,8 @@ private fun foregroundNotice(context: Context, running: List): Notific } /** What every one of them is: the same icon, and the same tap back to the card it is about. */ -private fun base(context: Context): NotificationCompat.Builder = - NotificationCompat.Builder(context, CHANNEL_ID) +private fun base(context: Context, channel: String): NotificationCompat.Builder = + NotificationCompat.Builder(context, channel) .setSmallIcon(R.drawable.ic_updating) .setContentIntent(openApp(context)) // The work moves every second or so; alerting on each of those @@ -384,25 +393,42 @@ private fun openApp(context: Context): PendingIntent = PendingIntent.FLAG_IMMUTABLE, ) -private var channelMade = false +private var channelsMade = false /** - * Low importance, so a build does not arrive as a sound and a heads-up every time somebody presses - * Update. It is an ongoing report of something they just asked for, not news. + * The two channels, which differ in the one thing: whether it makes a sound. + * + * Work in progress is low importance, so a build does not arrive as a sound and a heads-up every + * time somebody presses Update -- it is an ongoing report of something they just asked for, not + * news, and the screen they pressed it on is already saying the same thing. A build that has landed + * and is waiting to be installed is the opposite case: it is the arrival of something nobody was + * watching for, which is why that row carries the Install button at all, and a silent one is a + * button nobody is told about. It keeps its badge for the same reason. + * + * Two channels rather than one channel raised to default, because an app may only ever *lower* an + * existing channel's importance. Raising [CHANNEL_ID] in code would do nothing whatever on a phone + * that has already run this app, and the whole fix would be somebody finding the channel in + * Android's settings. */ -private fun createChannel(context: Context) { - if (channelMade || Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return - context - .getSystemService(NotificationManager::class.java) - .createNotificationChannel( - NotificationChannel( - CHANNEL_ID, - context.getString(R.string.work_channel_name), - NotificationManager.IMPORTANCE_LOW, - ) - .apply { setShowBadge(false) } +private fun createChannels(context: Context) { + if (channelsMade || Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return + val manager = context.getSystemService(NotificationManager::class.java) + manager.createNotificationChannel( + NotificationChannel( + CHANNEL_ID, + context.getString(R.string.work_channel_name), + NotificationManager.IMPORTANCE_LOW, + ) + .apply { setShowBadge(false) } + ) + manager.createNotificationChannel( + NotificationChannel( + READY_CHANNEL_ID, + context.getString(R.string.work_ready_channel_name), + NotificationManager.IMPORTANCE_DEFAULT, ) - channelMade = true + ) + channelsMade = true } /** @@ -429,7 +455,7 @@ class WorkNoticeService : Service() { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { lastStart = startId - createChannel(this) + createChannels(this) // Has to happen within seconds of the start whatever the list says // by now, or the system kills the process for not having done it. ServiceCompat.startForeground( diff --git a/app/androidApp/src/main/res/values/strings.xml b/app/androidApp/src/main/res/values/strings.xml index 90f1b4f..0288039 100644 --- a/app/androidApp/src/main/res/values/strings.xml +++ b/app/androidApp/src/main/res/values/strings.xml @@ -12,6 +12,11 @@ downloaded" notification belongs to. Named for what the person sees in it rather than for the foreground service underneath. --> Work in progress + + Ready to install Finished