A finished build keeps its notification, with an Install button on it

Handing an APK to the system installer means starting an activity, and
Android refuses one from the background: fired anyway it does nothing at
all, so a download that finished while the phone was in a pocket read as
an update that never landed. `updateComponent` now stops at
ReadyToInstall whenever this screen is not in front of anybody -- exactly
as it already did for the second APK of a project-wide update -- and that
state is drawn as a row of its own: no bar, dismissible, out of the
group, and carrying an Install action whose PendingIntent is the install
intent itself. An activity is the one press a notification may make from
any state.

Which is why the drawing moves from the service into WorkNotice: that
row outlives the last running thing, and something still alive has to
take it down. The list screen is that something. The service is now the
keep-alive and the one notification it is held up by, nothing else.

Two things had to be true for the row to come down again, and neither
was. continuePendingInstalls now begins by forgetting a download whose
install has already happened -- the installed copy's lastUpdateTime
against the downloaded file's mtime, rather than listening for an
install, since the same broadcast reports the removal the wrong-key case
is waiting for. Without it, pressing Install on the notification and then
returning to the app ran the whole install a second time; measured, not
feared. And registerPackageChangeReceiver dropped every arrival that was
an update: EXTRA_REPLACING marks the ADDED half of a replace and the
REPLACED that follows it, not only the REMOVED half the guard was written
for, so an in-place install reached nothing here until the screen next
resumed.

Verified on the emulator: Install pressed, app sent to the home screen,
build and download finish in the background, the row turns into
"Downloaded, waiting to install..." with the button, the service stops,
Install from the shade puts up the system installer, and the row goes
away by itself once the install lands -- with the app still in the
background, and with no second installer when it is opened again.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
iris-aiandClaude Opus 5 committed 2026-09-19 21:37:49 -04:00
1 parent ded6df559d
commit adaeb0c0ad
6 files changed
+449 -212

No files matched your search

+38
View File
@@ -897,6 +897,44 @@ mutable at runtime from the phone.
caught, and the next thing the run reports asks again. Stops go through
`stopSelf(lastStart)` rather than `stopSelf()`, so work starting in the
moment between deciding to stop and stopping is not taken away with it.
**A build that finishes while nobody is looking keeps its notification,
and that notification carries the install.** Handing an APK to the
system installer is starting an activity, which Android refuses from the
background -- fired anyway it does nothing at all, so a download that
worked read as an update that never landed. So `updateComponent` stops
at `ComponentState.ReadyToInstall` whenever this screen is not in front
of anybody, exactly as it already did for the second APK of a
project-wide update, and that state is a `WorkKind.Waiting` row: no bar,
dismissible, out of the group, and carrying an **Install** action whose
`PendingIntent` is the install intent itself. An activity is the one
press a notification may make from any state; a broadcast back into this
app would have to start the installer itself, which is the thing that
cannot be done from there.
Which is why the drawing lives in `WorkNotice` rather than in the
service: that row outlives the last running thing, and something still
alive has to take it down. The list screen is that something, since the
states being drawn are its own.
**Two things had to be true for it to come down again**, and neither was.
`continuePendingInstalls` now begins by forgetting every download whose
install has already happened -- compared as the installed copy's
`lastUpdateTime` against the downloaded file's mtime, rather than by
listening for an install, because the same broadcast reports removals
and a removal is what the wrong-key case is *waiting* for. Without it,
pressing Install on the notification and then returning to the app ran
the whole install a second time (measured, not feared). And
`registerPackageChangeReceiver` dropped **every** arrival that was an
update: `EXTRA_REPLACING` marks the REMOVED half of a replace, the
ADDED half, *and* the REPLACED that follows, so testing it without the
action threw the lot away and an in-place install reached nothing here
until the screen next resumed. Invisible while the resume was the only
thing waiting on it; the moment a notification depended on it, it was
the difference between the row coming down by itself and sitting there
after the install it was for.
What is left is a row that outlives the process: nothing cancels it if
the install happens with this app long since reclaimed. It is
`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.
`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