From ddd89f46df37d8524bff054e1c2835a933054ac6 Mon Sep 17 00:00:00 2001 From: hayaksi1 <193020925+hayaksi1@users.noreply.github.com> Date: Mon, 17 Aug 2026 11:33:09 +0300 Subject: [PATCH] Resolve the System theme from the application configuration Selecting a Light or Dark theme makes AppCompatDelegate rewrite the activity configuration to the forced night mode. isSystemInDarkTheme reads that same configuration, so switching back to System kept showing the previously forced theme. Read the night mode from the application configuration instead, which AppCompat leaves untouched and itself uses to expand MODE_NIGHT_FOLLOW_SYSTEM. The activity configuration is still used as the recomposition key so a system-wide theme change is picked up. --- .../element/android/compound/theme/Theme.kt | 23 +++++++++++-- .../android/compound/theme/ThemeTest.kt | 34 +++++++++++++++---- 2 files changed, 48 insertions(+), 9 deletions(-) diff --git a/libraries/compound/src/main/kotlin/io/element/android/compound/theme/Theme.kt b/libraries/compound/src/main/kotlin/io/element/android/compound/theme/Theme.kt index bf5932da59..e519984e9f 100644 --- a/libraries/compound/src/main/kotlin/io/element/android/compound/theme/Theme.kt +++ b/libraries/compound/src/main/kotlin/io/element/android/compound/theme/Theme.kt @@ -8,8 +8,11 @@ package io.element.android.compound.theme -import androidx.compose.foundation.isSystemInDarkTheme +import android.content.res.Configuration import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalContext import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map @@ -27,12 +30,28 @@ private fun Theme.coerceBlackTheme(allowBlackTheme: Boolean): Theme { @Composable fun Theme.isDark(): Boolean { return when (this) { - Theme.System -> isSystemInDarkTheme() + Theme.System -> isSystemThemeDark() Theme.Dark, Theme.Black -> true Theme.Light -> false } } +/** + * Whether the system-wide theme is dark. + * + * This deliberately does not use [androidx.compose.foundation.isSystemInDarkTheme], which reads the activity configuration: `AppCompatDelegate` rewrites + * that configuration whenever a non-system theme is selected, so it keeps reporting the previously forced value. The application configuration is left + * untouched by AppCompat and is what AppCompat itself reads to expand `MODE_NIGHT_FOLLOW_SYSTEM`. + */ +@Composable +private fun isSystemThemeDark(): Boolean { + val applicationContext = LocalContext.current.applicationContext + val activityConfiguration = LocalConfiguration.current + return remember(activityConfiguration) { + applicationContext.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES + } +} + fun Flow.mapToTheme(allowBlackTheme: Boolean): Flow = map { when (it) { null -> Theme.System diff --git a/libraries/compound/src/test/kotlin/io/element/android/compound/theme/ThemeTest.kt b/libraries/compound/src/test/kotlin/io/element/android/compound/theme/ThemeTest.kt index 3f43cfea67..57ae9dd332 100644 --- a/libraries/compound/src/test/kotlin/io/element/android/compound/theme/ThemeTest.kt +++ b/libraries/compound/src/test/kotlin/io/element/android/compound/theme/ThemeTest.kt @@ -11,19 +11,22 @@ package io.element.android.compound.theme import android.content.res.Configuration import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalContext import app.cash.molecule.RecompositionMode import app.cash.molecule.moleculeFlow import app.cash.turbine.test import com.google.common.truth.Truth.assertThat +import io.element.android.tests.testutils.robolectric.RobolectricTest import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runTest import org.junit.Test +import org.robolectric.RuntimeEnvironment -class ThemeTest { +class ThemeTest : RobolectricTest() { @Test fun `isDark for System dark returns true`() { `isDark for System`( - uiMode = Configuration.UI_MODE_NIGHT_YES, + systemUiMode = Configuration.UI_MODE_NIGHT_YES, expected = true, ) } @@ -31,21 +34,38 @@ class ThemeTest { @Test fun `isDark for System light return false`() { `isDark for System`( - uiMode = Configuration.UI_MODE_NIGHT_NO, + systemUiMode = Configuration.UI_MODE_NIGHT_NO, expected = false, ) } - fun `isDark for System`( - uiMode: Int, + @Test + fun `isDark for System ignores the night mode AppCompat forced on the activity`() { + `isDark for System`( + systemUiMode = Configuration.UI_MODE_NIGHT_YES, + activityUiMode = Configuration.UI_MODE_NIGHT_NO, + expected = true, + ) + `isDark for System`( + systemUiMode = Configuration.UI_MODE_NIGHT_NO, + activityUiMode = Configuration.UI_MODE_NIGHT_YES, + expected = false, + ) + } + + private fun `isDark for System`( + systemUiMode: Int, + activityUiMode: Int = systemUiMode, expected: Boolean, ) = runTest { + val application = RuntimeEnvironment.getApplication() + application.resources.configuration.uiMode = systemUiMode moleculeFlow(RecompositionMode.Immediate) { var result: Boolean? = null CompositionLocalProvider( - // Let set the system to dark + LocalContext provides application, LocalConfiguration provides Configuration().apply { - this.uiMode = uiMode + this.uiMode = activityUiMode }, ) { result = Theme.System.isDark()