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.
This commit is contained in:
hayaksi1
2026-08-17 11:33:09 +03:00
parent 1a3b89de12
commit ddd89f46df
2 changed files with 48 additions and 9 deletions
@@ -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<String?>.mapToTheme(allowBlackTheme: Boolean): Flow<Theme> = map {
when (it) {
null -> Theme.System
@@ -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()