Skip to content

Commit 4597337

Browse files
PM-27210: Add dynamic color support to Authenticator (#6063)
1 parent e610a75 commit 4597337

File tree

12 files changed

+297
-39
lines changed

12 files changed

+297
-39
lines changed

authenticator/src/main/kotlin/com/bitwarden/authenticator/MainActivity.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ class MainActivity : AppCompatActivity() {
6767
LocalManagerProvider {
6868
BitwardenTheme(
6969
theme = state.theme,
70+
dynamicColor = state.isDynamicColorsEnabled,
7071
) {
7172
RootNavScreen(
7273
navController = navController,

authenticator/src/main/kotlin/com/bitwarden/authenticator/MainViewModel.kt

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import com.bitwarden.ui.platform.base.BaseViewModel
99
import com.bitwarden.ui.platform.feature.settings.appearance.model.AppTheme
1010
import dagger.hilt.android.lifecycle.HiltViewModel
1111
import kotlinx.coroutines.flow.launchIn
12+
import kotlinx.coroutines.flow.map
1213
import kotlinx.coroutines.flow.onEach
1314
import kotlinx.coroutines.flow.update
1415
import kotlinx.coroutines.launch
@@ -25,20 +26,25 @@ class MainViewModel @Inject constructor(
2526
) : BaseViewModel<MainState, MainEvent, MainAction>(
2627
MainState(
2728
theme = settingsRepository.appTheme,
29+
isDynamicColorsEnabled = settingsRepository.isDynamicColorsEnabled,
2830
),
2931
) {
3032

3133
init {
3234
settingsRepository
3335
.appThemeStateFlow
34-
.onEach { trySendAction(MainAction.Internal.ThemeUpdate(it)) }
36+
.map { MainAction.Internal.ThemeUpdate(it) }
37+
.onEach(::sendAction)
38+
.launchIn(viewModelScope)
39+
settingsRepository
40+
.isDynamicColorsEnabledFlow
41+
.map { MainAction.Internal.DynamicColorUpdate(it) }
42+
.onEach(::sendAction)
3543
.launchIn(viewModelScope)
36-
3744
settingsRepository
3845
.isScreenCaptureAllowedStateFlow
39-
.onEach { isAllowed ->
40-
sendEvent(MainEvent.ScreenCaptureSettingChange(isAllowed))
41-
}
46+
.map { MainEvent.ScreenCaptureSettingChange(it) }
47+
.onEach(::sendEvent)
4248
.launchIn(viewModelScope)
4349
viewModelScope.launch {
4450
configRepository.getServerConfig(forceRefresh = false)
@@ -47,17 +53,28 @@ class MainViewModel @Inject constructor(
4753

4854
override fun handleAction(action: MainAction) {
4955
when (action) {
50-
is MainAction.Internal.ThemeUpdate -> handleThemeUpdated(action)
5156
is MainAction.ReceiveFirstIntent -> handleFirstIntentReceived(action)
5257
is MainAction.ReceiveNewIntent -> handleNewIntentReceived(action)
5358
MainAction.OpenDebugMenu -> handleOpenDebugMenu()
59+
is MainAction.Internal -> handleInternalAction(action)
60+
}
61+
}
62+
63+
private fun handleInternalAction(action: MainAction.Internal) {
64+
when (action) {
65+
is MainAction.Internal.DynamicColorUpdate -> handleDynamicColorUpdate(action)
66+
is MainAction.Internal.ThemeUpdate -> handleThemeUpdated(action)
5467
}
5568
}
5669

5770
private fun handleOpenDebugMenu() {
5871
sendEvent(MainEvent.NavigateToDebugMenu)
5972
}
6073

74+
private fun handleDynamicColorUpdate(action: MainAction.Internal.DynamicColorUpdate) {
75+
mutableStateFlow.update { it.copy(isDynamicColorsEnabled = action.isEnabled) }
76+
}
77+
6178
private fun handleThemeUpdated(action: MainAction.Internal.ThemeUpdate) {
6279
mutableStateFlow.update { it.copy(theme = action.theme) }
6380
sendEvent(MainEvent.UpdateAppTheme(osTheme = action.theme.osValue))
@@ -91,6 +108,7 @@ class MainViewModel @Inject constructor(
91108
@Parcelize
92109
data class MainState(
93110
val theme: AppTheme,
111+
val isDynamicColorsEnabled: Boolean,
94112
) : Parcelable
95113

96114
/**
@@ -116,6 +134,12 @@ sealed class MainAction {
116134
* Actions for internal use by the ViewModel.
117135
*/
118136
sealed class Internal : MainAction() {
137+
/**
138+
* Indicates that dynamic colors have been enabled or disabled.
139+
*/
140+
data class DynamicColorUpdate(
141+
val isEnabled: Boolean,
142+
) : Internal()
119143

120144
/**
121145
* Indicates that the app theme has changed.

authenticator/src/main/kotlin/com/bitwarden/authenticator/data/platform/datasource/disk/SettingsDiskSource.kt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,16 @@ interface SettingsDiskSource {
3535
*/
3636
val defaultSaveOptionFlow: Flow<DefaultSaveOption>
3737

38+
/**
39+
* The currently persisted dynamic colors setting (or `null` if not set).
40+
*/
41+
var isDynamicColorsEnabled: Boolean?
42+
43+
/**
44+
* Emits updates that track [isDynamicColorsEnabled].
45+
*/
46+
val isDynamicColorsEnabledFlow: Flow<Boolean?>
47+
3848
/**
3949
* The currently persisted biometric integrity source for the system.
4050
*/

authenticator/src/main/kotlin/com/bitwarden/authenticator/data/platform/datasource/disk/SettingsDiskSourceImpl.kt

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import kotlinx.coroutines.flow.onSubscription
1212
private const val APP_THEME_KEY = "theme"
1313
private const val APP_LANGUAGE_KEY = "appLocale"
1414
private const val DEFAULT_SAVE_OPTION_KEY = "defaultSaveOption"
15+
private const val DYNAMIC_COLORS_KEY = "dynamicColors"
1516
private const val SYSTEM_BIOMETRIC_INTEGRITY_SOURCE_KEY = "biometricIntegritySource"
1617
private const val ACCOUNT_BIOMETRIC_INTEGRITY_VALID_KEY = "accountBiometricIntegrityValid"
1718
private const val ALERT_THRESHOLD_SECONDS_KEY = "alertThresholdSeconds"
@@ -48,6 +49,8 @@ class SettingsDiskSourceImpl(
4849
private val mutableDefaultSaveOptionFlow =
4950
bufferedMutableSharedFlow<DefaultSaveOption>()
5051

52+
private val mutableDynamicColorsFlow = bufferedMutableSharedFlow<Boolean?>()
53+
5154
override var appLanguage: AppLanguage?
5255
get() = getString(key = APP_LANGUAGE_KEY)
5356
?.let { storedValue ->
@@ -98,6 +101,16 @@ class SettingsDiskSourceImpl(
98101
get() = mutableDefaultSaveOptionFlow
99102
.onSubscription { emit(defaultSaveOption) }
100103

104+
override var isDynamicColorsEnabled: Boolean?
105+
get() = getBoolean(key = DYNAMIC_COLORS_KEY)
106+
set(newValue) {
107+
putBoolean(key = DYNAMIC_COLORS_KEY, value = newValue)
108+
mutableDynamicColorsFlow.tryEmit(newValue)
109+
}
110+
111+
override val isDynamicColorsEnabledFlow: Flow<Boolean?>
112+
get() = mutableDynamicColorsFlow.onSubscription { emit(isDynamicColorsEnabled) }
113+
101114
override var systemBiometricIntegritySource: String?
102115
get() = getString(key = SYSTEM_BIOMETRIC_INTEGRITY_SOURCE_KEY)
103116
set(value) {

authenticator/src/main/kotlin/com/bitwarden/authenticator/data/platform/repository/SettingsRepository.kt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,16 @@ interface SettingsRepository {
3737
*/
3838
var defaultSaveOption: DefaultSaveOption
3939

40+
/**
41+
* The current setting for enabling dynamic colors.
42+
*/
43+
var isDynamicColorsEnabled: Boolean
44+
45+
/**
46+
* Tracks changes to the [isDynamicColorsEnabled] value.
47+
*/
48+
val isDynamicColorsEnabledFlow: StateFlow<Boolean>
49+
4050
/**
4151
* Flow that emits changes to [defaultSaveOption]
4252
*/

authenticator/src/main/kotlin/com/bitwarden/authenticator/data/platform/repository/SettingsRepositoryImpl.kt

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,22 @@ class SettingsRepositoryImpl(
4747
override val defaultSaveOptionFlow: Flow<DefaultSaveOption>
4848
by settingsDiskSource::defaultSaveOptionFlow
4949

50+
override var isDynamicColorsEnabled: Boolean
51+
get() = settingsDiskSource.isDynamicColorsEnabled ?: false
52+
set(value) {
53+
settingsDiskSource.isDynamicColorsEnabled = value
54+
}
55+
56+
override val isDynamicColorsEnabledFlow: StateFlow<Boolean>
57+
get() = settingsDiskSource
58+
.isDynamicColorsEnabledFlow
59+
.map { it ?: false }
60+
.stateIn(
61+
scope = unconfinedScope,
62+
started = SharingStarted.Eagerly,
63+
initialValue = isDynamicColorsEnabled,
64+
)
65+
5066
override val isUnlockWithBiometricsEnabled: Boolean
5167
get() = authDiskSource.getUserBiometricUnlockKey() != null
5268

authenticator/src/main/kotlin/com/bitwarden/authenticator/ui/platform/feature/settings/SettingsScreen.kt

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -204,12 +204,19 @@ fun SettingsScreen(
204204
)
205205
Spacer(modifier = Modifier.height(16.dp))
206206
AppearanceSettings(
207-
state = state,
207+
state = state.appearance,
208208
onThemeSelection = remember(viewModel) {
209209
{
210210
viewModel.trySendAction(SettingsAction.AppearanceChange.ThemeChange(it))
211211
}
212212
},
213+
onDynamicColorChange = remember(viewModel) {
214+
{
215+
viewModel.trySendAction(
216+
SettingsAction.AppearanceChange.DynamicColorChange(it),
217+
)
218+
}
219+
},
213220
)
214221
Spacer(Modifier.height(16.dp))
215222
HelpSettings(
@@ -518,8 +525,9 @@ private fun ScreenCaptureRow(
518525

519526
@Composable
520527
private fun ColumnScope.AppearanceSettings(
521-
state: SettingsState,
528+
state: SettingsState.Appearance,
522529
onThemeSelection: (theme: AppTheme) -> Unit,
530+
onDynamicColorChange: (isEnabled: Boolean) -> Unit,
523531
) {
524532
BitwardenListHeaderText(
525533
modifier = Modifier
@@ -529,19 +537,33 @@ private fun ColumnScope.AppearanceSettings(
529537
)
530538
Spacer(modifier = Modifier.height(height = 8.dp))
531539
ThemeSelectionRow(
532-
currentSelection = state.appearance.theme,
540+
currentSelection = state.theme,
533541
onThemeSelection = onThemeSelection,
542+
cardStyle = if (state.isDynamicColorsSupported) CardStyle.Top() else CardStyle.Full,
534543
modifier = Modifier
535544
.testTag("ThemeChooser")
536545
.standardHorizontalMargin()
537546
.fillMaxWidth(),
538547
)
548+
if (state.isDynamicColorsSupported) {
549+
BitwardenSwitch(
550+
label = stringResource(id = BitwardenString.use_dynamic_colors),
551+
isChecked = state.isDynamicColorsEnabled,
552+
onCheckedChange = onDynamicColorChange,
553+
cardStyle = CardStyle.Bottom,
554+
modifier = Modifier
555+
.testTag(tag = "DynamicColorsSwitch")
556+
.fillMaxWidth()
557+
.standardHorizontalMargin(),
558+
)
559+
}
539560
}
540561

541562
@Composable
542563
private fun ThemeSelectionRow(
543564
currentSelection: AppTheme,
544565
onThemeSelection: (AppTheme) -> Unit,
566+
cardStyle: CardStyle,
545567
modifier: Modifier = Modifier,
546568
resources: Resources = LocalResources.current,
547569
) {
@@ -555,7 +577,7 @@ private fun ThemeSelectionRow(
555577
.first { it.displayLabel(resources) == selectedOptionLabel }
556578
onThemeSelection(selectedOption)
557579
},
558-
cardStyle = CardStyle.Full,
580+
cardStyle = cardStyle,
559581
modifier = modifier,
560582
)
561583
}

0 commit comments

Comments
 (0)