PM-15229: Accomidate system bars on specific Android 15 revisions (#5617)

This commit is contained in:
David Perez
2025-07-30 16:07:06 -05:00
committed by GitHub
parent 9b297286e5
commit 2fa9ea18b5
11 changed files with 87 additions and 65 deletions

View File

@@ -15,6 +15,7 @@ import androidx.appcompat.app.AppCompatDelegate
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.core.app.ActivityCompat
import androidx.core.os.LocaleListCompat
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.lifecycle.compose.collectAsStateWithLifecycle
@@ -43,6 +44,8 @@ import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.map
import javax.inject.Inject
private const val ANDROID_15_BUG_MAX_REVISION: Int = 241007
/**
* Primary entry point for the application.
*/
@@ -221,7 +224,35 @@ class MainActivity : AppCompatActivity() {
}
private fun handleRecreate() {
recreate()
val isOldAndroidBuildRevision = {
// This fetches the date portion of the ID in order to determine the revision of
// Android 15 being used and whether we want to use the `recreate` API or not.
// If we fail to parse a date, we assume it is not an old revision.
"\\.([^.]+)\\."
.toRegex()
.find(Build.ID)
?.groups
?.get(1)
?.value
?.toIntOrNull()
?.let { it <= ANDROID_15_BUG_MAX_REVISION } == true
}
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.VANILLA_ICE_CREAM &&
isOldAndroidBuildRevision()
) {
// This is done to avoid a bug in specific older revisions of Android 15. The bug has
// been fixed but certain phones that are no longer supported will never get the fix.
// The OS bug is tracked here: https://issuetracker.google.com/issues/370180732
startActivity(
Intent
.makeMainActivity(componentName)
.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION),
)
finish()
overrideActivityTransition(OVERRIDE_TRANSITION_CLOSE, 0, 0)
} else {
ActivityCompat.recreate(this)
}
}
private fun updateScreenCapture(isScreenCaptureAllowed: Boolean) {

View File

@@ -44,12 +44,15 @@ import com.x8bit.bitwarden.ui.platform.util.isPasswordGeneratorShortcut
import com.x8bit.bitwarden.ui.vault.model.TotpData
import com.x8bit.bitwarden.ui.vault.util.getTotpDataOrNull
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.delay
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
@@ -58,11 +61,12 @@ import java.time.Clock
import javax.inject.Inject
private const val SPECIAL_CIRCUMSTANCE_KEY = "special-circumstance"
private const val ANIMATION_REFRESH_DELAY = 500L
private const val ANIMATION_DEBOUNCE_DELAY_MS = 500L
/**
* A view model that helps launch actions for the [MainActivity].
*/
@OptIn(FlowPreview::class)
@Suppress("LongParameterList", "TooManyFunctions")
@HiltViewModel
class MainViewModel @Inject constructor(
@@ -135,36 +139,23 @@ class MainViewModel @Inject constructor(
.onEach(::trySendAction)
.launchIn(viewModelScope)
authRepository
.userStateFlow
.drop(count = 1)
// Trigger an action whenever the current user changes or we go into/out of a pending
// account state (which acts like switching to a temporary user).
.map { it?.activeUserId to it?.hasPendingAccountAddition }
.distinctUntilChanged()
.onEach {
// Switching between account states often involves some kind of animation (ex:
// account switcher) that we might want to give time to finish before triggering
// a refresh.
delay(ANIMATION_REFRESH_DELAY)
trySendAction(MainAction.Internal.CurrentUserStateChange)
}
.launchIn(viewModelScope)
vaultRepository
.vaultStateEventFlow
.onEach {
when (it) {
is VaultStateEvent.Locked -> {
// Similar to account switching, triggering this action too soon can
// interfere with animations or navigation logic, so we will delay slightly.
delay(ANIMATION_REFRESH_DELAY)
trySendAction(MainAction.Internal.VaultUnlockStateChange)
}
is VaultStateEvent.Unlocked -> Unit
}
}
merge(
authRepository
.userStateFlow
.drop(count = 1)
// Trigger an action whenever the current user changes or we go into/out of a
// pending account state (which acts like switching to a temporary user).
.map { it?.activeUserId to it?.hasPendingAccountAddition }
.distinctUntilChanged(),
vaultRepository
.vaultStateEventFlow
.filter { it is VaultStateEvent.Locked },
)
// This debounce ensure we do not emit multiple times rapidly and also acts as a short
// delay to give animations time to finish (ex: account switcher).
.debounce(timeoutMillis = ANIMATION_DEBOUNCE_DELAY_MS)
.map { MainAction.Internal.CurrentUserOrVaultStateChange }
.onEach(::sendAction)
.launchIn(viewModelScope)
// On app launch, mark all active users as having previously logged in.
@@ -202,10 +193,12 @@ class MainViewModel @Inject constructor(
handleAutofillSelectionReceive(action)
}
is MainAction.Internal.CurrentUserStateChange -> handleCurrentUserStateChange()
is MainAction.Internal.CurrentUserOrVaultStateChange -> {
handleCurrentUserOrVaultStateChange()
}
is MainAction.Internal.ScreenCaptureUpdate -> handleScreenCaptureUpdate(action)
is MainAction.Internal.ThemeUpdate -> handleAppThemeUpdated(action)
is MainAction.Internal.VaultUnlockStateChange -> handleVaultUnlockStateChange()
is MainAction.Internal.DynamicColorsUpdate -> handleDynamicColorsUpdate(action)
}
}
@@ -239,8 +232,9 @@ class MainViewModel @Inject constructor(
sendEvent(MainEvent.CompleteAutofill(cipherView = action.cipherView))
}
private fun handleCurrentUserStateChange() {
recreateUiAndGarbageCollect()
private fun handleCurrentUserOrVaultStateChange() {
sendEvent(MainEvent.Recreate)
garbageCollectionManager.tryCollect()
}
private fun handleScreenCaptureUpdate(action: MainAction.Internal.ScreenCaptureUpdate) {
@@ -252,10 +246,6 @@ class MainViewModel @Inject constructor(
sendEvent(MainEvent.UpdateAppTheme(osTheme = action.theme.osValue))
}
private fun handleVaultUnlockStateChange() {
recreateUiAndGarbageCollect()
}
private fun handleDynamicColorsUpdate(action: MainAction.Internal.DynamicColorsUpdate) {
mutableStateFlow.update { it.copy(isDynamicColorsEnabled = action.isDynamicColorsEnabled) }
}
@@ -431,11 +421,6 @@ class MainViewModel @Inject constructor(
}
}
private fun recreateUiAndGarbageCollect() {
sendEvent(MainEvent.Recreate)
garbageCollectionManager.tryCollect()
}
private fun handleCompleteRegistrationData(data: CompleteRegistrationData) {
viewModelScope.launch {
// Attempt to load the environment for the user if they have a pre-auth environment
@@ -547,9 +532,9 @@ sealed class MainAction {
) : Internal()
/**
* Indicates a relevant change in the current user state.
* Indicates a relevant change in the current user state or vault locked state.
*/
data object CurrentUserStateChange : Internal()
data object CurrentUserOrVaultStateChange : Internal()
/**
* Indicates that the screen capture state has changed.
@@ -565,11 +550,6 @@ sealed class MainAction {
val theme: AppTheme,
) : Internal()
/**
* Indicates a relevant change in the current vault lock state.
*/
data object VaultUnlockStateChange : Internal()
/**
* Indicates that the dynamic colors state has changed.
*/

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="windowBackground">#FF202733</color>
</resources>

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="windowBackground">@android:color/transparent</color>
</resources>

View File

@@ -1,4 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<dimen name="dialogDimBackgroundAmount" format="float">0.75</dimen>
<color name="windowBackground">@android:color/transparent</color>
</resources>

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="windowBackground">#FFFFFFFF</color>
</resources>

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="windowBackground">@android:color/transparent</color>
</resources>

View File

@@ -8,6 +8,7 @@
<item name="android:windowActionModeOverlay">true</item>
<item name="android:windowLayoutInDisplayCutoutMode">@integer/displayCutoutMode</item>
<item name="android:backgroundDimAmount">@dimen/dialogDimBackgroundAmount</item>
<item name="android:windowBackground">@color/windowBackground</item>
</style>
<!-- Launch theme (for auto dark/light based on system) -->

View File

@@ -3,4 +3,5 @@
<dimen name="dialogDimBackgroundAmount" format="float">0.55</dimen>
<!-- default -->
<integer name="displayCutoutMode">0</integer>
<color name="windowBackground">@android:color/transparent</color>
</resources>

View File

@@ -16,6 +16,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import com.bitwarden.authenticator.R
import com.bitwarden.ui.platform.feature.settings.appearance.model.AppTheme
import com.bitwarden.ui.platform.util.isDarkMode
/**
* The overall application theme. This can be configured to support a [theme] and [dynamicColor].
@@ -26,12 +27,7 @@ fun AuthenticatorTheme(
dynamicColor: Boolean = false,
content: @Composable () -> Unit,
) {
val darkTheme = when (theme) {
AppTheme.DEFAULT -> isSystemInDarkTheme()
AppTheme.DARK -> true
AppTheme.LIGHT -> false
}
val darkTheme = theme.isDarkMode(isSystemDarkMode = isSystemInDarkTheme())
// Get the current scheme
val context = LocalContext.current
val colorScheme = when {

View File

@@ -25,6 +25,7 @@ import com.bitwarden.ui.platform.theme.shape.bitwardenShapes
import com.bitwarden.ui.platform.theme.type.BitwardenTypography
import com.bitwarden.ui.platform.theme.type.bitwardenTypography
import com.bitwarden.ui.platform.theme.type.toMaterialTypography
import com.bitwarden.ui.platform.util.isDarkMode
/**
* Static wrapper to make accessing the theme components easier.
@@ -64,12 +65,7 @@ fun BitwardenTheme(
dynamicColor: Boolean = false,
content: @Composable () -> Unit,
) {
val darkTheme = when (theme) {
AppTheme.DEFAULT -> isSystemInDarkTheme()
AppTheme.DARK -> true
AppTheme.LIGHT -> false
}
val darkTheme = theme.isDarkMode(isSystemDarkMode = isSystemInDarkTheme())
// Get the current scheme
val materialColorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {