PM-15229: Update logic for handling edge-to-edge (#5282)

This commit is contained in:
David Perez
2025-05-29 13:25:23 -05:00
committed by GitHub
parent b877487ce1
commit ab2ac60957
12 changed files with 138 additions and 43 deletions

View File

@@ -48,6 +48,7 @@
tools:targetApi="36">
<activity
android:name=".MainActivity"
android:configChanges="uiMode"
android:exported="true"
android:launchMode="@integer/launchModeAPIlevel"
android:theme="@style/LaunchTheme"

View File

@@ -22,6 +22,7 @@ import androidx.navigation.compose.NavHost
import com.bitwarden.annotation.OmitFromCoverage
import com.bitwarden.ui.platform.base.util.EventsEffect
import com.bitwarden.ui.platform.theme.BitwardenTheme
import com.bitwarden.ui.platform.util.setupEdgeToEdge
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityCompletionManager
import com.x8bit.bitwarden.data.autofill.manager.AutofillActivityManager
import com.x8bit.bitwarden.data.autofill.manager.AutofillCompletionManager
@@ -37,6 +38,7 @@ import com.x8bit.bitwarden.ui.platform.feature.rootnav.rootNavDestination
import com.x8bit.bitwarden.ui.platform.feature.settings.appearance.model.AppLanguage
import com.x8bit.bitwarden.ui.platform.util.appLanguage
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.map
import javax.inject.Inject
/**
@@ -77,6 +79,7 @@ class MainActivity : AppCompatActivity() {
// OS, but we need to ensure we properly set the values when upgrading from older versions
// that handle this differently or when the activity restarts.
AppCompatDelegate.setDefaultNightMode(settingsRepository.appTheme.osValue)
setupEdgeToEdge(appThemeFlow = mainViewModel.stateFlow.map { it.theme })
setContent {
val navController = rememberBitwardenNavController(name = "MainActivity")
SetupEventsEffect(navController = navController)

View File

@@ -2,8 +2,6 @@
<resources>
<style name="BaseTheme" parent="Theme.AppCompat.Light.NoActionBar">
<item name="android:navigationBarColor">@android:color/transparent</item>
<item name="android:statusBarColor">@android:color/transparent</item>
<item name="android:textCursorDrawable">@null</item>
<item name="android:windowActionBar">false</item>
<item name="android:windowNoTitle">true</item>

View File

@@ -28,6 +28,7 @@
tools:targetApi="36">
<activity
android:name="com.bitwarden.authenticator.MainActivity"
android:configChanges="uiMode"
android:exported="true"
android:launchMode="@integer/launchModeAPILevel"
android:theme="@style/LaunchTheme"

View File

@@ -20,8 +20,10 @@ import com.bitwarden.authenticator.ui.platform.feature.debugmenu.manager.DebugMe
import com.bitwarden.authenticator.ui.platform.feature.debugmenu.navigateToDebugMenuScreen
import com.bitwarden.authenticator.ui.platform.feature.rootnav.RootNavScreen
import com.bitwarden.authenticator.ui.platform.theme.AuthenticatorTheme
import com.bitwarden.ui.platform.util.setupEdgeToEdge
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import javax.inject.Inject
@@ -50,6 +52,7 @@ class MainActivity : AppCompatActivity() {
)
}
setupEdgeToEdge(appThemeFlow = mainViewModel.stateFlow.map { it.theme })
setContent {
val state by mainViewModel.stateFlow.collectAsStateWithLifecycle()
val navController = rememberNavController()

View File

@@ -1,6 +1,5 @@
package com.bitwarden.authenticator.ui.platform.theme
import android.app.Activity
import android.content.Context
import android.os.Build
import androidx.annotation.ColorRes
@@ -12,13 +11,9 @@ import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.ProvidableCompositionLocal
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView
import androidx.core.graphics.drawable.toDrawable
import androidx.core.view.WindowCompat
import com.bitwarden.authenticator.R
import com.bitwarden.ui.platform.feature.settings.appearance.model.AppTheme
@@ -48,19 +43,6 @@ fun AuthenticatorTheme(
else -> lightColorScheme(context)
}
// Update status bar according to scheme
val view = LocalView.current
if (!view.isInEditMode) {
SideEffect {
val window = (view.context as Activity).window
WindowCompat.setDecorFitsSystemWindows(window, false)
val insetsController = WindowCompat.getInsetsController(window, view)
insetsController.isAppearanceLightStatusBars = !darkTheme
insetsController.isAppearanceLightNavigationBars = !darkTheme
window.setBackgroundDrawable(colorScheme.surface.value.toInt().toDrawable())
}
}
val nonMaterialColors = if (darkTheme) {
darkNonMaterialColors(context)
} else {

View File

@@ -2,8 +2,6 @@
<resources>
<style name="BaseTheme" parent="Theme.AppCompat.Light.NoActionBar">
<item name="android:navigationBarColor">@android:color/transparent</item>
<item name="android:statusBarColor">@android:color/transparent</item>
<item name="android:textCursorDrawable">@null</item>
<item name="android:windowActionBar">false</item>
<item name="android:windowNoTitle">true</item>

View File

@@ -1,6 +1,5 @@
package com.bitwarden.ui.platform.theme
import android.app.Activity
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
@@ -12,12 +11,8 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.ProvidableCompositionLocal
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView
import androidx.core.graphics.drawable.toDrawable
import androidx.core.view.WindowCompat
import com.bitwarden.ui.platform.components.field.interceptor.IncognitoInput
import com.bitwarden.ui.platform.feature.settings.appearance.model.AppTheme
import com.bitwarden.ui.platform.theme.color.BitwardenColorScheme
@@ -76,9 +71,9 @@ fun BitwardenTheme(
}
// Get the current scheme
val context = LocalContext.current
val materialColorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
if (darkTheme) {
dynamicDarkColorScheme(context = context)
} else {
@@ -101,21 +96,6 @@ fun BitwardenTheme(
else -> lightBitwardenColorScheme
}
// Update status bar according to scheme
val view = LocalView.current
if (!view.isInEditMode) {
SideEffect {
val window = (view.context as Activity).window
WindowCompat.setDecorFitsSystemWindows(window, false)
val insetsController = WindowCompat.getInsetsController(window, view)
insetsController.isAppearanceLightStatusBars = !darkTheme
insetsController.isAppearanceLightNavigationBars = !darkTheme
window.setBackgroundDrawable(
bitwardenColorScheme.background.primary.value.toInt().toDrawable(),
)
}
}
CompositionLocalProvider(
LocalBitwardenColorScheme provides bitwardenColorScheme,
LocalBitwardenShapes provides bitwardenShapes,

View File

@@ -0,0 +1,15 @@
package com.bitwarden.ui.platform.util
import com.bitwarden.ui.platform.feature.settings.appearance.model.AppTheme
/**
* Returns `true` if the app is currently using dark mode.
*/
fun AppTheme.isDarkMode(
isSystemDarkMode: Boolean,
): Boolean =
when (this) {
AppTheme.DEFAULT -> isSystemDarkMode
AppTheme.DARK -> true
AppTheme.LIGHT -> false
}

View File

@@ -0,0 +1,75 @@
@file:OmitFromCoverage
package com.bitwarden.ui.platform.util
import android.content.res.Configuration
import android.graphics.Color
import androidx.activity.ComponentActivity
import androidx.activity.SystemBarStyle
import androidx.activity.enableEdgeToEdge
import androidx.annotation.ColorInt
import androidx.core.util.Consumer
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.bitwarden.annotation.OmitFromCoverage
import com.bitwarden.ui.platform.feature.settings.appearance.model.AppTheme
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.conflate
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.launch
@ColorInt
private val SCRIM_COLOR: Int = Color.TRANSPARENT
/**
* Helper method to handle edge-to-edge logic for dark mode.
*
* This logic is from the Now-In-Android app found [here](https://github.com/android/nowinandroid/blob/689ef92e41427ab70f82e2c9fe59755441deae92/app/src/main/kotlin/com/google/samples/apps/nowinandroid/MainActivity.kt#L94).
*/
@Suppress("MaxLineLength")
fun ComponentActivity.setupEdgeToEdge(
appThemeFlow: Flow<AppTheme>,
) {
lifecycleScope.launch {
lifecycle.repeatOnLifecycle(state = Lifecycle.State.STARTED) {
combine(
isSystemInDarkModeFlow(),
appThemeFlow,
) { isSystemDarkMode, appTheme ->
appTheme.isDarkMode(isSystemDarkMode = isSystemDarkMode)
}
.distinctUntilChanged()
.collect { isDarkMode ->
// This handles all the settings to go edge-to-edge. We are using a transparent
// scrim for system bars and switching between "light" and "dark" based on the
// system and internal app theme settings.
val style = if (isDarkMode) {
SystemBarStyle.dark(scrim = SCRIM_COLOR)
} else {
SystemBarStyle.light(scrim = SCRIM_COLOR, darkScrim = SCRIM_COLOR)
}
enableEdgeToEdge(statusBarStyle = style, navigationBarStyle = style)
}
}
}
}
/**
* Adds a configuration change listener to retrieve whether system is in dark theme or not.
* This will emit current status immediately and then will emit changes as needed.
*/
private fun ComponentActivity.isSystemInDarkModeFlow(): Flow<Boolean> =
callbackFlow {
channel.trySend(element = resources.configuration.isSystemInDarkMode)
val listener = Consumer<Configuration> {
channel.trySend(element = it.isSystemInDarkMode)
}
addOnConfigurationChangedListener(listener = listener)
awaitClose { removeOnConfigurationChangedListener(listener = listener) }
}
.distinctUntilChanged()
.conflate()

View File

@@ -0,0 +1,9 @@
package com.bitwarden.ui.platform.util
import android.content.res.Configuration
/**
* Convenience method to check if the system is currently in dark mode.
*/
val Configuration.isSystemInDarkMode
get() = (uiMode and Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES

View File

@@ -0,0 +1,30 @@
package com.bitwarden.ui.platform.util
import com.bitwarden.ui.platform.base.BaseComposeTest
import com.bitwarden.ui.platform.feature.settings.appearance.model.AppTheme
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
class AppThemeExtensionsTest : BaseComposeTest() {
@Test
fun `isDarkMode with Dark AppTheme should return true regardless of system mode`() {
val appTheme = AppTheme.DARK
assertTrue(appTheme.isDarkMode(isSystemDarkMode = false))
assertTrue(appTheme.isDarkMode(isSystemDarkMode = true))
}
@Test
fun `isDarkMode with Light AppTheme should return false regardless of system mode`() {
val appTheme = AppTheme.LIGHT
assertFalse(appTheme.isDarkMode(isSystemDarkMode = false))
assertFalse(appTheme.isDarkMode(isSystemDarkMode = true))
}
@Test
fun `isDarkMode with default AppTheme should return correct value based on system mode`() {
val appTheme = AppTheme.DEFAULT
assertFalse(appTheme.isDarkMode(isSystemDarkMode = false))
assertTrue(appTheme.isDarkMode(isSystemDarkMode = true))
}
}