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..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(); @@ -2334,10 +2343,11 @@ 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)); - Snackbar.make(getView(), errMsg, Snackbar.LENGTH_LONG).show(); + showImportSnackbar(getString(errMsg)); }); } return; @@ -2346,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) { @@ -2362,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) { @@ -2373,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())); + } +}