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 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.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.Flow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
@@ -27,12 +30,28 @@ private fun Theme.coerceBlackTheme(allowBlackTheme: Boolean): Theme {
@Composable @Composable
fun Theme.isDark(): Boolean { fun Theme.isDark(): Boolean {
return when (this) { return when (this) {
Theme.System -> isSystemInDarkTheme() Theme.System -> isSystemThemeDark()
Theme.Dark, Theme.Black -> true Theme.Dark, Theme.Black -> true
Theme.Light -> false 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 { fun Flow<String?>.mapToTheme(allowBlackTheme: Boolean): Flow<Theme> = map {
when (it) { when (it) {
null -> Theme.System null -> Theme.System
@@ -11,19 +11,22 @@ package io.element.android.compound.theme
import android.content.res.Configuration import android.content.res.Configuration
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext
import app.cash.molecule.RecompositionMode import app.cash.molecule.RecompositionMode
import app.cash.molecule.moleculeFlow import app.cash.molecule.moleculeFlow
import app.cash.turbine.test import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.tests.testutils.robolectric.RobolectricTest
import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Test import org.junit.Test
import org.robolectric.RuntimeEnvironment
class ThemeTest { class ThemeTest : RobolectricTest() {
@Test @Test
fun `isDark for System dark returns true`() { fun `isDark for System dark returns true`() {
`isDark for System`( `isDark for System`(
uiMode = Configuration.UI_MODE_NIGHT_YES, systemUiMode = Configuration.UI_MODE_NIGHT_YES,
expected = true, expected = true,
) )
} }
@@ -31,21 +34,38 @@ class ThemeTest {
@Test @Test
fun `isDark for System light return false`() { fun `isDark for System light return false`() {
`isDark for System`( `isDark for System`(
uiMode = Configuration.UI_MODE_NIGHT_NO, systemUiMode = Configuration.UI_MODE_NIGHT_NO,
expected = false, expected = false,
) )
} }
fun `isDark for System`( @Test
uiMode: Int, 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, expected: Boolean,
) = runTest { ) = runTest {
val application = RuntimeEnvironment.getApplication()
application.resources.configuration.uiMode = systemUiMode
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
var result: Boolean? = null var result: Boolean? = null
CompositionLocalProvider( CompositionLocalProvider(
// Let set the system to dark LocalContext provides application,
LocalConfiguration provides Configuration().apply { LocalConfiguration provides Configuration().apply {
this.uiMode = uiMode this.uiMode = activityUiMode
}, },
) { ) {
result = Theme.System.isDark() result = Theme.System.isDark()