From afd09685d48553a559f0527b9517112f26b703ac Mon Sep 17 00:00:00 2001 From: RioPlay Date: Fri, 18 Sep 2026 21:19:01 -0500 Subject: [PATCH 1/2] Make Android settings predictable and prepare alpha18 --- docs/mobile-roadmap.md | 2 +- docs/plans/active/android-alpha18-snapshot.md | 9 +- .../active/android-settings-experience.md | 66 ++++++ mobile/android/README.md | 5 + mobile/android/RELEASE_NOTES.md | 126 ++++------ mobile/android/app/build.gradle.kts | 4 +- .../utterleaf/voice/KeyboardGeometryTest.kt | 8 +- .../org/utterleaf/voice/KeyboardTuningTest.kt | 7 +- .../utterleaf/voice/SettingsExperienceTest.kt | 223 ++++++++++++++++++ .../android/app/src/main/AndroidManifest.xml | 3 +- .../voice/KeyboardSettingsActivity.kt | 106 +++++++-- .../java/org/utterleaf/voice/TypingPanel.kt | 8 +- 12 files changed, 448 insertions(+), 119 deletions(-) create mode 100644 docs/plans/active/android-settings-experience.md create mode 100644 mobile/android/app/src/androidTest/java/org/utterleaf/voice/SettingsExperienceTest.kt diff --git a/docs/mobile-roadmap.md b/docs/mobile-roadmap.md index 356145b..da851dd 100644 --- a/docs/mobile-roadmap.md +++ b/docs/mobile-roadmap.md @@ -30,7 +30,7 @@ One-hand reach and visual polish can proceed alongside foundation work. - **Checkout:** `android-keyboard-hardening` on `main`, verified at `aa296c9`. - **Landed:** alpha17 polish, PR #52 (`80bec1b`), with exact-source Android CI passing. -- **Published:** signed alpha17. **Next:** prepare and verify the [signed alpha18 candidate](plans/active/android-alpha18-snapshot.md); no new keyboard features in this slice. Version metadata, candidate CI and signing are not yet started. +- **Published:** signed alpha17. **Next:** finish the [settings experience pass](plans/active/android-settings-experience.md), then prepare and verify the [signed alpha18 candidate](plans/active/android-alpha18-snapshot.md). Android usability is the current priority; desktop remains parked. Candidate signing is not yet started. - **Open:** Pixel 8 Pro/GrapheneOS and named-editor QA, TalkBack/Switch Access, landscape, three-button navigation and real Obtainium updates. Emulator success does not close these gates. ## Current status diff --git a/docs/plans/active/android-alpha18-snapshot.md b/docs/plans/active/android-alpha18-snapshot.md index 2cc57f8..3d706e8 100644 --- a/docs/plans/active/android-alpha18-snapshot.md +++ b/docs/plans/active/android-alpha18-snapshot.md @@ -6,7 +6,9 @@ Prepare the next signed Android preview from the merged alpha17 stabilization patch (PR #52, `80bec1b`). Start in `android-keyboard-hardening` from current `main` (verified September 18 at `aa296c9`), using a dedicated Android release branch for implementation. Own Android version metadata, release notes and -release documentation. Alpha17 remains the published APK. +release documentation. Alpha17 remains the published APK. The authorized +[settings experience pass](android-settings-experience.md) now precedes candidate +preparation; include its reviewed usability and staging fixes with the polish. ## Constraints and non-goals @@ -59,8 +61,9 @@ gh workflow run android-release.yml --ref main -f tag=android-v0.1.0-alpha18 -f ## Current evidence, device gates and stop -Planning only: no alpha18 version change, tag, candidate build, signing or -publication has been performed. The [completed polish record](../completed/android-alpha17-polish.md) +Version metadata and release notes are prepared at 18 / `0.1.0-alpha18`, alongside +the settings experience pass. Final candidate build/CI, tag, signing and +publication remain pending. The [completed polish record](../completed/android-alpha17-polish.md) records CI 35290270474: 173 instrumentation tests, 14 tooling tests and JVM/lint/builds. Keep Pixel 8 Pro/GrapheneOS (record exact OS build), named editors, phone latency, diff --git a/docs/plans/active/android-settings-experience.md b/docs/plans/active/android-settings-experience.md new file mode 100644 index 0000000..f1e1cc1 --- /dev/null +++ b/docs/plans/active/android-settings-experience.md @@ -0,0 +1,66 @@ +# Android settings experience before alpha18 + +## Goal and area + +Make customization predictable and easy to use before the signed alpha18 +candidate. Own `KeyboardSettingsActivity.kt`, the optional preview preference +callback in `TypingPanel.kt`, focused settings instrumentation and Android docs. +The September 18 user instruction prioritizes Android usability improvements; +this bounded pass precedes the existing snapshot contract. + +## Constraints + +Every settings change, including voice, Reset and preview quick controls, stays +staged until Apply. Cancel must leave persisted preferences untouched. Reset must +keep models and other user data. No typing persistence, ambient recording, new +permissions/dependencies, desktop work or new prediction engine. Dispose replaced +preview panels and invalidate stale input callbacks. Real keyboard quick controls +retain their immediate-save behavior. + +## Acceptance + +- Apply stays reachable while scrolling; category controls precede the practice + preview. System Back returns from a category before leaving Settings. +- Theme/alignment/layout choices can be changed and changed back; each updates + the preview immediately. Search text and filtered results agree after return. +- Voice, reset and preview controls obey Apply/Cancel. Confirmed reset remains + cancellable until Apply, retains models and restores documented defaults. +- Preview input stays transient. Old panels cannot change preferences or input + after replacement/close. No-op previews must not silently discard staged edits. +- Named instrumentation, tooling/JVM/lint/build checks and owned-view visual + review pass. Physical phone and assistive acceptance stay explicitly open. + +## Verification + +`python -m unittest discover -s mobile/android/tools -p 'test_*.py'` + +From `mobile/android`: `gradlew.bat testDebugUnitTest lintDebug assembleDebug +assembleDebugAndroidTest --no-daemon --console=plain`. + +Run `KeyboardTuningTest`, new settings experience tests, `PersistenceTest`, +`PrivateTypingPanelTest`, `OneHandLayoutTest` and `LetterLayoutTest` on API 35. +Capture app-owned Settings views with synthetic practice text and inspect default, +scrolled, large-text and narrow/landscape states. Canonical CI owns the full suite. + +## Stop and non-goals + +Stop when the bounded contracts pass and the final diff is reviewed; integrate +the Android change and continue the signed alpha18 plan. No broad redesign, +keyboard-engine replacement or claim that emulator success proves phone comfort. + +## Evidence + +- Implemented staged voice/reset/preview controls, reversible radio choices, + persistent Apply, controls before practice, consistent Back/search, in-memory + rotation retention and disposal of replaced previews. No practice text is retained. +- Initial checks: 14 tooling and 40 JVM tests passed; lint/debug/test APK builds + passed. The first focused API 35 run passed 17 settings/persistence tests; + the related private-panel/one-hand/layout bundle passed 10 tests. +- Owned-view captures at 1080x2400, 840x1800 with font scale 1.3, and 2400x1080 + are retained under `artifacts/screenshots/android-settings-alpha18/`. The two + narrow/landscape capture checks passed. Visual review shortened the preview's + country label to avoid truncating "United States". +- The added system Back test initially nested a UI-thread helper inside a + UI-thread predicate. Corrected the fixture; final candidate verification and + canonical CI remain required. No production test failure was hidden or waived. +- Version metadata is now 18 / `0.1.0-alpha18`; signing and publication remain pending. diff --git a/mobile/android/README.md b/mobile/android/README.md index 9467175..3618ab7 100644 --- a/mobile/android/README.md +++ b/mobile/android/README.md @@ -106,6 +106,11 @@ prediction, accessibility and device coverage remain on the change sizing, theme, number row, terminal controls, vibration or repeat filtering. Reopen the keyboard to apply saved preferences. The private practice field lets you try the layout without entering text into another app; it clears when you leave settings. + In the upcoming alpha18 candidate, **Apply** stays visible while you scroll; + controls come before the practice preview. All changes, including voice, + preview quick controls and Reset, wait for Apply. **Cancel** discards them. + Theme/layout choices update the preview immediately. Rotating Settings keeps + pending preferences in memory, while practice text is cleared. 4. For optional dictation, open **Optional · offline voice** in setup. Choose a model below, open its download in your browser, then return and **Import** that file. Allow microphone permission. The voice status shows what is still missing; diff --git a/mobile/android/RELEASE_NOTES.md b/mobile/android/RELEASE_NOTES.md index 338682c..0828c4a 100644 --- a/mobile/android/RELEASE_NOTES.md +++ b/mobile/android/RELEASE_NOTES.md @@ -1,89 +1,59 @@ -# Utterleaf Android 0.1.0-alpha17 +# Utterleaf Android 0.1.0-alpha18 -This experimental preview rebuilds the everyday keyboard to the approved design: -an icon toolbar with a fold-out Extra keys panel, a hinted number row, a -redesigned Settings screen, a password-manager shortcut restricted to password -fields, and a suggestion strip that completes the word you are typing. +This preview focuses on dependable everyday typing and predictable customization. +It includes the alpha17 typing/suggestion stabilization and a settings usability +pass. Alpha17's layouts, local emoji, Compose, private drafts, extra keys and +password-field-only password-manager shortcut remain available. ## What changed -- **The approved daily redesign.** The letter keyboard gains an icon toolbar - (undo, redo, copy, cut, paste, private draft, dictate and an expand chevron) - with editing actions one tap away instead of stacked text buttons. The - chevron opens the **Extra keys panel** — Esc, Tab, Ctrl, Alt, Shift, Home, - End, Ins, Del, PgUp, PgDn and the arrow keys — above the letters, with - **F1–F12** behind Fn. The old permanent Terminal row and the Tools/Edit - button strips are superseded; all previous routes keep tap-accessible - equivalents (Caps lock is a long-press on Shift, keyboard tools and settings - are a hold on the emoji key). -- **Hinted number row on by default**, with the digit hints you hold to type. - Letter hints show symbols while the number row is visible and digits when it - is hidden, so hold-to-insert stays complete. Hints sit where the design puts - them and remain optional in Settings. -- **The mockup bottom row**: `?123`, an emoji key (hold it for keyboard tools - and settings), a space labeled with the active language such as - "English (US)", period, and a rounded Enter pill that follows the editor's - action. The comma key moves to the period key's slide menu and the symbols - page. -- **Redesigned Settings**: a searchable category list (Layout & size, - Navigation & terminal, Typing assistance, Holds & gestures, Appearance, - Voice input, Privacy & data) with staged edits — **Apply** saves and - **Cancel** discards — plus a practice message with a live keyboard preview. - New options include fold-out extra keys, auto-capitalization, arrow repeat, - key borders and a System/Light/Dark theme. Reset preferences restores the - new defaults and never deletes models. -- **Password-manager shortcut, password fields only.** A keyring button - appears in the toolbar on explicit password fields and opens your configured - autofill application. It is never shown on ordinary text, email or number - fields; the keyboard sends nothing to the target application, and the key - does not exist when no autofill application is configured. -- **Suggestion strip.** While you type, up to three completions of the current - word appear below the toolbar, drawn from a frequency-ordered English list - derived solely from public-domain Project Gutenberg texts. Tapping a - suggestion completes only the word being typed — nothing is auto-corrected, - learned or sent. The strip is off on password and terminal fields, has an - on/off switch under Typing assistance, and adds no permissions. - -Alpha16's All Actions behavior, latched Ctrl/Alt, complete US punctuation, -Backspace selection gesture, private drafts, local emoji, Latin composition, -QWERTY/QWERTZ/AZERTY layouts, one-hand alignment and long-press hint insertion -remain included. The keyboard engine is Utterleaf's own implementation. -Existing preferences migrate once to the new defaults; your explicit choices -are kept from then on. +- **Typing stays independent of suggestions.** Dictionary loading and bounded + suggestion reads run off the typing path. Completions refresh after cursor + changes and verify the exact word and session before replacing it. Stale or + selected text is rejected. Ordinary touches avoid redundant modifier updates. +- **Stable suggestion geometry.** Empty and populated completion states keep the + same row height, so the letters do not jump as you type. Insets and explicit + bottom spacing have separate regression coverage. +- **Settings that honor Apply and Cancel.** Voice options, preview quick controls + and confirmed resets now stay staged with the other preferences. Apply saves; + Cancel discards. Reset never deletes models or other user data. +- **Controls before the preview.** Apply stays at the top while categories scroll. + Theme, alignment and letter-layout selections immediately refresh the practice + keyboard, including when returning to the original choice. +- **Consistent navigation.** Back returns from a category to the settings list; + search text and results remain in sync. Rotation retains pending preferences in + memory, while private practice input is cleared. Replaced preview panels are + disposed so their old controls cannot insert text or save preferences. ## Install and updates -Use **Utterleaf-Android-0.1.0-alpha17.apk**, version code **17**. The package remains -`org.utterleaf.voice` and retains the persistent alpha03-and-later signing -identity. Update an existing signed preview in place; do not uninstall first. -The display name does not create a second app or a new settings store. -Alpha01/alpha02 used disposable debug signers and require a one-time reinstall, -which removes their app data. +Use **Utterleaf-Android-0.1.0-alpha18.apk**, version code **18**. The package remains +`org.utterleaf.voice` with the persistent alpha03-and-later signing identity. +Update an existing signed preview in place; do not uninstall first. Alpha01/02 +used different debug signers and require a one-time reinstall that removes their +app data. The experimental foundation package is a separate channel. -The release includes SHA-256 checksums, public signing-certificate information -and version metadata. The separate experimental foundation package is not this -update channel. Never substitute an unsigned or CI debug APK for a signed update. +The release includes SHA-256 checksums, signing-certificate information and version +metadata. Do not substitute an unsigned or CI debug APK for a signed update. ## Verification and limits -This is an Android 8+ ARM64/x86_64 development preview, with English typing layouts -and reviewed English offline speech models. Automated checks cover the new -toolbar, panel and strip behavior, the restricted-field gates, native editor, -gesture, privacy and layout contracts. The signing workflow requires successful -exact-revision Android CI, package/version/certificate checks and signed -install/upgrade/reinstall checks before publication. - -Physical-phone testing is deferred to follow-up user feedback. TalkBack, Switch -Access, landscape, broad editor behavior and real Obtainium updates remain -unverified. The suggestion list is drawn from classical prose and -under-represents some modern vocabulary; expandable local word lists are -planned. Suggestions complete words but never replace them: auto-correction, -next-word prediction, swipe typing and broader language support are not -included in this snapshot. - -Typing needs no model or microphone permission. Dictation starts only by an -explicit action and is disabled in password fields. The password-manager key -launches your configured autofill application and sends it nothing. No Internet -permission, ambient recording, typing history, clipboard monitoring or saved -audio history is added. Explicit Copy/Cut/Paste still use the receiving editor's -clipboard commands. Keyboard screenshot protection remains enabled. +Android 8+ ARM64/x86_64 development preview. The release workflow requires +successful exact-revision CI, package/version/certificate checks and signed +install/upgrade/reinstall checks. Focused emulator tests cover staged voice/reset, +preview controls, reversible choices, rotation, navigation and model preservation. +Settings views are reviewed at default, narrow/large-text and landscape emulator +sizes. These checks do not establish physical-phone comfort or latency. + +Pixel 8 Pro/GrapheneOS, broad external editors, TalkBack/Switch Access, sustained +landscape use and real Obtainium updates remain open acceptance gates. An editor +that blocks a suggestion read can delay later suggestions until that read returns; +ordinary typing remains independent. Tapping a suggestion still re-verifies the +word through the editor and can wait for a slow editor response. + +The public-domain English completion list under-represents modern vocabulary. +No automatic correction, next-word prediction, swipe or broader language support +is added. No Internet permission, typing history, passive learning, clipboard +monitoring or ambient recording is added. Typing needs no model or microphone +permission. Dictation requires an explicit action and is disabled in password +fields. Keyboard screenshot protection remains enabled. diff --git a/mobile/android/app/build.gradle.kts b/mobile/android/app/build.gradle.kts index 6f342e0..a231748 100644 --- a/mobile/android/app/build.gradle.kts +++ b/mobile/android/app/build.gradle.kts @@ -10,8 +10,8 @@ android { applicationId = "org.utterleaf.voice" minSdk = 26 targetSdk = 36 - versionCode = 17 - versionName = "0.1.0-alpha17" + versionCode = 18 + versionName = "0.1.0-alpha18" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" ndk { abiFilters += listOf("arm64-v8a", "x86_64") } externalNativeBuild { cmake { arguments += "-DANDROID_STL=c++_shared" } } diff --git a/mobile/android/app/src/androidTest/java/org/utterleaf/voice/KeyboardGeometryTest.kt b/mobile/android/app/src/androidTest/java/org/utterleaf/voice/KeyboardGeometryTest.kt index d304032..5dc6097 100644 --- a/mobile/android/app/src/androidTest/java/org/utterleaf/voice/KeyboardGeometryTest.kt +++ b/mobile/android/app/src/androidTest/java/org/utterleaf/voice/KeyboardGeometryTest.kt @@ -462,10 +462,12 @@ class KeyboardGeometryTest { .performClick() } instrumentation.waitForIdleSync() - // Alignment radios sit below the practice preview; scroll to them. + // Controls precede the preview; scroll the alignment group into view. main { - (descendants(preferences.window.decorView) - .filterIsInstance().first()).fullScroll(View.FOCUS_DOWN) + val views = descendants(preferences.window.decorView) + val alignment = views.filterIsInstance() + .single { it.text == "Full width" }.parent as View + views.filterIsInstance().first().scrollTo(0, alignment.top) } instrumentation.waitForIdleSync() val bitmap = main { diff --git a/mobile/android/app/src/androidTest/java/org/utterleaf/voice/KeyboardTuningTest.kt b/mobile/android/app/src/androidTest/java/org/utterleaf/voice/KeyboardTuningTest.kt index a960bb7..ab7345b 100644 --- a/mobile/android/app/src/androidTest/java/org/utterleaf/voice/KeyboardTuningTest.kt +++ b/mobile/android/app/src/androidTest/java/org/utterleaf/voice/KeyboardTuningTest.kt @@ -180,13 +180,14 @@ class KeyboardTuningTest { .single { it.contentDescription == "Emoji" }.performLongClick() views().filterIsInstance() .single { it.contentDescription == "Number row on" }.performClick() - // The hub saved directly; the staged controls re-render from the saved snapshot. + // Preview quick controls stage just like the settings controls. assertFalse(views().filterIsInstance() .single { it.text == "Number row" }.isChecked) + assertTrue(KeyboardOptions.load(context).numberRow) views().filterIsInstance() .single { it.text == "Number row" }.performClick() - // Staged only: the hub's save stays until Apply commits the staged value. - assertFalse(KeyboardOptions.load(context).numberRow) + // No preview action persists until Apply. + assertTrue(KeyboardOptions.load(context).numberRow) views().filterIsInstance().single { it.text == "Apply" }.performClick() } instrumentation.waitForIdleSync() diff --git a/mobile/android/app/src/androidTest/java/org/utterleaf/voice/SettingsExperienceTest.kt b/mobile/android/app/src/androidTest/java/org/utterleaf/voice/SettingsExperienceTest.kt new file mode 100644 index 0000000..a46dca8 --- /dev/null +++ b/mobile/android/app/src/androidTest/java/org/utterleaf/voice/SettingsExperienceTest.kt @@ -0,0 +1,223 @@ +package org.utterleaf.voice + +import android.app.Activity +import android.content.Intent +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.Rect +import android.view.View +import android.view.ViewGroup +import android.widget.* +import androidx.test.core.app.ActivityScenario +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import org.junit.Assert.* +import org.junit.Test +import org.junit.runner.RunWith +import java.io.File + +@RunWith(AndroidJUnit4::class) +class SettingsExperienceTest { + private val instrumentation = InstrumentationRegistry.getInstrumentation() + private val context = instrumentation.targetContext + private val prefs get() = context.getSharedPreferences("keyboard", 0) + private fun views(view: View): List = listOf(view) + + if (view is ViewGroup) (0 until view.childCount).flatMap { views(view.getChildAt(it)) } else emptyList() + private fun all(activity: Activity) = views(activity.window.decorView) + private fun button(activity: Activity, label: String) = all(activity).filterIsInstance