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
34 changes: 29 additions & 5 deletions controller/app/src/main/java/org/iiab/controller/MainActivity.java
Original file line number Diff line number Diff line change
Expand Up @@ -264,16 +264,20 @@ public void onReceive(Context context, Intent intent) {
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

// Intercept launch and redirect to Setup Wizard if first time
SharedPreferences internalPrefs = getSharedPreferences(getString(R.string.pref_file_internal), Context.MODE_PRIVATE);
if (!internalPrefs.getBoolean(getString(R.string.pref_key_setup_complete), false)) {
// Intercept launch and redirect to the legacy Setup Wizard when there is nothing to run.
// ADFA-5137: asks the device rather than setup_complete, and says which mode it wants. The
// old catch wrote the flag true so this branch would stop firing; with the flag gone there is
// nothing to write, and nothing needs writing — the condition is re-derived every launch, so
// a missing Activity stops being permanent state and goes back to being a log line.
if (!terminalOnlyLaunch(getIntent())
&& !org.iiab.controller.system.data.SystemFactsReader.hereOrOnTheWay(this)) {
try {
startActivity(new Intent(this, SetupActivity.class));
startActivity(new Intent(this, SetupActivity.class)
.putExtra(SetupActivity.EXTRA_WIZARD_MODE, true));
finish();
return; // We stop the execution of MainActivity right here
} catch (android.content.ActivityNotFoundException e) {
android.util.Log.w(TAG, "SetupActivity not found. Skipping initial setup.");
internalPrefs.edit().putBoolean(getString(R.string.pref_key_setup_complete), true).apply();
}
}

Expand Down Expand Up @@ -558,6 +562,26 @@ protected void onNewIntent(Intent intent) {
maybeOpenTerminalFromIntent(intent);
}

/**
* ADFA-5137 (review): was this Activity launched only to show the terminal?
*
* <p>Asked before the first-run redirect above, because the order matters and it did not use to.
* That redirect fired on {@code setup_complete}, which was true on any device that had ever
* started an install, so a terminal launch never met it. Now it asks the disk — and with no
* system, Settings → Terminal and the terminal's own keep-alive notification would land in the
* legacy setup shell in wizard mode with Back blocked, having dropped the extras that said what
* they came for.
*
* <p>Read here rather than deferring to {@code maybeOpenTerminalFromIntent}, which runs much later
* in {@code onCreate}: the redirect happens first, so the question has to be answerable first.
*/
private static boolean terminalOnlyLaunch(Intent intent) {
// EXTRA_OPEN_TERMINAL alone, not paired with EXTRA_TERMINAL_ONLY: the redesign's Settings entry
// sets both, but TerminalSessionService's keep-alive notification sets only the first, and both
// came here to open a terminal. What decides the redirect is what the caller came for.
return intent != null && intent.getBooleanExtra(EXTRA_OPEN_TERMINAL, false);
}

/** Open the full terminal when launched from its keep-alive notification (ADFA-4696). */
private void maybeOpenTerminalFromIntent(Intent intent) {
if (intent == null || terminalController == null) return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@
*/
package org.iiab.controller;

import android.content.Context;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.view.View;

Expand All @@ -31,6 +29,14 @@
*/
public class SetupActivity extends AppCompatActivity {

/**
* ADFA-5137: true opens this screen as the first-run wizard, false as Settings. Set by whoever
* opens it, because the mode is a property of the reason for opening it and of nothing else.
* Absent means Settings, which is the safe default: a Settings screen is navigable and a wizard
* blocks Back.
*/
public static final String EXTRA_WIZARD_MODE = "org.iiab.controller.SETUP_WIZARD_MODE";

private boolean wizardMode;

@Override
Expand All @@ -39,9 +45,14 @@ protected void onCreate(Bundle savedInstanceState) {
setContentView(R.layout.activity_setup);
org.iiab.controller.help.TooltipWiring.wireAll(getWindow().getDecorView());

SharedPreferences prefs = getSharedPreferences(
getString(R.string.pref_file_internal), Context.MODE_PRIVATE);
wizardMode = !prefs.getBoolean(getString(R.string.pref_key_setup_complete), false);
// ADFA-5137: the caller says which mode this is, because only the caller knows.
//
// It used to read setup_complete, and that was the one reader asking a genuinely different
// question: not "is there a system" but "am I the first-run wizard or am I Settings". Two
// callers open this screen for those two reasons — MainActivity's first-run redirect and its
// Settings button — so the answer belongs in the Intent. Migrating this one to the presence
// rule would have produced a Settings screen that believes it is a wizard.
wizardMode = getIntent() != null && getIntent().getBooleanExtra(EXTRA_WIZARD_MODE, false);

View rail = findViewById(R.id.setup_rail);
if (wizardMode) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -240,9 +240,10 @@ private void finishEnrollment(SharedPreferences delivery) {
private void completeSetup() {
// ADFA-4466 Phase 2: setup funnel completion (no-op unless opted in).
org.iiab.controller.analytics.AnalyticsClient.with(requireContext()).logOnboardingCompleted();
SharedPreferences prefs = requireContext().getSharedPreferences(
getString(R.string.pref_file_internal), Context.MODE_PRIVATE);
prefs.edit().putBoolean(getString(R.string.pref_key_setup_complete), true).apply();
// ADFA-5137: nothing to mark. This screen belongs to the legacy setup shell, and it wrote
// setup_complete on its way out — a fourth writer of a flag nobody cleared. LibraryActivity
// now asks the device instead, so if this path really did install a system it will be found,
// and if it did not, the wizard is where the user should land.
startActivity(new Intent(requireContext(), org.iiab.controller.redesign.LibraryActivity.class));
requireActivity().finish();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1509,7 +1509,7 @@ private void doCancel() {
/**
* ADFA-5119: erase every trace of a system the user decided not to build.
*
* <p>Five things claimed that system existed or was about to. Leaving any one of them behind is
* <p>Four things claimed that system existed or was about to. Leaving any one of them behind is
* a specific bug, not untidiness:
*
* <ol>
Expand All @@ -1523,11 +1523,13 @@ private void doCancel() {
* the content chosen for the tier being given up is not drained into the next one.</li>
* <li><b>{@code installed_tier}</b>, which a later "Get more" reads to size content against a
* system that was never installed.</li>
* <li><b>{@code setup_complete}</b>, which is what decides whether the next launch opens the
* library or the wizard. This is the one that keeps the promise: no path ends in the app
* with no system.</li>
* </ol>
*
* <p>There used to be a fifth: {@code setup_complete} → false, which was what kept the promise that
* no path ends in the app with no system. ADFA-5137 deleted the flag, so the promise is now kept by
* not having anything to unset — the launch asks whether a rootfs, an install or a deep operation
* is there, and after this cleanup none of the three is.
*
* <p>The install marker is a sixth, and it is already handled — {@link #teardown()} clears it on
* every clean terminal. Left set, the next launch would open in damaged-system recovery over a
* system nobody asked for.
Expand Down Expand Up @@ -1564,22 +1566,22 @@ private void forgetTheAbandonedSystem() {
org.iiab.controller.system.data.ContentStateInvalidator.replacementSucceeded(this,
org.iiab.controller.system.domain.SystemReplacement.Cause.ABANDONED_INSTALL);

// 4 + 5. The decision itself, and last on purpose. If the process is killed part-way
// 4. The recorded tier, and last on purpose. If the process is killed part-way
// through this method, everything above it is disposable wreckage and the install marker
// is still set, so the next launch enters damaged-system recovery — which offers a
// reinstall. Clearing setup_complete first and dying here would instead send the user to
// the wizard while the marker still says an install is running.
//
// commit(), not apply(): the state posted below sends the UI
// to the tier selection immediately, and a later cold launch reads setup_complete to
// decide between the library and the wizard. An asynchronous write is a race with both.
// commit(), not apply(): the state posted below sends the UI to the tier selection
// immediately, and an asynchronous write would race it.
//
// We are the first writer to set setup_complete false — the other four only ever set it
// true, which is why an abandoned install used to strand the user on an empty library.
// ADFA-5119 added a setup_complete → false here and called itself the first writer of
// false among five. ADFA-5137 removed the flag altogether, so there is nothing to unset:
// the marker cleared by teardown() and the absent rootfs now say the same thing between
// them, and they cannot disagree with each other the way the flag could disagree with both.
getSharedPreferences(getString(R.string.pref_file_internal), Context.MODE_PRIVATE)
.edit()
.remove("installed_tier")
.putBoolean(getString(R.string.pref_key_setup_complete), false)
.commit();
} catch (Exception e) {
// Never leave the UI waiting because a cleanup step failed: the state below is what
Expand All @@ -1602,7 +1604,7 @@ private void teardown() {
* no system, so forgetting that an install happened is exactly what let the
* app open an empty library. Success, cancellation and every module or reset
* path still pass true — a cancellation has already removed the residue and
* cleared setup_complete, so it needs no marker to be recovered from.
* removed the residue and left no rootfs, so it needs no marker to say so.
*/
private void teardown(boolean clearMarker) {
// ADFA-5119: nothing to wait for once this is over — neither the window nor a queued attempt.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.res.Configuration;
import android.os.Bundle;
import android.os.Handler;
Expand Down Expand Up @@ -40,6 +39,23 @@ public class LibraryActivity extends AppCompatActivity implements ServerControll
public static final String EXTRA_INSTALLING = "installing";
/** ADFA-4777: preselect a bottom-nav tab on launch (e.g. from the wizard's "Copy from a phone"). */
public static final String EXTRA_TAB = "tab";
/**
* ADFA-5137: the caller knows there is no system and is bringing the user here to get one.
*
* <p>Only the wizard's "Copy from a phone" sets it. That choice has to land on the Clone tab with
* nothing installed and nothing yet in flight, which is precisely the state that otherwise sends
* the user back to the wizard — so before this ticket the wizard wrote {@code setup_complete} to
* get past the check, and that lie is the entrance to findings 3 and 5.
*
* <p>An Intent extra rather than a stored fact — but its lifetime is the <b>task record</b>, not
* this navigation, and the difference is worth stating because a first draft of this comment got it
* wrong in both directions. Android replays the launching Intent when the process is killed and the
* task is restored, so the extra survives that; swiping the task away is what ends it. And
* {@code onNewIntent} calls {@code setIntent}, so a later arrival carrying no {@code settingUp}
* replaces it. Both outcomes are truthful — the user lands on Home, which since ADFA-5137 has a
* labelled way to install a system — but nobody should read this as "it dies when you navigate".
*/
public static final String EXTRA_SETTING_UP = "settingUp";
private boolean installing = false;

/** ADFA-4799: bottom bar (compact) and rail (medium/expanded) share the NavigationBarView
Expand Down Expand Up @@ -97,10 +113,18 @@ public class LibraryActivity extends AppCompatActivity implements ServerControll
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

// Not set up yet? Run the first-run wizard, then it routes back here.
SharedPreferences prefs0 = getSharedPreferences(
getString(R.string.pref_file_internal), MODE_PRIVATE);
if (!prefs0.getBoolean(getString(R.string.pref_key_setup_complete), false)) {
// ADFA-5137: nothing here and nothing coming? Run the first-run wizard, then it routes back.
//
// This used to read setup_complete, a flag written when an install STARTED and cleared by
// nobody — so it could say "set up" while the device had no system, and then this branch
// routed past the wizard forever. That pair is findings 3 and 5 of state-spine.svg. The
// question was never "did setup happen": it is "is there a system, or one on the way", and
// that is answerable from the disk and the two markers, none of which can drift from what
// they describe.
boolean broughtHereToSetUp = getIntent() != null
&& getIntent().getBooleanExtra(EXTRA_SETTING_UP, false);
if (!broughtHereToSetUp
&& !org.iiab.controller.system.data.SystemFactsReader.hereOrOnTheWay(this)) {
startActivity(new Intent(this, WizardActivity.class));
finish();
return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,13 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c
requireContext(), SetupProgressActivity.class));
return;
}
// ADFA-5137: with no system, the way forward is choosing one — the tier step, the same
// place the wizard sends people. Named, on the header, instead of a sentence pointing
// at a control at the bottom of the screen called "Get more".
if (headerState == H_NO_LIBRARY) {
openGetMore();
return;
}
// ADFA-4837: retry only when it is genuinely safe — canStartServer guards
// against stacking a second proot over a live one.
if (headerState != H_FAILED) return;
Expand Down Expand Up @@ -242,6 +249,26 @@ private View makeGetMoreCell(int cardH) {
}

private void openGetMore() {
// ADFA-5137 (review): refuse while a deep operation owns the environment, and refuse HERE so
// both entrances are covered — the footer control and the header button this ticket added.
//
// The hole is not theoretical and the header made it one tap wide. isSystemInstalled() is false
// for the whole time an install marker is set, and a clone-receive holds both the marker and
// the lock — so during a live receive the header reads "no library" and this method would take
// the Step-1 branch. That branch starts an install with reinstall=false, InstallService's
// non-destructive guard sees the half-received rootfs directory, skips the extract and reports
// success, and its teardown clears the marker that a killed receive needs for recovery. That is
// exactly the "boot the wreck" failure InstallService's own cleanup comment warns about,
// reached from a button labelled as a way out.
//
// ownerHeld, not isHeld: a live content download holds no owner marker and must not block this
// (ADFA-4957 draws the same line for the server toggle).
if (org.iiab.controller.env.EnvironmentLock.ownerHeld(requireContext())) {
if (getView() != null) {
org.iiab.controller.util.Snackbars.make(getView(), R.string.k2go_install_busy).show();
}
return;
}
// If a system is already installed, skip the destructive system step and go straight
// to content (Step 2). Otherwise run the full setup from Step 1.
Intent i = new Intent(requireContext(), SetupLibraryActivity.class);
Expand Down Expand Up @@ -572,8 +599,19 @@ private void setHeader(int h) {
// app working and gets a spinner; the rest are statements and get nothing. The status
// colour lives on the dot only — the button wears the brand colour, because it is a
// control rather than a severity.
// ADFA-5137: H_NO_LIBRARY gets one too. It was the only state here that offered nothing, and
// that is finding 5 of state-spine.svg: the header said "tap Get more to install" while being
// plain text, pointing at a control at the far bottom of the screen whose name says content
// rather than system. Meanwhile both cards on the way there offer Install and Schedule, and
// both refuse.
//
// ADFA-5137 also closes the way INTO this state, so in principle nobody arrives here any more.
// The button stays anyway, because "in principle" is what the last four dead ends had in
// common: a state with no exit is a bug whoever reaches it, including by a route that does not
// exist yet. One line in a switch that already hands out two other buttons.
int action = h == H_FAILED ? R.string.k2go_home_retry
: h == H_INSTALLING ? R.string.k2go_home_see_progress : 0;
: h == H_INSTALLING ? R.string.k2go_home_see_progress
: h == H_NO_LIBRARY ? R.string.k2go_home_install_system : 0;
if (homeStatusAction != null) {
homeStatusAction.setVisibility(action != 0 ? View.VISIBLE : View.GONE);
if (action != 0) homeStatusAction.setText(action);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,12 @@ public class ModuleHubFragment extends Fragment {
* with. The screen still listed all six as installable, because it decided that from a probe
* — and with no system nothing answers, so everything looked missing.
*
* <p>Reachable, not theoretical: {@code pref_key_setup_complete} is only ever written true —
* five writers, none of them clears it — so after a reset or a failed restore the app still
* routes to the Library, and Settings still opens this screen over an empty rootfs.
* <p>Reachable, not theoretical — and it stays reachable after ADFA-5137. That ticket removed the
* flag this note used to blame ({@code setup_complete}, written true by four sites and cleared by
* none), so a device that simply has no system now opens the wizard instead of the Library. What
* still lands here is the case where a marker is held: a failed restore keeps it, so the launch
* treats the device as having something on the way, reaches the Library, and this screen can be
* opened over a rootfs that is empty or half-written. The precondition still has to be asked here.
*
* <p>Seeded true so the first frame looks like the ordinary case; the background pass
* corrects it before anything is offered.
Expand Down
Loading