use crate::prelude::*; use crate::task::RequestRedraw; use accesskit_android::Adapter as AccessAdapter; use android_view::{ AccessibilityNodeInfo, AccessibilityNodeProvider, Bundle, CallbackCtx, Context, InputConnection, KeyEvent, MotionEvent, Rect, View, ViewPeer, jni::{ JNIEnv, JavaVM, objects::{GlobalRef, JValue}, sys::jint, }, ndk::event::{Axis, Keycode, MotionAction}, }; use std::{cell::RefCell, marker::Sized, rc::Rc, sync::Arc, time::Instant}; use super::{ access::{AndroidAccessSource, NullActionHandler, raise_if_enabled}, insets::{Insets, Shared}, render::{AndroidRedrawHandle, AndroidRenderer}, }; /// Android host state. The renderer follows the `SurfaceView` lifecycle. /// How many frames after each `surface_changed` `render()` logs a full /// diagnostic line for -- see the log site's own comment. const DIAGNOSTIC_FRAMES: u64 = 10; pub struct AndroidUiState { pub root: Option, pub renderer: Option, pub focus: Option>, pub cursor: CursorState, pub last_click: Instant, /// Previous IME preedit length, in characters. pub compose_len: usize, /// Deferred until the input callback regains access to JNI. pub pending_show_keyboard: bool, /// Also deferred until a JNI callback is available. pub pending_open_url: Option, /// Filled by the native insets callback registered in `android/insets.rs`. shared: Rc>, pub access_adapter: AccessAdapter, pub access: AccessTree, pub frame_report: FrameReport, pub content_scale: f32, /// The last insets `render()` saw -- compared each frame so /// `AndroidAppState::on_insets_changed` fires only when they actually /// change (once at startup for the status bar, again if the device /// rotates), not every frame. last_insets: Insets, } impl AndroidUiState { fn new(shared: Rc>, content_scale: f32) -> Self { Self { root: None, renderer: None, focus: None, cursor: Default::default(), last_click: Instant::now(), compose_len: 0, pending_show_keyboard: false, pending_open_url: None, shared, access_adapter: Default::default(), access: AccessTree::new(), frame_report: FrameReport::new(), content_scale, last_insets: Insets::default(), } } pub fn insets(&self) -> Insets { self.shared.borrow().insets } pub fn insets_report(&self) -> String { let shared = self.shared.borrow(); let i = shared.insets; if shared.updates == 0 { return "insets: dispatches=0 -- the platform has never called \ onApplyWindowInsets, so nothing below was measured" .to_string(); } format!( "insets: dispatches={} left={} top={} right={} bottom={} ime_bottom={} \ ime_visible={}", shared.updates, i.left, i.top, i.right, i.bottom, i.ime_bottom, i.ime_visible, ) } } impl HasRoot> for AndroidUiState { fn set_root(&mut self, rsc: &mut StdRsc, root: StrongWidget) { self.root = Some(crate::overlay::default_overlay_root(rsc, root)); } } pub trait HasAndroidUiState: Sized + 'static { fn android_state(&self) -> &AndroidUiState; fn android_state_mut(&mut self) -> &mut AndroidUiState; } pub trait AndroidAppState: HasAndroidUiState { fn new(ui_state: AndroidUiState, rsc: &mut StdRsc) -> Self; #[allow(unused_variables)] fn back_pressed(&mut self, rsc: &mut StdRsc) -> bool { false } #[allow(unused_variables)] fn platform_ready(&mut self, rsc: &mut StdRsc, vm: JavaVM, view: GlobalRef) {} #[allow(unused_variables)] fn on_insets_changed(&mut self, rsc: &mut StdRsc, insets: WindowInsets) {} } /// Widget-facing insets in physical pixels, decoupled from JNI's integer shape. #[derive(Clone, Copy, Default, Debug, PartialEq)] pub struct WindowInsets { pub left: f32, pub top: f32, pub right: f32, pub bottom: f32, /// How much of the window the keyboard covers, in physical pixels -- /// what a layout pads by. See `insets::Insets::ime_visible` for why /// "is the keyboard up" is a separate field rather than this one /// compared against zero. pub ime_bottom: f32, pub ime_visible: bool, } impl WindowInsets { fn from_physical(insets: Insets) -> Self { Self { left: insets.left as f32, top: insets.top as f32, right: insets.right as f32, bottom: insets.bottom as f32, ime_bottom: insets.ime_bottom as f32, ime_visible: insets.ime_visible, } } } /// The `ViewPeer` android-view dispatches every callback to. One per /// `RustView` instance; `new_peer` (below) builds it and hands the id to /// Java the same way android-view's own demo does. pub struct IrisViewPeer { pub(super) rsc: StdRsc, pub(super) state: State, task_recv: TaskMsgReceiver>, /// Converts input and Choreographer timestamps onto one monotonic clock. device_clock: Option, } impl IrisViewPeer { fn drain_tasks(&mut self) { while let Ok(update) = self.task_recv.try_recv() { update(&mut self.state, &mut self.rsc); } } /// One pointer sample through the sensors, plus the platform calls a /// handler can only ask for by raising a flag. Split out of /// [`Self::after_input`] because a batched `MotionEvent` carries /// several samples that all belong to the same *frame* /// (`on_touch_event`): each one is a real input frame the widgets must /// see, but only the last one ends the frame and asks for a redraw. fn run_input_frame(&mut self, ctx: &mut CallbackCtx) { let window_size = self.window_size(); let ui_state = self.state.android_state_mut(); let cursor = ui_state.cursor.clone(); let old_focus = ui_state.focus; let render_state = self.rsc.ui.render_state(); render_state .get() .run_sensors(&mut self.rsc, &mut self.state, cursor, window_size); let ui_state = self.state.android_state_mut(); if old_focus != ui_state.focus && let Some(old) = old_focus { old.edit(&mut self.rsc).deselect(); } if std::mem::take(&mut ui_state.pending_show_keyboard) { show_soft_input(&mut ctx.env, &ctx.view); } if let Some(url) = ui_state.pending_open_url.take() { super::platform::open_url(&mut ctx.env, &ctx.view, &url); } } /// Common tail for every callback that might have changed the cursor, /// the text focus, or the widget tree: run the sensors that touch /// input feeds, then ask for a frame if the result needs drawing. /// Mirrors `desktop::DesktopApp::window_event`'s tail, split across /// android-view's several entry points instead of winit's one. pub(super) fn after_input(&mut self, ctx: &mut CallbackCtx) { self.run_input_frame(ctx); self.update_ime_selection(ctx); let ui_state = self.state.android_state_mut(); ui_state.cursor.end_frame(); let render_state = self.rsc.ui.render_state(); if render_state .get() .needs_redraw(&ui_state.root, self.rsc.widgets()) { ctx.view.post_frame_callback(&mut ctx.env); } } fn device_clock(&mut self, event_time: i64, oldest: i64) -> DeviceClock { *self .device_clock .get_or_insert_with(|| DeviceClock::anchored(Instant::now(), event_time, oldest)) } fn generic_motion<'local>( &mut self, ctx: &mut CallbackCtx<'local>, event: &MotionEvent<'local>, ) -> bool { let action = event.action_masked(&mut ctx.env); let event_time = event.event_time_nanos(&mut ctx.env); let mut clock = self.device_clock(event_time, event_time); let at = clock.sample(event_time); self.device_clock = Some(clock); let ui = self.state.android_state_mut(); ui.cursor.time = at; ui.cursor.pos = vec2(event.x(&mut ctx.env), event.y(&mut ctx.env)); ui.cursor.exists = !matches!(action, MotionAction::HoverExit); let buttons = event.button_state(&mut ctx.env); ui.cursor.buttons.left.update(buttons.primary()); ui.cursor.buttons.right.update(buttons.secondary()); ui.cursor.buttons.middle.update(buttons.teriary()); match action { MotionAction::HoverEnter | MotionAction::HoverMove | MotionAction::HoverExit | MotionAction::ButtonPress | MotionAction::ButtonRelease => {} MotionAction::Scroll => { ui.cursor.scroll_delta = vec2( event.axis(&mut ctx.env, Axis::Hscroll, 0), event.axis(&mut ctx.env, Axis::Vscroll, 0), ); } _ => return false, } self.after_input(ctx); true } fn window_size(&self) -> Vec2 { let ui_state = self.state.android_state(); match &ui_state.renderer { Some(r) => r.size(), None => Vec2::ZERO, } } fn render(&mut self, ctx: &mut CallbackCtx, now: Instant) { if self.state.android_state().renderer.is_none() { return; } // See `AndroidAppState::on_insets_changed`'s doc comment: fires // exactly when insets actually differ from last frame, not every // frame -- most frames this is one `Insets` equality check against // a `Copy` struct. Done before `ui_state` is bound below, since // `on_insets_changed` needs `&mut self.state`/`&mut self.rsc` both. let ui_state = self.state.android_state(); let current_insets = ui_state.insets(); if current_insets != ui_state.last_insets { let physical = WindowInsets::from_physical(current_insets); log::info!( "iris insets: left={} top={} right={} bottom={} ime_bottom={} \ ime_visible={} window={:?}", physical.left, physical.top, physical.right, physical.bottom, physical.ime_bottom, physical.ime_visible, self.window_size(), ); self.state.android_state_mut().last_insets = current_insets; self.state.on_insets_changed(&mut self.rsc, physical); } if crate::diagnostics::trace_enabled() { let ui_state = self.state.android_state(); log::debug!( target: "iris::frame", "render(): root={:?} widgets={} active={} root_px={:?} out_size={:?}", ui_state.root.is_some(), self.rsc.widgets().len(), self.rsc.ui.render_state().get().active_widgets(), ui_state .root .as_ref() .and_then(|r| { self.rsc .ui .render_state() .get() .window_region(r, &self.rsc) }), self.window_size(), ); } let frame_start = Instant::now(); let animating = self.rsc.ui.tick_animations(now); if animating { ctx.view.post_frame_callback(&mut ctx.env); } let ui_state = self.state.android_state_mut(); self.rsc.draw(&ui_state.root); let ui_state = self.state.android_state_mut(); let Some(renderer) = &mut ui_state.renderer else { return; }; let frame_diagnostics = renderer.update(&mut self.rsc.ui); if renderer.frame_count() <= DIAGNOSTIC_FRAMES { log::info!( "iris frame diagnostics: frame={} masks_resized={} moves_resized={} \ paints_resized={} \ atlas_pages_grown_prev={} image_bind_group_creates_prev={} wgpu_errors={}", renderer.frame_count(), frame_diagnostics.masks_resized, frame_diagnostics.moves_resized, frame_diagnostics.paints_resized, frame_diagnostics.atlas_pages_grown_prev, frame_diagnostics.image_bind_group_creates_prev, renderer.wgpu_errors.snapshot().len(), ); } let mut parts = renderer.draw(); parts.total = frame_start.elapsed(); self.state .android_state_mut() .frame_report .record(now, parts, animating); let render_state = self.rsc.ui.render_state(); crate::diagnostics::log_frame(&render_state.get(), now, parts, animating); if crate::diagnostics::trace_enabled() { let ui_state = self.state.android_state(); log::debug!( target: "iris::frame", "render(): after update active={} root_px={:?}", self.rsc.ui.render_state().get().active_widgets(), ui_state .root .as_ref() .and_then(|r| { self.rsc .ui .render_state() .get() .window_region(r, &self.rsc) }), ); } let ui_state = self.state.android_state_mut(); if let Some(tree_update) = ui_state.access.update( self.rsc.widgets(), &self.rsc.ui.render_state().get(), &self.rsc, ) { let ui_state = self.state.android_state_mut(); if let Some(events) = ui_state.access_adapter.update_if_active(|| tree_update) { ctx.push_dynamic_deferred_callback(move |env, view| { raise_if_enabled(env, view, events); }); } } } } fn show_soft_input<'local>(env: &mut JNIEnv<'local>, view: &View<'local>) { let imm = view.input_method_manager(env); imm.show_soft_input(env, view, 0); } fn show_renderer_error<'local>(env: &mut JNIEnv<'local>, view: &View<'local>, report: &str) { let Ok(message) = env.new_string(report) else { return; }; let _ = env.call_method( &view.0, "showRendererError", "(Ljava/lang/String;)V", &[JValue::Object(message.as_ref())], ); } impl ViewPeer for IrisViewPeer { fn on_key_down<'local>( &mut self, ctx: &mut CallbackCtx<'local>, key_code: Keycode, event: &KeyEvent<'local>, ) -> bool { self.drain_tasks(); // With no `OnBackPressedCallback` registered on the Java side, the // system still delivers the back gesture as a synthetic // `KEYCODE_BACK` through this same path -- the legacy behaviour // every view-based app gets by default, and enough for "the back // gesture as an event" without a second JNI registry. See // `android/insets.rs`'s doc comment for why insets could not take // the same shortcut. if key_code == Keycode::Back { if self.rsc.run_command(Command::Escape) != CommandResult::Unused { self.after_input(ctx); return true; } let handled = self.state.back_pressed(&mut self.rsc); if handled { self.after_input(ctx); } return handled; } if self.rsc.events.controllers.command_target_blocks_input() { return true; } let handled = super::input::on_key( &mut self.rsc, &mut self.state, &mut ctx.env, key_code, event, ); if handled { self.after_input(ctx); } handled } fn on_touch_event<'local>( &mut self, ctx: &mut CallbackCtx<'local>, event: &MotionEvent<'local>, ) -> bool { self.drain_tasks(); let action = event.action_masked(&mut ctx.env); // MotionEvent and layout both use physical pixels. let x = event.x(&mut ctx.env); let y = event.y(&mut ctx.env); // Use the event clock so batched movement keeps its real timing. let event_time = event.event_time_nanos(&mut ctx.env); let history = event.history_size(&mut ctx.env); let mut clock = match self.device_clock { Some(clock) => clock, None => { let oldest = if history > 0 { event.historical_event_time_nanos(&mut ctx.env, 0) } else { event_time }; self.device_clock(event_time, oldest) } }; // Avoid allocating trace history when input tracing is disabled. let trace_input = crate::diagnostics::trace_enabled(); let mut historical_ms: Vec<(u64, f32, f32)> = Vec::new(); if matches!(action, MotionAction::Move) { // Android orders history oldest-first; `sample` checks monotonicity. for pos in 0..history { let hx = event.historical_axis(&mut ctx.env, Axis::X, 0, pos); let hy = event.historical_axis(&mut ctx.env, Axis::Y, 0, pos); let ht = event.historical_event_time_nanos(&mut ctx.env, pos); let sample_at = clock.sample(ht); if trace_input { historical_ms.push((clock.ms_since_anchor(ht), hx, hy)); } let ui_state = self.state.android_state_mut(); ui_state.cursor.pos = vec2(hx, hy); ui_state.cursor.time = sample_at; self.run_input_frame(ctx); } } let event_at = clock.sample(event_time); let event_ms = clock.ms_since_anchor(event_time); self.device_clock = Some(clock); let ui_state = self.state.android_state_mut(); ui_state.cursor.time = event_at; match action { MotionAction::Down => { ui_state.cursor.pos = vec2(x, y); ui_state.cursor.exists = true; ui_state.cursor.buttons.left.update(true); } MotionAction::Move => { ui_state.cursor.pos = vec2(x, y); } MotionAction::Up => { ui_state.cursor.pos = vec2(x, y); ui_state.cursor.buttons.left.update(false); } MotionAction::Cancel => { ui_state.cursor.pos = vec2(x, y); ui_state.cursor.buttons.left.update(false); ui_state.cursor.cancelled = true; } _ => return false, } if trace_input { let action_word = match action { MotionAction::Down => "down", MotionAction::Move => "move", MotionAction::Up => "up", MotionAction::Cancel => "cancel", _ => "other", }; crate::sense::log_input_event(action_word, x, y, event_ms, &historical_ms); } self.after_input(ctx); true } fn on_generic_motion_event<'local>( &mut self, ctx: &mut CallbackCtx<'local>, event: &MotionEvent<'local>, ) -> bool { self.drain_tasks(); self.generic_motion(ctx, event) } fn on_hover_event<'local>( &mut self, ctx: &mut CallbackCtx<'local>, event: &MotionEvent<'local>, ) -> bool { let action = event.action(&mut ctx.env); let x = event.x(&mut ctx.env); let y = event.y(&mut ctx.env); let access_events = { let render_handle = self.rsc.ui.render_state(); let render_state = render_handle.get(); let mut source = AndroidAccessSource { widgets: self.rsc.widgets(), render: &render_state, rsc: &self.rsc, }; self.state .android_state_mut() .access_adapter .on_hover_event(&mut source, action, x, y) }; if let Some(events) = access_events { ctx.push_dynamic_deferred_callback(move |env, view| { raise_if_enabled(env, view, events); }); true } else { self.drain_tasks(); self.generic_motion(ctx, event) } } fn on_focus_changed<'local>( &mut self, ctx: &mut CallbackCtx<'local>, gain_focus: bool, _direction: i32, _previously_focused_rect: Option<&Rect<'local>>, ) { self.drain_tasks(); if !gain_focus { let ui_state = self.state.android_state_mut(); if let Some(focus) = ui_state.focus.take() { focus.edit(&mut self.rsc).deselect(); } } self.after_input(ctx); } fn on_attached_to_window(&mut self, _ctx: &mut CallbackCtx) { self.drain_tasks(); } fn surface_changed<'local>( &mut self, ctx: &mut CallbackCtx<'local>, holder: &android_view::SurfaceHolder<'local>, _format: i32, width: i32, height: i32, ) { self.drain_tasks(); // The layout canvas and wgpu surface are separate and both use physical pixels. self.rsc.ui.resize((width as f32, height as f32)); // Resizing preserves GPU resources; recreating a destroyed surface does not. let already_live = self.state.android_state().renderer.is_some(); log::info!( "iris surface: surface_changed {width}x{height} already_live={already_live} \ glyphs_cached={} atlas_pages={}", self.rsc.ui.text.atlas.glyph_count(), self.rsc.ui.text.atlas.page_count(), ); if already_live { let ui_state = self.state.android_state_mut(); ui_state .renderer .as_mut() .expect("checked Some above") .resize(width as u32, height as u32); self.render(ctx, Instant::now()); return; } let window = holder.surface(&mut ctx.env).to_native_window(&mut ctx.env); // `content_scale` reaches `AndroidRenderer` only for the // Diagnostics page's report text now -- window size and the // shader's window uniform are physical pixels throughout (see the // `resize` call above), not divided by it. let content_scale = self.state.android_state().content_scale; match AndroidRenderer::new(window, width as u32, height as u32, content_scale) { Ok(renderer) => { // A genuinely new renderer means a genuinely new GPU // device, holding none of the textures the old one did -- // while the CPU side of them (`UiData::textures`, and the // glyph atlas built on it) lives on `self.rsc` and // survives. So every slot has to be uploaded again, and // `Textures::reupload` queues exactly that, in slot order. // // It replaces clearing them, which threw away the *slot // numbering* as well as the pixels: every `TextureHandle` // a live widget still held -- one per icon or image on // screen, and one per folded card at the time -- then // named a slot nothing recognised, and the next frame // panicked in `image_bind_group` ("texture slot 89 is not // a live standalone image: None"). Re-uploading also keeps // the glyph atlas, so an app switch no longer re-rasterises // every glyph on screen. This only runs on the branch that // actually builds a new renderer, never on the reuse // branch above, where the textures are still on the device // that holds them. log::info!( "iris surface: new renderer built ({:?}), re-uploading textures: \ glyphs={} pages={}", renderer.adapter_backend, self.rsc.ui.text.atlas.glyph_count(), self.rsc.ui.text.atlas.page_count(), ); self.rsc.ui.textures.reupload(); self.rsc.ui.paints.reupload(); self.state.android_state_mut().renderer = Some(renderer); self.render(ctx, Instant::now()); } Err(report) => { log::error!("iris renderer init failed: {}", report.replace('\n', " | ")); ctx.push_dynamic_deferred_callback(move |env, view| { show_renderer_error(env, view, &report); }); } } } fn surface_destroyed<'local>( &mut self, _ctx: &mut CallbackCtx<'local>, _holder: &android_view::SurfaceHolder<'local>, ) { log::info!( "iris surface: surface_destroyed, tearing the renderer down \ (glyphs_cached={} atlas_pages={})", self.rsc.ui.text.atlas.glyph_count(), self.rsc.ui.text.atlas.page_count(), ); self.state.android_state_mut().renderer = None; } fn do_frame(&mut self, ctx: &mut CallbackCtx, frame_time_nanos: i64) { self.drain_tasks(); // The vsync this frame is for, dated on the same ruler touch // samples are (`DeviceClock`), rather than `Instant::now()` here: // this callback runs some variable distance after that vsync -- // behind `drain_tasks`, behind whatever else the UI thread was // doing -- and anything advanced by that variable amount moves // unevenly between frames the display shows evenly. `at` rather // than `sample`, since a frame time is not part of the touch // samples' own ordering. let now = self .device_clock(frame_time_nanos, frame_time_nanos) .at(frame_time_nanos); self.render(ctx, now); } /// Where `AndroidRedrawHandle::request_redraw` (`android/render.rs`) /// actually lands: `View.postDelayed`'s Runnable resolves to this, on /// the UI thread, which is what makes it safe to call from a background /// task's own thread when `post_frame_callback`'s `Choreographer` /// requirement (a `Looper` on the *calling* thread) is not. Same body /// as `do_frame` -- draining tasks and rendering immediately is a /// perfectly good answer to "a background fetch has new state," and /// avoids a second frame-scheduling path to keep in sync with the real /// one. fn delayed_callback(&mut self, ctx: &mut CallbackCtx) { self.drain_tasks(); self.render(ctx, Instant::now()); } fn as_input_connection(&mut self) -> Option<&mut dyn InputConnection> { Some(self) } fn as_accessibility_node_provider(&mut self) -> Option<&mut dyn AccessibilityNodeProvider> { Some(self) } } impl AccessibilityNodeProvider for IrisViewPeer { fn create_accessibility_node_info<'local>( &mut self, ctx: &mut CallbackCtx<'local>, virtual_view_id: jint, ) -> AccessibilityNodeInfo<'local> { let render_handle = self.rsc.ui.render_state(); let render_state = render_handle.get(); let mut source = AndroidAccessSource { widgets: self.rsc.widgets(), render: &render_state, rsc: &self.rsc, }; let ui_state = self.state.android_state_mut(); AccessibilityNodeInfo(ui_state.access_adapter.create_accessibility_node_info( &mut source, &mut ctx.env, &ctx.view.0, virtual_view_id, )) } fn find_focus<'local>( &mut self, ctx: &mut CallbackCtx<'local>, focus_type: jint, ) -> AccessibilityNodeInfo<'local> { let render_handle = self.rsc.ui.render_state(); let render_state = render_handle.get(); let mut source = AndroidAccessSource { widgets: self.rsc.widgets(), render: &render_state, rsc: &self.rsc, }; let ui_state = self.state.android_state_mut(); AccessibilityNodeInfo(ui_state.access_adapter.find_focus( &mut source, &mut ctx.env, &ctx.view.0, focus_type, )) } fn perform_action<'local>( &mut self, ctx: &mut CallbackCtx<'local>, virtual_view_id: jint, action: jint, arguments: &Bundle<'local>, ) -> bool { let Some(action) = accesskit_android::PlatformAction::from_java(&mut ctx.env, action, &arguments.0) else { return false; }; let ui_state = self.state.android_state_mut(); let Some(events) = ui_state.access_adapter.perform_action( &mut NullActionHandler, virtual_view_id, &action, ) else { return false; }; ctx.push_dynamic_deferred_callback(move |env, view| { raise_if_enabled(env, view, events); }); true } } /// Registers `IrisViewPeer`'s native methods and builds one on every /// `newViewPeer` call from Java. `State`'s app crate wraps this in a /// concrete `extern "system" fn` (a generic function cannot be handed to /// `register_view_class`, which wants a plain function pointer) -- see /// `iris/android-app/src/lib.rs`. pub fn new_peer<'local, State: AndroidAppState>( mut env: JNIEnv<'local>, view: View<'local>, context: Context<'local>, ) -> android_view::jni::sys::jlong { // `DisplayMetrics.density` -- physical pixels per dp on this device. // Read once here, at the one point in this file already handed a // `Context`, and carried on `AndroidUiState` from then on (see // `content_scale`'s field comment for what depends on it). let content_scale = context .resources(&mut env) .display_metrics(&mut env) .density(&mut env); log::info!("iris: new_peer content_scale={content_scale}"); let vm = env.get_java_vm().unwrap(); let global_view = env.new_global_ref(&view.0).unwrap(); let redraw: Arc = Arc::new(AndroidRedrawHandle::new(vm, global_view)); let (mut rsc, task_recv) = StdRsc::new(redraw); rsc.ui.set_density(content_scale); let shared = Rc::new(RefCell::new(Shared::default())); let ui_state = AndroidUiState::new(shared.clone(), content_scale); let mut state = State::new(ui_state, &mut rsc); let platform_vm = env.get_java_vm().unwrap(); let platform_view = env.new_global_ref(&view.0).unwrap(); state.platform_ready(&mut rsc, platform_vm, platform_view); let peer = IrisViewPeer { rsc, state, task_recv, device_clock: None, }; let id = android_view::register_view_peer(peer); super::insets::register(id, shared); id }