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..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(); @@ -388,7 +395,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 +443,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 +509,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/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 abbd186d3..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. @@ -491,8 +494,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, ROOTFS_INSTALL_STAGES, + 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 +534,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, 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) { // 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). 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)); + } }