A build waiting to be installed arrives with a sound
Every notification this app posts was on one IMPORTANCE_LOW channel, so all of them were silent -- including the row for a build that landed while nobody was looking, which is the one that carries the Install button and is the only thing here that is news rather than a report of something somebody just pressed a button for. A second channel rather than raising that one, because Android lets an app only ever lower an existing channel's importance: raising "work" in code would do nothing whatever 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 has to undo the setOnlyAlertOnce every other row wants. It replaces the running row it grew out of -- same tag, same id -- and an update to a notification posted alert-once is silent however loud its channel is, which is exactly the arrival nobody is watching for. Measured on the API 36 emulator: pressing Update on a test project and going to the home screen posts the row on channel=ready, importance 3, with mSound resolved and mIsInterruptive=true, and the shade files it above its own "Silent" divider. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
adaeb0c0ad
commit
7bf10061e2
4 files changed
+64
-17
No files matched your search
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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<WorkItem>) {
|
||||
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<WorkItem>): 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<WorkItem>): 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,17 +393,27 @@ 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(
|
||||
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),
|
||||
@@ -402,7 +421,14 @@ private fun createChannel(context: Context) {
|
||||
)
|
||||
.apply { setShowBadge(false) }
|
||||
)
|
||||
channelMade = true
|
||||
manager.createNotificationChannel(
|
||||
NotificationChannel(
|
||||
READY_CHANNEL_ID,
|
||||
context.getString(R.string.work_ready_channel_name),
|
||||
NotificationManager.IMPORTANCE_DEFAULT,
|
||||
)
|
||||
)
|
||||
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(
|
||||
|
||||
@@ -12,6 +12,11 @@
|
||||
downloaded" notification belongs to. Named for what the person
|
||||
sees in it rather than for the foreground service underneath. -->
|
||||
<string name="work_channel_name">Work in progress</string>
|
||||
<!-- The channel the "this build has arrived, press to install it"
|
||||
notification belongs to. A separate one because it is the only
|
||||
one that makes a sound; named for what is waiting rather than
|
||||
for the notification. -->
|
||||
<string name="work_ready_channel_name">Ready to install</string>
|
||||
<!-- Shown for the moment between the last thing finishing and the
|
||||
notification going away. -->
|
||||
<string name="work_done">Finished</string>
|
||||
|
||||
Reference in new issue
Block a user