iris is the framework alone; the app is one crate in app-rust/
Iris: "the organization of the rust rewrite is a mess right now... there shouldn't be anything related to the app inside of iris. Iris is supposed to be the UI framework alone." And, on the crate count: "I'm confused why the app only code needs more than one crate though." Nine cargo workspaces become three, and the port's project code -- which sat in five places, four of them inside the framework -- becomes one crate, `ai-app`, in `app-rust/`: client-core -> app-rust/src/client iris/transcript-ui -> app-rust/src/ui iris/transcript-fixture -> app-rust/src/ui/fixture.rs + tests/ + touch/ iris/desktop-app -> app-rust/src/desktop + src/bin_desktop.rs iris/android-app -> app-rust/src/android + android-project/ android-shell -> app-rust/src/shell iris/ keeps core, macro, the iris crate, tabs-ui and rig-input, and now mentions no session, transcript, setup or server anywhere. Only two of the old splits had a reason that survived reading. event-model stays a crate at the repo root because server/ depends on it too, so a crate is what makes the backend and the app agree by construction. The two Android .so names looked like a hard constraint -- a package produces one library artifact -- until P2 turned out to already plan merging those two Android apps into one; both faces now come out of libai_app.so, picked apart by features so `--no-default-features --features shell` keeps wgpu, parley and iris out of the Compose app's APK. docs/RUST.md's "One app crate" has the rest, including what each remaining feature is for. DECISIONS.md and SUBAGENTS.md move into docs/ with everything else. Verified: ./run-tests.sh and `cd iris && cargo test` green, clippy and fmt clean in all five workspaces, `cargo ndk -t x86_64` links libai_app.so, build-apk.sh produces an APK that installs and launches on this checkout's emulator (Gl ... virgl, as expected), and the phone-sized headless screenshot renders the transcript unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
e9a6562dc6
commit
6d5a231f5c
100 files changed
+924
-3295
No files matched your search
@@ -0,0 +1,252 @@
|
||||
//! JNI calls the `bench` feature needs that go through the shell's own
|
||||
//! Java side rather than anything `iris`/`android-view` already wraps:
|
||||
//! `BatteryManager.getIntProperty(BATTERY_PROPERTY_CURRENT_NOW)` for the
|
||||
//! per-second battery sample, `ClipboardManager.setPrimaryClip` for the
|
||||
//! "Copy report" control (P0's iris half, docs/RUST.md), and -- added for
|
||||
//! RUST.md's "Benchmark v2" -- `Display.getRefreshRate()` for the phase
|
||||
//! report's real late-frame budget and `InputMethodManager.
|
||||
//! showSoftInput`/`hideSoftInputFromWindow` for the keyboard phase. None
|
||||
//! of these are part of `android_view::context`'s own `Context`/
|
||||
//! `Resources` wrappers (that file's own `// TODO: more methods?`), so
|
||||
//! this calls them directly rather than growing that crate's wrapper for
|
||||
//! calls this crate alone needs.
|
||||
//!
|
||||
//! Holds its own `JavaVM` + `GlobalRef` to the view (handed in through
|
||||
//! [`iris::android::AndroidAppState::platform_ready`]) so it can attach
|
||||
//! whichever thread calls it -- the battery sampler runs on a background
|
||||
//! tokio task, not the UI thread the rest of `IrisViewPeer`'s JNI calls
|
||||
//! run on. `JavaVM::attach_current_thread` is safe to call from a thread
|
||||
//! already attached (the `jni` crate detects it and does not double
|
||||
//! attach), so no caller here needs to know or care which thread it is.
|
||||
|
||||
use android_view::jni::{
|
||||
JNIEnv, JavaVM,
|
||||
objects::{GlobalRef, JObject, JValue},
|
||||
};
|
||||
|
||||
/// `android.os.BatteryManager.BATTERY_PROPERTY_CURRENT_NOW` -- not exposed
|
||||
/// as a constant anywhere reachable without the Android SDK jar, so named
|
||||
/// here with its source rather than left as a bare `2`.
|
||||
const BATTERY_PROPERTY_CURRENT_NOW: i32 = 2;
|
||||
|
||||
pub struct PlatformHandle {
|
||||
vm: JavaVM,
|
||||
view: GlobalRef,
|
||||
}
|
||||
|
||||
impl PlatformHandle {
|
||||
pub fn new(vm: JavaVM, view: GlobalRef) -> Self {
|
||||
Self { vm, view }
|
||||
}
|
||||
|
||||
fn context<'e>(&self, env: &mut JNIEnv<'e>) -> Option<JObject<'e>> {
|
||||
env.call_method(
|
||||
self.view.as_obj(),
|
||||
"getContext",
|
||||
"()Landroid/content/Context;",
|
||||
&[],
|
||||
)
|
||||
.ok()?
|
||||
.l()
|
||||
.ok()
|
||||
}
|
||||
|
||||
fn system_service<'e>(
|
||||
&self,
|
||||
env: &mut JNIEnv<'e>,
|
||||
context: &JObject<'e>,
|
||||
name: &str,
|
||||
) -> Option<JObject<'e>> {
|
||||
let jname = env.new_string(name).ok()?;
|
||||
env.call_method(
|
||||
context,
|
||||
"getSystemService",
|
||||
"(Ljava/lang/String;)Ljava/lang/Object;",
|
||||
&[JValue::Object(jname.as_ref())],
|
||||
)
|
||||
.ok()?
|
||||
.l()
|
||||
.ok()
|
||||
}
|
||||
|
||||
/// One sample of `BATTERY_PROPERTY_CURRENT_NOW`, in microamps. `None`
|
||||
/// on any JNI failure, on a device with no `BatteryManager` service,
|
||||
/// or when the platform itself answers "not supported" -- `0` or
|
||||
/// `Integer.MIN_VALUE` are both documented SDK answers for that, and
|
||||
/// both would read as a real (and wrong) measurement if folded into an
|
||||
/// average rather than named apart. UI_RULES.md: never present an
|
||||
/// inferred value as a measured one.
|
||||
pub fn battery_current_ua(&self) -> Option<i32> {
|
||||
let mut guard = self.vm.attach_current_thread().ok()?;
|
||||
let env: &mut JNIEnv = &mut guard;
|
||||
let context = self.context(env)?;
|
||||
let battery_manager = self.system_service(env, &context, "batterymanager")?;
|
||||
let value = env
|
||||
.call_method(
|
||||
&battery_manager,
|
||||
"getIntProperty",
|
||||
"(I)I",
|
||||
&[JValue::Int(BATTERY_PROPERTY_CURRENT_NOW)],
|
||||
)
|
||||
.ok()?
|
||||
.i()
|
||||
.ok()?;
|
||||
if value == 0 || value == i32::MIN {
|
||||
None
|
||||
} else {
|
||||
Some(value)
|
||||
}
|
||||
}
|
||||
|
||||
/// Puts `text` on the system clipboard through `ClipboardManager` --
|
||||
/// `true` only if the whole JNI chain (service lookup, `ClipData`,
|
||||
/// `setPrimaryClip`) succeeded.
|
||||
pub fn copy_to_clipboard(&self, label: &str, text: &str) -> bool {
|
||||
self.try_copy_to_clipboard(label, text).is_some()
|
||||
}
|
||||
|
||||
fn try_copy_to_clipboard(&self, label: &str, text: &str) -> Option<()> {
|
||||
let mut guard = self.vm.attach_current_thread().ok()?;
|
||||
let env: &mut JNIEnv = &mut guard;
|
||||
let context = self.context(env)?;
|
||||
let clipboard = self.system_service(env, &context, "clipboard")?;
|
||||
let jlabel = env.new_string(label).ok()?;
|
||||
let jtext = env.new_string(text).ok()?;
|
||||
let clip = env
|
||||
.call_static_method(
|
||||
"android/content/ClipData",
|
||||
"newPlainText",
|
||||
"(Ljava/lang/CharSequence;Ljava/lang/CharSequence;)Landroid/content/ClipData;",
|
||||
&[
|
||||
JValue::Object(jlabel.as_ref()),
|
||||
JValue::Object(jtext.as_ref()),
|
||||
],
|
||||
)
|
||||
.ok()?
|
||||
.l()
|
||||
.ok()?;
|
||||
env.call_method(
|
||||
&clipboard,
|
||||
"setPrimaryClip",
|
||||
"(Landroid/content/ClipData;)V",
|
||||
&[JValue::Object(&clip)],
|
||||
)
|
||||
.ok()?;
|
||||
Some(())
|
||||
}
|
||||
|
||||
/// The display's own refresh rate in Hz (`View::getDisplay()` ->
|
||||
/// `Display::getRefreshRate()`), for RUST.md's "Benchmark v2": late
|
||||
/// frames are judged against *this* device's real budget, not an
|
||||
/// assumed 60Hz -- a 90Hz or 120Hz phone would otherwise call frames
|
||||
/// "late" that met their own faster deadline. `None` if the view is
|
||||
/// not yet attached to a window (`getDisplay` returns `null`) or the
|
||||
/// platform reports a non-positive rate, which is not a real answer
|
||||
/// either.
|
||||
pub fn refresh_rate_hz(&self) -> Option<f32> {
|
||||
let mut guard = self.vm.attach_current_thread().ok()?;
|
||||
let env: &mut JNIEnv = &mut guard;
|
||||
let display = env
|
||||
.call_method(
|
||||
self.view.as_obj(),
|
||||
"getDisplay",
|
||||
"()Landroid/view/Display;",
|
||||
&[],
|
||||
)
|
||||
.ok()?
|
||||
.l()
|
||||
.ok()?;
|
||||
if display.is_null() {
|
||||
return None;
|
||||
}
|
||||
let rate = env
|
||||
.call_method(&display, "getRefreshRate", "()F", &[])
|
||||
.ok()?
|
||||
.f()
|
||||
.ok()?;
|
||||
if rate > 0.0 { Some(rate) } else { None }
|
||||
}
|
||||
|
||||
/// `InputMethodManager.showSoftInput(view, 0)` -- the keyboard phase's
|
||||
/// own show, called directly rather than through the focus-driven
|
||||
/// `pending_show_keyboard` path `android/view.rs` uses for a real tap,
|
||||
/// since RUST.md's "Benchmark v2" spec asks for this "through the
|
||||
/// shell's InputMethodManager" independent of focus state. `true` only
|
||||
/// if the platform itself reports the request succeeded -- whether the
|
||||
/// IME actually became visible is confirmed separately, from
|
||||
/// `on_insets_changed`, per UI_RULES.md ("never present an inferred
|
||||
/// value as a measured one").
|
||||
pub fn show_ime(&self) -> bool {
|
||||
self.try_toggle_ime(true).unwrap_or(false)
|
||||
}
|
||||
|
||||
/// `InputMethodManager.hideSoftInputFromWindow(windowToken, 0)`.
|
||||
pub fn hide_ime(&self) -> bool {
|
||||
self.try_toggle_ime(false).unwrap_or(false)
|
||||
}
|
||||
|
||||
fn try_toggle_ime(&self, show: bool) -> Option<bool> {
|
||||
let mut guard = self.vm.attach_current_thread().ok()?;
|
||||
let env: &mut JNIEnv = &mut guard;
|
||||
let context = self.context(env)?;
|
||||
let imm = self.system_service(env, &context, "input_method")?;
|
||||
if show {
|
||||
env.call_method(
|
||||
&imm,
|
||||
"showSoftInput",
|
||||
"(Landroid/view/View;I)Z",
|
||||
&[JValue::Object(self.view.as_obj()), JValue::Int(0)],
|
||||
)
|
||||
.ok()?
|
||||
.z()
|
||||
.ok()
|
||||
} else {
|
||||
let token = env
|
||||
.call_method(
|
||||
self.view.as_obj(),
|
||||
"getWindowToken",
|
||||
"()Landroid/os/IBinder;",
|
||||
&[],
|
||||
)
|
||||
.ok()?
|
||||
.l()
|
||||
.ok()?;
|
||||
env.call_method(
|
||||
&imm,
|
||||
"hideSoftInputFromWindow",
|
||||
"(Landroid/os/IBinder;I)Z",
|
||||
&[JValue::Object(&token), JValue::Int(0)],
|
||||
)
|
||||
.ok()?
|
||||
.z()
|
||||
.ok()
|
||||
}
|
||||
}
|
||||
|
||||
/// 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(())
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user