mirror of
https://github.com/bitwarden/android.git
synced 2026-04-27 19:38:42 -05:00
PM-15229: Accomidate system bars on specific Android 15 revisions (#5617)
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
4
app/src/main/res/values-night-v35/values.xml
Normal file
4
app/src/main/res/values-night-v35/values.xml
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="windowBackground">#FF202733</color>
|
||||
</resources>
|
||||
4
app/src/main/res/values-night-v36/values.xml
Normal file
4
app/src/main/res/values-night-v36/values.xml
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="windowBackground">@android:color/transparent</color>
|
||||
</resources>
|
||||
@@ -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>
|
||||
|
||||
4
app/src/main/res/values-v35/values.xml
Normal file
4
app/src/main/res/values-v35/values.xml
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="windowBackground">#FFFFFFFF</color>
|
||||
</resources>
|
||||
4
app/src/main/res/values-v36/values.xml
Normal file
4
app/src/main/res/values-v36/values.xml
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="windowBackground">@android:color/transparent</color>
|
||||
</resources>
|
||||
@@ -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) -->
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 -> {
|
||||
|
||||
Reference in New Issue
Block a user