Skip to content

Add the per-keyboard volume E2E run - #140

Open
Slacker-LLC wants to merge 103 commits into
mainfrom
test/volume-e2e
Open

Slacker-LLC wants to merge 103 commits into
mainfrom
test/volume-e2e

Conversation

@Slacker-LLC

Copy link
Copy Markdown
Owner

The stress script counts taps and mode switches; this run types real text and checks what was committed.

scripts/typing_volume_e2e.py, driven by scripts/build_volume_data.py:

Keyboard random keys words four-character sentences
26-key pinyin, nine-key, double pinyin, stroke 2,000 1,000 500 100
Wubi 2,000 500 – –
English 2,000 1,000 – 100
Digits 2,000 40 groups – –

Input comes from the bundled dictionaries and this repository's own prose, so no network is needed and nothing is written into the repository. A word is a hit only when the committed text equals the expected text; a sentence is scored by character accuracy. On a miss the harness re-types that word alone and records the expected text, the committed text and the candidate list, which is how it found the ranking defect fixed in #138 (dang offered 打 first, and 当地 sat at rank 6 behind single characters).

The two debug-only commands the harness needs — editor-tail64 and candidates64 — read the committed text and the candidate bar in one round trip and stay unreachable in a release build, like the rest of the test bridge. docs/TEST_SOP.md section 13 records how to run it.

🤖 Generated with Claude Code

Slacker-LLC and others added 30 commits October 14, 2026 22:46
The corner marks were an older scheme of our own (a=~, s=!, d=@ …). They now
follow the reference product's map (ficon_text in input_kbd_pinyin26.xml and
input_kbd_english26.xml): the same key carries the full-width mark in Chinese
and the ASCII mark in English, the top row keeps 1–0, and a, s, z, c, m are the
same in both languages.

A long press now opens only the letter in both cases, upper first, as the
reference does (long_press_bubble_text); the mark stays on the swipe-up gesture
(fusiyin_keycode). The comma/period key also commits the mark above it on a
swipe up.

Co-Authored-By: Claude Code <noreply@anthropic.com>
… reference skin

- The 26-key draws A and L half a letter wider (skin weight 15 against 10) and
  the last row's 分词 / 退格 with the same ratio, instead of nine equal letters.
- The nine-key and stroke side column is 删除 / 重输 / 发送 with send twice as
  tall; the separate zero key goes away and the space bar carries the corner 0
  (an up swipe types it, behind the 上滑数字 setting).
- Candidate items pad 8dp per side, so more candidates fit on the strip.

Co-Authored-By: Claude Code <noreply@anthropic.com>
The rail during composition is now the same flat list the symbols use: one
reading per row at one text size, a hairline between rows, and no state drawn
on any item — the pre-edit says which reading is current. The list keeps the
user's scroll position until the open digits change, instead of re-centring on
every keystroke.

The reference ships one FilterListLayout for both jobs and gives the reading
the symbol item's own style (cls_pinyin9_filter_button inherits
cls_vertical_symbol_list_button, style.xml:1023-1032), with syllables one
weight heavier at the same size (style.xml:112,121-122). The native-side
evidence is recorded in docs/DECISIONS.md.

Co-Authored-By: Claude Code <noreply@anthropic.com>
The separate zero key went away with the reference-aligned side column, so the
rail case swipes up on the space bar, the way the key works.

Co-Authored-By: Claude Code <noreply@anthropic.com>
When this app is the user's input method its service is bound in the test
process and its clipboard listener captures every copy — including the fixture
these two tests copy — by design, so the queued panel load they check is
indistinguishable from the listener. They now hold the precondition they need
instead of failing; a system IME active (CI) runs them as before.

Co-Authored-By: Claude Code <noreply@anthropic.com>
-PopenimeAppIdSuffix=.dev installs beside the release build (the owner's phone
runs the .dev variant); release builds never pass it.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Typing "dang" also produced the split da + "ng", and no syllable starts
with "ng", so that path can never complete a reading of the input; the
words for the settled "da" prefix (打, score 7798) nonetheless entered the
candidate set and outranked the complete reading 当 (6899), so the space
bar committed 打 and 当地 fell to rank 6 of "dangdi".

Candidates now carry a coverage rank: a word whose reading covers the whole
input sorts above one that only matches a prefix of it, and frequency
decides inside each rank. "dang" -> 当 党 档, "dangdi" -> 当地 荡涤.

Co-Authored-By: Claude Code <noreply@anthropic.com>
An address or e-mail field now gets `. / www. .com .cn http wap. ftp:` on
the nine-key rail instead of the punctuation marks, and no + cell,
because that list belongs to the field. The values are the reference nine-key
layout's own `uri_filter_list` attribute; only that layout declares one.

Co-Authored-By: Claude Code <noreply@anthropic.com>
The reference's first-item class takes the candidate font one weight
heavier and the accent as its text colour, with no background
(cls_option_first_canditem, style.xml:551-554; first_cand_font weight 500
against 400; first_cand_text_color rgba(79,132,255,1)). openIME drew it
bold on a key-coloured tile.

Co-Authored-By: Claude Code <noreply@anthropic.com>
The expanded candidates used to be the key grid itself: one bordered
tile per candidate with the first one filled in the accent. The
reference's 更多候选 page is a different surface, and its skin file says
how it is built.

The page now opens with 全部 / 单字 as one option group over the list
(input_more_cand.xml:9-13), the list sits in one rounded card (radius 11,
2 edge, cls_morecand_bk), and the backspace floats in that card's
bottom-right corner at 25.2% by 20.2% of the list, rounding only that
corner with the card's own radius (bkround="0,0,11,0"). Every cell is
the same flat label with a hairline edge (cls_morecands_button): the
skin gives the expanded list no first-item class, so no cell carries the
accent tile.

单字 keeps single characters and 全部 is the page's default, as the
options declare. The corner key deletes one letter of the open
composition, so the page stays open on the shorter list.

The reference's left syllable list (FilterListLayout, 音节过滤) is not
in this change: the 26-key path has no syllable-fixing state yet.

Co-Authored-By: Claude Code <noreply@anthropic.com>
The URL rail, the first-candidate accent, the candidate ranking fix and
the expanded page were user-visible changes without an entry.

Co-Authored-By: Claude Code <noreply@anthropic.com>
The 26-key bottom row gains the reference's two symbol keys beside the
space bar, in Chinese and English, with the address-field marks in a URL
or e-mail field, and each key takes the reference's own weight. The
English merged key now leads with the period and carries the comma in
its corner, as the skin draws it, instead of the other way round.

The nine-key 1 cell becomes the reference's @# key: the label names the
symbols it offers, a tap and an up swipe still type the digit, and a long
press opens the reference's own seven-symbol bubble. The board has no
分词 key because the reference's has none, and the 26-key 分词 key drops
the invented corner mark and popup its class never declares.

Co-Authored-By: Claude Code <noreply@anthropic.com>
The open Pinyin string now paints the syllables the user has fixed in
the accent and the unconverted tail in the key text colour, as the
reference's cls_comp_half / cls_comp_right pair does, and it takes
comp_font's own size. A decoder-inserted divider stays in the plain
colour: only a syllable the user chose is fixed.

Co-Authored-By: Claude Code <noreply@anthropic.com>
The strip now insets the candidate list at both ends, draws the
reference's hairline split between the list and the expand key, keeps
a 3-unit gap before it, and gives the expand key the reference's pill
press shape. Its 48dp touch target stays: that is our own rule.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Each idle-toolbar key is now the reference's own 40-unit pill with a
press tint, instead of a bare icon stretched by weight across the row,
and the slack of the strip sits in a spacer ahead of the hide key. The
48dp touch target stays our own rule.

Co-Authored-By: Claude Code <noreply@anthropic.com>
The digits page was a 3x3 grid over a 返回 / 空格 / 0 row with
退格 / . / @ / 发送 down the right. The reference's 26-key 123 key opens
input_kbd_number_from26.xml instead: a symbol rail over the return key in
the left column, three digit columns closed by 符号 / 0 / . , and a
delete / space / send column, at the skin's own weights 66 / 75 / 75 / 75 / 66
and with send twice as tall as the keys above it. The @ key is gone with no
number page declaring one; in a phone field the symbol cell carries the
dialer's # and the period cell its +, so * / + / # stay reachable.

Co-Authored-By: Claude Code <noreply@anthropic.com>
input_kbd_stroke.xml draws three columns of three cells: 一 丨 丿 /
丶 折 通配 / @# :;, each with the skin's own corner digit and an up swipe
that types it. The last row was missing from our board, so 通配's digit 6
and the two punctuation marks had no key; the @# cell now opens the
reference's own / - _ @ # . 7 list on a long press. The bottom rows of the
stroke and nine-key boards take the reference's 45.5 / 134 / 45.5, which
fills the 225-unit centre column exactly.

Co-Authored-By: Claude Code <noreply@anthropic.com>
…trip

The reference's idle row runs clipboard, emoji, board selector, where ours
led with the board selector, and its icons sit in a strip that scrolls when
they do not fit, with the slack in a spacer at the strip's own end
(sliderenable, input_toolbar_idle.xml:14,34). Ours stretched the icons by
weight instead, so a narrow screen shrank them, and the spacer sat outside
the strip. 收起 stays outside it.

Co-Authored-By: Claude Code <noreply@anthropic.com>
input_kbd_symbol.xml ends the page with a row of the return key and the
category container, weighted 85 against 13.1, and scrolling. Ours put the
categories down a 74dp column at the left edge, which cost the grid a fifth
of the page's width. The strip is the chip row the emoji page and the
expanded candidate page already use, so the symbol surfaces agree now.

Co-Authored-By: Claude Code <noreply@anthropic.com>
The reference's symbol row closes with its 123 key and its backspace at the
two ends (input_kbd_symbol.xml:31-41, weight 15 against the cells' 70), so
the page reaches the digits and deletes without being left first. Our cells
are five to a row, so both keys take the ends of the tab strip's row, the
row the reference gives to the page's own return key.

Co-Authored-By: Claude Code <noreply@anthropic.com>
The reference fills its symbol board's toolbar with the history of what the
user has typed (input_toolbar_quickinput.xml:4-18): a 4-unit lead inset, the
scrolling list, the candidate strip's own hairline split, a 3-unit gap, a
40-unit close key and a 4-unit tail. Each cell is 36 units wide with a
radius-6 press face over candidate-sized text in the candidate text colour
(style.xml:1061-1070), and a tap commits through the same path the board's
own cells use, so the list re-reads itself after every symbol.

Co-Authored-By: Claude Code <noreply@anthropic.com>
The tools page and the keyboard switcher are one surface in the reference:
a container 88.9% of the page wide with its own paddings, tiles filling
their cells in a 4-by-2 grid capped at 192 units (style.xml:616-619), each
tile a radius-14 card filled with setting_button_bk, its pressed colour, or
the theme colour when it is the chosen one, and its name dimmed to
keybutton_text_3 (style.xml:626-647, values/colors.xml:15,87-93).

Co-Authored-By: Claude Code <noreply@anthropic.com>
The reference fills the toolbar row with a tip while the floating
keyboard is up (`input_toolbar_tip.xml:43-50`): one pill across the
row, a 3.571% lead space, a left-aligned ellipsized label and a round
close key whose width is its own height. Each of those numbers is now
a constant with the line it came from, and the close key keeps our
48dp touch target.

Ours names our own way back — a tap on the handle — where the
reference asks for a drag to the bottom edge, because the docked
keyboard is what our handle restores. The tip settles for the stay
afloat once its close key is tapped, and the next stay shows it again.

Co-Authored-By: Claude Code <noreply@anthropic.com>
The typing toolbar holds the pinyin block in a wrapper of at most half
the page (`maxwidth="halfMaxWidth"`, `input_toolbar_typing.xml:7`),
with a 5-unit lead of its own and the row's 3-unit top inset; the other
half goes to an empty control, the room the reference keeps beside it
(line 18). The string scrolls inside that cap, as the reference's own
composition container does, and the block carries the container's own
press tint (`comp_pushedbk`, black 8% light and white 12% dark).

Our pre-edit ran the full width of the row in portrait and stopped at a
160dp cap of our own making in landscape, and it gave no press feedback
while it was edited.

Co-Authored-By: Claude Code <noreply@anthropic.com>
A fresh copy or a text message is answered by the reference's own
quick-input toolbar (`input_toolbar_quickinput.xml`): the text at the
candidate font in `canditem_text` with the glyph that names where it
came from (lines 39-45), and the row's own close key borrowed from
`cls_history_close_button` (line 46) instead of a back control.

Our chips were filled pills at the body size behind a back chevron, a
shape no reference toolbar draws. The system's autofill chips keep
that back control, because they are the platform's own views in the
same strip; every row keeps our 48dp touch target.

Co-Authored-By: Claude Code <noreply@anthropic.com>
The tip, pre-edit and quick-input comments quoted nearby lines for the
container, the lead space, the close key and the composition wrapper.
Each now names the line the attribute is actually on.

Co-Authored-By: Claude Code <noreply@anthropic.com>
The reference's quick-input row measures both parts against the row
(`input_toolbar_quickinput.xml:42,44-45`): the text box is inset
0.055/0.111 of it, and the glyph that names where the text came from is
a superscript at the row's top edge, `fi_toolbar_tip_superscript`'s 30
units against `cand_font`'s 56 (`style.xml:138,83`).

Our first pass drew the glyph as a fixed 18dp icon centred on the row,
which reads as a second key beside the text rather than as the mark of
its origin.

Co-Authored-By: Claude Code <noreply@anthropic.com>
The strip's container is `input_toolbar_typing.xml:23-32`, not 16-30, in
the keyboard's own comment and in DECISIONS.md.

Co-Authored-By: Claude Code <noreply@anthropic.com>
The theme pass rescales every text view it meets by the reference
scale, so a badge sized from the design token alone drifted from its
own label on a device whose scale is not 1. The badge now takes the
label's measured size at layout, which also drops the policy helper
the view no longer needs.

Co-Authored-By: Claude Code <noreply@anthropic.com>
CI failed the same sixteen tests on every API level, in four groups.

The tip banner (6b6a199) took the floating toolbar row: the quick-phrase and
long-press-drag tests read a row that no longer carries icons, and the
banner's own width tests measured before the first layout pass. They now
settle the banner as the user does and wait for the row to place itself.

The quick-input rows belong to a real editor session, which a keyboard view
built by hand has none of, so they now declare the editor options they are
drawn for through the new `editorOptionsOverride` seam.

The symbol history tests cleared the store inside their own harness, wiping
the history they had just arranged; the `@After` cleanup already empties it
between tests, so the record stands.

The rest follow the reference rather than our earlier guess:

- English's merged key commits "." and corners "," , as its layout draws it
  (`input_kbd_english26.xml:154-161`), and its up swipe types the comma.
- The launcher grid packs four tiles per row and leaves the last row short
  (`input_settings.xml:13-45`); the cells keep the page's one column width.
- The tile radius goes through the page's own reference scale, as every other
  reference unit does.
- The handwriting tile follows the strokes table only local builds ship.
- The bottom row is measured with the reference's own key widths (123 70.6,
  merged 37.6, both symbol keys 35.5, space 133.6, 中/英 38.6, 确定 74.6,
  `style.xml:960-978`), which put a hair more weight on the left than the
  right, so the space bar sits just left of the centre line.
- The font test counts only keys the user can see, and names the key it
  reports.

The merged and symbol keys of the 26-key bottom row now shrink their mark
rather than ellipsizing it, as the reference gives them their own fonts for
(`text_comma_cn` 72 units against `key_letter` 57, `style.xml:127`, `:4`).

Co-Authored-By: Claude Code <noreply@anthropic.com>
Slacker-LLC and others added 13 commits October 15, 2026 18:13
updateResponsiveGeometry runs from onMeasure and wrote mainDock's and the
second key row's layout parameters back unconditionally. Assigning
layoutParams always calls requestLayout(), and a request made after
performTraversals has cleared mLayoutRequested schedules another full
layout pass ("Clear this now, so that if anything requests a layout in
the rest of this function we will catch it and re-run a full layout
pass", ViewRootImpl.performTraversals) — while nothing in the chain was
conditional. The keyboard therefore re-measured, re-laid out and redrew
every frame for as long as it was shown, docked or floating.

The three writes now happen only when the value they carry differs.

Co-Authored-By: Claude Code <noreply@anthropic.com>
The floating keyboard's long-press drag only started when the press landed
on a toolbar icon. With no child under the finger, the framework dispatches
the rest of the gesture straight to the zone's own onTouchEvent and never
asks the interceptor again ("intercepted = true" in the else branch,
ViewGroup.dispatchTouchEvent, actionMasked != ACTION_DOWN with no touch
target), and the zone's onTouchEvent returned super's false — so the
long press armed and nothing ever came of it. The zone now claims the
gesture itself once it was going to intercept the press, swallows the
DOWN, and starts the drag on the first move after the long press.

The docked height drag never armed at all. It gates on the press being
inside the toolbar row (ImeTopZone.isInsideToolbar) and compared the row
taken with getLocationInWindow — window-local — against MotionEvent.rawY,
which is "the original location of the event on the screen, before it had
been adjusted for the containing window and views". This keyboard's window
is not the screen (dumpsys window: frame=[0,1731][1080,2400], gravity
BOTTOM), so its toolbar row sits at window y 0 while a finger on the row
reports a raw y of about 1731: the check could not be true on a device.
It now measures the row on screen, the same way the candidate choice strip
tests raw touches (KeyPopupController.trackChoiceTouch).

Co-Authored-By: Claude Code <noreply@anthropic.com>
Dragging the card wrote the window's attributes on every touch sample,
including the samples whose rounded delta is zero: at 60Hz that is a
relayout of the IME window per frame while a finger jitters on the
toolbar, and the drag path also re-read the opacity preference each
time. applyLayout now returns early when position, size and alpha all
match what the window already carries, and the opacity comes from the
value scheduleLayout last applied.

A drag that does move the card still writes, so the bounds still clamp
and the position is still remembered.

Co-Authored-By: Claude Code <noreply@anthropic.com>
hasPressedKey() answers "a key still owns this gesture" by walking the rows
for isPressed, and renderModeBody defers to a 40ms poll while it is true.
isPressed is not cleared when a view is taken out of the hierarchy, so a
gesture whose key was rebuilt away under it — or one a rebuild swallowed —
left a key that looked pressed forever: every later rebuild (a keyboard
switch, a rotation, closing a panel) silently did nothing and the poll
re-posted itself for as long as the keyboard was attached.

A touch stream is only live between a DOWN and its UP, and this view
already sees every event that reaches the keyboard, so dispatchTouchEvent
now tracks whether one is open and hasPressedKey() answers false when none
is. The deferral still protects a real gesture, and the rebuild it deferred
lands as soon as the finger lifts.

Co-Authored-By: Claude Code <noreply@anthropic.com>
The docked height drag went through setKeyboardHeightPercent on every touch
sample: each one wrote the preference and rebuilt the whole key surface,
so a single drag persisted the height a hundred times and rebuilt the
keyboard a hundred times.

The drag now previews — the row heights and the wanted window height follow
the finger, and the rebuild is posted to the next frame so a frame with
several samples rebuilds once — while the commit happens on release, from
either the UP or a CANCEL that takes the gesture away
(KeyboardHeightDragController.onDragEnd). The keyboard reports and persists
the height exactly once, when the user lets go.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Co-Authored-By: Claude Code <noreply@anthropic.com>
The zone gives toolbarRow its own full height, so the row reaches the zone's
bottom: measuring the band below the row found 0px and the test polled until
its 30s timeout. The band is the slack around the 48dp icon strip inside the
row, which is where the gesture lands on no child.

Co-Authored-By: Claude Code <noreply@anthropic.com>
The stress script covers taps and modes; this one types real text: two
thousand random keys, a thousand words, five hundred four-character words
and a hundred sentences of fifteen characters or more per Chinese
keyboard, on every keyboard the app offers. Words come from the bundled
dictionaries, sentences from this repository's own Chinese prose, so the
run needs no network and writes nothing into the repository.

A word counts as a hit only when the committed text is the expected text;
on a miss the harness re-types the word alone and keeps the expected
text, the committed text and the candidate list, which is how the ranking
defect behind "dang -> 打" was found. The two debug-only read commands
(editor-tail64, candidates64) give the harness one round trip per check
and stay unreachable in a release build.

Co-Authored-By: Claude Code <noreply@anthropic.com>
The word and four-character lists leave 452 of the thousand most common
characters untouched, so the run gained a character phase: each character
by its own reading (wubi and stroke by their own code), checked the same
way, and a --phases filter to run one phase alone.

Co-Authored-By: Claude Code <noreply@anthropic.com>
A long per-keyboard run can outlive a foreground change: another app
comes to the front, the input view goes away, and the next bounds read
raises. The run then dies hours in and loses the whole keyboard.

Keys now takes an on_lost callback. When refresh_bounds fails, it calls
back once, re-shows the keyboard and re-applies the mode, then reads the
bounds again, so the run continues from where it stopped.

Co-Authored-By: Claude Code <noreply@anthropic.com>
The run lasts hours. Its recovery already brings the keyboard back when
another app takes the foreground (which is what stopped the first full
run); on top of that the device now stays awake and lit for the whole run,
so a screen timeout can never take the input method away either.

Co-Authored-By: Claude Code <noreply@anthropic.com>
phase_rows only kept the wrong rows, so a report could not split a phase
by input shape - the word list is two blocks (two-character words first,
three-character after) and one rate over both hides the step at word 700.
Each row now lands in the phase's results with the text that came out and
the rank the expected word got.

ensure() also replaces the two inline refresh-and-retry blocks: a lost
keyboard is recovered through the same path, and the recovery stops the
apps known to take the front of the shared emulator first.

Co-Authored-By: Claude Code <noreply@anthropic.com>
The 2026-10-15 run started on a tree 29-43 commits behind HEAD and had its
APK swapped 22 minutes in, so the report described an engine nobody would
ship. Compare the installed package's lastUpdateTime with the built APK's
mtime before the first tap and again at every keyboard switch, and stop
with the install command instead of measuring the wrong build.

Co-Authored-By: Claude Code <noreply@anthropic.com>
@Slacker-LLC
Slacker-LLC changed the base branch from main to align/letter-hints October 15, 2026 14:42
Slacker-LLC and others added 13 commits October 15, 2026 23:01
The reference documents the gesture itself: the subtitle of its own switch
(`res/cU.xml`, key `enable_key_enter_send_msg`, default false) reads
「长按「发送」可换行。开关支持App范围」, and its confirm dialog repeats the
line. Only the send face is documented, so only that face takes a long
press here; a composition reads 确定 on the key and keeps its press-only
behaviour, as do the other action faces.

`enterKeyLongPressFor` sits next to the other Enter-key policy so the key
face, the dispatched action and the long press all resolve from the same
EditorInfo bits. `ImeKeyboardView.syncEnterKeyPresentation` wires the
listener and says the gesture in the key's contentDescription
(「发送,长按换行」), `onEnterNewline` commits a live composition the way
the key's own press does and otherwise sends a raw Enter.

Co-Authored-By: Claude Code <noreply@anthropic.com>
The reference names all three parts of this feature itself: the clipboard
list's tip 「长按列表可进入拆词」 (`string/clipboard_history_word_seg_tip`),
the view it opens, titled 「滑动选词」 (`word_segmentation_title`), and its
one action, 「添加到常用语」 (`word_segmentation_add_to_common_phrase`),
with a colour set that separates a clickable label from an un-clickable one
and gives that action a disabled face. This port had nothing behind a long
press on a clipboard card.

`HanWordSegmenter` takes the longest word the tables carry at each
position (they hold up to eight characters, 依法追究刑事责任 being the
longest row of `pinyin_phrases.tsv`); `TextWordIndex` is their text side,
built once on the panel's own thread. `ClipboardPanelController` opens the
split on a long press, selects a run by sliding across the words — the run
stays when the finger lifts — and files it into 常用语
(`QuickPhraseRepository`), disabled until something is selected.
`WordFlowLayout` is the wrapped list the words are laid out in.

Co-Authored-By: Claude Code <noreply@anthropic.com>
The reference ships a poetry grammar (sys_poem_gram, type 11010) whose
lines are keyed by the pinyin they are typed with, and its own settings
text names poetry among what 联想 recommends (关闭联想后,将不会推荐诗词、
计算结果、emoji表情。). Generate poem/lines.tsv from the decoded table and
offer a line as a candidate once the composition spells three of its
syllables, behind the engine's own candidates; a line spelled out whole
brings the line that follows it in its own poem. The layer follows the
智能联想 switch the reference groups it under.

Three syllables is the measured floor: of the 3,000 most frequent pinyin
keys, 2 syllables match 380 (12.7%) and 3 match 3 (0.1%).

Co-Authored-By: Claude Code <noreply@anthropic.com>
Policies and routing for the 快捷格式输入 batch, whose settings rows and
service wiring follow in a separate commit (another session is editing the
same files).

数字、英文前后加空格: LetterDigitSpacePolicy (core) and a letterDigitSpace
hook in InputConnectionGateway.commitText, the single funnel every commit
goes through. The reference's own title and subtitle are the rule
(string/letter_digit_space_list_item_title/subtitle, res/xq.xml): a space
between Chinese and digits/English, on typing and on voice alike.

双击空格键输入句号: DoubleSpacePeriodPolicy keeps the reference's 200 ms
window (InputViewRoot$z.invoke, classes.dex code_off 0x4c1764, const-wide/16
0xc8) and its 31-character punctuation gate.

算式出结果: ArithmeticExpressionPolicy evaluates the expression that ends at
the cursor. The reference computes it natively (libkeyboard.so
InputModel::Impl::AssociateCalculator 0x26523c, GetCalculatorCandidate
0x255ca4, CommitCalculatorCand 0x264594) and its 智能联想 text lists
计算结果 among what that switch offers (string/associate_close_content).

使用耳机麦克风录音: VoiceInputRoutePolicy decides between a headset and the
phone's own microphone, as the reference's enable_bluetooth_input does
(string/enable_bluetooth_input_show, res/cU.xml defaultValue=false);
VoiceAudioRouteManager and LocalAudioVoiceBackend read the switch per
session.

Unit tests for all four policies and the gateway spacing, plus an
instrumented test for the service-side double space.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Second half of the batch, carrying both lanes' wiring: the settings rows for
数字、英文前后加空格 / 首字母自动大写 / 双击空格键输入句号 (the reference's
own 快捷格式输入 page, res/xq.xml), 候选栏展示 and 使用耳机麦克风录音, plus
首字母自动大写 and 按键音量 from the other session working this tree.

首字母自动大写 reads the text before the cursor at every text change and
raises Shift at a sentence start; it rides the reference's switch
(enable_first_letter_capitalization, defaultValue=false) and the Shift key
now announces the reference's own guide text (string/caps_lock_guide_text).

按键音量 scales the bundled click samples and the system click with the
reference's key_sound_volume (0-100, default 50).

算式出结果 and 候选栏展示 fill the row between words: an expression that ends
at the cursor offers its result, and with 候选栏展示 on the clipboard's
content is offered as a candidate that writes what was copied.
Candidate-bar clipboard display ships off even though the reference defaults
it on (enable_clipboard_candidate_bar_show, defaultValue=true), because this
batch's personalisation features are off until switched on; the divergence is
recorded in the changelog and the decision log.

使用耳机麦克风录音 routes voice input to a headset when one is worn and back
to the phone's own microphone when it is not, per the reference's own
subtitle 「默认使用手机录音。佩戴耳机时,使用耳机麦克风」.

User-data export and import carry all six new settings. The
FuzzyRulesInstrumentedTest expectation now matches the shipped fuzzy defaults
(z=zh among them, fcae0dd) instead of the pre-fcae0dd behaviour.

Co-Authored-By: Claude Code <noreply@anthropic.com>
The reference enumerates the whole open digit tail on every key press and
cuts nothing. Its left-list builder (libshell.so 0x5ba1e0) pushes one
string per marked syllable id, then the letters of the open head key --
each with its `+0x20` twin (0x5ba6c4) -- and the leading digit item,
reserving only (marked + 8) at 0x5ba430; decoding syllable.dat end to end
gives 64 -> 10 items, 9426 -> 18, 5426 -> 15. Fuzzy widens the lattice at
the digit level rather than selecting a lookup: every data3 record carries
the smallest rule mask that reaches its spelling and pass2 (0x5b962c)
admits it while `mask` is a subset of the reader's fuzzy set, so node 64
carries `li` (0x0008), 426 carries `fang` (0x0050), and 9426 runs from 19
readings to 32 with every rule on.

This port listed syllables only, offered letters for a single key press
only, cut the rail at 12 and never used fuzzy while decoding, so those
readings could not appear at all. The rail now lists the head key's
syllable-capable letters for a tail of any length (i/u/v stay out as dead
ends), the rail bound goes 12 -> 48, the reading bound 12 -> 24 and the
librime reading count 6 -> 12; the decoder files one fuzzy-variant entry
per syllable under its plain spelling's digits carrying the rules it
needs, admitted only while all of them are on, while the rail keeps plain
spellings the way the reference's left builder does (mask == 0, 0x5ba378).

ENGINE-SPEC.md:222 records the 0x6c0508 family as a nine-key fuzzy path;
the disassembly places it in the wubi engine, so that line is recorded as
wrong and not followed.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Keep the input improvements while restoring the five-key bottom row, equal letter widths, nine-key zero and segmentation keys, stroke grid, numeric arrangement, and uniform key gaps. Restore the numeric return key to its original function-key color and cover the layouts and phone symbols.

Co-Authored-By: Claude Code <noreply@anthropic.com>
…fficial model packages

The keyboard and the app pages now share one accent, the launcher grids stay
centred at any window width, and the toolbar keeps our own key order. The
voice-model page lists the official download addresses, and its own import
takes the tar.bz2 packages those addresses serve.

- Unify the accent (light #1668D0, dark #2D6FD6) across the keyboard palette
  and setup_primary, with white on the accent, and give the toolbar icons the
  weight they need to read on the strip. A test pins the two together.
- Centre the tools and keyboard-switcher grids: the reference's 88.9% width
  was computed but never applied, so the rows sat left of the window centre.
  The rows also follow the window when it changes width.
- Keep our own toolbar order (切换键盘, 剪贴板, 表情, 文本编辑, 撤销, 更多),
  in the reference's scrolling strip. docs/DECISIONS.md records that this is
  deliberate against the reference's own row.
- Refresh the SMS code chip from a ContentObserver on Telephony.Sms.CONTENT_URI
  with a 200 ms debounce, so a code that arrives while the keyboard is open
  shows up; unregister it when the chip is off, the field is a password or the
  view detaches. SmsCodeExtractor also reads the layouts real messages use.
- Import the official sherpa-onnx tar.bz2 model packages as they arrive, with
  commons-compress for bzip2 (java.util.zip has zip and gzip only), the
  application/x-bzip2 MIME type in the picker, and the archive's own traversal
  and size checks kept.
- List the official model addresses on the voice-model page with copy buttons,
  and centre that page's content and cards.
- Hide the handwriting entry (HandwritingFeaturePolicy.ENTRY_OFFERED): the pad
  is still rough. The provider, the pad and the strokes table stay in the build.
- Name the release assets openIME-llm-v<VERSION>.apk (the offline voice models)
  and openIME-v<VERSION>.apk (the smaller package), and compare the draft's
  asset names against those two instead of counting them, so a tag run with
  both APKs attached can publish.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Fold [Unreleased] into the 1.0.0 section and set VERSION. No code changes.

Co-Authored-By: Claude Code <noreply@anthropic.com>
# Conflicts:
#	.github/workflows/release.yml
`theDefaultModeWouldLeaveTheCardWithNoTouchableArea` insets a Rect on all four
sides. That overload is hidden in the platform source
(`frameworks/base/graphics/java/android/graphics/Rect.java`, the javadoc block
above `public void inset(int left, int top, int right, int bottom)` carries
`@hide` at android-10.0.0_r1), so the API 29 compatibility job dies with

    NoSuchMethodError: No virtual method inset(IIII)V in class android.graphics.Rect

API 26 predates hidden-API enforcement, which is why that job stayed green and
API 29 did not. The test now moves the edge it cares about, which is what the
four-argument call did here.

Co-Authored-By: Claude Code <noreply@anthropic.com>
View.onTouchEvent posts the click on ACTION_UP, and detaching the view cancels
that post. The pending-rebuild poll could be due ahead of the post, so a tap on
Space during a layout change was swallowed. The poll now yields one turn after
it sees the finger lift. The matching test also skips when the main thread ran
past the long-press deadline, where the press is a voice hold by design.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Base automatically changed from align/letter-hints to main October 15, 2026 23:01
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