listEntries(String tarBinary, String archivePath, boolean i
try (BufferedReader reader = new BufferedReader(new InputStreamReader(listProcess.getInputStream()))) {
String line;
while ((line = reader.readLine()) != null) {
+ // K2GO-384: Cancel during verify. A confirmed cancel aborts (kill the lister -> onCancelled).
+ // A still-undecided Cancel (pauseAtBoundary) kills the lister too -- for responsiveness --
+ // but unwinds to onHeldForDecision so the caller keeps the copied temp and can re-run this
+ // pass. Both act before any write.
+ if (cancelledBeforeExtract != null && cancelledBeforeExtract.get()) {
+ listProcess.destroy();
+ throw new java.util.concurrent.CancellationException("cancelled during verify");
+ }
+ if (pauseAtBoundary != null && pauseAtBoundary.get()) {
+ listProcess.destroy();
+ throw new HeldForDecisionException();
+ }
names.add(line);
lastFile[0] = line; // ADFA-5118: the member tar -t is listing right now
}
diff --git a/controller/app/src/main/java/org/appdevforall/k2go/deepop/DeepOpProgressRepository.java b/controller/app/src/main/java/org/appdevforall/k2go/deepop/DeepOpProgressRepository.java
index db805a31..51d886a0 100644
--- a/controller/app/src/main/java/org/appdevforall/k2go/deepop/DeepOpProgressRepository.java
+++ b/controller/app/src/main/java/org/appdevforall/k2go/deepop/DeepOpProgressRepository.java
@@ -40,9 +40,10 @@ public DeepOpState current() {
public boolean isRunning() { return current().isRunning(); }
// All posts are thread-safe (callable from the DeepOpService worker thread).
- public void postRunning(EnvironmentLock.Owner owner, String step, int percent) { post(DeepOpState.running(owner, step, percent)); }
+ public void postRunning(EnvironmentLock.Owner owner, String step, int percent, long etaSeconds, DeepOpState.CancelKind cancelKind) { post(DeepOpState.running(owner, step, percent, etaSeconds, cancelKind)); }
public void postSuccess(EnvironmentLock.Owner owner, String message) { post(DeepOpState.success(owner, message)); }
public void postFailed(EnvironmentLock.Owner owner, String message) { post(DeepOpState.failed(owner, message)); }
+ public void postCancelled(EnvironmentLock.Owner owner) { post(DeepOpState.cancelled(owner)); }
public void postIdle() { post(DeepOpState.idle()); }
private synchronized void post(DeepOpState s) {
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 629d69db..b3a5fd28 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
@@ -72,6 +72,10 @@ public final class DeepOpService extends Service {
public static final String ACTION_BACKUP = "org.iiab.controller.DEEPOP_BACKUP";
public static final String ACTION_RESTORE = "org.iiab.controller.DEEPOP_RESTORE";
public static final String ACTION_CANCEL = "org.iiab.controller.DEEPOP_CANCEL";
+ // K2GO-384: the pausable-copy handshake -- Cancel pauses the copy, then the confirm dialog resolves it.
+ public static final String ACTION_RESUME = "org.iiab.controller.DEEPOP_RESUME";
+ public static final String ACTION_CANCEL_CONFIRM = "org.iiab.controller.DEEPOP_CANCEL_CONFIRM";
+ public static final String ACTION_FORCE_CANCEL = "org.iiab.controller.DEEPOP_FORCE_CANCEL"; // K2GO-384: acknowledged cancel DURING extract
public static final String EXTRA_URI = "uri"; // backup: SAF dest; restore: SAF source
/** K2GO-372: a restore is one run of three passes — stage the file, verify it, extract it — so they
@@ -89,6 +93,29 @@ public final class DeepOpService extends Service {
private volatile boolean done = false; // terminal reached (cancel OR natural) — clean up once
private EnvironmentLock.Owner owner;
private String stepText = "";
+ /** K2GO-384 (ADR-5343c): a CONFIRMED abort in the SAFE zone (copy / verify / the verify->extract
+ * boundary). The copy loop and TarExtractor's verify + boundary read it and abort with the rootfs
+ * untouched. It is NEVER read by the extract feeder -- a safe-zone abort must not be able to tear the
+ * rootfs (that is {@link #forceExtractCancel}'s job). Set only while currentCancelKind == CANCELLABLE. */
+ private final java.util.concurrent.atomic.AtomicBoolean cancelBeforeExtract = new java.util.concurrent.atomic.AtomicBoolean(false);
+ /** K2GO-384 (ADR-5343c): an ACKNOWLEDGED destructive kill, read ONLY by the extract feeder (past the
+ * point of no return). Set only while currentCancelKind == DESTRUCTIVE. Kept separate from
+ * cancelBeforeExtract so the two cancel intents can never alias across the verify->extract boundary. */
+ private final java.util.concurrent.atomic.AtomicBoolean forceExtractCancel = new java.util.concurrent.atomic.AtomicBoolean(false);
+ /** K2GO-384: the owner of "one verify+extract pass at a time". Set when a pass starts, cleared at every
+ * pass outcome (complete / error / cancelled / held). Guards the re-callable attemptVerifyAndExtract so a
+ * "Keep restoring" cannot start a second pass over the same temp while one is running. */
+ private volatile boolean passRunning = false;
+ /** K2GO-384: true while the user is deciding on a paused COPY (Cancel pressed). The copy loop blocks on
+ * it; ACTION_RESUME clears it (continue), ACTION_CANCEL_CONFIRM sets cancelBeforeExtract and clears it
+ * (abort). Only the copy is pausable -- verify/extract are external tar processes. */
+ private final java.util.concurrent.atomic.AtomicBoolean pauseRequested = new java.util.concurrent.atomic.AtomicBoolean(false);
+ /** K2GO-384: what cancelling the current restore pass means -- set from the real phase, published in
+ * DeepOpState so the UI shows the right dialog without guessing from the step text. */
+ private volatile DeepOpState.CancelKind currentCancelKind = DeepOpState.CancelKind.NONE;
+ /** K2GO-384: non-null while a verify pass is HELD for an undecided Cancel -- carries the copied temp so
+ * ACTION_RESUME can re-run verify+extract on it (no re-copy) and ACTION_CANCEL_CONFIRM can delete it. */
+ private volatile String heldTempPath = null;
/** Start a backup: stream a gzip'd tar of the rootfs to the SAF destination. */
public static void startBackup(Context ctx, Uri dest) {
@@ -118,6 +145,35 @@ public int onStartCommand(Intent intent, int flags, int startId) {
if (intent == null) { stopSelf(); return START_NOT_STICKY; }
final String action = intent.getAction();
if (ACTION_CANCEL.equals(action)) { if (started) cancel(); else stopSelf(); return START_NOT_STICKY; }
+ // K2GO-384: resolve a paused COPY -- resume (keep restoring) or confirm the cancel (abort).
+ if (ACTION_RESUME.equals(action)) {
+ // K2GO-384: keep restoring. A paused COPY just resumes; a HELD verify re-runs on the same temp
+ // (verify + extract, no re-copy).
+ pauseRequested.set(false);
+ final String held = heldTempPath;
+ if (held != null) { heldTempPath = null; attemptVerifyAndExtract(held); }
+ return START_NOT_STICKY;
+ }
+ if (ACTION_CANCEL_CONFIRM.equals(action)) {
+ // K2GO-384 (ADR-5343c): the reversible abort applies ONLY in the safe zone. Guarding on the
+ // service's own currentCancelKind (not the UI's lagging copy) means a confirm that races past the
+ // verify->extract boundary is ignored here -- the extract simply finishes -- rather than reaching
+ // the feeder. The system is never torn by a "system unchanged" confirm.
+ if (started && currentCancelKind == DeepOpState.CancelKind.CANCELLABLE) {
+ cancelBeforeExtract.set(true); // a paused COPY bails on this; stageThenRestore then cleans up
+ pauseRequested.set(false);
+ final String held = heldTempPath;
+ if (held != null) { heldTempPath = null; main.post(() -> endRestore(held, false, "")); }
+ }
+ return START_NOT_STICKY;
+ }
+ // K2GO-384 (ADR-5343c): acknowledged cancel DURING extract -- kill tar mid-write via its OWN token.
+ // Guarded on DESTRUCTIVE so it acts only past the point of no return; InstallGuard stays planted so
+ // recovery reinstalls.
+ if (ACTION_FORCE_CANCEL.equals(action)) {
+ if (started && currentCancelKind == DeepOpState.CancelKind.DESTRUCTIVE) forceExtractCancel.set(true);
+ return START_NOT_STICKY;
+ }
if (started) return START_NOT_STICKY; // one op per service instance
started = true;
@@ -195,6 +251,7 @@ private void stageThenRestore(final String uriStr) {
endRestore(temp.getAbsolutePath(), false, outcome);
return;
}
+ currentCancelKind = DeepOpState.CancelKind.CANCELLABLE; // K2GO-384: stopping -- cancellable
setStep(getString(R.string.k2go_br_status_stopping), -1);
EnvironmentControl.stop(this, this::log, () -> runRestore(temp.getAbsolutePath()));
});
@@ -278,8 +335,19 @@ private String stageArchive(Uri src, File temp, long size) {
if (in == null) throw new IOException("The picked file could not be opened");
byte[] buf = new byte[1 << 16];
long copied = 0L, lastEmit = 0L;
+ final long startMs = android.os.SystemClock.elapsedRealtime();
+ currentCancelKind = DeepOpState.CancelKind.CANCELLABLE; // K2GO-384: copy -- cancellable (pauses here)
int n;
while ((n = in.read(buf)) != -1) { // a 0-length read is not end of stream
+ // K2GO-384: Cancel during the copy PAUSES here (our native loop). Block while paused; a
+ // confirmed cancel sets cancelBeforeExtract and we bail (endRestore deletes the temp, the
+ // rootfs untouched); resume just continues. The empty return is never shown -- a cancel
+ // returns the screen to the bifurcation.
+ if (cancelBeforeExtract.get()) return "";
+ while (pauseRequested.get() && !cancelBeforeExtract.get()) {
+ try { Thread.sleep(120L); } catch (InterruptedException e) { return ""; }
+ }
+ if (cancelBeforeExtract.get()) return "";
out.write(buf, 0, n);
copied += n;
long now = android.os.SystemClock.elapsedRealtime();
@@ -288,7 +356,13 @@ private String stageArchive(Uri src, File temp, long size) {
final int pct = org.appdevforall.k2go.deploy.domain.ExtractProgress
.unifiedPercent(org.appdevforall.k2go.deploy.domain.ExtractProgress
.percent(copied, size), COPY_PASS, RESTORE_PASSES);
- main.post(() -> setStep(getString(R.string.k2go_br_status_copying), pct));
+ // K2GO-384: the copy is the restore's first pass; give it the same live per-pass ETA
+ // the extract/verify passes already report (TarExtractor computes theirs the same way).
+ final long rate = org.appdevforall.k2go.system.domain.TransferRate
+ .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));
}
}
out.flush();
@@ -323,13 +397,63 @@ private void runRestore(final String path) {
// thrown away what the user asked for over an operation that never happened.
org.appdevforall.k2go.system.data.ContentStateInvalidator.replacementStarting(this,
org.appdevforall.k2go.system.domain.SystemReplacement.Cause.RESTORE);
+ attemptVerifyAndExtract(path);
+ }
+
+ /**
+ * K2GO-384: run (or RE-run) the verify + extract on the already-staged {@code path}. Re-callable so a
+ * "Keep restoring" after a Cancel raised during verify re-runs just this pass -- verify (`tar -t`) then
+ * extract -- on the copied temp, WITHOUT re-copying. The copy above is never repeated.
+ */
+ 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);
final File destParent = new File(getFilesDir(), "rootfs");
new TarExtractor().startExtraction(this, path, destParent.getAbsolutePath(), true,
+ cancelBeforeExtract, pauseRequested, forceExtractCancel,
new TarExtractor.ExtractionListener() {
- @Override public void onComplete(String destDir) { main.post(() -> endRestore(path, true, null)); }
- @Override public void onError(String error) { main.post(() -> endRestore(path, false, error)); }
+ @Override public void onComplete(String destDir) { passRunning = false; main.post(() -> endRestore(path, true, null)); }
+ @Override public void onError(String error) {
+ passRunning = false;
+ // K2GO-384 (ADR-5343c): "known damage" is owned by "the extract began and did not
+ // complete", not by the cancel button. isLive == true means onExtractStarting planted
+ // the marker, i.e. the rootfs was being written and is now torn (a force-cancel OR a
+ // real mid-write failure). Mark it DAMAGED so isLive drops (the k2go_busy_install gate
+ // lifts, unblocking a fresh restore) and desired stays DOWN (isSystemInstalled=false, no
+ // flap on the torn base); isInterrupted stays true so recovery owns it next launch/return.
+ final boolean torn = InstallGuard.isLive(DeepOpService.this);
+ if (torn) InstallGuard.markDamaged(DeepOpService.this);
+ // forced only picks the MESSAGE: our own acknowledged kill -> the damaged line; a real
+ // failure keeps its diagnostic (the system is still marked damaged above).
+ final boolean forced = forceExtractCancel.get();
+ main.post(() -> endRestore(path, false,
+ forced ? getString(R.string.k2go_br_restore_damaged) : error));
+ }
@Override public void onProgress(String line) { }
+ // K2GO-384: the point of no return -- fired once at the verify->extract boundary, BEFORE
+ // the first write, on the extractor thread. Plant the destructive marker here (decoupled
+ // from the progress emits) so an ungraceful kill during the write is recovered next launch.
+ @Override public void onExtractStarting() {
+ currentCancelKind = DeepOpState.CancelKind.DESTRUCTIVE; // K2GO-384: past the point of no return
+ InstallGuard.begin(DeepOpService.this);
+ }
+ // K2GO-384: cancelled before any write (Option B) -- the rootfs is untouched and no marker
+ // was planted. Delete the staged temp and end (the screen returns to the bifurcation, so
+ // no terminal message is shown -- an empty reason keeps endRestore's cleanup path).
+ @Override public void onCancelled() {
+ passRunning = false;
+ main.post(() -> endRestore(path, false, ""));
+ }
+ // K2GO-384: Cancel pressed during verify, still undecided -- `tar -t` was killed but the
+ // copied temp is intact. HOLD: keep the temp and wait. ACTION_RESUME re-runs this pass on
+ // the same temp (no re-copy); ACTION_CANCEL_CONFIRM aborts (deletes the temp, bifurcation).
+ // passRunning drops here too: the extractor thread has exited, so a Keep may start a fresh pass.
+ @Override public void onHeldForDecision() {
+ passRunning = false;
+ main.post(() -> heldTempPath = path);
+ }
/**
* K2GO-372: a restore reads the whole archive three times — the copy above, the
@@ -343,23 +467,18 @@ private void runRestore(final String path) {
public void onExtractPhase(TarExtractor.Phase phase, int passPercent,
long etaSeconds, String line) {
final boolean extracting = phase == TarExtractor.Phase.EXTRACT;
- // K2GO-372: the destructive window opens here, at the first byte written over the
- // rootfs. The marker used to be planted when the service started, which also
- // covered the copy and the whole verify pass — phases where a kill damages
- // nothing, so an ungraceful exit in them declared a false DAMAGED, and an archive
- // the extractor rejects would have left the system marked damaged untouched.
- // isLive() reads the marker itself, so knowing it is already planted needs no
- // second flag here.
- if (extracting && !InstallGuard.isLive(DeepOpService.this)) {
- InstallGuard.begin(DeepOpService.this);
- }
+ // K2GO-384: the destructive marker (InstallGuard.begin) is now planted in
+ // onExtractStarting() -- once, at the verify->extract boundary, before the first
+ // write and decoupled from these progress emits -- so it no longer rides on the
+ // first extract progress callback.
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);
- main.post(() -> setStep(label, unified));
+ // K2GO-384: pass through the per-pass ETA TarExtractor already computed (was dropped).
+ main.post(() -> setStep(label, unified, etaSeconds));
}
});
}
@@ -374,6 +493,14 @@ private void endRestore(String tempPath, boolean ok, String failMessage) {
}
File temp = new File(tempPath);
if (temp.exists()) temp.delete();
+ // K2GO-384 (ADR-5343c): an empty reason means a user cancel in the SAFE zone (copy/verify) -- terminal
+ // but not a failure. Route it to a CANCELLED terminal (the screen returns to the bifurcation, decided
+ // by phase so it survives a config change), not a "Restore failed" screen. A real failure or the
+ // acknowledged-damaged message (both non-empty) goes to FAILED.
+ if (!ok && (failMessage == null || failMessage.trim().isEmpty())) {
+ finishCancelled();
+ return;
+ }
// K2GO-372: the extractor already produces the exact reason (wrong architecture, not a rootfs);
// it used to be discarded here and replaced by a generic "Restore failed".
finishJob(ok, getString(R.string.k2go_br_restore_done),
@@ -403,6 +530,21 @@ private void finishJob(boolean ok, String okMsg, String failMsg) {
teardown();
}
+ /**
+ * K2GO-384 (ADR-5343c): terminal for a user cancel in the safe zone (copy/verify). Same teardown as
+ * finishJob's failed path -- re-enable desired and drop the lock -- but posts a CANCELLED phase (not
+ * FAILED) and never touches InstallGuard: a pre-destructive cancel planted no marker, so there is nothing
+ * to end and nothing damaged.
+ */
+ private void finishCancelled() {
+ if (done) return;
+ done = true;
+ new org.appdevforall.k2go.Preferences(this).setWatchdogEnable(true);
+ EnvironmentLock.release(this);
+ DeepOpProgressRepository.get().postCancelled(owner);
+ teardown();
+ }
+
/**
* Notification "Cancel" — offered only for BACKUP (read-only, safe to abandon). Restore has no
* Cancel action (destructive, hard gate). The in-flight backup stream sees {@code done} and no-ops
@@ -411,8 +553,16 @@ private void finishJob(boolean ok, String okMsg, String failMsg) {
private void cancel() {
if (owner == EnvironmentLock.Owner.BACKUP) {
finishJob(false, "", getString(R.string.k2go_br_backup_failed));
+ return;
+ }
+ // K2GO-384: a Cancel on any pre-destructive pass (CANCELLABLE) HOLDS the run and waits for the
+ // confirm dialog. pauseRequested pauses the copy loop AND blocks the verify->extract boundary, so
+ // whether we are mid-copy or mid-verify nothing advances into the destructive extract while the user
+ // decides. ACTION_RESUME continues; ACTION_CANCEL_CONFIRM aborts. DESTRUCTIVE (extract) is not
+ // handled here -- it takes the acknowledged force-cancel path.
+ if (currentCancelKind == DeepOpState.CancelKind.CANCELLABLE) {
+ pauseRequested.set(true);
}
- // A stray CANCEL for restore (which has no cancel action) is ignored — the extract must finish.
}
private void teardown() {
@@ -423,14 +573,19 @@ private void teardown() {
}
// ---- progress ----
- private void setStep(String step, int percent) {
+ private void setStep(String step, int percent) { setStep(step, percent, -1L); }
+
+ /** K2GO-384: overload carrying the current pass's ETA (seconds; {@code -1} = unknown/hidden). */
+ private void setStep(String step, int percent, long etaSeconds) {
stepText = step;
updateNotification(step);
- post(step, percent);
+ post(step, percent, etaSeconds);
}
- private void post(String step, int percent) {
- DeepOpProgressRepository.get().postRunning(owner, step, percent);
+ private void post(String step, int percent) { post(step, percent, -1L); }
+
+ private void post(String step, int percent, long etaSeconds) {
+ DeepOpProgressRepository.get().postRunning(owner, step, percent, etaSeconds, currentCancelKind);
}
private void log(String line) { Log.d(TAG, line); }
diff --git a/controller/app/src/main/java/org/appdevforall/k2go/deepop/DeepOpState.java b/controller/app/src/main/java/org/appdevforall/k2go/deepop/DeepOpState.java
index 0632fa45..db0ad00d 100644
--- a/controller/app/src/main/java/org/appdevforall/k2go/deepop/DeepOpState.java
+++ b/controller/app/src/main/java/org/appdevforall/k2go/deepop/DeepOpState.java
@@ -16,42 +16,68 @@
public final class DeepOpState {
- public enum Phase { IDLE, RUNNING, SUCCESS, FAILED }
+ // K2GO-384: CANCELLED is a terminal distinct from FAILED — a user cancel in the pre-destructive zone
+ // (copy/verify) is not an error. It lives on the state (not a fragment flag) so the "return to the
+ // bifurcation, not a failure screen" decision survives a config change (mirrors InstallState.Phase).
+ public enum Phase { IDLE, RUNNING, SUCCESS, FAILED, CANCELLED }
+
+ /**
+ * K2GO-384: what cancelling the CURRENT restore pass would do -- the single source the UI reads to
+ * pick the right dialog, owned by the service's real phase (never derived from the step text).
+ * NONE - not cancellable here (e.g. backup, or no op).
+ * CANCELLABLE - a pre-destructive pass (copy / stopping / verify): tap Cancel HOLDS the run (the copy
+ * pauses mid-loop; verify runs to completion and holds at the verify->extract boundary,
+ * before any write) and asks -- Keep restoring (continue) or Cancel (abort, system
+ * unchanged). Reversible.
+ * DESTRUCTIVE - the extract (`tar -x`, writing the rootfs): cancelling leaves the system damaged ->
+ * recovery reinstalls; needs the strong red + acknowledgement confirm.
+ */
+ public enum CancelKind { NONE, CANCELLABLE, DESTRUCTIVE }
public final Phase phase;
/** Which deep-env op this state belongs to (BACKUP / RESTORE / CLONE); null only when IDLE. */
public final EnvironmentLock.Owner owner;
public final int percent; // 0..100, or -1 for indeterminate
+ public final long etaSeconds; // K2GO-384: seconds left for the current pass, or -1 when unknown/hidden
+ public final CancelKind cancelKind; // K2GO-384: what a Cancel here means (drives the UI's dialog)
public final String step; // resolved status label, e.g. "Backing up"
public final String message; // terminal message / error text
public final long seq; // assigned by the repository; identifies terminal events
- private DeepOpState(Phase phase, EnvironmentLock.Owner owner, int percent, String step, String message, long seq) {
+ private DeepOpState(Phase phase, EnvironmentLock.Owner owner, int percent, long etaSeconds, CancelKind cancelKind, String step, String message, long seq) {
this.phase = phase;
this.owner = owner;
this.percent = percent;
+ this.etaSeconds = etaSeconds;
+ this.cancelKind = cancelKind != null ? cancelKind : CancelKind.NONE;
this.step = step != null ? step : "";
this.message = message != null ? message : "";
this.seq = seq;
}
public boolean isRunning() { return phase == Phase.RUNNING; }
- public boolean isTerminal() { return phase == Phase.SUCCESS || phase == Phase.FAILED; }
+ public boolean isTerminal() { return phase == Phase.SUCCESS || phase == Phase.FAILED || phase == Phase.CANCELLED; }
/** Returns a copy with the given sequence number (the repository assigns it). */
- DeepOpState withSeq(long seq) { return new DeepOpState(phase, owner, percent, step, message, seq); }
+ DeepOpState withSeq(long seq) { return new DeepOpState(phase, owner, percent, etaSeconds, cancelKind, step, message, seq); }
- public static DeepOpState idle() { return new DeepOpState(Phase.IDLE, null, 0, "", "", 0L); }
+ public static DeepOpState idle() { return new DeepOpState(Phase.IDLE, null, 0, -1L, CancelKind.NONE, "", "", 0L); }
- public static DeepOpState running(EnvironmentLock.Owner owner, String step, int percent) {
- return new DeepOpState(Phase.RUNNING, owner, percent, step, "", 0L);
+ public static DeepOpState running(EnvironmentLock.Owner owner, String step, int percent, long etaSeconds, CancelKind cancelKind) {
+ return new DeepOpState(Phase.RUNNING, owner, percent, etaSeconds, cancelKind, step, "", 0L);
}
public static DeepOpState success(EnvironmentLock.Owner owner, String message) {
- return new DeepOpState(Phase.SUCCESS, owner, 0, "", message, 0L);
+ return new DeepOpState(Phase.SUCCESS, owner, 0, -1L, CancelKind.NONE, "", message, 0L);
}
public static DeepOpState failed(EnvironmentLock.Owner owner, String message) {
- return new DeepOpState(Phase.FAILED, owner, 0, "", message, 0L);
+ return new DeepOpState(Phase.FAILED, owner, 0, -1L, CancelKind.NONE, "", message, 0L);
+ }
+
+ /** K2GO-384: a user cancel in the pre-destructive zone — terminal, but not a failure (no message; the
+ * screen returns to the bifurcation). */
+ public static DeepOpState cancelled(EnvironmentLock.Owner owner) {
+ return new DeepOpState(Phase.CANCELLED, owner, 0, -1L, CancelKind.NONE, "", "", 0L);
}
}
diff --git a/controller/app/src/main/java/org/appdevforall/k2go/env/domain/ServerReconcile.java b/controller/app/src/main/java/org/appdevforall/k2go/env/domain/ServerReconcile.java
index 2ae86d93..b66d4ff6 100644
--- a/controller/app/src/main/java/org/appdevforall/k2go/env/domain/ServerReconcile.java
+++ b/controller/app/src/main/java/org/appdevforall/k2go/env/domain/ServerReconcile.java
@@ -64,6 +64,12 @@ public enum Intent {
* marker-derived is ever added (e.g. a structural rootfs check), this invariant must be revisited —
* desired would then have a reason to gate on health again.
*
+ * K2GO-384 (ADR-5343c) added exactly such a signal — KNOWN damage from a force-cancelled restore
+ * ({@code InstallGuard.isDamaged}) — and kept this invariant intact. Rather than teach {@code desired}
+ * to read health, a known-damaged base is folded into {@code installed=false} (in {@code
+ * SystemStateEvaluator.isSystemInstalled}, the same lever a LIVE install already uses), so {@code desired}
+ * stays DOWN through its existing {@code installed} argument. Desired still does not read {@code healthy}.
+ *
* @param installed a rootfs is present and no LIVE install is running over it.
* @param userWantsOn the persisted user intent (today {@code Preferences.WatchdogEnable}).
* @param holderClass the execution class of the current environment holder
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 e57a7c8d..88c3cb70 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
@@ -67,6 +67,12 @@ public static BackupJobFragment newInstance(String mode) {
private View progressRow;
private com.google.android.material.progressindicator.LinearProgressIndicator progress;
private TextView progressPct;
+ /** K2GO-384: per-pass ETA caption next to the percent; blank when the pass can't estimate yet. */
+ private TextView progressEta;
+ /** K2GO-384: on-screen Cancel, shown only while a RESTORE is running and still before the extract. */
+ private View cancel;
+ /** K2GO-384: the current pass's cancel semantics (single source = DeepOpState.cancelKind). */
+ private DeepOpState.CancelKind lastCancelKind = DeepOpState.CancelKind.NONE;
private org.appdevforall.k2go.util.EllipsisAnimator statusDots;
private boolean running = false;
private long lastSeq = -1L;
@@ -102,6 +108,8 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c
progressRow = v.findViewById(R.id.k2go_bj_progress_row);
progress = v.findViewById(R.id.k2go_bj_progress);
progressPct = v.findViewById(R.id.k2go_bj_progress_pct);
+ progressEta = v.findViewById(R.id.k2go_bj_progress_eta);
+ cancel = v.findViewById(R.id.k2go_bj_cancel);
finish = v.findViewById(R.id.k2go_bj_finish);
waitCard = v.findViewById(R.id.k2go_bj_wait_card);
// ADFA-4947 fixed-width mode: this status line is centred, so variable-width dots slide the
@@ -111,6 +119,7 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c
title.setText(getString(isRestore() ? R.string.k2go_br_restore_title : R.string.k2go_br_backup_title));
sub.setText(getString(isRestore() ? R.string.k2go_br_restore_sub : R.string.k2go_br_backup_sub));
finish.setOnClickListener(x -> popToIntro(true));
+ cancel.setOnClickListener(x -> onCancelTapped());
// Hard gate: while the op runs, back is consumed with a styled snackbar (module-index behavior).
backGate = new androidx.activity.OnBackPressedCallback(false) {
@@ -147,7 +156,10 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c
beginRunning(); // deep-link / recreation into a live op
setStatusAnimated(cur.step);
} else if (s != null && cur.owner == myOwner() && cur.isTerminal()) {
- showTerminal(cur.phase == DeepOpState.Phase.SUCCESS, cur.message); // recreated at the result
+ // K2GO-384: a CANCELLED terminal returns to the bifurcation even when the fragment is recreated
+ // exactly at it (config change) -- the decision lives on the phase, not a fragment flag.
+ if (cur.phase == DeepOpState.Phase.CANCELLED) v.post(() -> popToIntro(false));
+ else showTerminal(cur.phase == DeepOpState.Phase.SUCCESS, cur.message); // recreated at the result
} else if (s == null) {
v.post(this::launchPicker); // fresh entry: the intro card is the Start
}
@@ -209,7 +221,7 @@ private void prepareRestore(Uri uri) {
*
A negative percent means the step cannot say how far along it is, and a bar frozen at zero
* reads as a stall — worse than no bar. Those steps keep the animation above instead.
*/
- private void showProgress(int percent) {
+ private void showProgress(int percent, long etaSeconds) {
if (progressRow == null) return;
final boolean measurable = percent >= 0;
progressRow.setVisibility(measurable ? View.VISIBLE : View.GONE);
@@ -217,6 +229,14 @@ private void showProgress(int percent) {
progress.setProgressCompat(percent, true);
progressPct.setText(percent + "%"); // matches the module-install and maps screens
}
+ // K2GO-384: the per-pass ETA ("~2 min"), reusing the install screens' EtaText so the wording
+ // lives in one place. Blank (unknown, or not measurable yet) leaves the slot empty rather than
+ // showing a stale estimate — the bar and % already carry the progress.
+ if (progressEta != null) {
+ progressEta.setText(measurable
+ ? org.appdevforall.k2go.install.presentation.EtaText.of(requireContext(), etaSeconds)
+ : "");
+ }
}
// ---- observe the app-scoped op state (DeepOpService is the writer) ----
@@ -225,10 +245,17 @@ private void onDeepOpState(DeepOpState st) {
if (st.isRunning()) {
if (!running) beginRunning();
setStatusAnimated(st.step);
- showProgress(st.percent);
+ showProgress(st.percent, st.etaSeconds);
+ lastCancelKind = st.cancelKind; // K2GO-384: track the current pass's cancel semantics
+ updateCancelVisibility();
} else if (st.isTerminal() && st.seq > lastSeq) {
lastSeq = st.seq;
- showProgress(-1); // K2GO-372: a finished op has no bar to keep filling
+ showProgress(-1, -1L); // K2GO-372: a finished op has no bar to keep filling
+ // K2GO-384: a user cancel in the safe zone is its own terminal (CANCELLED) -- return to the
+ // bifurcation, not a "something went wrong" screen. The service owns that distinction now (the
+ // phase), so it holds across a config change. FAILED (a real error, or the acknowledged
+ // destructive cancel's "damaged" message) and SUCCESS both show a terminal.
+ if (st.phase == DeepOpState.Phase.CANCELLED) { popToIntro(false); return; }
showTerminal(st.phase == DeepOpState.Phase.SUCCESS, st.message);
}
}
@@ -240,10 +267,12 @@ private void beginRunning() {
finish.setVisibility(View.GONE);
if (waitCard != null) waitCard.setVisibility(View.VISIBLE);
if (anim != null) anim.playAnimation();
+ updateCancelVisibility();
}
private void showTerminal(boolean ok, String message) {
running = false;
+ if (cancel != null) cancel.setVisibility(View.GONE); // K2GO-384: no cancel once the op has ended
if (backGate != null) backGate.setEnabled(false); // done → back / Finish returns to the bifurcation
if (statusDots != null) statusDots.stop();
if (anim != null) anim.pauseAnimation();
@@ -261,6 +290,104 @@ private void setStatusAnimated(String text) {
if (statusDots != null) statusDots.start(text);
}
+ /**
+ * K2GO-384: on a cancellable pass (copy / stopping / verify) Cancel HOLDS the run -- the service pauses
+ * the copy loop or blocks the verify->extract boundary, so nothing advances into the destructive extract
+ * while the user decides -- and shows a light M3 dialog: Keep restoring (continue) / Cancel restore
+ * (abort, system unchanged). During the DESTRUCTIVE extract, Cancel instead shows the acknowledged
+ * force-cancel dialog (red + checkbox), which leaves the system damaged for recovery.
+ */
+ private void onCancelTapped() {
+ if (!isAdded()) return;
+ if (lastCancelKind == DeepOpState.CancelKind.DESTRUCTIVE) { showDestructiveCancelDialog(); return; }
+ if (lastCancelKind != DeepOpState.CancelKind.CANCELLABLE) return;
+ sendToService(DeepOpService.ACTION_CANCEL); // hold the run while the user decides (no race)
+ // K2GO-384: non-cancelable -- the run is HELD (copy paused / verify blocked at the boundary) the moment
+ // this shows, so the user MUST resolve it. A scrim/Back dismiss would leave pauseRequested set and hang
+ // the restore forever; forcing a Keep/Cancel choice closes that lifecycle gap.
+ new BrandDialog(requireContext())
+ .setCancelable(false)
+ .setTitle(getString(R.string.k2go_br_cancel_title))
+ .setMessage(getString(R.string.k2go_br_cancel_body))
+ .setPositive(R.string.k2go_br_cancel_confirm, () -> {
+ if (cancel != null) cancel.setEnabled(false);
+ sendToService(DeepOpService.ACTION_CANCEL_CONFIRM); // abort -> CANCELLED terminal -> bifurcation
+ })
+ .setNegative(R.string.k2go_br_cancel_keep, () -> sendToService(DeepOpService.ACTION_RESUME))
+ .show();
+ }
+
+ private void sendToService(String action) {
+ if (!isAdded()) return;
+ requireContext().startService(
+ new android.content.Intent(requireContext(), DeepOpService.class).setAction(action));
+ }
+
+ /**
+ * K2GO-384: cancelling DURING the extract is destructive -- the rootfs is being overwritten. A strong
+ * M3 dialog: RED confirm (colorError) gated by an acknowledgement checkbox. Confirming force-cancels the
+ * extract (kills tar mid-write); the system is left torn and reinstalls itself on next boot (InstallGuard).
+ * Unlike the trivial cancel, this keeps a terminal so the user is told the system will be reinstalled.
+ */
+ private void showDestructiveCancelDialog() {
+ if (!isAdded()) return;
+ final float density = getResources().getDisplayMetrics().density;
+ // ADFA-5339 pattern: a MaterialCheckBox as a dialog's custom view sits flush-left; a holder padded by
+ // dialogPreferredPadding (the title/message inset) with the checkbox's own left padding at 0 lines it
+ // up with the text above.
+ final com.google.android.material.checkbox.MaterialCheckBox box =
+ new com.google.android.material.checkbox.MaterialCheckBox(requireContext());
+ box.setText(R.string.k2go_br_cancel_extract_ack);
+ box.setCompoundDrawablePadding(Math.round(8 * density));
+ final int pad = dialogContentPadding();
+ final android.widget.FrameLayout holder = new android.widget.FrameLayout(requireContext());
+ holder.setPadding(pad, Math.round(8 * density), pad, 0);
+ holder.addView(box);
+ final androidx.appcompat.app.AlertDialog d =
+ new com.google.android.material.dialog.MaterialAlertDialogBuilder(requireContext())
+ .setTitle(R.string.k2go_br_cancel_title)
+ .setMessage(R.string.k2go_br_cancel_extract_body)
+ .setView(holder)
+ .setPositiveButton(R.string.k2go_br_cancel_confirm, null) // click set below to gate dismiss
+ .setNegativeButton(R.string.k2go_br_cancel_keep, null)
+ .show();
+ final android.widget.Button confirm = d.getButton(androidx.appcompat.app.AlertDialog.BUTTON_POSITIVE);
+ confirm.setTextColor(ContextCompat.getColor(requireContext(), R.color.btn_danger)); // same red as BrandDialog DESTRUCTIVE
+ // K2GO-384: keep the button ENABLED and validate on click -- an inert disabled button gives no
+ // feedback; a snackbar tells the user why nothing happened until they acknowledge.
+ confirm.setOnClickListener(v -> {
+ if (!box.isChecked()) {
+ Snackbars.make(requireActivity().findViewById(android.R.id.content),
+ R.string.k2go_br_cancel_extract_need_ack).show();
+ return;
+ }
+ // K2GO-384: the destructive kill ends as FAILED with the "damaged" message (non-empty), so the
+ // terminal shows -- the user sees "the next launch will start recovery" (no CANCELLED short-circuit).
+ if (cancel != null) cancel.setEnabled(false);
+ sendToService(DeepOpService.ACTION_FORCE_CANCEL);
+ d.dismiss();
+ });
+ }
+
+ /** The dialog's horizontal content inset (title/message use it); a custom view must match it to line up
+ * (ADFA-5339). */
+ private int dialogContentPadding() {
+ android.util.TypedValue tv = new android.util.TypedValue();
+ if (requireContext().getTheme().resolveAttribute(androidx.appcompat.R.attr.dialogPreferredPadding, tv, true)) {
+ return android.util.TypedValue.complexToDimensionPixelSize(tv.data, getResources().getDisplayMetrics());
+ }
+ return Math.round(24 * getResources().getDisplayMetrics().density); // Material default
+ }
+
+ /** K2GO-384: Cancel is shown while the current pass is cancellable (PAUSABLE copy or ABORTABLE
+ * verify/stopping). It hides at the destructive extract and when there is nothing to cancel. */
+ private void updateCancelVisibility() {
+ if (cancel == null || !isAdded()) return;
+ boolean show = running && (lastCancelKind == DeepOpState.CancelKind.CANCELLABLE
+ || lastCancelKind == DeepOpState.CancelKind.DESTRUCTIVE);
+ cancel.setVisibility(show ? View.VISIBLE : View.GONE);
+ }
+
/** Return to the bifurcation (BackupRestoreFragment). When {@code fromFinish}, arm the intro's
* index-style "returning to Home" countdown. */
private void popToIntro(boolean fromFinish) {
diff --git a/controller/app/src/main/res/layout/fragment_k2go_backup_job.xml b/controller/app/src/main/res/layout/fragment_k2go_backup_job.xml
index 5d987046..1a7d695b 100644
--- a/controller/app/src/main/res/layout/fragment_k2go_backup_job.xml
+++ b/controller/app/src/main/res/layout/fragment_k2go_backup_job.xml
@@ -115,27 +115,42 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
- android:gravity="center_vertical"
- android:orientation="horizontal"
+ android:orientation="vertical"
android:visibility="gone">
-
+
+ android:layout_marginTop="6dp"
+ android:orientation="horizontal">
+
+
+
+
+
@@ -206,6 +221,19 @@
android:textIsSelectable="true" />
+
+
+
diff --git a/controller/app/src/main/res/values-ar/strings.xml b/controller/app/src/main/res/values-ar/strings.xml
index 8f4d6728..3a642751 100644
--- a/controller/app/src/main/res/values-ar/strings.xml
+++ b/controller/app/src/main/res/values-ar/strings.xml
@@ -638,6 +638,14 @@
استبدال النظام
تمت استعادة النظام
فشلت الاستعادة
+ إلغاء الاستعادة?
+ نظامك الحالي دون تغيير. يمكنك بدء الاستعادة مرة أخرى في أي وقت.
+ إلغاء الاستعادة
+ متابعة الاستعادة
+ تتم كتابة نظامك الآن. سيؤدي الإلغاء إلى جعله غير قابل للاستخدام حتى تتم إعادة تثبيته تلقائيًا.
+ أفهم أن الإلغاء الآن قد يترك نظامي تالفًا أو غير قابل للاستخدام.
+ تم إلغاء الاستعادة. سيبدأ نظامك في وضع الاسترداد عند التشغيل التالي.
+ حدد المربع لتأكيد فهمك.
تعذّرت قراءة الملف المحدد، لذا لم يتغيّر أي شيء.
النسخ الاحتياطي والاستعادة
يُبقي عملية نسخ احتياطي أو استعادة قيد التشغيل بأمان في الخلفية
diff --git a/controller/app/src/main/res/values-az/strings.xml b/controller/app/src/main/res/values-az/strings.xml
index fabaee95..b6ee64bc 100644
--- a/controller/app/src/main/res/values-az/strings.xml
+++ b/controller/app/src/main/res/values-az/strings.xml
@@ -658,6 +658,14 @@
Sistemi əvəz et
Sistem bərpa olundu
Bərpa alınmadı
+ Bərpanı ləğv edilsin?
+ Cari sisteminiz dəyişməyib. Bərpanı istənilən vaxt yenidən başlada bilərsiniz.
+ Bərpanı ləğv et
+ Bərpanı davam etdir
+ Sisteminiz indi yazılır. Ləğv etmək onu özünü yenidən quraşdırana qədər yararsız hala salacaq.
+ İndi ləğv etməyin sistemimi zədələnmiş və ya yararsız qoya biləcəyini başa düşürəm.
+ Bərpa ləğv edildi. Sisteminiz növbəti işə salınmada bərpa rejimində başlayacaq.
+ Başa düşdüyünüzü təsdiqləmək üçün qutunu işarələyin.
Seçilmiş fayl oxuna bilmədi, ona görə heç nə dəyişdirilmədi.
Ehtiyat nüsxə & bərpa
Ehtiyat nüsxə və ya bərpanı arxa planda təhlükəsiz işlədir
diff --git a/controller/app/src/main/res/values-bg/strings.xml b/controller/app/src/main/res/values-bg/strings.xml
index 6e58e643..40d9349b 100644
--- a/controller/app/src/main/res/values-bg/strings.xml
+++ b/controller/app/src/main/res/values-bg/strings.xml
@@ -645,6 +645,14 @@
Замени системата
Системата е възстановена
Неуспешно възстановяване
+ Отказ на възстановяването?
+ Текущата ви система е непроменена. Можете да стартирате възстановяването отново по всяко време.
+ Отказ на възстановяването
+ Продължи възстановяването
+ Системата ви се записва в момента. Отказът ще я направи неизползваема, докато не се преинсталира сама.
+ Разбирам, че отказът сега може да остави системата ми повредена или неизползваема.
+ Възстановяването е отказано. Системата ви ще стартира в режим на възстановяване при следващото стартиране.
+ Отметнете полето, за да потвърдите, че разбирате.
Избраният файл не можа да бъде прочетен, затова нищо не беше променено.
Резервно копие и възстановяване
Поддържа безопасното изпълнение на резервно копие или възстановяване във фонов режим
diff --git a/controller/app/src/main/res/values-bn/strings.xml b/controller/app/src/main/res/values-bn/strings.xml
index cdfd2570..35ce5781 100644
--- a/controller/app/src/main/res/values-bn/strings.xml
+++ b/controller/app/src/main/res/values-bn/strings.xml
@@ -651,6 +651,14 @@
সিস্টেম প্রতিস্থাপন করুন
সিস্টেম পুনরুদ্ধার হয়েছে
পুনরুদ্ধার ব্যর্থ হয়েছে
+ পুনরুদ্ধার বাতিল করবেন?
+ আপনার বর্তমান সিস্টেম অপরিবর্তিত আছে। আপনি যেকোনো সময় আবার পুনরুদ্ধার শুরু করতে পারেন।
+ পুনরুদ্ধার বাতিল করুন
+ পুনরুদ্ধার চালিয়ে যান
+ আপনার সিস্টেম এখন লেখা হচ্ছে। বাতিল করলে এটি নিজে থেকে পুনরায় ইনস্টল না হওয়া পর্যন্ত ব্যবহারের অযোগ্য থাকবে।
+ আমি বুঝি যে এখন বাতিল করলে আমার সিস্টেম ক্ষতিগ্রস্ত বা ব্যবহারের অযোগ্য হয়ে যেতে পারে।
+ পুনরুদ্ধার বাতিল করা হয়েছে। পরবর্তী চালুতে আপনার সিস্টেম রিকভারি মোডে শুরু হবে।
+ আপনি বুঝেছেন তা নিশ্চিত করতে বাক্সটি চেক করুন।
নির্বাচিত ফাইলটি পড়া যায়নি, তাই কিছুই পরিবর্তন করা হয়নি।
ব্যাকআপ ও পুনরুদ্ধার
ব্যাকআপ বা পুনরুদ্ধার ব্যাকগ্রাউন্ডে নিরাপদে চালু রাখে
diff --git a/controller/app/src/main/res/values-cs/strings.xml b/controller/app/src/main/res/values-cs/strings.xml
index 0d79e433..967a6ec9 100644
--- a/controller/app/src/main/res/values-cs/strings.xml
+++ b/controller/app/src/main/res/values-cs/strings.xml
@@ -645,6 +645,14 @@
Nahradit systém
Systém obnoven
Obnova selhala
+ Zrušit obnovení?
+ Váš aktuální systém zůstává nezměněn. Obnovení můžete kdykoli spustit znovu.
+ Zrušit obnovení
+ Pokračovat v obnovení
+ Váš systém se právě zapisuje. Zrušení jej ponechá nepoužitelný, dokud se sám znovu nenainstaluje.
+ Rozumím, že zrušení nyní může můj systém poškodit nebo učinit nepoužitelným.
+ Obnovení zrušeno. Váš systém se při příštím spuštění spustí v režimu obnovy.
+ Zaškrtnutím políčka potvrďte, že rozumíte.
Vybraný soubor se nepodařilo přečíst, takže se nic nezměnilo.
Záloha & obnova
Udržuje zálohu nebo obnovu bezpečně spuštěnou na pozadí
diff --git a/controller/app/src/main/res/values-de/strings.xml b/controller/app/src/main/res/values-de/strings.xml
index cd919f93..f46aa1af 100644
--- a/controller/app/src/main/res/values-de/strings.xml
+++ b/controller/app/src/main/res/values-de/strings.xml
@@ -638,6 +638,14 @@
System ersetzen
System wiederhergestellt
Wiederherstellung fehlgeschlagen
+ Wiederherstellung abbrechen?
+ Ihr aktuelles System bleibt unverändert. Sie können die Wiederherstellung jederzeit erneut starten.
+ Wiederherstellung abbrechen
+ Wiederherstellung fortsetzen
+ Ihr System wird gerade geschrieben. Ein Abbruch macht es unbrauchbar, bis es sich selbst neu installiert.
+ Ich verstehe, dass ein Abbruch jetzt mein System beschädigen oder unbrauchbar machen kann.
+ Wiederherstellung abgebrochen. Ihr System startet beim nächsten Start im Wiederherstellungsmodus.
+ Aktivieren Sie das Kontrollkästchen, um zu bestätigen, dass Sie es verstanden haben.
Die ausgewählte Datei konnte nicht gelesen werden, daher wurde nichts geändert.
Sichern & wiederherstellen
Hält ein Backup oder eine Wiederherstellung sicher im Hintergrund am Laufen
diff --git a/controller/app/src/main/res/values-el/strings.xml b/controller/app/src/main/res/values-el/strings.xml
index 5853cae6..867c0bf0 100644
--- a/controller/app/src/main/res/values-el/strings.xml
+++ b/controller/app/src/main/res/values-el/strings.xml
@@ -645,6 +645,14 @@
Αντικατάσταση συστήματος
Το σύστημα επαναφέρθηκε
Αποτυχία επαναφοράς
+ Ακύρωση επαναφοράς?
+ Το τρέχον σύστημά σας παραμένει αμετάβλητο. Μπορείτε να ξεκινήσετε ξανά την επαναφορά οποιαδήποτε στιγμή.
+ Ακύρωση επαναφοράς
+ Συνέχεια επαναφοράς
+ Το σύστημά σας εγγράφεται τώρα. Η ακύρωση θα το αφήσει άχρηστο μέχρι να επανεγκατασταθεί μόνο του.
+ Καταλαβαίνω ότι η ακύρωση τώρα μπορεί να αφήσει το σύστημά μου κατεστραμμένο ή άχρηστο.
+ Η επαναφορά ακυρώθηκε. Το σύστημά σας θα ξεκινήσει σε λειτουργία ανάκτησης κατά την επόμενη εκκίνηση.
+ Επιλέξτε το πλαίσιο για να επιβεβαιώσετε ότι καταλαβαίνετε.
Δεν ήταν δυνατή η ανάγνωση του επιλεγμένου αρχείου, επομένως δεν άλλαξε τίποτα.
Αντίγραφο ασφαλείας & επαναφορά
Διατηρεί ένα αντίγραφο ασφαλείας ή μια επαναφορά σε ασφαλή εκτέλεση στο παρασκήνιο
diff --git a/controller/app/src/main/res/values-es/strings.xml b/controller/app/src/main/res/values-es/strings.xml
index 30c768e9..5550364d 100644
--- a/controller/app/src/main/res/values-es/strings.xml
+++ b/controller/app/src/main/res/values-es/strings.xml
@@ -714,6 +714,14 @@
Reemplazar sistema
Sistema restaurado
La restauración falló
+ ¿Cancelar la restauración?
+ Tu sistema actual no ha cambiado. Puedes iniciar la restauración de nuevo en cualquier momento.
+ Cancelar restauración
+ Continuar restaurando
+ Tu sistema se está escribiendo ahora. Cancelar lo dejará inutilizable hasta que se reinstale por sí solo.
+ Entiendo que cancelar ahora puede dejar mi sistema dañado o inutilizable.
+ Restauración cancelada. Tu sistema se iniciará en modo de recuperación en el próximo arranque.
+ Marca la casilla para confirmar que lo entiendes.
No se pudo leer el archivo seleccionado, así que no se cambió nada.
Copia de seguridad y restauración
Mantiene una copia o restauración ejecutándose de forma segura en segundo plano
diff --git a/controller/app/src/main/res/values-fa/strings.xml b/controller/app/src/main/res/values-fa/strings.xml
index fecb922d..85dcd518 100644
--- a/controller/app/src/main/res/values-fa/strings.xml
+++ b/controller/app/src/main/res/values-fa/strings.xml
@@ -638,6 +638,14 @@
جایگزینی سیستم
سیستم بازیابی شد
بازیابی ناموفق بود
+ لغو بازیابی?
+ سیستم فعلی شما بدون تغییر است. میتوانید بازیابی را در هر زمان دوباره شروع کنید.
+ لغو بازیابی
+ ادامه بازیابی
+ سیستم شما در حال نوشته شدن است. لغو کردن آن را تا زمانی که خودش را دوباره نصب کند غیرقابل استفاده میکند.
+ میفهمم که لغو کردن اکنون ممکن است سیستم من را آسیبدیده یا غیرقابل استفاده کند.
+ بازیابی لغو شد. سیستم شما در راهاندازی بعدی در حالت بازیابی شروع میشود.
+ برای تأیید اینکه متوجه شدید، کادر را علامت بزنید.
فایل انتخابشده خوانده نشد، بنابراین چیزی تغییر نکرد.
پشتیبانگیری و بازیابی
پشتیبانگیری یا بازیابی را با ایمنی در پسزمینه اجرا نگه میدارد
diff --git a/controller/app/src/main/res/values-fr/strings.xml b/controller/app/src/main/res/values-fr/strings.xml
index 549cdd34..a6d3f650 100644
--- a/controller/app/src/main/res/values-fr/strings.xml
+++ b/controller/app/src/main/res/values-fr/strings.xml
@@ -716,6 +716,14 @@
Remplacer le système
Système restauré
Échec de la restauration
+ Annuler la restauration?
+ Votre système actuel est inchangé. Vous pouvez relancer la restauration à tout moment.
+ Annuler la restauration
+ Continuer la restauration
+ Votre système est en cours d\'écriture. L\'annulation le rendra inutilisable jusqu\'à ce qu\'il se réinstalle.
+ Je comprends que l\'annulation maintenant peut laisser mon système endommagé ou inutilisable.
+ Restauration annulée. Votre système démarrera en mode récupération au prochain lancement.
+ Cochez la case pour confirmer que vous avez compris.
Le fichier sélectionné n\'a pas pu être lu, donc rien n\'a été modifié.
Sauvegarde & restauration
Maintient une sauvegarde ou une restauration en cours en toute sécurité en arrière-plan
diff --git a/controller/app/src/main/res/values-gu/strings.xml b/controller/app/src/main/res/values-gu/strings.xml
index d499fae9..d82fc630 100644
--- a/controller/app/src/main/res/values-gu/strings.xml
+++ b/controller/app/src/main/res/values-gu/strings.xml
@@ -651,6 +651,14 @@
સિસ્ટમ બદલો
સિસ્ટમ રિસ્ટોર થઈ
રિસ્ટોર નિષ્ફળ
+ પુનઃસ્થાપન રદ કરવું છે?
+ તમારી વર્તમાન સિસ્ટમ યથાવત છે. તમે કોઈપણ સમયે ફરીથી પુનઃસ્થાપન શરૂ કરી શકો છો.
+ પુનઃસ્થાપન રદ કરો
+ પુનઃસ્થાપન ચાલુ રાખો
+ તમારી સિસ્ટમ હમણાં લખાઈ રહી છે. રદ કરવાથી તે પોતાને ફરીથી ઇન્સ્ટોલ ન કરે ત્યાં સુધી બિનઉપયોગી રહેશે.
+ હું સમજું છું કે હમણાં રદ કરવાથી મારી સિસ્ટમ ક્ષતિગ્રસ્ત અથવા બિનઉપયોગી થઈ શકે છે.
+ પુનઃસ્થાપન રદ થયું. તમારી સિસ્ટમ આગલા લોન્ચ પર રિકવરી મોડમાં શરૂ થશે.
+ તમે સમજ્યા છો તેની પુષ્ટિ કરવા બોક્સ પર ટિક કરો.
પસંદ કરેલી ફાઇલ વાંચી શકાઈ નહીં, તેથી કંઈ બદલાયું નથી.
બૅકઅપ & રિસ્ટોર
બૅકઅપ કે રિસ્ટોરને બૅકગ્રાઉન્ડમાં સુરક્ષિત રીતે ચાલુ રાખે છે
diff --git a/controller/app/src/main/res/values-hi/strings.xml b/controller/app/src/main/res/values-hi/strings.xml
index 1ebd30ba..994a63d2 100644
--- a/controller/app/src/main/res/values-hi/strings.xml
+++ b/controller/app/src/main/res/values-hi/strings.xml
@@ -715,6 +715,14 @@
सिस्टम बदलें
सिस्टम पुनर्स्थापित हुआ
पुनर्स्थापना विफल
+ पुनर्स्थापना रद्द करें?
+ आपका वर्तमान सिस्टम अपरिवर्तित है। आप कभी भी पुनर्स्थापना फिर से शुरू कर सकते हैं।
+ पुनर्स्थापना रद्द करें
+ पुनर्स्थापना जारी रखें
+ आपका सिस्टम अभी लिखा जा रहा है। रद्द करने पर यह तब तक अनुपयोगी रहेगा जब तक यह खुद को फिर से इंस्टॉल न कर ले।
+ मैं समझता हूँ कि अभी रद्द करने से मेरा सिस्टम क्षतिग्रस्त या अनुपयोगी हो सकता है।
+ पुनर्स्थापना रद्द कर दी गई। आपका सिस्टम अगली बार शुरू होने पर रिकवरी मोड में शुरू होगा।
+ पुष्टि करने के लिए बॉक्स को चेक करें कि आप समझते हैं।
चुनी गई फ़ाइल पढ़ी नहीं जा सकी, इसलिए कुछ भी नहीं बदला गया।
बैकअप और पुनर्स्थापना
बैकअप या पुनर्स्थापना को बैकग्राउंड में सुरक्षित रूप से चलाए रखता है
diff --git a/controller/app/src/main/res/values-hu/strings.xml b/controller/app/src/main/res/values-hu/strings.xml
index bc783bc3..65934e49 100644
--- a/controller/app/src/main/res/values-hu/strings.xml
+++ b/controller/app/src/main/res/values-hu/strings.xml
@@ -638,6 +638,14 @@
Rendszer cseréje
A rendszer visszaállítva
A visszaállítás nem sikerült
+ Megszakítod a visszaállítást?
+ A jelenlegi rendszered változatlan. A visszaállítást bármikor újraindíthatod.
+ Visszaállítás megszakítása
+ Visszaállítás folytatása
+ A rendszered éppen íródik. A megszakítás használhatatlanná teszi, amíg újra nem telepíti magát.
+ Megértem, hogy a mostani megszakítás sérültté vagy használhatatlanná teheti a rendszeremet.
+ Visszaállítás megszakítva. A rendszered a következő indításkor helyreállítási módban indul.
+ Jelöld be a négyzetet a megerősítéshez.
A kiválasztott fájlt nem sikerült beolvasni, ezért semmi sem változott.
Biztonsági mentés & visszaállítás
Biztonságosan futtatja a mentést vagy visszaállítást a háttérben
diff --git a/controller/app/src/main/res/values-in/strings.xml b/controller/app/src/main/res/values-in/strings.xml
index b2cc705f..ed385f4c 100644
--- a/controller/app/src/main/res/values-in/strings.xml
+++ b/controller/app/src/main/res/values-in/strings.xml
@@ -645,6 +645,14 @@
Ganti sistem
Sistem dipulihkan
Pemulihan gagal
+ Batalkan pemulihan?
+ Sistem Anda saat ini tidak berubah. Anda dapat memulai pemulihan lagi kapan saja.
+ Batalkan pemulihan
+ Lanjutkan pemulihan
+ Sistem Anda sedang ditulis sekarang. Membatalkan akan membuatnya tidak dapat digunakan hingga menginstal ulang dirinya sendiri.
+ Saya memahami bahwa membatalkan sekarang dapat membuat sistem saya rusak atau tidak dapat digunakan.
+ Pemulihan dibatalkan. Sistem Anda akan memulai dalam mode pemulihan pada peluncuran berikutnya.
+ Centang kotak untuk mengonfirmasi bahwa Anda memahami.
Berkas yang dipilih tidak dapat dibaca, jadi tidak ada yang diubah.
Cadangkan & pulihkan
Menjaga pencadangan atau pemulihan berjalan aman di latar belakang
diff --git a/controller/app/src/main/res/values-it/strings.xml b/controller/app/src/main/res/values-it/strings.xml
index 7d4d1cbf..6a15d397 100644
--- a/controller/app/src/main/res/values-it/strings.xml
+++ b/controller/app/src/main/res/values-it/strings.xml
@@ -638,6 +638,14 @@
Sostituisci sistema
Sistema ripristinato
Ripristino non riuscito
+ Annullare il ripristino?
+ Il tuo sistema attuale è invariato. Puoi riavviare il ripristino in qualsiasi momento.
+ Annulla ripristino
+ Continua il ripristino
+ Il tuo sistema è in fase di scrittura. L\'annullamento lo renderà inutilizzabile finché non si reinstalla da solo.
+ Capisco che annullare ora potrebbe lasciare il mio sistema danneggiato o inutilizzabile.
+ Ripristino annullato. Il tuo sistema si avvierà in modalità di ripristino al prossimo avvio.
+ Seleziona la casella per confermare di aver capito.
Non è stato possibile leggere il file selezionato, quindi non è stato modificato nulla.
Backup & ripristino
Mantiene un backup o ripristino in esecuzione in modo sicuro in background
diff --git a/controller/app/src/main/res/values-ja/strings.xml b/controller/app/src/main/res/values-ja/strings.xml
index bb00b365..682f9856 100644
--- a/controller/app/src/main/res/values-ja/strings.xml
+++ b/controller/app/src/main/res/values-ja/strings.xml
@@ -639,6 +639,14 @@
システムを置き換え
システムを復元しました
復元に失敗しました
+ 復元をキャンセルしますか?
+ 現在のシステムは変更されていません。いつでも復元を再開できます。
+ 復元をキャンセル
+ 復元を続ける
+ システムは現在書き込み中です。キャンセルすると、システムが自動的に再インストールされるまで使用できなくなります。
+ 今キャンセルすると、システムが破損したり使用できなくなったりする可能性があることを理解しています。
+ 復元をキャンセルしました。次回の起動時にシステムはリカバリーモードで起動します。
+ 理解したことを確認するにはチェックボックスをオンにしてください。
選択したファイルを読み取れなかったため、何も変更されていません。
バックアップと復元
バックアップや復元をバックグラウンドで安全に実行し続けます
diff --git a/controller/app/src/main/res/values-ko/strings.xml b/controller/app/src/main/res/values-ko/strings.xml
index 38a8f807..897ed539 100644
--- a/controller/app/src/main/res/values-ko/strings.xml
+++ b/controller/app/src/main/res/values-ko/strings.xml
@@ -639,6 +639,14 @@
시스템 대체
시스템 복원됨
복원 실패
+ 복원을 취소하시겠습니까?
+ 현재 시스템은 변경되지 않았습니다. 언제든지 복원을 다시 시작할 수 있습니다.
+ 복원 취소
+ 복원 계속하기
+ 시스템을 지금 기록하는 중입니다. 취소하면 시스템이 자동으로 재설치될 때까지 사용할 수 없게 됩니다.
+ 지금 취소하면 시스템이 손상되거나 사용할 수 없게 될 수 있음을 이해합니다.
+ 복원이 취소되었습니다. 다음 실행 시 시스템이 복구 모드로 시작됩니다.
+ 이해했음을 확인하려면 확인란을 선택하세요.
선택한 파일을 읽을 수 없어 아무것도 변경되지 않았습니다.
백업 및 복원
백업 또는 복원을 백그라운드에서 안전하게 계속 실행합니다
diff --git a/controller/app/src/main/res/values-lt/strings.xml b/controller/app/src/main/res/values-lt/strings.xml
index 1b1f0501..1b01fe3c 100644
--- a/controller/app/src/main/res/values-lt/strings.xml
+++ b/controller/app/src/main/res/values-lt/strings.xml
@@ -646,6 +646,14 @@
Pakeisti sistemą
Sistema atkurta
Nepavyko atkurti
+ Atšaukti atkūrimą?
+ Jūsų dabartinė sistema nepakeista. Atkūrimą galite pradėti iš naujo bet kada.
+ Atšaukti atkūrimą
+ Tęsti atkūrimą
+ Jūsų sistema dabar įrašoma. Atšaukus ji liks netinkama naudoti, kol pati iš naujo neįsidiegs.
+ Suprantu, kad atšaukus dabar mano sistema gali likti sugadinta arba netinkama naudoti.
+ Atkūrimas atšauktas. Kito paleidimo metu jūsų sistema pasileis atkūrimo režimu.
+ Pažymėkite langelį, kad patvirtintumėte, jog suprantate.
Nepavyko perskaityti pasirinkto failo, todėl niekas nebuvo pakeista.
Atsarginė kopija ir atkūrimas
Saugiai vykdo atsarginės kopijos kūrimą ar atkūrimą fone
diff --git a/controller/app/src/main/res/values-nl/strings.xml b/controller/app/src/main/res/values-nl/strings.xml
index 7642952c..c3d688d2 100644
--- a/controller/app/src/main/res/values-nl/strings.xml
+++ b/controller/app/src/main/res/values-nl/strings.xml
@@ -638,6 +638,14 @@
Systeem vervangen
Systeem hersteld
Herstel mislukt
+ Herstel annuleren?
+ Uw huidige systeem is ongewijzigd. U kunt het herstel op elk moment opnieuw starten.
+ Herstel annuleren
+ Doorgaan met herstellen
+ Uw systeem wordt nu geschreven. Annuleren maakt het onbruikbaar totdat het zichzelf opnieuw installeert.
+ Ik begrijp dat annuleren nu mijn systeem beschadigd of onbruikbaar kan achterlaten.
+ Herstel geannuleerd. Uw systeem start bij de volgende keer opstarten in de herstelmodus.
+ Vink het vakje aan om te bevestigen dat u het begrijpt.
Het geselecteerde bestand kon niet worden gelezen, dus er is niets gewijzigd.
Back-up & herstel
Houdt een back-up of herstel veilig actief op de achtergrond
diff --git a/controller/app/src/main/res/values-no/strings.xml b/controller/app/src/main/res/values-no/strings.xml
index 8a11d084..0e6afb09 100644
--- a/controller/app/src/main/res/values-no/strings.xml
+++ b/controller/app/src/main/res/values-no/strings.xml
@@ -639,6 +639,14 @@
Erstatt system
System gjenopprettet
Gjenoppretting mislyktes
+ Avbryte gjenopprettingen?
+ Det nåværende systemet ditt er uendret. Du kan starte gjenopprettingen på nytt når som helst.
+ Avbryt gjenoppretting
+ Fortsett gjenoppretting
+ Systemet ditt skrives nå. Å avbryte vil gjøre det ubrukelig til det installerer seg selv på nytt.
+ Jeg forstår at å avbryte nå kan gjøre systemet mitt skadet eller ubrukelig.
+ Gjenoppretting avbrutt. Systemet ditt starter i gjenopprettingsmodus ved neste oppstart.
+ Kryss av i boksen for å bekrefte at du forstår.
Den valgte filen kunne ikke leses, så ingenting ble endret.
Sikkerhetskopiering & gjenoppretting
Holder en sikkerhetskopiering eller gjenoppretting trygt i gang i bakgrunnen
diff --git a/controller/app/src/main/res/values-pl/strings.xml b/controller/app/src/main/res/values-pl/strings.xml
index 5f5a6494..f9ea4b7d 100644
--- a/controller/app/src/main/res/values-pl/strings.xml
+++ b/controller/app/src/main/res/values-pl/strings.xml
@@ -639,6 +639,14 @@
Zastąp system
System przywrócony
Przywracanie nie powiodło się
+ Anulować przywracanie?
+ Twój obecny system pozostaje bez zmian. Możesz ponownie rozpocząć przywracanie w dowolnym momencie.
+ Anuluj przywracanie
+ Kontynuuj przywracanie
+ Twój system jest teraz zapisywany. Anulowanie sprawi, że będzie bezużyteczny, dopóki nie zainstaluje się ponownie.
+ Rozumiem, że anulowanie teraz może pozostawić mój system uszkodzony lub bezużyteczny.
+ Przywracanie anulowane. Twój system uruchomi się w trybie odzyskiwania przy następnym uruchomieniu.
+ Zaznacz pole, aby potwierdzić, że rozumiesz.
Nie udało się odczytać wybranego pliku, więc nic nie zostało zmienione.
Kopia zapasowa & przywracanie
Utrzymuje bezpieczne działanie kopii lub przywracania w tle
diff --git a/controller/app/src/main/res/values-pt/strings.xml b/controller/app/src/main/res/values-pt/strings.xml
index 84b48f01..cba2d95f 100644
--- a/controller/app/src/main/res/values-pt/strings.xml
+++ b/controller/app/src/main/res/values-pt/strings.xml
@@ -717,6 +717,14 @@
Substituir sistema
Sistema restaurado
O restauro falhou
+ Cancelar o restauro?
+ O seu sistema atual permanece inalterado. Pode iniciar o restauro novamente a qualquer momento.
+ Cancelar restauro
+ Continuar o restauro
+ O seu sistema está a ser escrito agora. Cancelar deixá-lo-á inutilizável até que se reinstale sozinho.
+ Compreendo que cancelar agora pode deixar o meu sistema danificado ou inutilizável.
+ Restauro cancelado. O seu sistema iniciará em modo de recuperação no próximo arranque.
+ Marque a caixa para confirmar que compreende.
Não foi possível ler o ficheiro selecionado, por isso nada foi alterado.
Cópia de segurança & restauro
Mantém uma cópia de segurança ou restauro a decorrer com segurança em segundo plano
diff --git a/controller/app/src/main/res/values-ro/strings.xml b/controller/app/src/main/res/values-ro/strings.xml
index d3144a9d..f299f5c7 100644
--- a/controller/app/src/main/res/values-ro/strings.xml
+++ b/controller/app/src/main/res/values-ro/strings.xml
@@ -638,6 +638,14 @@
Înlocuiește sistemul
Sistem restaurat
Restaurarea a eșuat
+ Anulați restaurarea?
+ Sistemul dvs. actual rămâne neschimbat. Puteți reîncepe restaurarea oricând.
+ Anulează restaurarea
+ Continuă restaurarea
+ Sistemul dvs. este scris acum. Anularea îl va lăsa inutilizabil până când se reinstalează singur.
+ Înțeleg că anularea acum îmi poate lăsa sistemul deteriorat sau inutilizabil.
+ Restaurare anulată. Sistemul dvs. va porni în modul de recuperare la următoarea lansare.
+ Bifați caseta pentru a confirma că înțelegeți.
Fișierul selectat nu a putut fi citit, așa că nu s-a schimbat nimic.
Copie de rezervă & restaurare
Menține o copie de rezervă sau o restaurare rulând în siguranță în fundal
diff --git a/controller/app/src/main/res/values-ru-rRU/strings.xml b/controller/app/src/main/res/values-ru-rRU/strings.xml
index 0d53269b..e25dcf74 100644
--- a/controller/app/src/main/res/values-ru-rRU/strings.xml
+++ b/controller/app/src/main/res/values-ru-rRU/strings.xml
@@ -714,6 +714,14 @@
Заменить систему
Система восстановлена
Не удалось восстановить
+ Отменить восстановление?
+ Ваша текущая система не изменена. Вы можете начать восстановление заново в любое время.
+ Отменить восстановление
+ Продолжить восстановление
+ Ваша система сейчас записывается. Отмена сделает её непригодной для использования, пока она не переустановится сама.
+ Я понимаю, что отмена сейчас может привести к повреждению или непригодности моей системы.
+ Восстановление отменено. При следующем запуске ваша система запустится в режиме восстановления.
+ Установите флажок, чтобы подтвердить, что вы понимаете.
Не удалось прочитать выбранный файл, поэтому ничего не изменилось.
Резервное копирование и восстановление
Позволяет безопасно выполнять резервное копирование или восстановление в фоне
diff --git a/controller/app/src/main/res/values-sk/strings.xml b/controller/app/src/main/res/values-sk/strings.xml
index a1835fe2..a99e7518 100644
--- a/controller/app/src/main/res/values-sk/strings.xml
+++ b/controller/app/src/main/res/values-sk/strings.xml
@@ -645,6 +645,14 @@
Nahradiť systém
Systém obnovený
Obnova zlyhala
+ Zrušiť obnovenie?
+ Váš aktuálny systém zostáva nezmenený. Obnovenie môžete kedykoľvek spustiť znova.
+ Zrušiť obnovenie
+ Pokračovať v obnovení
+ Váš systém sa práve zapisuje. Zrušenie ho ponechá nepoužiteľný, kým sa sám znova nenainštaluje.
+ Rozumiem, že zrušenie teraz môže môj systém poškodiť alebo urobiť nepoužiteľným.
+ Obnovenie zrušené. Váš systém sa pri ďalšom spustení spustí v režime obnovy.
+ Začiarknutím políčka potvrďte, že rozumiete.
Vybraný súbor sa nepodarilo prečítať, takže sa nič nezmenilo.
Zálohovanie & obnova
Udržiava zálohovanie alebo obnovu bezpečne bežiace na pozadí
diff --git a/controller/app/src/main/res/values-sr/strings.xml b/controller/app/src/main/res/values-sr/strings.xml
index 2bd36211..50dacbfb 100644
--- a/controller/app/src/main/res/values-sr/strings.xml
+++ b/controller/app/src/main/res/values-sr/strings.xml
@@ -651,6 +651,14 @@
Замени систем
Систем враћен
Враћање није успело
+ Отказати враћање?
+ Ваш тренутни систем је непромењен. Враћање можете поново да покренете у било ком тренутку.
+ Откажи враћање
+ Настави враћање
+ Ваш систем се сада уписује. Отказивање ће га учинити неупотребљивим док се сам поново не инсталира.
+ Разумем да отказивање сада може оставити мој систем оштећеним или неупотребљивим.
+ Враћање је отказано. Ваш систем ће се при следећем покретању покренути у режиму опоравка.
+ Означите поље да бисте потврдили да разумете.
Изабрана датотека није могла да се прочита, па ништа није промењено.
Резервна копија и враћање
Одржава прављење резервне копије или враћање безбедно у позадини
diff --git a/controller/app/src/main/res/values-sw/strings.xml b/controller/app/src/main/res/values-sw/strings.xml
index cad06eb6..9b326334 100644
--- a/controller/app/src/main/res/values-sw/strings.xml
+++ b/controller/app/src/main/res/values-sw/strings.xml
@@ -658,6 +658,14 @@
Badilisha mfumo
Mfumo umerejeshwa
Urejeshaji umeshindwa
+ Ghairi urejeshaji?
+ Mfumo wako wa sasa haujabadilika. Unaweza kuanza urejeshaji tena wakati wowote.
+ Ghairi urejeshaji
+ Endelea kurejesha
+ Mfumo wako unaandikwa sasa. Kughairi kutauacha usiofaa kutumika hadi ujisakinishe upya wenyewe.
+ Ninaelewa kuwa kughairi sasa kunaweza kuacha mfumo wangu ukiwa umeharibika au usiofaa kutumika.
+ Urejeshaji umeghairiwa. Mfumo wako utaanza katika hali ya urejeshaji wakati wa kuzindua ujao.
+ Weka alama kwenye kisanduku ili kuthibitisha kuwa unaelewa.
Faili uliyochagua haikuweza kusomwa, kwa hivyo hakuna kilichobadilishwa.
Hifadhi & rejesha
Huweka nakala rudufu au urejeshaji ukiendelea kwa usalama chinichini
diff --git a/controller/app/src/main/res/values-ta/strings.xml b/controller/app/src/main/res/values-ta/strings.xml
index 143f3763..c6070fb0 100644
--- a/controller/app/src/main/res/values-ta/strings.xml
+++ b/controller/app/src/main/res/values-ta/strings.xml
@@ -658,6 +658,14 @@
அமைப்பை மாற்று
அமைப்பு மீட்டமைக்கப்பட்டது
மீட்பு தோல்வியடைந்தது
+ மீட்பை ரத்து செய்யவா?
+ உங்கள் தற்போதைய கணினி மாறாமல் உள்ளது. நீங்கள் எந்த நேரத்திலும் மீட்பை மீண்டும் தொடங்கலாம்.
+ மீட்பை ரத்து செய்
+ மீட்பைத் தொடரவும்
+ உங்கள் கணினி இப்போது எழுதப்படுகிறது. ரத்து செய்தால், அது தானாகவே மீண்டும் நிறுவப்படும் வரை பயன்படுத்த முடியாததாக இருக்கும்.
+ இப்போது ரத்து செய்தால் எனது கணினி சேதமடையலாம் அல்லது பயன்படுத்த முடியாததாக மாறலாம் என்பதை நான் புரிந்துகொள்கிறேன்.
+ மீட்பு ரத்து செய்யப்பட்டது. அடுத்த முறை தொடங்கும்போது உங்கள் கணினி மீட்பு பயன்முறையில் தொடங்கும்.
+ நீங்கள் புரிந்துகொண்டதை உறுதிப்படுத்த பெட்டியில் தேர்வு செய்யவும்.
தேர்ந்தெடுத்த கோப்பைப் படிக்க முடியவில்லை, எனவே எதுவும் மாற்றப்படவில்லை.
காப்புப்பிரதி & மீட்பு
காப்புப்பிரதி அல்லது மீட்பை பின்னணியில் பாதுகாப்பாக இயக்கி வைக்கிறது
diff --git a/controller/app/src/main/res/values-tr/strings.xml b/controller/app/src/main/res/values-tr/strings.xml
index 54d4af83..0061dd7d 100644
--- a/controller/app/src/main/res/values-tr/strings.xml
+++ b/controller/app/src/main/res/values-tr/strings.xml
@@ -638,6 +638,14 @@
Sistemi değiştir
Sistem geri yüklendi
Geri yükleme başarısız
+ Geri yükleme iptal edilsin mi?
+ Mevcut sisteminiz değişmedi. Geri yüklemeyi istediğiniz zaman yeniden başlatabilirsiniz.
+ Geri yüklemeyi iptal et
+ Geri yüklemeye devam et
+ Sisteminiz şu anda yazılıyor. İptal etmek, kendini yeniden yükleyene kadar onu kullanılamaz hale getirir.
+ Şimdi iptal etmenin sistemimi hasarlı veya kullanılamaz bırakabileceğini anlıyorum.
+ Geri yükleme iptal edildi. Sisteminiz bir sonraki başlatmada kurtarma modunda başlayacak.
+ Anladığınızı onaylamak için kutuyu işaretleyin.
Seçilen dosya okunamadı, bu yüzden hiçbir şey değiştirilmedi.
Yedekle & geri yükle
Bir yedekleme veya geri yüklemeyi arka planda güvenle çalışır durumda tutar
diff --git a/controller/app/src/main/res/values-uk/strings.xml b/controller/app/src/main/res/values-uk/strings.xml
index d49f3a63..1137dd41 100644
--- a/controller/app/src/main/res/values-uk/strings.xml
+++ b/controller/app/src/main/res/values-uk/strings.xml
@@ -638,6 +638,14 @@
Замінити систему
Систему відновлено
Помилка відновлення
+ Скасувати відновлення?
+ Ваша поточна система не змінена. Ви можете розпочати відновлення знову будь-коли.
+ Скасувати відновлення
+ Продовжити відновлення
+ Ваша система зараз записується. Скасування зробить її непридатною до використання, доки вона не перевстановиться сама.
+ Я розумію, що скасування зараз може залишити мою систему пошкодженою або непридатною до використання.
+ Відновлення скасовано. Під час наступного запуску ваша система запуститься в режимі відновлення.
+ Позначте прапорець, щоб підтвердити, що ви розумієте.
Не вдалося прочитати вибраний файл, тому нічого не змінено.
Резервне копіювання та відновлення
Безпечно виконує резервне копіювання чи відновлення у фоні
diff --git a/controller/app/src/main/res/values-vi/strings.xml b/controller/app/src/main/res/values-vi/strings.xml
index f3e85bd9..6cc3b4aa 100644
--- a/controller/app/src/main/res/values-vi/strings.xml
+++ b/controller/app/src/main/res/values-vi/strings.xml
@@ -638,6 +638,14 @@
Thay thế hệ thống
Đã khôi phục hệ thống
Khôi phục thất bại
+ Hủy khôi phục?
+ Hệ thống hiện tại của bạn không thay đổi. Bạn có thể bắt đầu khôi phục lại bất cứ lúc nào.
+ Hủy khôi phục
+ Tiếp tục khôi phục
+ Hệ thống của bạn đang được ghi. Việc hủy sẽ khiến hệ thống không sử dụng được cho đến khi nó tự cài đặt lại.
+ Tôi hiểu rằng việc hủy bây giờ có thể khiến hệ thống của tôi bị hỏng hoặc không sử dụng được.
+ Đã hủy khôi phục. Hệ thống của bạn sẽ khởi động ở chế độ khôi phục trong lần khởi chạy tiếp theo.
+ Chọn ô để xác nhận rằng bạn hiểu.
Không thể đọc tệp đã chọn, nên không có gì bị thay đổi.
Sao lưu & khôi phục
Giữ quá trình sao lưu hoặc khôi phục chạy an toàn ở chế độ nền
diff --git a/controller/app/src/main/res/values-yo/strings.xml b/controller/app/src/main/res/values-yo/strings.xml
index 0b2cd7e3..20bf6b1a 100644
--- a/controller/app/src/main/res/values-yo/strings.xml
+++ b/controller/app/src/main/res/values-yo/strings.xml
@@ -658,6 +658,14 @@
Rọ́pò ẹ̀rọ
A ti mú ẹ̀rọ padà
Ìmúpadà kùnà
+ Fagilee ìmúpadà?
+ Ẹ̀rọ rẹ lọ́wọ́lọ́wọ́ kò yí padà. O le bẹ̀rẹ̀ ìmúpadà lẹ́ẹ̀kansi nígbàkúgbà.
+ Fagilee ìmúpadà
+ Tẹ̀síwájú ìmúpadà
+ Ẹ̀rọ rẹ ń kọ báyìí. Fífagilee yóò jẹ́ kí ó máṣe wúlò títí yóò fi tún ara rẹ̀ fi sori ẹrọ.
+ Mo ye mi pé fífagilee báyìí lè mú kí ẹ̀rọ mi bàjẹ́ tàbí kí ó máṣe wúlò.
+ A ti fagilee ìmúpadà. Ẹ̀rọ rẹ yóò bẹ̀rẹ̀ ní ipo ìgbàpadà ní ìfilọlẹ̀ tó kàn.
+ Sàmì sí àpótí náà láti jẹ́rìí sí i pé o ye ọ́.
A kò lè ka fáìlì tí a yàn, nítorí náà kò sí ohun tí ó yípadà.
Àfẹ̀yìntì & ìmúpadà
Ń jẹ́ kí àfẹ̀yìntì tàbí ìmúpadà máa ṣiṣẹ́ láìséwu lẹ́yìn ẹ̀rọ
diff --git a/controller/app/src/main/res/values-zh-rCN/strings.xml b/controller/app/src/main/res/values-zh-rCN/strings.xml
index 271211a9..3d184859 100644
--- a/controller/app/src/main/res/values-zh-rCN/strings.xml
+++ b/controller/app/src/main/res/values-zh-rCN/strings.xml
@@ -639,6 +639,14 @@
替换系统
系统已恢复
恢复失败
+ 取消恢复?
+ 您当前的系统保持不变。您可以随时重新开始恢复。
+ 取消恢复
+ 继续恢复
+ 您的系统正在写入。取消将使其无法使用,直到它自行重新安装。
+ 我明白现在取消可能会使我的系统损坏或无法使用。
+ 恢复已取消。您的系统将在下次启动时进入恢复模式。
+ 勾选此框以确认您已了解。
无法读取所选文件,因此未做任何更改。
备份与恢复
使备份或恢复在后台安全运行
diff --git a/controller/app/src/main/res/values/strings.xml b/controller/app/src/main/res/values/strings.xml
index 3ee8aae9..5e7ec71e 100644
--- a/controller/app/src/main/res/values/strings.xml
+++ b/controller/app/src/main/res/values/strings.xml
@@ -782,6 +782,16 @@
Replace system
System restored
Restore failed
+
+ Cancel restore?
+ Your current system is unchanged. You can start the restore again anytime.
+ Cancel restore
+ Keep restoring
+
+ Your system is being written now. Cancelling will leave it unusable until it reinstalls itself.
+ I understand that cancelling now may leave my system damaged or unusable.
+ Restore cancelled. Your system will start in recovery on the next launch.
+ Check the box to confirm you understand.
The selected file could not be read, so nothing was changed.
Backup & restore
Keeps a backup or restore running safely in the background
diff --git a/controller/docs/ADR-5343c-known-damage-from-cancelled-restore.md b/controller/docs/ADR-5343c-known-damage-from-cancelled-restore.md
new file mode 100644
index 00000000..133939dc
--- /dev/null
+++ b/controller/docs/ADR-5343c-known-damage-from-cancelled-restore.md
@@ -0,0 +1,170 @@
+# ADR-5343c — KNOWN damage from a force-cancelled restore (delta to ADR-5343)
+
+**Status:** Implemented under K2GO-384 (restore Cancel slice); maintainer sign-off + device verification pending.
+**Date:** 2026-09-04.
+**Deciders:** the maintainer.
+**Ticket:** K2GO-384 (standardize backup & restore: bar / % / ETA + **Cancel**), Epic ADFA-1028. Extends
+**ADR-5343** (`controller/docs/ADR-5343-server-lifecycle-reconciler.md`) and its Phase-5 delta **ADR-5343b**
+(`ADR-5343b-installguard-token-and-recovery-residue.md`, the InstallGuard session token / three-state marker).
+Sibling to **ADR-5343a** (flap recovery). Everything in ADR-5343 / 5343b stands.
+
+> Why this lives in K2GO-384 and not its own ticket: restore is a **data-touching** operation, so the lifecycle
+> of the artifact it damages is part of the same deliverable. The reduction gate (ADR-5343 §8) binds: **no new
+> source of truth, no new "who may act" special-case, no compensating flag.** This delta adds one *reading* on
+> the existing marker and one *lever* on an existing owner — the counts do not go up.
+
+---
+
+## 1. The gap
+
+The restore Cancel slice added an acknowledged **force-cancel during the destructive extract**: the user checks
+"I understand this may leave my system damaged", we kill `tar -x` mid-write, and the rootfs is now half
+overwritten. The destructive marker (`InstallGuard.begin`, planted at the verify→extract boundary) was left
+**LIVE** — the same launch's token, never `end()`ed on the failed path.
+
+A LIVE marker is the wrong fact for an *abandoned* write, and it produced three bad outcomes, all observed on
+device:
+
+1. **A fresh restore was blocked** with *"An install is in progress. Please wait for it to finish"*
+ (`k2go_busy_install`). Source: `EnvironmentLock.currentHolder → Holder.INSTALL` because
+ `InstallGuard.isLive` is true (`EnvironmentLock.java:216`). But nothing is installing — the op is over.
+2. **No in-session recovery.** The deep-op terminal fell into the retry/bifurcation screen, not the damaged
+ dialog. The in-session "damaged" path (`LibraryActivity` install observer, `:323-338`) only fires for the
+ *install* repository, not a deep-op restore.
+3. Recovery only arrived on a **relaunch** (token mismatch → INTERRUPTED → `LibraryActivity.recovering`), and a
+ LIVE marker in-session held the box down as `Holder.INSTALL` (desired=DOWN), so the reconciler never reached
+ the try-boot that the interrupted-install verdict needs — a deadlock until the process was restarted.
+
+The user's decision: a force-cancel should present as **damaged** and route to the existing recovery, and must
+**not** leave a blocking "install in progress" marker.
+
+## 2. The distinction — inferred vs known damage
+
+ADR-5343b/ADFA-5330 deliberately made an **INTERRUPTED** marker (a dead launch's token) fall through to
+`rootfsPresent` in `SystemStateEvaluator.isSystemInstalled`, so the reconciler **tries** to boot the base: a
+killed *module* install usually left a fine rootfs, and *whether it boots* is what separates a fine base from a
+damaged one. Booting is the diagnostic.
+
+A force-cancelled restore is different in one decisive way: **the damage is known, not inferred.** We tore the
+rootfs ourselves, mid-extract. There is nothing to learn from booting it — trying would only flap the reconciler
+(`desired=UP` every tick) on a base that cannot come up, which is precisely the "half-cooked" failure mode this
+project avoids. So known damage must keep the box **down** and go straight to recovery.
+
+That is exactly the case `ServerReconcile.desired`'s own invariant note anticipated:
+
+> *"If a health signal that is NOT marker-derived is ever added (e.g. a structural rootfs check), this invariant
+> must be revisited — desired would then have a reason to gate on health again."*
+
+We honor it **without** re-adding a health gate to `desired` (see §4).
+
+## 3. Decision — a fourth reading on the same marker
+
+`InstallGuard` gains a sentinel token `"DAMAGED"` (never a per-launch UUID) and two members: `markDamaged(ctx)`
+(overwrite the marker with the sentinel) and `isDamaged(ctx)` (token == sentinel). One marker, one new *reading* —
+no second file, no new flag.
+
+| Reading | Marker token | Meaning | `isLive` | `isInterrupted` | `isDamaged` | `isSystemInstalled` |
+|---|---|---|---|---|---|---|
+| ABSENT | (none) | nothing in progress | — | — | — | `rootfsPresent` |
+| LIVE | this launch's UUID | an install runs now, this process | ✓ | — | — | **false** |
+| INTERRUPTED | another launch's UUID | inferred damage (maybe fine) | — | ✓ | — | `rootfsPresent` (try-boot) |
+| **DAMAGED** | `"DAMAGED"` sentinel | **known** damage (torn on purpose) | — | ✓ | ✓ | **false** (never boot) |
+
+The design collapses to a single insight: **DAMAGED reads `isInterrupted` too**, so the entire recovery/verdict
+path (`LibraryActivity.recovering :235`, `evaluateRecovery :917`, `SystemFactsReader.verdict :108`,
+`SetupProgressActivity :186`) owns it with **zero changes**. The known-vs-inferred difference surfaces in exactly
+**one** reader — `SystemStateEvaluator.isSystemInstalled`, which forces `false` for DAMAGED as it already does
+for LIVE — and nowhere else. That single lever keeps `desired=DOWN` (no flap) while `isLive=false` lifts the
+`k2go_busy_install` gate so a fresh restore is allowed.
+
+The producer is one call site: `DeepOpService`'s extract `onError`. **Known damage is owned by "the extract
+began and did not complete", not by the cancel button** — so the guard is `InstallGuard.isLive` (the marker was
+planted at `onExtractStarting`, i.e. the rootfs was being written and is now torn), which covers *both* an
+acknowledged force-cancel *and* a real mid-write failure (disk full, tar crash). Both leave a torn rootfs, so
+both must drop out of the "install running" state and route to recovery. The `forced` flag only picks the
+user-facing *message* (the "damaged, next launch recovers" line vs. the raw diagnostic).
+
+### 3.1 Two cancel intents must not alias (the safe-zone / feeder split)
+
+The restore has a **safe zone** (copy + verify + the verify→extract boundary, where an abort touches nothing)
+and a **destructive zone** (the extract feeder, past the point of no return). These are two different cancel
+intents and must be **two separate tokens**, or a "system unchanged" confirm can reach the feeder and tear the
+rootfs across the boundary window:
+
+- `cancelBeforeExtract` — a confirmed abort, read **only** in the safe zone (copy loop, verify listing, boundary
+ check). Never read by the feeder.
+- `forceExtractCancel` — an acknowledged destructive kill, read **only** by the extract feeder.
+
+Both service→extractor tokens are passed explicitly to `TarExtractor.startExtraction`. And the service is the
+**authority** on which zone it is in: `ACTION_CANCEL_CONFIRM` sets `cancelBeforeExtract` only while
+`currentCancelKind == CANCELLABLE`, and `ACTION_FORCE_CANCEL` sets `forceExtractCancel` only while
+`DESTRUCTIVE` — so a UI action that races a phase change (the fragment's `cancelKind` is a lagging copy) is
+ignored by the service rather than misapplied. A confirm that arrives after the boundary simply lets the extract
+finish; the system is never torn by a dialog that told the user it was safe.
+
+### 3.2 A safe-zone cancel is a terminal of its own (CANCELLED)
+
+A user cancel in the safe zone is **not a failure**. `DeepOpState` gains a `CANCELLED` phase (mirroring
+`InstallState.Phase.CANCELLED`); the service posts it from a dedicated `finishCancelled()` terminal (release the
+lock, re-enable desired, never touch InstallGuard — nothing was planted). The screen returns to the bifurcation
+by branching on that phase, so the decision lives on the op's state and **survives a config change** — it is no
+longer a fragment-local flag that a recreation would drop.
+
+## 4. Why the reduction gate still holds
+
+- **No new source of truth.** The one marker file remains the single durable fact. `isDamaged` is a *reading* of
+ it, like `isLive`/`isInterrupted`.
+- **No new "who may act" special-case.** Recovery ownership is unchanged: `LibraryActivity` still owns it, still
+ keyed on `isInterrupted`.
+- **The `desired` invariant is intact.** `desired` still does **not** read `healthy`. Known damage is expressed
+ as `installed=false` — the argument `desired` already takes — via the same `isSystemInstalled` lever a LIVE
+ install uses. We did not give `desired` a new reason to gate on health; we told the existing `installed` fact
+ the truth (a rootfs we tore is not an installed system).
+- **Prefer removing over adding:** the change *removes* a false state (a LIVE marker over a finished op) and
+ replaces it with the honest one, rather than adding a flag to compensate for the false one.
+
+## 5. Lifecycle (who sets it, who clears it, what if the process dies)
+
+- **Sets DAMAGED:** `DeepOpService` extract `onError`, whenever `InstallGuard.isLive` (the destructive marker was
+ planted at the boundary, so the rootfs was being written and is now torn) — covering both an acknowledged
+ force-cancel and a real mid-write failure. A cancel *before* any write never planted a marker (`onCancelled`
+ path) and never reaches here.
+- **Clears it:** identical to INTERRUPTED. The recovery route's reinstall calls `InstallGuard.begin` (overwrites
+ the sentinel with a live token, then `end()` on success); a base that unexpectedly boots clears it via the
+ server-alive observer (`LibraryActivity:284`); an OK verdict clears it (`:936`). No new clearer.
+- **Process dies mid-cancel:** if `markDamaged` already wrote, the sentinel persists → next launch reads
+ DAMAGED (still `isInterrupted`) → recovery. If it died *before* `markDamaged`, the marker is still LIVE with a
+ now-dead token → next launch reads INTERRUPTED → recovery. Either way recovery fires; there is no deadlock and
+ no orphaned blocking state.
+
+## 6. What changed
+
+- `InstallGuard.java` — `DAMAGED_TOKEN` sentinel; `markDamaged(ctx)`; `isDamaged(ctx)`; class + `isInterrupted`
+ javadoc note the fourth reading.
+- `SystemStateEvaluator.isSystemInstalled` — force `false` on `isDamaged` (the single divergence).
+- `deepop/DeepOpService.java` — extract `onError` marks DAMAGED whenever `isLive` (extract began and failed);
+ a **separate** `forceExtractCancel` token for the feeder (de-aliased from `cancelBeforeExtract`, §3.1);
+ `ACTION_CANCEL_CONFIRM` / `ACTION_FORCE_CANCEL` guarded on the service's own `currentCancelKind`; a `passRunning`
+ guard so the re-callable verify+extract runs one pass at a time; a `finishCancelled()` terminal.
+- `deepop/DeepOpState.java` + `DeepOpProgressRepository.java` — a `CANCELLED` phase + `cancelled()`/`postCancelled()`
+ (§3.2), so a safe-zone cancel is a terminal that survives a config change.
+- `TarExtractor.java` — `startExtraction` takes the third `forceCancelDuringExtract` token; only the feeder reads
+ it (safe-zone tokens can never reach the destructive write).
+- `redesign/BackupJobFragment.java` — the safe-zone cancel dialog is non-cancelable (closes the pause-hang
+ lifecycle gap); the terminal branches on `CANCELLED` (the fragment-local `cancelling` flag is removed).
+- `env/domain/ServerReconcile.java` — the invariant note records that this signal was added without breaking it.
+- Terminal string `k2go_br_restore_damaged` = *"Restore cancelled. Your system will start in recovery on the next
+ launch."* (states the consequence, not an accusation of damage).
+
+## 7. Device verification (the real gate)
+
+On the test device (arm64 + a 32-bit build for ABI coverage), force-cancel a restore during the extract pass and
+confirm, without a relaunch:
+
+1. The terminal shows the *damaged* message (not retry/bifurcation).
+2. A fresh restore is **no longer blocked** by "An install is in progress".
+3. The box does **not** flap (no repeated `pdsm start` on the torn rootfs in the reconciler log; `desired=DOWN`).
+4. Returning to the library **or** relaunching lands on the damaged-recovery dialog; Recover → reinstall
+ repairs the system.
+5. Repeat with a force-cancel that races the very first extract write (boundary), and with a normal (non-forced)
+ extract error, to confirm the non-forced path is unchanged.