fix color scheme for custom tabs

This commit is contained in:
Fabian Freund
2026-06-06 11:51:36 +02:00
parent 69aad79b93
commit b0267a66b7
5 changed files with 103 additions and 8 deletions
@@ -216,12 +216,11 @@ class EngineSettingsReplicationService
settings.globalPrivacyControlEnabled,
);
}
if (previous.value?.preferredColorScheme !=
settings.preferredColorScheme) {
await _service.preferredColorScheme(
settings.preferredColorScheme,
);
}
// Note: preferredColorScheme is intentionally NOT replicated from the
// engine settings here. The app theme (generalSettings.themeMode) is the
// sole source of truth for the color scheme; see the themeMode listener
// above. EngineSettings.preferredColorScheme is vestigial and always
// `system`, so replicating it would clobber the real theme (issue #436).
if (previous.value?.cookieBannerHandlingMode !=
settings.cookieBannerHandlingMode) {
await _service.cookieBannerHandlingMode(
@@ -0,0 +1,78 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package eu.weblibre.flutter_mozilla_components
import android.content.Context
import android.content.SharedPreferences
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.content.edit
import androidx.preference.PreferenceManager
import mozilla.components.concept.engine.mediaquery.PreferredColorScheme
/**
* Persists the user's preferred color scheme in the default [SharedPreferences].
*
* Flutter remains the source of truth for the theme, but Custom Tab / PWA sessions
* run in a native, non-Flutter activity ([eu.weblibre.flutter_mozilla_components.activities.ExternalAppBrowserActivity]).
* When such a session cold-starts the app, the engine is built before Flutter ever
* runs, so it cannot ask Flutter for the scheme. Without a persisted value the engine
* falls back to a hardcoded default and reports the wrong `prefers-color-scheme` to
* web content (see issue #436). This lets the native side resolve the last known
* choice on cold start.
*/
object ColorSchemePreference {
private const val PREF_KEY = "weblibre_preferred_color_scheme"
private const val VALUE_SYSTEM = "system"
private const val VALUE_LIGHT = "light"
private const val VALUE_DARK = "dark"
/**
* Fallback used before Flutter has written a value (e.g. the very first
* Custom Tab / PWA launch). Must match WebLibre's default app theme
* (`ThemeMode.dark`, general_settings.dart) so a direct launch doesn't briefly
* disagree with the app's default.
*/
private val DEFAULT = PreferredColorScheme.Dark
fun read(prefs: SharedPreferences): PreferredColorScheme =
when (prefs.getString(PREF_KEY, null)) {
VALUE_SYSTEM -> PreferredColorScheme.System
VALUE_LIGHT -> PreferredColorScheme.Light
VALUE_DARK -> PreferredColorScheme.Dark
else -> DEFAULT
}
fun read(context: Context): PreferredColorScheme =
read(PreferenceManager.getDefaultSharedPreferences(context))
fun write(prefs: SharedPreferences, scheme: PreferredColorScheme) {
prefs.edit {
putString(
PREF_KEY,
when (scheme) {
PreferredColorScheme.Light -> VALUE_LIGHT
PreferredColorScheme.Dark -> VALUE_DARK
PreferredColorScheme.System -> VALUE_SYSTEM
},
)
}
}
/**
* Maps the persisted scheme to an [AppCompatDelegate] night mode so native
* activities (Custom Tab / PWA) render their window chrome in the mode the user
* selected in WebLibre, instead of always following the system. This avoids the
* window flashing dark before the page paints when WebLibre is forced to light.
*/
fun nightMode(context: Context): Int =
when (read(context)) {
PreferredColorScheme.Light -> AppCompatDelegate.MODE_NIGHT_NO
PreferredColorScheme.Dark -> AppCompatDelegate.MODE_NIGHT_YES
PreferredColorScheme.System -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
}
}
@@ -13,6 +13,7 @@ import android.util.Log
import androidx.activity.addCallback
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.WindowCompat
import eu.weblibre.flutter_mozilla_components.ColorSchemePreference
import eu.weblibre.flutter_mozilla_components.ExternalAppBrowserFragment
import eu.weblibre.flutter_mozilla_components.GlobalComponents
import eu.weblibre.flutter_mozilla_components.PwaConstants
@@ -76,6 +77,12 @@ open class ExternalAppBrowserActivity : AppCompatActivity() {
get() = intent?.getStringExtra(EXTRA_WEB_APP_MANIFEST_URL)
override fun onCreate(savedInstanceState: Bundle?) {
// Match the window chrome (status/nav bar + pre-paint background) to the
// user's WebLibre color scheme rather than just the system mode, so a
// cold-started Custom Tab / PWA doesn't flash dark when WebLibre is light.
// Set before super.onCreate so the correct mode is applied without a recreate.
delegate.localNightMode = ColorSchemePreference.nightMode(this)
super.onCreate(savedInstanceState)
onBackPressedDispatcher.addCallback(this) {
@@ -8,6 +8,7 @@ package eu.weblibre.flutter_mozilla_components.api
import androidx.core.content.edit
import androidx.preference.PreferenceManager
import eu.weblibre.flutter_mozilla_components.ColorSchemePreference
import eu.weblibre.flutter_mozilla_components.GlobalComponents
import eu.weblibre.flutter_mozilla_components.R
import eu.weblibre.flutter_mozilla_components.pigeons.AppLinksMode
@@ -336,6 +337,13 @@ class GeckoEngineSettingsApiImpl : GeckoEngineSettingsApi {
}
if(settings.preferredColorScheme != null) {
components.core.engine.settings.preferredColorScheme = components.core.engineSettings.preferredColorScheme
// Persist so cold-started Custom Tab / PWA sessions resolve the right
// scheme before Flutter runs. Only done here (the runtime path driven by
// the app theme), not in setDefaultSettings, so the vestigial engine
// settings payload can't clobber the real theme. See issue #436.
components.core.engineSettings.preferredColorScheme?.let { scheme ->
ColorSchemePreference.write(components.core.prefs, scheme)
}
reloadSession = true
}
if(settings.cookieBannerHandlingMode != null) {
@@ -11,6 +11,7 @@ import android.content.SharedPreferences
import android.os.Environment
import androidx.core.content.ContextCompat
import androidx.preference.PreferenceManager
import eu.weblibre.flutter_mozilla_components.ColorSchemePreference
import eu.weblibre.flutter_mozilla_components.Components
import eu.weblibre.flutter_mozilla_components.interceptor.AppRequestInterceptor
import eu.weblibre.flutter_mozilla_components.services.DownloadService
@@ -47,7 +48,6 @@ import mozilla.components.concept.engine.Engine
import mozilla.components.concept.engine.EngineSession
import mozilla.components.concept.engine.EngineSession.TrackingProtectionPolicy
import mozilla.components.concept.engine.fission.WebContentIsolationStrategy
import mozilla.components.concept.engine.mediaquery.PreferredColorScheme
import mozilla.components.concept.fetch.Client
import mozilla.components.feature.addons.AddonManager
import mozilla.components.feature.addons.amo.AMOAddonsProvider
@@ -122,7 +122,10 @@ class Core(
//fingerprintingProtectionPrivateBrowsing
httpsOnlyMode = Engine.HttpsOnlyMode.ENABLED,
globalPrivacyControlEnabled = true,
preferredColorScheme = PreferredColorScheme.Dark,
// Resolve the last persisted choice so cold-started Custom Tab / PWA
// sessions report the correct `prefers-color-scheme` before Flutter
// (the source of truth) runs. Defaults to System. See issue #436.
preferredColorScheme = ColorSchemePreference.read(prefs),
cookieBannerHandlingMode = EngineSession.CookieBannerHandlingMode.REJECT_ALL,
cookieBannerHandlingModePrivateBrowsing = EngineSession.CookieBannerHandlingMode.REJECT_ALL,
cookieBannerHandlingGlobalRules = true,