Skip to content

Commit bf02718

Browse files
ADFA-4915 fix(install): reading indicator + address self-review
- Reading sub-phase: listEntries() scans the whole archive (~1 min; longer on low-end devices). Show an animated "reading …" (0..3 dots) + an indeterminate bar instead of a frozen 0%. Driven by a percent=-1 sentinel; the label string is translatable=false (minimalist technical term, localize later). - Drop unused InstallState.done/total (self-review): the N/M counter was removed on the prior UX pass, leaving them write-only. percent=-1 is the only extra signal needed, so state stays lean. - Extract detail = % + current file basename via the pure ExtractProgress.fileLabel; status label via ExtractProgress.firstLine; both JVM unit-tested. maxLines/ ellipsize set once at view init. - TarExtractor keeps the determinate onProgress callback; stray stderr lines just approach the 99% clamp (documented).
1 parent 40c5e27 commit bf02718

9 files changed

Lines changed: 121 additions & 54 deletions

File tree

‎controller/app/src/main/java/org/iiab/controller/DeployFragment.java‎

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -334,12 +334,15 @@ private void renderInstallProgress(org.iiab.controller.install.presentation.Inst
334334
case DOWNLOADING:
335335
btnFastInstall.setText(getString(R.string.install_status_os_download, s.percent, s.speed));
336336
break;
337-
case EXTRACTING:
338-
// ADFA-4915: surface real extract % on the legacy button (ProgressButton is
339-
// indeterminate, so % rides the text). Reuses the existing string (no new string).
340-
btnFastInstall.setText(getString(R.string.install_status_extracting).split("\\n")[0].trim()
341-
+ " " + s.percent + "%");
337+
case EXTRACTING: {
338+
// ADFA-4915: legacy button shows the extract % (or "…" during the reading sub-phase).
339+
String lbl = org.iiab.controller.deploy.domain.ExtractProgress.firstLine(
340+
getString(R.string.install_status_extracting));
341+
btnFastInstall.setText(s.percent < 0
342+
? (getString(R.string.k2go_reading) + " …")
343+
: (lbl + " " + s.percent + "%"));
342344
break;
345+
}
343346
case PROVISIONING:
344347
btnFastInstall.setText(s.message);
345348
break;

‎controller/app/src/main/java/org/iiab/controller/TarExtractor.java‎

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ public void startExtraction(Context context, String archivePath, String destDir,
148148
lastLog[0] = now;
149149
Log.d(TAG, "Tar Output: " + line);
150150
}
151-
doneMembers[0]++; // ADFA-4915: ~one verbose line per extracted member
151+
doneMembers[0]++; // ADFA-4915: ~one verbose line per member (stray stderr lines just push toward the 99% clamp)
152152
if (now - lastEmit[0] >= 50) {
153153
lastEmit[0] = now;
154154
final String l = line;
@@ -164,8 +164,6 @@ public void startExtraction(Context context, String archivePath, String destDir,
164164
}
165165
});
166166
readerThread.start();
167-
// ADFA-4915: publish the denominator up front so the UI can show "0 / N".
168-
uiHandler.post(() -> listener.onProgress(0, 0L, totalMembers, ""));
169167

170168
// 4. THE JAVA DECOMPRESSION PIPE (If it's a .gz file)
171169
boolean pipeBroke = false;

‎controller/app/src/main/java/org/iiab/controller/deploy/domain/ExtractProgress.java‎

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,13 @@
33
* Name : ExtractProgress.java
44
* Author : AppDevForAll
55
* Copyright : Copyright (c) 2026 AppDevForAll
6-
* Description : ADFA-4915. Pure rule for the rootfs Extract phase progress:
7-
* percent = members_extracted / total, clamped to [0,99]. 100 is
8-
* reserved for completion (set explicitly), so the bar never shows
9-
* "done" mid-extraction. Pure JVM (no android.*) => unit-testable.
6+
* Description : ADFA-4915. Pure rules for the rootfs Extract phase UI:
7+
* - percent(done,total): members_extracted / total, clamped [0,99]
8+
* (100 is reserved for completion, set explicitly by the caller).
9+
* - firstLine(text): the first line of a multi-line label.
10+
* - fileLabel(line): basename of a verbose tar line (files only;
11+
* "" for directory entries or empty input).
12+
* Pure JVM (no android.*) => unit-testable.
1013
* ============================================================================
1114
*/
1215
package org.iiab.controller.deploy.domain;
@@ -27,4 +30,22 @@ public static int percent(long done, long total) {
2730
if (p > 99L) return 99;
2831
return (int) p;
2932
}
33+
34+
/** First line of a possibly multi-line label, trimmed. Null-safe. */
35+
public static String firstLine(String s) {
36+
if (s == null) return "";
37+
int i = s.indexOf('\n');
38+
return (i >= 0 ? s.substring(0, i) : s).trim();
39+
}
40+
41+
/**
42+
* Basename of a verbose tar line: the segment after the last '/'.
43+
* Returns "" for empty input or a directory entry (line ends with '/'),
44+
* so the UI shows only files, never a bare directory path.
45+
*/
46+
public static String fileLabel(String line) {
47+
if (line == null || line.isEmpty() || line.endsWith("/")) return "";
48+
int slash = line.lastIndexOf('/');
49+
return slash >= 0 ? line.substring(slash + 1) : line;
50+
}
3051
}

‎controller/app/src/main/java/org/iiab/controller/install/presentation/InstallProgressRepository.java‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ public boolean isRunning() {
6060
// All posts are thread-safe (callable from the aria2 / proot worker threads).
6161
public void postDownloading(int percent, String speed) { post(InstallState.downloading(percent, speed)); }
6262
public void postExtracting(String message) { post(InstallState.extracting(message)); }
63-
public void postExtracting(int percent, long done, long total, String message) { post(InstallState.extracting(percent, done, total, message)); }
63+
public void postExtracting(int percent, String message) { post(InstallState.extracting(percent, message)); }
6464
public void postProvisioning(String message) { post(InstallState.provisioning(message)); }
6565
public void postSuccess() { post(InstallState.success()); }
6666
public void postFailed(String message) { post(InstallState.failed(message)); }

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -301,7 +301,7 @@ public void onIntegrityFailure(String reason) {
301301
}
302302

303303
private void onRootfsDownloaded(String downloadPath) {
304-
InstallProgressRepository.get().postExtracting(0, 0L, 0L, ""); // ADFA-4915: UI label is fixed; % arrives via TarExtractor
304+
InstallProgressRepository.get().postExtracting(-1, ""); // ADFA-4915: -1 = indeterminate "reading/listing" until the first member is extracted
305305
updateNotification(getString(R.string.install_status_extracting));
306306

307307
File downloadDir = new File(downloadPath);
@@ -317,7 +317,7 @@ private void onRootfsDownloaded(String downloadPath) {
317317
new TarExtractor.ExtractionListener() {
318318
@Override
319319
public void onProgress(int percent, long done, long total, String line) {
320-
InstallProgressRepository.get().postExtracting(percent, done, total, line);
320+
InstallProgressRepository.get().postExtracting(percent, line);
321321
}
322322

323323
@Override

‎controller/app/src/main/java/org/iiab/controller/install/presentation/InstallState.java‎

Lines changed: 20 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -22,19 +22,17 @@ public enum Op { INSTALL, RESET }
2222

2323
public final Phase phase;
2424
public final Op op;
25-
public final int percent; // 0..100, meaningful for DOWNLOADING (and EXTRACTING, ADFA-4915)
26-
public final long done; // ADFA-4915: extracted members (EXTRACTING); 0 otherwise
27-
public final long total; // ADFA-4915: total members (EXTRACTING); 0 otherwise
25+
// 0..100 for DOWNLOADING and for EXTRACTING while extracting; -1 means
26+
// indeterminate (the EXTRACTING "reading/listing" sub-phase, ADFA-4915).
27+
public final int percent;
2828
public final String speed; // e.g. "12.3MiB", may be empty
29-
public final String message; // resolved status text / error to render
29+
public final String message; // resolved status text / current file line / error
3030
public final long seq; // assigned by the repository; identifies terminal events
3131

32-
private InstallState(Phase phase, Op op, int percent, long done, long total, String speed, String message, long seq) {
32+
private InstallState(Phase phase, Op op, int percent, String speed, String message, long seq) {
3333
this.phase = phase;
3434
this.op = op != null ? op : Op.INSTALL;
3535
this.percent = percent;
36-
this.done = done;
37-
this.total = total;
3836
this.speed = speed != null ? speed : "";
3937
this.message = message != null ? message : "";
4038
this.seq = seq;
@@ -50,40 +48,44 @@ public boolean isTerminal() {
5048

5149
/** Returns a copy with the given sequence number (the repository assigns it). */
5250
InstallState withSeq(long seq) {
53-
return new InstallState(phase, op, percent, done, total, speed, message, seq);
51+
return new InstallState(phase, op, percent, speed, message, seq);
5452
}
5553

5654
/** Returns a copy tagged with the given operation (the repository stamps it). */
5755
InstallState withOp(Op op) {
58-
return new InstallState(phase, op, percent, done, total, speed, message, seq);
56+
return new InstallState(phase, op, percent, speed, message, seq);
5957
}
6058

6159
public static InstallState idle() {
62-
return new InstallState(Phase.IDLE, Op.INSTALL, 0, 0L, 0L, "", "", 0L);
60+
return new InstallState(Phase.IDLE, Op.INSTALL, 0, "", "", 0L);
6361
}
6462

6563
public static InstallState downloading(int percent, String speed) {
66-
return new InstallState(Phase.DOWNLOADING, Op.INSTALL, percent, 0L, 0L, speed, "", 0L);
64+
return new InstallState(Phase.DOWNLOADING, Op.INSTALL, percent, speed, "", 0L);
6765
}
6866

6967
public static InstallState extracting(String message) {
70-
return new InstallState(Phase.EXTRACTING, Op.INSTALL, 0, 0L, 0L, "", message, 0L);
68+
return new InstallState(Phase.EXTRACTING, Op.INSTALL, 0, "", message, 0L);
7169
}
7270

73-
/** ADFA-4915: determinate extract progress. percent is pre-clamped by ExtractProgress; done/total are members; message is the current file line (may be empty). */
74-
public static InstallState extracting(int percent, long done, long total, String message) {
75-
return new InstallState(Phase.EXTRACTING, Op.INSTALL, percent, done, total, "", message, 0L);
71+
/**
72+
* ADFA-4915: determinate extract progress. {@code percent} is pre-clamped by
73+
* ExtractProgress (0..99, then 100 on completion) or -1 for the indeterminate
74+
* "reading/listing" sub-phase; {@code message} is the current file line (may be empty).
75+
*/
76+
public static InstallState extracting(int percent, String message) {
77+
return new InstallState(Phase.EXTRACTING, Op.INSTALL, percent, "", message, 0L);
7678
}
7779

7880
public static InstallState provisioning(String message) {
79-
return new InstallState(Phase.PROVISIONING, Op.INSTALL, 0, 0L, 0L, "", message, 0L);
81+
return new InstallState(Phase.PROVISIONING, Op.INSTALL, 0, "", message, 0L);
8082
}
8183

8284
public static InstallState success() {
83-
return new InstallState(Phase.SUCCESS, Op.INSTALL, 0, 0L, 0L, "", "", 0L);
85+
return new InstallState(Phase.SUCCESS, Op.INSTALL, 0, "", "", 0L);
8486
}
8587

8688
public static InstallState failed(String message) {
87-
return new InstallState(Phase.FAILED, Op.INSTALL, 0, 0L, 0L, "", message, 0L);
89+
return new InstallState(Phase.FAILED, Op.INSTALL, 0, "", message, 0L);
8890
}
8991
}

‎controller/app/src/main/java/org/iiab/controller/redesign/LibraryActivity.java‎

Lines changed: 42 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,9 @@ protected void onCreate(Bundle savedInstanceState) {
106106
installStatus = findViewById(R.id.k2go_install_status);
107107
installBar = findViewById(R.id.k2go_install_bar);
108108
installDetail = findViewById(R.id.k2go_install_detail);
109+
// ADFA-4915: extract detail is one middle-ellipsized line so long file names never overlap.
110+
installDetail.setMaxLines(1);
111+
installDetail.setEllipsize(android.text.TextUtils.TruncateAt.MIDDLE);
109112
installing = getIntent().getBooleanExtra(EXTRA_INSTALLING, false);
110113
// The Lottie has a text layer (OPEN/CLOSED sign). Use the system typeface (Noto-based,
111114
// global script fallback) so localized words render in any language; a TextDelegate maps
@@ -214,24 +217,23 @@ private void showInstallProgress(InstallState st) {
214217
installBar.setProgress(st.percent);
215218
installDetail.setText(st.percent + "%" + (st.speed.isEmpty() ? "" : " · " + st.speed));
216219
} else if (st.phase == InstallState.Phase.EXTRACTING) {
217-
// ADFA-4915: determinate extract — real % plus just the current file's basename.
218-
// No internal path prefix and no N/M counter (UX review): the internal
219-
// installed-rootfs/iiab/... path added nothing and wrapped to 3-4 lines, overlapping
220-
// the animation on short screens. Skip directory entries (line ends with '/'), keep
221-
// it to one middle-ellipsized line so long names never overlap.
222-
installStatus.setText(getString(R.string.install_status_extracting).split("\\n")[0].trim());
223-
installBar.setIndeterminate(false);
224-
installBar.setProgress(st.percent);
225-
String det = st.percent + "%";
226-
String line = st.message;
227-
if (!line.isEmpty() && !line.endsWith("/")) {
228-
int slash = line.lastIndexOf('/');
229-
String name = slash >= 0 ? line.substring(slash + 1) : line;
230-
if (!name.isEmpty()) det += " · " + name;
220+
if (st.percent < 0) {
221+
// ADFA-4915: "reading/listing" sub-phase. listEntries() scans the whole archive
222+
// (~1 min; longer on low-end devices), so show an animated "reading …" + an
223+
// indeterminate bar instead of a frozen 0%.
224+
startReadingEllipsis(getString(R.string.k2go_reading));
225+
installBar.setIndeterminate(true);
226+
installDetail.setText("");
227+
} else {
228+
// ADFA-4915: determinate extract — real % plus just the current file's basename
229+
// (no internal path, no counter): one ellipsized line that never overlaps.
230+
installStatus.setText(org.iiab.controller.deploy.domain.ExtractProgress.firstLine(
231+
getString(R.string.install_status_extracting)));
232+
installBar.setIndeterminate(false);
233+
installBar.setProgress(st.percent);
234+
String name = org.iiab.controller.deploy.domain.ExtractProgress.fileLabel(st.message);
235+
installDetail.setText(name.isEmpty() ? (st.percent + "%") : (st.percent + "% · " + name));
231236
}
232-
installDetail.setMaxLines(1);
233-
installDetail.setEllipsize(android.text.TextUtils.TruncateAt.MIDDLE);
234-
installDetail.setText(det);
235237
} else {
236238
installStatus.setText(st.message.isEmpty() ? getString(R.string.k2go_setting_up_library) : st.message);
237239
installBar.setIndeterminate(true);
@@ -512,6 +514,29 @@ private void stopBootEllipsis() {
512514
ellipsisRunnable = null;
513515
}
514516

517+
/** ADFA-4915: animate "reading" + . / .. / … while the archive is being listed. Reuses the
518+
* ellipsis handler (boot and reading never run at once) without touching {@code bootBaseText}. */
519+
private void startReadingEllipsis(final String base) {
520+
stopBootEllipsis();
521+
ellipsisRunnable = new Runnable() {
522+
int i = 0;
523+
final String[] frames = {" ", ". ", ".. ", "..."}; // 0..3 dots, fixed 3-slot width (monospace)
524+
@Override public void run() {
525+
if (installStatus != null) {
526+
String suffix = frames[i % frames.length];
527+
android.text.SpannableString sp = new android.text.SpannableString(base + " " + suffix);
528+
sp.setSpan(new android.text.style.TypefaceSpan("monospace"),
529+
base.length() + 1, base.length() + 1 + suffix.length(),
530+
android.text.Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
531+
installStatus.setText(sp);
532+
}
533+
i++;
534+
ellipsisHandler.postDelayed(this, 450L);
535+
}
536+
};
537+
ellipsisHandler.post(ellipsisRunnable);
538+
}
539+
515540
@Override
516541
public void enableSystemProtection() {
517542
Intent i = new Intent(this, WatchdogService.class);

‎controller/app/src/main/res/values/strings_k2go.xml‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,8 @@
273273
<string name="k2go_step2b_info_courses">Courses — set up on the device after install.</string>
274274
<string name="k2go_step2b_info_review">Review your library, then download.</string>
275275
<string name="k2go_setting_up_library">Setting up your library…</string>
276+
<!-- ADFA-4915: minimalist indicator during the archive "reading/listing" sub-phase. Technical term, not localized for now. -->
277+
<string name="k2go_reading" translatable="false">reading</string>
276278
<string name="k2go_clone_hide_code">Hide code ▴</string>
277279
<string name="k2go_clone_chooser_send_code">Send transfer code</string>
278280
<string name="k2go_clone_scan_prompt_receive">Scan the other phone\'s transfer code</string>

‎controller/app/src/test/java/org/iiab/controller/deploy/domain/ExtractProgressTest.java‎

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package org.iiab.controller.deploy.domain;
22

33
import static org.junit.Assert.assertEquals;
4+
import static org.junit.Assert.assertTrue;
45

56
import org.junit.Test;
67

@@ -15,22 +16,37 @@ public class ExtractProgressTest {
1516

1617
@Test public void midValuesRoundDown() {
1718
assertEquals(50, ExtractProgress.percent(50, 100));
18-
assertEquals(33, ExtractProgress.percent(1, 3)); // 33.3 -> 33
19+
assertEquals(33, ExtractProgress.percent(1, 3));
1920
assertEquals(68, ExtractProgress.percent(680, 1000));
2021
}
2122

2223
@Test public void cappedAt99UntilComplete() {
23-
assertEquals(99, ExtractProgress.percent(100, 100)); // exactly done -> still 99 (100 is set on completion)
24+
assertEquals(99, ExtractProgress.percent(100, 100));
2425
assertEquals(99, ExtractProgress.percent(999, 1000));
25-
assertEquals(99, ExtractProgress.percent(5000, 1000)); // over-count (stray stderr lines) still clamps
26+
assertEquals(99, ExtractProgress.percent(5000, 1000));
2627
}
2728

2829
@Test public void monotonicNonDecreasingForGrowingDone() {
2930
int prev = 0;
3031
for (long done = 0; done <= 1000; done += 37) {
3132
int p = ExtractProgress.percent(done, 1000);
32-
org.junit.Assert.assertTrue("percent must not regress", p >= prev);
33+
assertTrue("percent must not regress", p >= prev);
3334
prev = p;
3435
}
3536
}
37+
38+
@Test public void firstLineTakesTextBeforeNewline() {
39+
assertEquals("Extracting System...", ExtractProgress.firstLine("Extracting System...\n(This takes a while)"));
40+
assertEquals("Solo una linea", ExtractProgress.firstLine("Solo una linea"));
41+
assertEquals("", ExtractProgress.firstLine(null));
42+
assertEquals("trimmed", ExtractProgress.firstLine(" trimmed \nrest"));
43+
}
44+
45+
@Test public void fileLabelIsBasenameFilesOnly() {
46+
assertEquals("os.py", ExtractProgress.fileLabel("usr/lib/python3.11/os.py"));
47+
assertEquals("foo", ExtractProgress.fileLabel("foo"));
48+
assertEquals("", ExtractProgress.fileLabel("usr/lib/"));
49+
assertEquals("", ExtractProgress.fileLabel(""));
50+
assertEquals("", ExtractProgress.fileLabel(null));
51+
}
3652
}

0 commit comments

Comments
 (0)