Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,27 @@ protected void onCreate(@Nullable Bundle s) {
serverController = new org.iiab.controller.ServerController(this, this);
serverController.start();

// ADFA-5343 (§3.3 follow-up): an install marker left by a DEAD process launch
// (InstallGuard.isInterrupted) is a killed install, not a resumable session. If the OS
// restores this index on top after such a kill, resuming here strands it polling for a server
// that may never come up — and the reconciler keeps retrying pdsm start uncut, because the
// DAMAGED loop-cut lives only in LibraryActivity.evaluateRecovery. Hand it to that single
// recovery owner instead: it boots a healthy base and clears the marker, or declares DAMAGED
// and cuts the retry. A live install in THIS process reads isLive (the marker is cleared on a
// clean finish), so a normal run never takes this branch.
//
// Guarded so it only fires for a genuinely stranded install with nothing live: this screen is
// also the home of the dashboard rebuild (which does NOT plant InstallGuard) and of live content
// downloads (LIVE), so a stale marker coinciding with one of those must not hijack it into
// recovery. After a real kill the queue/downloads are idle (device-observed: holder=NONE), so the
// guard never blocks the case it exists for.
if (org.iiab.controller.InstallGuard.isInterrupted(this)
&& !rebuildInSession()
&& !org.iiab.controller.env.EnvironmentLock.isBusyNow()) {
routeToRecovery();
return;
}

// ADFA-4919: observe the maps (proot) queue so its RUNNING -> DONE transition always
// re-renders the index. The REST streams have service listeners; the proot stage had none,
// so a proot-only install could finish without the index ever updating to Finish/redirect.
Expand Down Expand Up @@ -1239,6 +1260,21 @@ private void onModuleBatchTerminal() {
}
}

/**
* ADFA-5343 (§3.3 follow-up): hand an interrupted (dead-process) install to LibraryActivity's single
* recovery path. {@code CLEAR_TOP} <b>without</b> {@code SINGLE_TOP} so the standard-launchMode
* LibraryActivity is finished and re-created — its {@code onCreate} re-computes {@code recovering}
* and schedules {@code evaluateRecovery} (which boots a healthy base and clears the marker, or
* declares DAMAGED and sets {@code userWantsOn=false} to cut the reconciler's retry). Reusing the
* instance via {@code onNewIntent} would not, since Home is a monitor there.
*/
private void routeToRecovery() {
startActivity(new android.content.Intent(this, LibraryActivity.class)
.addFlags(android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP)
.putExtra(LibraryActivity.EXTRA_TAB, R.id.nav_library));
finish();
}

private void goHome(boolean clearSessions) {
cancelRedirect();
if (clearSessions) { ZimDownloadService.finishSession(); BooksDownloadService.finishSession(); KolibriSeedService.finishSession(); }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,31 @@ not re-open it, and the window is bounded by the first foreground recovery (rebo
boot), so the cut is momentary. This is the one net-new line of behavior beyond deletions, and it is
subtraction-shaped (it *stops* work).

**Device-verified (2026-08-31) and closed by a follow-up.** On device the cut works on the `LibraryActivity`
path (S2), but the residual has a second, real face: after a killed **module** install, the OS can restore the
`SetupProgressActivity` task on top; that index resumes and polls for a server that never comes on a broken base,
and `evaluateRecovery` (the only owner of the cut) never runs — so the reconciler retries `pdsm start` **uncut**
until the user manually reaches Home. Root: the *give-up* decision is owned by a **UI Activity**, not by the
lifecycle owner, so any path that bypasses `LibraryActivity.onCreate` bypasses the cut.

- **Fix (landed — small seam, one recovery owner).** `SetupProgressActivity.onCreate`: if
`InstallGuard.isInterrupted(this)` (a dead-process marker = a killed install, never a live run — the marker is
cleared on a clean finish) **and nothing live owns the screen** (`!rebuildInSession() && !EnvironmentLock.isBusyNow()`,
so a stale marker coinciding with a dashboard rebuild or a live content download — both of which also live on this
screen and neither plants InstallGuard — cannot hijack it), it does **not** resume the index — it routes to the
single recovery owner via
`startActivity(LibraryActivity, FLAG_ACTIVITY_CLEAR_TOP)` **without** `SINGLE_TOP`, so the standard-launchMode
`LibraryActivity` is re-created and its `onCreate` re-computes `recovering` and schedules `evaluateRecovery`
(reusing it via `onNewIntent` would not — Home is a monitor there). `LibraryActivity` then boots a healthy base
and clears the marker, or declares DAMAGED and cuts. No new state, no duplicated verdict — the cut stays owned in
one place; this only funnels the stray path into it. (The `batchServerSlow` 45 s timeout is *not* the hook: after
a fresh-process restore the reset queue is not "terminal", so its wait anchor never arms.)
- **End-state (recorded, not pulled in) — move the give-up to the owner.** The clean fix is for the *reconciler*
itself to stop driving `desired=UP` after the box repeatedly fails to boot on an **installed** system (a bounded
retry / present-but-unbootable signal), publishing a DAMAGED phase any screen observes — which lets us delete
both the `userWantsOn=false` cut-hack and the Activity-coupling entirely. That is a larger redesign of the core
owner (a retry budget + a new phase) with its own note, deliberately out of scope for this follow-up.

---

## 4. Decision (piece 2) — route recovery through `desired`, delete the residue
Expand Down
Loading