Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@
import org.appdevforall.k2go.deploy.data.RootfsManifest;
import org.appdevforall.k2go.env.EnvironmentControl;
import org.appdevforall.k2go.env.EnvironmentLock;
import org.appdevforall.k2go.redesign.LibraryActivity;
import org.appdevforall.k2go.redesign.OpReturnNavigator;
import org.appdevforall.k2go.util.AppExecutors;

import java.io.File;
Expand Down Expand Up @@ -648,8 +648,10 @@ private void createNotificationChannel() {
}

private Notification buildNotification(String text) {
Intent open = new Intent(this, LibraryActivity.class);
PendingIntent contentIntent = PendingIntent.getActivity(this, 0, open, PendingIntent.FLAG_IMMUTABLE);
// K2GO-382: tapping returns to the live backup/restore screen. It was a bare LibraryActivity
// that fell back to the last tab (Settings); opening LibraryActivity mid-op also fights the
// boot gate. The route now has one owner (OpReturnNavigator).
PendingIntent contentIntent = OpReturnNavigator.notify(this, OpReturnNavigator.backupRestore(this, owner));
NotificationCompat.Builder b = new NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle(getString(R.string.deepop_notif_title))
.setContentText(text)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1833,11 +1833,13 @@ private void createNotificationChannel() {
}

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

NotificationCompat.Builder b = new NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle(getString(R.string.install_notif_title))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@
import org.appdevforall.k2go.kolibri.data.KolibriRestClient;
import org.appdevforall.k2go.kolibri.domain.ChannelSelection;
import org.appdevforall.k2go.kolibri.domain.SeedPlan;
import org.appdevforall.k2go.redesign.SetupProgressActivity;

import java.util.ArrayList;
import java.util.Collections;
Expand Down Expand Up @@ -364,14 +363,11 @@ private void createNotificationChannel() {
}

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

Intent cancel = new Intent(this, KolibriSeedService.class).setAction(ACTION_CANCEL);
PendingIntent cancelIntent = PendingIntent.getService(this, 1, cancel,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -171,10 +171,8 @@ private void createNotificationChannel() {
}

private Notification buildNotification(String title) {
Intent openI = new Intent(this, SetupProgressActivity.class)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_CLEAR_TOP);
PendingIntent open = PendingIntent.getActivity(this, 0, openI,
PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT);
// K2GO-382: route via the single owner (OpReturnNavigator).
PendingIntent open = OpReturnNavigator.notify(this, OpReturnNavigator.contentDownload(this));
PendingIntent cancel = PendingIntent.getService(this, 1,
new Intent(this, BooksDownloadService.class).setAction(ACTION_CANCEL),
PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,10 +112,9 @@ private void createNotificationChannel() {
}

private Notification buildNotification() {
Intent open = new Intent(this, LibraryActivity.class)
.putExtra(LibraryActivity.EXTRA_TAB, R.id.nav_clone)
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
PendingIntent contentIntent = PendingIntent.getActivity(this, 0, open, PendingIntent.FLAG_IMMUTABLE);
// K2GO-382: route via the single owner (OpReturnNavigator), which also supplies the NEW_TASK
// flag this notification was missing.
PendingIntent contentIntent = OpReturnNavigator.notify(this, OpReturnNavigator.cloneShare(this));
return new NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle(getString(R.string.k2go_clone_notif_title))
.setContentText(getString(R.string.k2go_clone_notif_text))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -236,12 +236,9 @@ private void createNotificationChannel() {
/** Deep-link to Module management -> Dashboard (the card that shows the in-progress indicator), so the
* notification is a way back into the update rather than a dead end. */
private PendingIntent openDashboardDetail() {
Intent openI = new Intent(this, SetupLibraryActivity.class)
.putExtra(SetupLibraryActivity.EXTRA_MODULE_MGMT, true)
.putExtra(SetupLibraryActivity.EXTRA_DASHBOARD_DETAIL, true)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_CLEAR_TOP);
return PendingIntent.getActivity(this, 0, openI,
PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT);
// K2GO-382: the route now has one owner (OpReturnNavigator). This service was the reference
// pattern; it now shares the mapping with every other deep-op instead of spelling its own.
return OpReturnNavigator.notify(this, OpReturnNavigator.dashboardRebuild(this));
}

/** Ongoing "updating…" notification. Not dismissible and does NOT auto-cancel on tap — while the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -252,23 +252,15 @@ public android.graphics.Typeface fetchFont(String fontFamily) {
// ADFA-4919 (2c): a proot module install is live (its queue is RUNNING = the service is up).
// Reopening the app (fresh LibraryActivity, e.g. from the notification) must land on the
// progress index, not the empty home — open it over the gate; the index drives completion.
if (!installing && !recovering
&& org.appdevforall.k2go.install.presentation.ModuleQueueRepository.get().isRunning()) {
startActivity(new android.content.Intent(this, SetupProgressActivity.class));
}

// ADFA-4957: same idea for a live deep-env op (backup/restore). A fresh LibraryActivity — from
// the notification, or a swipe-away relaunch — must land back on the op screen, not Home/Library
// (which fights the gate and would try to boot the server mid-op). Route straight to the
// backup/restore index; BackupJobFragment re-binds to the live op from DeepOpProgressRepository.
if (!installing && !recovering
&& org.appdevforall.k2go.deepop.DeepOpProgressRepository.get().isRunning()) {
org.appdevforall.k2go.deepop.DeepOpState dop = org.appdevforall.k2go.deepop.DeepOpProgressRepository.get().current();
String brMode = dop.owner == org.appdevforall.k2go.env.EnvironmentLock.Owner.RESTORE
? BackupJobFragment.MODE_RESTORE : BackupJobFragment.MODE_BACKUP;
startActivity(new android.content.Intent(this, SetupLibraryActivity.class)
.putExtra(SetupLibraryActivity.EXTRA_BACKUP_RESTORE, true)
.putExtra(SetupLibraryActivity.EXTRA_BR_JOB_MODE, brMode));
// ADFA-4957 / K2GO-382: a fresh LibraryActivity — from a deep-op notification, or a swipe-away
// relaunch — must land back on a live op's own screen, not Home/Library (which fights the boot
// gate and would try to boot the server mid-op). The "which op -> which screen" mapping has one
// owner now (OpReturnNavigator.forActiveOp), shared with the deep-op notifications so the two
// cannot drift; the op screens (SetupProgressActivity for a proot queue, BackupJobFragment for a
// deep-env op) re-bind to the live op from their repositories.
if (!installing && !recovering) {
android.content.Intent op = OpReturnNavigator.forActiveOp(this);
if (op != null) startActivity(op);
}

serverController = new ServerController(this, this);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
/*
* ============================================================================
* Name : OpReturnNavigator.java
* Author : AppDevForAll
* Copyright : Copyright (c) 2026 AppDevForAll
* Description : K2GO-382. ONE source for "return to a running operation's own screen."
*
* Every deep-op foreground notification (install / backup / restore / dashboard
* rebuild / clone) and every content-download stream (kiwix / books / kolibri) must,
* when tapped, land on that op's own progress screen -- not on Home/Settings. That
* "op -> screen" mapping used to be spelled out separately in each service's
* contentIntent AND in LibraryActivity's relaunch re-route, so the two could drift
* (they did: several services opened a bare LibraryActivity that fell back to the last
* tab). This class is the single owner of the mapping.
*
* Lifecycle / state: this class is STATELESS. It holds no "what is running" flag of its
* own; {@link #forActiveOp} reads the existing run-state repositories (the module queue
* and the deep-op repository), which stay the single source of truth for what is live.
*
* Flags: the per-op builders return the target + extras only (the "where"). Notification
* callers wrap them with {@link #notify} (which adds the from-a-service task flags, so a
* tap brings an existing screen forward instead of stacking a duplicate). Activity
* callers start the bare intent in their own task and add flags as their context needs.
* ============================================================================
*/
package org.appdevforall.k2go.redesign;

import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;

import org.appdevforall.k2go.R;
import org.appdevforall.k2go.deepop.DeepOpProgressRepository;
import org.appdevforall.k2go.deepop.DeepOpState;
import org.appdevforall.k2go.env.EnvironmentLock;
import org.appdevforall.k2go.install.presentation.ModuleQueueRepository;

public final class OpReturnNavigator {
private OpReturnNavigator() {}

/** Content downloads (kiwix / books / kolibri): the shared provisioning progress screen. */
public static Intent contentDownload(Context ctx) {
return new Intent(ctx, SetupProgressActivity.class);
}

/** Backup / restore live op: the backup/restore job screen; BackupJobFragment re-binds to the
* running op from DeepOpProgressRepository (ADFA-4957). */
public static Intent backupRestore(Context ctx, EnvironmentLock.Owner owner) {
String mode = owner == EnvironmentLock.Owner.RESTORE
? BackupJobFragment.MODE_RESTORE : BackupJobFragment.MODE_BACKUP;
return new Intent(ctx, SetupLibraryActivity.class)
.putExtra(SetupLibraryActivity.EXTRA_BACKUP_RESTORE, true)
.putExtra(SetupLibraryActivity.EXTRA_BR_JOB_MODE, mode);
}

/** Dashboard rebuild: the module-management dashboard card with its in-progress indicator. */
public static Intent dashboardRebuild(Context ctx) {
return new Intent(ctx, SetupLibraryActivity.class)
.putExtra(SetupLibraryActivity.EXTRA_MODULE_MGMT, true)
.putExtra(SetupLibraryActivity.EXTRA_DASHBOARD_DETAIL, true);
}

/** Rootfs / module install: a bare LibraryActivity. It detects a live rootfs install itself
* (InstallProgressRepository.isRunning -> the boot-gate progress) and, when a proot module queue
* is live instead, its relaunch re-route ({@link #forActiveOp}) forwards to the install index.
* Do NOT set EXTRA_INSTALLING here: it forces LibraryActivity.installing=true, which suppresses
* that module re-route and would strand a module install's notification on the gate. */
public static Intent install(Context ctx) {
return new Intent(ctx, LibraryActivity.class);
}

/** Clone / share: the Connect/clone tab. */
public static Intent cloneShare(Context ctx) {
return new Intent(ctx, LibraryActivity.class)
.putExtra(LibraryActivity.EXTRA_TAB, R.id.nav_clone);
}

/**
* The live op's screen, chosen from the run-state repositories -- the single place that decides
* "which op is running." Returns {@code null} when nothing tracked here is live (the caller keeps
* its normal destination). Mirrors the order LibraryActivity used: a proot module queue first,
* then a deep-env op (they are mutually exclusive by ContentAdmission, but the order is kept).
*/
public static Intent forActiveOp(Context ctx) {
if (ModuleQueueRepository.get().isRunning()) {
return contentDownload(ctx); // the module install index lives on the progress screen
}
if (DeepOpProgressRepository.get().isRunning()) {
DeepOpState dop = DeepOpProgressRepository.get().current();
if (dop != null) return backupRestore(ctx, dop.owner);
}
return null;
}

/**
* Wrap a per-op intent as a notification contentIntent. Adds the from-a-service task flags
* (NEW_TASK is required when a Service launches an Activity; SINGLE_TOP|CLEAR_TOP bring an
* existing op screen forward instead of stacking a duplicate) -- one policy for every deep-op
* notification, so none can drift (this also supplies the NEW_TASK that clone was missing).
*/
public static PendingIntent notify(Context ctx, Intent target) {
target.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK
| Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_CLEAR_TOP);
return PendingIntent.getActivity(ctx, 0, target,
PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,10 @@ protected void onCreate(@Nullable Bundle s) {
detailRoot = findViewById(R.id.k2go_sp_detail);

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

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

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

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

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