Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 22 additions & 3 deletions src/android/host.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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`].
//!
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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 });
}
Expand Down
121 changes: 100 additions & 21 deletions src/android/jni.rs
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,10 @@ static HOST_ACTIVITY: Mutex<HostActivity> = 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<jni::refs::Global<JObject<'static>>> = OnceLock::new();

struct HostActivity {
current: Option<jni::refs::Global<JObject<'static>>>,
previous: Option<jni::refs::Global<JObject<'static>>>,
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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<i32, String> {
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.
Expand Down Expand Up @@ -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");
}
Expand Down Expand Up @@ -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();
}
}

Expand Down
56 changes: 47 additions & 9 deletions src/android/platform.rs
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,11 @@ pub struct AndroidPlatform {
state: Mutex<AndroidPlatformState>,
/// 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<super::window::WindowAppearance>,
}

/// Check whether a TrueType/OpenType font file contains CBDT (Color Bitmap
Expand Down Expand Up @@ -486,6 +491,7 @@ impl AndroidPlatform {
preferred_backend: AndroidBackend::Vulkan,
}),
should_quit: AtomicBool::new(false),
appearance: Mutex::default(),
}
}

Expand Down Expand Up @@ -646,22 +652,54 @@ impl AndroidPlatform {
scale_factor: f32,
transparent: bool,
) -> Result<Arc<AndroidWindow>> {
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`].
Expand Down Expand Up @@ -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) {
Expand Down
21 changes: 13 additions & 8 deletions src/android/window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,18 @@ pub enum WindowAppearance {
HighContrastDark,
}

impl From<WindowAppearance> 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.
Expand Down Expand Up @@ -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<Rc<dyn PlatformDisplay>> {
Expand Down
Loading