mirror of
https://github.com/bitwarden/android.git
synced 2025-12-10 20:07:59 -06:00
PM-27210: Add dynamic color support to Authenticator (#6063)
This commit is contained in:
parent
e610a7541d
commit
4597337500
@ -67,6 +67,7 @@ class MainActivity : AppCompatActivity() {
|
||||
LocalManagerProvider {
|
||||
BitwardenTheme(
|
||||
theme = state.theme,
|
||||
dynamicColor = state.isDynamicColorsEnabled,
|
||||
) {
|
||||
RootNavScreen(
|
||||
navController = navController,
|
||||
|
||||
@ -9,6 +9,7 @@ import com.bitwarden.ui.platform.base.BaseViewModel
|
||||
import com.bitwarden.ui.platform.feature.settings.appearance.model.AppTheme
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
@ -25,20 +26,25 @@ class MainViewModel @Inject constructor(
|
||||
) : BaseViewModel<MainState, MainEvent, MainAction>(
|
||||
MainState(
|
||||
theme = settingsRepository.appTheme,
|
||||
isDynamicColorsEnabled = settingsRepository.isDynamicColorsEnabled,
|
||||
),
|
||||
) {
|
||||
|
||||
init {
|
||||
settingsRepository
|
||||
.appThemeStateFlow
|
||||
.onEach { trySendAction(MainAction.Internal.ThemeUpdate(it)) }
|
||||
.map { MainAction.Internal.ThemeUpdate(it) }
|
||||
.onEach(::sendAction)
|
||||
.launchIn(viewModelScope)
|
||||
settingsRepository
|
||||
.isDynamicColorsEnabledFlow
|
||||
.map { MainAction.Internal.DynamicColorUpdate(it) }
|
||||
.onEach(::sendAction)
|
||||
.launchIn(viewModelScope)
|
||||
|
||||
settingsRepository
|
||||
.isScreenCaptureAllowedStateFlow
|
||||
.onEach { isAllowed ->
|
||||
sendEvent(MainEvent.ScreenCaptureSettingChange(isAllowed))
|
||||
}
|
||||
.map { MainEvent.ScreenCaptureSettingChange(it) }
|
||||
.onEach(::sendEvent)
|
||||
.launchIn(viewModelScope)
|
||||
viewModelScope.launch {
|
||||
configRepository.getServerConfig(forceRefresh = false)
|
||||
@ -47,10 +53,17 @@ class MainViewModel @Inject constructor(
|
||||
|
||||
override fun handleAction(action: MainAction) {
|
||||
when (action) {
|
||||
is MainAction.Internal.ThemeUpdate -> handleThemeUpdated(action)
|
||||
is MainAction.ReceiveFirstIntent -> handleFirstIntentReceived(action)
|
||||
is MainAction.ReceiveNewIntent -> handleNewIntentReceived(action)
|
||||
MainAction.OpenDebugMenu -> handleOpenDebugMenu()
|
||||
is MainAction.Internal -> handleInternalAction(action)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleInternalAction(action: MainAction.Internal) {
|
||||
when (action) {
|
||||
is MainAction.Internal.DynamicColorUpdate -> handleDynamicColorUpdate(action)
|
||||
is MainAction.Internal.ThemeUpdate -> handleThemeUpdated(action)
|
||||
}
|
||||
}
|
||||
|
||||
@ -58,6 +71,10 @@ class MainViewModel @Inject constructor(
|
||||
sendEvent(MainEvent.NavigateToDebugMenu)
|
||||
}
|
||||
|
||||
private fun handleDynamicColorUpdate(action: MainAction.Internal.DynamicColorUpdate) {
|
||||
mutableStateFlow.update { it.copy(isDynamicColorsEnabled = action.isEnabled) }
|
||||
}
|
||||
|
||||
private fun handleThemeUpdated(action: MainAction.Internal.ThemeUpdate) {
|
||||
mutableStateFlow.update { it.copy(theme = action.theme) }
|
||||
sendEvent(MainEvent.UpdateAppTheme(osTheme = action.theme.osValue))
|
||||
@ -91,6 +108,7 @@ class MainViewModel @Inject constructor(
|
||||
@Parcelize
|
||||
data class MainState(
|
||||
val theme: AppTheme,
|
||||
val isDynamicColorsEnabled: Boolean,
|
||||
) : Parcelable
|
||||
|
||||
/**
|
||||
@ -116,6 +134,12 @@ sealed class MainAction {
|
||||
* Actions for internal use by the ViewModel.
|
||||
*/
|
||||
sealed class Internal : MainAction() {
|
||||
/**
|
||||
* Indicates that dynamic colors have been enabled or disabled.
|
||||
*/
|
||||
data class DynamicColorUpdate(
|
||||
val isEnabled: Boolean,
|
||||
) : Internal()
|
||||
|
||||
/**
|
||||
* Indicates that the app theme has changed.
|
||||
|
||||
@ -35,6 +35,16 @@ interface SettingsDiskSource {
|
||||
*/
|
||||
val defaultSaveOptionFlow: Flow<DefaultSaveOption>
|
||||
|
||||
/**
|
||||
* The currently persisted dynamic colors setting (or `null` if not set).
|
||||
*/
|
||||
var isDynamicColorsEnabled: Boolean?
|
||||
|
||||
/**
|
||||
* Emits updates that track [isDynamicColorsEnabled].
|
||||
*/
|
||||
val isDynamicColorsEnabledFlow: Flow<Boolean?>
|
||||
|
||||
/**
|
||||
* The currently persisted biometric integrity source for the system.
|
||||
*/
|
||||
|
||||
@ -12,6 +12,7 @@ import kotlinx.coroutines.flow.onSubscription
|
||||
private const val APP_THEME_KEY = "theme"
|
||||
private const val APP_LANGUAGE_KEY = "appLocale"
|
||||
private const val DEFAULT_SAVE_OPTION_KEY = "defaultSaveOption"
|
||||
private const val DYNAMIC_COLORS_KEY = "dynamicColors"
|
||||
private const val SYSTEM_BIOMETRIC_INTEGRITY_SOURCE_KEY = "biometricIntegritySource"
|
||||
private const val ACCOUNT_BIOMETRIC_INTEGRITY_VALID_KEY = "accountBiometricIntegrityValid"
|
||||
private const val ALERT_THRESHOLD_SECONDS_KEY = "alertThresholdSeconds"
|
||||
@ -48,6 +49,8 @@ class SettingsDiskSourceImpl(
|
||||
private val mutableDefaultSaveOptionFlow =
|
||||
bufferedMutableSharedFlow<DefaultSaveOption>()
|
||||
|
||||
private val mutableDynamicColorsFlow = bufferedMutableSharedFlow<Boolean?>()
|
||||
|
||||
override var appLanguage: AppLanguage?
|
||||
get() = getString(key = APP_LANGUAGE_KEY)
|
||||
?.let { storedValue ->
|
||||
@ -98,6 +101,16 @@ class SettingsDiskSourceImpl(
|
||||
get() = mutableDefaultSaveOptionFlow
|
||||
.onSubscription { emit(defaultSaveOption) }
|
||||
|
||||
override var isDynamicColorsEnabled: Boolean?
|
||||
get() = getBoolean(key = DYNAMIC_COLORS_KEY)
|
||||
set(newValue) {
|
||||
putBoolean(key = DYNAMIC_COLORS_KEY, value = newValue)
|
||||
mutableDynamicColorsFlow.tryEmit(newValue)
|
||||
}
|
||||
|
||||
override val isDynamicColorsEnabledFlow: Flow<Boolean?>
|
||||
get() = mutableDynamicColorsFlow.onSubscription { emit(isDynamicColorsEnabled) }
|
||||
|
||||
override var systemBiometricIntegritySource: String?
|
||||
get() = getString(key = SYSTEM_BIOMETRIC_INTEGRITY_SOURCE_KEY)
|
||||
set(value) {
|
||||
|
||||
@ -37,6 +37,16 @@ interface SettingsRepository {
|
||||
*/
|
||||
var defaultSaveOption: DefaultSaveOption
|
||||
|
||||
/**
|
||||
* The current setting for enabling dynamic colors.
|
||||
*/
|
||||
var isDynamicColorsEnabled: Boolean
|
||||
|
||||
/**
|
||||
* Tracks changes to the [isDynamicColorsEnabled] value.
|
||||
*/
|
||||
val isDynamicColorsEnabledFlow: StateFlow<Boolean>
|
||||
|
||||
/**
|
||||
* Flow that emits changes to [defaultSaveOption]
|
||||
*/
|
||||
|
||||
@ -47,6 +47,22 @@ class SettingsRepositoryImpl(
|
||||
override val defaultSaveOptionFlow: Flow<DefaultSaveOption>
|
||||
by settingsDiskSource::defaultSaveOptionFlow
|
||||
|
||||
override var isDynamicColorsEnabled: Boolean
|
||||
get() = settingsDiskSource.isDynamicColorsEnabled ?: false
|
||||
set(value) {
|
||||
settingsDiskSource.isDynamicColorsEnabled = value
|
||||
}
|
||||
|
||||
override val isDynamicColorsEnabledFlow: StateFlow<Boolean>
|
||||
get() = settingsDiskSource
|
||||
.isDynamicColorsEnabledFlow
|
||||
.map { it ?: false }
|
||||
.stateIn(
|
||||
scope = unconfinedScope,
|
||||
started = SharingStarted.Eagerly,
|
||||
initialValue = isDynamicColorsEnabled,
|
||||
)
|
||||
|
||||
override val isUnlockWithBiometricsEnabled: Boolean
|
||||
get() = authDiskSource.getUserBiometricUnlockKey() != null
|
||||
|
||||
|
||||
@ -204,12 +204,19 @@ fun SettingsScreen(
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
AppearanceSettings(
|
||||
state = state,
|
||||
state = state.appearance,
|
||||
onThemeSelection = remember(viewModel) {
|
||||
{
|
||||
viewModel.trySendAction(SettingsAction.AppearanceChange.ThemeChange(it))
|
||||
}
|
||||
},
|
||||
onDynamicColorChange = remember(viewModel) {
|
||||
{
|
||||
viewModel.trySendAction(
|
||||
SettingsAction.AppearanceChange.DynamicColorChange(it),
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
Spacer(Modifier.height(16.dp))
|
||||
HelpSettings(
|
||||
@ -518,8 +525,9 @@ private fun ScreenCaptureRow(
|
||||
|
||||
@Composable
|
||||
private fun ColumnScope.AppearanceSettings(
|
||||
state: SettingsState,
|
||||
state: SettingsState.Appearance,
|
||||
onThemeSelection: (theme: AppTheme) -> Unit,
|
||||
onDynamicColorChange: (isEnabled: Boolean) -> Unit,
|
||||
) {
|
||||
BitwardenListHeaderText(
|
||||
modifier = Modifier
|
||||
@ -529,19 +537,33 @@ private fun ColumnScope.AppearanceSettings(
|
||||
)
|
||||
Spacer(modifier = Modifier.height(height = 8.dp))
|
||||
ThemeSelectionRow(
|
||||
currentSelection = state.appearance.theme,
|
||||
currentSelection = state.theme,
|
||||
onThemeSelection = onThemeSelection,
|
||||
cardStyle = if (state.isDynamicColorsSupported) CardStyle.Top() else CardStyle.Full,
|
||||
modifier = Modifier
|
||||
.testTag("ThemeChooser")
|
||||
.standardHorizontalMargin()
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
if (state.isDynamicColorsSupported) {
|
||||
BitwardenSwitch(
|
||||
label = stringResource(id = BitwardenString.use_dynamic_colors),
|
||||
isChecked = state.isDynamicColorsEnabled,
|
||||
onCheckedChange = onDynamicColorChange,
|
||||
cardStyle = CardStyle.Bottom,
|
||||
modifier = Modifier
|
||||
.testTag(tag = "DynamicColorsSwitch")
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ThemeSelectionRow(
|
||||
currentSelection: AppTheme,
|
||||
onThemeSelection: (AppTheme) -> Unit,
|
||||
cardStyle: CardStyle,
|
||||
modifier: Modifier = Modifier,
|
||||
resources: Resources = LocalResources.current,
|
||||
) {
|
||||
@ -555,7 +577,7 @@ private fun ThemeSelectionRow(
|
||||
.first { it.displayLabel(resources) == selectedOptionLabel }
|
||||
onThemeSelection(selectedOption)
|
||||
},
|
||||
cardStyle = CardStyle.Full,
|
||||
cardStyle = cardStyle,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
package com.bitwarden.authenticator.ui.platform.feature.settings
|
||||
|
||||
import android.os.Build
|
||||
import android.os.Parcelable
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.core.os.LocaleListCompat
|
||||
@ -16,6 +17,7 @@ import com.bitwarden.authenticator.ui.platform.feature.settings.appearance.model
|
||||
import com.bitwarden.authenticator.ui.platform.feature.settings.data.model.DefaultSaveOption
|
||||
import com.bitwarden.authenticatorbridge.manager.AuthenticatorBridgeManager
|
||||
import com.bitwarden.authenticatorbridge.manager.model.AccountSyncState
|
||||
import com.bitwarden.core.util.isBuildVersionAtLeast
|
||||
import com.bitwarden.ui.platform.base.BaseViewModel
|
||||
import com.bitwarden.ui.platform.feature.settings.appearance.model.AppTheme
|
||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
||||
@ -43,7 +45,7 @@ private const val KEY_STATE = "state"
|
||||
class SettingsViewModel @Inject constructor(
|
||||
savedStateHandle: SavedStateHandle,
|
||||
clock: Clock,
|
||||
private val authenticatorRepository: AuthenticatorRepository,
|
||||
authenticatorRepository: AuthenticatorRepository,
|
||||
private val authenticatorBridgeManager: AuthenticatorBridgeManager,
|
||||
private val settingsRepository: SettingsRepository,
|
||||
private val clipboardManager: BitwardenClipboardManager,
|
||||
@ -59,6 +61,7 @@ class SettingsViewModel @Inject constructor(
|
||||
defaultSaveOption = settingsRepository.defaultSaveOption,
|
||||
sharedAccountsState = authenticatorRepository.sharedCodesStateFlow.value,
|
||||
isScreenCaptureAllowed = settingsRepository.isScreenCaptureAllowed,
|
||||
isDynamicColorsEnabled = settingsRepository.isDynamicColorsEnabled,
|
||||
),
|
||||
) {
|
||||
|
||||
@ -66,13 +69,17 @@ class SettingsViewModel @Inject constructor(
|
||||
authenticatorRepository
|
||||
.sharedCodesStateFlow
|
||||
.map { SettingsAction.Internal.SharedAccountsStateUpdated(it) }
|
||||
.onEach(::handleAction)
|
||||
.onEach(::sendAction)
|
||||
.launchIn(viewModelScope)
|
||||
settingsRepository
|
||||
.isDynamicColorsEnabledFlow
|
||||
.map { SettingsAction.Internal.DynamicColorsUpdated(it) }
|
||||
.onEach(::sendAction)
|
||||
.launchIn(viewModelScope)
|
||||
|
||||
settingsRepository
|
||||
.defaultSaveOptionFlow
|
||||
.map { SettingsAction.Internal.DefaultSaveOptionUpdated(it) }
|
||||
.onEach(::handleAction)
|
||||
.onEach(::sendAction)
|
||||
.launchIn(viewModelScope)
|
||||
}
|
||||
|
||||
@ -109,6 +116,8 @@ class SettingsViewModel @Inject constructor(
|
||||
is SettingsAction.Internal.DefaultSaveOptionUpdated -> {
|
||||
handleDefaultSaveOptionUpdated(action)
|
||||
}
|
||||
|
||||
is SettingsAction.Internal.DynamicColorsUpdated -> handleDynamicColorsUpdated(action)
|
||||
}
|
||||
}
|
||||
|
||||
@ -213,6 +222,12 @@ class SettingsViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleDynamicColorsUpdated(action: SettingsAction.Internal.DynamicColorsUpdated) {
|
||||
mutableStateFlow.update {
|
||||
it.copy(appearance = it.appearance.copy(isDynamicColorsEnabled = action.isEnabled))
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleSyncWithBitwardenClick() {
|
||||
when (authenticatorBridgeManager.accountSyncStateFlow.value) {
|
||||
AccountSyncState.AppNotInstalled -> {
|
||||
@ -241,36 +256,46 @@ class SettingsViewModel @Inject constructor(
|
||||
|
||||
private fun handleAppearanceChange(action: SettingsAction.AppearanceChange) {
|
||||
when (action) {
|
||||
is SettingsAction.AppearanceChange.DynamicColorChange -> {
|
||||
handleDynamicColorChange(action)
|
||||
}
|
||||
|
||||
is SettingsAction.AppearanceChange.LanguageChange -> {
|
||||
handleLanguageChange(action.language)
|
||||
handleLanguageChange(action)
|
||||
}
|
||||
|
||||
is SettingsAction.AppearanceChange.ThemeChange -> {
|
||||
handleThemeChange(action.appTheme)
|
||||
handleThemeChange(action)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleLanguageChange(language: AppLanguage) {
|
||||
private fun handleDynamicColorChange(
|
||||
action: SettingsAction.AppearanceChange.DynamicColorChange,
|
||||
) {
|
||||
settingsRepository.isDynamicColorsEnabled = action.isEnabled
|
||||
}
|
||||
|
||||
private fun handleLanguageChange(action: SettingsAction.AppearanceChange.LanguageChange) {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
appearance = it.appearance.copy(language = language),
|
||||
appearance = it.appearance.copy(language = action.language),
|
||||
)
|
||||
}
|
||||
settingsRepository.appLanguage = language
|
||||
settingsRepository.appLanguage = action.language
|
||||
val appLocale: LocaleListCompat = LocaleListCompat.forLanguageTags(
|
||||
language.localeName,
|
||||
action.language.localeName,
|
||||
)
|
||||
AppCompatDelegate.setApplicationLocales(appLocale)
|
||||
}
|
||||
|
||||
private fun handleThemeChange(theme: AppTheme) {
|
||||
private fun handleThemeChange(action: SettingsAction.AppearanceChange.ThemeChange) {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
appearance = it.appearance.copy(theme = theme),
|
||||
appearance = it.appearance.copy(theme = action.appTheme),
|
||||
)
|
||||
}
|
||||
settingsRepository.appTheme = theme
|
||||
settingsRepository.appTheme = action.appTheme
|
||||
}
|
||||
|
||||
private fun handleHelpClick(action: SettingsAction.HelpClick) {
|
||||
@ -332,6 +357,7 @@ class SettingsViewModel @Inject constructor(
|
||||
accountSyncState: AccountSyncState,
|
||||
sharedAccountsState: SharedVerificationCodesState,
|
||||
isScreenCaptureAllowed: Boolean,
|
||||
isDynamicColorsEnabled: Boolean,
|
||||
): SettingsState {
|
||||
val currentYear = Year.now(clock)
|
||||
val copyrightInfo = "© Bitwarden Inc. 2015-$currentYear".asText()
|
||||
@ -345,6 +371,8 @@ class SettingsViewModel @Inject constructor(
|
||||
appearance = SettingsState.Appearance(
|
||||
language = appLanguage,
|
||||
theme = appTheme,
|
||||
isDynamicColorsSupported = isBuildVersionAtLeast(Build.VERSION_CODES.S),
|
||||
isDynamicColorsEnabled = isDynamicColorsEnabled,
|
||||
),
|
||||
isUnlockWithBiometricsEnabled = unlockWithBiometricsEnabled,
|
||||
isSubmitCrashLogsEnabled = isSubmitCrashLogsEnabled,
|
||||
@ -400,6 +428,8 @@ data class SettingsState(
|
||||
data class Appearance(
|
||||
val language: AppLanguage,
|
||||
val theme: AppTheme,
|
||||
val isDynamicColorsSupported: Boolean,
|
||||
val isDynamicColorsEnabled: Boolean,
|
||||
) : Parcelable
|
||||
}
|
||||
|
||||
@ -558,6 +588,13 @@ sealed class SettingsAction(
|
||||
data class ThemeChange(
|
||||
val appTheme: AppTheme,
|
||||
) : AppearanceChange()
|
||||
|
||||
/**
|
||||
* Indicates the user selected a new theme.
|
||||
*/
|
||||
data class DynamicColorChange(
|
||||
val isEnabled: Boolean,
|
||||
) : AppearanceChange()
|
||||
}
|
||||
|
||||
/**
|
||||
@ -604,5 +641,12 @@ sealed class SettingsAction(
|
||||
data class DefaultSaveOptionUpdated(
|
||||
val option: DefaultSaveOption,
|
||||
) : SettingsAction()
|
||||
|
||||
/**
|
||||
* Indicates that the dynamic colors state on disk was updated.
|
||||
*/
|
||||
data class DynamicColorsUpdated(
|
||||
val isEnabled: Boolean,
|
||||
) : SettingsAction()
|
||||
}
|
||||
}
|
||||
|
||||
@ -17,28 +17,28 @@ class MainViewModelTest : BaseViewModelTest() {
|
||||
|
||||
private val mutableAppThemeFlow = MutableStateFlow(AppTheme.DEFAULT)
|
||||
private val mutableScreenCaptureAllowedFlow = MutableStateFlow(false)
|
||||
private val mutableIsDynamicColorsEnabledFlow = MutableStateFlow(false)
|
||||
private val settingsRepository = mockk<SettingsRepository> {
|
||||
every { appTheme } returns AppTheme.DEFAULT
|
||||
every { appThemeStateFlow } returns mutableAppThemeFlow
|
||||
every { isScreenCaptureAllowedStateFlow } returns mutableScreenCaptureAllowedFlow
|
||||
every { isDynamicColorsEnabled } returns false
|
||||
every { isDynamicColorsEnabledFlow } returns mutableIsDynamicColorsEnabledFlow
|
||||
}
|
||||
private val fakeServerConfigRepository = FakeServerConfigRepository()
|
||||
private val mainViewModel: MainViewModel = MainViewModel(
|
||||
settingsRepository = settingsRepository,
|
||||
configRepository = fakeServerConfigRepository,
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `on AppThemeChanged should update state`() = runTest {
|
||||
mainViewModel.stateEventFlow(backgroundScope) { stateFlow, eventFlow ->
|
||||
val viewModel = createViewModel()
|
||||
viewModel.stateEventFlow(backgroundScope) { stateFlow, eventFlow ->
|
||||
eventFlow.skipItems(count = 2)
|
||||
assertEquals(
|
||||
MainState(theme = AppTheme.DEFAULT),
|
||||
DEFAULT_STATE,
|
||||
stateFlow.awaitItem(),
|
||||
)
|
||||
mainViewModel.trySendAction(MainAction.Internal.ThemeUpdate(theme = AppTheme.DARK))
|
||||
viewModel.trySendAction(MainAction.Internal.ThemeUpdate(theme = AppTheme.DARK))
|
||||
assertEquals(
|
||||
MainState(theme = AppTheme.DARK),
|
||||
DEFAULT_STATE.copy(theme = AppTheme.DARK),
|
||||
stateFlow.awaitItem(),
|
||||
)
|
||||
assertEquals(
|
||||
@ -53,13 +53,46 @@ class MainViewModelTest : BaseViewModelTest() {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on DynamicColorUpdate should update state`() = runTest {
|
||||
val viewModel = createViewModel()
|
||||
viewModel.stateFlow.test {
|
||||
assertEquals(
|
||||
DEFAULT_STATE,
|
||||
awaitItem(),
|
||||
)
|
||||
viewModel.trySendAction(MainAction.Internal.DynamicColorUpdate(isEnabled = true))
|
||||
assertEquals(
|
||||
DEFAULT_STATE.copy(isDynamicColorsEnabled = true),
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
|
||||
verify {
|
||||
settingsRepository.isDynamicColorsEnabled
|
||||
settingsRepository.isDynamicColorsEnabledFlow
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `send NavigateToDebugMenu action when OpenDebugMenu action is sent`() = runTest {
|
||||
mainViewModel.eventFlow.test {
|
||||
val viewModel = createViewModel()
|
||||
viewModel.eventFlow.test {
|
||||
// Ignore the events that are fired off by flows in the ViewModel init
|
||||
skipItems(2)
|
||||
mainViewModel.trySendAction(MainAction.OpenDebugMenu)
|
||||
viewModel.trySendAction(MainAction.OpenDebugMenu)
|
||||
assertEquals(MainEvent.NavigateToDebugMenu, awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
private fun createViewModel(): MainViewModel =
|
||||
MainViewModel(
|
||||
settingsRepository = settingsRepository,
|
||||
configRepository = fakeServerConfigRepository,
|
||||
)
|
||||
}
|
||||
|
||||
private val DEFAULT_STATE = MainState(
|
||||
theme = AppTheme.DEFAULT,
|
||||
isDynamicColorsEnabled = false,
|
||||
)
|
||||
|
||||
@ -7,6 +7,7 @@ import com.bitwarden.authenticator.data.authenticator.datasource.sdk.Authenticat
|
||||
import com.bitwarden.authenticator.data.platform.datasource.disk.SettingsDiskSource
|
||||
import com.bitwarden.authenticator.data.platform.manager.BiometricsEncryptionManager
|
||||
import com.bitwarden.authenticator.ui.platform.feature.settings.data.model.DefaultSaveOption
|
||||
import com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow
|
||||
import com.bitwarden.data.datasource.disk.base.FakeDispatcherManager
|
||||
import io.mockk.every
|
||||
import io.mockk.just
|
||||
@ -117,6 +118,36 @@ class SettingsRepositoryTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `isDynamicColorsEnabled should pull from and update SettingsDiskSource`() {
|
||||
// Reading from repository should read from disk source:
|
||||
every { settingsDiskSource.isDynamicColorsEnabled } returns null
|
||||
assertFalse(settingsRepository.isDynamicColorsEnabled)
|
||||
verify { settingsDiskSource.isDynamicColorsEnabled }
|
||||
|
||||
// Writing to repository should write to disk source:
|
||||
every { settingsDiskSource.isDynamicColorsEnabled = true } just runs
|
||||
settingsRepository.isDynamicColorsEnabled = true
|
||||
verify { settingsDiskSource.isDynamicColorsEnabled = true }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `isDynamicColorsEnabledFlow should match SettingsDiskSource`() = runTest {
|
||||
// Reading from repository should read from disk source:
|
||||
val mutableDynamicColorsFlow = bufferedMutableSharedFlow<Boolean?>()
|
||||
every { settingsDiskSource.isDynamicColorsEnabledFlow } returns mutableDynamicColorsFlow
|
||||
every { settingsDiskSource.isDynamicColorsEnabled } returns null
|
||||
|
||||
settingsRepository.isDynamicColorsEnabledFlow.test {
|
||||
assertFalse(awaitItem())
|
||||
mutableDynamicColorsFlow.emit(true)
|
||||
assertTrue(awaitItem())
|
||||
mutableDynamicColorsFlow.emit(false)
|
||||
assertFalse(awaitItem())
|
||||
expectNoEvents()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `previouslySyncedBitwardenAccountIds should pull from and update SettingsDiskSource`() {
|
||||
// Reading from repository should read from disk source:
|
||||
|
||||
@ -208,6 +208,17 @@ class SettingsScreenTest : AuthenticatorComposeTest() {
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on use dynamic colors row click should send DynamicColorChange event`() {
|
||||
composeTestRule
|
||||
.onNodeWithText(text = "Use dynamic colors")
|
||||
.performScrollTo()
|
||||
.performClick()
|
||||
verify(exactly = 1) {
|
||||
viewModel.trySendAction(SettingsAction.AppearanceChange.DynamicColorChange(true))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val APP_LANGUAGE = AppLanguage.ENGLISH
|
||||
@ -215,8 +226,10 @@ private val APP_THEME = AppTheme.DEFAULT
|
||||
private val DEFAULT_SAVE_OPTION = DefaultSaveOption.NONE
|
||||
private val DEFAULT_STATE = SettingsState(
|
||||
appearance = SettingsState.Appearance(
|
||||
APP_LANGUAGE,
|
||||
APP_THEME,
|
||||
language = APP_LANGUAGE,
|
||||
theme = APP_THEME,
|
||||
isDynamicColorsSupported = true,
|
||||
isDynamicColorsEnabled = false,
|
||||
),
|
||||
isSubmitCrashLogsEnabled = true,
|
||||
isUnlockWithBiometricsEnabled = true,
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
package com.bitwarden.authenticator.ui.platform.feature.settings
|
||||
|
||||
import android.os.Build
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import app.cash.turbine.test
|
||||
import com.bitwarden.authenticator.BuildConfig
|
||||
@ -13,6 +14,7 @@ import com.bitwarden.authenticator.ui.platform.feature.settings.data.model.Defau
|
||||
import com.bitwarden.authenticatorbridge.manager.AuthenticatorBridgeManager
|
||||
import com.bitwarden.authenticatorbridge.manager.model.AccountSyncState
|
||||
import com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow
|
||||
import com.bitwarden.core.util.isBuildVersionAtLeast
|
||||
import com.bitwarden.ui.platform.base.BaseViewModelTest
|
||||
import com.bitwarden.ui.platform.feature.settings.appearance.model.AppTheme
|
||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
||||
@ -48,6 +50,7 @@ class SettingsViewModelTest : BaseViewModelTest() {
|
||||
}
|
||||
private val mutableDefaultSaveOptionFlow = bufferedMutableSharedFlow<DefaultSaveOption>()
|
||||
private val mutableScreenCaptureAllowedStateFlow = MutableStateFlow(false)
|
||||
private val mutableIsDynamicColorsEnabledFlow = MutableStateFlow(false)
|
||||
private val settingsRepository: SettingsRepository = mockk {
|
||||
every { appLanguage } returns APP_LANGUAGE
|
||||
every { appTheme } returns APP_THEME
|
||||
@ -58,6 +61,9 @@ class SettingsViewModelTest : BaseViewModelTest() {
|
||||
every { isScreenCaptureAllowedStateFlow } returns mutableScreenCaptureAllowedStateFlow
|
||||
every { isScreenCaptureAllowed } answers { mutableScreenCaptureAllowedStateFlow.value }
|
||||
every { isScreenCaptureAllowed = any() } just runs
|
||||
every { isDynamicColorsEnabled } answers { mutableIsDynamicColorsEnabledFlow.value }
|
||||
every { isDynamicColorsEnabled = any() } just runs
|
||||
every { isDynamicColorsEnabledFlow } returns mutableIsDynamicColorsEnabledFlow
|
||||
}
|
||||
private val clipboardManager: BitwardenClipboardManager = mockk()
|
||||
|
||||
@ -65,11 +71,14 @@ class SettingsViewModelTest : BaseViewModelTest() {
|
||||
fun setup() {
|
||||
mockkStatic(SharedVerificationCodesState::isSyncWithBitwardenEnabled)
|
||||
every { MOCK_SHARED_CODES_STATE.isSyncWithBitwardenEnabled } returns false
|
||||
mockkStatic(::isBuildVersionAtLeast)
|
||||
every { isBuildVersionAtLeast(Build.VERSION_CODES.S) } returns true
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
fun teardown() {
|
||||
unmockkStatic(SharedVerificationCodesState::isSyncWithBitwardenEnabled)
|
||||
unmockkStatic(::isBuildVersionAtLeast)
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -225,6 +234,35 @@ class SettingsViewModelTest : BaseViewModelTest() {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on DynamicColorChange should update value in state and SettingsRepository`() =
|
||||
runTest {
|
||||
val viewModel = createViewModel()
|
||||
|
||||
viewModel.trySendAction(
|
||||
SettingsAction.AppearanceChange.DynamicColorChange(isEnabled = true),
|
||||
)
|
||||
|
||||
verify(exactly = 1) {
|
||||
settingsRepository.isDynamicColorsEnabled = true
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on DynamicColorsUpdated should update value in state and SettingsRepository`() =
|
||||
runTest {
|
||||
val viewModel = createViewModel()
|
||||
|
||||
viewModel.trySendAction(SettingsAction.Internal.DynamicColorsUpdated(isEnabled = true))
|
||||
|
||||
assertEquals(
|
||||
DEFAULT_STATE.copy(
|
||||
appearance = DEFAULT_APPEARANCE_STATE.copy(isDynamicColorsEnabled = true),
|
||||
),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
}
|
||||
|
||||
private fun createViewModel(
|
||||
savedState: SettingsState? = DEFAULT_STATE,
|
||||
) = SettingsViewModel(
|
||||
@ -245,11 +283,14 @@ private val CLOCK = Clock.fixed(
|
||||
ZoneOffset.UTC,
|
||||
)
|
||||
private val DEFAULT_SAVE_OPTION = DefaultSaveOption.NONE
|
||||
private val DEFAULT_APPEARANCE_STATE = SettingsState.Appearance(
|
||||
language = APP_LANGUAGE,
|
||||
theme = APP_THEME,
|
||||
isDynamicColorsSupported = true,
|
||||
isDynamicColorsEnabled = false,
|
||||
)
|
||||
private val DEFAULT_STATE = SettingsState(
|
||||
appearance = SettingsState.Appearance(
|
||||
APP_LANGUAGE,
|
||||
APP_THEME,
|
||||
),
|
||||
appearance = DEFAULT_APPEARANCE_STATE,
|
||||
isSubmitCrashLogsEnabled = true,
|
||||
isUnlockWithBiometricsEnabled = true,
|
||||
showSyncWithBitwarden = true,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user