Skip to content

ADFA-5137 refactor(state): setup_complete stops existing — the app as… - #403

Merged
luisguzman-adfa merged 3 commits into
mainfrom
refactor/ADFA-5137-setup-complete-stops-existing
Aug 14, 2026
Merged

ADFA-5137 refactor(state): setup_complete stops existing — the app as…#403
luisguzman-adfa merged 3 commits into
mainfrom
refactor/ADFA-5137-setup-complete-stops-existing

Conversation

@luisguzman-adfa

Copy link
Copy Markdown
Collaborator

…ks whether a system is here

Closes findings 3 and 5 of state-spine.svg and the first row of the standing directive's ledger, the one that read: setup_complete | SharedPreferences | 4 sites | nobody | never | no lifecycle.

THE FAULT.

setup_complete was a claim about the past — "setup happened" — answering a question about the present: "should this person see the wizard?". It was written when an install STARTED and cleared by nobody, so it could say yes while the device had nothing, and the launch then routed past the wizard forever. Finding
3. From there the only way forward was a sentence pointing at a control named "Get more". Finding 5. One bug read twice.

WHAT REPLACES IT — nothing stored.

The answer was already on the device, in three facts that each die with the thing they describe: a rootfs on disk, the durable install marker, the environment lock's owner. SystemPresence.hereOrOnTheWay() is that disjunction, pure and with its whole eight-row truth table in tests; SystemPresenceReader gathers the three reads, and is the only place that does — four screens each assembling them would be the same duplication arriving by a different door.

Deleting the flag rather than giving it a lifecycle is the point. A stored copy has to be kept in agreement with the original forever, and that agreement is what broke. Four boolean facts allow sixteen combinations; three allow eight, and every one of the eight describes something physically true. The dead end is not fixed, it becomes unrepresentable.

FOLLOWING THE VARIABLE, NOT THE SCREENS.

Nine sites touched the key. Each reader was asked what it was really asking, and only one of the four was asking the question the flag pretended to answer:

  • LibraryActivity — "is there a system, or one on the way": migrated.
  • WizardActivity — "where do I resume": the flag half of that condition was always true, because LibraryActivity is the only thing that opens the wizard and only when there is nothing. allPermsGranted() was doing all the work.
  • SetupActivity — "am I the first-run wizard or am I Settings". A different question entirely, and the reason this step matters: migrating it to the presence rule would have produced a Settings screen that believes it is a wizard. It takes its mode from the Intent now, from the two callers that know.
  • MainActivity — still reachable, because it runs whenever Settings opens the Terminal. Migrated, and its catch no longer writes the flag to stop itself firing: the condition is re-derived every launch, so a missing Activity is a log line again instead of permanent state.

With no readers, all five writers went, and the key with them — including the setup_complete → false that ADFA-5119 added a few hours ago.

TWO THINGS THE ANALYSIS DID NOT PREDICT.

A race on the happy path. startWizardInstall starts the service and the Activity one after the other with no ordering between them, so LibraryActivity.onCreate can run before InstallService.onStartCommand — and with the flag gone it would find no rootfs, no marker and no lock, and bounce the user back to the wizard on the install they had just asked for. The flag was hiding that. The marker is now planted where the user commits rather than whenever the scheduler reaches the service. If the service then never starts, the marker is left set and the next launch enters recovery — a state with a dialog and a way out, which is the right side to fail on.

"Copy from a phone" needed a replacement, not a deletion. It is the one navigation that must land with no system and nothing yet in flight, which is why it wrote the flag. It now says so — EXTRA_SETTING_UP, an Intent extra that dies with the navigation. Leave before scanning and the next launch is back at the wizard, correctly, because nothing was started.

THE SAFETY NET.

H_NO_LIBRARY gets an action, "Install a system", routing to the tier step. It was the one header state offering nothing while FAILED offers Retry and INSTALLING offers See progress. This ticket also closes the way into that state, so in principle nobody arrives there — the button stays because "in principle" is what the last four dead ends had in common.

Ledger: the setup_complete row is deleted rather than turned green. 34 files lose the key; the new string is translated into 33 locales.

…ks whether a system is here

Closes findings 3 and 5 of state-spine.svg and the first row of the standing
directive's ledger, the one that read: setup_complete | SharedPreferences |
4 sites | nobody | never | no lifecycle.

THE FAULT.

setup_complete was a claim about the past — "setup happened" — answering a
question about the present: "should this person see the wizard?". It was written
when an install STARTED and cleared by nobody, so it could say yes while the
device had nothing, and the launch then routed past the wizard forever. Finding
3. From there the only way forward was a sentence pointing at a control named
"Get more". Finding 5. One bug read twice.

WHAT REPLACES IT — nothing stored.

The answer was already on the device, in three facts that each die with the thing
they describe: a rootfs on disk, the durable install marker, the environment
lock's owner. SystemPresence.hereOrOnTheWay() is that disjunction, pure and with
its whole eight-row truth table in tests; SystemPresenceReader gathers the three
reads, and is the only place that does — four screens each assembling them would
be the same duplication arriving by a different door.

Deleting the flag rather than giving it a lifecycle is the point. A stored copy
has to be kept in agreement with the original forever, and that agreement is what
broke. Four boolean facts allow sixteen combinations; three allow eight, and every
one of the eight describes something physically true. The dead end is not fixed,
it becomes unrepresentable.

FOLLOWING THE VARIABLE, NOT THE SCREENS.

Nine sites touched the key. Each reader was asked what it was really asking, and
only one of the four was asking the question the flag pretended to answer:

* LibraryActivity — "is there a system, or one on the way": migrated.
* WizardActivity — "where do I resume": the flag half of that condition was
  always true, because LibraryActivity is the only thing that opens the wizard
  and only when there is nothing. allPermsGranted() was doing all the work.
* SetupActivity — "am I the first-run wizard or am I Settings". A different
  question entirely, and the reason this step matters: migrating it to the
  presence rule would have produced a Settings screen that believes it is a
  wizard. It takes its mode from the Intent now, from the two callers that know.
* MainActivity — still reachable, because it runs whenever Settings opens the
  Terminal. Migrated, and its catch no longer writes the flag to stop itself
  firing: the condition is re-derived every launch, so a missing Activity is a
  log line again instead of permanent state.

With no readers, all five writers went, and the key with them — including the
setup_complete → false that ADFA-5119 added a few hours ago.

TWO THINGS THE ANALYSIS DID NOT PREDICT.

A race on the happy path. startWizardInstall starts the service and the Activity
one after the other with no ordering between them, so LibraryActivity.onCreate
can run before InstallService.onStartCommand — and with the flag gone it would
find no rootfs, no marker and no lock, and bounce the user back to the wizard on
the install they had just asked for. The flag was hiding that. The marker is now
planted where the user commits rather than whenever the scheduler reaches the
service. If the service then never starts, the marker is left set and the next
launch enters recovery — a state with a dialog and a way out, which is the right
side to fail on.

"Copy from a phone" needed a replacement, not a deletion. It is the one
navigation that must land with no system and nothing yet in flight, which is why
it wrote the flag. It now says so — EXTRA_SETTING_UP, an Intent extra that dies
with the navigation. Leave before scanning and the next launch is back at the
wizard, correctly, because nothing was started.

THE SAFETY NET.

H_NO_LIBRARY gets an action, "Install a system", routing to the tier step. It was
the one header state offering nothing while FAILED offers Retry and INSTALLING
offers See progress. This ticket also closes the way into that state, so in
principle nobody arrives there — the button stays because "in principle" is what
the last four dead ends had in common.

Ledger: the setup_complete row is deleted rather than turned green. 34 files lose
the key; the new string is translated into 33 locales.
…ops being a way to break things

Second review of the branch. Three findings acted on, and the first one is the
kind only the second pass catches.

I ADDED THE TENTH ANSWER.

SystemPresenceReader stood four files from SystemFactsReader, in the same
package, reading the same three sources — and SystemFactsReader's own javadoc
says the ADFA-5061 survey found nine answers to "is a system installed" and that
"adding a tenth would have been the joke telling itself". Mine claimed to be
"the only place that gathers them". Both cannot be true, and the file I
duplicated warns against exactly this. The two also disagreed on the null
context: SystemFactsReader.read(null) is conservative, mine was permissive.

hereOrOnTheWay now lives on SystemFactsReader and the rival class is gone. Why
it is a method there rather than a field on SystemFacts is written down, because
it was worth verifying rather than assuming: isInstalled() is rootfs && !marker,
so an install in flight and a device with nothing produce the identical tuple —
installed=false, healthy=true — and telling those apart is the whole question.
It is not derivable from what read() returns. SystemPresence stays as the pure
rule with its eight-row table.

THE SAFETY NET WAS A ONE-TAP ROUTE INTO THE FAILURE IT GUARDED AGAINST.

H_NO_LIBRARY is computed from isSystemInstalled(), which is false for the whole
time an install marker is set — including while a clone-receive holds both the
marker and the lock, which is a state this branch keeps the user in. The gate
lifts for ownerHeld, so Home is reachable, the header reads "no library", and the
new button offered a tier flow with reinstall=false. InstallService's
non-destructive guard would then see the half-received rootfs directory, skip the
extract, report success, and its teardown would clear the marker a killed receive
needs for recovery. That is the "boot the wreck" failure InstallService's own
cleanup comment warns about, reached from the button labelled as the way out.

Fixed at the root rather than in my branch: the header now calls openGetMore(),
which already re-reads the facts at click time, and the lock check went INTO
openGetMore — so the footer control that had the same hole is covered by the same
line. ownerHeld rather than isHeld, so a live content download does not block it
(ADFA-4957 draws that line for the server toggle).

ORDER MATTERS IN MainActivity, AND IT DID NOT USE TO.

The first-run redirect runs long before the terminal extras are read. It used to
fire on setup_complete, true on any device that had ever started an install, so a
terminal launch never met it. Asking the disk instead, a device with no system
sent Settings → Terminal and the terminal's keep-alive notification into the
legacy setup shell in wizard mode with Back blocked, extras dropped. The redirect
now asks first whether this launch came to open a terminal. EXTRA_OPEN_TERMINAL
alone, because the notification does not pass EXTRA_TERMINAL_ONLY and both
callers came for the same thing.

COMMENTS THAT WERE WRONG, WHICH HERE COUNTS AS A DEFECT.

EXTRA_SETTING_UP's javadoc said it "dies with this navigation". It does not, in
either direction: its lifetime is the task record, so Android replays it when the
process is killed and the task is restored, and onNewIntent's setIntent can erase
it before then. Both outcomes are truthful — the user lands on Home, which now
has a labelled way to install a system — but the lifecycle claim was mine to get
right and I did not.

ModuleHubFragment said the app "no longer routes to the Library after a failed
restore". A failed restore keeps the marker, so it does route there, with the
recovery dialog. Corrected to say what actually still lands on that screen.

InstallService's cleanup prose still counted five items over a four-item list and
credited a setup_complete clear that no longer happens.

Also: three imports left unused by the previous commit.

LEFT AS A DECISION, NOT CHANGED IN PLACE.

The review proposes fixing the startWizardInstall race with EXTRA_INSTALLING —
already on the same Intent, already read by LibraryActivity — instead of the early
InstallGuard.begin. That version persists nothing and removes the "if the service
never starts, the next launch enters recovery" trade-off. Against it: the early
marker also suppresses a racing server auto-start, which the extra does not. Both
work, the difference is design, so it is recorded rather than swapped.
…ew one named

Both diagrams had fallen behind the code, and the spine had started
contradicting itself: its map still listed setup_complete as live state while its
findings said the flag was gone.

STATE SPINE.

Findings 3 and 5 turn green beside 1. Three of the seven original dead ends are
closed, 4 has lost one of its three blind spots, and the ranking line says so.

The map above them stopped disagreeing. S0 and S2 no longer describe a flag that
does not exist, the "Copy from a phone" edge no longer claims to mark anything,
and S1 is marked UNREACHABLE with the reason the box is still drawn: a
LibraryActivity instance that is already alive never re-runs the launch check,
because onNewIntent skips it — which is why the header keeps its Install button
rather than relying on the entrance being shut.

Finding 8 is new and was found on a device, by navigating, not by reading: while
a clone is being shared the server is stopped on purpose, the header reads !alive
and concludes "Couldn't start — tap to retry", and canStartServer() does not know
about CloneSendSession, so that Retry starts the server on top of a live
transfer. ADFA-5143.

ROADMAP.

The ledger's first row does not turn green, it disappears: setup_complete is
DELETED, lives nowhere, is written by nobody and has nothing to clear. And the
standing directive gains the move it was missing. It named two good patterns,
both of them "the fact dies with the thing it describes"; the third is that a
fact which can be derived is deleted rather than maintained.

ADFA-5119's card goes to DONE with its leftover written on it — the kiwix index
leg still sends exit and error onward, so a failed companion run still reports as
finished. Cards added for ADFA-5137 and ADFA-5143.

Two things were quietly broken and are fixed in passing: the roadmap's background
rect was still 2679 high under a 2825 canvas, leaving an unpainted band, and one
of finding 5's citation lines had been pushed inside finding 6 by an earlier
coordinate shift.
@luisguzman-adfa
luisguzman-adfa merged commit c10da6f into main Aug 14, 2026
3 checks passed
@luisguzman-adfa
luisguzman-adfa deleted the refactor/ADFA-5137-setup-complete-stops-existing branch August 14, 2026 11:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant