3636
3737import org .appdevforall .k2go .R ;
3838import org .appdevforall .k2go .dashboard .domain .DashboardCardState ;
39- import org .appdevforall .k2go .dashboard .domain .RebuildPhase ;
40- import org .appdevforall .k2go .dashboard .domain .RebuildProgress ;
39+ import org .appdevforall .k2go .install .presentation .EtaText ;
4140import org .appdevforall .k2go .util .AppExecutors ;
4241
4342public class DashboardDetailFragment extends Fragment {
@@ -55,16 +54,16 @@ public class DashboardDetailFragment extends Fragment {
5554 // ADFA-5339 / K2GO-374: expandable Details — the live rebuild log via the shared LiveLogPanel. The
5655 // toggle stays hidden until there are lines (an older box without /rebuild/log shows no Details).
5756 private org .appdevforall .k2go .widget .LiveLogPanel logPanel ;
58- private static final long LOG_POLL_MS = 1500L ;
59- private final Runnable logPoll = this ::pollLog ;
60- // K2GO-95 (Phase 2): the in-progress bar is determinate, driven by RebuildProgress from the polled
61- // log. We keep the current phase and when it began (a monotonic clock — the silent native-build
62- // stretch carries no log timestamp) so the bar interpolates within a phase and the next real marker
63- // snaps it forward. Indeterminate only until the first marker (NONE); the snap to 100 is the
64- // service's completion broadcast, not this poll.
57+ // K2GO-95 / K2GO-383: the determinate in-progress bar + its "NN% ~N min left" caption. The values
58+ // come from DashboardRebuildService (the persistent owner, which outlives this card's recreation) via
59+ // its ACTION_PROGRESS broadcast, so the bar resumes where the rebuild actually is after minimize /
60+ // restore instead of restarting from 0.
6561 private LinearProgressIndicator progressBar ;
66- private RebuildPhase progressPhase = RebuildPhase .NONE ;
67- private long progressPhaseStartMs ;
62+ // K2GO-383: the status line under the label is two equal-weight columns, the percent and the ETA each
63+ // centered in its own half. Separate columns, so a width change in one never reaches the other. Style
64+ // matches the content updaters' status line (BodySmall, muted, single line).
65+ private TextView updatingPercent ;
66+ private TextView updatingEta ;
6867
6968 /** ADFA-5333: the live update runs in the background (DashboardRebuildService), which broadcasts each
7069 * state change. While this card is on screen we show/hide an in-progress bar and, on done, refresh
@@ -73,6 +72,15 @@ public class DashboardDetailFragment extends Fragment {
7372 private final BroadcastReceiver rebuildState = new BroadcastReceiver () {
7473 @ Override public void onReceive (Context c , Intent i ) {
7574 if (!isAdded ()) return ;
75+ if (DashboardRebuildService .ACTION_PROGRESS .equals (i .getAction ())) {
76+ onProgress (i .getIntExtra (DashboardRebuildService .EXTRA_PERCENT , -1 ),
77+ i .getLongExtra (DashboardRebuildService .EXTRA_ETA_SECONDS , -1L ));
78+ // K2GO-383: the same tick carries the log tail; feed the Details panel from it instead of
79+ // a second /rebuild/log poll. reveal + auto-scroll are handled inside setContent.
80+ String log = i .getStringExtra (DashboardRebuildService .EXTRA_LOG );
81+ if (updating && logPanel != null && log != null ) logPanel .setContent (log );
82+ return ;
83+ }
7684 String state = i .getStringExtra (DashboardRebuildService .EXTRA_STATE );
7785 if (DashboardRebuildService .STATE_RUNNING .equals (state )) {
7886 setUpdating (true );
@@ -142,6 +150,7 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c
142150 public void onStart () {
143151 super .onStart ();
144152 IntentFilter f = new IntentFilter (DashboardRebuildService .ACTION_STATE );
153+ f .addAction (DashboardRebuildService .ACTION_PROGRESS ); // K2GO-383: per-tick percent + eta
145154 ContextCompat .registerReceiver (requireContext (), rebuildState , f ,
146155 ContextCompat .RECEIVER_NOT_EXPORTED );
147156 resolveInitialUpdatingState ();
@@ -277,6 +286,36 @@ private View buildUpdatingRow(Button rebuildBtn) {
277286 label .setTextColor (ContextCompat .getColor (requireContext (), R .color .k2go_muted ));
278287 row .addView (label );
279288
289+ // K2GO-383: the status line is two equal columns (weight 1 each), the percent and the ETA each
290+ // centered in its own half. Separate columns, so a width change in one never reaches the other;
291+ // centering only reflows the value slightly around its own column center. Same BodySmall / muted /
292+ // single-line style as the content updaters' status line.
293+ LinearLayout statusRow = new LinearLayout (requireContext ());
294+ statusRow .setOrientation (LinearLayout .HORIZONTAL );
295+ LinearLayout .LayoutParams statusLp = new LinearLayout .LayoutParams (
296+ ViewGroup .LayoutParams .MATCH_PARENT , ViewGroup .LayoutParams .WRAP_CONTENT );
297+ statusLp .topMargin = Math .round (2 * d );
298+ statusRow .setLayoutParams (statusLp );
299+
300+ updatingPercent = new TextView (requireContext ());
301+ updatingPercent .setTextAppearance (com .google .android .material .R .style .TextAppearance_Material3_BodySmall );
302+ updatingPercent .setTextColor (ContextCompat .getColor (requireContext (), R .color .k2go_muted ));
303+ updatingPercent .setMaxLines (1 );
304+ updatingPercent .setGravity (android .view .Gravity .CENTER );
305+ statusRow .addView (updatingPercent , new LinearLayout .LayoutParams (
306+ 0 , ViewGroup .LayoutParams .WRAP_CONTENT , 1f ));
307+
308+ updatingEta = new TextView (requireContext ());
309+ updatingEta .setTextAppearance (com .google .android .material .R .style .TextAppearance_Material3_BodySmall );
310+ updatingEta .setTextColor (ContextCompat .getColor (requireContext (), R .color .k2go_muted ));
311+ updatingEta .setMaxLines (1 );
312+ updatingEta .setEllipsize (android .text .TextUtils .TruncateAt .END );
313+ updatingEta .setGravity (android .view .Gravity .CENTER );
314+ statusRow .addView (updatingEta , new LinearLayout .LayoutParams (
315+ 0 , ViewGroup .LayoutParams .WRAP_CONTENT , 1f ));
316+
317+ row .addView (statusRow );
318+
280319 // The bar and Cancel sit on one line: bar takes the width, Cancel is right beside it.
281320 LinearLayout line = new LinearLayout (requireContext ());
282321 line .setOrientation (LinearLayout .HORIZONTAL );
@@ -351,61 +390,37 @@ private void setUpdating(boolean on) {
351390 if (updatingCancel != null ) updatingCancel .setEnabled (on ); // re-enable when a new update shows
352391 if (rebuild != null ) { rebuild .setEnabled (!on ); rebuild .setAlpha (on ? 0.5f : 1f ); }
353392 if (on && rebuildHint != null ) rebuildHint .setVisibility (View .GONE );
354- // ADFA-5339: the Details log only exists while a rebuild runs. Poll it on, tear it down on off.
355- main .removeCallbacks (logPoll );
356393 if (on ) {
357- // K2GO-95: a fresh run starts indeterminate until the first marker; pollLog then drives it.
358- progressPhase = RebuildPhase . NONE ;
394+ // K2GO-383: start indeterminate with the plain label; the service's ACTION_PROGRESS then
395+ // drives the bar, the "NN% ~N min left" caption, and (same tick) the Details panel.
359396 if (progressBar != null ) progressBar .setIndeterminate (true );
397+ if (updatingPercent != null ) updatingPercent .setText ("" ); // filled by the first progress tick
398+ if (updatingEta != null ) updatingEta .setText ("" );
360399 if (logPanel != null ) logPanel .reset (); // K2GO-374: start clean; toggle hidden until lines
361- main .post (logPoll );
362400 }
363401 // On "off" the whole updatingRow is hidden above, which takes the panel with it.
364402 }
365403
366- /** ADFA-5339: poll the rebuild log tail while updating. Fork B — the toggle appears only once there
367- * are lines, so an older box without the endpoint (empty) shows no Details affordance. Reschedules
368- * itself while {@code updating}; setUpdating(false) and onDestroyView remove the callback. */
369- private void pollLog () {
370- if (!isAdded () || !updating ) return ;
371- DashboardClient .rebuildLog (new DashboardClient .RebuildLogCb () {
372- @ Override public void onLines (java .util .List <String > lines ) {
373- if (!isAdded () || !updating ) return ;
374- String log = android .text .TextUtils .join ("\n " , lines );
375- updateProgressBar (log );
376- if (logPanel != null ) logPanel .setContent (log ); // K2GO-374: reveal + auto-scroll handled here
377- main .postDelayed (logPoll , LOG_POLL_MS );
378- }
379- @ Override public void onErr (String message ) {
380- // No endpoint / transient: keep the panel as-is (toggle hidden if never populated) and retry.
381- if (isAdded () && updating ) main .postDelayed (logPoll , LOG_POLL_MS );
382- }
383- });
384- }
385-
386- /** K2GO-95 (Phase 2): drive the determinate bar from the polled log. The phase comes from the log's
387- * markers ({@link RebuildProgress#phaseOf}); time within a phase comes from the monotonic clock kept
388- * here, so the silent native-build stretch (no log timestamp) still advances. Indeterminate until the
389- * first marker; the final snap to 100 is the service's completion broadcast, not this poll. */
390- private void updateProgressBar (String log ) {
391- if (progressBar == null ) return ;
392- RebuildPhase phase = RebuildProgress .phaseOf (log );
393- if (phase != progressPhase ) {
394- progressPhase = phase ;
395- progressPhaseStartMs = android .os .SystemClock .elapsedRealtime ();
396- }
397- if (phase == RebuildPhase .NONE ) {
404+ /** K2GO-383: apply a progress tick from DashboardRebuildService (the persistent owner, so the value
405+ * survives this card's recreation). A negative percent means "no phase marker yet" -> indeterminate;
406+ * otherwise set the determinate bar and the two status columns: "NN%" left, the ETA right (eta via
407+ * the shared, already-localized EtaText). */
408+ private void onProgress (int percent , long etaSeconds ) {
409+ if (!updating || progressBar == null ) return ;
410+ if (percent < 0 ) {
398411 if (!progressBar .isIndeterminate ()) progressBar .setIndeterminate (true );
412+ if (updatingPercent != null ) updatingPercent .setText ("" ); // no marker yet; the label stands alone
413+ if (updatingEta != null ) updatingEta .setText ("" );
399414 return ;
400415 }
401- long elapsed = android .os .SystemClock .elapsedRealtime () - progressPhaseStartMs ;
402- int pct = RebuildProgress .percentFor (phase , elapsed );
403416 if (progressBar .isIndeterminate ()) progressBar .setIndeterminate (false );
404- progressBar .setProgressCompat (pct , true ); // animated determinate step
417+ progressBar .setProgressCompat (percent , true ); // animated determinate step
418+ if (updatingPercent != null ) updatingPercent .setText (percent + "%" );
419+ if (updatingEta != null ) updatingEta .setText (EtaText .of (requireContext (), etaSeconds ));
405420 }
406421
407422 @ Override public void onDestroyView () {
408- main .removeCallbacks ( logPoll ); // ADFA-5339: never poll past the view's life
423+ main .removeCallbacksAndMessages ( null ); // ADFA-5339: drop any pending posts past the view's life
409424 super .onDestroyView ();
410425 }
411426
0 commit comments