Skip to content

Commit 5339291

Browse files
Merge pull request #277 from appdevforall/fix/ADFA-4915-extract-percent
ADFA-4915 feat(install): determinate % for the rootfs Extract phase
2 parents 1099901 + bf02718 commit 5339291

9 files changed

Lines changed: 200 additions & 5 deletions

File tree

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

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -334,7 +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:
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 + "%"));
344+
break;
345+
}
338346
case PROVISIONING:
339347
btnFastInstall.setText(s.message);
340348
break;

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

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
package org.iiab.controller;
1111

1212
import org.iiab.controller.deploy.domain.ArchiveEntry;
13+
import org.iiab.controller.deploy.domain.ExtractProgress;
1314

1415
import android.content.Context;
1516
import android.os.Handler;
@@ -37,6 +38,14 @@ public interface ExtractionListener {
3738

3839
/** A streamed line of extraction output (verbose tar). Default no-op. */
3940
default void onProgress(String line) { }
41+
42+
/**
43+
* ADFA-4915: determinate extraction progress. {@code percent} is clamped to
44+
* [0,99] while extracting and set to 100 exactly once on completion; {@code done}
45+
* / {@code total} are archive-member counts; {@code line} is the current verbose
46+
* tar line (may be empty). Default no-op.
47+
*/
48+
default void onProgress(int percent, long done, long total, String line) { }
4049
}
4150

4251
public void startExtraction(Context context, String archivePath, String destDir, ExtractionListener listener) {
@@ -79,6 +88,11 @@ public void startExtraction(Context context, String archivePath, String destDir,
7988
}
8089
}
8190

91+
// ADFA-4915: entries.size() is the extraction denominator (members), already
92+
// computed above for the traversal/ABI checks — no extra tar pass.
93+
final long totalMembers = entries.size();
94+
final long[] doneMembers = {0L};
95+
8296
// For untrusted imports/restores: it must be a valid rootfs of THIS
8397
// app's architecture (ABI policy: 32<->32, 64<->64). Reuses the
8498
// listing above. Fail closed before extracting.
@@ -134,10 +148,16 @@ public void startExtraction(Context context, String archivePath, String destDir,
134148
lastLog[0] = now;
135149
Log.d(TAG, "Tar Output: " + line);
136150
}
151+
doneMembers[0]++; // ADFA-4915: ~one verbose line per member (stray stderr lines just push toward the 99% clamp)
137152
if (now - lastEmit[0] >= 50) {
138153
lastEmit[0] = now;
139154
final String l = line;
140-
uiHandler.post(() -> listener.onProgress(l));
155+
final long d = doneMembers[0];
156+
final int pct = ExtractProgress.percent(d, totalMembers);
157+
uiHandler.post(() -> {
158+
listener.onProgress(l);
159+
listener.onProgress(pct, d, totalMembers, l);
160+
});
141161
}
142162
}
143163
} catch (Exception ignored) {
@@ -201,6 +221,7 @@ public void startExtraction(Context context, String archivePath, String destDir,
201221
new Handler(Looper.getMainLooper()).post(() -> {
202222
if (exitCode == 0 && !broke) {
203223
Log.d(TAG, "Extraction successful.");
224+
listener.onProgress(100, totalMembers, totalMembers, ""); // ADFA-4915: 100% only on completion
204225
listener.onComplete(destDir);
205226
} else {
206227
String diag = "tar exit=" + exitCode
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/*
2+
* ============================================================================
3+
* Name : ExtractProgress.java
4+
* Author : AppDevForAll
5+
* Copyright : Copyright (c) 2026 AppDevForAll
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.
13+
* ============================================================================
14+
*/
15+
package org.iiab.controller.deploy.domain;
16+
17+
public final class ExtractProgress {
18+
19+
private ExtractProgress() { }
20+
21+
/**
22+
* Percent of archive members extracted so far, clamped to [0,99].
23+
* Returns 0 when nothing is known yet (total unknown/empty or done<=0).
24+
* 99 is the cap during extraction; the caller sets 100 only on completion.
25+
*/
26+
public static int percent(long done, long total) {
27+
if (total <= 0L || done <= 0L) return 0;
28+
long p = done * 100L / total;
29+
if (p < 0L) return 0;
30+
if (p > 99L) return 99;
31+
return (int) p;
32+
}
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+
}
51+
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +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, String message) { post(InstallState.extracting(percent, message)); }
6364
public void postProvisioning(String message) { post(InstallState.provisioning(message)); }
6465
public void postSuccess() { post(InstallState.success()); }
6566
public void postFailed(String message) { post(InstallState.failed(message)); }

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

Lines changed: 6 additions & 1 deletion
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(getString(R.string.install_status_extracting));
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);
@@ -315,6 +315,11 @@ private void onRootfsDownloaded(String downloadPath) {
315315
File downloadedArchive = archives[0];
316316
new TarExtractor().startExtraction(this, downloadedArchive.getAbsolutePath(), iiabRootDir.getAbsolutePath(),
317317
new TarExtractor.ExtractionListener() {
318+
@Override
319+
public void onProgress(int percent, long done, long total, String line) {
320+
InstallProgressRepository.get().postExtracting(percent, line);
321+
}
322+
318323
@Override
319324
public void onComplete(String destDir) {
320325
if (cancelled) return;

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

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,11 @@ 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
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;
2628
public final String speed; // e.g. "12.3MiB", may be empty
27-
public final String message; // resolved status text / error to render
29+
public final String message; // resolved status text / current file line / error
2830
public final long seq; // assigned by the repository; identifies terminal events
2931

3032
private InstallState(Phase phase, Op op, int percent, String speed, String message, long seq) {
@@ -66,6 +68,15 @@ public static InstallState extracting(String message) {
6668
return new InstallState(Phase.EXTRACTING, Op.INSTALL, 0, "", message, 0L);
6769
}
6870

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);
78+
}
79+
6980
public static InstallState provisioning(String message) {
7081
return new InstallState(Phase.PROVISIONING, Op.INSTALL, 0, "", message, 0L);
7182
}

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

Lines changed: 44 additions & 0 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
@@ -215,6 +218,24 @@ private void showInstallProgress(InstallState st) {
215218
installBar.setIndeterminate(false);
216219
installBar.setProgress(st.percent);
217220
installDetail.setText(st.percent + "%" + (st.speed.isEmpty() ? "" : " · " + st.speed));
221+
} else if (st.phase == InstallState.Phase.EXTRACTING) {
222+
if (st.percent < 0) {
223+
// ADFA-4915: "reading/listing" sub-phase. listEntries() scans the whole archive
224+
// (~1 min; longer on low-end devices), so show an animated "reading …" + an
225+
// indeterminate bar instead of a frozen 0%.
226+
startReadingEllipsis(getString(R.string.k2go_reading));
227+
installBar.setIndeterminate(true);
228+
installDetail.setText("");
229+
} else {
230+
// ADFA-4915: determinate extract — real % plus just the current file's basename
231+
// (no internal path, no counter): one ellipsized line that never overlaps.
232+
installStatus.setText(org.iiab.controller.deploy.domain.ExtractProgress.firstLine(
233+
getString(R.string.install_status_extracting)));
234+
installBar.setIndeterminate(false);
235+
installBar.setProgress(st.percent);
236+
String name = org.iiab.controller.deploy.domain.ExtractProgress.fileLabel(st.message);
237+
installDetail.setText(name.isEmpty() ? (st.percent + "%") : (st.percent + "% · " + name));
238+
}
218239
} else {
219240
installStatus.setText(st.message.isEmpty() ? getString(R.string.k2go_setting_up_library) : st.message);
220241
installBar.setIndeterminate(true);
@@ -495,6 +516,29 @@ private void stopBootEllipsis() {
495516
ellipsisRunnable = null;
496517
}
497518

519+
/** ADFA-4915: animate "reading" + . / .. / … while the archive is being listed. Reuses the
520+
* ellipsis handler (boot and reading never run at once) without touching {@code bootBaseText}. */
521+
private void startReadingEllipsis(final String base) {
522+
stopBootEllipsis();
523+
ellipsisRunnable = new Runnable() {
524+
int i = 0;
525+
final String[] frames = {" ", ". ", ".. ", "..."}; // 0..3 dots, fixed 3-slot width (monospace)
526+
@Override public void run() {
527+
if (installStatus != null) {
528+
String suffix = frames[i % frames.length];
529+
android.text.SpannableString sp = new android.text.SpannableString(base + " " + suffix);
530+
sp.setSpan(new android.text.style.TypefaceSpan("monospace"),
531+
base.length() + 1, base.length() + 1 + suffix.length(),
532+
android.text.Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
533+
installStatus.setText(sp);
534+
}
535+
i++;
536+
ellipsisHandler.postDelayed(this, 450L);
537+
}
538+
};
539+
ellipsisHandler.post(ellipsisRunnable);
540+
}
541+
498542
@Override
499543
public void enableSystemProtection() {
500544
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>
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package org.iiab.controller.deploy.domain;
2+
3+
import static org.junit.Assert.assertEquals;
4+
import static org.junit.Assert.assertTrue;
5+
6+
import org.junit.Test;
7+
8+
public class ExtractProgressTest {
9+
10+
@Test public void zeroWhenUnknownOrEmpty() {
11+
assertEquals(0, ExtractProgress.percent(0, 0));
12+
assertEquals(0, ExtractProgress.percent(10, 0));
13+
assertEquals(0, ExtractProgress.percent(0, 100));
14+
assertEquals(0, ExtractProgress.percent(-5, 100));
15+
}
16+
17+
@Test public void midValuesRoundDown() {
18+
assertEquals(50, ExtractProgress.percent(50, 100));
19+
assertEquals(33, ExtractProgress.percent(1, 3));
20+
assertEquals(68, ExtractProgress.percent(680, 1000));
21+
}
22+
23+
@Test public void cappedAt99UntilComplete() {
24+
assertEquals(99, ExtractProgress.percent(100, 100));
25+
assertEquals(99, ExtractProgress.percent(999, 1000));
26+
assertEquals(99, ExtractProgress.percent(5000, 1000));
27+
}
28+
29+
@Test public void monotonicNonDecreasingForGrowingDone() {
30+
int prev = 0;
31+
for (long done = 0; done <= 1000; done += 37) {
32+
int p = ExtractProgress.percent(done, 1000);
33+
assertTrue("percent must not regress", p >= prev);
34+
prev = p;
35+
}
36+
}
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+
}
52+
}

0 commit comments

Comments
 (0)