Skip to content

android: Follow the system night mode on the host-driven path - #30

Open
Bombatomica64 wants to merge 2 commits into
longbridge:mainfrom
Bombatomica64:android-host-night-mode
Open

Bombatomica64 wants to merge 2 commits into
longbridge:mainfrom
Bombatomica64:android-host-night-mode

Conversation

@Bombatomica64

@Bombatomica64 Bombatomica64 commented Oct 9, 2026 •

Copy link
Copy Markdown

On the host-driven entry point (android::host), the GPUI window does not follow the system's dark mode. This PR reads night mode from the host Activity and applies it at the points where it can change.

Problem

  • query_night_mode_via_jni() reads the configuration through AndroidApp's AssetManager. The host-driven path has no AndroidApp, so the window stayed Light whatever the system used.
  • Platform::window_appearance always answered Dark, on either path.

Change

  • Without an AndroidApp, night mode is read from the application context's getResources().getConfiguration().uiMode. The first set_host_activity keeps that context for the life of the process. Its configuration follows system-wide changes whatever the Activity in front handles itself, whereas an Activity that lists uiMode in configChanges keeps its creation-time configuration in its AssetManager.
  • The host path applies the system appearance when a window first opens, when a recreated Activity's surface is attached, and on resumed (the mode may have changed while another app was in front).
  • New host::configuration_changed(). An Activity that handles uiMode itself must call it from onConfigurationChanged, otherwise its window keeps the old mode until it is resumed. It syncs every host's window, so stacked Activities follow too. Like MainEvent::ConfigChanged on the android-activity path, it also calls notify_keyboard_layout_change, so that now runs on every configuration change the host forwards.
  • window_appearance returns the same value.

The android-activity path reads night mode as before; only window_appearance changes there.

Screenshots

The system switched to dark while the app was open. Left: without this change, the window stays Light. Right: with it. (OnePlus CPH2581, Android 16, host-driven test app.)

night mode before / after

Testing

  • cargo fmt --all -- --check, cargo clippy --target aarch64-linux-android --lib: no new warnings.
  • On a OnePlus CPH2581 (Android 16), with a host Activity that lists uiMode in configChanges: cold start in light and in dark; switching to dark while the app is in front (configuration_changed); and switching back to light while it is in the background, then returning to it. In that last case Android delivers onConfigurationChanged just before onResume, so the resumed sync is there for hosts that do not forward configuration changes.
  • Before the change, on the same device, the window stayed Light after switching to dark.

Test app: Bombatomica64/Gpui-android (its Diagnostics screen shows the window and platform appearance).

The emoji font on the host path is a separate PR (#29), since it does not depend on this one.

🤖 Generated with Claude Code

Bombatomica64 added a commit to Bombatomica64/Gpui-android that referenced this pull request Oct 9, 2026
…#22)

Fork main 3d74121 merges the revised android-host-night-mode (c62c318) and
android-host-emoji-font (77126f2), which longbridge/gpui-mobile#30 and #29
will carry: night mode is read from the application context, and the
emoji font's AssetManager is kept for the life of the process.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
@Bombatomica64
Bombatomica64 force-pushed the android-host-night-mode branch from 8da05ca to c62c318 Compare October 9, 2026 10:33

@huacnlee huacnlee left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for following up on the night-mode note from #29. A few things to fix before merging. These come from reading the diff; I have not checked them on a device.

1. The android-activity path still starts Light, and now disagrees with the platform (jni.rs, InitWindow branch)
Platform::window_appearance() now returns the real night mode, but the android-activity InitWindow path (platform.open_window and the init_window reattach) never calls sync_appearance. On a cold start in dark mode, cx.window_appearance() says Dark while the window's appearance() stays Light until a ConfigChanged, and observe_window_appearance never fires. Before this PR both paths at least agreed on Dark; now they disagree.

2. Sync the initial appearance in the shared code, not only at host.rs call sites (host.rs)
The initial sync is added in three host-only places (new window, reattach, resume). Calling sync_appearance inside AndroidPlatform::open_window (and after init_window) would fix both entry points in one place and resolve item 1.

3. The application context ignores app-local night mode (jni.rs, host_ui_mode)
Night mode is read from getApplicationContext().getResources().getConfiguration(). An app-local setting such as AppCompatDelegate.setDefaultNightMode changes only the Activity's configuration, so an app with an in-app dark-theme toggle would be dark everywhere except the GPUI window. Please read the Activity's Resources instead.

4. configuration_changed depends on the order Android delivers config changes in (host.rs)
Inside onConfigurationChanged, the code reads the application's configuration, assuming the process-level change has already been applied. Process and Activity configuration changes are separate transactions, so this can read a stale uiMode. Passing newConfig.uiMode from the callback into configuration_changed would remove that dependency.

Non-blocking:

  • window_appearance() now makes several JNI calls on every call on the host path, and builds a new AConfiguration each time on the android-activity path. Theme code that calls it during render pays that cost every frame. Returning the appearance already stored by sync_appearance would cost nothing and stay consistent with PlatformWindow::appearance.
  • The ConfigurationChanged handler runs the same process-wide query once per window. Query once before the loop and call set_appearance on each window.
  • host_ui_mode errors are swallowed by is_ok_and, so a failed context capture silently means Light forever. Please log a warning.

The host-driven entry point has no AndroidApp, and night mode was only read
through its AssetManager, so a host-driven window stayed Light whatever the
system used. Platform::window_appearance always answered Dark on either
path.

Night mode is now read from the application context's Resources when there
is no AndroidApp. The first set_host_activity keeps that context for the
life of the process. Its configuration follows system-wide changes whatever
the Activity in front handles itself, whereas an Activity that handles
uiMode keeps a stale configuration in its AssetManager. The host path
applies it when a window first opens, when a recreated Activity's surface
is attached, on resume, and, for every host's window, from the new
host::configuration_changed(), which an Activity that handles uiMode must
call from onConfigurationChanged. window_appearance reports the same night
mode.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Bombatomica64
Bombatomica64 force-pushed the android-host-night-mode branch from c62c318 to ec5c89d Compare October 12, 2026 13:07
Bombatomica64 added a commit to Bombatomica64/gpui-mobile that referenced this pull request Oct 13, 2026
Review fixes for longbridge#30:

- AndroidPlatform::open_window and the new reinit_window apply the night
  mode, so the android-activity InitWindow path no longer starts Light
  while window_appearance says Dark. host.rs drops its call-site syncs.
- The host path reads uiMode from the current host Activity's Resources,
  which carry an app-local night mode (AppCompatDelegate). The application
  context is no longer kept; the AssetManager is fetched as on main.
- host::configuration_changed takes newConfig.uiMode, so it does not
  depend on when the process configuration is updated.
- The platform stores the appearance it last applied. window_appearance
  returns it with no JNI call, and agrees with every window's appearance.
  It has its own lock: GPUI reads it from tasks that tick() runs with the
  platform state locked. A config change reads the night mode once for
  all windows.
- A failed uiMode read logs a warning instead of silently meaning Light.
  It runs in a local frame, since the render thread never returns to Java.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review fixes for longbridge#30:

- AndroidPlatform::open_window and the new reinit_window apply the night
  mode, so the android-activity InitWindow path no longer starts Light
  while window_appearance says Dark. host.rs drops its call-site syncs.
- The host path reads uiMode from the current host Activity's Resources,
  which carry an app-local night mode (AppCompatDelegate). The application
  context is no longer kept; the AssetManager is fetched as on main.
- host::configuration_changed takes newConfig.uiMode, so it does not
  depend on when the process configuration is updated.
- The platform stores the appearance it last applied. window_appearance
  returns it with no JNI call, and agrees with every window's appearance.
  It has its own lock: GPUI reads it from main-thread tasks, which
  flush_main_thread_tasks() runs with the platform state locked. A config
  change reads the night mode once for all windows.
- A failed uiMode read logs a warning instead of silently meaning Light.
  It runs in a local frame, since the render thread never returns to Java.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Bombatomica64

Copy link
Copy Markdown
Author

Thanks for the review, and sorry for the silent force-push on the 12th: that
was only a rebase onto #29 and did not address anything. The fixes are in
fa0075b, on top of it:

  1. android-activity path starts Light. Fixed by item 2. On a cold start in
    dark mode, cx.window_appearance() and window.appearance() both say Dark
    on the first frame now. Before, the window said Light.
  2. Sync in the shared code. AndroidPlatform::open_window applies the night
    mode, and so does a new AndroidPlatform::reinit_window, which both
    InitWindow and the host's reattach now use instead of calling
    init_window directly. The call-site syncs in host.rs are gone. Only
    resumed still re-reads it, in case it changed while another app was in
    front.
  3. App-local night mode. uiMode now comes from the current host Activity's
    Resources, so AppCompatDelegate.setDefaultNightMode counts. With the
    application context no longer needed for night mode, I dropped the global
    this PR had added. The AssetManager is fetched as on main again.
  4. Order of config changes. It's now host::configuration_changed(ui_mode: i32),
    and the host passes newConfig.uiMode. Nothing re-reads a configuration in
    that path. The module docs show the call.

Non-blocking:

  • The platform stores the appearance it last applied, and
    window_appearance() just returns it, with no JNI call or AConfiguration.
    It has its own small lock rather than living in the platform state, because
    GPUI reads it from main-thread tasks, and flush_main_thread_tasks() runs
    those with the state locked. My first version stored it in the state and
    deadlocked on the first night-mode change on the android-activity path.
    The device test below caught it.
  • ConfigurationChanged reads the night mode once and applies it to every
    window (AndroidPlatform::set_appearance).
  • A failed uiMode read logs a warn!. The read also runs in a local frame
    now, since the render thread never returns to Java.

Tested on a OnePlus with Android 16, switching with cmd uimode night:

  • host path (Bombatomica64/Gpui-android, Diagnostics screen): cold start in
    dark mode, a live toggle to light (the Activity handles uiMode), back to
    dark while in the background then resumed, and a light system with the
    Activity forced to dark the way AppCompat does it. All four show the right
    appearance.
  • android-activity path (this repo's example, with a log in an
    observe_window_appearance callback): a cold start in dark mode logs
    platform=Dark window=Dark (ec5c89d logged platform=Dark window=Light), and
    toggling light and back fires the observer each time with both values
    agreeing.

Cold start dark, live toggle to light, app-local dark on a light system

One API change for hosts: configuration_changed now takes the uiMode
argument.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants