From 7b927f484c5f7f0a5c55d901c4c22acf1a1f9be3 Mon Sep 17 00:00:00 2001 From: Luis Guzman Date: Fri, 31 Jul 2026 10:09:20 -0600 Subject: [PATCH 1/2] ADFA-4971: don't misread a live restore as a killed install + confine Back MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A healthy restore reached Home and showed 'Installation interrupted — reinstall the app', and reopening landed on the boot gate instead of the restore screen. Both come from one miscalc: restore holds InstallGuard (correct), but LibraryActivity.recovering didn't exclude a live deep-op, so it read the restore as a killed install — firing the recovery AND (via the !recovering guard) blocking the backup/restore return-to-op routing. - LibraryActivity: recovering += !EnvironmentLock.ownerHeld(this). Kills the false 'reinstall' and unblocks the return-to-op routing, so a reopen/notification tap lands directly on the restore screen. A truly killed restore (process dead -> ownerHeld self-heals) still enters recovery. - BackupJobFragment: confine Back like the module index — first Back shows the soft hint, every Back after sends the app to the background (moveTaskToBack), never in-app navigation to a server-down Home. --- .../redesign/BackupJobFragment.java | 20 +++++++++++++------ .../controller/redesign/LibraryActivity.java | 8 +++++++- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/controller/app/src/main/java/org/iiab/controller/redesign/BackupJobFragment.java b/controller/app/src/main/java/org/iiab/controller/redesign/BackupJobFragment.java index e2ed026c..82b0daaf 100644 --- a/controller/app/src/main/java/org/iiab/controller/redesign/BackupJobFragment.java +++ b/controller/app/src/main/java/org/iiab/controller/redesign/BackupJobFragment.java @@ -70,6 +70,7 @@ public static BackupJobFragment newInstance(String mode) { private org.iiab.controller.util.EllipsisAnimator statusDots; private boolean running = false; private long lastSeq = -1L; + private boolean leaveWarned = false; // ADFA-4971: first Back reassures, later Backs background the app private androidx.activity.OnBackPressedCallback backGate; private final ActivityResultLauncher createDoc = @@ -108,12 +109,18 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c // Hard gate: while the op runs, back is consumed with a styled snackbar (module-index behavior). backGate = new androidx.activity.OnBackPressedCallback(false) { @Override public void handleOnBackPressed() { - // ADFA-4961 (B): the op keeps running in DeepOpService, so leaving doesn't interrupt it. - // Show a soft, mode-specific hint (anchored to the activity so it survives the pop) and - // return to the bifurcation — no hard block. The notification brings the user back. - Snackbars.make(requireActivity().findViewById(android.R.id.content), - isRestore() ? R.string.k2go_br_leave_restore : R.string.k2go_br_leave_backup).show(); - popToIntro(false); + // ADFA-4971: confine like the module index (SetupProgressActivity.onBackPressed). The op + // runs with the server stopped in DeepOpService; walking Back to a (server-down) Home is + // what triggered the false "reinstall" recovery. First Back reassures with the soft, + // mode-specific hint; every Back after sends the app to the background — never in-app + // navigation to Home. The op keeps running; the notification returns the user here. + if (!leaveWarned) { + leaveWarned = true; + Snackbars.make(requireActivity().findViewById(android.R.id.content), + isRestore() ? R.string.k2go_br_leave_restore : R.string.k2go_br_leave_backup).show(); + } else { + requireActivity().moveTaskToBack(true); + } } }; requireActivity().getOnBackPressedDispatcher().addCallback(getViewLifecycleOwner(), backGate); @@ -212,6 +219,7 @@ private void onDeepOpState(DeepOpState st) { private void beginRunning() { running = true; + leaveWarned = false; // ADFA-4971: each op's first Back re-warns before backgrounding if (backGate != null) backGate.setEnabled(true); finish.setVisibility(View.GONE); if (anim != null) anim.playAnimation(); diff --git a/controller/app/src/main/java/org/iiab/controller/redesign/LibraryActivity.java b/controller/app/src/main/java/org/iiab/controller/redesign/LibraryActivity.java index abf30f3b..a507df0f 100644 --- a/controller/app/src/main/java/org/iiab/controller/redesign/LibraryActivity.java +++ b/controller/app/src/main/java/org/iiab/controller/redesign/LibraryActivity.java @@ -153,7 +153,13 @@ public android.graphics.Typeface fetchFont(String fontFamily) { recovering = !installing && org.iiab.controller.InstallGuard.inProgress(this) && !InstallProgressRepository.get().current().isRunning() - && !org.iiab.controller.install.presentation.ModuleQueueRepository.get().isRunning(); + && !org.iiab.controller.install.presentation.ModuleQueueRepository.get().isRunning() + // ADFA-4971: a LIVE deep-env op (backup/restore, clone-receive) legitimately holds + // InstallGuard. Without this it was mistaken for a killed install → false "reinstall" + // dialog, and (via the !recovering guard) it blocked the return-to-op routing so a + // reopen fell to the boot gate instead of the op screen. ownerHeld self-heals after a + // true kill, so a genuinely interrupted restore still enters recovery. + && !org.iiab.controller.env.EnvironmentLock.ownerHeld(this); android.util.Log.i("K2Go-Recover", "onCreate recovering=" + recovering + " marker=" + org.iiab.controller.InstallGuard.inProgress(this) + " systemInstalled=" + systemInstalled From a569fde317547cfb17f697f2733b879f09cdbab1 Mon Sep 17 00:00:00 2001 From: Luis Guzman Date: Fri, 31 Jul 2026 19:02:50 -0600 Subject: [PATCH 2/2] ADFA-4971: only confine Back once the service owns the op (review #317) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Back confinement was active during the pre-service restore copy+validate phase too, where there's no DeepOpService notification yet (it starts at confirm) and nothing is destructive — so backgrounding there left no way back. Gate the confinement on DeepOpProgressRepository running for this owner; before that (copy/validate) a Back just cancels to the bifurcation. --- .../redesign/BackupJobFragment.java | 28 ++++++++++++------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/controller/app/src/main/java/org/iiab/controller/redesign/BackupJobFragment.java b/controller/app/src/main/java/org/iiab/controller/redesign/BackupJobFragment.java index 82b0daaf..0e74094a 100644 --- a/controller/app/src/main/java/org/iiab/controller/redesign/BackupJobFragment.java +++ b/controller/app/src/main/java/org/iiab/controller/redesign/BackupJobFragment.java @@ -109,17 +109,25 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c // Hard gate: while the op runs, back is consumed with a styled snackbar (module-index behavior). backGate = new androidx.activity.OnBackPressedCallback(false) { @Override public void handleOnBackPressed() { - // ADFA-4971: confine like the module index (SetupProgressActivity.onBackPressed). The op - // runs with the server stopped in DeepOpService; walking Back to a (server-down) Home is - // what triggered the false "reinstall" recovery. First Back reassures with the soft, - // mode-specific hint; every Back after sends the app to the background — never in-app - // navigation to Home. The op keeps running; the notification returns the user here. - if (!leaveWarned) { - leaveWarned = true; - Snackbars.make(requireActivity().findViewById(android.R.id.content), - isRestore() ? R.string.k2go_br_leave_restore : R.string.k2go_br_leave_backup).show(); + // ADFA-4971: confine like the module index (SetupProgressActivity.onBackPressed) — but + // only once the SERVICE owns the op (there is a notification and the work survives + // backgrounding). Then the op runs with the server stopped, and walking Back to a + // server-down Home is what triggered the false "reinstall" recovery: first Back + // reassures with the soft hint, every Back after sends the app to the background — never + // in-app nav to Home. During the pre-service copy+validate phase (restore) there's no + // notification yet and nothing is destructive, so a Back there just cancels to the + // bifurcation. + DeepOpState cur = DeepOpProgressRepository.get().current(); + if (cur.owner == myOwner() && cur.isRunning()) { + if (!leaveWarned) { + leaveWarned = true; + Snackbars.make(requireActivity().findViewById(android.R.id.content), + isRestore() ? R.string.k2go_br_leave_restore : R.string.k2go_br_leave_backup).show(); + } else { + requireActivity().moveTaskToBack(true); + } } else { - requireActivity().moveTaskToBack(true); + popToIntro(false); } } };