From 0b587629e6c113176a07da9ee3e17438cfc25503 Mon Sep 17 00:00:00 2001 From: iris <2+iris@noreply.localhost> Date: Sun, 6 Sep 2026 00:39:14 -0400 Subject: [PATCH] iris/android-app bench: auto-capture diagnostics when the keyboard opens So Iris can get a report off the phone even if the keyboard wipe (or some other keyboard-triggered regression) is still present on whatever build she is holding, independent of whether the on-screen Diagnostics button itself is drawing. on_insets_changed edge-triggers on ime_bottom becoming non-zero, waits KEYBOARD_DIAGNOSTICS_DELAY_MS (500ms, long enough for the resize and a couple of frames to settle) via a spawned task, then capture_keyboard_diagnostics reuses show_diagnostics's exact report text, logs it, copies it to the clipboard unprompted, and shows it through a new PlatformHandle::show_diagnostics_overlay call into IrisView.showDiagnosticsOverlay -- a plain TextView + Copy/Close panel added over the existing IrisView (not replacing it, unlike showRendererError's one-way trip) so it draws independently of whatever iris's own renderer is doing, and Close returns to the still-running session underneath. Co-Authored-By: Claude Fable 5.1 --- .../java/dev/iris/android/demo/IrisView.java | 84 +++++++++++++++++++ iris/android-app/src/bench_client.rs | 72 ++++++++++++++++ iris/android-app/src/bench_jni.rs | 27 ++++++ 3 files changed, 183 insertions(+) diff --git a/iris/android-app/app/src/main/java/dev/iris/android/demo/IrisView.java b/iris/android-app/app/src/main/java/dev/iris/android/demo/IrisView.java index 9516e43..ef53bfc 100644 --- a/iris/android-app/app/src/main/java/dev/iris/android/demo/IrisView.java +++ b/iris/android-app/app/src/main/java/dev/iris/android/demo/IrisView.java @@ -1,8 +1,15 @@ package dev.iris.android.demo; import android.app.Activity; +import android.content.ClipData; +import android.content.ClipboardManager; import android.content.Context; import android.view.Gravity; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.FrameLayout; +import android.widget.LinearLayout; import android.widget.ScrollView; import android.widget.TextView; @@ -68,4 +75,81 @@ public final class IrisView extends RustView { scroll.addView(text); activity.setContentView(scroll); } + + private static final String DIAGNOSTICS_OVERLAY_TAG = "iris-diagnostics-overlay"; + + /** + * The bench build's keyboard diagnostics capture + * (`bench_client.rs`'s `on_insets_changed` / + * `capture_keyboard_diagnostics`, via `bench_jni.rs`'s + * `PlatformHandle::show_diagnostics_overlay`): unlike + * `showRendererError` above, this adds a panel *over* this view + * (`MainActivity`'s `FrameLayout` still holds `IrisView` underneath, + * running) rather than replacing the activity's content, and gives it + * a Copy button and a Close that removes the panel -- so it draws + * (and can be read) whether or not iris itself is still putting + * anything on screen, without abandoning the session that produced + * it. Runs on the UI thread regardless of which thread calls it, + * since the call comes from a background task (a delayed capture + * after the keyboard opens), and touching the view tree off the UI + * thread is undefined. + */ + void showDiagnosticsOverlay(String report) { + Context context = getContext(); + if (!(context instanceof Activity)) { + return; + } + Activity activity = (Activity) context; + activity.runOnUiThread(() -> { + ViewGroup parent = (ViewGroup) getParent(); + if (parent == null) { + return; + } + View existing = parent.findViewWithTag(DIAGNOSTICS_OVERLAY_TAG); + if (existing != null) { + parent.removeView(existing); + } + + float density = activity.getResources().getDisplayMetrics().density; + int pad = (int) (16 * density); + + LinearLayout overlay = new LinearLayout(activity); + overlay.setTag(DIAGNOSTICS_OVERLAY_TAG); + overlay.setOrientation(LinearLayout.VERTICAL); + overlay.setBackgroundColor(0xEE000000); + overlay.setPadding(pad, pad, pad, pad); + + TextView text = new TextView(activity); + text.setText(report); + text.setTextIsSelectable(true); + text.setTextColor(0xFFFFFFFF); + ScrollView scroll = new ScrollView(activity); + scroll.addView(text); + overlay.addView(scroll, new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, 0, 1f)); + + LinearLayout buttonRow = new LinearLayout(activity); + buttonRow.setOrientation(LinearLayout.HORIZONTAL); + buttonRow.setPadding(0, pad, 0, 0); + + Button copy = new Button(activity); + copy.setText("Copy"); + copy.setOnClickListener(v -> { + ClipboardManager clipboard = + (ClipboardManager) activity.getSystemService(Context.CLIPBOARD_SERVICE); + if (clipboard != null) { + clipboard.setPrimaryClip(ClipData.newPlainText("iris diagnostics", report)); + } + }); + Button close = new Button(activity); + close.setText("Close"); + close.setOnClickListener(v -> parent.removeView(overlay)); + buttonRow.addView(copy); + buttonRow.addView(close); + overlay.addView(buttonRow); + + parent.addView(overlay, new FrameLayout.LayoutParams( + FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT)); + }); + } } diff --git a/iris/android-app/src/bench_client.rs b/iris/android-app/src/bench_client.rs index beb14ca..b35da88 100644 --- a/iris/android-app/src/bench_client.rs +++ b/iris/android-app/src/bench_client.rs @@ -74,6 +74,13 @@ pub struct BenchClient { platform: Option>, last_report: Option, running: bool, + /// Edge-triggers the keyboard diagnostics capture below -- set on the + /// first `on_insets_changed` where `ime_bottom > 0.0`, cleared on the + /// first where it is not, so opening the keyboard fires this once + /// rather than on every insets update while it stays open (a rotation + /// or a status-bar change with the keyboard already up would otherwise + /// re-fire it). + keyboard_was_visible: bool, } impl HasAndroidUiState for BenchClient { @@ -219,6 +226,7 @@ impl AndroidAppState for BenchClient { platform: None, last_report: None, running: false, + keyboard_was_visible: false, }; let (backlog, stream_tail) = parse_fixture(); @@ -246,6 +254,16 @@ impl AndroidAppState for BenchClient { /// Pads the top button row by the status-bar inset -- see `top_bar`'s /// field comment. Rebuilds the row rather than mutating a stored /// `Padding` in place, since nothing here holds a handle to one. + /// + /// **Also the trigger for the keyboard diagnostics capture** (RUST.md's + /// P0 box): the IME resizing the surface is exactly the case the + /// previous commit found wiped text, and Iris needs a way to get a + /// report off the phone even if that (or some other keyboard-triggered + /// regression) is still happening on the build she is holding -- + /// `capture_keyboard_diagnostics` below fires ~500ms after the + /// keyboard becomes visible, once per keyboard opening, and shows its + /// report in a plain overlay view that draws independently of + /// whatever iris itself is doing. fn on_insets_changed( &mut self, rsc: &mut AndroidRsc, @@ -253,9 +271,33 @@ impl AndroidAppState for BenchClient { ) { let controls = bench_controls(rsc, insets.top); (self.top_bar)(rsc).set(controls); + + let ime_visible = insets.ime_bottom > 0.0; + if ime_visible && !self.keyboard_was_visible { + self.keyboard_was_visible = true; + let redraw = rsc.tasks.redraw_handle(); + rsc.spawn_task(async move |mut ctx| { + tokio::time::sleep(Duration::from_millis(KEYBOARD_DIAGNOSTICS_DELAY_MS)).await; + ctx.update(|state: &mut BenchClient, rsc| { + state.capture_keyboard_diagnostics(rsc); + }); + redraw.request_redraw(); + }); + } else if !ime_visible { + self.keyboard_was_visible = false; + } } } +/// How long to wait after the keyboard becomes visible before capturing +/// diagnostics -- long enough that the resize, the reported wipe (if it is +/// still happening) and a couple of frames have all had time to land, per +/// AGENTS.md's "so that operations that finish in milliseconds have states +/// on the way that nothing can observe" reasoning applied the other way: +/// this wants to observe the state *after* the transition settles, not +/// mid-flight. +const KEYBOARD_DIAGNOSTICS_DELAY_MS: u64 = 500; + type Rsc = AndroidRsc; /// The header row's own backdrop -- see `bench_controls`'s doc comment on @@ -378,6 +420,36 @@ impl BenchClient { self.last_report = Some(report); } + /// The keyboard's own diagnostics capture -- see `on_insets_changed`'s + /// doc comment. Reuses `show_diagnostics`'s exact report (so it is the + /// same text the on-screen `Diagnostics` button produces, plus the + /// per-frame log `FrameReport` already keeps around the resize -- + /// `frame_report.report()` above covers "the frames around the + /// resize" without a second accounting mechanism), then does three + /// things the button does not: logs it (so a `logcat` pull gets it + /// even if nothing on screen does), copies it to the clipboard + /// unprompted, and shows it in the shell's plain overlay view, which + /// draws independently of iris's own renderer -- the whole point, + /// since the renderer is exactly what might be in the wiped state + /// this exists to report on. + fn capture_keyboard_diagnostics(&mut self, rsc: &mut Rsc) { + self.show_diagnostics(rsc); + let Some(report) = self.last_report.clone() else { + return; + }; + log::info!("iris keyboard diagnostics:\n{report}"); + let Some(platform) = &self.platform else { + log::info!("iris keyboard diagnostics: no platform handle, can't reach the shell"); + return; + }; + if platform.copy_to_clipboard("iris keyboard diagnostics", &report) { + log::info!("iris keyboard diagnostics: copied to clipboard"); + } else { + log::info!("iris keyboard diagnostics: clipboard copy failed"); + } + platform.show_diagnostics_overlay(&report); + } + fn copy_report(&mut self) { let Some(report) = &self.last_report else { log::info!("iris bench report: nothing to copy -- run the benchmark first"); diff --git a/iris/android-app/src/bench_jni.rs b/iris/android-app/src/bench_jni.rs index 0a5ae4b..b05312e 100644 --- a/iris/android-app/src/bench_jni.rs +++ b/iris/android-app/src/bench_jni.rs @@ -131,4 +131,31 @@ impl PlatformHandle { .ok()?; Some(()) } + + /// Shows `report` in the shell's plain-view diagnostics overlay + /// (`IrisView.showDiagnosticsOverlay`) -- a real `TextView` plus Copy + /// and Close controls, added over whatever iris itself is drawing + /// rather than replacing it (unlike `android::view::show_renderer_error`, + /// which exists for the case the renderer can never recover from and + /// intentionally never returns). Called from a background task after + /// the keyboard-open delay (`bench_client.rs`'s `on_insets_changed`), + /// so the Java side hops onto the UI thread itself before touching the + /// view tree -- see that method's own comment. + pub fn show_diagnostics_overlay(&self, report: &str) -> bool { + self.try_show_diagnostics_overlay(report).is_some() + } + + fn try_show_diagnostics_overlay(&self, report: &str) -> Option<()> { + let mut guard = self.vm.attach_current_thread().ok()?; + let env: &mut JNIEnv = &mut guard; + let jreport = env.new_string(report).ok()?; + env.call_method( + self.view.as_obj(), + "showDiagnosticsOverlay", + "(Ljava/lang/String;)V", + &[JValue::Object(jreport.as_ref())], + ) + .ok()?; + Some(()) + } }