[PM-21750] Only show dynamic colors option on Android 12+ (#5507)

This commit is contained in:
Patrick Honkonen 2025-07-10 15:40:28 -04:00 committed by GitHub
parent e193661f5f
commit f9914e5b46
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 85 additions and 20 deletions

View File

@ -27,6 +27,7 @@ import com.bitwarden.ui.platform.base.util.EventsEffect
import com.bitwarden.ui.platform.base.util.standardHorizontalMargin
import com.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar
import com.bitwarden.ui.platform.components.model.CardStyle
import com.bitwarden.ui.platform.components.toggle.BitwardenSwitch
import com.bitwarden.ui.platform.components.util.rememberVectorPainter
import com.bitwarden.ui.platform.feature.settings.appearance.model.AppTheme
import com.bitwarden.ui.platform.resource.BitwardenDrawable
@ -34,7 +35,6 @@ import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenTwoButtonDialog
import com.x8bit.bitwarden.ui.platform.components.dropdown.BitwardenMultiSelectButton
import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
import com.bitwarden.ui.platform.components.toggle.BitwardenSwitch
import com.x8bit.bitwarden.ui.platform.feature.settings.appearance.model.AppLanguage
import com.x8bit.bitwarden.ui.platform.util.displayLabel
import kotlinx.collections.immutable.toImmutableList
@ -111,20 +111,22 @@ fun AppearanceScreen(
.fillMaxWidth(),
)
Spacer(modifier = Modifier.height(height = 8.dp))
BitwardenSwitch(
label = stringResource(id = R.string.dynamic_colors),
supportingText = stringResource(id = R.string.dynamic_colors_description),
isChecked = state.isDynamicColorsEnabled,
onCheckedChange = remember(viewModel) {
{ viewModel.trySendAction(AppearanceAction.DynamicColorsToggle(it)) }
},
cardStyle = CardStyle.Full,
modifier = Modifier
.testTag("DynamicColorsSwitch")
.fillMaxWidth()
.standardHorizontalMargin(),
)
Spacer(modifier = Modifier.height(height = 8.dp))
if (state.isDynamicColorsSupported) {
BitwardenSwitch(
label = stringResource(id = R.string.dynamic_colors),
supportingText = stringResource(id = R.string.dynamic_colors_description),
isChecked = state.isDynamicColorsEnabled,
onCheckedChange = remember(viewModel) {
{ viewModel.trySendAction(AppearanceAction.DynamicColorsToggle(it)) }
},
cardStyle = CardStyle.Full,
modifier = Modifier
.testTag("DynamicColorsSwitch")
.fillMaxWidth()
.standardHorizontalMargin(),
)
Spacer(modifier = Modifier.height(height = 8.dp))
}
BitwardenSwitch(
label = stringResource(id = R.string.show_website_icons),
supportingText = stringResource(id = R.string.show_website_icons_description),

View File

@ -1,8 +1,10 @@
package com.x8bit.bitwarden.ui.platform.feature.settings.appearance
import android.os.Build
import android.os.Parcelable
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
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.x8bit.bitwarden.data.platform.repository.SettingsRepository
@ -31,6 +33,7 @@ class AppearanceViewModel @Inject constructor(
showWebsiteIcons = !settingsRepository.isIconLoadingDisabled,
theme = settingsRepository.appTheme,
isDynamicColorsEnabled = settingsRepository.isDynamicColorsEnabled,
isDynamicColorsSupported = isBuildVersionAtLeast(Build.VERSION_CODES.S),
dialogState = null,
),
) {
@ -42,11 +45,13 @@ class AppearanceViewModel @Inject constructor(
.onEach(::sendAction)
.launchIn(viewModelScope)
settingsRepository
.isDynamicColorsEnabledFlow
.map { AppearanceAction.Internal.DynamicColorsStateUpdateReceive(it) }
.onEach(::sendAction)
.launchIn(viewModelScope)
if (state.isDynamicColorsSupported) {
settingsRepository
.isDynamicColorsEnabledFlow
.map { AppearanceAction.Internal.DynamicColorsStateUpdateReceive(it) }
.onEach(::sendAction)
.launchIn(viewModelScope)
}
}
override fun handleAction(action: AppearanceAction): Unit = when (action) {
@ -139,6 +144,7 @@ data class AppearanceState(
val language: AppLanguage,
val showWebsiteIcons: Boolean,
val theme: AppTheme,
val isDynamicColorsSupported: Boolean,
val isDynamicColorsEnabled: Boolean,
val dialogState: DialogState?,
) : Parcelable {

View File

@ -11,15 +11,19 @@ import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performScrollTo
import com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow
import com.bitwarden.core.util.isBuildVersionAtLeast
import com.bitwarden.ui.platform.feature.settings.appearance.model.AppTheme
import com.bitwarden.ui.util.assertNoDialogExists
import com.x8bit.bitwarden.ui.platform.base.BitwardenComposeTest
import com.x8bit.bitwarden.ui.platform.feature.settings.appearance.model.AppLanguage
import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkStatic
import io.mockk.unmockkStatic
import io.mockk.verify
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
import org.junit.After
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
@ -36,6 +40,8 @@ class AppearanceScreenTest : BitwardenComposeTest() {
@Before
fun setup() {
mockkStatic(::isBuildVersionAtLeast)
every { isBuildVersionAtLeast(any()) } returns true
setContent {
AppearanceScreen(
onNavigateBack = { haveCalledNavigateBack = true },
@ -44,6 +50,11 @@ class AppearanceScreenTest : BitwardenComposeTest() {
}
}
@After
fun tearDown() {
unmockkStatic(::isBuildVersionAtLeast)
}
@Test
fun `on back click should send BackClick`() {
composeTestRule.onNodeWithContentDescription("Back").performClick()
@ -167,6 +178,19 @@ class AppearanceScreenTest : BitwardenComposeTest() {
assertTrue(haveCalledNavigateBack)
}
@Test
fun `dynamic colors should be displayed based on state`() {
composeTestRule.onNodeWithText("Dynamic colors")
.performScrollTo()
.assertIsDisplayed()
mutableStateFlow.update {
it.copy(isDynamicColorsSupported = false)
}
composeTestRule.onNodeWithText("Dynamic colors")
.assertIsNotDisplayed()
}
@Test
fun `on DynamicColorsToggle should send DynamicColorsToggle`() {
composeTestRule.onNodeWithText("Dynamic colors")
@ -203,5 +227,6 @@ private val DEFAULT_STATE = AppearanceState(
showWebsiteIcons = false,
theme = AppTheme.DEFAULT,
isDynamicColorsEnabled = false,
isDynamicColorsSupported = true,
dialogState = null,
)

View File

@ -1,7 +1,9 @@
package com.x8bit.bitwarden.ui.platform.feature.settings.appearance
import android.os.Build
import androidx.lifecycle.SavedStateHandle
import app.cash.turbine.test
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.x8bit.bitwarden.data.platform.repository.SettingsRepository
@ -9,12 +11,16 @@ import com.x8bit.bitwarden.ui.platform.feature.settings.appearance.model.AppLang
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
import io.mockk.mockkStatic
import io.mockk.runs
import io.mockk.unmockkStatic
import io.mockk.verify
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
class AppearanceViewModelTest : BaseViewModelTest() {
@ -33,6 +39,17 @@ class AppearanceViewModelTest : BaseViewModelTest() {
every { isDynamicColorsEnabledFlow } returns mutableIsDynamicColorsEnabledFlow
}
@BeforeEach
fun setUp() {
mockkStatic(::isBuildVersionAtLeast)
every { isBuildVersionAtLeast(any()) } returns true
}
@AfterEach
fun tearDown() {
unmockkStatic(::isBuildVersionAtLeast)
}
@Test
fun `initial state should be correct when not set`() {
val viewModel = createViewModel(state = null)
@ -46,6 +63,20 @@ class AppearanceViewModelTest : BaseViewModelTest() {
assertEquals(state, viewModel.stateFlow.value)
}
@Test
fun `initial state should be correct when build version is below 31`() {
every { isBuildVersionAtLeast(Build.VERSION_CODES.S) } returns false
val viewModel = createViewModel(state = null)
assertEquals(
DEFAULT_STATE.copy(isDynamicColorsSupported = false),
viewModel.stateFlow.value,
)
verify(exactly = 0) {
mockSettingsRepository.isDynamicColorsEnabledFlow
}
}
@Test
fun `on BackClick should emit NavigateBack`() = runTest {
val viewModel = createViewModel()
@ -210,6 +241,7 @@ class AppearanceViewModelTest : BaseViewModelTest() {
language = AppLanguage.DEFAULT,
showWebsiteIcons = true,
theme = AppTheme.DEFAULT,
isDynamicColorsSupported = true,
isDynamicColorsEnabled = false,
dialogState = null,
)