From 749f624bd57806dc2379ab522b963aaf3f6bee97 Mon Sep 17 00:00:00 2001 From: Luis Guzman Date: Sat, 3 Oct 2026 04:36:19 -0600 Subject: [PATCH 1/3] K2GO-448 feat(restore): show stage index on the multi-stage progress bar Restore runs three passes (copy, verify, extract) on one unified bar, but each pass reports its own ETA, so the resetting time read as the whole run slowing down. Prefix each stage label with its position via ExtractProgress.stepped: "[1/3] Copying the file", "[2/3] Checking the file", "[3/3] Restoring". Pure helper, unit-tested; backup (single pass) is unchanged. Label only, no change to unifiedPercent or ETA. --- .../k2go/deepop/DeepOpService.java | 14 +++++++---- .../k2go/deploy/domain/ExtractProgress.java | 21 +++++++++++++++++ .../deploy/domain/ExtractProgressTest.java | 23 +++++++++++++++++++ 3 files changed, 53 insertions(+), 5 deletions(-) diff --git a/controller/app/src/main/java/org/appdevforall/k2go/deepop/DeepOpService.java b/controller/app/src/main/java/org/appdevforall/k2go/deepop/DeepOpService.java index 40c157183..be05a2371 100644 --- a/controller/app/src/main/java/org/appdevforall/k2go/deepop/DeepOpService.java +++ b/controller/app/src/main/java/org/appdevforall/k2go/deepop/DeepOpService.java @@ -388,7 +388,8 @@ private String stageArchive(Uri src, File temp, long size) { .perSecond(copied, now - startMs); final long eta = org.appdevforall.k2go.deploy.domain.ExtractProgress .etaSeconds(copied, size, rate); - main.post(() -> setStep(getString(R.string.k2go_br_status_copying), pct, eta)); + main.post(() -> setStep(org.appdevforall.k2go.deploy.domain.ExtractProgress + .stepped(COPY_PASS, RESTORE_PASSES, getString(R.string.k2go_br_status_copying)), pct, eta)); } } out.flush(); @@ -435,7 +436,8 @@ private void attemptVerifyAndExtract(final String path) { if (done || passRunning) return; // K2GO-384: one verify+extract pass at a time (service is the owner) passRunning = true; currentCancelKind = DeepOpState.CancelKind.CANCELLABLE; // K2GO-384: verify -- cancellable (kill + hold) - setStep(getString(R.string.k2go_br_status_checking), 0); + setStep(org.appdevforall.k2go.deploy.domain.ExtractProgress + .stepped(VERIFY_PASS, RESTORE_PASSES, getString(R.string.k2go_br_status_checking)), 0); final File destParent = new File(getFilesDir(), "rootfs"); new TarExtractor().startExtraction(this, path, destParent.getAbsolutePath(), true, cancelBeforeExtract, pauseRequested, forceExtractCancel, @@ -500,9 +502,11 @@ public void onExtractPhase(TarExtractor.Phase phase, int passPercent, final int unified = org.appdevforall.k2go.deploy.domain.ExtractProgress .unifiedPercent(passPercent, extracting ? EXTRACT_PASS : VERIFY_PASS, RESTORE_PASSES); - final String label = getString(extracting - ? R.string.k2go_br_status_restoring - : R.string.k2go_br_status_checking); + final String label = org.appdevforall.k2go.deploy.domain.ExtractProgress.stepped( + extracting ? EXTRACT_PASS : VERIFY_PASS, RESTORE_PASSES, + getString(extracting + ? R.string.k2go_br_status_restoring + : R.string.k2go_br_status_checking)); // K2GO-384: pass through the per-pass ETA TarExtractor already computed (was dropped). main.post(() -> setStep(label, unified, etaSeconds)); } diff --git a/controller/app/src/main/java/org/appdevforall/k2go/deploy/domain/ExtractProgress.java b/controller/app/src/main/java/org/appdevforall/k2go/deploy/domain/ExtractProgress.java index 5c0643d1d..59a5b2519 100644 --- a/controller/app/src/main/java/org/appdevforall/k2go/deploy/domain/ExtractProgress.java +++ b/controller/app/src/main/java/org/appdevforall/k2go/deploy/domain/ExtractProgress.java @@ -79,6 +79,27 @@ public static int unifiedPercent(int passPercent, int passIndex, int passCount) return v > 99 ? 99 : v; } + /** + * K2GO-448: a stage prefix for a multi-pass progress label, so the single unified bar reads as + * what it is: "[1/3] Copying the file", "[2/3] Checking the file", "[3/3] Restoring". The unified + * bar is monotone, but each pass reports its own ETA, so without the position the resetting time + * reads as the whole run slowing down. passIndex is 0-based (like unifiedPercent); the shown + * position is passIndex + 1, clamped the same way so a bad index never prints out of range. A + * single-pass run has nothing to disambiguate, so callers that do not want a prefix (a one-stage + * backup) simply do not call this. The "[N/M]" form carries no words, so it needs no translation. + * + * @param passIndex this pass, 0-based. + * @param passCount total passes in the run. + * @param label the stage's own (already localized) label. + * @return "[N/M] " + label. + */ + public static String stepped(int passIndex, int passCount, String label) { + if (passCount < 1) passCount = 1; + if (passIndex < 0) passIndex = 0; + if (passIndex > passCount - 1) passIndex = passCount - 1; + return "[" + (passIndex + 1) + "/" + passCount + "] " + (label == null ? "" : label); + } + /** First line of a possibly multi-line label, trimmed. Null-safe. */ public static String firstLine(String s) { if (s == null) return ""; diff --git a/controller/app/src/test/java/org/appdevforall/k2go/deploy/domain/ExtractProgressTest.java b/controller/app/src/test/java/org/appdevforall/k2go/deploy/domain/ExtractProgressTest.java index e2b01330f..6785f239f 100644 --- a/controller/app/src/test/java/org/appdevforall/k2go/deploy/domain/ExtractProgressTest.java +++ b/controller/app/src/test/java/org/appdevforall/k2go/deploy/domain/ExtractProgressTest.java @@ -152,4 +152,27 @@ public void anythingUnrecognisedIsStillShown() { assertEquals(ExtractProgress.unifiedPercent(0, 0, 3), ExtractProgress.unifiedPercent(0, -1, 3)); assertEquals(50, ExtractProgress.unifiedPercent(50, 0, 0)); // a run of no passes is one pass } + + // ---- K2GO-448: the stage prefix for the unified multi-pass bar ---- + + @Test public void steppedPrefixesTheRestoreStagesInOrder() { + assertEquals("[1/3] Copying the file", ExtractProgress.stepped(0, 3, "Copying the file")); + assertEquals("[2/3] Checking the file", ExtractProgress.stepped(1, 3, "Checking the file")); + assertEquals("[3/3] Restoring", ExtractProgress.stepped(2, 3, "Restoring")); + } + + @Test public void steppedPrefixesTheTwoRootfsStages() { + assertEquals("[1/2] Downloading", ExtractProgress.stepped(0, 2, "Downloading")); + assertEquals("[2/2] Extracting", ExtractProgress.stepped(1, 2, "Extracting")); + } + + @Test public void steppedClampsOutOfRangeLikeUnifiedPercent() { + assertEquals("[3/3] x", ExtractProgress.stepped(9, 3, "x")); // past the end -> last pass + assertEquals("[1/3] x", ExtractProgress.stepped(-1, 3, "x")); // before the start -> first pass + assertEquals("[1/1] x", ExtractProgress.stepped(0, 0, "x")); // a run of no passes is one pass + } + + @Test public void steppedNullLabelIsEmptyNotNullText() { + assertEquals("[1/3] ", ExtractProgress.stepped(0, 3, null)); + } } From f5beef8d8690c03b45d1d5853f27448e12953498 Mon Sep 17 00:00:00 2001 From: Luis Guzman Date: Sat, 3 Oct 2026 04:46:09 -0600 Subject: [PATCH 2/3] K2GO-448 feat(rootfs): show stage index on the install progress bars The rootfs install shows two measured bars: download, then verify+extract. Prefix the boot-screen status with its position ([1/2] download, [2/2] verify+extract) via ExtractProgress.stepped, so the per-stage ETA does not read as the whole run slowing down. UI only (LibraryActivity); reuses the restore helper. PROVISIONING stays an unnumbered spinner. --- .../appdevforall/k2go/redesign/LibraryActivity.java | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/controller/app/src/main/java/org/appdevforall/k2go/redesign/LibraryActivity.java b/controller/app/src/main/java/org/appdevforall/k2go/redesign/LibraryActivity.java index abbd186d3..994a7670d 100644 --- a/controller/app/src/main/java/org/appdevforall/k2go/redesign/LibraryActivity.java +++ b/controller/app/src/main/java/org/appdevforall/k2go/redesign/LibraryActivity.java @@ -491,8 +491,11 @@ private void showInstallProgress(InstallState st) { // from the IPv4/IPv6 probe, so without it the same three probes scroll past a second and // a third time with nothing to say which time this is — and the count is precisely what // tells the user whether waiting is still worth it. - installStatus.setText(st.message.isEmpty() - ? getString(R.string.k2go_downloading_library) : st.message); + // K2GO-448: the rootfs install is two measured bars (download, then verify+extract); + // mark this one as stage 1 of 2 so the per-stage time does not read as a slowdown. + installStatus.setText(org.appdevforall.k2go.deploy.domain.ExtractProgress.stepped(0, 2, + st.message.isEmpty() + ? getString(R.string.k2go_downloading_library) : st.message)); installBar.setIndeterminate(false); installBar.setProgress(st.percent); // ADFA-4895: one table per line, sized to the line, rather than one table stretched @@ -528,8 +531,10 @@ private void showInstallProgress(InstallState st) { // bar + % + ETA + current file — so there is no "first nothing, then detail" asymmetry. // Only the verb on the status line changes at the handoff. boolean verifying = st.phase == InstallState.Phase.VERIFYING; - installStatus.setText(org.appdevforall.k2go.deploy.domain.ExtractProgress.firstLine( - getString(verifying ? R.string.k2go_verifying_files : R.string.install_status_extracting))); + // K2GO-448: stage 2 of 2 (verify and extract share one unified bar; only the verb changes). + installStatus.setText(org.appdevforall.k2go.deploy.domain.ExtractProgress.stepped(1, 2, + org.appdevforall.k2go.deploy.domain.ExtractProgress.firstLine( + getString(verifying ? R.string.k2go_verifying_files : R.string.install_status_extracting)))); if (st.percent < 0) { // Indeterminate fallback: before the first byte lands, or an archive whose size we // couldn't read (no byte-based %). Animated hint on the DETAIL line (where the % goes). From ab0827547b05a38664b2043a350660809d53118e Mon Sep 17 00:00:00 2001 From: Luis Guzman Date: Sat, 3 Oct 2026 04:54:28 -0600 Subject: [PATCH 3/3] K2GO-448 fix(progress): apply the stage prefix consistently Code-review follow-up. The "[N/M]" prefix was only on the progress emits, so the restore copy stage still showed an unprefixed "Copying the file" first: in the notification and the initial indeterminate state (DeepOpService) and in the fragment's optimistic status (BackupJobFragment), flipping to "[1/3] ..." only once the copy reported a percent. Prefix at the source, and label the fragment from the same pass constants (now public). Name the rootfs stage count (ROOTFS_INSTALL_STAGES) so the two call sites share one source. --- .../org/appdevforall/k2go/deepop/DeepOpService.java | 13 ++++++++++--- .../k2go/redesign/BackupJobFragment.java | 6 +++++- .../appdevforall/k2go/redesign/LibraryActivity.java | 7 +++++-- 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/controller/app/src/main/java/org/appdevforall/k2go/deepop/DeepOpService.java b/controller/app/src/main/java/org/appdevforall/k2go/deepop/DeepOpService.java index be05a2371..637f7fa5a 100644 --- a/controller/app/src/main/java/org/appdevforall/k2go/deepop/DeepOpService.java +++ b/controller/app/src/main/java/org/appdevforall/k2go/deepop/DeepOpService.java @@ -80,8 +80,10 @@ public final class DeepOpService extends Service { /** K2GO-372: a restore is one run of three passes — stage the file, verify it, extract it — so they * share one bar instead of each filling it and sending it back to zero. */ - private static final int RESTORE_PASSES = 3; - private static final int COPY_PASS = 0; + // K2GO-448: public so the restore UI (BackupJobFragment) labels its optimistic first-stage status + // from the same source, instead of hardcoding the count. The owner of the pass model is this service. + public static final int RESTORE_PASSES = 3; + public static final int COPY_PASS = 0; private static final int VERIFY_PASS = 1; private static final int EXTRACT_PASS = 2; @@ -186,7 +188,12 @@ public int onStartCommand(Intent intent, int flags, int startId) { // Taking the lock here also sends the box down (RESTORE is a STOPPED-class holder, so desired // goes DOWN on the next reconciler tick) — which is what the confirm the user just answered // promised, and it means the explicit stop below has little left to wait for. - stepText = getString(restoring ? R.string.k2go_br_status_copying : R.string.k2go_br_status_stopping); + // K2GO-448: the restore's first stage ("[1/3] Copying the file") must carry the prefix from the + // start, so the notification and the initial indeterminate state match the progress emits below. + stepText = restoring + ? org.appdevforall.k2go.deploy.domain.ExtractProgress.stepped( + COPY_PASS, RESTORE_PASSES, getString(R.string.k2go_br_status_copying)) + : getString(R.string.k2go_br_status_stopping); startForeground(NOTIFICATION_ID, buildNotification(stepText)); acquireHardwareLocks(); diff --git a/controller/app/src/main/java/org/appdevforall/k2go/redesign/BackupJobFragment.java b/controller/app/src/main/java/org/appdevforall/k2go/redesign/BackupJobFragment.java index 0a32030bc..88b3f5189 100644 --- a/controller/app/src/main/java/org/appdevforall/k2go/redesign/BackupJobFragment.java +++ b/controller/app/src/main/java/org/appdevforall/k2go/redesign/BackupJobFragment.java @@ -208,7 +208,11 @@ private void prepareRestore(Uri uri) { .setMessage(getString(R.string.k2go_br_restore_warn)) .setPositive(R.string.k2go_br_restore_confirm, BrandDialog.Role.DESTRUCTIVE, () -> { beginRunning(); - setStatusAnimated(getString(R.string.k2go_br_status_copying)); + // K2GO-448: same "[1/3]" prefix the service posts, from the same source, so the + // optimistic status matches instead of flashing an unprefixed "Copying the file". + setStatusAnimated(org.appdevforall.k2go.deploy.domain.ExtractProgress.stepped( + DeepOpService.COPY_PASS, DeepOpService.RESTORE_PASSES, + getString(R.string.k2go_br_status_copying))); DeepOpService.startRestore(requireContext(), uri); }) .setNegative(R.string.cancel, () -> popToIntro(false)) diff --git a/controller/app/src/main/java/org/appdevforall/k2go/redesign/LibraryActivity.java b/controller/app/src/main/java/org/appdevforall/k2go/redesign/LibraryActivity.java index 994a7670d..ca01c6877 100644 --- a/controller/app/src/main/java/org/appdevforall/k2go/redesign/LibraryActivity.java +++ b/controller/app/src/main/java/org/appdevforall/k2go/redesign/LibraryActivity.java @@ -34,6 +34,9 @@ public class LibraryActivity extends AppCompatActivity private static final long GATE_SAFETY_MS = 25000L; /** Nothing installed → nothing to boot: dismiss the gate promptly instead of waiting. */ private static final long NO_SYSTEM_GATE_MS = 900L; + // K2GO-448: the rootfs install shows two measured bars (download, then verify+extract); one source + // for the count so both stage labels below agree. PROVISIONING is an unnumbered spinner, not a bar. + private static final int ROOTFS_INSTALL_STAGES = 2; /** Set by the Setup "Download" so the gate waits for the install to finish, not a timeout. */ public static final String EXTRA_INSTALLING = "installing"; // K2GO-391: the disk guard's notification opens this activity with a pre-filled report to send. @@ -493,7 +496,7 @@ private void showInstallProgress(InstallState st) { // tells the user whether waiting is still worth it. // K2GO-448: the rootfs install is two measured bars (download, then verify+extract); // mark this one as stage 1 of 2 so the per-stage time does not read as a slowdown. - installStatus.setText(org.appdevforall.k2go.deploy.domain.ExtractProgress.stepped(0, 2, + installStatus.setText(org.appdevforall.k2go.deploy.domain.ExtractProgress.stepped(0, ROOTFS_INSTALL_STAGES, st.message.isEmpty() ? getString(R.string.k2go_downloading_library) : st.message)); installBar.setIndeterminate(false); @@ -532,7 +535,7 @@ private void showInstallProgress(InstallState st) { // Only the verb on the status line changes at the handoff. boolean verifying = st.phase == InstallState.Phase.VERIFYING; // K2GO-448: stage 2 of 2 (verify and extract share one unified bar; only the verb changes). - installStatus.setText(org.appdevforall.k2go.deploy.domain.ExtractProgress.stepped(1, 2, + installStatus.setText(org.appdevforall.k2go.deploy.domain.ExtractProgress.stepped(1, ROOTFS_INSTALL_STAGES, org.appdevforall.k2go.deploy.domain.ExtractProgress.firstLine( getString(verifying ? R.string.k2go_verifying_files : R.string.install_status_extracting)))); if (st.percent < 0) {