Skip to content

Commit 63b765f

Browse files
Merge pull request #633 from appdevforall/fix/K2GO-383-rebuild-progress-persist
K2GO-383 fix(ui): dashboard rebuild progress persists, shows % and ETA
2 parents 29c2709 + df4ad24 commit 63b765f

5 files changed

Lines changed: 190 additions & 57 deletions

File tree

‎controller/app/src/main/java/org/appdevforall/k2go/dashboard/domain/RebuildProgress.java‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,4 +87,22 @@ public static int percentFor(RebuildPhase phase, long elapsedInPhaseMs) {
8787
int filled = Math.min(width - 1, (int) Math.floor(width * fraction));
8888
return phase.startPercent + filled;
8989
}
90+
91+
/**
92+
* K2GO-383: a rough ETA to reach 100, in seconds, from the static model: the time left in the
93+
* current phase (its median minus how long we have been in it, floored at 0) plus the full median
94+
* of every later phase. It is an estimate, like the bar itself, not a live fact. {@link
95+
* RebuildPhase#NONE} (no marker yet) returns -1 so the caller can hide the ETA until it is known.
96+
*/
97+
public static long etaSecondsFor(RebuildPhase phase, long elapsedInPhaseMs) {
98+
if (phase == null || phase == RebuildPhase.NONE) {
99+
return -1L;
100+
}
101+
long remainingMs = Math.max(0L, phase.medianMs - Math.max(0L, elapsedInPhaseMs));
102+
RebuildPhase[] phases = RebuildPhase.values();
103+
for (int i = phase.ordinal() + 1; i < phases.length; i++) {
104+
remainingMs += phases[i].medianMs;
105+
}
106+
return (remainingMs + 999L) / 1000L; // ceil to whole seconds
107+
}
90108
}

‎controller/app/src/main/java/org/appdevforall/k2go/install/presentation/EtaText.java‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,13 @@ public static String of(Context ctx, long etaSeconds) {
2323
default: return ""; // UNKNOWN -> blank slot
2424
}
2525
}
26+
27+
/** K2GO-383: the one-line progress caption, "NN%" or "NN% ~N min left" (the eta is appended only when
28+
* known). {@code percent} must be >= 0; the caller owns the indeterminate (percent &lt; 0) case. Used
29+
* by the rebuild notification, which needs the percent and eta in a single string; the in-app card
30+
* renders them as two columns instead, built from {@link #of} plus its own "NN%". */
31+
public static String percentAndEta(Context ctx, int percent, long etaSeconds) {
32+
String eta = of(ctx, etaSeconds);
33+
return eta.isEmpty() ? percent + "%" : percent + "% " + eta;
34+
}
2635
}

‎controller/app/src/main/java/org/appdevforall/k2go/redesign/DashboardDetailFragment.java‎

Lines changed: 67 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,7 @@
3636

3737
import org.appdevforall.k2go.R;
3838
import 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;
4140
import org.appdevforall.k2go.util.AppExecutors;
4241

4342
public 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

Comments
 (0)