Skip to content

Commit bced279

Browse files
Merge pull request #556 from appdevforall/feat/K2GO-382-deepop-notifications
K2GO-382 fix(ui): deep-op notifications return to the op screen; run-in-background lands on Home
2 parents 8bd356f + 13ee09b commit bced279

10 files changed

Lines changed: 152 additions & 57 deletions

File tree

‎controller/app/src/main/java/org/appdevforall/k2go/deepop/DeepOpService.java‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@
5555
import org.appdevforall.k2go.deploy.data.RootfsManifest;
5656
import org.appdevforall.k2go.env.EnvironmentControl;
5757
import org.appdevforall.k2go.env.EnvironmentLock;
58-
import org.appdevforall.k2go.redesign.LibraryActivity;
58+
import org.appdevforall.k2go.redesign.OpReturnNavigator;
5959
import org.appdevforall.k2go.util.AppExecutors;
6060

6161
import java.io.File;
@@ -648,8 +648,10 @@ private void createNotificationChannel() {
648648
}
649649

650650
private Notification buildNotification(String text) {
651-
Intent open = new Intent(this, LibraryActivity.class);
652-
PendingIntent contentIntent = PendingIntent.getActivity(this, 0, open, PendingIntent.FLAG_IMMUTABLE);
651+
// K2GO-382: tapping returns to the live backup/restore screen. It was a bare LibraryActivity
652+
// that fell back to the last tab (Settings); opening LibraryActivity mid-op also fights the
653+
// boot gate. The route now has one owner (OpReturnNavigator).
654+
PendingIntent contentIntent = OpReturnNavigator.notify(this, OpReturnNavigator.backupRestore(this, owner));
653655
NotificationCompat.Builder b = new NotificationCompat.Builder(this, CHANNEL_ID)
654656
.setContentTitle(getString(R.string.deepop_notif_title))
655657
.setContentText(text)

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

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1833,11 +1833,13 @@ private void createNotificationChannel() {
18331833
}
18341834

18351835
private Notification buildNotification(String text) {
1836-
// ADFA-4919: return to the modern progress surface — LibraryActivity shows rootfs progress
1837-
// (boot gate) and routes to the proot install index when a module is running — unlike legacy
1838-
// MainActivity, which shows neither. Reusable for any proot module install (delivery, etc.).
1839-
Intent open = new Intent(this, org.appdevforall.k2go.redesign.LibraryActivity.class);
1840-
PendingIntent contentIntent = PendingIntent.getActivity(this, 0, open, PendingIntent.FLAG_IMMUTABLE);
1836+
// ADFA-4919 / K2GO-382: return to the modern progress surface — LibraryActivity shows rootfs
1837+
// progress (boot gate) and routes to the proot install index when a module is running — unlike
1838+
// legacy MainActivity, which shows neither. The route now has one owner (OpReturnNavigator);
1839+
// EXTRA_INSTALLING makes a fresh/refreshed LibraryActivity land on install progress instead of
1840+
// the last tab.
1841+
PendingIntent contentIntent = org.appdevforall.k2go.redesign.OpReturnNavigator.notify(this,
1842+
org.appdevforall.k2go.redesign.OpReturnNavigator.install(this));
18411843

18421844
NotificationCompat.Builder b = new NotificationCompat.Builder(this, CHANNEL_ID)
18431845
.setContentTitle(getString(R.string.install_notif_title))

‎controller/app/src/main/java/org/appdevforall/k2go/kolibri/presentation/KolibriSeedService.java‎

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@
3333
import org.appdevforall.k2go.kolibri.data.KolibriRestClient;
3434
import org.appdevforall.k2go.kolibri.domain.ChannelSelection;
3535
import org.appdevforall.k2go.kolibri.domain.SeedPlan;
36-
import org.appdevforall.k2go.redesign.SetupProgressActivity;
3736

3837
import java.util.ArrayList;
3938
import java.util.Collections;
@@ -364,14 +363,11 @@ private void createNotificationChannel() {
364363
}
365364

366365
private Notification buildNotification(String label) {
367-
// ADFA-5074: the index, not this stream's detail — it is the only surface that can end
368-
// the run, and a notification is how someone comes back to ask whether it is going well.
369-
Intent open = new Intent(this, SetupProgressActivity.class)
370-
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK
371-
| Intent.FLAG_ACTIVITY_SINGLE_TOP
372-
| Intent.FLAG_ACTIVITY_CLEAR_TOP);
373-
PendingIntent contentIntent = PendingIntent.getActivity(this, 0, open,
374-
PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT);
366+
// ADFA-5074 / K2GO-382: the index, not this stream's detail — it is the only surface that can
367+
// end the run, and a notification is how someone comes back to ask whether it is going well.
368+
// The route now has one owner (OpReturnNavigator).
369+
PendingIntent contentIntent = org.appdevforall.k2go.redesign.OpReturnNavigator.notify(this,
370+
org.appdevforall.k2go.redesign.OpReturnNavigator.contentDownload(this));
375371

376372
Intent cancel = new Intent(this, KolibriSeedService.class).setAction(ACTION_CANCEL);
377373
PendingIntent cancelIntent = PendingIntent.getService(this, 1, cancel,

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

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -171,10 +171,8 @@ private void createNotificationChannel() {
171171
}
172172

173173
private Notification buildNotification(String title) {
174-
Intent openI = new Intent(this, SetupProgressActivity.class)
175-
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_CLEAR_TOP);
176-
PendingIntent open = PendingIntent.getActivity(this, 0, openI,
177-
PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT);
174+
// K2GO-382: route via the single owner (OpReturnNavigator).
175+
PendingIntent open = OpReturnNavigator.notify(this, OpReturnNavigator.contentDownload(this));
178176
PendingIntent cancel = PendingIntent.getService(this, 1,
179177
new Intent(this, BooksDownloadService.class).setAction(ACTION_CANCEL),
180178
PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT);

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

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -112,10 +112,9 @@ private void createNotificationChannel() {
112112
}
113113

114114
private Notification buildNotification() {
115-
Intent open = new Intent(this, LibraryActivity.class)
116-
.putExtra(LibraryActivity.EXTRA_TAB, R.id.nav_clone)
117-
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
118-
PendingIntent contentIntent = PendingIntent.getActivity(this, 0, open, PendingIntent.FLAG_IMMUTABLE);
115+
// K2GO-382: route via the single owner (OpReturnNavigator), which also supplies the NEW_TASK
116+
// flag this notification was missing.
117+
PendingIntent contentIntent = OpReturnNavigator.notify(this, OpReturnNavigator.cloneShare(this));
119118
return new NotificationCompat.Builder(this, CHANNEL_ID)
120119
.setContentTitle(getString(R.string.k2go_clone_notif_title))
121120
.setContentText(getString(R.string.k2go_clone_notif_text))

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

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -236,12 +236,9 @@ private void createNotificationChannel() {
236236
/** Deep-link to Module management -> Dashboard (the card that shows the in-progress indicator), so the
237237
* notification is a way back into the update rather than a dead end. */
238238
private PendingIntent openDashboardDetail() {
239-
Intent openI = new Intent(this, SetupLibraryActivity.class)
240-
.putExtra(SetupLibraryActivity.EXTRA_MODULE_MGMT, true)
241-
.putExtra(SetupLibraryActivity.EXTRA_DASHBOARD_DETAIL, true)
242-
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_CLEAR_TOP);
243-
return PendingIntent.getActivity(this, 0, openI,
244-
PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT);
239+
// K2GO-382: the route now has one owner (OpReturnNavigator). This service was the reference
240+
// pattern; it now shares the mapping with every other deep-op instead of spelling its own.
241+
return OpReturnNavigator.notify(this, OpReturnNavigator.dashboardRebuild(this));
245242
}
246243

247244
/** Ongoing "updating…" notification. Not dismissible and does NOT auto-cancel on tap — while the

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

Lines changed: 9 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -243,23 +243,15 @@ public android.graphics.Typeface fetchFont(String fontFamily) {
243243
// ADFA-4919 (2c): a proot module install is live (its queue is RUNNING = the service is up).
244244
// Reopening the app (fresh LibraryActivity, e.g. from the notification) must land on the
245245
// progress index, not the empty home — open it over the gate; the index drives completion.
246-
if (!installing && !recovering
247-
&& org.appdevforall.k2go.install.presentation.ModuleQueueRepository.get().isRunning()) {
248-
startActivity(new android.content.Intent(this, SetupProgressActivity.class));
249-
}
250-
251-
// ADFA-4957: same idea for a live deep-env op (backup/restore). A fresh LibraryActivity — from
252-
// the notification, or a swipe-away relaunch — must land back on the op screen, not Home/Library
253-
// (which fights the gate and would try to boot the server mid-op). Route straight to the
254-
// backup/restore index; BackupJobFragment re-binds to the live op from DeepOpProgressRepository.
255-
if (!installing && !recovering
256-
&& org.appdevforall.k2go.deepop.DeepOpProgressRepository.get().isRunning()) {
257-
org.appdevforall.k2go.deepop.DeepOpState dop = org.appdevforall.k2go.deepop.DeepOpProgressRepository.get().current();
258-
String brMode = dop.owner == org.appdevforall.k2go.env.EnvironmentLock.Owner.RESTORE
259-
? BackupJobFragment.MODE_RESTORE : BackupJobFragment.MODE_BACKUP;
260-
startActivity(new android.content.Intent(this, SetupLibraryActivity.class)
261-
.putExtra(SetupLibraryActivity.EXTRA_BACKUP_RESTORE, true)
262-
.putExtra(SetupLibraryActivity.EXTRA_BR_JOB_MODE, brMode));
246+
// ADFA-4957 / K2GO-382: a fresh LibraryActivity — from a deep-op notification, or a swipe-away
247+
// relaunch — must land back on a live op's own screen, not Home/Library (which fights the boot
248+
// gate and would try to boot the server mid-op). The "which op -> which screen" mapping has one
249+
// owner now (OpReturnNavigator.forActiveOp), shared with the deep-op notifications so the two
250+
// cannot drift; the op screens (SetupProgressActivity for a proot queue, BackupJobFragment for a
251+
// deep-env op) re-bind to the live op from their repositories.
252+
if (!installing && !recovering) {
253+
android.content.Intent op = OpReturnNavigator.forActiveOp(this);
254+
if (op != null) startActivity(op);
263255
}
264256

265257
serverController = new ServerController(this, this);
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
/*
2+
* ============================================================================
3+
* Name : OpReturnNavigator.java
4+
* Author : AppDevForAll
5+
* Copyright : Copyright (c) 2026 AppDevForAll
6+
* Description : K2GO-382. ONE source for "return to a running operation's own screen."
7+
*
8+
* Every deep-op foreground notification (install / backup / restore / dashboard
9+
* rebuild / clone) and every content-download stream (kiwix / books / kolibri) must,
10+
* when tapped, land on that op's own progress screen -- not on Home/Settings. That
11+
* "op -> screen" mapping used to be spelled out separately in each service's
12+
* contentIntent AND in LibraryActivity's relaunch re-route, so the two could drift
13+
* (they did: several services opened a bare LibraryActivity that fell back to the last
14+
* tab). This class is the single owner of the mapping.
15+
*
16+
* Lifecycle / state: this class is STATELESS. It holds no "what is running" flag of its
17+
* own; {@link #forActiveOp} reads the existing run-state repositories (the module queue
18+
* and the deep-op repository), which stay the single source of truth for what is live.
19+
*
20+
* Flags: the per-op builders return the target + extras only (the "where"). Notification
21+
* callers wrap them with {@link #notify} (which adds the from-a-service task flags, so a
22+
* tap brings an existing screen forward instead of stacking a duplicate). Activity
23+
* callers start the bare intent in their own task and add flags as their context needs.
24+
* ============================================================================
25+
*/
26+
package org.appdevforall.k2go.redesign;
27+
28+
import android.app.PendingIntent;
29+
import android.content.Context;
30+
import android.content.Intent;
31+
32+
import org.appdevforall.k2go.R;
33+
import org.appdevforall.k2go.deepop.DeepOpProgressRepository;
34+
import org.appdevforall.k2go.deepop.DeepOpState;
35+
import org.appdevforall.k2go.env.EnvironmentLock;
36+
import org.appdevforall.k2go.install.presentation.ModuleQueueRepository;
37+
38+
public final class OpReturnNavigator {
39+
private OpReturnNavigator() {}
40+
41+
/** Content downloads (kiwix / books / kolibri): the shared provisioning progress screen. */
42+
public static Intent contentDownload(Context ctx) {
43+
return new Intent(ctx, SetupProgressActivity.class);
44+
}
45+
46+
/** Backup / restore live op: the backup/restore job screen; BackupJobFragment re-binds to the
47+
* running op from DeepOpProgressRepository (ADFA-4957). */
48+
public static Intent backupRestore(Context ctx, EnvironmentLock.Owner owner) {
49+
String mode = owner == EnvironmentLock.Owner.RESTORE
50+
? BackupJobFragment.MODE_RESTORE : BackupJobFragment.MODE_BACKUP;
51+
return new Intent(ctx, SetupLibraryActivity.class)
52+
.putExtra(SetupLibraryActivity.EXTRA_BACKUP_RESTORE, true)
53+
.putExtra(SetupLibraryActivity.EXTRA_BR_JOB_MODE, mode);
54+
}
55+
56+
/** Dashboard rebuild: the module-management dashboard card with its in-progress indicator. */
57+
public static Intent dashboardRebuild(Context ctx) {
58+
return new Intent(ctx, SetupLibraryActivity.class)
59+
.putExtra(SetupLibraryActivity.EXTRA_MODULE_MGMT, true)
60+
.putExtra(SetupLibraryActivity.EXTRA_DASHBOARD_DETAIL, true);
61+
}
62+
63+
/** Rootfs / module install: a bare LibraryActivity. It detects a live rootfs install itself
64+
* (InstallProgressRepository.isRunning -> the boot-gate progress) and, when a proot module queue
65+
* is live instead, its relaunch re-route ({@link #forActiveOp}) forwards to the install index.
66+
* Do NOT set EXTRA_INSTALLING here: it forces LibraryActivity.installing=true, which suppresses
67+
* that module re-route and would strand a module install's notification on the gate. */
68+
public static Intent install(Context ctx) {
69+
return new Intent(ctx, LibraryActivity.class);
70+
}
71+
72+
/** Clone / share: the Connect/clone tab. */
73+
public static Intent cloneShare(Context ctx) {
74+
return new Intent(ctx, LibraryActivity.class)
75+
.putExtra(LibraryActivity.EXTRA_TAB, R.id.nav_clone);
76+
}
77+
78+
/**
79+
* The live op's screen, chosen from the run-state repositories -- the single place that decides
80+
* "which op is running." Returns {@code null} when nothing tracked here is live (the caller keeps
81+
* its normal destination). Mirrors the order LibraryActivity used: a proot module queue first,
82+
* then a deep-env op (they are mutually exclusive by ContentAdmission, but the order is kept).
83+
*/
84+
public static Intent forActiveOp(Context ctx) {
85+
if (ModuleQueueRepository.get().isRunning()) {
86+
return contentDownload(ctx); // the module install index lives on the progress screen
87+
}
88+
if (DeepOpProgressRepository.get().isRunning()) {
89+
DeepOpState dop = DeepOpProgressRepository.get().current();
90+
if (dop != null) return backupRestore(ctx, dop.owner);
91+
}
92+
return null;
93+
}
94+
95+
/**
96+
* Wrap a per-op intent as a notification contentIntent. Adds the from-a-service task flags
97+
* (NEW_TASK is required when a Service launches an Activity; SINGLE_TOP|CLEAR_TOP bring an
98+
* existing op screen forward instead of stacking a duplicate) -- one policy for every deep-op
99+
* notification, so none can drift (this also supplies the NEW_TASK that clone was missing).
100+
*/
101+
public static PendingIntent notify(Context ctx, Intent target) {
102+
target.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK
103+
| Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_CLEAR_TOP);
104+
return PendingIntent.getActivity(ctx, 0, target,
105+
PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT);
106+
}
107+
}

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

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,10 @@ protected void onCreate(@Nullable Bundle s) {
150150
detailRoot = findViewById(R.id.k2go_sp_detail);
151151

152152
finishBtn.setOnClickListener(v -> goHome(true));
153-
runBgBtn.setOnClickListener(v -> finish()); // leave; the Library keeps provisioning going
153+
// K2GO-382: land deliberately on Home (goHome), not a bare finish() that pops to whatever
154+
// launched this (the wizard/hub). goHome(false) keeps the download sessions alive, so the
155+
// Library keeps provisioning in the background.
156+
runBgBtn.setOnClickListener(v -> goHome(false));
154157
cancel.setOnClickListener(v -> { redirectCancelled = true; cancelRedirect(); render(); });
155158

156159
detailBackBtn = findViewById(R.id.k2go_sp_back);
@@ -161,7 +164,7 @@ protected void onCreate(@Nullable Bundle s) {
161164
// configure and any non-module detail.
162165
detailBackBtn.setOnClickListener(v -> backToIndex());
163166
detailRunBgBtn.setText(R.string.k2go_zim_run_bg); // in a detail, secondary = leave (never abort)
164-
detailRunBgBtn.setOnClickListener(v -> finish());
167+
detailRunBgBtn.setOnClickListener(v -> goHome(false)); // K2GO-382: land on Home, keep provisioning
165168

166169
// ADFA-4842: own a ServerController so the index can restart the server after a module batch
167170
// (it was pdsm-stopped for the runroles) and keep ServerStateRepository fresh so the start
@@ -1282,8 +1285,9 @@ private void goHome(boolean clearSessions) {
12821285

12831286
// ADFA-4919: the natural end of installing is the Library — go there directly and clear the
12841287
// install screens above it. Both the wizard and Get More launch from LibraryActivity, so
1285-
// CLEAR_TOP + SINGLE_TOP lands on the existing Library (dropping Get More + this index). Only
1286-
// success/Finish reach here; "Run in background" (REST) still finish()es in place. ADFA-5343: a
1288+
// CLEAR_TOP + SINGLE_TOP lands on the existing Library (dropping Get More + this index).
1289+
// K2GO-382: "Run in background" now reaches here too via goHome(false) — same Home landing, but
1290+
// clearSessions=false leaves the download sessions running. ADFA-5343: a
12871291
// module batch set desired=UP; the reconciler brings the server up and keeps re-driving it wherever
12881292
// the app is, so the reused Library is (or becomes) live on arrival — even Finish under a slow/flap
12891293
// start lands on a Home the reconciler drives up, not a dead one (5336).
@@ -1378,7 +1382,7 @@ private void configureDetailBar() {
13781382
detailBackBtn.setText(R.string.k2go_setup_back);
13791383
detailBackBtn.setOnClickListener(v -> backToIndex());
13801384
detailRunBgBtn.setText(R.string.k2go_zim_run_bg);
1381-
detailRunBgBtn.setOnClickListener(v -> finish());
1385+
detailRunBgBtn.setOnClickListener(v -> goHome(false)); // K2GO-382: land on Home, keep provisioning
13821386
detailRunBgBtn.setVisibility(isLiveDetail(detailKey) ? View.VISIBLE : View.GONE);
13831387
}
13841388
}

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

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -196,10 +196,8 @@ private void createNotificationChannel() {
196196
}
197197

198198
private Notification buildNotification(String currentLabel) {
199-
Intent open = new Intent(this, SetupProgressActivity.class)
200-
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_CLEAR_TOP);
201-
PendingIntent contentIntent = PendingIntent.getActivity(this, 0, open,
202-
PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT);
199+
// K2GO-382: route via the single owner (OpReturnNavigator).
200+
PendingIntent contentIntent = OpReturnNavigator.notify(this, OpReturnNavigator.contentDownload(this));
203201
Intent cancel = new Intent(this, ZimDownloadService.class).setAction(ACTION_CANCEL);
204202
PendingIntent cancelIntent = PendingIntent.getService(this, 1, cancel,
205203
PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT);

0 commit comments

Comments
 (0)