diff --git a/docs/mobile-roadmap.md b/docs/mobile-roadmap.md index 5c505e7..cb0abe5 100644 --- a/docs/mobile-roadmap.md +++ b/docs/mobile-roadmap.md @@ -31,10 +31,14 @@ wait for the editor, privacy and testing foundation. ## Now -- **Checkout:** `android-keyboard-hardening`, branch `codex/android-alpha21-release-record`. +- **Checkout:** `android-keyboard-hardening`, branch `codex/android-comfort-pass`. - **Released:** [signed alpha21](https://github.com/RioPlay/utterleaf/releases/tag/android-v0.1.0-alpha21), including the clean-room daily-surface reset and live selected-text completion safeguard (PRs #64–#66). -- **Next:** complete the workflow-only Node.js/action and runner-image maintenance recorded in - [alpha20 release readiness](plans/completed/android-alpha20-release-readiness.md#post-release-ci-maintenance-memory). +- **Current:** finish review and exact-main validation of the bounded + [daily-keyboard comfort pass](plans/active/android-comfort-pass.md). It quiets + suggestions, improves hint hierarchy, guarantees bottom-row touch widths and + unifies the action-key shape without changing typing, privacy or editor logic. +- **Next:** complete the workflow-only Node.js/action and runner-image maintenance + recorded in [alpha20 release readiness](plans/completed/android-alpha20-release-readiness.md#post-release-ci-maintenance-memory). Editor-bound dictation recovery and the remaining Tools architecture work stay in the [capability and experience plan](plans/active/android-experience-refresh.md). Android remains the priority; desktop is parked. @@ -50,6 +54,18 @@ wait for the editor, privacy and testing foundation. ## Current status +- **Daily-keyboard comfort candidate verified locally — September 27, 2026:** + suggestions now use quiet, borderless fixed slots and clear stale hidden text; + empty states add no filler. Secondary hints use the palette's muted color at + 9 sp, ordinary keys have explicit pressed fills, and the dynamic action uses + the standard key radius. Fixed mode/punctuation/action widths guarantee at + least 48 dp bottom-row targets while Space absorbs the remainder. Android + tooling passed **26/26**, JVM/lint passed, and the focused emulator bundle + passed **58/58** after exact reruns documented in the + [comfort plan](plans/active/android-comfort-pass.md#status--september-27-2026). + Final empty/one/three-candidate renders were inspected. No physical-device or + live assistive-technology claim is made. + - **Alpha21 signed prerelease published — September 27, 2026:** PR #66 merged release metadata as exact main revision `394fdff11b8ed300d320d9e28c04876d00cc2c93`. Exact-main Android run diff --git a/docs/plans/active/android-comfort-pass.md b/docs/plans/active/android-comfort-pass.md new file mode 100644 index 0000000..43d4d78 --- /dev/null +++ b/docs/plans/active/android-comfort-pass.md @@ -0,0 +1,97 @@ +# Android daily-keyboard comfort pass + +## Goal + +Make ordinary Utterleaf typing feel calmer and more comfortable while retaining +the independently implemented interaction model released in alpha21. + +## Area + +- `mobile/android/app/src/main/java/org/utterleaf/voice/TypingPanel.kt` +- `mobile/android/app/src/main/java/org/utterleaf/voice/Ui.kt` +- focused daily-strip and keyboard-geometry instrumentation tests +- Android roadmap and this plan + +## Constraints + +- Preserve editor behavior, privacy policy, suggestion computation, gestures, + key ordering, preference identifiers and specialist Tools behavior. +- Keep the suggestion region fixed at 48 dp and every actionable daily control + at least 48 dp in both orientations. +- Do not copy FUTO code, assets, layouts, labels, constants or theme values. + FUTO is a behavioral comfort benchmark only; Utterleaf's implementation and + visual tokens remain independent. +- Emulator rendering is not evidence of physical-phone comfort. + +## Acceptance + +- Suggestions read as quiet inline text choices rather than a second set of + raised keys, while retaining three predictable tap regions. +- An empty suggestion state is visually calm and does not add instructional + filler or change keyboard geometry. +- Secondary hints remain legible but are subordinate to primary key labels. +- The dynamic action key uses the same rounded-key language as the keyboard; + the branded dictation control remains visually distinct. +- Pressed, focused, selected and disabled controls retain visible state. +- Portrait, landscape, one-handed, split and sensitive layouts retain their + existing geometry and behavior contracts. + +## Verification + +1. `python -m unittest discover -s mobile/android/tools -p test_*.py` +2. `mobile/android/gradlew.bat testDebugUnitTest lintDebug --no-daemon` +3. Focused emulator classes: `DailyToolbarContractTest`, + `KeyboardSpacingTest`, `KeyboardGeometryTest`, `KeyboardTuningTest`, + `SuggestionStripTest`, `SensitiveTypingPanelTest`, `OneHandLayoutTest`, + `SplitKeyboardTest`, `CompactLayerTest` and `KeyboardEditorContractTest`. +4. Render ordinary empty, one-candidate and three-candidate states for visual + comparison. Record the emulator-only limitation. + +## Non-goals + +- No new prediction, autocorrection, gesture typing or language behavior. +- No Tools architecture, settings, dictation-state or editor-lifecycle changes. +- No release/version bump in this slice. +- No claim of literal visual parity with another keyboard. + +## Stop + +Stop when the bounded daily-surface changes and focused checks pass. Move any +interaction or architecture issue discovered during review to the owning plan +instead of expanding this pass. + +## Status — September 27, 2026 + +Implemented on `codex/android-comfort-pass`: + +- Added a dedicated flat suggestion role with stateful pressed/focus feedback, + three stable slots and no empty-state filler. +- Hidden slots now clear candidate text and accessibility descriptions instead + of retaining stale words in invisible views. +- Centralized the bounded key radii, hint size and bottom-row widths in + `Ui.KeyboardTokens`; hints use the opaque palette-muted color at 9 sp. +- Mode, comma, Space, period and action now retain at least 48 dp touch width; + Space absorbs remaining width. The action keeps an explicit accent role and + uses the standard key radius. Dictation remains the branded pill. +- Added explicit pressed fills without changing gesture routing or animation. +- Added a contrast-safe inset focus/selected outline for primary controls; the + action remains visually distinct under external-keyboard focus. + +Verification: + +- Android tooling: **26/26 passed**. +- JVM tests and `lintDebug`: **passed**; lint reported no build-stopping issue. +- Focused emulator bundle: **58/58 passed** across daily toolbar, spacing, + geometry, tuning, suggestions, sensitive mode, one-hand, split and compact + layers, plus the real-editor action contract. Earlier runs exposed one newly + over-strict one-pixel width assertion, test assumptions that did not account + for split mode's two Space keys, and one default-IME readiness race. Review + also found that primary focus initially matched the default state. The test + assumptions were corrected, the readiness case passed on exact rerun, the + focus outline and rendering assertion were added, and the final combined run + was clean. +- Final emulator renders for empty, no-match, one- and three-candidate states + were inspected locally under `.grok/validation/comfort-final-2/`. + +This is emulator evidence only. Physical-phone comfort and live assistive- +technology review remain unverified. diff --git a/mobile/android/app/src/androidTest/java/org/utterleaf/voice/KeyboardSpacingTest.kt b/mobile/android/app/src/androidTest/java/org/utterleaf/voice/KeyboardSpacingTest.kt index 561fc4e..c4946f6 100644 --- a/mobile/android/app/src/androidTest/java/org/utterleaf/voice/KeyboardSpacingTest.kt +++ b/mobile/android/app/src/androidTest/java/org/utterleaf/voice/KeyboardSpacingTest.kt @@ -88,10 +88,19 @@ class KeyboardSpacingTest { @Test fun suggestionStripKeepsKeyGeometryAcrossCandidateStates() = main { var state = SuggestionEngine.SuggestionState.EMPTY val panel = panel(state = { state }) + fun dailyControlBounds() = listOf("Keyboard tools", "Editing tools", "Dictate") + .associateWith { bounds(panel, it) } panel.reset(false, false, "Enter") val emptyHeight = measure(panel) val emptySpace = bounds(panel, "Space") val emptyQ = bounds(panel, "q") + val emptyControls = dailyControlBounds() + val emptySuggestionSlots = descendants(panel.view).filterIsInstance() + .filter { it.visualRole == KeyVisualRole.SUGGESTION } + assertEquals("Daily strip must retain three predictable suggestion slots", + 3, emptySuggestionSlots.size) + assertTrue("Empty suggestion slots must stay quiet without collapsing", + emptySuggestionSlots.all { it.visibility == View.INVISIBLE && it.measuredWidth > 0 }) capture(panel, "empty") state = SuggestionEngine.SuggestionState("q", emptyList()) @@ -99,7 +108,8 @@ class KeyboardSpacingTest { val noMatchHeight = measure(panel) val noMatchSpace = bounds(panel, "Space") val noMatchQ = bounds(panel, "q") - assertTrue("No-match state must be represented accessibly", + val noMatchControls = dailyControlBounds() + assertFalse("No-match state must not add instructional filler", descendants(panel.view).filterIsInstance().any { it.text == "No completions" }) capture(panel, "no-match") @@ -108,6 +118,7 @@ class KeyboardSpacingTest { val oneHeight = measure(panel) val oneSpace = bounds(panel, "Space") val oneQ = bounds(panel, "q") + val oneControls = dailyControlBounds() val oneChipCount = descendants(panel.view).filterIsInstance