3333import androidx .core .content .ContextCompat ;
3434
3535import org .iiab .controller .R ;
36+ import org .iiab .controller .install .presentation .ModuleQueueRepository ;
37+ import org .iiab .controller .install .presentation .ModuleQueueState ;
3638import org .iiab .controller .util .AppExecutors ;
3739
3840public class SetupProgressActivity extends AppCompatActivity {
@@ -56,6 +58,7 @@ public class SetupProgressActivity extends AppCompatActivity {
5658 private boolean redirectScheduled = false ;
5759 private boolean showingDetail = false ;
5860 private boolean probing = false ;
61+ private boolean mapsLaunched = false ; // ADFA-4900: maps (proot) stage has been handed to the queue
5962 private int readyPolls = 0 ; // ADFA-4874: failed readiness polls so far (slow-start message)
6063
6164 private int px (int dp ) { return Math .round (dp * getResources ().getDisplayMetrics ().density ); }
@@ -115,35 +118,66 @@ public void onBackPressed() {
115118 else super .onBackPressed ();
116119 }
117120
118- // ---- readiness gate: wait for the REST engine, then drain the wishlists ----
121+ // ---- readiness gate + serialized install pipeline (ADFA-4900) ----
122+ // Once the REST engine is up, run the install tasks as an ORDERED, serialized pipeline:
123+ // maps (proot / runrole) exclusively first, then ZIM, then Books, auto-continuing between
124+ // stages HERE (never dropping to Home/Library mid-sequence). Keep polling until every stage
125+ // has been started and finished; render() then auto-advances (or shows Finish on failure).
119126 private final Runnable readyPoll = new Runnable () {
120127 @ Override public void run () {
121- if (probing || servicesReady ) return ;
128+ if (probing ) return ;
122129 probing = true ;
123130 AppExecutors .get ().io ().execute (() -> {
124131 final boolean ready = RestReadiness .apiReady ();
125132 main .post (() -> {
126133 probing = false ;
127134 if (isFinishing ()) return ;
128- if (ready ) {
129- servicesReady = true ;
130- if (!drained ) {
131- if (BooksProvisioner .hasPending (SetupProgressActivity .this )) BooksProvisioner .drain (SetupProgressActivity .this );
132- if (ZimProvisioner .hasPending (SetupProgressActivity .this )) ZimProvisioner .drain (SetupProgressActivity .this );
133- if (MapsProvisioner .hasPending (SetupProgressActivity .this )) MapsProvisioner .drain (SetupProgressActivity .this ); // ADFA-4900
134- drained = true ;
135- }
136- render ();
137- } else {
135+ if (!ready ) {
138136 readyPolls ++; // ADFA-4874: feeds the slow-start message in render()
139137 render ();
140138 main .postDelayed (readyPoll , READY_POLL_MS );
139+ return ;
141140 }
141+ servicesReady = true ;
142+ boolean moreWork = orchestrateStep ();
143+ render ();
144+ if (moreWork ) main .postDelayed (readyPoll , READY_POLL_MS );
142145 });
143146 });
144147 }
145148 };
146149
150+ /**
151+ * ADFA-4900: one step of the serialized install pipeline. Starts the next stage only when the
152+ * previous one has finished; proot (maps) runs exclusively before any REST download, so Ansible's
153+ * background forks never overlap a live REST job. Returns true while work remains (keep polling),
154+ * false once every stage has been started and is complete.
155+ */
156+ private boolean orchestrateStep () {
157+ ModuleQueueState mq = ModuleQueueRepository .get ().current ();
158+ boolean queueRunning = ModuleQueueRepository .get ().isRunning ();
159+
160+ // Stage 1 — maps (proot), exclusive of all REST work.
161+ if (MapsProvisioner .hasPending (this )) {
162+ if (!queueRunning ) { MapsProvisioner .drain (this ); mapsLaunched = true ; }
163+ return true ;
164+ }
165+ if (queueRunning ) return true ; // maps runrole in flight
166+ if (mapsLaunched && mq .phase != ModuleQueueState .Phase .DONE ) return true ; // launched; await DONE (race guard)
167+
168+ // Stage 2 — ZIM (REST), only after maps is done.
169+ if (ZimDownloadService .hasSession () && !ZimDownloadService .isComplete ()) return true ;
170+ if (ZimProvisioner .hasPending (this )) { ZimProvisioner .drain (this ); return true ; }
171+
172+ // Stage 3 — Books (REST), only after ZIM is done.
173+ if (BooksDownloadService .hasSession () && !BooksDownloadService .isComplete ()) return true ;
174+ if (BooksProvisioner .hasPending (this )) { BooksProvisioner .drain (this ); return true ; }
175+
176+ // Every stage has been started and is complete.
177+ drained = true ;
178+ return false ;
179+ }
180+
147181 // ---- render ----
148182 private void render () {
149183 if (sections == null || showingDetail ) return ;
@@ -165,8 +199,12 @@ private void render() {
165199 boolean allComplete = drained
166200 && (!ZimDownloadService .hasSession () || ZimDownloadService .isComplete ())
167201 && (!BooksDownloadService .hasSession () || BooksDownloadService .isComplete ());
202+ // ADFA-4900: a failed maps runrole must count as a failure too (Finish, not a false success).
203+ ModuleQueueState mq = ModuleQueueRepository .get ().current ();
204+ boolean mapsFailed = mq .phase == ModuleQueueState .Phase .DONE && mq .failedModules .contains ("maps" );
168205 int failedTotal = failedCount (ZimDownloadService .hasSession () ? ZimDownloadService .status () : null , ZimDownloadService .FAILED )
169- + failedCount (BooksDownloadService .hasSession () ? BooksDownloadService .status () : null , BooksDownloadService .FAILED );
206+ + failedCount (BooksDownloadService .hasSession () ? BooksDownloadService .status () : null , BooksDownloadService .FAILED )
207+ + (mapsFailed ? 1 : 0 );
170208
171209 // Status dot + line. While waiting, a long-stuck engine shows a softer "taking longer"
172210 // message instead of "Starting services" so it doesn't look frozen (ADFA-4874).
0 commit comments