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
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()