33 * Name : DashboardRebuild.java
44 * Author : AppDevForAll
55 * Copyright : Copyright (c) 2026 AppDevForAll
6- * Description : ADFA-5011. Single entry point for starting a dash-node REST-core rebuild, shared by
7- * the Module-management hub row and the dashboard detail card so both offer the exact
8- * same gated flow: busy check (EnvironmentLock) -> internet check -> confirm dialog ->
9- * start the guarded InstallService rebuild + open the progress screen (which now stays
10- * put until the rebuild reaches SUCCESS/FAILED).
6+ * Description : ADFA-5011 / ADFA-5051. Single entry point for starting a dash-node REST-core rebuild,
7+ * shared by the Module-management hub row and the dashboard detail card. Gated flow:
8+ * busy check (EnvironmentLock) -> internet check -> confirm dialog -> start.
9+ *
10+ * ADFA-5051 — version-gated cutover: from dash-node 1.2.0 the core updates ITSELF LIVE
11+ * over REST (POST /system/dashboard/rebuild: the blue-green rebuild that stages, smoke-
12+ * tests, atomically swaps and rolls back on failure — no rootfs/proot). Installs still
13+ * on < 1.2.0 predate that, so they take the heavier proot rebuild (InstallService) once
14+ * to reach 1.2.0; from there every later update is the live REST path.
1115 * ============================================================================
1216 */
1317package org .iiab .controller .redesign ;
1418
1519import android .content .Context ;
1620import android .content .Intent ;
1721import android .os .Build ;
22+ import android .os .Handler ;
23+ import android .os .Looper ;
24+ import android .view .Gravity ;
1825import android .view .View ;
26+ import android .widget .LinearLayout ;
27+ import android .widget .TextView ;
1928
2029import androidx .annotation .NonNull ;
30+ import androidx .annotation .Nullable ;
31+ import androidx .appcompat .app .AlertDialog ;
2132import androidx .fragment .app .Fragment ;
2233
2334import com .google .android .material .dialog .MaterialAlertDialogBuilder ;
35+ import com .google .android .material .progressindicator .CircularProgressIndicator ;
2436
2537import org .iiab .controller .R ;
2638import org .iiab .controller .install .presentation .InstallService ;
39+ import org .iiab .controller .util .AppExecutors ;
2740import org .iiab .controller .util .Snackbars ;
2841
2942public final class DashboardRebuild {
3043 private DashboardRebuild () {}
3144
32- /** Gate then confirm then start. {@code anchor} is where a "busy"/"no internet" snackbar shows. */
33- public static void confirmAndStart (@ NonNull Fragment host , @ NonNull View anchor ) {
45+ // Live-rebuild poll cadence + overall cap. The blue-green rebuild does yarn install/build + smoke
46+ // test + a dash-node restart, so allow a generous ceiling; the restart briefly makes the status
47+ // endpoint unreachable, which we treat as "keep polling", not a failure.
48+ private static final long POLL_MS = 2500L ;
49+ private static final int MAX_POLLS = 160 ; // ~6.5 min
50+
51+ /** Gate then confirm then start. {@code anchor} is where a "busy"/"no internet" snackbar shows.
52+ * {@code onLiveUpdated} (nullable) runs after a successful LIVE (REST) update so the caller can
53+ * refresh its version chip / update pill in place (ADFA-5051). */
54+ public static void confirmAndStart (@ NonNull Fragment host , @ NonNull View anchor ,
55+ @ Nullable Runnable onLiveUpdated ) {
3456 Context ctx = host .requireContext ();
3557 if (org .iiab .controller .env .EnvironmentLock .isHeld (ctx )) {
3658 Snackbars .make (anchor , R .string .k2go_install_busy ).show ();
@@ -44,13 +66,29 @@ public static void confirmAndStart(@NonNull Fragment host, @NonNull View anchor)
4466 .setTitle (R .string .k2go_dash_rebuild_confirm_title )
4567 .setMessage (R .string .k2go_dash_rebuild_confirm_msg )
4668 .setNegativeButton (android .R .string .cancel , null )
47- .setPositiveButton (R .string .k2go_dash_rebuild , (d , w ) -> start (host ))
69+ .setPositiveButton (R .string .k2go_dash_rebuild , (d , w ) -> start (host , anchor , onLiveUpdated ))
4870 .show ();
4971 }
5072
51- /** Kick the foreground rebuild service and open the guarded progress screen (flagged as a rebuild
52- * so it stays on the animation and blocks leaving until the rebuild finishes). */
53- private static void start (@ NonNull Fragment host ) {
73+ /** ADFA-5051: route by the installed dash-node version. >= 1.2.0 updates live over REST; older
74+ * installs take the proot rebuild once as a bridge to 1.2.0. The version read hits disk, so it
75+ * runs off the main thread; the routing itself is posted back to the UI. */
76+ private static void start (@ NonNull Fragment host , @ NonNull View anchor , @ Nullable Runnable onLiveUpdated ) {
77+ final Context app = host .requireContext ().getApplicationContext ();
78+ final Handler main = new Handler (Looper .getMainLooper ());
79+ AppExecutors .get ().io ().execute (() -> {
80+ final boolean rest = DashboardVersion .atLeast (DashboardVersion .installed (app ), 1 , 2 , 0 );
81+ main .post (() -> {
82+ if (!host .isAdded ()) return ;
83+ if (rest ) startRest (host , anchor , onLiveUpdated );
84+ else startProot (host );
85+ });
86+ });
87+ }
88+
89+ /** Kick the foreground proot rebuild service and open the guarded progress screen (flagged as a
90+ * rebuild so it stays on the animation and blocks leaving until the rebuild finishes). */
91+ private static void startProot (@ NonNull Fragment host ) {
5492 Context ctx = host .requireContext ();
5593 Intent svc = new Intent (ctx , InstallService .class )
5694 .setAction (InstallService .ACTION_REBUILD_DASHBOARD );
@@ -60,6 +98,88 @@ private static void start(@NonNull Fragment host) {
6098 .putExtra (SetupProgressActivity .EXTRA_REBUILD , true ));
6199 }
62100
101+ /** ADFA-5051: live REST update. Trigger the in-server rebuild, then show a non-cancelable progress
102+ * dialog that polls the state until done/error (tolerating the brief restart window). */
103+ private static void startRest (@ NonNull Fragment host , @ NonNull View anchor , @ Nullable Runnable onLiveUpdated ) {
104+ Context ctx = host .requireContext ();
105+ LinearLayout box = new LinearLayout (ctx );
106+ box .setOrientation (LinearLayout .HORIZONTAL );
107+ box .setGravity (Gravity .CENTER_VERTICAL );
108+ int pad = Math .round (24 * ctx .getResources ().getDisplayMetrics ().density );
109+ box .setPadding (pad , pad , pad , pad );
110+ CircularProgressIndicator spin = new CircularProgressIndicator (ctx );
111+ spin .setIndeterminate (true );
112+ box .addView (spin );
113+ TextView msg = new TextView (ctx );
114+ msg .setText (R .string .k2go_dash_live_running );
115+ LinearLayout .LayoutParams mlp = new LinearLayout .LayoutParams (-2 , -2 );
116+ mlp .leftMargin = pad ;
117+ msg .setLayoutParams (mlp );
118+ box .addView (msg );
119+
120+ final AlertDialog dialog = new MaterialAlertDialogBuilder (ctx )
121+ .setTitle (R .string .k2go_dash_live_title )
122+ .setView (box )
123+ .setCancelable (false )
124+ .show ();
125+
126+ final Handler poller = new Handler (Looper .getMainLooper ());
127+ DashboardClient .rebuildStart (new DashboardClient .RebuildStartCb () {
128+ @ Override public void onStarted (boolean alreadyRunning ) {
129+ if (!host .isAdded ()) { safeDismiss (dialog ); return ; }
130+ pollStatus (host , dialog , poller , new int []{0 }, onLiveUpdated );
131+ }
132+ @ Override public void onErr (String message ) {
133+ safeDismiss (dialog );
134+ if (host .isAdded ()) Snackbars .make (anchor , R .string .k2go_dash_live_start_failed ).show ();
135+ }
136+ });
137+ }
138+
139+ /** Poll rebuild state until a terminal one, reusing a single {@code poller} Handler. A status error
140+ * is transient (dash-node restarts mid-swap) so we keep polling until MAX_POLLS. Hitting the cap is
141+ * NOT a failure: the rebuild runs detached server-side and may still finish, so we show a distinct
142+ * "still working in the background" message rather than the rollback/failure one. */
143+ private static void pollStatus (@ NonNull Fragment host , @ NonNull AlertDialog dialog ,
144+ @ NonNull Handler poller , int [] tries , @ Nullable Runnable onLiveUpdated ) {
145+ if (!host .isAdded ()) { safeDismiss (dialog ); return ; }
146+ if (tries [0 ]++ >= MAX_POLLS ) { finishRest (host , dialog , R .string .k2go_dash_live_timeout , onLiveUpdated ); return ; }
147+ DashboardClient .rebuildStatus (new DashboardClient .RebuildStatusCb () {
148+ @ Override public void onState (String state ) {
149+ if (!host .isAdded ()) { safeDismiss (dialog ); return ; }
150+ if ("done" .equals (state )) { finishRest (host , dialog , R .string .k2go_dash_live_done , onLiveUpdated ); return ; }
151+ if ("error" .equals (state )) { finishRest (host , dialog , R .string .k2go_dash_live_error , onLiveUpdated ); return ; }
152+ schedule ();
153+ }
154+ @ Override public void onErr (String message ) {
155+ if (!host .isAdded ()) { safeDismiss (dialog ); return ; }
156+ schedule (); // API likely restarting mid-swap; keep waiting
157+ }
158+ private void schedule () {
159+ poller .postDelayed (() -> pollStatus (host , dialog , poller , tries , onLiveUpdated ), POLL_MS );
160+ }
161+ });
162+ }
163+
164+ /** Swap the progress dialog for a simple result dialog carrying {@code msgRes} (done/error/timeout).
165+ * On success, fire {@code onLiveUpdated} (ADFA-5051) so the caller refreshes its version/pill in
166+ * place — otherwise the card keeps showing "update available" and users re-tap Rebuild. */
167+ private static void finishRest (@ NonNull Fragment host , @ NonNull AlertDialog dialog , int msgRes ,
168+ @ Nullable Runnable onLiveUpdated ) {
169+ safeDismiss (dialog );
170+ if (!host .isAdded ()) return ;
171+ if (msgRes == R .string .k2go_dash_live_done && onLiveUpdated != null ) onLiveUpdated .run ();
172+ new MaterialAlertDialogBuilder (host .requireContext ())
173+ .setTitle (R .string .k2go_dash_live_title )
174+ .setMessage (msgRes )
175+ .setPositiveButton (android .R .string .ok , null )
176+ .show ();
177+ }
178+
179+ private static void safeDismiss (@ NonNull AlertDialog d ) {
180+ try { if (d .isShowing ()) d .dismiss (); } catch (Exception ignore ) { /* activity gone */ }
181+ }
182+
63183 /** True when the device reports an internet-capable active network. Unknown -> true (let the
64184 * preflight decide), matching the previous inline check in ModuleHubFragment. */
65185 public static boolean hasInternet (@ NonNull Context ctx ) {
0 commit comments