use crate::{
android_host_window,
android_jni::{clear_pending_android_jni_exception, with_android_activity_env},
android_keyboard::{self, is_system_key, AndroidKeyTranslator, AndroidSoftKeyboard},
android_overlay_window,
android_surface::{create_android_wgpu_surface, AndroidSurfaceError},
android_text_input::{self, AndroidImeEvent},
launcher::{AndroidOverlayWindowOptions, AppSettings},
wgpu_surface::surface_present_required,
wgpu_surface::{current_surface_texture, SurfaceFrame},
};
use cranpose_app_shell::{
default_root_key, AppShell, KeyEvent, PlatformFrameDriver, PointerSource,
};
use cranpose_platform_android::AndroidPlatform;
use cranpose_render_wgpu::WgpuRenderer;
use cranpose_ui::{Point, Size};
use ndk::native_window::NativeWindow;
use std::time::{Duration, Instant};
use std::{
cell::{Cell, RefCell},
ffi::c_void,
ptr::NonNull,
rc::Rc,
sync::{
atomic::{AtomicBool, Ordering},
Arc,
},
};
struct GpuResources {
surface: wgpu::Surface<'static>,
native_window_ptr: NonNull<c_void>,
adapter: Arc<wgpu::Adapter>,
device: Arc<wgpu::Device>,
queue: Arc<wgpu::Queue>,
surface_format: wgpu::TextureFormat,
backend: wgpu::Backend,
config: wgpu::SurfaceConfiguration,
_native_window: Option<NativeWindow>,
surface_dirty: bool,
}
enum PendingInput {
PointerDown(f32, f32, Option<i64>, PointerSource),
PointerUp(f32, f32, Option<i64>, PointerSource),
PointerMove(f32, f32, Option<i64>, PointerSource),
Key(KeyEvent),
SecondaryPointerDown(u64, f32, f32, Option<i64>),
SecondaryPointerUp(u64, f32, f32, Option<i64>),
SecondaryPointerMove(u64, f32, f32, Option<i64>),
}
fn android_event_time_ms(event_time_ns: i64) -> i64 {
event_time_ns / 1_000_000
}
fn android_pointer_source(
pointer: &android_activity::input::Pointer<'_>,
event_source: android_activity::input::Source,
) -> PointerSource {
use crate::android_input::{resolve_pointer_source, AndroidSourceKind, AndroidToolKind};
use android_activity::input::{Source, ToolType};
let tool = match pointer.tool_type() {
ToolType::Finger => AndroidToolKind::Finger,
ToolType::Mouse => AndroidToolKind::Mouse,
ToolType::Stylus | ToolType::Eraser => AndroidToolKind::Stylus,
_ => AndroidToolKind::Indeterminate,
};
let source = match event_source {
Source::Touchscreen => AndroidSourceKind::Touchscreen,
Source::Stylus | Source::BluetoothStylus => AndroidSourceKind::Stylus,
Source::Mouse | Source::MouseRelative => AndroidSourceKind::Mouse,
_ => AndroidSourceKind::Other,
};
resolve_pointer_source(tool, source)
}
fn push_pending_input_from_android_key_event(
key_event: &android_activity::input::KeyEvent<'_>,
key_translator: &mut AndroidKeyTranslator,
pending_inputs: &mut Vec<PendingInput>,
) -> bool {
if key_event.key_code() == android_activity::input::Keycode::Back {
if cranpose_services::back_interception_enabled() {
if key_event.action() == android_activity::input::KeyAction::Up {
cranpose_services::push_back_request();
}
return true;
}
return false;
}
if is_system_key(key_event.key_code()) {
return false;
}
let Some(event) = key_translator.translate(key_event) else {
return false;
};
pending_inputs.push(PendingInput::Key(event));
true
}
thread_local! {
static ANDROID_PLATFORM_ENV: Rc<crate::platform_env::PlatformEnvironment> =
crate::platform_env::PlatformEnvironment::new();
static ANDROID_IME_DENSITY: Cell<f32> = const { Cell::new(1.0) };
}
pub(crate) fn android_platform_env() -> Rc<crate::platform_env::PlatformEnvironment> {
ANDROID_PLATFORM_ENV.with(Rc::clone)
}
fn set_android_ime_density(density: f32) {
ANDROID_IME_DENSITY.with(|cell| cell.set(density.max(f32::EPSILON)));
}
fn set_android_ime_bottom_px(bottom_px: i32) -> bool {
let density = ANDROID_IME_DENSITY.with(|cell| cell.get());
let bottom = (bottom_px.max(0) as f32) / density;
let insets = cranpose_ui::EdgeInsets {
bottom,
..cranpose_ui::EdgeInsets::default()
};
android_platform_env().set_ime_insets(insets)
}
fn system_theme_from_android(
night: ndk::configuration::UiModeNight,
) -> cranpose_services::SystemTheme {
match night {
ndk::configuration::UiModeNight::Yes => cranpose_services::SystemTheme::Dark,
_ => cranpose_services::SystemTheme::Light,
}
}
const IME_ACTION_DONE: i32 = 6;
fn dispatch_android_ime_event(shell: &mut AppShell<WgpuRenderer>, event: AndroidImeEvent) {
match event {
AndroidImeEvent::CommitText { text, .. } => {
let _ = shell.on_ime_preedit("", None);
let _ = shell.on_paste(&text);
}
AndroidImeEvent::SetComposingText { text, cursor_bytes } => {
let _ = shell.on_ime_preedit(&text, Some((cursor_bytes, cursor_bytes)));
}
AndroidImeEvent::SetComposingRegion {
start_bytes,
end_bytes,
} => {
let _ = shell.on_ime_set_composing_region(start_bytes, end_bytes);
}
AndroidImeEvent::SetSelection {
start_bytes,
end_bytes,
} => {
let _ = shell.on_ime_set_selection(start_bytes, end_bytes);
}
AndroidImeEvent::FinishComposing => {
let _ = shell.on_ime_finish_composing();
}
AndroidImeEvent::DeleteSurrounding {
before_bytes,
after_bytes,
} => {
let _ = shell.on_ime_delete_surrounding(before_bytes, after_bytes);
}
AndroidImeEvent::Key {
action,
key_code,
meta_state,
unicode_char,
} => {
if let Some(event) =
android_keyboard::ime_key_event(action, key_code, meta_state, unicode_char)
{
let _ = shell.on_key_event(&event);
}
}
AndroidImeEvent::EditorAction { action } => {
if action == IME_ACTION_DONE {
let _ = shell.on_ime_finish_composing();
shell.clear_text_field_focus();
} else {
for event_type in [
cranpose_app_shell::KeyEventType::KeyDown,
cranpose_app_shell::KeyEventType::KeyUp,
] {
let key = KeyEvent::new(
cranpose_app_shell::KeyCode::Enter,
"",
cranpose_app_shell::Modifiers::NONE,
event_type,
);
let _ = shell.on_key_event(&key);
}
}
}
AndroidImeEvent::ImeInsetsChanged { bottom_px } => {
if set_android_ime_bottom_px(bottom_px) {
shell.request_root_render();
}
}
}
}
fn shell_pointer_id(android_pointer_id: i32, primary_pointer_id: i32) -> u64 {
if android_pointer_id == primary_pointer_id {
0
} else {
android_pointer_id as u64 + 1
}
}
fn push_pending_inputs_from_android_event(
event: &android_activity::input::InputEvent<'_>,
android_platform: &AndroidPlatform,
key_translator: &mut AndroidKeyTranslator,
pending_inputs: &mut Vec<PendingInput>,
primary_pointer_id: &mut Option<i32>,
) -> bool {
let motion_event = match event {
android_activity::input::InputEvent::MotionEvent(motion_event) => motion_event,
android_activity::input::InputEvent::KeyEvent(key_event) => {
return push_pending_input_from_android_key_event(
key_event,
key_translator,
pending_inputs,
);
}
_ => return false,
};
let time_ms = Some(android_event_time_ms(motion_event.event_time()));
let event_source = motion_event.source();
let logical_of = |x: f32, y: f32| {
let logical = android_platform.pointer_position(x as f64, y as f64);
(logical.x as f32, logical.y as f32)
};
match motion_event.action() {
android_activity::input::MotionAction::Down => {
let pointer = motion_event.pointer_at_index(0);
*primary_pointer_id = Some(pointer.pointer_id());
let (x, y) = logical_of(pointer.x(), pointer.y());
pending_inputs.push(PendingInput::PointerDown(
x,
y,
time_ms,
android_pointer_source(&pointer, event_source),
));
true
}
android_activity::input::MotionAction::PointerDown => {
let pointer = motion_event.pointer_at_index(motion_event.pointer_index());
let Some(primary) = *primary_pointer_id else {
return true;
};
let (x, y) = logical_of(pointer.x(), pointer.y());
pending_inputs.push(PendingInput::SecondaryPointerDown(
shell_pointer_id(pointer.pointer_id(), primary),
x,
y,
time_ms,
));
true
}
android_activity::input::MotionAction::Up => {
let pointer = motion_event.pointer_at_index(motion_event.pointer_index());
let (x, y) = logical_of(pointer.x(), pointer.y());
match *primary_pointer_id {
Some(primary) if pointer.pointer_id() != primary => {
pending_inputs.push(PendingInput::SecondaryPointerUp(
shell_pointer_id(pointer.pointer_id(), primary),
x,
y,
time_ms,
));
}
_ => {}
}
pending_inputs.push(PendingInput::PointerUp(
x,
y,
time_ms,
android_pointer_source(&pointer, event_source),
));
*primary_pointer_id = None;
true
}
android_activity::input::MotionAction::PointerUp => {
let pointer = motion_event.pointer_at_index(motion_event.pointer_index());
let Some(primary) = *primary_pointer_id else {
return true;
};
let (x, y) = logical_of(pointer.x(), pointer.y());
if pointer.pointer_id() == primary {
for other in motion_event.pointers() {
if other.pointer_id() == primary {
continue;
}
let (ox, oy) = logical_of(other.x(), other.y());
pending_inputs.push(PendingInput::SecondaryPointerUp(
shell_pointer_id(other.pointer_id(), primary),
ox,
oy,
time_ms,
));
}
pending_inputs.push(PendingInput::PointerUp(
x,
y,
time_ms,
android_pointer_source(&pointer, event_source),
));
*primary_pointer_id = None;
} else {
pending_inputs.push(PendingInput::SecondaryPointerUp(
shell_pointer_id(pointer.pointer_id(), primary),
x,
y,
time_ms,
));
}
true
}
android_activity::input::MotionAction::Move => {
let primary = *primary_pointer_id;
for pointer in motion_event.pointers() {
let is_primary = primary == Some(pointer.pointer_id());
let source = android_pointer_source(&pointer, event_source);
if is_primary {
for historical in pointer.history() {
let (hx, hy) = logical_of(historical.x(), historical.y());
pending_inputs.push(PendingInput::PointerMove(
hx,
hy,
Some(android_event_time_ms(historical.event_time())),
source,
));
}
}
let (x, y) = logical_of(pointer.x(), pointer.y());
match (is_primary, primary) {
(true, _) => {
pending_inputs.push(PendingInput::PointerMove(x, y, time_ms, source));
}
(false, Some(primary)) => {
pending_inputs.push(PendingInput::SecondaryPointerMove(
shell_pointer_id(pointer.pointer_id(), primary),
x,
y,
time_ms,
));
}
(false, None) => {}
}
}
true
}
_ => false,
}
}
fn drain_android_input_events(
app: &android_activity::AndroidApp,
android_platform: &AndroidPlatform,
key_translator: &mut AndroidKeyTranslator,
pending_inputs: &mut Vec<PendingInput>,
primary_pointer_id: &mut Option<i32>,
) {
let Ok(mut iter) = app.input_events_iter() else {
return;
};
for _ in 0..MAX_ANDROID_INPUT_EVENTS_PER_POLL {
let event_available = iter.next(|event| {
if push_pending_inputs_from_android_event(
event,
android_platform,
key_translator,
pending_inputs,
primary_pointer_id,
) {
android_activity::InputStatus::Handled
} else {
android_activity::InputStatus::Unhandled
}
});
if !event_available {
break;
}
}
}
const MAX_ANDROID_INPUT_EVENTS_PER_POLL: usize = 10;
#[derive(Clone, Copy)]
struct PendingHostWindowSizeRequest {
state: Option<android_host_window::AndroidHostWindowState>,
requested: Size,
requested_at: Instant,
}
struct AndroidFrameDriver {
need_frame: Arc<AtomicBool>,
app_waker: android_activity::AndroidAppWaker,
next_deadline: Cell<Option<web_time::Instant>>,
}
impl AndroidFrameDriver {
fn new(app_waker: android_activity::AndroidAppWaker) -> Self {
Self {
need_frame: Arc::new(AtomicBool::new(false)),
app_waker,
next_deadline: Cell::new(None),
}
}
fn frame_waker(&self) -> impl Fn() + Send + Sync + 'static {
let need_frame = self.need_frame.clone();
let app_waker = self.app_waker.clone();
move || {
need_frame.store(true, Ordering::Relaxed);
app_waker.wake();
}
}
fn frame_requested(&self) -> bool {
self.need_frame.load(Ordering::Relaxed)
}
fn take_frame_request(&self) -> bool {
self.need_frame.swap(false, Ordering::Relaxed)
}
fn deadline_timeout(&self) -> Option<Duration> {
self.next_deadline.get().map(duration_until_frame_deadline)
}
}
impl PlatformFrameDriver for AndroidFrameDriver {
fn request_frame(&self) {
self.need_frame.store(true, Ordering::Relaxed);
self.app_waker.wake();
}
fn request_wake_at(&self, deadline: web_time::Instant) {
self.next_deadline.set(Some(deadline));
}
fn clear_wake(&self) {
self.next_deadline.set(None);
}
}
fn duration_until_frame_deadline(deadline: web_time::Instant) -> Duration {
deadline
.checked_duration_since(web_time::Instant::now())
.unwrap_or(Duration::ZERO)
}
fn earliest_android_poll_timeout(
first: Option<Duration>,
second: Option<Duration>,
) -> Option<Duration> {
match (first, second) {
(Some(first), Some(second)) => Some(first.min(second)),
(Some(duration), None) | (None, Some(duration)) => Some(duration),
(None, None) => None,
}
}
fn get_display_density(app: &android_activity::AndroidApp) -> f32 {
let config = app.config();
let density_dpi = config.density();
density_dpi.map(|dpi| dpi as f32 / 160.0).unwrap_or(2.0) }
fn surface_inset_px(app: &android_activity::AndroidApp) -> (f64, f64) {
let Some(window) = app.native_window() else {
return (0.0, 0.0);
};
let buffer_w = window.width() as f64;
let buffer_h = window.height() as f64;
let content = app.content_rect();
let frame_w = content.right as f64;
let frame_h = content.bottom as f64;
if frame_w <= 0.0 || frame_h <= 0.0 || frame_w > buffer_w || frame_h > buffer_h {
return (0.0, 0.0);
}
(
((buffer_w - frame_w) / 2.0).max(0.0),
((buffer_h - frame_h) / 2.0).max(0.0),
)
}
fn update_android_platform_geometry(
app: &android_activity::AndroidApp,
android_platform: &mut AndroidPlatform,
) -> f32 {
let density = get_display_density(app);
android_platform.set_scale_factor(density as f64);
let (offset_x, offset_y) = surface_inset_px(app);
android_platform.set_input_surface_offset_px(offset_x, offset_y);
density
}
fn update_android_shell_geometry(
shell: &mut AppShell<WgpuRenderer>,
density: f32,
host_window_registry: &android_host_window::AndroidHostWindowRegistry,
) -> Option<Size> {
shell.renderer().set_root_scale(density);
shell.set_density(density);
let (width, height) = shell.buffer_size();
if width > 0 && height > 0 {
let width_dp = width as f32 / density;
let height_dp = height as f32 / density;
shell.set_viewport(width_dp, height_dp);
let actual = Size::new(width_dp, height_dp);
android_host_window::sync_android_host_window_actual_size(host_window_registry, actual);
Some(actual)
} else {
None
}
}
fn render_once(resources: &mut GpuResources, shell: &mut AppShell<WgpuRenderer>) -> bool {
match current_surface_texture(&resources.surface, "android") {
SurfaceFrame::Ready(frame) => {
let view = frame
.texture
.create_view(&wgpu::TextureViewDescriptor::default());
let (width, height) = shell.buffer_size();
if let Err(e) = shell.renderer().render(&view, width, height) {
log::error!("Render error: {:?}", e);
}
frame.present();
resources.surface_dirty = false;
false
}
SurfaceFrame::Reconfigure => {
let (width, height) = shell.buffer_size();
resources.config.width = width;
resources.config.height = height;
resources
.surface
.configure(&resources.device, &resources.config);
resources.surface_dirty = true;
shell.mark_dirty();
false
}
SurfaceFrame::Skip => {
resources.surface_dirty = true;
false
}
}
}
struct AndroidGpuSetup {
resources: GpuResources,
renderer_needs_init: bool,
}
fn initialize_android_rendering<F>(
instance: &wgpu::Instance,
existing_resources: Option<GpuResources>,
app_shell: &mut Option<AppShell<WgpuRenderer>>,
content: &Rc<RefCell<F>>,
settings: &AppSettings,
frame_driver: &AndroidFrameDriver,
host_window_registry: &android_host_window::AndroidHostWindowRegistry,
native_window_ptr: NonNull<c_void>,
native_window_owner: Option<NativeWindow>,
width: u32,
height: u32,
density: f32,
) -> Result<(GpuResources, Option<Size>), AndroidSurfaceError>
where
F: FnMut() + 'static,
{
let setup = create_android_gpu_resources(
instance,
existing_resources,
native_window_ptr,
native_window_owner,
width,
height,
)?;
if app_shell.is_none() {
let fonts: &[&[u8]] = settings.fonts.unwrap_or(&[]);
let mut renderer = WgpuRenderer::new(fonts);
renderer.init_gpu(
setup.resources.device.clone(),
setup.resources.queue.clone(),
setup.resources.surface_format,
setup.resources.backend,
);
let content_clone = content.clone();
let density = density.max(f32::EPSILON);
let platform_env = android_platform_env();
let shell = AppShell::new_with_size_and_density(
renderer,
default_root_key(),
move || {
platform_env.compose_root(|| content_clone.borrow_mut()());
},
(width, height),
(width as f32 / density, height as f32 / density),
density,
);
*app_shell = Some(shell);
if let Some(shell) = app_shell {
shell.set_frame_waker(frame_driver.frame_waker());
}
log::info!("App shell created");
} else if setup.renderer_needs_init {
if let Some(shell) = app_shell {
shell.renderer().init_gpu(
setup.resources.device.clone(),
setup.resources.queue.clone(),
setup.resources.surface_format,
setup.resources.backend,
);
log::info!("Renderer reinitialized with new Android GPU pipeline resources");
}
} else {
log::debug!("Reused Android WGPU device and renderer resources for surface update");
}
if let Some(shell) = app_shell {
shell.renderer().set_root_scale(density);
shell.set_density(density);
set_android_ime_density(density);
}
let actual_size = app_shell.as_mut().and_then(|shell| {
shell.set_buffer_size(width, height);
update_android_shell_geometry(shell, density, host_window_registry)
});
Ok((setup.resources, actual_size))
}
fn create_android_gpu_resources(
instance: &wgpu::Instance,
existing_resources: Option<GpuResources>,
native_window_ptr: NonNull<c_void>,
native_window_owner: Option<NativeWindow>,
width: u32,
height: u32,
) -> Result<AndroidGpuSetup, AndroidSurfaceError> {
if let Some(mut resources) = existing_resources {
if resources.native_window_ptr == native_window_ptr {
resources.config.width = width;
resources.config.height = height;
resources
.surface
.configure(&resources.device, &resources.config);
if let Some(native_window_owner) = native_window_owner {
resources._native_window = Some(native_window_owner);
}
return Ok(AndroidGpuSetup {
resources,
renderer_needs_init: false,
});
}
return create_android_gpu_resources_for_existing_device(
instance,
&resources,
native_window_ptr,
native_window_owner,
width,
height,
);
}
let surface = create_android_wgpu_surface(instance, native_window_ptr)?;
let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::HighPerformance,
compatible_surface: Some(&surface),
force_fallback_adapter: false,
}))?;
let adapter_info = adapter.get_info();
log::info!("Found adapter: {:?}", adapter_info.backend);
let adapter = Arc::new(adapter);
let (device, queue) = pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor {
label: Some("Android Device"),
required_features: wgpu::Features::empty(),
required_limits: crate::gpu_limits::mobile_device_limits(adapter.limits()),
experimental_features: wgpu::ExperimentalFeatures::disabled(),
memory_hints: wgpu::MemoryHints::default(),
trace: wgpu::Trace::Off,
}))?;
let device = Arc::new(device);
let queue = Arc::new(queue);
let config = create_android_surface_config(&surface, &adapter, width, height)?;
surface.configure(&device, &config);
Ok(AndroidGpuSetup {
resources: GpuResources {
surface,
native_window_ptr,
adapter,
device,
queue,
surface_format: config.format,
backend: adapter_info.backend,
config,
_native_window: native_window_owner,
surface_dirty: true,
},
renderer_needs_init: true,
})
}
fn create_android_gpu_resources_for_existing_device(
instance: &wgpu::Instance,
existing: &GpuResources,
native_window_ptr: NonNull<c_void>,
native_window_owner: Option<NativeWindow>,
width: u32,
height: u32,
) -> Result<AndroidGpuSetup, AndroidSurfaceError> {
let surface = create_android_wgpu_surface(instance, native_window_ptr)?;
let config = create_android_surface_config(&surface, &existing.adapter, width, height)?;
surface.configure(&existing.device, &config);
let renderer_needs_init = config.format != existing.surface_format;
Ok(AndroidGpuSetup {
resources: GpuResources {
surface,
native_window_ptr,
adapter: existing.adapter.clone(),
device: existing.device.clone(),
queue: existing.queue.clone(),
surface_format: config.format,
backend: existing.backend,
config,
_native_window: native_window_owner,
surface_dirty: true,
},
renderer_needs_init,
})
}
fn create_android_surface_config(
surface: &wgpu::Surface<'static>,
adapter: &wgpu::Adapter,
width: u32,
height: u32,
) -> Result<wgpu::SurfaceConfiguration, AndroidSurfaceError> {
let surface_caps = surface.get_capabilities(&adapter);
let surface_format =
crate::surface_format::select_display_surface_format(&surface_caps.formats)
.ok_or(AndroidSurfaceError::NoSurfaceFormat)?;
let alpha_mode = surface_caps
.alpha_modes
.first()
.copied()
.ok_or(AndroidSurfaceError::NoAlphaMode)?;
let present_mode = crate::present_mode::select_present_mode(&surface_caps);
Ok(wgpu::SurfaceConfiguration {
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
format: surface_format,
width,
height,
present_mode,
alpha_mode,
view_formats: vec![],
desired_maximum_frame_latency: 2,
})
}
fn dispatch_android_surface_size_request(
app: &android_activity::AndroidApp,
requested: Size,
position: Point,
density: f32,
overlay_options: Option<AndroidOverlayWindowOptions>,
) -> Result<(), String> {
let requested =
android_host_window::validate_logical_size(requested).map_err(|error| error.to_string())?;
if overlay_options.is_some() {
return android_overlay_window::update_android_overlay_window_bounds(
app, position, requested, density,
);
}
let (width_px, height_px) =
android_host_window::logical_to_physical_window_size(requested, density);
set_android_window_layout_px(app, width_px, height_px)
}
fn dispatch_registered_android_surface_size_request(
app: &android_activity::AndroidApp,
host_window_registry: &android_host_window::AndroidHostWindowRegistry,
density: f32,
overlay_options: Option<AndroidOverlayWindowOptions>,
last_dispatched: &mut Option<(android_host_window::AndroidHostWindowState, u64, u64)>,
pending_confirmation: &mut Option<PendingHostWindowSizeRequest>,
) {
let Some(request) =
android_host_window::latest_android_host_window_request(host_window_registry)
else {
return;
};
let dispatch_key = (
request.state,
request.size_revision,
if overlay_options.is_some() {
request.position_revision
} else {
0
},
);
if *last_dispatched == Some(dispatch_key) {
return;
}
let position = overlay_options
.filter(|_| request.position_revision == 0)
.map(|options| Point::new(options.x as f32, options.y as f32))
.unwrap_or(request.position);
request.state.mark_pending(request.size);
match dispatch_android_surface_size_request(
app,
request.size,
position,
density,
overlay_options,
) {
Ok(()) => {
*last_dispatched = Some(dispatch_key);
*pending_confirmation = Some(PendingHostWindowSizeRequest {
state: Some(request.state),
requested: request.size,
requested_at: Instant::now(),
});
let target = if overlay_options.is_some() {
"Android overlay surface"
} else {
"Android host-window"
};
if overlay_options.is_some() {
log::info!(
"Requested {target} bounds {:.1}x{:.1} dp at {:.1},{:.1} dp",
request.size.width,
request.size.height,
position.x,
position.y
);
} else {
log::info!(
"Requested {target} size {:.1}x{:.1} dp",
request.size.width,
request.size.height
);
}
}
Err(message) => {
*last_dispatched = Some(dispatch_key);
request.state.mark_dispatch_failed(request.size, message);
}
}
}
fn confirm_android_host_window_request(
pending_confirmation: &mut Option<PendingHostWindowSizeRequest>,
actual_size: Size,
) {
let Some(pending) = *pending_confirmation else {
return;
};
if android_host_window::sizes_match(pending.requested, actual_size) {
if let Some(state) = pending.state {
state.mark_applied(pending.requested, actual_size);
}
*pending_confirmation = None;
return;
}
if pending.requested_at.elapsed() >= android_host_window::HOST_WINDOW_CONFIRMATION_TIMEOUT {
if let Some(state) = pending.state {
state.mark_unsupported(pending.requested, actual_size);
}
log::info!(
"Android surface size request {:.1}x{:.1} dp was not honored; actual is {:.1}x{:.1} dp",
pending.requested.width,
pending.requested.height,
actual_size.width,
actual_size.height
);
*pending_confirmation = None;
}
}
fn android_activity_in_multi_window_mode(app: &android_activity::AndroidApp) -> bool {
use jni::{jni_sig, jni_str};
with_android_activity_env(app, |env, activity| {
env.call_method(
&activity,
jni_str!("isInMultiWindowMode"),
jni_sig!("()Z"),
&[],
)
.and_then(|value| value.z())
.map_err(|error| {
clear_pending_android_jni_exception(env);
format!("failed to query Android multi-window mode: {error}")
})
})
.unwrap_or_else(|error| {
log::warn!("{error}");
false
})
}
fn set_android_window_layout_px(
app: &android_activity::AndroidApp,
width_px: i32,
height_px: i32,
) -> Result<(), String> {
use jni::{jni_sig, jni_str, objects::JValue};
with_android_activity_env(app, |env, activity| {
let class = android_overlay_window::find_android_overlay_class(env, &activity)?;
let result = env
.call_static_method(
class,
jni_str!("setActivityWindowLayout"),
jni_sig!("(Landroid/app/Activity;II)I"),
&[
JValue::Object(&activity),
JValue::Int(width_px),
JValue::Int(height_px),
],
)
.and_then(|value| value.i())
.map_err(|error| {
clear_pending_android_jni_exception(env);
format!("failed to request Android window layout: {error}")
})?;
match result {
0 => Ok(()),
code => Err(format!(
"Android window layout request failed with code {code}"
)),
}
})
}
pub fn run(
app: android_activity::AndroidApp,
settings: AppSettings,
content: impl FnMut() + 'static,
) {
use android_activity::{MainEvent, PollEvent};
crate::android_file_picker::register(app.clone());
crate::android_writable_folder::register(app.clone());
crate::android_services::register(app.clone());
android_platform_env()
.set_system_theme(system_theme_from_android(app.config().ui_mode_night()));
std::panic::set_hook(Box::new(|panic_info| {
let location = panic_info
.location()
.map(|l| format!("{}:{}:{}", l.file(), l.line(), l.column()))
.unwrap_or_else(|| "unknown location".to_string());
let message = panic_info
.payload()
.downcast_ref::<&str>()
.map(|s| *s)
.or_else(|| {
panic_info
.payload()
.downcast_ref::<String>()
.map(|s| s.as_str())
})
.unwrap_or("Box<dyn Any>");
log::error!("PANIC at {}: {}", location, message);
}));
let content = std::rc::Rc::new(std::cell::RefCell::new(content));
let mut app_shell: Option<AppShell<WgpuRenderer>> = None;
android_logger::init_once(
android_logger::Config::default()
.with_max_level(log::LevelFilter::Info)
.with_tag("ComposeRS")
.with_filter(
android_logger::FilterBuilder::new()
.filter_level(log::LevelFilter::Info)
.filter_module("wgpu_core", log::LevelFilter::Warn)
.filter_module("wgpu_hal", log::LevelFilter::Warn)
.filter_module("naga", log::LevelFilter::Warn)
.build(),
),
);
log::info!("Starting Compose Android Application");
let android_frame_driver = AndroidFrameDriver::new(app.create_waker());
let host_window_registry = Rc::new(android_host_window::AndroidHostWindowRegistry::default());
let overlay_event_queue = Arc::new(android_overlay_window::AndroidOverlayEventQueue::default());
let ime_event_queue = Arc::new(android_text_input::AndroidImeEventQueue::new());
ime_event_queue.set_waker(app.create_waker());
let ime_session =
android_keyboard::AndroidImeSession::new(app.clone(), Arc::clone(&ime_event_queue));
let should_exit = Arc::new(AtomicBool::new(false));
let backends = wgpu::Backends::GL | wgpu::Backends::VULKAN;
let mut instance_descriptor = wgpu::InstanceDescriptor::new_without_display_handle();
instance_descriptor.backends = backends;
instance_descriptor.flags = wgpu::InstanceFlags::empty();
let instance = wgpu::Instance::new(instance_descriptor);
let mut android_platform = AndroidPlatform::new();
let mut current_host_window_size = Size::ZERO;
let mut initial_host_window_size = settings.initial_size_explicit.then(|| {
Size::new(
settings.initial_width as f32,
settings.initial_height as f32,
)
});
let mut last_dispatched_host_window_request =
None::<(android_host_window::AndroidHostWindowState, u64, u64)>;
let mut pending_host_window_confirmation = None::<PendingHostWindowSizeRequest>;
let mut overlay_window_options = settings.android_overlay_window;
let mut overlay_window_requested = false;
let mut gpu_resources: Option<GpuResources> = None;
let mut pending_inputs: Vec<PendingInput> = Vec::new();
let mut primary_pointer_id: Option<i32> = None;
let mut key_translator = AndroidKeyTranslator::new(app.clone());
let mut soft_keyboard_installed = false;
loop {
let pending_confirmation_timeout = pending_host_window_confirmation.map(|pending| {
android_host_window::HOST_WINDOW_CONFIRMATION_TIMEOUT
.checked_sub(pending.requested_at.elapsed())
.unwrap_or(Duration::ZERO)
});
if let Some(shell) = app_shell.as_ref() {
shell.schedule_platform_frame(&android_frame_driver);
} else {
android_frame_driver.clear_wake();
}
let frame_deadline_timeout = android_frame_driver.deadline_timeout();
let idle_timeout =
earliest_android_poll_timeout(pending_confirmation_timeout, frame_deadline_timeout);
let poll_duration = if !pending_inputs.is_empty() {
Some(Duration::ZERO)
} else if android_frame_driver.frame_requested() {
Some(Duration::ZERO)
} else {
idle_timeout
};
app.poll_events(poll_duration, |event| {
match event {
PollEvent::Main(main_event) => match main_event {
MainEvent::InitWindow { .. } => {
log::info!("Window initialized, setting up rendering");
if let Some(options) = overlay_window_options {
let density =
update_android_platform_geometry(&app, &mut android_platform);
android_platform.set_input_surface_offset_px(0.0, 0.0);
if !overlay_window_requested {
match android_overlay_window::show_android_overlay_window(
&app,
options,
density,
&overlay_event_queue,
) {
Ok(()) => {
overlay_window_requested = true;
log::info!(
"Requested Android overlay surface {}x{} dp at ({}, {})",
options.width,
options.height,
options.x,
options.y
);
}
Err(error) => {
overlay_window_options = None;
log::warn!(
"Android overlay surface unavailable; waiting for activity surface fallback: {error}"
);
}
}
}
}
if overlay_window_options.is_none() {
if let Some(native_window) = app.native_window() {
let width = native_window.width() as u32;
let height = native_window.height() as u32;
let density =
update_android_platform_geometry(&app, &mut android_platform);
let (input_offset_x, input_offset_y) =
android_platform.input_surface_offset_px();
log::info!(
"Display density: {:.2}x, input surface offset: ({:.1}, {:.1}) px",
density,
input_offset_x,
input_offset_y
);
match initialize_android_rendering(
&instance,
gpu_resources.take(),
&mut app_shell,
&content,
&settings,
&android_frame_driver,
&host_window_registry,
native_window.ptr().cast(),
None,
width,
height,
density,
) {
Ok((resources, actual_size)) => {
if let Some(actual_size) = actual_size {
current_host_window_size = actual_size;
}
let width_dp = current_host_window_size.width;
let height_dp = current_host_window_size.height;
log::info!(
"Set viewport to {:.1}x{:.1} dp ({}x{} px at {:.2}x density)",
width_dp,
height_dp,
width,
height,
density
);
if let Some(requested) = initial_host_window_size.take() {
if !android_activity_in_multi_window_mode(&app) {
log::info!(
"Ignoring initial window size {:.1}x{:.1} dp: fullscreen Android activities keep the display-sized edge-to-edge surface; size requests apply in multi-window/freeform modes",
requested.width,
requested.height
);
} else {
match dispatch_android_surface_size_request(
&app,
requested,
Point::ZERO,
density,
None,
) {
Ok(()) => {
pending_host_window_confirmation =
Some(PendingHostWindowSizeRequest {
state: None,
requested,
requested_at: Instant::now(),
});
log::info!(
"Requested initial Android host-window size {:.1}x{:.1} dp",
requested.width,
requested.height
);
}
Err(error) => {
log::warn!(
"Initial Android host-window size request failed: {error}"
);
}
}
}
}
gpu_resources = Some(resources);
log::info!("Rendering initialized successfully");
}
Err(error) => {
log::error!("Android rendering initialization failed: {error}");
}
}
}
}
}
MainEvent::TerminateWindow { .. } => {
log::info!("Window terminated");
if overlay_window_options.is_none() {
gpu_resources = None;
}
}
MainEvent::WindowResized { .. } => {
if overlay_window_options.is_none() {
if let Some(native_window) = app.native_window() {
let width = native_window.width() as u32;
let height = native_window.height() as u32;
let density =
update_android_platform_geometry(&app, &mut android_platform);
let (input_offset_x, input_offset_y) =
android_platform.input_surface_offset_px();
log::info!(
"Window resized to {}x{} at {:.2}x density with input surface offset ({:.1}, {:.1}) px",
width,
height,
density,
input_offset_x,
input_offset_y
);
if let (Some(resources), Some(shell)) =
(&mut gpu_resources, &mut app_shell)
{
if width > 0 && height > 0 {
resources.config.width = width;
resources.config.height = height;
resources
.surface
.configure(&resources.device, &resources.config);
shell.set_buffer_size(width, height);
if let Some(actual_size) =
update_android_shell_geometry(
shell,
density,
&host_window_registry,
)
{
current_host_window_size = actual_size;
}
}
}
}
}
}
MainEvent::ContentRectChanged { .. } => {
let density = update_android_platform_geometry(&app, &mut android_platform);
if overlay_window_options.is_some() {
android_platform.set_input_surface_offset_px(0.0, 0.0);
}
let (input_offset_x, input_offset_y) =
android_platform.input_surface_offset_px();
log::info!(
"Content rect changed; input surface offset: ({:.1}, {:.1}) px at {:.2}x density",
input_offset_x,
input_offset_y,
density
);
if let Some(shell) = &mut app_shell {
if let Some(actual_size) =
update_android_shell_geometry(shell, density, &host_window_registry)
{
current_host_window_size = actual_size;
}
}
}
MainEvent::RedrawNeeded { .. } => {
if let Some(shell) = &mut app_shell {
shell.mark_dirty();
}
}
MainEvent::Pause => {
log::info!("App paused");
if let Some(shell) = &mut app_shell {
shell.notify_app_paused();
}
ime_session.ensure_hidden();
}
MainEvent::Resume { .. } => {
log::info!("App resumed");
let reopened = app_shell
.as_mut()
.map(|shell| shell.notify_app_resumed())
.unwrap_or(false);
if !reopened {
ime_session.ensure_hidden();
}
}
MainEvent::Start => {
log::info!("App started");
}
MainEvent::Stop => {
log::info!("App stopped");
}
MainEvent::SaveState { .. } => {
log::info!("Save state requested");
}
MainEvent::Destroy => {
log::info!("App destroy requested, will exit after this event");
if overlay_window_options.is_some() {
android_overlay_window::hide_android_overlay_window(&app);
}
should_exit.store(true, Ordering::Relaxed);
}
MainEvent::InputAvailable => {
drain_android_input_events(
&app,
&android_platform,
&mut key_translator,
&mut pending_inputs,
&mut primary_pointer_id,
);
}
MainEvent::ConfigChanged { .. } => {
let theme = system_theme_from_android(app.config().ui_mode_night());
if android_platform_env().set_system_theme(theme) {
if let Some(shell) = &mut app_shell {
shell.request_root_render();
}
}
}
_ => {}
},
_ => {
}
}
});
for event in
android_overlay_window::drain_android_overlay_window_events(&overlay_event_queue)
{
match event {
android_overlay_window::AndroidOverlayWindowEvent::CreateFailed(message) => {
log::warn!("Android overlay surface failed: {message}");
overlay_window_options = None;
if let Some(native_window) = app.native_window() {
let width = native_window.width() as u32;
let height = native_window.height() as u32;
if width > 0 && height > 0 {
let density =
update_android_platform_geometry(&app, &mut android_platform);
match initialize_android_rendering(
&instance,
gpu_resources.take(),
&mut app_shell,
&content,
&settings,
&android_frame_driver,
&host_window_registry,
native_window.ptr().cast(),
None,
width,
height,
density,
) {
Ok((resources, actual_size)) => {
if let Some(actual_size) = actual_size {
current_host_window_size = actual_size;
}
gpu_resources = Some(resources);
}
Err(error) => {
log::error!(
"Android activity surface fallback initialization failed: {error}"
);
}
}
}
}
}
android_overlay_window::AndroidOverlayWindowEvent::SurfaceChanged {
native_window,
width,
height,
} => {
if width > 0 && height > 0 {
let density = get_display_density(&app);
android_platform.set_scale_factor(density as f64);
android_platform.set_input_surface_offset_px(0.0, 0.0);
if let Some(shell) = app_shell.as_mut() {
shell.set_density(density);
}
let native_window_ptr = native_window.ptr().cast();
match initialize_android_rendering(
&instance,
gpu_resources.take(),
&mut app_shell,
&content,
&settings,
&android_frame_driver,
&host_window_registry,
native_window_ptr,
Some(native_window),
width,
height,
density,
) {
Ok((resources, actual_size)) => {
if let Some(actual_size) = actual_size {
current_host_window_size = actual_size;
}
gpu_resources = Some(resources);
log::info!(
"Android overlay surface ready at {}x{} px ({:.2}x density)",
width,
height,
density
);
}
Err(error) => {
log::error!(
"Android overlay surface initialization failed: {error}"
);
}
}
}
}
android_overlay_window::AndroidOverlayWindowEvent::SurfaceDestroyed => {
if overlay_window_options.is_some() {
gpu_resources = None;
}
}
android_overlay_window::AndroidOverlayWindowEvent::Pointer { action, x, y } => {
let logical = android_platform.pointer_position(x as f64, y as f64);
match action {
android_overlay_window::AndroidOverlayPointerAction::Down => {
pending_inputs.push(PendingInput::PointerDown(
logical.x as f32,
logical.y as f32,
None,
PointerSource::Touch,
));
}
android_overlay_window::AndroidOverlayPointerAction::Up
| android_overlay_window::AndroidOverlayPointerAction::Cancel => {
pending_inputs.push(PendingInput::PointerUp(
logical.x as f32,
logical.y as f32,
None,
PointerSource::Touch,
));
}
android_overlay_window::AndroidOverlayPointerAction::Move => {
pending_inputs.push(PendingInput::PointerMove(
logical.x as f32,
logical.y as f32,
None,
PointerSource::Touch,
));
}
}
}
}
}
if !soft_keyboard_installed {
if let Some(shell) = &mut app_shell {
shell.set_platform_text_input(Rc::new(AndroidSoftKeyboard::new(Rc::clone(
&ime_session,
))));
soft_keyboard_installed = true;
log::info!("Android soft keyboard focus hook installed");
let clipboard_app = app.clone();
shell.app_context().enter(move || {
cranpose_ui::clipboard_session::set_platform_clipboard(Rc::new(
crate::android_services::AndroidClipboard { app: clipboard_app },
));
});
if !shell.notify_app_resumed() {
ime_session.ensure_hidden();
log::info!("No focused field at launch; ensured soft keyboard hidden");
}
}
}
if let Some(shell) = &mut app_shell {
for event in ime_event_queue.drain() {
dispatch_android_ime_event(shell, event);
}
}
crate::android_services::apply_pending_platform_signals(
get_display_density(&app),
&mut app_shell,
);
if !pending_inputs.is_empty() {
if let Some(shell) = &mut app_shell {
for input in pending_inputs.drain(..) {
match input {
PendingInput::PointerDown(x, y, time_ms, source) => {
shell.set_pointer_source(source);
shell.set_cursor_at_time(x, y, time_ms);
shell.pointer_pressed_at_time(time_ms);
}
PendingInput::PointerUp(x, y, time_ms, source) => {
shell.set_pointer_source(source);
shell.pointer_released_at_position_time(x, y, time_ms);
}
PendingInput::PointerMove(x, y, time_ms, source) => {
shell.set_pointer_source(source);
shell.set_cursor_at_time(x, y, time_ms);
}
PendingInput::Key(event) => {
shell.on_key_event(&event);
}
PendingInput::SecondaryPointerDown(id, x, y, time_ms) => {
shell.set_pointer_source(PointerSource::Touch);
shell.secondary_pointer_pressed(id, x, y, time_ms);
}
PendingInput::SecondaryPointerUp(id, x, y, time_ms) => {
shell.set_pointer_source(PointerSource::Touch);
shell.secondary_pointer_released(id, x, y, time_ms);
}
PendingInput::SecondaryPointerMove(id, x, y, time_ms) => {
shell.set_pointer_source(PointerSource::Touch);
shell.secondary_pointer_moved(id, x, y, time_ms);
}
}
}
}
}
if ime_session.is_active() {
if let Some(shell) = &mut app_shell {
ime_session.sync_editor_state(shell.ime_editor_state());
}
}
if android_frame_driver.take_frame_request() {
if let Some(shell) = &mut app_shell {
shell.mark_dirty();
}
}
confirm_android_host_window_request(
&mut pending_host_window_confirmation,
current_host_window_size,
);
if should_exit.load(Ordering::Relaxed) {
log::info!("Exiting cleanly after Destroy event");
break;
}
if let (Some(resources), Some(shell)) = (&mut gpu_resources, &mut app_shell) {
if shell.needs_update() {
let update_result = android_host_window::with_android_host_window_registry(
&host_window_registry,
|| shell.update(),
);
dispatch_registered_android_surface_size_request(
&app,
&host_window_registry,
android_platform.scale_factor(),
overlay_window_options,
&mut last_dispatched_host_window_request,
&mut pending_host_window_confirmation,
);
if surface_present_required(
resources.surface_dirty,
update_result.visual_changed,
shell.needs_redraw(),
) && render_once(resources, shell)
{
break; }
}
}
}
}