diff --git a/src/android/host.rs b/src/android/host.rs index f6b9c53..816684a 100644 --- a/src/android/host.rs +++ b/src/android/host.rs @@ -38,7 +38,10 @@ //! callback — so a host may stack several AI screens and each returns to its own //! panel. Reuse the same id across an Activity's configuration changes; pick a fresh //! one for a new instance. -//! 3. From `onResume` / `onPause`: [`resumed`] / [`paused`]. +//! 3. From `onResume` / `onPause`: [`resumed`] / [`paused`]. An Activity that +//! lists `uiMode` in `configChanges` must also call [`configuration_changed`] from +//! `onConfigurationChanged` with `newConfig.uiMode`, or its window keeps the old +//! night mode until resumed. //! 4. From `onTouchEvent`, `dispatchKeyEvent` and the `InputConnection`: //! [`motion_event`], [`key`], [`ime_event`]. //! @@ -121,6 +124,9 @@ enum Command { Paused { host: HostId, }, + ConfigurationChanged { + ui_mode: i32, + }, /// Input arrives on the Java UI thread but GPUI may only be touched from the /// render thread, so both go through the queue like everything else. Touch(TouchPoint), @@ -372,6 +378,13 @@ fn render_thread(launch: Launch) { if let Some(win) = app.windows.get(&host) { win.set_active(true); } + // Night mode may have changed while another app was in front. + platform.refresh_appearance(); + } + Command::ConfigurationChanged { ui_mode } => { + platform.notify_keyboard_layout_change(); + // Stacked hosts' windows follow too. + platform.set_appearance(super::jni::appearance_from_ui_mode(ui_mode)); } Command::Paused { host } => { platform.did_enter_background(); @@ -454,8 +467,7 @@ fn on_surface_created( // This host has been here before — a recreated or resumed Activity. Re-attach // its own window, so it comes back to the panel it left rather than whichever // one happened to run last. - let gpu = platform.gpu_context(); - match existing.init_window(window, gpu) { + match platform.reinit_window(&existing, window) { Ok(()) => { CURRENT_SURFACE.store(incoming, Ordering::SeqCst); app.attached = Some(host); @@ -668,6 +680,13 @@ pub fn ime_event(session: u64, kind: i32, text: String, start: usize, end: usize }); } +/// Call from `Activity.onConfigurationChanged` with `newConfig.uiMode`, for an +/// Activity that handles configuration changes (`uiMode` for night mode) instead of +/// being recreated. Applies that night mode to every host's window. +pub fn configuration_changed(ui_mode: i32) { + post(Command::ConfigurationChanged { ui_mode }); +} + pub fn resumed(host: HostId) { post(Command::Resumed { host }); } diff --git a/src/android/jni.rs b/src/android/jni.rs index 03ff1df..8312d81 100644 --- a/src/android/jni.rs +++ b/src/android/jni.rs @@ -292,6 +292,10 @@ static HOST_ACTIVITY: Mutex = Mutex::new(HostActivity { previous: None, }); +/// Application context kept independently of the newest Activity. Its Resources +/// follow system configuration, without an Activity's local night-mode override. +static HOST_CONTEXT: OnceLock>> = OnceLock::new(); + struct HostActivity { current: Option>>, previous: Option>>, @@ -326,6 +330,22 @@ fn register_host_activity(env: &mut jni::Env<'_>, activity: &JObject<'_>) -> Res vm.get_raw() as *mut c_void, std::sync::atomic::Ordering::SeqCst, ); + if HOST_CONTEXT.get().is_none() { + let context = env + .with_local_frame(1, |env| { + let context = env + .call_method( + activity, + jni::jni_str!("getApplicationContext"), + jni::jni_sig!("()Landroid/content/Context;"), + &[], + )? + .l()?; + env.new_global_ref(&context) + }) + .e()?; + let _ = HOST_CONTEXT.set(context); + } let mut slot = HOST_ACTIVITY.lock().expect("poisoned"); slot.previous = slot.current.take(); slot.current = Some(global); @@ -495,25 +515,93 @@ const AMOTION_EVENT_ACTION_UP: u32 = 1; const AMOTION_EVENT_ACTION_MOVE: u32 = 2; const AMOTION_EVENT_ACTION_CANCEL: u32 = 3; -// ── night mode query via NDK Configuration ─────────────────────────────────── +// ── night mode query ───────────────────────────────────────────────────────── -/// Query the current night mode using the NDK Configuration API. +/// Query the current night mode. /// /// Returns `true` if the system is in dark mode. pub fn query_night_mode_via_jni() -> bool { - let app = match android_app() { - Some(app) => app, - None => return false, + let is_dark = if let Some(app) = android_app() { + // Build an ndk::configuration::Configuration from the app's asset manager. + let config = ndk::configuration::Configuration::from_asset_manager(&app.asset_manager()); + config.ui_mode_night() == ndk::configuration::UiModeNight::Yes + } else { + // No AndroidApp on the host-driven path: query the application context, + // independent of the last Activity registered or its local night mode. + match host_ui_mode() { + Ok(ui_mode) => is_night(ui_mode), + Err(err) => { + log::warn!("query_night_mode: cannot read uiMode, assuming Light: {err}"); + false + } + } }; - // Build an ndk::configuration::Configuration from the app's asset manager. - let config = ndk::configuration::Configuration::from_asset_manager(&app.asset_manager()); - let is_dark = config.ui_mode_night() == ndk::configuration::UiModeNight::Yes; - - log::debug!("query_night_mode (ndk): is_dark={}", is_dark); + log::debug!("query_night_mode: is_dark={}", is_dark); is_dark } +/// `Configuration.UI_MODE_NIGHT_MASK` / `UI_MODE_NIGHT_YES`. +const UI_MODE_NIGHT_MASK: i32 = 0x30; +const UI_MODE_NIGHT_YES: i32 = 0x20; + +fn is_night(ui_mode: i32) -> bool { + ui_mode & UI_MODE_NIGHT_MASK == UI_MODE_NIGHT_YES +} + +/// `getResources().getConfiguration().uiMode` of the application context. +fn host_ui_mode() -> Result { + with_env(|env| { + let context = HOST_CONTEXT + .get() + .ok_or("Application context not available")?; + // A local frame frees `resources` and `config`: the render thread never + // returns to Java. + let result = env.with_local_frame(2, |env| { + let resources = env + .call_method( + context, + jni::jni_str!("getResources"), + jni::jni_sig!("()Landroid/content/res/Resources;"), + &[], + )? + .l()?; + let config = env + .call_method( + &resources, + jni::jni_str!("getConfiguration"), + jni::jni_sig!("()Landroid/content/res/Configuration;"), + &[], + )? + .l()?; + env.get_field(&config, jni::jni_str!("uiMode"), jni::jni_sig!("I"))? + .i() + }); + if result.is_err() { + env.exception_clear(); + } + result.e() + }) +} + +/// The system night mode as a window appearance. +pub(crate) fn query_appearance() -> crate::android::window::WindowAppearance { + appearance(query_night_mode_via_jni()) +} + +/// The appearance a `Configuration.uiMode` asks for. +pub(crate) fn appearance_from_ui_mode(ui_mode: i32) -> crate::android::window::WindowAppearance { + appearance(is_night(ui_mode)) +} + +fn appearance(is_dark: bool) -> crate::android::window::WindowAppearance { + if is_dark { + crate::android::window::WindowAppearance::Dark + } else { + crate::android::window::WindowAppearance::Light + } +} + // ── input event processing ──────────────────────────────────────────────────── /// Process input events from the `AndroidApp` and dispatch them to the window. @@ -808,8 +896,7 @@ pub fn run_event_loop(app: &AndroidApp) { .unwrap_or(1.0); if let Some(existing) = platform.primary_window() { - let gpu_ctx = platform.gpu_context(); - match existing.init_window(native_window, gpu_ctx) { + match platform.reinit_window(&existing, native_window) { Ok(()) => { log::info!("InitWindow: reinitialised existing window"); } @@ -869,15 +956,7 @@ pub fn run_event_loop(app: &AndroidApp) { log::debug!("deferred: ConfigChanged"); if let Some(platform) = PLATFORM.get() { platform.notify_keyboard_layout_change(); - let is_dark = query_night_mode_via_jni(); - if let Some(win) = platform.primary_window() { - let appearance = if is_dark { - crate::android::window::WindowAppearance::Dark - } else { - crate::android::window::WindowAppearance::Light - }; - win.set_appearance(appearance); - } + platform.refresh_appearance(); } } diff --git a/src/android/platform.rs b/src/android/platform.rs index eaafa6e..419f3db 100644 --- a/src/android/platform.rs +++ b/src/android/platform.rs @@ -218,6 +218,11 @@ pub struct AndroidPlatform { state: Mutex, /// Set to `true` when `quit()` is called; the main loop checks this. should_quit: AtomicBool, + /// The system night mode, as last applied to every window. What + /// `Platform::window_appearance` answers, so it agrees with each window's + /// `appearance()` and costs no JNI call. Not in `state`: GPUI asks for it from + /// main-thread tasks, which `flush_main_thread_tasks` runs with `state` locked. + appearance: Mutex, } /// Check whether a TrueType/OpenType font file contains CBDT (Color Bitmap @@ -486,6 +491,7 @@ impl AndroidPlatform { preferred_backend: AndroidBackend::Vulkan, }), should_quit: AtomicBool::new(false), + appearance: Mutex::default(), } } @@ -646,22 +652,54 @@ impl AndroidPlatform { scale_factor: f32, transparent: bool, ) -> Result> { - let mut state = self.state.lock(); - let window = AndroidWindow::new( - native_window, - Rc::clone(&state.gpu_context), - scale_factor, - transparent, - )?; - state.windows.push(Arc::clone(&window)); + let window = { + let mut state = self.state.lock(); + let window = AndroidWindow::new( + native_window, + Rc::clone(&state.gpu_context), + scale_factor, + transparent, + )?; + state.windows.push(Arc::clone(&window)); + window + }; log::info!( "AndroidPlatform::open_window — id={:#x} scale={:.1}", window.id(), scale_factor ); + self.refresh_appearance(); Ok(window) } + /// Give an existing window a new surface, as when its Activity is recreated, and + /// re-read night mode, which may have changed while it had none. + pub fn reinit_window( + &self, + window: &AndroidWindow, + native_window: ndk::native_window::NativeWindow, + ) -> Result<()> { + window.init_window(native_window, self.gpu_context())?; + self.refresh_appearance(); + Ok(()) + } + + /// Read the system night mode and apply it to every window. + pub fn refresh_appearance(&self) { + self.set_appearance(super::jni::query_appearance()); + } + + /// Apply `appearance` to every window, and answer it from + /// `Platform::window_appearance` from now on. + pub fn set_appearance(&self, appearance: super::window::WindowAppearance) { + *self.appearance.lock() = appearance; + let windows: Vec<_> = self.state.lock().windows.iter().cloned().collect(); + // Outside the lock: a window's appearance callback calls into GPUI. + for window in windows { + window.set_appearance(appearance); + } + } + /// Name the window the next [`Platform::open_window`] should return. /// /// Cleared by that call. See [`AndroidPlatformState::pending_window`]. @@ -1178,7 +1216,7 @@ impl Platform for AndroidPlatform { } fn window_appearance(&self) -> WindowAppearance { - WindowAppearance::Dark + (*self.appearance.lock()).into() } fn open_url(&self, url: &str) { diff --git a/src/android/window.rs b/src/android/window.rs index efdcee0..c9b0076 100644 --- a/src/android/window.rs +++ b/src/android/window.rs @@ -278,6 +278,18 @@ pub enum WindowAppearance { HighContrastDark, } +impl From for gpui::WindowAppearance { + fn from(appearance: WindowAppearance) -> Self { + match appearance { + WindowAppearance::Dark => gpui::WindowAppearance::Dark, + WindowAppearance::HighContrastDark => gpui::WindowAppearance::VibrantDark, + WindowAppearance::Light | WindowAppearance::HighContrastLight => { + gpui::WindowAppearance::Light + } + } + } +} + // ── window state (interior-mutable) ────────────────────────────────────────── /// Safe area insets in physical (device) pixels. @@ -1501,14 +1513,7 @@ impl PlatformWindow for AndroidPlatformWindow { } fn appearance(&self) -> gpui::WindowAppearance { - let local_appearance = self.window.appearance(); - match local_appearance { - WindowAppearance::Dark => gpui::WindowAppearance::Dark, - WindowAppearance::HighContrastDark => gpui::WindowAppearance::VibrantDark, - WindowAppearance::Light | WindowAppearance::HighContrastLight => { - gpui::WindowAppearance::Light - } - } + self.window.appearance().into() } fn display(&self) -> Option> {