From 4f84a44f8d70141f55cefa9662706689ab979e1a Mon Sep 17 00:00:00 2001 From: luisguzman-adfa Date: Wed, 24 Jun 2026 02:50:22 +0000 Subject: [PATCH 1/2] fix(backup): stop import spinner when arch validation rejects the file MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On WRONG_ARCH/CORRUPT/not-rootfs the import is rejected, but stopImportSpinner() was missing in that branch (it's present in the success and catch paths), so the braille spinner kept running and overwrote the button text every 90ms — the button looked stuck spinning forever. Stop the spinner so the rejection ends cleanly. --- .../app/src/main/java/org/iiab/controller/DeployFragment.java | 1 + 1 file changed, 1 insertion(+) diff --git a/controller/app/src/main/java/org/iiab/controller/DeployFragment.java b/controller/app/src/main/java/org/iiab/controller/DeployFragment.java index f77036e..45ab8ac 100644 --- a/controller/app/src/main/java/org/iiab/controller/DeployFragment.java +++ b/controller/app/src/main/java/org/iiab/controller/DeployFragment.java @@ -2334,6 +2334,7 @@ private void importBackupSafely(Uri sourceUri) { if (getActivity() != null) { getActivity().runOnUiThread(() -> { isImporting = false; + stopImportSpinner(); // stop the braille spinner; rejection ends the import updateDynamicButtons(); btnImportBackup.setEnabled(true); btnImportBackup.setText(getString(R.string.install_btn_import_backup)); From 53c4b165840c688a91269d4ee8259a1a0ae12483 Mon Sep 17 00:00:00 2001 From: luisguzman-adfa Date: Wed, 24 Jun 2026 04:40:07 +0000 Subject: [PATCH 2/2] feat(ux): scale import Snackbar duration to message length (reading time) Snackbar only offers SHORT/LONG; the arch-mismatch rejection text is too long to read in LONG (~2.75s). Add SnackbarDuration (pure, unit-tested): ~200 wpm reading speed + base, clamped to [3s, 10s]. Route the import Snackbars (rejection, manifest/ checksum warnings, success, failure) through a showImportSnackbar() helper that sets the computed duration. --- .../org/iiab/controller/DeployFragment.java | 19 +++++++--- .../controller/util/SnackbarDuration.java | 37 +++++++++++++++++++ .../controller/util/SnackbarDurationTest.java | 32 ++++++++++++++++ 3 files changed, 83 insertions(+), 5 deletions(-) create mode 100644 controller/app/src/main/java/org/iiab/controller/util/SnackbarDuration.java create mode 100644 controller/app/src/test/java/org/iiab/controller/util/SnackbarDurationTest.java diff --git a/controller/app/src/main/java/org/iiab/controller/DeployFragment.java b/controller/app/src/main/java/org/iiab/controller/DeployFragment.java index 45ab8ac..c1b2f6a 100644 --- a/controller/app/src/main/java/org/iiab/controller/DeployFragment.java +++ b/controller/app/src/main/java/org/iiab/controller/DeployFragment.java @@ -2276,6 +2276,15 @@ private String queryDisplayName(Uri uri) { return null; } + /** Show a Snackbar whose visible time scales with the message length (reading time). */ + private void showImportSnackbar(CharSequence text) { + View v = getView(); + if (v != null) { + Snackbar.make(v, text, + org.iiab.controller.util.SnackbarDuration.millisForText(text.toString())).show(); + } + } + private void importBackupSafely(Uri sourceUri) { isImporting = true; updateDynamicButtons(); @@ -2338,7 +2347,7 @@ private void importBackupSafely(Uri sourceUri) { updateDynamicButtons(); btnImportBackup.setEnabled(true); btnImportBackup.setText(getString(R.string.install_btn_import_backup)); - Snackbar.make(getView(), errMsg, Snackbar.LENGTH_LONG).show(); + showImportSnackbar(getString(errMsg)); }); } return; @@ -2347,12 +2356,12 @@ private void importBackupSafely(Uri sourceUri) { // user (a future version will validate silently). See docs/ROOTFS_MANIFEST.md. if (okNoManifest && getActivity() != null) { getActivity().runOnUiThread(() -> - Snackbar.make(getView(), R.string.install_warn_manifest_missing, Snackbar.LENGTH_LONG).show()); + showImportSnackbar(getString(R.string.install_warn_manifest_missing))); } // Transparency: an app-made (device) backup carries no integrity checksum. if (okNoChecksum && getActivity() != null) { getActivity().runOnUiThread(() -> - Snackbar.make(getView(), R.string.install_warn_no_checksum, Snackbar.LENGTH_LONG).show()); + showImportSnackbar(getString(R.string.install_warn_no_checksum))); } if (getActivity() != null) { @@ -2363,7 +2372,7 @@ private void importBackupSafely(Uri sourceUri) { btnImportBackup.setText(getString(R.string.install_btn_import_backup)); selectedBackupFile = fileName; updateDynamicButtons(); - Snackbar.make(getView(), getString(R.string.install_msg_import_success), Snackbar.LENGTH_LONG).show(); + showImportSnackbar(getString(R.string.install_msg_import_success)); }); } } catch (Exception e) { @@ -2374,7 +2383,7 @@ private void importBackupSafely(Uri sourceUri) { updateDynamicButtons(); btnImportBackup.setEnabled(true); btnImportBackup.setText(getString(R.string.install_btn_import_backup)); - Snackbar.make(getView(), getString(R.string.install_msg_import_failed, e.getMessage()), Snackbar.LENGTH_LONG).show(); + showImportSnackbar(getString(R.string.install_msg_import_failed, e.getMessage())); }); } } finally { diff --git a/controller/app/src/main/java/org/iiab/controller/util/SnackbarDuration.java b/controller/app/src/main/java/org/iiab/controller/util/SnackbarDuration.java new file mode 100644 index 0000000..82364c1 --- /dev/null +++ b/controller/app/src/main/java/org/iiab/controller/util/SnackbarDuration.java @@ -0,0 +1,37 @@ +/* + * ============================================================================ + * Name : SnackbarDuration.java + * Author : AppDevForAll + * Copyright : Copyright (c) 2026 AppDevForAll + * Description : Reading-time based Snackbar duration (longer text shows longer). + * ============================================================================ + */ +package org.iiab.controller.util; + +/** + * Snackbar has only SHORT (~1.5s) / LONG (~2.75s). Long messages get cut off. + * This scales the duration with the message length using an average reading + * speed (~200 wpm ≈ 300ms/word) plus a reaction margin, clamped so it never + * flashes too briefly nor lingers forever. Pure JVM (unit-tested). + */ +public final class SnackbarDuration { + + private SnackbarDuration() {} + + static final int BASE_MS = 1500; // fixed reaction/append time + static final int PER_WORD_MS = 320; // ~200 wpm reading speed + margin + static final int MIN_MS = 3000; // floor (~ LENGTH_LONG) + static final int MAX_MS = 10000; // cap so it doesn't linger + + /** Duration in ms appropriate for reading {@code text}. */ + public static int millisForText(String text) { + if (text == null) return MIN_MS; + String t = text.trim(); + if (t.isEmpty()) return MIN_MS; + int words = t.split("\\s+").length; + long ms = BASE_MS + (long) words * PER_WORD_MS; + if (ms < MIN_MS) ms = MIN_MS; + if (ms > MAX_MS) ms = MAX_MS; + return (int) ms; + } +} diff --git a/controller/app/src/test/java/org/iiab/controller/util/SnackbarDurationTest.java b/controller/app/src/test/java/org/iiab/controller/util/SnackbarDurationTest.java new file mode 100644 index 0000000..1f05083 --- /dev/null +++ b/controller/app/src/test/java/org/iiab/controller/util/SnackbarDurationTest.java @@ -0,0 +1,32 @@ +package org.iiab.controller.util; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +/** Pure-JVM tests for reading-time Snackbar duration. */ +public class SnackbarDurationTest { + + @Test public void nullOrBlankIsFloor() { + assertEquals(SnackbarDuration.MIN_MS, SnackbarDuration.millisForText(null)); + assertEquals(SnackbarDuration.MIN_MS, SnackbarDuration.millisForText(" ")); + } + + @Test public void shortTextStaysAtFloor() { + assertEquals(SnackbarDuration.MIN_MS, SnackbarDuration.millisForText("Done")); + } + + @Test public void longerTextScalesUp() { + int many = SnackbarDuration.millisForText( + "This backup was built for a different processor architecture and cannot run on this device"); + assertTrue("expected > floor, got " + many, many > SnackbarDuration.MIN_MS); + assertTrue(many <= SnackbarDuration.MAX_MS); + } + + @Test public void veryLongTextIsCapped() { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < 200; i++) sb.append("word "); + assertEquals(SnackbarDuration.MAX_MS, SnackbarDuration.millisForText(sb.toString())); + } +}