Skip to content

feat(ime): 'Always start docked' setting + float-mode diagnostics (#73 round 3) - #75

Merged
willwade merged 3 commits into
mainfrom
fix/ime-always-docked
Oct 24, 2026
Merged

willwade merged 3 commits into
mainfrom
fix/ime-always-docked

Conversation

@willwade

Copy link
Copy Markdown
Contributor

Round 3 for Heide's float/dock report, after her v0.1.23 test: "It still starts in float mode. And this time, it doesn't stay in docked mode when you bring the keyboard up again. But one thing that's fixed though, is the letters are not right on top of your cursor… better than the last version."

The v0.1.23 fix (#74) was insufficient. A full reproduction attempt on an API 35 emulator (edge-to-edge enforced, IME switch cycles, hide/show, pref manipulation via run-as) could not reproduce the float restart — the docked keyboard lays out correctly, mode restore breadcrumbs show the expected state machine, and the full preference matrix behaves as designed (unset→docked; floating→float restore; floating+always-docked→docked). The remaining trigger is Samsung/OneUI-specific (Heide's Android 16 tablet) and needs data from her device.

This PR therefore ships three things:

  1. Escape hatch — new Settings → Input → "Keyboard always starts docked" (dasher_ime/always_docked). Heide has never wanted float (three consecutive reports); pinning docked makes the underlying bug irrelevant for her. Float users who want restore keep it when the toggle is off.
  2. Diagnostics — ime_mode_restore analytics event with every decision input (opt-in, no-op otherwise), ime_float_enter/ime_float_exit with reasons (button/restore/selfheal/unbind), plus Log.d lifecycle breadcrumbs. If float still appears for her after this build, the next session shows exactly which path took it there.
  3. Hardening from the repro session — setDockedHeight now sets Gravity.BOTTOM explicitly (a no-gravity FrameLayout child defaults to TOP|START in the IME decor — never the intent, even where platform layout happens to re-dock it).

#74's fixes (engine re-entry guard, self-healing restore, unbind cleanup) remain — they closed real holes by inspection; they just weren't her hole.

Testing

  • :app:assembleDebug + :app:testDebugUnitTest green
  • Emulator (API 35): full pref matrix verified via run-as state injection + logcat breadcrumbs (docked default / pinned docked / float restore)
  • Emulator: IME switch cycles with the service alive AND destroyed — breadcrumbs show clean state convergence
  • Heide's device: enable Settings → Input → "Keyboard always starts docked"; float should never reappear
  • If it still does: the ime_mode_restore event from her session identifies the path

Fixes #73 (as far as it can be fixed without her device; diagnostics close the loop either way).

…d 3)

Heide on v0.1.23: "It's still starts in float mode. And this time, it
doesn't stay in docked mode when you bring the keyboard up again. But
one thing that's fixed though, is the letters are not right on top of
your cursor… better than the last version."

The v0.1.23 fix (#74) did not fix her device, and a full emulator repro
attempt (API 35, edge-to-edge enforced) could NOT reproduce the float
restart — the docked keyboard lays out correctly there, including IME
switch cycles. Remaining hypotheses are Samsung/OneUI-specific and need
data from the affected device. This change does three things:

1. Escape hatch (product-level, Heide has never wanted float — three
   reports): new Settings → Input → 'Keyboard always starts docked'
   toggle (dasher_ime/always_docked). When set, the saved float
   preference is ignored on every keyboard show. Verified on the
   emulator across the full matrix: unset->docked; floating+alwaysDocked
   ->docked; floating without the pin ->floating restore still works.

2. Diagnostics: every mode restore now captures ime_mode_restore with
   the decision inputs (saved_mode, always_docked, floating_before,
   restored_floating, overlay_perm — opt-in analytics, no-op
   otherwise), and transitions capture ime_float_enter/exit with
   reasons (button/restore/selfheal/unbind). If float still appears for
   Heide after this, the next session tells us exactly which path.

3. Hardening kept from the repro session: setDockedHeight now sets
   Gravity.BOTTOM explicitly (a no-gravity FrameLayout child defaults
   TOP|START in the IME decor — never correct, even if current
   platform layout paths happen to re-dock it). Log.d lifecycle
   breadcrumbs for onCreateInputView/onStartInputView/onUnbindInput/
   onDestroy/createEngine-restore.

Note: #74's fixes (engine re-entry guard, self-healing restore, unbind
cleanup) remain — they closed real holes by inspection, they just
weren't HER hole.

Signed-off-by: will wade <willwade@gmail.com>
… ImePrefs constants, gate truth table

Addresses the #75 review (7/10):

- I-1: floating_before was captured AFTER the self-heal exit — always
  false, neutering the single most valuable diagnostic (#73's exact
  path). Captured before the exit now.
- I-2: ime_mode_restore / ime_float_enter / ime_float_exit were outside
  the published analytics-events.json — 'if it's not in this schema,
  it's not collected'. All three added (properties, reason enums,
  pii:false), plus ime_always_docked_changed (M7) and android in the
  platform/app_variant examples.
- I-3: dasher_ime/always_docked/start_mode/emoji_return_alphabet
  literals were spread across three files — a typo in any one silently
  broke the gate. ImePrefs object, used everywhere.
- I-4: the restore decision extracted to pure shouldRestoreFloating();
  truth table pinned in ImePrefsTest (pin overrides floating pref;
  no-permission never restores; unset never restores).
- M5/M8: setting now uses stringResource (EN default; locales fall
  back) and the row is one toggleable touch target with Role.Switch —
  TalkBack announces label+state together, bigger target for
  motor-impaired users.
- M9: setting description documents the in-session float edge
  ('Float still floats until you dock — this decides where the
  keyboard STARTS').
- M6 accepted as-is: Log.d breadcrumbs ship deliberately (support
  case — logcat from a user device is the fallback when analytics
  can't reproduce).

Signed-off-by: will wade <willwade@gmail.com>
…ort, test inputs, hoisted reads

Final polish from the #75 second review (9/10):

- M3: onDestroy while floating now emits ime_float_exit
  (persist=false, reason=destroy — added to the schema enum) so
  enter/exit correlation stays balanced when the service dies floating.
- M1: unused android.content.Context import removed from ImePrefs.kt
  (contradicted its own 'pure, no Context' doc).
- M2: duplicate truth-table row now actually sets the pin (name true to
  inputs; pins must never force floating either).
- Nits: saved-mode read once with a null default (elvis reachable,
  definite String type); canDrawOverlays hoisted to one read per show.

Signed-off-by: will wade <willwade@gmail.com>
@willwade
willwade merged commit 1e29e69 into main Oct 24, 2026
2 checks passed
@willwade
willwade deleted the fix/ime-always-docked branch October 24, 2026 06:00
willwade added a commit that referenced this pull request Oct 28, 2026
… + Lucide toolbars + no fullscreen extract (#78)

Fixes #76 (round 2 — the real mechanism), plus the icon/toolbar overhaul
flagged in hands-on testing.

## The headline fix: the "docked" keyboard was full-screen and
top-anchored on API 35

Found in a pixel-verified E2E sweep on the API 35 emulator:
`onCreateInputView` sets the docked root's
`FrameLayout.LayoutParams(1007px, Gravity.BOTTOM)` **before attachment**
(parent=null) — then the IME framework **stamps its own
`MATCH_PARENT×WRAP_CONTENT` params over them** when attaching the view
to the decor. Measured: root laid out `1080x2272 at [0,0]` — toolbar
over the status bar, canvas swallowing the app.

That is exactly "I get fullscreen Dasher but I can't see the search
field" (hands-on), and is a strong candidate for Heide's entire #73/#76
saga on MuMu: a giant, overlay-looking, untypeable-feeling keyboard. It
also explains why #75's `Gravity.BOTTOM` "verified" on the API 33
emulator — that layout path re-docks on its own, so the stamping bug was
invisible there.

**Fix:** `setDockedHeight` runs again in the posted handler —
post-attachment, post-stamp — where the parent is real and the params
stick. Verified: toolbar strip at y1400–1540 (bottom), app visible above
it, in **portrait and landscape**, surviving **rotation and IME
switches**.

## Fullscreen extract disabled

`onEvaluateFullscreenMode() = false` — in landscape the default IMS
behavior replaces the whole screen with the IME. The keyboard now docks
at the bottom in every orientation, consistent with the product stance
and Windows/macOS Dasher.

## RFC 0002: Lucide icons + compact toolbars

The IME's View-based toolbars used text labels ("⌫", "Sel", "Cp", "Ps")
and default Material pills for Float/Hide ("no icons, looks poor, the
dock buttons are massive"):

- Lucide icons baked as tintable vector drawables (lucide-static v1.62,
ISC): arrow-left/right, delete, copy, clipboard-paste, text-select,
panel-bottom, picture-in-picture-2, keyboard-off
- Editing toolbar: 48dp icon targets (56dp delete), 8dp rounded corners
(design token rounded.md), contentDescriptions preserved
- Top bar: compact 48dp icon buttons; the Float⇄Dock toggle swaps
icon+description+action via `setFloatDockButton`; emoji toggle keeps its
"😀"/"Abc" state label in a compact pill
- **Pressed states** (design tokens: highlight-light/dark fills) — the
flat borders gave zero touch feedback
- **Long-press tooltips** (API 26+) on every icon — note: tooltip
rendering inside IME windows is unverified; press feedback + TalkBack
confirmed working

## Also

- `onConfigurationChanged` re-pushes `setSystemAppearance` (RFC 0007:
mid-session dark/light flips now re-derive the palette for
System-palette users instead of waiting for engine recreation)
- Diagnostics: `setDockedHeight`/after-layout/flush Log.d breadcrumbs

## E2E verification (API 35 emulator, pixel + logcat verified)

| Scenario | Result |
|---|---|
| Docked portrait / landscape | ✓ app visible above a bottom-docked
keyboard |
| Text commits → target field | ✓ (real typing; engine→coalescer→batched
flush→InputConnection) |
| Float→Dock cycle incl. persistence | ✓ overlay window lifecycle +
reasons logged |
| IME switch cycle | ✓ clean recreate, canvas alive |
| Rotation mid-typing (same-instance recreate) | ✓ canvas alive |
| Cold start | ✓ 2.7s to engine ready |
| Dark mode | ✓ toolbar switches; canvas follows the palette setting per
RFC 0007 |

Known-open: tooltip popups may not render inside the IME window (needs
eyes; press states and TalkBack cover the affordance), mechanical
synthetic typing can't zoom deep enough to re-verify ⌫-on-text (code
path unchanged since #71's review).

---------

Signed-off-by: will wade <willwade@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

IME: keyboard starts in float mode after switching keyboards — docked choice doesn't survive (Heide, pre-v0.1.22)

1 participant