Skip to content

Commit e7d2e8b

Browse files
K2GO-434 refactor(setup): extract the detail host and action bar
Move opening a row's detail over the index, the detail action bar, and the detail state out of SetupProgressActivity into SetupDetailHost. The Activity keeps the actions that need its pipeline or context (retry the seed, cancel a module); the host fires them through a narrow Host. Behavior-preserving.
1 parent 696a6bb commit e7d2e8b

2 files changed

Lines changed: 257 additions & 181 deletions

File tree

Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
/*
2+
* ============================================================================
3+
* Name : SetupDetailHost.java
4+
* Author : AppDevForAll
5+
* Copyright : Copyright (c) 2026 AppDevForAll
6+
* Description : K2GO-434 (slice 6). The per-stream detail host carved out of
7+
* SetupProgressActivity: opening a row's real detail fragment over the index, the
8+
* two-button detail action bar (Back / Run-in-background, or Retry / Cancel for a
9+
* module or seed), and returning to the index. Activity-scoped (owns the index/detail
10+
* views and a narrow Host for the FragmentManager and the actions a bar can fire).
11+
* Behavior-preserving: the fragment transactions keep commitNow exactly as before: the
12+
* listener-reclaim ordering the ADFA-5074 / backToIndex comments protect is unchanged.
13+
* See controller/docs/ADR-434-setupprogress-decomposition.md.
14+
* ============================================================================
15+
*/
16+
package org.appdevforall.k2go.redesign;
17+
18+
import android.view.View;
19+
import android.widget.Button;
20+
21+
import androidx.fragment.app.Fragment;
22+
import androidx.fragment.app.FragmentManager;
23+
24+
import org.appdevforall.k2go.R;
25+
import org.appdevforall.k2go.install.presentation.ModuleQueueRepository;
26+
import org.appdevforall.k2go.install.presentation.ModuleQueueState;
27+
import org.appdevforall.k2go.kolibri.presentation.KolibriSeedingFragment;
28+
import org.appdevforall.k2go.system.domain.ContentType;
29+
import org.appdevforall.k2go.system.domain.Operation;
30+
31+
/**
32+
* The Activity provides only what the detail host cannot do itself: the FragmentManager for the
33+
* transactions, a view refresh, and the three actions a configured bar can fire (leave and keep
34+
* provisioning, retry the Forgejo seed, confirm-cancel a running module). The detail state
35+
* (showing/key/bounce) and the index/detail view toggle live here.
36+
*/
37+
public final class SetupDetailHost {
38+
39+
public interface Host {
40+
FragmentManager fragmentManager();
41+
void render();
42+
/** K2GO-382: land on Home; keepSessionsAlive=false leaves the downloads running in the background. */
43+
void goHome(boolean keepSessionsAlive);
44+
/** K2GO-423: re-run a Forgejo seed that gave up (the Retry on the failed seed detail). */
45+
void retryForgejoSeed();
46+
/** ADFA-4898 P5: confirm, then cancel a running module install. */
47+
void confirmCancelModule();
48+
}
49+
50+
private final Host host;
51+
private final View indexScroll;
52+
private final View detailRoot;
53+
private final Button detailBackBtn;
54+
private final Button detailRunBgBtn;
55+
56+
private boolean showingDetail = false;
57+
/** ADFA-4898: the key currently shown in the detail host ("mod:<k>", "zim", ...), or null on the index. */
58+
private String detailKey;
59+
60+
/**
61+
* ADFA-5074: whether a completed run should take this detail away again.
62+
*
63+
* <p>Armed when a detail is opened over work still in flight, cleared when it fires. A detail
64+
* opened over a run that had already finished is a deliberate look at the result: usually at a
65+
* failed row, whose retry lives only there, and must not be closed underneath the user.
66+
*/
67+
private boolean bounceOnComplete = false;
68+
69+
/** The last completion verdict, so opening a detail can tell "still working" from "finished". */
70+
private boolean lastAllComplete = false;
71+
72+
public SetupDetailHost(Host host, View indexScroll, View detailRoot,
73+
Button detailBackBtn, Button detailRunBgBtn) {
74+
this.host = host;
75+
this.indexScroll = indexScroll;
76+
this.detailRoot = detailRoot;
77+
this.detailBackBtn = detailBackBtn;
78+
this.detailRunBgBtn = detailRunBgBtn;
79+
// ADFA-4898: the two detail buttons are (re)configured per shown detail by configureDetailBar()
80+
// : normally [Back (primary) / Run in background (secondary, LIVE only)], and for a failed module
81+
// [Retry (primary) / Back (secondary)]. The defaults here cover the window before the first
82+
// configure and any non-module detail.
83+
detailBackBtn.setOnClickListener(v -> backToIndex());
84+
detailRunBgBtn.setText(R.string.k2go_zim_run_bg); // in a detail, secondary = leave (never abort)
85+
detailRunBgBtn.setOnClickListener(v -> host.goHome(false)); // K2GO-382: land on Home, keep provisioning
86+
}
87+
88+
public boolean isShowingDetail() { return showingDetail; }
89+
90+
/** render() records the latest verdict here every pass, even on the index, so a later openDetail
91+
* arms the bounce correctly. */
92+
public void onVerdict(boolean allComplete) { lastAllComplete = allComplete; }
93+
94+
/**
95+
* render() detail branch: keep the bar in step with the queue, and bounce back to the index when a
96+
* run that was still in flight when the detail opened completes while the user is watching.
97+
* {@code resumed} is passed in so this stays free of the Activity lifecycle.
98+
*/
99+
public void refreshWhileShowing(boolean allComplete, boolean resumed) {
100+
// ADFA-4898: keep the detail bar in step with the queue : a module that fails shows Retry,
101+
// and a Retry that puts it back to RUNNING restores Back/Run-in-background on the next tick.
102+
configureDetailBar();
103+
if (allComplete && bounceOnComplete && resumed) {
104+
bounceOnComplete = false;
105+
backToIndex();
106+
}
107+
}
108+
109+
// ---- detail: host the real per-module card ----
110+
public void openDetail(String key) {
111+
showingDetail = true;
112+
detailKey = key;
113+
bounceOnComplete = !lastAllComplete;
114+
Fragment f;
115+
// Per-key detail view : presentation routing only; the execution class is NOT decided here.
116+
if (key.startsWith("mod:")) { f = ModuleInstallFragment.newInstance(key.substring(4)); } // ADFA-4842
117+
else if ("zim".equals(key)) { f = new ZimPreparingFragment(); } // ADFA-5074: observe-only
118+
else if ("kolibri".equals(key)) { f = new KolibriSeedingFragment(); } // ADFA-4954: observe-only
119+
else if ("forgejo".equals(key)) { f = new org.appdevforall.k2go.forgejo.presentation.ForgejoSeedingFragment(); } // K2GO-423: observe-only
120+
else if ("maps".equals(key)) { f = MapsPreparingFragment.newInstance(true); } // ADFA-4901: observe-only
121+
else { f = BooksDownloadsFragment.newInstance(true); }
122+
configureDetailBar();
123+
// ADFA-5074: commitNow, to match backToIndex. With an async commit a render() landing in
124+
// between set showingDetail back to false and found nothing to remove, and the queued
125+
// transaction then added the fragment into a hidden host : where ZimPreparingFragment
126+
// takes the service listener with nobody left to reclaim it, freezing the index's row.
127+
// The mirror image of the bug backToIndex's commitNow already exists for. Only reachable
128+
// from a row tap now that the intent routing is gone, so the activity is resumed and the
129+
// synchronous commit is safe.
130+
host.fragmentManager().beginTransaction().replace(R.id.k2go_sp_fraghost, f).commitNow();
131+
indexScroll.setVisibility(View.GONE);
132+
detailRoot.setVisibility(View.VISIBLE);
133+
}
134+
135+
/**
136+
* ADFA-5062: only a LIVE op (zim/kolibri/books) can keep running in the background; a stopped-class
137+
* detail (maps or a module install) cannot. Read from the model, not re-derived from the key prefix.
138+
*/
139+
private boolean isLiveDetail(String key) {
140+
if (key == null) return false;
141+
if (key.startsWith("mod:")) return Operation.appInstall(key.substring(4)).isLive();
142+
if ("forgejo".equals(key)) return true; // K2GO-423: the seed is a live, backgroundable step
143+
ContentType ct = ContentType.byKey(key);
144+
return ct != null && ct.isLive();
145+
}
146+
147+
/**
148+
* ADFA-4898: (re)configure the two-button detail bar for the currently shown detail. Reuses the one
149+
* existing template : a filled primary (k2go_sp_back) over an outlined secondary (k2go_sp_detail_finish):
150+
* - a failed module -> Retry (primary) + Back (secondary), so the recovery action sits where the LIVE
151+
* details put Run-in-background, instead of a bespoke button in the card;
152+
* - anything else -> Back (primary) + Run in background (secondary, LIVE only).
153+
* Recomputed on every render while a detail is open, so a Retry that puts the module back to RUNNING
154+
* flips the bar back to Back/Run-in-background on the next tick (no stale Retry). Retry stays on the
155+
* card: this detail is the live progress view and follows the re-run with its log.
156+
*/
157+
private void configureDetailBar() {
158+
if (!showingDetail || detailKey == null || detailBackBtn == null) return;
159+
// K2GO-423: the Forgejo seed detail offers Retry on failure, mirroring the module Retry. The
160+
// seed's give-up cleared the banked marker (A), so retryForgejoSeed() re-banks with the same
161+
// repo opt-in and restarts the service; the bar flips back to Back/Run-in-background on the
162+
// next tick once the seed is running again. (A durable/one-tap re-seed after leaving is K2GO-422.)
163+
if ("forgejo".equals(detailKey)
164+
&& org.appdevforall.k2go.forgejo.presentation.ForgejoSeedRepository.get().isFailed()) {
165+
detailBackBtn.setText(R.string.k2go_home_retry);
166+
detailBackBtn.setOnClickListener(v -> host.retryForgejoSeed());
167+
detailRunBgBtn.setText(R.string.k2go_setup_back);
168+
detailRunBgBtn.setOnClickListener(v -> backToIndex());
169+
detailRunBgBtn.setVisibility(View.VISIBLE);
170+
return;
171+
}
172+
// K2GO-394: the maps detail opens under the legacy key "maps" (not "mod:maps"), but maps IS a
173+
// module, so treat it as one here -> it gets the same Cancel-while-running (the mockup's op-level
174+
// Cancel install) and Retry-on-failure the other modules already have, with no duplicated logic.
175+
final boolean isModule = detailKey.startsWith("mod:") || "maps".equals(detailKey);
176+
final String moduleKey = !isModule ? null
177+
: (detailKey.startsWith("mod:") ? detailKey.substring(4) : detailKey);
178+
ModuleQueueState mq = ModuleQueueRepository.get().current();
179+
boolean moduleFailed = isModule && mq.didFail(moduleKey);
180+
boolean moduleRunning = isModule && mq.isInstalling(moduleKey);
181+
if (moduleFailed) {
182+
detailBackBtn.setText(R.string.k2go_home_retry);
183+
detailBackBtn.setOnClickListener(v -> ModuleRetry.fire(v, moduleKey));
184+
detailRunBgBtn.setText(R.string.k2go_setup_back);
185+
detailRunBgBtn.setOnClickListener(v -> backToIndex());
186+
detailRunBgBtn.setVisibility(View.VISIBLE);
187+
} else if (moduleRunning) {
188+
// ADFA-4898 P5: while this module's runrole runs, offer a confirmed Cancel in the same
189+
// secondary slot (Back stays primary). Cancel kills the runrole and surfaces the module as
190+
// failed, so the Retry above appears on the next tick : the "immediate retry" of the ticket.
191+
detailBackBtn.setText(R.string.k2go_setup_back);
192+
detailBackBtn.setOnClickListener(v -> backToIndex());
193+
detailRunBgBtn.setText(R.string.k2go_setup_cancel);
194+
detailRunBgBtn.setOnClickListener(v -> host.confirmCancelModule());
195+
detailRunBgBtn.setVisibility(View.VISIBLE);
196+
} else {
197+
detailBackBtn.setText(R.string.k2go_setup_back);
198+
detailBackBtn.setOnClickListener(v -> backToIndex());
199+
detailRunBgBtn.setText(R.string.k2go_zim_run_bg);
200+
detailRunBgBtn.setOnClickListener(v -> host.goHome(false)); // K2GO-382: land on Home, keep provisioning
201+
detailRunBgBtn.setVisibility(isLiveDetail(detailKey) ? View.VISIBLE : View.GONE);
202+
}
203+
}
204+
205+
public void backToIndex() {
206+
showingDetail = false;
207+
detailKey = null;
208+
// commitNow (synchronous) so the fragment's onDestroyView : which nulls the service
209+
// listener : runs BEFORE we reclaim it. With async commit() the teardown fired later and
210+
// clobbered the index's listener, so a job finishing while back on the index never updated
211+
// the UI (spinner stuck) until the card was reopened.
212+
Fragment cur = host.fragmentManager().findFragmentById(R.id.k2go_sp_fraghost);
213+
if (cur != null) host.fragmentManager().beginTransaction().remove(cur).commitNow();
214+
detailRoot.setVisibility(View.GONE);
215+
indexScroll.setVisibility(View.VISIBLE);
216+
ZimDownloadService.setListener(host::render);
217+
BooksDownloadService.setListener(host::render);
218+
host.render();
219+
}
220+
}

0 commit comments

Comments
 (0)