diff --git a/RUST.md b/RUST.md index cbdf3eb..33d4a20 100644 --- a/RUST.md +++ b/RUST.md @@ -542,14 +542,103 @@ accepted. composition, autocorrect and suggestions are exactly what the composer in this app needs and exactly what `winit` cannot do at all. - *One crash seen once and not reproduced.* With an accessibility - client attached the app aborted, stack: + **Cause found 2026-09-04, and it is Masonry's, not android-view's.** + `~/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` → `accesskit_android::event::QueuedEvents::raise` → `send_completed_event` → `unwrap()` on `Err(JavaException)`. android-view builds `panic = "abort"`, so a JNI call that throws 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 given `sleep N; emu down` watchdogs, and one armed for an earlier