Record the keep-alive corrections in the port notes and the manual checks

The port doc's backgrounding decision now carries the four corrections
rather than describing a wiring that was not true, and Phase 14 gains
the checks a phone can actually run: a backgrounded shell surviving, an
idle Files connection surviving, the permission ask arriving at the
first thing worth showing, and a refusal costing the notification and
nothing else.
This commit is contained in:
2026-08-09 10:14:24 +02:00
parent 48ea5e22d5
commit 3977f68870
2 changed files with 87 additions and 4 deletions
+38 -3
View File
@@ -30,7 +30,10 @@ verified is that it compiles, links, packages, and carries the right natives.
transfers protected by a **foreground service**. File transfer is not in the first scope; when it arrives it
is **one remote pane** with Android's document picker for moving files in and out. *It has since arrived,
both ways:* the pane, the queue, `ACTION_OPEN_DOCUMENT` going in and `ACTION_CREATE_DOCUMENT` coming out,
with the foreground service now counting transfers as well as shells.
with the foreground service now counting transfers as well as shells — and, since, an idle-but-connected
Files session as well, which a transfer count alone was blind to. *Corrected the same round:* the service's
other half — a shell's own opening — had never been wired to anything at all, so a shell survived only for
as long as the app stayed foreground; see [Sessions survive backgrounding](#sessions-survive-backgrounding-via-a-foreground-service).
**What was actually checked**, so the rest can be read with the right amount of trust:
@@ -280,13 +283,40 @@ What is desktop-only is the *left* pane — `LocalDirectory`, the drive list, th
### Sessions survive backgrounding, via a foreground service
A persistent notification for as long as a shell or a transfer is live.
A persistent notification for as long as a shell, a transfer, or an idle-but-connected Files session is
live.
It costs the user a notification and some battery. It buys the behaviour the desktop client already promises
and documents — that a shell outlives a vault lock, and that a transfer finishes — and the alternative was
to make `TerminalWorkspace`'s guarantee desktop-only, which is a worse thing to have to write down than a
notification is to look at.
**Three corrections found after the first cut shipped, all in the wiring rather than the design:**
- **A shell opening never started the service.** `SessionKeepAlive` heard `TerminalWorkspace.SessionEnded`
and refreshed on that, but nothing announced the opposite event — so a user who opened a shell and
backgrounded the app immediately had no foreground service at all, and Android was free to kill the
process holding it. `MainWindowViewModel.TerminalSessionOpened` is now wired the same way in
`App.axaml.cs`'s `ComposeKeepAlive`.
- **A connected-but-idle Files session counted as nothing.** A host open on the Files screen with no
transfer moving is a live SFTP connection a dying process would sever, and the old two-argument
`Reconcile(liveSessions, activeTransfers)` had no way to hear about it. `TransfersViewModel.HasLiveFileSession`
`IsConnected` with a real `ConnectedCipher`, which a bucket never has — is the third fact `Reconcile` now
takes.
- **Refreshing the notification restarted the service, which throws when backgrounded.** `Reconcile` called
`StartForegroundService` on every refresh, including the common case of a service that was already
running. On API 31+ that throws `ForegroundServiceStartNotAllowedException` the instant the app is
backgrounded — a transfer finishing in the pocket, one of two shells dying — which crashed the process and
took every session with it. `SessionForegroundService` now tracks whether it is already running and, when
it is, posts the updated notification through `NotificationManager.Notify` instead of asking Android to
start anything.
**The notification permission is requested, not just declared.** API 33+ requires `POST_NOTIFICATIONS` at
runtime or the receipt is silently invisible — the service still runs, but nothing on screen says so.
`SessionForegroundService.Reconcile` asks for it the first time in this process there is actually something
to show, at most once, with no result read back: a refusal costs the notification and nothing else, which is
what the manifest's own comment on the permission says.
### Phone first
About 360dp wide. The tablet route was cheaper — a landscape tablet is close to the existing 880×560 minimum
@@ -523,7 +553,12 @@ go at 360dp:
stopping it from a count rather than a lifecycle. `TerminalWorkspace.LiveSessionCount` is the source of
truth deliberately: it already knows that a session whose shell exited is not live, which a counter
incremented on open would not, and a phone showing "1 shell connected" over nothing would be exactly the
dishonesty the unlock screen's count exists to prevent.
dishonesty the unlock screen's count exists to prevent. *Corrected since:* the opened half of a shell's
lifecycle was never wired in, so the service could never come up for a shell at all; an idle-but-connected
Files session now counts as a third live fact rather than nothing; a refresh while backgrounded updates
the notification in place instead of restarting the service, which the API throws on; and
`POST_NOTIFICATIONS` is now actually requested rather than merely declared. See
[Sessions survive backgrounding](#sessions-survive-backgrounding-via-a-foreground-service) for all four.
7. ~~**The interface**, phone-first.~~ **Done for the decided scope** — all seven screens of the design,
plus the two states the design does not draw because it starts at an enrolled phone (naming a server, and
choosing a passphrase).