Skip to content

Typing in every environment: landscape and tablets, terminals, physical keyboards, crash and freeze resilience - #116

Closed
Slacker-LLC wants to merge 20 commits into
mainfrom
fix/typing-environments
Closed

Slacker-LLC wants to merge 20 commits into
mainfrom
fix/typing-environments

Conversation

@Slacker-LLC

@Slacker-LLC Slacker-LLC commented Oct 2, 2026 •

Copy link
Copy Markdown
Owner

Typing in every environment, and surviving the ones that go wrong. Stacked on the 1.0.0 release PR (#114): until that is squash-merged this PR also lists its commits; the compatibility work is the last seven.

Found by running the keyboard through real environments

  • Landscape went fullscreen. The framework default hides the app's own editor behind an unthemed copy of the field. onEvaluateFullscreenMode is now false: the keyboard is a bottom panel in every orientation.
  • Rows were clipped in landscape, on tablets and foldables. Content-box padding and row widths were applied from onSizeChanged, to views laid out later in the same pass; their requestLayout() calls are dropped (each view clears its force-layout flag when it finishes laying out), so rows kept the full window width inside the clamped box. Geometry is now applied in onMeasure, before children are measured.
  • 200% system font broke key labels ("m" became an ellipsis, "中/英" lost a glyph). Key labels follow the system font up to 1.3x; function labels keep autosizing.
  • TYPE_NULL editors (terminals, games, remote desktops): Backspace did nothing, because their dummy BaseInputConnection answers true to deleteSurroundingText and removes nothing. They now start in English, get each letter as a real key event, and delete with key events. Date/time fields start on digits.
  • No physical keyboard support: letters reached the app as Latin text. In 26-key Chinese mode they now compose pinyin (space, 1-9, Enter, Esc, ', full-width punctuation; shortcuts and capitals go to the app).

Crashes, freezes, conflicts

  • A failing typing handler is contained (recorded without typed text, composition dropped) instead of killing the keyboard.
  • Crash history from Java crashes, native crashes and ANRs (Android 11+ exit reasons); three in ten minutes start safe mode (no librime, no voice preload). The About screen can copy the diagnostics or leave safe mode.
  • librime start marker with escalation: clear compiled data, set the user dictionary aside, skip native. A start killed by the user or system is not counted.
  • Media volume muted for voice input is restored after an unclean exit and by a two-minute watchdog.
  • Huge commits are chunked below the Binder limit; the lexicon and nine-key decoder are built off the main thread; Backspace no longer makes three synchronous round trips into the app when the editor reported a collapsed cursor.
  • Nine-key left rail: one character's pinyin per item, as Baidu and rime-t9-shiyin do it.

Verification

Check Result
Unit tests, lint, assemble pass (plus 9 crash-resilience, 12 physical-keyboard, 6 editor/gateway tests)
Instrumented suite, API 36 emulator 102 tests, 0 failures, 2 skipped (assumeTrue on API level)
scripts/display_matrix_regression.py (11 environments) 11/11, fails on the unfixed build
scripts/core_regression.sh 19/19, including 037 (one pinyin per character), 038 (injected failure), 040-043 (physical keyboard)
New regression tests fail without their fixes checked for the layout and font tests

docs/COMPATIBILITY.md lists every environment, how it is handled and verified, and what is not covered (nine-key with a physical keyboard, candidates when the keyboard panel is hidden, OEM differences on real phones).

CI now also runs the instrumented suite on API 26 and 34 (informational; the release gate still requires 29 and 31), so a failure there is a finding rather than a regression.

🤖 Generated with Claude Code

Slacker-LLC and others added 20 commits October 1, 2026 20:19
Local UI iteration on top of the v1 visual language: updated design tokens,
key/panel renderers, icon set (new delete/download/floating/pin/return/shield/
text-cursor/undo vectors), zh resources, reference emoji assets and the
reference capture/review scripts.

Known state: :app:testDebugUnitTest has 12 failing tests (IconLintTest,
KeyboardLayoutMetricsTest, NoDecorativeGlyphTest, NoRawColorTest,
NoRawTextSizeTest, ProductionKeyPolicyTest, TokenDriftTest) that guard the
design-token/metrics contracts; these need follow-up. output/ (screenshot
artifacts) is intentionally not committed.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…swipe-clear

Nine-key left rail
- While composing, the rail lists the real syllables that can start the open
  digits (zhong/xiong/xin/yin), longest first, led by the previewed syllable.
  Tapping one fixes it and the rail moves on to the next position
  (rime-t9-shiyin / Trime style). Backspace unlocks the last fixed syllable
  and the segment key's boundary instead of deleting letters.
- Fixed syllables reach Rime as letters (luna_pinyin accepts letters and 2-9
  in one input) so zhong/xiong stay distinct and candidates match the choice.
- The pre-edit pinyin follows the word Rime ranks first
  (9694264244326 -> wo'xiang'chi'fan / 我想吃饭) instead of the local
  decoder's independent guess.
- The fixed prefix is the one the view recorded, not whatever follows a space:
  decoder-inserted spaces between guessed syllables were being treated as
  user-fixed boundaries. Edit/delete logic is now divider-aware.
- Side-rail symbols are centred by their ink bounds; full-width punctuation
  sat in the corner of its em box.

Enter / space
- Space commits the first candidate; Enter ("确定") commits the typed pinyin as
  text, matching Rime, fcitx and Gboard Pinyin. They were identical before.

Backspace swipe-up clear
- Arm distance lowered from 56dp to 32dp (CLEAR_ARM_DP); ordinary flicks fell
  through to a single delete. The debug clear-swipe hook moved only 48dp and
  never armed it; it now uses the shared constant.

Tests: core_regression.sh gains enter-keeps-pinyin cases (9/9 on emulator);
unit tests added for locked-code, syllable options and word readings. The 12
design-contract unit failures from the previous commit are unchanged.

Known open: choosing a partial candidate still drops the remaining input.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Choosing a word that spells only the start of the input (你 for 你好吗, in 26-
or 9-key) committed it and dropped the remaining input; the native
whole-composition commit also taught the user dictionary a phrase the user
never chose (probe: input 64426, pick 你, librime committed 你好).

- JNI: nativeCandidateEnds() returns each candidate's input extent
  (Candidate::end()). RimeCandidateEntry / NativeCandidateReference carry it
  as `consumed`.
- Service: a candidate with consumed < input length commits only its text and
  republishes the leftover (nine-key keeps any fixed syllables fixed). Space
  and tap share this path. No native learning for a partial pick.
- The commit and the new composing span run in one batch edit. Without it the
  editor reports a half-way selection state and onUpdateSelection discards
  the new composition as "user moved the cursor".

core_regression.sh: +033..036 (9-key/26-key partial pick, then space).
13/13 on emulator. Unit: the same 12 design-contract failures as before.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ation row

Swipe-up clear did nothing in editors that are not an EditText (Compose,
custom canvas, web: chat apps such as the user's own). clearAllText() only
accepted editors with a select-all action or a complete ExtractedText and
otherwise failed silently, while the gesture itself fired correctly (log:
clearArmed=true, finish commit=true). It now falls back to before/after-cursor
text: capture everything, delete until the editor reports empty (safe for
editors that cap their answers), keep an undo snapshot. An answer as long as
the request is treated as a possible window and is never deleted.
A new debug CustomEditorTestActivity reproduces such an editor; old build left
the text untouched, new build clears and restores it.

Gesture UI is one component now. Clear and restore were a red pill, a white
card and a tiny label inside the key; the pill was flush against the screen
edge. Now a single bubble (same size/position/animation) beside the key where
the thumb does not cover it: dark "上滑清空" -> red "松手清空" for clear,
dark "下滑撤回" -> accent "松手撤回" for restore. After a clear the toolbar
shows "已清空 [撤销]" for the five-second undo window. The restore hint is
driven by the gateway's real undo snapshot instead of a flag that never
expired. The release coordinates count as a last move (quick flicks cross
the threshold on the UP itself). The arm distance is 32dp (was 56dp).

Association row (after committing a word) matches the design: "‹ words ∨".
The hide chevron kept layout weight 1 and shared the row with the words, so
the chevron floated mid-row and the words were pushed right.

Accent: pre-edit text, tool icons and setup accents used a hard-coded #006AB1
in light themes and ignored the chosen accent, so a custom accent put two
unrelated blues side by side. selectedText() now derives a readable shade
from the active accent.

Known: 12 design-contract unit tests still fail (unchanged).
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…xed syllables stay fixed

Left rail (composing): a list of readings, one continuous panel with flat
items and an accent pill on the selected one, as in the design draft.
Short input lists whole readings (ni'hao, mi'gan, ni'gao ...); once a whole
reading no longer fits the rail it lists first syllables (zhong, xiong) and
fixing one moves the list to the next position (Baidu / rime-t9-shiyin). A tap
fixes exactly what the item shows. Readings come from a beam search over real
syllables that rewards word-forming paths and drops digit-grid artefacts
(lone a/o/e, vowel-less ng/m); choices that leave unreadable digits are never
offered.

A syllable the user fixed stays fixed when more digits follow (it is sealed
with a boundary instead of being re-decoded), and backspace unlocks the last
fixed syllable or the boundary the segment key made.

Pre-edit boundaries are apostrophes everywhere (ni'hao), not spaces.

Ranking: words the typed digits spell exactly now come before longer words
that merely start with them. Typing xian (9426) led with 自从 (zi'cong, a
prediction) while the pre-edit said xian. Rime's order is kept inside each
group and nothing is dropped.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…cisions); changelog

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…p a11y and clipboard targets

Visual refresh (design tokens, vector marks, typography roles, setup page)
and the fixes it needed to land green:

- layout: widen the reference-scale change epsilon (0.001 -> 0.01) and post
  a relayout after a rebuild in onSizeChanged. Configuration.screenWidthDp
  is a whole number while the measured width is not, so the same window
  looked like a new geometry, every key row was rebuilt mid-layout and the
  keyboard stayed blank on first show.
- setup: hide each card's decoration from accessibility in code. The layout
  refresh dropped the importantForAccessibility attributes and the runtime
  status pill never had them, so a screen reader landed on every child.
- clipboard: retention / confirm actions keep a 48dp touch target and are
  painted 36dp inside it (as the settings segmented controls, 34dp in 48dp).
- tests: IconLint, TokenDrift, KeyboardLayoutMetrics and ProductionKeyPolicy
  follow the redesigned contracts; instrumented tests follow the current UI
  (skin sub-page, appearance picker, voice language segments, preview popup,
  scaled cursor-drag distances, arrow glyph cursor keys, setup cards, nine-key
  reading list, expanded candidate overlay). isKeyPopupShown() and scaledPx()
  are the new test hooks: the key preview is a permanent child that only
  toggles visibility, and gesture thresholds scale with the reference size.
- docs: changelog, SOP swipe-clear criteria; ignore generated output/.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- VERSION (MAJOR.MINOR.PATCH) is read by app/build.gradle.kts; versionCode is
  derived as major*10000 + minor*100 + patch, so it cannot drift and every
  release is strictly greater than the one before (1.0.0 -> 10000; development
  builds were at 4, so in-place upgrades keep working).
- scripts/release_check.py applies the same rules to the repository (VERSION is
  plain semver, CHANGELOG.md has the dated section for it right below
  [Unreleased], tag == v<VERSION>) and to a built APK (package, versionName,
  versionCode via aapt2), with unit tests.
- CHANGELOG.md is reorganised into Keep a Changelog form with a user-facing
  1.0.0 section (it doubles as the GitHub Release notes). The old
  development-period entries are kept verbatim in docs/CHANGELOG_PRE_1.0.md.
- .gitignore: Python caches and signing material (*.jks, *.keystore, *.p12).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…l CI

Release workflow (scripts/release_build.sh does the work, so it can also be run
locally):
- tags must be vX.Y.Z, equal VERSION, sit on main and have passed Build and
  verify plus both compatibility jobs; the changelog section becomes the
  release notes.
- verifies the APK: signed by a non-debug key, only arm64-v8a, package and
  version match VERSION, certificate matches docs/release-cert.sha256 once it is
  recorded.
- the job that holds the signing key cannot write to the repository; a second
  job (contents: write, no key) creates a draft release, checks all three
  assets are attached, then publishes.
- the same pipeline runs with a throwaway key on manual runs and on pull
  requests that touch the pipeline (nothing is published), so the first real
  release is not the first run. lintRelease, which PR CI never ran, is part of it.
- scripts/setup_release_signing.sh creates the key and the four Actions secrets
  without printing the passwords, and refuses to overwrite an existing key.

Android CI: version/changelog checks run before the toolchain is installed, the
built APK's identity is verified, and the API 29/31 jobs now fail when the
instrumentation report does not end in "OK (N tests)" (am instrument exits 0 on
failures).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- docs/RELEASE.md: versioning rules, changelog rules, signing key custody,
  release steps, rehearsal, rollback.
- docs/REPOSITORY.md and scripts/apply_repo_settings.sh: squash-only merges with
  the PR title/body as the commit message, protected main (PR + Build and verify,
  linear history, no force push), immutable v* tags, security features, so the
  settings are reviewable and reproducible.
- Dependabot (Actions and Gradle, weekly, grouped), CODEOWNERS, PR template
  changelog/version checklist, SECURITY.md supported versions and private
  reporting, CONTRIBUTING branch/merge/version rules, README download and
  verification instructions, CI gate documentation.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The release workflow writes three secrets into GITHUB_ENV as NAME=value lines,
so a value containing a newline could inject further variables; refuse it.
release_build.sh needs mapfile and empty-array expansion under set -u (bash
4.4+); fail with a clear message instead of an obscure error on macOS bash 3.2.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… system fonts

Found by running the live keyboard through a display matrix instead of only the
default portrait phone:

- Landscape no longer enters fullscreen "extract" mode (onEvaluateFullscreenMode
  = false). By default the framework hid the app's own editor behind an
  unthemed copy of the field; chat, search and form typing disappeared.
- Content-box geometry (paddings, row widths) is now applied in onMeasure,
  before the children are measured. It used to be applied from onSizeChanged,
  i.e. to views that are laid out later in the same pass; their own
  requestLayout() calls are dropped because each view clears its force-layout
  flag when it finishes laying out, so the rows kept the full window width
  inside a clamped content box and were clipped (landscape, tablets, foldables).
  A deferred relayout flags the changed containers for late callers.
- Key labels follow the system font only up to 1.3x and the autosize of function
  labels survives applyMainTextScale. At 200% "m" became an ellipsis and "中/英"
  lost a glyph.

Tests: DisplayEnvironmentInstrumentedTest (content box and label fit across
layouts and font scales; both fail without these fixes), and
scripts/display_matrix_regression.py, which drives the real IME through 11
environments and asserts bottom-panel mode plus every key inside the window.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Commercial and open-source nine-key keyboards (Baidu, rime-t9-shiyin, iOS) let
the user choose the pinyin of one character at a time: the left column holds
the syllables for the next character, a tap fixes it and the list moves on.
The rail listed whole readings (ni'hao) for short input, which made one tap
decide the spelling of several characters. It now always lists first syllables.

Unit tests follow; core_regression case 037 types 64426, picks ni, then hao,
then space and expects 你好. docs/NINE_KEY_REFERENCE.md and the 1.0.0 notes
describe the new behaviour.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Both were built in onCreate on the main thread (about 0.3 s on a fast host) at
every cold start. The service starts with an empty pipeline and swaps in the
real one when the background thread is done; Rime supplies candidates meanwhile.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- CrashGuard records crashes locally (types and frames only, never typed text),
  folds native crashes and ANRs from Android's exit history into a crash loop
  counter, and three in ten minutes put the next start in safe mode (no librime,
  no voice preload) so the user can keep typing.
- A failing typing handler is contained: it is recorded, the half-finished
  composition is dropped and the keyboard carries on instead of being replaced
  by another keyboard. Debug command fail-next injects one (core_regression 038).
- Librime startup leaves a marker until the engine proves itself. A marker left
  by a start that died natively escalates: clear compiled data, set the user
  dictionary aside, then skip the native engine. A start merely killed by the
  user or system is not counted (Android 11+ exit reasons).
- Media volume muted for voice input is persisted and restored on the next start
  if the process dies mid-recording, and a watchdog restores it after two minutes
  (instrumented test).
- Huge commits (pasting megabytes) are chunked below the Binder limit.
- The lexicon and nine-key decoder are built off the main thread (they cost
  about 0.3 s on every cold start).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…er in the entry activities

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… handling

- TYPE_NULL editors (terminals, games, remote desktops) start in English,
  receive each letter as it is typed, and are edited with key events: their
  InputConnection is normally BaseInputConnection in dummy mode, where
  deleteSurroundingText answers true and removes nothing, so Backspace did
  nothing there. Pinyin still composes if the user switches to it.
- Date and time fields (TYPE_CLASS_DATETIME) start on digits.

Unit tests cover the editor kinds and the key-event paths.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
On tablets, foldables with a keyboard cover, Chromebooks, desktop mode and the
emulator, letters used to reach the app as plain Latin text. In the 26-key
Chinese mode they now compose pinyin: space picks the first candidate, 1-9 pick
a candidate, Enter keeps the typed pinyin, Esc cancels, ' separates syllables,
and , . ? ! ; : ( ) give full-width punctuation (after a digit , . : stay ASCII).
Ctrl/Alt/Meta shortcuts, capitals and every other key go to the app unchanged.
HardwareKeyPolicy is pure and unit-tested; core_regression 040-043 drive real
key events on the emulator.

docs/COMPATIBILITY.md lists every environment the keyboard handles, how, and how
it is verified, and what is still not covered.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Informational like the others; the release gate still requires 29 and 31.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…own collapsed

Deleting asked the app for the selected text and the extracted text before every
single-character delete: three synchronous Binder calls, each of which waits as
long as a slow or stuck app takes. Once the editor itself has reported a
collapsed cursor (onUpdateSelection, not the start-up values) there is no
selection to ask about.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@Slacker-LLC

Copy link
Copy Markdown
Owner Author

Superseded by #117 (the same commits rebased onto main after #114 was merged, plus the certificate fingerprint).

@Slacker-LLC Slacker-LLC closed this Oct 2, 2026
@Slacker-LLC
Slacker-LLC deleted the fix/typing-environments branch October 2, 2026 05:52
Slacker-LLC added a commit that referenced this pull request Oct 2, 2026
… resilience (#117)

Typing in every environment, and surviving the ones that go wrong: the
second half of 1.0.0 (the first half is #114, already on main). It
supersedes #116 (same commits rebased onto main) and records the release
signing certificate fingerprint, so the first release is checked against
the key in the repository secrets.

## Found by running the keyboard through real environments

- **Landscape went fullscreen.** The framework default hides the app's
own editor behind an unthemed copy of the field.
`onEvaluateFullscreenMode` is now false: the keyboard is a bottom panel
in every orientation.
- **Rows were clipped in landscape, on tablets and foldables.**
Content-box padding and row widths were applied from `onSizeChanged`, to
views laid out later in the same pass; their `requestLayout()` calls are
dropped (each view clears its force-layout flag when it finishes laying
out), so rows kept the full window width inside the clamped box.
Geometry is now applied in `onMeasure`, before children are measured.
- **200% system font broke key labels** ("m" became an ellipsis, "中/英"
lost a glyph). Key labels follow the system font up to 1.3x; function
labels keep autosizing.
- **TYPE_NULL editors (terminals, games, remote desktops)**: Backspace
did nothing, because their dummy `BaseInputConnection` answers true to
`deleteSurroundingText` and removes nothing. They now start in English,
get each letter as a real key event, and delete with key events.
Date/time fields start on digits.
- **No physical keyboard support**: letters reached the app as Latin
text. In 26-key Chinese mode they now compose pinyin (space, 1-9, Enter,
Esc, `'`, full-width punctuation; shortcuts and capitals go to the app).

## Crashes, freezes, conflicts

- A failing typing handler is contained (recorded without typed text,
composition dropped) instead of killing the keyboard.
- Crash history from Java crashes, native crashes and ANRs (Android 11+
exit reasons); three in ten minutes start safe mode (no librime, no
voice preload). The About screen can copy the diagnostics or leave safe
mode.
- librime start marker with escalation: clear compiled data, set the
user dictionary aside, skip native. A start killed by the user or system
is not counted.
- Media volume muted for voice input is restored after an unclean exit
and by a two-minute watchdog.
- Huge commits are chunked below the Binder limit; the lexicon and
nine-key decoder are built off the main thread; Backspace no longer
makes three synchronous round trips into the app when the editor
reported a collapsed cursor.
- Nine-key left rail: one character's pinyin per item, as Baidu and
rime-t9-shiyin do it.

## Verification

| Check | Result |
|---|---|
| Unit tests, lint, assemble | pass (plus 9 crash-resilience, 12
physical-keyboard, 6 editor/gateway tests) |
| Instrumented suite, API 36 emulator | 102 tests, 0 failures, 2 skipped
(`assumeTrue` on API level) |
| `scripts/display_matrix_regression.py` (11 environments) | 11/11,
fails on the unfixed build |
| `scripts/core_regression.sh` | 19/19, including 037 (one pinyin per
character), 038 (injected failure), 040-043 (physical keyboard) |
| New regression tests fail without their fixes | checked for the layout
and font tests |

`docs/COMPATIBILITY.md` lists every environment, how it is handled and
verified, and what is not covered (nine-key with a physical keyboard,
candidates when the keyboard panel is hidden, OEM differences on real
phones).

CI now also runs the instrumented suite on API 26 and 34 (informational;
the release gate still requires 29 and 31), so a failure there is a
finding rather than a regression.

🤖 Generated with [Claude Code](https://claude.ai/code)

---------

Co-authored-by: limuzi013 <128580527+limuzi013@users.noreply.github.com>
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.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.

1 participant