mirror of
https://github.com/bitwarden/android.git
synced 2026-03-12 05:04:17 -05:00
PM-15229: Update logic for handling edge-to-edge (#5282)
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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()
|
||||
@@ -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
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user