Merge pull request #7453 from hayaksi1/fix/4639-system-theme

Apply the system theme when switching back to it from a forced light or dark theme
This commit is contained in:
Benoit Marty
2026-08-21 12:18:52 +02:00
committed by GitHub
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()