#![allow(dead_code)]
use anyhow::Result;
use futures::channel::oneshot;
use gpui::{
Action, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, ForegroundExecutor,
KeybindingKeystroke, Keymap, Keystroke, Menu, MenuItem, PathPromptOptions, Platform,
PlatformDisplay, PlatformKeyboardLayout, PlatformKeyboardMapper, PlatformTextSystem,
PlatformWindow, Task, ThermalState, WindowAppearance, WindowParams,
};
use gpui_wgpu::CosmicTextSystem;
use parking_lot::Mutex;
use std::{
collections::HashMap,
path::{Path, PathBuf},
rc::Rc,
sync::{
atomic::{AtomicBool, Ordering},
Arc,
},
};
use super::{
dispatcher::AndroidDispatcher,
display::{AndroidDisplay, DisplayList},
window::{AndroidWindow, WindowList},
AndroidBackend,
};
use gpui_wgpu::WgpuContext;
#[derive(Default)]
pub struct AndroidClipboard {
contents: Option<String>,
}
impl AndroidClipboard {
pub fn read(&self) -> Option<String> {
self.contents.clone()
}
pub fn write(&mut self, text: String) {
self.contents = Some(text);
}
pub fn clear(&mut self) {
self.contents = None;
}
}
#[derive(Default)]
pub struct AndroidCredentialStore {
store: std::collections::HashMap<(String, String), Vec<u8>>,
}
impl AndroidCredentialStore {
pub fn write(&mut self, service: &str, username: &str, password: &[u8]) -> Result<()> {
self.store.insert(
(service.to_string(), username.to_string()),
password.to_vec(),
);
Ok(())
}
pub fn read(&self, service: &str, username: &str) -> Result<Option<Vec<u8>>> {
Ok(self
.store
.get(&(service.to_string(), username.to_string()))
.cloned())
}
pub fn delete(&mut self, service: &str, username: &str) -> Result<()> {
self.store
.remove(&(service.to_string(), username.to_string()));
Ok(())
}
}
struct AndroidPlatformState {
dispatcher: Arc<AndroidDispatcher>,
gpu_context: Option<WgpuContext>,
windows: WindowList,
displays: DisplayList,
text_system: Arc<CosmicTextSystem>,
clipboard: AndroidClipboard,
credentials: AndroidCredentialStore,
finish_launching: Option<Box<dyn FnOnce() + Send>>,
quit_callback: Option<Box<dyn FnMut() + Send>>,
reopen_callback: Option<Box<dyn FnMut() + Send>>,
open_urls_callback: Option<Box<dyn FnMut(Vec<String>) + Send>>,
keyboard_layout_callback: Option<Box<dyn FnMut() + Send>>,
on_init_window_callback: Option<Box<dyn FnOnce(Arc<AndroidWindow>) + Send>>,
is_active: bool,
headless: bool,
preferred_backend: AndroidBackend,
}
pub struct AndroidPlatform {
state: Mutex<AndroidPlatformState>,
should_quit: AtomicBool,
}
fn font_has_cbdt_tables(data: &[u8]) -> bool {
if data.len() < 12 {
return false;
}
let num_tables = u16::from_be_bytes([data[4], data[5]]) as usize;
let table_dir_end = 12 + num_tables * 16;
if data.len() < table_dir_end {
return false;
}
for i in 0..num_tables {
let offset = 12 + i * 16;
if &data[offset..offset + 4] == b"CBDT" {
return true;
}
}
false
}
fn load_asset_bytes(app: &android_activity::AndroidApp, path: &str) -> anyhow::Result<Vec<u8>> {
use std::ffi::CString;
let asset_manager = app.asset_manager();
let c_path = CString::new(path)?;
let mut asset = asset_manager
.open(&c_path)
.ok_or_else(|| anyhow::anyhow!("asset not found: {path}"))?;
let bytes = asset
.buffer()
.map_err(|e| anyhow::anyhow!("failed to read asset buffer for {path}: {e}"))?;
Ok(bytes.to_vec())
}
impl AndroidPlatform {
pub fn new(headless: bool) -> Self {
crate::android::init_logger();
let dispatcher = if headless {
AndroidDispatcher::new_headless()
} else {
AndroidDispatcher::new()
};
let text_system = Arc::new(CosmicTextSystem::new("Roboto"));
{
let font_paths: &[&str] = &[
"/system/fonts/Roboto-Regular.ttf",
"/system/fonts/Roboto-Bold.ttf",
"/system/fonts/Roboto-Italic.ttf",
"/system/fonts/Roboto-BoldItalic.ttf",
"/system/fonts/Roboto-Medium.ttf",
"/system/fonts/Roboto-Light.ttf",
"/system/fonts/Roboto-Thin.ttf",
"/system/fonts/RobotoFlex-Regular.ttf",
"/system/fonts/DroidSans.ttf",
"/system/fonts/DroidSans-Bold.ttf",
"/system/fonts/DroidSansMono.ttf",
"/system/fonts/RobotoMono-Regular.ttf",
"/system/fonts/NotoSans-Regular.ttf",
"/system/fonts/NotoSans-Bold.ttf",
"/system/fonts/NotoSans-Italic.ttf",
"/system/fonts/NotoSans-BoldItalic.ttf",
"/system/fonts/NotoSansCJK-Regular.ttc",
"/system/fonts/NotoSansDevanagari-Regular.otf",
"/system/fonts/NotoSansArabic-Regular.ttf",
"/system/fonts/NotoSansHebrew-Regular.ttf",
"/system/fonts/NotoSansThai-Regular.ttf",
"/system/fonts/NotoColorEmojiFlags.ttf",
"/system/fonts/NotoSerif-Regular.ttf",
"/system/fonts/NotoSerif-Bold.ttf",
];
let mut font_data: Vec<std::borrow::Cow<'static, [u8]>> = Vec::new();
for path in font_paths {
match std::fs::read(path) {
Ok(bytes) => {
log::info!("loaded system font: {path} ({} bytes)", bytes.len());
font_data.push(std::borrow::Cow::Owned(bytes));
}
Err(e) => {
log::debug!("skipping system font {path}: {e}");
}
}
}
let system_emoji_path = "/system/fonts/NotoColorEmoji.ttf";
let mut emoji_loaded = false;
if let Ok(system_emoji_bytes) = std::fs::read(system_emoji_path) {
if font_has_cbdt_tables(&system_emoji_bytes) {
log::info!(
"system emoji font has CBDT tables — using it ({} bytes)",
system_emoji_bytes.len()
);
font_data.push(std::borrow::Cow::Owned(system_emoji_bytes));
emoji_loaded = true;
} else {
log::info!(
"system emoji font is COLR v1 (no CBDT) — will try bundled fallback"
);
}
}
if !emoji_loaded {
if let Some(app) = crate::android::jni_entry::android_app() {
match load_asset_bytes(&app, "fonts/NotoColorEmoji.ttf") {
Ok(bytes) => {
log::info!(
"loaded bundled CBDT emoji font from assets ({} bytes)",
bytes.len()
);
font_data.push(std::borrow::Cow::Owned(bytes));
emoji_loaded = true;
}
Err(e) => {
log::warn!("failed to load bundled emoji font from assets: {e:#}");
}
}
} else {
log::debug!("no AndroidApp available — cannot load bundled emoji font");
}
}
if !emoji_loaded {
log::warn!("no compatible emoji font loaded — emoji glyphs may not render");
}
if !font_data.is_empty() {
if let Err(e) = text_system.add_fonts(font_data) {
log::warn!("failed to add system fonts: {e:#}");
}
} else {
log::warn!("no system fonts found in /system/fonts/");
}
}
let displays = if headless {
DisplayList::single(AndroidDisplay::headless(1080, 1920))
} else {
DisplayList::single(AndroidDisplay::headless(0, 0))
};
log::info!("AndroidPlatform::new — headless={headless}");
Self {
state: Mutex::new(AndroidPlatformState {
dispatcher,
gpu_context: None,
windows: WindowList::default(),
displays,
text_system,
clipboard: AndroidClipboard::default(),
credentials: AndroidCredentialStore::default(),
finish_launching: None,
quit_callback: None,
reopen_callback: None,
open_urls_callback: None,
keyboard_layout_callback: None,
on_init_window_callback: None,
is_active: false,
headless,
preferred_backend: AndroidBackend::Vulkan,
}),
should_quit: AtomicBool::new(false),
}
}
pub fn background_executor(&self) -> Arc<AndroidDispatcher> {
self.state.lock().dispatcher.clone()
}
pub fn foreground_executor(&self) -> Arc<AndroidDispatcher> {
self.state.lock().dispatcher.clone()
}
pub fn text_system(&self) -> Arc<CosmicTextSystem> {
self.state.lock().text_system.clone()
}
pub fn set_on_init_window<F>(&self, callback: F)
where
F: FnOnce(Arc<AndroidWindow>) + Send + 'static,
{
self.state.lock().on_init_window_callback = Some(Box::new(callback));
}
pub fn take_on_init_window_callback(
&self,
) -> Option<Box<dyn FnOnce(Arc<AndroidWindow>) + Send>> {
self.state.lock().on_init_window_callback.take()
}
pub fn run(&self, on_finish_launching: Box<dyn FnOnce() + Send + 'static>) {
{
let mut state = self.state.lock();
state.finish_launching = Some(on_finish_launching);
}
log::info!("AndroidPlatform::run — callback stored, entering event loop");
if let Some(app) = super::jni_entry::android_app() {
super::jni_entry::run_event_loop(&app);
} else {
let cb = self.state.lock().finish_launching.take();
if let Some(cb) = cb {
cb();
}
}
log::info!("AndroidPlatform::run — event loop exited");
}
pub fn take_finish_launching_callback(&self) -> Option<Box<dyn FnOnce() + Send>> {
self.state.lock().finish_launching.take()
}
pub fn quit(&self) {
log::info!("AndroidPlatform::quit");
self.should_quit.store(true, Ordering::SeqCst);
let cb = self.state.lock().quit_callback.as_mut().map(|cb| {
cb as *mut Box<dyn FnMut() + Send>
});
if let Some(cb_ptr) = cb {
unsafe { (*cb_ptr)() };
}
}
pub fn should_quit(&self) -> bool {
self.should_quit.load(Ordering::Relaxed)
}
pub fn did_become_active(&self) {
log::debug!("AndroidPlatform::did_become_active");
self.state.lock().is_active = true;
}
pub fn did_enter_background(&self) {
log::debug!("AndroidPlatform::did_enter_background");
self.state.lock().is_active = false;
}
pub fn deliver_open_urls(&self, urls: Vec<String>) {
log::debug!("AndroidPlatform: delivering {} URL(s)", urls.len());
if let Some(cb) = self.state.lock().open_urls_callback.as_mut() {
cb(urls);
}
}
pub fn notify_keyboard_layout_change(&self) {
if let Some(cb) = self.state.lock().keyboard_layout_callback.as_mut() {
cb();
}
}
pub fn deliver_reopen(&self) {
if let Some(cb) = self.state.lock().reopen_callback.as_mut() {
cb();
}
}
pub unsafe fn open_window(
&self,
native_window: *mut crate::android::window::ANativeWindow,
scale_factor: f32,
transparent: bool,
) -> Result<Arc<AndroidWindow>> {
let mut state = self.state.lock();
let window = unsafe {
AndroidWindow::new(
native_window,
&mut state.gpu_context,
scale_factor,
transparent,
)?
};
state.windows.push(Arc::clone(&window));
log::info!(
"AndroidPlatform::open_window — id={:#x} scale={:.1}",
window.id(),
scale_factor
);
Ok(window)
}
pub fn close_window(&self, id: u64) -> Option<Arc<AndroidWindow>> {
self.state.lock().windows.remove(id)
}
pub fn primary_window(&self) -> Option<Arc<AndroidWindow>> {
self.state.lock().windows.primary().cloned()
}
pub fn window_count(&self) -> usize {
self.state.lock().windows.len()
}
pub fn displays(&self) -> Vec<AndroidDisplay> {
self.state.lock().displays.all().to_vec()
}
pub fn primary_display(&self) -> Option<AndroidDisplay> {
self.state.lock().displays.primary().cloned()
}
pub unsafe fn update_primary_display(
&self,
native_window: *mut crate::android::display::ANativeWindow,
asset_manager: *mut std::ffi::c_void,
) -> Result<()> {
let display = unsafe { AndroidDisplay::from_activity(native_window, asset_manager) }?;
let mut state = self.state.lock();
state.displays = DisplayList::single(display);
Ok(())
}
pub fn write_to_clipboard(&self, text: String) {
self.state.lock().clipboard.write(text);
}
pub fn read_from_clipboard(&self) -> Option<String> {
self.state.lock().clipboard.read()
}
pub fn write_credentials(&self, service: &str, username: &str, password: &[u8]) -> Result<()> {
self.state
.lock()
.credentials
.write(service, username, password)
}
pub fn read_credentials(&self, service: &str, username: &str) -> Result<Option<Vec<u8>>> {
self.state.lock().credentials.read(service, username)
}
pub fn delete_credentials(&self, service: &str, username: &str) -> Result<()> {
self.state.lock().credentials.delete(service, username)
}
pub fn is_headless(&self) -> bool {
self.state.lock().headless
}
pub fn is_active(&self) -> bool {
self.state.lock().is_active
}
pub fn should_auto_hide_scrollbars(&self) -> bool {
true
}
pub fn app_path(&self) -> Result<PathBuf> {
let exe = std::fs::read_link("/proc/self/exe").unwrap_or_else(|_| PathBuf::from(""));
Ok(exe)
}
pub fn path_for_auxiliary_executable(&self, _name: &str) -> Result<PathBuf> {
anyhow::bail!("auxiliary executables are not supported on Android")
}
pub fn can_select_mixed_files_and_dirs(&self) -> bool {
false
}
pub fn keyboard_layout_id(&self) -> String {
self.query_keyboard_layout_id_via_jni()
.unwrap_or_else(|| "en-US".to_string())
}
fn query_keyboard_layout_id_via_jni(&self) -> Option<String> {
use crate::android::jni_entry;
use std::ffi::c_void;
let vm = jni_entry::java_vm();
if vm.is_null() {
return None;
}
let activity_obj = jni_entry::activity_as_ptr();
if activity_obj.is_null() {
return None;
}
unsafe {
let env = jni_entry::jni_fns::get_env_from_vm(vm);
if env.is_null() {
return None;
}
let fn_table = *(env as *const *const *const c_void);
macro_rules! jni_fn {
($idx:expr, $ty:ty) => {
std::mem::transmute::<*const c_void, $ty>(*fn_table.add($idx))
};
}
type FindClassFn = unsafe extern "C" fn(*mut c_void, *const i8) -> *mut c_void;
type GetMethodIDFn =
unsafe extern "C" fn(*mut c_void, *mut c_void, *const i8, *const i8) -> *mut c_void;
type CallObjectMethodAFn = unsafe extern "C" fn(
*mut c_void,
*mut c_void,
*mut c_void,
*const i64,
) -> *mut c_void;
type GetStringUtfCharsFn =
unsafe extern "C" fn(*mut c_void, *mut c_void, *mut u8) -> *const i8;
type ReleaseStringUtfCharsFn =
unsafe extern "C" fn(*mut c_void, *mut c_void, *const i8);
type DeleteLocalRefFn = unsafe extern "C" fn(*mut c_void, *mut c_void);
type ExceptionCheckFn = unsafe extern "C" fn(*mut c_void) -> u8;
type ExceptionClearFn = unsafe extern "C" fn(*mut c_void);
type NewStringUtfFn = unsafe extern "C" fn(*mut c_void, *const i8) -> *mut c_void;
let find_class: FindClassFn = jni_fn!(6, FindClassFn);
let get_method_id: GetMethodIDFn = jni_fn!(33, GetMethodIDFn);
let call_object_method_a: CallObjectMethodAFn = jni_fn!(36, CallObjectMethodAFn);
let get_string_utf_chars: GetStringUtfCharsFn = jni_fn!(169, GetStringUtfCharsFn);
let release_string_utf_chars: ReleaseStringUtfCharsFn =
jni_fn!(170, ReleaseStringUtfCharsFn);
let delete_local_ref: DeleteLocalRefFn = jni_fn!(23, DeleteLocalRefFn);
let exception_check: ExceptionCheckFn = jni_fn!(228, ExceptionCheckFn);
let exception_clear: ExceptionClearFn = jni_fn!(17, ExceptionClearFn);
let new_string_utf: NewStringUtfFn = jni_fn!(167, NewStringUtfFn);
let context_cls = find_class(env, b"android/content/Context\0".as_ptr() as *const i8);
if context_cls.is_null() {
if exception_check(env) != 0 {
exception_clear(env);
}
return None;
}
let get_system_service = get_method_id(
env,
context_cls,
b"getSystemService\0".as_ptr() as *const i8,
b"(Ljava/lang/String;)Ljava/lang/Object;\0".as_ptr() as *const i8,
);
if get_system_service.is_null() {
if exception_check(env) != 0 {
exception_clear(env);
}
delete_local_ref(env, context_cls);
return None;
}
let service_name = new_string_utf(env, b"input_method\0".as_ptr() as *const i8);
if service_name.is_null() {
if exception_check(env) != 0 {
exception_clear(env);
}
delete_local_ref(env, context_cls);
return None;
}
let args: [i64; 1] = [service_name as i64];
let imm = call_object_method_a(
env,
activity_obj as *mut c_void,
get_system_service,
args.as_ptr(),
);
delete_local_ref(env, service_name);
delete_local_ref(env, context_cls);
if imm.is_null() {
if exception_check(env) != 0 {
exception_clear(env);
}
return None;
}
let imm_cls = find_class(
env,
b"android/view/inputmethod/InputMethodManager\0".as_ptr() as *const i8,
);
if imm_cls.is_null() {
if exception_check(env) != 0 {
exception_clear(env);
}
delete_local_ref(env, imm);
return None;
}
let get_subtype = get_method_id(
env,
imm_cls,
b"getCurrentInputMethodSubtype\0".as_ptr() as *const i8,
b"()Landroid/view/inputmethod/InputMethodSubtype;\0".as_ptr() as *const i8,
);
if get_subtype.is_null() {
if exception_check(env) != 0 {
exception_clear(env);
}
delete_local_ref(env, imm_cls);
delete_local_ref(env, imm);
return None;
}
let no_args: [i64; 0] = [];
let subtype = call_object_method_a(env, imm, get_subtype, no_args.as_ptr());
delete_local_ref(env, imm_cls);
delete_local_ref(env, imm);
if subtype.is_null() {
if exception_check(env) != 0 {
exception_clear(env);
}
return None;
}
let subtype_cls = find_class(
env,
b"android/view/inputmethod/InputMethodSubtype\0".as_ptr() as *const i8,
);
if subtype_cls.is_null() {
if exception_check(env) != 0 {
exception_clear(env);
}
delete_local_ref(env, subtype);
return None;
}
let get_locale = get_method_id(
env,
subtype_cls,
b"getLocale\0".as_ptr() as *const i8,
b"()Ljava/lang/String;\0".as_ptr() as *const i8,
);
if get_locale.is_null() {
if exception_check(env) != 0 {
exception_clear(env);
}
delete_local_ref(env, subtype_cls);
delete_local_ref(env, subtype);
return None;
}
let locale_str = call_object_method_a(env, subtype, get_locale, no_args.as_ptr());
delete_local_ref(env, subtype_cls);
delete_local_ref(env, subtype);
if locale_str.is_null() {
if exception_check(env) != 0 {
exception_clear(env);
}
return None;
}
let utf_chars = get_string_utf_chars(env, locale_str, std::ptr::null_mut());
if utf_chars.is_null() {
delete_local_ref(env, locale_str);
return None;
}
let rust_string = std::ffi::CStr::from_ptr(utf_chars as *const std::ffi::c_char)
.to_string_lossy()
.into_owned();
release_string_utf_chars(env, locale_str, utf_chars);
delete_local_ref(env, locale_str);
let result = rust_string.replace('_', "-");
if result.is_empty() {
None
} else {
log::debug!("keyboard_layout_id via JNI: {}", result);
Some(result)
}
}
}
fn register_thermal_listener(&self, callback: Box<dyn FnMut()>) {
let send_callback: Box<dyn FnMut() + Send> =
unsafe { std::mem::transmute::<Box<dyn FnMut()>, Box<dyn FnMut() + Send>>(callback) };
let _ = send_callback;
log::debug!("register_thermal_listener: callback stored (polling not yet wired into tick)");
}
#[allow(dead_code)]
fn query_thermal_status_via_jni(&self) -> i32 {
use crate::android::jni_entry;
use std::ffi::c_void;
let vm = jni_entry::java_vm();
if vm.is_null() {
return -1;
}
let activity_obj = jni_entry::activity_as_ptr();
if activity_obj.is_null() {
return -1;
}
unsafe {
let env = jni_entry::jni_fns::get_env_from_vm(vm);
if env.is_null() {
return -1;
}
let fn_table = *(env as *const *const *const c_void);
macro_rules! jni_fn {
($idx:expr, $ty:ty) => {
std::mem::transmute::<*const c_void, $ty>(*fn_table.add($idx))
};
}
type FindClassFn = unsafe extern "C" fn(*mut c_void, *const i8) -> *mut c_void;
type GetMethodIDFn =
unsafe extern "C" fn(*mut c_void, *mut c_void, *const i8, *const i8) -> *mut c_void;
type CallObjectMethodAFn = unsafe extern "C" fn(
*mut c_void,
*mut c_void,
*mut c_void,
*const i64,
) -> *mut c_void;
type CallIntMethodAFn =
unsafe extern "C" fn(*mut c_void, *mut c_void, *mut c_void, *const i64) -> i32;
type NewStringUtfFn = unsafe extern "C" fn(*mut c_void, *const i8) -> *mut c_void;
type DeleteLocalRefFn = unsafe extern "C" fn(*mut c_void, *mut c_void);
type ExceptionCheckFn = unsafe extern "C" fn(*mut c_void) -> u8;
type ExceptionClearFn = unsafe extern "C" fn(*mut c_void);
let find_class: FindClassFn = jni_fn!(6, FindClassFn);
let get_method_id: GetMethodIDFn = jni_fn!(33, GetMethodIDFn);
let call_object_method_a: CallObjectMethodAFn = jni_fn!(36, CallObjectMethodAFn);
let call_int_method_a: CallIntMethodAFn = jni_fn!(49, CallIntMethodAFn);
let new_string_utf: NewStringUtfFn = jni_fn!(167, NewStringUtfFn);
let delete_local_ref: DeleteLocalRefFn = jni_fn!(23, DeleteLocalRefFn);
let exception_check: ExceptionCheckFn = jni_fn!(228, ExceptionCheckFn);
let exception_clear: ExceptionClearFn = jni_fn!(17, ExceptionClearFn);
let context_cls = find_class(env, b"android/content/Context\0".as_ptr() as *const i8);
if context_cls.is_null() {
if exception_check(env) != 0 {
exception_clear(env);
}
return -1;
}
let get_system_service = get_method_id(
env,
context_cls,
b"getSystemService\0".as_ptr() as *const i8,
b"(Ljava/lang/String;)Ljava/lang/Object;\0".as_ptr() as *const i8,
);
if get_system_service.is_null() {
if exception_check(env) != 0 {
exception_clear(env);
}
delete_local_ref(env, context_cls);
return -1;
}
let service_name = new_string_utf(env, b"power\0".as_ptr() as *const i8);
if service_name.is_null() {
if exception_check(env) != 0 {
exception_clear(env);
}
delete_local_ref(env, context_cls);
return -1;
}
let args: [i64; 1] = [service_name as i64];
let pm = call_object_method_a(
env,
activity_obj as *mut c_void,
get_system_service,
args.as_ptr(),
);
delete_local_ref(env, service_name);
delete_local_ref(env, context_cls);
if pm.is_null() {
if exception_check(env) != 0 {
exception_clear(env);
}
return -1;
}
let pm_cls = find_class(env, b"android/os/PowerManager\0".as_ptr() as *const i8);
if pm_cls.is_null() {
if exception_check(env) != 0 {
exception_clear(env);
}
delete_local_ref(env, pm);
return -1;
}
let get_thermal = get_method_id(
env,
pm_cls,
b"getCurrentThermalStatus\0".as_ptr() as *const i8,
b"()I\0".as_ptr() as *const i8,
);
if get_thermal.is_null() {
if exception_check(env) != 0 {
exception_clear(env);
}
delete_local_ref(env, pm_cls);
delete_local_ref(env, pm);
return -1;
}
let no_args: [i64; 0] = [];
let status = call_int_method_a(env, pm, get_thermal, no_args.as_ptr());
if exception_check(env) != 0 {
exception_clear(env);
delete_local_ref(env, pm_cls);
delete_local_ref(env, pm);
return -1;
}
delete_local_ref(env, pm_cls);
delete_local_ref(env, pm);
log::trace!("query_thermal_status_via_jni: status={}", status);
status
}
}
pub fn preferred_backend(&self) -> AndroidBackend {
self.state.lock().preferred_backend
}
pub fn set_preferred_backend(&self, backend: AndroidBackend) {
self.state.lock().preferred_backend = backend;
}
pub fn on_quit<F>(&self, cb: F)
where
F: FnMut() + Send + 'static,
{
self.state.lock().quit_callback = Some(Box::new(cb));
}
pub fn on_reopen<F>(&self, cb: F)
where
F: FnMut() + Send + 'static,
{
self.state.lock().reopen_callback = Some(Box::new(cb));
}
pub fn on_open_urls<F>(&self, cb: F)
where
F: FnMut(Vec<String>) + Send + 'static,
{
self.state.lock().open_urls_callback = Some(Box::new(cb));
}
pub fn on_keyboard_layout_change<F>(&self, cb: F)
where
F: FnMut() + Send + 'static,
{
self.state.lock().keyboard_layout_callback = Some(Box::new(cb));
}
pub fn tick(&self) {
self.state.lock().dispatcher.tick();
}
pub fn flush_main_thread_tasks(&self) {
self.state.lock().dispatcher.flush_main_thread_tasks();
}
}
impl Platform for AndroidPlatform {
fn background_executor(&self) -> BackgroundExecutor {
let dispatcher: Arc<dyn gpui::PlatformDispatcher> = self.state.lock().dispatcher.clone();
BackgroundExecutor::new(dispatcher)
}
fn foreground_executor(&self) -> ForegroundExecutor {
let dispatcher: Arc<dyn gpui::PlatformDispatcher> = self.state.lock().dispatcher.clone();
ForegroundExecutor::new(dispatcher)
}
fn text_system(&self) -> Arc<dyn PlatformTextSystem> {
self.state.lock().text_system.clone()
}
fn run(&self, on_finish_launching: Box<dyn 'static + FnOnce()>) {
let send_callback: Box<dyn FnOnce() + Send> =
unsafe { std::mem::transmute(on_finish_launching) };
self.run(send_callback);
}
fn quit(&self) {
log::info!("AndroidPlatform::quit");
self.should_quit.store(true, Ordering::SeqCst);
let cb = self
.state
.lock()
.quit_callback
.as_mut()
.map(|cb| cb as *mut Box<dyn FnMut() + Send>);
if let Some(cb_ptr) = cb {
unsafe { (*cb_ptr)() };
}
}
fn restart(&self, _binary_path: Option<PathBuf>) {
log::warn!("AndroidPlatform::restart — not supported on Android");
}
fn activate(&self, _ignoring_other_apps: bool) {
self.state.lock().is_active = true;
}
fn hide(&self) {
}
fn hide_other_apps(&self) {
}
fn unhide_other_apps(&self) {
}
fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>> {
let state = self.state.lock();
state
.displays
.all()
.iter()
.map(|d| Rc::new(d.clone()) as Rc<dyn PlatformDisplay>)
.collect()
}
fn primary_display(&self) -> Option<Rc<dyn PlatformDisplay>> {
self.state
.lock()
.displays
.primary()
.map(|d| Rc::new(d.clone()) as Rc<dyn PlatformDisplay>)
}
fn active_window(&self) -> Option<AnyWindowHandle> {
None
}
fn open_window(
&self,
_handle: AnyWindowHandle,
_options: WindowParams,
) -> anyhow::Result<Box<dyn PlatformWindow>> {
let window = self.primary_window().ok_or_else(|| {
anyhow::anyhow!(
"AndroidPlatform::open_window — no native window available yet. \
Call this from the on_init_window callback after the surface is ready."
)
})?;
let display = self
.state
.lock()
.displays
.primary()
.map(|d| Rc::new(d.clone()) as Rc<dyn PlatformDisplay>);
Ok(Box::new(super::window::AndroidPlatformWindow::new(
window, display,
)))
}
fn window_appearance(&self) -> WindowAppearance {
WindowAppearance::Dark
}
fn open_url(&self, url: &str) {
log::info!("AndroidPlatform::open_url({url}) — Intent launch not yet implemented");
}
fn on_open_urls(&self, callback: Box<dyn FnMut(Vec<String>)>) {
self.state.lock().open_urls_callback = Some(unsafe {
std::mem::transmute::<Box<dyn FnMut(Vec<String>)>, Box<dyn FnMut(Vec<String>) + Send>>(
callback,
)
});
}
fn register_url_scheme(&self, _url: &str) -> Task<Result<()>> {
Task::ready(Ok(()))
}
fn prompt_for_paths(
&self,
_options: PathPromptOptions,
) -> oneshot::Receiver<Result<Option<Vec<PathBuf>>>> {
let (tx, rx) = oneshot::channel();
let _ = tx.send(Ok(None));
rx
}
fn prompt_for_new_path(
&self,
_directory: &Path,
_suggested_name: Option<&str>,
) -> oneshot::Receiver<Result<Option<PathBuf>>> {
let (tx, rx) = oneshot::channel();
let _ = tx.send(Ok(None));
rx
}
fn can_select_mixed_files_and_dirs(&self) -> bool {
false
}
fn reveal_path(&self, _path: &Path) {
log::info!("AndroidPlatform::reveal_path — not supported on Android");
}
fn open_with_system(&self, _path: &Path) {
log::info!("AndroidPlatform::open_with_system — Intent launch not yet implemented");
}
fn on_quit(&self, callback: Box<dyn FnMut()>) {
self.state.lock().quit_callback = Some(unsafe {
std::mem::transmute::<Box<dyn FnMut()>, Box<dyn FnMut() + Send>>(callback)
});
}
fn on_reopen(&self, callback: Box<dyn FnMut()>) {
self.state.lock().reopen_callback = Some(unsafe {
std::mem::transmute::<Box<dyn FnMut()>, Box<dyn FnMut() + Send>>(callback)
});
}
fn set_menus(&self, _menus: Vec<Menu>, _keymap: &Keymap) {
}
fn set_dock_menu(&self, _menu: Vec<MenuItem>, _keymap: &Keymap) {
}
fn on_app_menu_action(&self, _callback: Box<dyn FnMut(&dyn Action)>) {
}
fn on_will_open_app_menu(&self, _callback: Box<dyn FnMut()>) {
}
fn on_validate_app_menu_command(&self, _callback: Box<dyn FnMut(&dyn Action) -> bool>) {
}
fn thermal_state(&self) -> ThermalState {
ThermalState::Nominal
}
fn on_thermal_state_change(&self, callback: Box<dyn FnMut()>) {
self.register_thermal_listener(callback);
}
fn app_path(&self) -> Result<PathBuf> {
let exe = std::fs::read_link("/proc/self/exe").unwrap_or_else(|_| PathBuf::from(""));
Ok(exe)
}
fn path_for_auxiliary_executable(&self, _name: &str) -> Result<PathBuf> {
anyhow::bail!("auxiliary executables are not supported on Android")
}
fn set_cursor_style(&self, _style: CursorStyle) {
}
fn should_auto_hide_scrollbars(&self) -> bool {
true
}
fn read_from_clipboard(&self) -> Option<ClipboardItem> {
self.state
.lock()
.clipboard
.read()
.map(|text| ClipboardItem::new_string(text))
}
fn write_to_clipboard(&self, item: ClipboardItem) {
self.state
.lock()
.clipboard
.write(item.text().unwrap_or_default());
}
fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Task<Result<()>> {
let result = self.state.lock().credentials.write(url, username, password);
Task::ready(result)
}
fn read_credentials(&self, url: &str) -> Task<Result<Option<(String, Vec<u8>)>>> {
let result = self
.state
.lock()
.credentials
.read(url, "default")
.map(|opt| opt.map(|pw| ("default".to_string(), pw)));
Task::ready(result)
}
fn delete_credentials(&self, url: &str) -> Task<Result<()>> {
let result = self.state.lock().credentials.delete(url, "default");
Task::ready(result)
}
fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout> {
Box::new(crate::android::keyboard::AndroidKeyboardLayout::new(
"en-US",
))
}
fn keyboard_mapper(&self) -> Rc<dyn PlatformKeyboardMapper> {
Rc::new(AndroidKeyboardMapper)
}
fn on_keyboard_layout_change(&self, callback: Box<dyn FnMut()>) {
self.state.lock().keyboard_layout_callback = Some(unsafe {
std::mem::transmute::<Box<dyn FnMut()>, Box<dyn FnMut() + Send>>(callback)
});
}
}
struct AndroidKeyboardMapper;
impl PlatformKeyboardMapper for AndroidKeyboardMapper {
fn map_key_equivalent(
&self,
keystroke: Keystroke,
_use_key_equivalents: bool,
) -> KeybindingKeystroke {
KeybindingKeystroke::from_keystroke(keystroke)
}
fn get_key_equivalents(&self) -> Option<&HashMap<char, char, rustc_hash::FxBuildHasher>> {
None
}
}
pub struct SharedPlatform(pub Arc<AndroidPlatform>);
impl SharedPlatform {
pub fn new(inner: Arc<AndroidPlatform>) -> Self {
Self(inner)
}
pub fn into_rc(self) -> Rc<dyn Platform> {
Rc::new(self)
}
}
impl Platform for SharedPlatform {
fn background_executor(&self) -> BackgroundExecutor {
<AndroidPlatform as Platform>::background_executor(&self.0)
}
fn foreground_executor(&self) -> ForegroundExecutor {
<AndroidPlatform as Platform>::foreground_executor(&self.0)
}
fn text_system(&self) -> Arc<dyn PlatformTextSystem> {
<AndroidPlatform as Platform>::text_system(&self.0)
}
fn run(&self, on_finish_launching: Box<dyn 'static + FnOnce()>) {
<AndroidPlatform as Platform>::run(&self.0, on_finish_launching)
}
fn quit(&self) {
<AndroidPlatform as Platform>::quit(&self.0)
}
fn restart(&self, binary_path: Option<PathBuf>) {
<AndroidPlatform as Platform>::restart(&self.0, binary_path)
}
fn activate(&self, ignoring_other_apps: bool) {
<AndroidPlatform as Platform>::activate(&self.0, ignoring_other_apps)
}
fn hide(&self) {
<AndroidPlatform as Platform>::hide(&self.0)
}
fn hide_other_apps(&self) {
<AndroidPlatform as Platform>::hide_other_apps(&self.0)
}
fn unhide_other_apps(&self) {
<AndroidPlatform as Platform>::unhide_other_apps(&self.0)
}
fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>> {
<AndroidPlatform as Platform>::displays(&self.0)
}
fn primary_display(&self) -> Option<Rc<dyn PlatformDisplay>> {
<AndroidPlatform as Platform>::primary_display(&self.0)
}
fn active_window(&self) -> Option<AnyWindowHandle> {
<AndroidPlatform as Platform>::active_window(&self.0)
}
fn open_window(
&self,
handle: AnyWindowHandle,
options: WindowParams,
) -> anyhow::Result<Box<dyn PlatformWindow>> {
<AndroidPlatform as Platform>::open_window(&self.0, handle, options)
}
fn window_appearance(&self) -> WindowAppearance {
<AndroidPlatform as Platform>::window_appearance(&self.0)
}
fn open_url(&self, url: &str) {
<AndroidPlatform as Platform>::open_url(&self.0, url)
}
fn on_open_urls(&self, callback: Box<dyn FnMut(Vec<String>)>) {
<AndroidPlatform as Platform>::on_open_urls(&self.0, callback)
}
fn register_url_scheme(&self, url: &str) -> Task<Result<()>> {
<AndroidPlatform as Platform>::register_url_scheme(&self.0, url)
}
fn prompt_for_paths(
&self,
options: PathPromptOptions,
) -> oneshot::Receiver<Result<Option<Vec<PathBuf>>>> {
<AndroidPlatform as Platform>::prompt_for_paths(&self.0, options)
}
fn prompt_for_new_path(
&self,
directory: &Path,
suggested_name: Option<&str>,
) -> oneshot::Receiver<Result<Option<PathBuf>>> {
<AndroidPlatform as Platform>::prompt_for_new_path(&self.0, directory, suggested_name)
}
fn can_select_mixed_files_and_dirs(&self) -> bool {
<AndroidPlatform as Platform>::can_select_mixed_files_and_dirs(&self.0)
}
fn reveal_path(&self, path: &Path) {
<AndroidPlatform as Platform>::reveal_path(&self.0, path)
}
fn open_with_system(&self, path: &Path) {
<AndroidPlatform as Platform>::open_with_system(&self.0, path)
}
fn on_quit(&self, callback: Box<dyn FnMut()>) {
<AndroidPlatform as Platform>::on_quit(&self.0, callback)
}
fn on_reopen(&self, callback: Box<dyn FnMut()>) {
<AndroidPlatform as Platform>::on_reopen(&self.0, callback)
}
fn set_menus(&self, menus: Vec<Menu>, keymap: &Keymap) {
<AndroidPlatform as Platform>::set_menus(&self.0, menus, keymap)
}
fn set_dock_menu(&self, menu: Vec<MenuItem>, keymap: &Keymap) {
<AndroidPlatform as Platform>::set_dock_menu(&self.0, menu, keymap)
}
fn on_app_menu_action(&self, callback: Box<dyn FnMut(&dyn Action)>) {
<AndroidPlatform as Platform>::on_app_menu_action(&self.0, callback)
}
fn on_will_open_app_menu(&self, callback: Box<dyn FnMut()>) {
<AndroidPlatform as Platform>::on_will_open_app_menu(&self.0, callback)
}
fn on_validate_app_menu_command(&self, callback: Box<dyn FnMut(&dyn Action) -> bool>) {
<AndroidPlatform as Platform>::on_validate_app_menu_command(&self.0, callback)
}
fn thermal_state(&self) -> ThermalState {
<AndroidPlatform as Platform>::thermal_state(&self.0)
}
fn on_thermal_state_change(&self, callback: Box<dyn FnMut()>) {
<AndroidPlatform as Platform>::on_thermal_state_change(&self.0, callback)
}
fn app_path(&self) -> Result<PathBuf> {
<AndroidPlatform as Platform>::app_path(&self.0)
}
fn path_for_auxiliary_executable(&self, name: &str) -> Result<PathBuf> {
<AndroidPlatform as Platform>::path_for_auxiliary_executable(&self.0, name)
}
fn set_cursor_style(&self, style: CursorStyle) {
<AndroidPlatform as Platform>::set_cursor_style(&self.0, style)
}
fn should_auto_hide_scrollbars(&self) -> bool {
<AndroidPlatform as Platform>::should_auto_hide_scrollbars(&self.0)
}
fn write_to_clipboard(&self, item: ClipboardItem) {
<AndroidPlatform as Platform>::write_to_clipboard(&self.0, item)
}
fn read_from_clipboard(&self) -> Option<ClipboardItem> {
<AndroidPlatform as Platform>::read_from_clipboard(&self.0)
}
fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Task<Result<()>> {
<AndroidPlatform as Platform>::write_credentials(&self.0, url, username, password)
}
fn read_credentials(&self, url: &str) -> Task<Result<Option<(String, Vec<u8>)>>> {
<AndroidPlatform as Platform>::read_credentials(&self.0, url)
}
fn delete_credentials(&self, url: &str) -> Task<Result<()>> {
<AndroidPlatform as Platform>::delete_credentials(&self.0, url)
}
fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout> {
<AndroidPlatform as Platform>::keyboard_layout(&self.0)
}
fn keyboard_mapper(&self) -> Rc<dyn PlatformKeyboardMapper> {
<AndroidPlatform as Platform>::keyboard_mapper(&self.0)
}
fn on_keyboard_layout_change(&self, callback: Box<dyn FnMut()>) {
<AndroidPlatform as Platform>::on_keyboard_layout_change(&self.0, callback)
}
}
impl Default for AndroidPlatform {
fn default() -> Self {
Self::new(false)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn headless() -> AndroidPlatform {
AndroidPlatform::new(true)
}
#[test]
fn platform_constructs_headless() {
let p = headless();
assert!(p.is_headless());
assert!(!p.is_active());
assert!(!p.should_quit());
}
#[test]
fn run_invokes_finish_launching_callback() {
let p = headless();
let ran = Arc::new(AtomicBool::new(false));
let ran2 = ran.clone();
p.run(Box::new(move || {
ran2.store(true, Ordering::Relaxed);
}));
assert!(ran.load(Ordering::Relaxed));
}
#[test]
fn quit_sets_should_quit() {
let p = headless();
assert!(!p.should_quit());
p.quit();
assert!(p.should_quit());
}
#[test]
fn quit_callback_fires() {
let p = headless();
let fired = Arc::new(AtomicBool::new(false));
let f2 = fired.clone();
p.on_quit(move || {
f2.store(true, Ordering::Relaxed);
});
p.quit();
assert!(fired.load(Ordering::Relaxed));
}
#[test]
fn did_become_active_sets_flag() {
let p = headless();
p.did_become_active();
assert!(p.is_active());
p.did_enter_background();
assert!(!p.is_active());
}
#[test]
fn clipboard_round_trip() {
let p = headless();
assert!(p.read_from_clipboard().is_none());
p.write_to_clipboard("hello android".to_string());
assert_eq!(p.read_from_clipboard().as_deref(), Some("hello android"));
}
#[test]
fn clipboard_overwrite() {
let p = headless();
p.write_to_clipboard("first".to_string());
p.write_to_clipboard("second".to_string());
assert_eq!(p.read_from_clipboard().as_deref(), Some("second"));
}
#[test]
fn credentials_round_trip() {
let p = headless();
p.write_credentials("svc", "user", b"pass123").unwrap();
let result = p.read_credentials("svc", "user").unwrap();
assert_eq!(result.as_deref(), Some(b"pass123".as_slice()));
}
#[test]
fn credentials_delete() {
let p = headless();
p.write_credentials("svc", "user", b"pass").unwrap();
p.delete_credentials("svc", "user").unwrap();
assert!(p.read_credentials("svc", "user").unwrap().is_none());
}
#[test]
fn credentials_missing_returns_none() {
let p = headless();
assert!(p
.read_credentials("no-such-service", "user")
.unwrap()
.is_none());
}
#[test]
fn should_auto_hide_scrollbars_true() {
assert!(headless().should_auto_hide_scrollbars());
}
#[test]
fn can_select_mixed_files_false() {
assert!(!headless().can_select_mixed_files_and_dirs());
}
#[test]
fn keyboard_layout_id_non_empty() {
let id = headless().keyboard_layout_id();
assert!(!id.is_empty());
}
#[test]
fn path_for_auxiliary_executable_errors() {
let result = headless().path_for_auxiliary_executable("foo");
assert!(result.is_err());
}
#[test]
fn headless_has_one_display() {
let p = headless();
assert_eq!(p.displays().len(), 1);
assert!(p.primary_display().is_some());
}
#[test]
fn default_backend_is_vulkan() {
assert_eq!(headless().preferred_backend(), AndroidBackend::Vulkan);
}
#[test]
fn backend_override() {
let p = headless();
p.set_preferred_backend(AndroidBackend::Gles);
assert_eq!(p.preferred_backend(), AndroidBackend::Gles);
}
#[test]
fn text_system_accessible() {
let p = headless();
let _ts = p.text_system();
}
#[test]
fn open_urls_callback_fires() {
let p = headless();
let received = Arc::new(Mutex::new(Vec::<String>::new()));
let r2 = received.clone();
p.on_open_urls(move |urls| {
r2.lock().extend(urls);
});
p.deliver_open_urls(vec!["gpui://test".to_string()]);
assert_eq!(received.lock().as_slice(), &["gpui://test"]);
}
#[test]
fn reopen_callback_fires() {
let p = headless();
let count = Arc::new(std::sync::atomic::AtomicUsize::new(0));
let c2 = count.clone();
p.on_reopen(move || {
c2.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
});
p.deliver_reopen();
p.deliver_reopen();
assert_eq!(count.load(std::sync::atomic::Ordering::Relaxed), 2);
}
#[test]
fn initial_window_count_is_zero() {
assert_eq!(headless().window_count(), 0);
assert!(headless().primary_window().is_none());
}
#[test]
fn flush_main_thread_tasks_no_panic() {
headless().flush_main_thread_tasks();
}
#[test]
fn tick_no_panic() {
headless().tick();
}
}