RUST.md: the Masonry IME gap is a TODO, and the accesskit abort is reproducible
Two things E1 left open, both settled on the emulator. The missing autocorrect is Masonry's as_input_connection returning None, not android-view and not EditorInfo: android-view's own demo implements the trait over a parley editor and Gboard suggests from that buffer. The abort seen once is a client *detaching*: accesskit_android's adapter never returns to Inactive, so the first tree change after a ui-trace run sends an accessibility event with accessibility off, which throws. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
43743ba171
commit
c70a670356
1 file changed
+92
-3
@@ -542,14 +542,103 @@ accepted.
|
|||||||
composition, autocorrect and suggestions are exactly what the
|
composition, autocorrect and suggestions are exactly what the
|
||||||
composer in this app needs and exactly what `winit` cannot do at all.
|
composer in this app needs and exactly what `winit` cannot do at all.
|
||||||
|
|
||||||
*One crash seen once and not reproduced.* With an accessibility
|
**Cause found 2026-09-04, and it is Masonry's, not android-view's.**
|
||||||
client attached the app aborted, stack:
|
`~/src/android-view/masonry/src/lib.rs:531` is
|
||||||
|
|
||||||
|
fn as_input_connection(&mut self) -> Option<&mut dyn InputConnection> {
|
||||||
|
// TODO
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
so the Masonry demo has **no `InputConnection` at all**; `RustView`
|
||||||
|
returns null from `onCreateInputConnection` and the IME falls back to
|
||||||
|
dispatching raw key events, which is exactly the behaviour observed —
|
||||||
|
keys arrive, composition does not exist, so there is nothing for
|
||||||
|
Gboard to suggest against. It is not a wrong `EditorInfo`, and the
|
||||||
|
guess above that it was is withdrawn.
|
||||||
|
|
||||||
|
android-view's **own** demo (`demo/src/lib.rs`, packaged by `app/`)
|
||||||
|
implements the whole trait against a parley editor and asks for
|
||||||
|
`INPUT_TYPE_CLASS_TEXT | CAP_SENTENCES | AUTO_CORRECT | MULTI_LINE`
|
||||||
|
with `IME_FLAG_NO_FULLSCREEN | NO_EXTRACT_UI | NO_ENTER_ACTION`
|
||||||
|
(`demo/src/lib.rs:588`). So the capability is present in the layer
|
||||||
|
iris would sit on, and the 30-odd method `InputConnection` trait in
|
||||||
|
`src/ime.rs` — `set_composing_text`, `set_composing_region`,
|
||||||
|
`finish_composing_text`, `text_before_cursor`, `cursor_caps_mode`,
|
||||||
|
`request_cursor_updates`, and `InputMethodManager::update_selection`
|
||||||
|
to push the selection back — is the full surface an IME needs.
|
||||||
|
**This changes what I2 costs**: the IME bridge is a trait to
|
||||||
|
implement over iris's parley editor, not a gap to fund upstream. It
|
||||||
|
also means E2 inherits Masonry's TODO, so a Masonry transcript will
|
||||||
|
have the same dead composer until somebody fills that in.
|
||||||
|
|
||||||
|
**Measured on the emulator, same session, same device.** Built
|
||||||
|
android-view's own demo — `cargo ndk -t x86_64 -P 26 -o
|
||||||
|
app/src/main/jniLibs/ build -p android-view-demo --release`, then
|
||||||
|
`./gradlew :app:assembleDebug`, installed with `ANDROID_SERIAL=$(emu
|
||||||
|
serial)` — and tapped into its editor. `dumpsys input_method` reports
|
||||||
|
`mInputShown=true` with `mServedView=…viewdemo.DemoView`, and the
|
||||||
|
screenshot shows **Gboard's suggestion strip populated with "dolor |
|
||||||
|
Dolores | door"**: the caret had landed inside the word *dolor* in
|
||||||
|
the demo's lorem ipsum, and Gboard read that word out of the Rust
|
||||||
|
editor through `text_before_cursor`. So on this emulator, through
|
||||||
|
android-view, a parley editor gets a real IME with real suggestions
|
||||||
|
drawn from its own buffer. That is the bar E1 could not reach and the
|
||||||
|
bar I2 is written against, and it is now known to be reachable.
|
||||||
|
|
||||||
|
*One crash seen once — reproduced and diagnosed 2026-09-04.* With an
|
||||||
|
accessibility client attached the app aborted, stack:
|
||||||
`android_view::view::do_frame` → `CallbackCtx::finish` →
|
`android_view::view::do_frame` → `CallbackCtx::finish` →
|
||||||
`accesskit_android::event::QueuedEvents::raise` →
|
`accesskit_android::event::QueuedEvents::raise` →
|
||||||
`send_completed_event` → `unwrap()` on `Err(JavaException)`.
|
`send_completed_event` → `unwrap()` on `Err(JavaException)`.
|
||||||
android-view builds `panic = "abort"`, so a JNI call that throws
|
android-view builds `panic = "abort"`, so a JNI call that throws
|
||||||
takes the process. Two later `ui-trace record` runs left the app
|
takes the process. Two later `ui-trace record` runs left the app
|
||||||
alive, so the trigger is narrower than "a client is attached".
|
alive, so the trigger looked narrower than "a client is attached".
|
||||||
|
|
||||||
|
**It is the opposite of "a client is attached": it is a client
|
||||||
|
having *detached*.** `accesskit_android`'s `State` enum
|
||||||
|
(`adapter.rs:161` in 0.4.0, `:192` in 0.8.0) is
|
||||||
|
`Inactive | Placeholder | Active`, and **nothing ever moves it back
|
||||||
|
to `Inactive`**. A client — `ui-trace`, which is uiautomator — calls
|
||||||
|
into the node provider once, `get_or_init_tree` promotes the adapter
|
||||||
|
to `Active`, and it stays there for the life of the process. Every
|
||||||
|
later change then returns `Some(QueuedEvents)`, `raise` calls
|
||||||
|
`ViewParent.requestSendAccessibilityEvent`, and that reaches
|
||||||
|
`AccessibilityManager.sendAccessibilityEvent`, which on the main
|
||||||
|
looper **throws `IllegalStateException("Accessibility off. Did you
|
||||||
|
forget to check that?")` when accessibility is disabled**. jni-rs
|
||||||
|
returns `Err(JavaException)`, `send_completed_event` unwraps it, and
|
||||||
|
`panic = "abort"` ends the process.
|
||||||
|
|
||||||
|
*The controlled run*, one process (pid 4085), `settings get secure
|
||||||
|
accessibility_enabled` = 0 throughout:
|
||||||
|
|
||||||
|
- tapped the editor and typed three keys with `adb shell input tap`,
|
||||||
|
no client ever attached — **alive**;
|
||||||
|
- one `ui-trace record -d 800` with no gesture at all, then two
|
||||||
|
seconds' wait — **still alive** (the queue was raised while the
|
||||||
|
client was still there);
|
||||||
|
- the very next three keystrokes, same process — **aborted**, same
|
||||||
|
stack.
|
||||||
|
|
||||||
|
So the failure is not the recording; it is the **first thing that
|
||||||
|
changes the accessibility tree after a recording ends**. That makes
|
||||||
|
it a standing hazard for this project rather than an oddity:
|
||||||
|
`transcript-bench.sh`, `stream-bench.sh` and `bench-lib.sh`'s
|
||||||
|
tap-by-name all attach and detach uiautomator, so on a Rust app the
|
||||||
|
typing or scrolling *after* a bench run is what dies, several
|
||||||
|
seconds away from anything that looks like a cause.
|
||||||
|
|
||||||
|
**Still present at head**: 0.8.0 is the newest `accesskit_android`
|
||||||
|
(the demo resolves 0.4.0) and both the unconditional `unwrap` in
|
||||||
|
`send_completed_event` and the one-way `State` are unchanged there,
|
||||||
|
so upgrading is not the fix. **Our mitigation for I2/I4 is a gate we
|
||||||
|
own**: ask `AccessibilityManager.isEnabled()` before calling
|
||||||
|
`raise`, and drop the events when it says no. Worth reporting
|
||||||
|
upstream as well — the honest fix is for `raise` to clear a pending
|
||||||
|
exception rather than unwrap it, since a view can be detached or
|
||||||
|
accessibility switched off between queueing and raising no matter
|
||||||
|
who is calling.
|
||||||
|
|
||||||
*A rig trap that cost a wrong conclusion.* Several bounded runs were
|
*A rig trap that cost a wrong conclusion.* Several bounded runs were
|
||||||
given `sleep N; emu down` watchdogs, and one armed for an earlier
|
given `sleep N; emu down` watchdogs, and one armed for an earlier
|
||||||
|
|||||||
Reference in new issue
Block a user