diff --git a/apps/weblibre/lib/features/geckoview/features/browser/domain/services/engine_settings_replication.dart b/apps/weblibre/lib/features/geckoview/features/browser/domain/services/engine_settings_replication.dart index 8f835288..02fc25db 100644 --- a/apps/weblibre/lib/features/geckoview/features/browser/domain/services/engine_settings_replication.dart +++ b/apps/weblibre/lib/features/geckoview/features/browser/domain/services/engine_settings_replication.dart @@ -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( diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/ColorSchemePreference.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/ColorSchemePreference.kt new file mode 100644 index 00000000..a3999537 --- /dev/null +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/ColorSchemePreference.kt @@ -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 + } +} diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/activities/ExternalAppBrowserActivity.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/activities/ExternalAppBrowserActivity.kt index 90906c44..05a68908 100644 --- a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/activities/ExternalAppBrowserActivity.kt +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/activities/ExternalAppBrowserActivity.kt @@ -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) { diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/api/GeckoEngineSettingsApiImpl.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/api/GeckoEngineSettingsApiImpl.kt index 5fd6ca36..f4a9ad03 100644 --- a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/api/GeckoEngineSettingsApiImpl.kt +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/api/GeckoEngineSettingsApiImpl.kt @@ -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) { diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/components/Core.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/components/Core.kt index 5bc17de3..200cf206 100644 --- a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/components/Core.kt +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/components/Core.kt @@ -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,