Add dynamic color option in settings for API >=32

Change-Id: Ib42c1cbd0208e98ef7610a1c8aca5d721aa464e6
pull/542/head
Angie Sasmita 1 year ago
parent eb6dbaac0b
commit 6046f6e943

@ -106,7 +106,8 @@ class MainActivity : ComponentActivity() {
NiaTheme(
darkTheme = darkTheme,
androidTheme = shouldUseAndroidTheme(uiState)
androidTheme = shouldUseAndroidTheme(uiState),
disableDynamicTheming = shouldDisableDynamicTheming(uiState)
) {
NiaApp(
networkMonitor = networkMonitor,
@ -141,6 +142,17 @@ private fun shouldUseAndroidTheme(
}
}
/**
* Returns `true` if the dynamic color is disabled, as a function of the [uiState].
*/
@Composable
private fun shouldDisableDynamicTheming(
uiState: MainActivityUiState,
): Boolean = when (uiState) {
Loading -> false
is Success -> !uiState.userData.useDynamicColor
}
/**
* Returns `true` if dark theme should be used, as a function of the [uiState] and the
* current system context.

@ -45,6 +45,9 @@ class OfflineFirstUserDataRepository @Inject constructor(
override suspend fun setDarkThemeConfig(darkThemeConfig: DarkThemeConfig) =
niaPreferencesDataSource.setDarkThemeConfig(darkThemeConfig)
override suspend fun setDynamicColorPreference(useDynamicColor: Boolean) =
niaPreferencesDataSource.setDynamicColorPreference(useDynamicColor)
override suspend fun setShouldHideOnboarding(shouldHideOnboarding: Boolean) =
niaPreferencesDataSource.setShouldHideOnboarding(shouldHideOnboarding)
}

@ -53,6 +53,11 @@ interface UserDataRepository {
*/
suspend fun setDarkThemeConfig(darkThemeConfig: DarkThemeConfig)
/**
* Sets the preferred dynamic color config.
*/
suspend fun setDynamicColorPreference(useDynamicColor: Boolean)
/**
* Sets whether the user has completed the onboarding process.
*/

@ -55,6 +55,10 @@ class FakeUserDataRepository @Inject constructor(
niaPreferencesDataSource.setDarkThemeConfig(darkThemeConfig)
}
override suspend fun setDynamicColorPreference(useDynamicColor: Boolean) {
niaPreferencesDataSource.setDynamicColorPreference(useDynamicColor)
}
override suspend fun setShouldHideOnboarding(shouldHideOnboarding: Boolean) {
niaPreferencesDataSource.setShouldHideOnboarding(shouldHideOnboarding)
}

@ -60,6 +60,7 @@ class OfflineFirstUserDataRepositoryTest {
followedTopics = emptySet(),
themeBrand = ThemeBrand.DEFAULT,
darkThemeConfig = DarkThemeConfig.FOLLOW_SYSTEM,
useDynamicColor = false,
shouldHideOnboarding = false
),
subject.userData.first()
@ -170,6 +171,26 @@ class OfflineFirstUserDataRepositoryTest {
)
}
@Test
fun offlineFirstUserDataRepository_set_dynamic_color_delegates_to_nia_preferences() =
runTest {
subject.setDynamicColorPreference(true)
assertEquals(
true,
subject.userData
.map { it.useDynamicColor }
.first()
)
assertEquals(
true,
niaPreferencesDataSource
.userData
.map { it.useDynamicColor }
.first()
)
}
@Test
fun offlineFirstUserDataRepository_set_dark_theme_config_delegates_to_nia_preferences() =
runTest {

@ -29,7 +29,6 @@ import kotlinx.coroutines.flow.map
class NiaPreferencesDataSource @Inject constructor(
private val userPreferences: DataStore<UserPreferences>
) {
val userData = userPreferences.data
.map {
UserData(
@ -52,6 +51,7 @@ class NiaPreferencesDataSource @Inject constructor(
DarkThemeConfig.LIGHT
DarkThemeConfigProto.DARK_THEME_CONFIG_DARK -> DarkThemeConfig.DARK
},
useDynamicColor = it.useDynamicColor,
shouldHideOnboarding = it.shouldHideOnboarding
)
}
@ -98,6 +98,14 @@ class NiaPreferencesDataSource @Inject constructor(
}
}
suspend fun setDynamicColorPreference(useDynamicColor: Boolean) {
userPreferences.updateData {
it.copy {
this.useDynamicColor = useDynamicColor
}
}
}
suspend fun setDarkThemeConfig(darkThemeConfig: DarkThemeConfig) {
userPreferences.updateData {
it.copy {

@ -45,4 +45,6 @@ message UserPreferences {
DarkThemeConfigProto dark_theme_config = 17;
bool should_hide_onboarding = 18;
bool use_dynamic_color = 19;
}

@ -77,4 +77,15 @@ class NiaPreferencesDataSourceTest {
// Then: onboarding should be shown again
assertFalse(subject.userData.first().shouldHideOnboarding)
}
@Test
fun shouldUseDynamicColorFalseByDefault() = runTest {
assertFalse(subject.userData.first().useDynamicColor)
}
@Test
fun userShouldUseDynamicColorIsTrueWhenSet() = runTest {
subject.setDynamicColorPreference(true)
assertTrue(subject.userData.first().useDynamicColor)
}
}

@ -31,6 +31,7 @@ android {
dependencies {
implementation(libs.androidx.core.ktx)
implementation(libs.coil.kt.compose)
api(libs.androidx.compose.foundation)
api(libs.androidx.compose.foundation.layout)
api(libs.androidx.compose.material.iconsExtended)
@ -41,4 +42,4 @@ dependencies {
api(libs.androidx.compose.runtime)
lintPublish(project(":lint"))
androidTestImplementation(project(":core:testing"))
}
}

@ -38,7 +38,9 @@ import com.google.samples.apps.nowinandroid.core.designsystem.theme.LightAndroid
import com.google.samples.apps.nowinandroid.core.designsystem.theme.LightDefaultColorScheme
import com.google.samples.apps.nowinandroid.core.designsystem.theme.LocalBackgroundTheme
import com.google.samples.apps.nowinandroid.core.designsystem.theme.LocalGradientColors
import com.google.samples.apps.nowinandroid.core.designsystem.theme.LocalTintTheme
import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme
import com.google.samples.apps.nowinandroid.core.designsystem.theme.TintTheme
import kotlin.test.assertEquals
import org.junit.Rule
import org.junit.Test
@ -70,6 +72,8 @@ class ThemeTest {
assertEquals(gradientColors, LocalGradientColors.current)
val backgroundTheme = defaultBackgroundTheme(colorScheme)
assertEquals(backgroundTheme, LocalBackgroundTheme.current)
val tintTheme = defaultTintTheme()
assertEquals(tintTheme, LocalTintTheme.current)
}
}
}
@ -88,6 +92,8 @@ class ThemeTest {
assertEquals(gradientColors, LocalGradientColors.current)
val backgroundTheme = defaultBackgroundTheme(colorScheme)
assertEquals(backgroundTheme, LocalBackgroundTheme.current)
val tintTheme = defaultTintTheme()
assertEquals(tintTheme, LocalTintTheme.current)
}
}
}
@ -97,6 +103,7 @@ class ThemeTest {
composeTestRule.setContent {
NiaTheme(
darkTheme = false,
disableDynamicTheming = false,
androidTheme = false
) {
val colorScheme = dynamicLightColorSchemeWithFallback()
@ -105,6 +112,8 @@ class ThemeTest {
assertEquals(gradientColors, LocalGradientColors.current)
val backgroundTheme = defaultBackgroundTheme(colorScheme)
assertEquals(backgroundTheme, LocalBackgroundTheme.current)
val tintTheme = dynamicTintTheme(colorScheme)
assertEquals(tintTheme, LocalTintTheme.current)
}
}
}
@ -114,6 +123,7 @@ class ThemeTest {
composeTestRule.setContent {
NiaTheme(
darkTheme = true,
disableDynamicTheming = false,
androidTheme = false
) {
val colorScheme = dynamicDarkColorSchemeWithFallback()
@ -122,6 +132,8 @@ class ThemeTest {
assertEquals(gradientColors, LocalGradientColors.current)
val backgroundTheme = defaultBackgroundTheme(colorScheme)
assertEquals(backgroundTheme, LocalBackgroundTheme.current)
val tintTheme = dynamicTintTheme(colorScheme)
assertEquals(tintTheme, LocalTintTheme.current)
}
}
}
@ -140,6 +152,8 @@ class ThemeTest {
assertEquals(gradientColors, LocalGradientColors.current)
val backgroundTheme = LightAndroidBackgroundTheme
assertEquals(backgroundTheme, LocalBackgroundTheme.current)
val tintTheme = defaultTintTheme()
assertEquals(tintTheme, LocalTintTheme.current)
}
}
}
@ -158,6 +172,8 @@ class ThemeTest {
assertEquals(gradientColors, LocalGradientColors.current)
val backgroundTheme = DarkAndroidBackgroundTheme
assertEquals(backgroundTheme, LocalBackgroundTheme.current)
val tintTheme = defaultTintTheme()
assertEquals(tintTheme, LocalTintTheme.current)
}
}
}
@ -167,6 +183,7 @@ class ThemeTest {
composeTestRule.setContent {
NiaTheme(
darkTheme = false,
disableDynamicTheming = false,
androidTheme = true
) {
val colorScheme = LightAndroidColorScheme
@ -175,6 +192,8 @@ class ThemeTest {
assertEquals(gradientColors, LocalGradientColors.current)
val backgroundTheme = LightAndroidBackgroundTheme
assertEquals(backgroundTheme, LocalBackgroundTheme.current)
val tintTheme = defaultTintTheme()
assertEquals(tintTheme, LocalTintTheme.current)
}
}
}
@ -184,6 +203,7 @@ class ThemeTest {
composeTestRule.setContent {
NiaTheme(
darkTheme = true,
disableDynamicTheming = false,
androidTheme = true
) {
val colorScheme = DarkAndroidColorScheme
@ -192,6 +212,8 @@ class ThemeTest {
assertEquals(gradientColors, LocalGradientColors.current)
val backgroundTheme = DarkAndroidBackgroundTheme
assertEquals(backgroundTheme, LocalBackgroundTheme.current)
val tintTheme = defaultTintTheme()
assertEquals(tintTheme, LocalTintTheme.current)
}
}
}
@ -241,6 +263,18 @@ class ThemeTest {
)
}
private fun defaultTintTheme(): TintTheme {
return TintTheme()
}
private fun dynamicTintTheme(colorScheme: ColorScheme): TintTheme {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
TintTheme(colorScheme.primary)
} else {
TintTheme()
}
}
/**
* Workaround for the fact that the NiA design system specify all color scheme values.
*/

@ -158,7 +158,7 @@ fun BackgroundDefault() {
@ThemePreviews
@Composable
fun BackgroundDynamic() {
NiaTheme {
NiaTheme(disableDynamicTheming = false) {
NiaBackground(Modifier.size(100.dp), content = {})
}
}
@ -182,7 +182,7 @@ fun GradientBackgroundDefault() {
@ThemePreviews
@Composable
fun GradientBackgroundDynamic() {
NiaTheme {
NiaTheme(disableDynamicTheming = false) {
NiaGradientBackground(Modifier.size(100.dp), content = {})
}
}

@ -0,0 +1,44 @@
/*
* Copyright 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.samples.apps.nowinandroid.core.designsystem.component
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.painter.Painter
import coil.compose.AsyncImage
import com.google.samples.apps.nowinandroid.core.designsystem.theme.LocalTintTheme
/**
* A wrapper around [AsyncImage] which determines the colorFilter based on the theme
*/
@Composable
fun DynamicAsyncImage(
imageUrl: String,
contentDescription: String?,
modifier: Modifier = Modifier,
placeholder: Painter? = null
) {
val iconTint = LocalTintTheme.current.iconTint
AsyncImage(
placeholder = placeholder,
model = imageUrl,
contentDescription = contentDescription,
colorFilter = if(iconTint != null) ColorFilter.tint(iconTint) else null,
modifier = modifier
)
}

@ -185,35 +185,15 @@ val DarkAndroidBackgroundTheme = BackgroundTheme(color = Color.Black)
*
* @param darkTheme Whether the theme should use a dark color scheme (follows system by default).
* @param androidTheme Whether the theme should use the Android theme color scheme instead of the
* default theme. If this is `false`, then dynamic theming will be used when supported.
*/
@Composable
fun NiaTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
androidTheme: Boolean = false,
content: @Composable () -> Unit
) = NiaTheme(
darkTheme = darkTheme,
androidTheme = androidTheme,
disableDynamicTheming = false,
content = content
)
/**
* Now in Android theme. This is an internal only version, to allow disabling dynamic theming
* in tests.
*
* @param darkTheme Whether the theme should use a dark color scheme (follows system by default).
* @param androidTheme Whether the theme should use the Android theme color scheme instead of the
* default theme.
* @param disableDynamicTheming If `true`, disables the use of dynamic theming, even when it is
* supported. This parameter has no effect if [androidTheme] is `true`.
*/
@Composable
internal fun NiaTheme(
fun NiaTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
androidTheme: Boolean = false,
disableDynamicTheming: Boolean,
disableDynamicTheming: Boolean = true,
content: @Composable () -> Unit
) {
// Color scheme
@ -223,6 +203,7 @@ internal fun NiaTheme(
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}
else -> if (darkTheme) DarkDefaultColorScheme else LightDefaultColorScheme
}
// Gradient colors
@ -246,10 +227,16 @@ internal fun NiaTheme(
androidTheme -> if (darkTheme) DarkAndroidBackgroundTheme else LightAndroidBackgroundTheme
else -> defaultBackgroundTheme
}
val tintTheme = when {
androidTheme -> TintTheme()
!disableDynamicTheming && supportsDynamicTheming() -> TintTheme(colorScheme.primary)
else -> TintTheme()
}
// Composition locals
CompositionLocalProvider(
LocalGradientColors provides gradientColors,
LocalBackgroundTheme provides backgroundTheme
LocalBackgroundTheme provides backgroundTheme,
LocalTintTheme provides tintTheme
) {
MaterialTheme(
colorScheme = colorScheme,
@ -260,4 +247,4 @@ internal fun NiaTheme(
}
@ChecksSdkIntAtLeast(api = Build.VERSION_CODES.S)
private fun supportsDynamicTheming() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
fun supportsDynamicTheming() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S

@ -0,0 +1,34 @@
/*
* Copyright 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.samples.apps.nowinandroid.core.designsystem.theme
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.graphics.Color
/**
* A class to model background color and tonal elevation values for Now in Android.
*/
@Immutable
data class TintTheme(
val iconTint: Color? = null,
)
/**
* A composition local for [TintTheme].
*/
val LocalTintTheme = staticCompositionLocalOf { TintTheme() }

@ -72,6 +72,7 @@ class UserNewsResourceTest {
followedTopics = setOf("T1"),
themeBrand = DEFAULT,
darkThemeConfig = FOLLOW_SYSTEM,
useDynamicColor = false,
shouldHideOnboarding = true
)

@ -24,5 +24,6 @@ data class UserData(
val followedTopics: Set<String>,
val themeBrand: ThemeBrand,
val darkThemeConfig: DarkThemeConfig,
val useDynamicColor: Boolean,
val shouldHideOnboarding: Boolean
)

@ -30,6 +30,7 @@ val emptyUserData = UserData(
followedTopics = emptySet(),
themeBrand = ThemeBrand.DEFAULT,
darkThemeConfig = DarkThemeConfig.FOLLOW_SYSTEM,
useDynamicColor = false,
shouldHideOnboarding = false
)
@ -77,6 +78,12 @@ class TestUserDataRepository : UserDataRepository {
}
}
override suspend fun setDynamicColorPreference(useDynamicColor: Boolean) {
currentUserData.let { current ->
_userData.tryEmit(current.copy(useDynamicColor = useDynamicColor))
}
}
override suspend fun setShouldHideOnboarding(shouldHideOnboarding: Boolean) {
currentUserData.let { current ->
_userData.tryEmit(current.copy(shouldHideOnboarding = shouldHideOnboarding))

@ -60,7 +60,6 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.layout.layout
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalView
@ -76,7 +75,7 @@ import androidx.core.view.doOnPreDraw
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.ExperimentalLifecycleComposeApi
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import coil.compose.AsyncImage
import com.google.samples.apps.nowinandroid.core.designsystem.component.DynamicAsyncImage
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaButton
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaIconToggleButton
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaOverlayLoadingWheel
@ -374,12 +373,11 @@ fun TopicIcon(
imageUrl: String,
modifier: Modifier = Modifier
) {
AsyncImage(
DynamicAsyncImage(
// TODO b/228077205, show loading image visual instead of static placeholder
placeholder = painterResource(R.drawable.ic_icon_placeholder),
model = imageUrl,
imageUrl = imageUrl,
contentDescription = null, // decorative
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.primary),
modifier = modifier
.padding(10.dp)
.size(32.dp)

@ -31,12 +31,11 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage
import com.google.samples.apps.nowinandroid.core.designsystem.component.DynamicAsyncImage
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaIconToggleButton
import com.google.samples.apps.nowinandroid.core.designsystem.icon.NiaIcons
import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme
@ -122,10 +121,9 @@ private fun InterestsIcon(topicImageUrl: String, modifier: Modifier = Modifier)
contentDescription = null, // decorative image
)
} else {
AsyncImage(
model = topicImageUrl,
DynamicAsyncImage(
imageUrl = topicImageUrl,
contentDescription = null,
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.primary),
modifier = modifier
)
}

@ -22,6 +22,7 @@ import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onNodeWithText
import com.google.samples.apps.nowinandroid.core.model.data.DarkThemeConfig.DARK
import com.google.samples.apps.nowinandroid.core.model.data.ThemeBrand.ANDROID
import com.google.samples.apps.nowinandroid.core.model.data.ThemeBrand.DEFAULT
import com.google.samples.apps.nowinandroid.feature.settings.SettingsUiState.Loading
import com.google.samples.apps.nowinandroid.feature.settings.SettingsUiState.Success
import org.junit.Rule
@ -40,7 +41,8 @@ class SettingsDialogTest {
composeTestRule.setContent {
SettingsDialog(
settingsUiState = Loading,
onDismiss = { },
onDismiss = {},
onChangeDynamicColorPreference = {},
onChangeThemeBrand = {},
onChangeDarkThemeConfig = {}
)
@ -52,16 +54,18 @@ class SettingsDialogTest {
}
@Test
fun whenStateIsSuccess_allSettingsAreDisplayed() {
fun whenStateIsSuccess_allDefaultSettingsAreDisplayed() {
composeTestRule.setContent {
SettingsDialog(
settingsUiState = Success(
UserEditableSettings(
brand = ANDROID,
useDynamicColor = false,
darkThemeConfig = DARK
)
),
onDismiss = { },
onChangeDynamicColorPreference = {},
onChangeThemeBrand = {},
onChangeDarkThemeConfig = {}
)
@ -81,6 +85,79 @@ class SettingsDialogTest {
composeTestRule.onNodeWithText(getString(R.string.dark_mode_config_dark)).assertIsSelected()
}
@Test
fun whenStateIsSuccess_supportsDynamicColor_usesDefaultBrand_DynamicColorOptionIsDisplayed() {
composeTestRule.setContent {
SettingsDialog(
settingsUiState = Success(
UserEditableSettings(
brand = DEFAULT,
darkThemeConfig = DARK,
useDynamicColor = false,
)
),
supportDynamicColor = true,
onDismiss = {},
onChangeDynamicColorPreference = {},
onChangeThemeBrand = {},
onChangeDarkThemeConfig = {}
)
}
composeTestRule.onNodeWithText(getString(R.string.dynamic_color_preference)).assertExists()
composeTestRule.onNodeWithText(getString(R.string.dynamic_color_yes)).assertExists()
composeTestRule.onNodeWithText(getString(R.string.dynamic_color_no)).assertExists()
// Check that the correct default dynamic color setting is selected.
composeTestRule.onNodeWithText(getString(R.string.dynamic_color_no)).assertIsSelected()
}
@Test
fun whenStateIsSuccess_notSupportDynamicColor_DynamicColorOptionIsNotDisplayed() {
composeTestRule.setContent {
SettingsDialog(
settingsUiState = Success(
UserEditableSettings(
brand = ANDROID,
darkThemeConfig = DARK,
useDynamicColor = false,
)
),
onDismiss = {},
onChangeDynamicColorPreference = {},
onChangeThemeBrand = {},
onChangeDarkThemeConfig = {}
)
}
composeTestRule.onNodeWithText(getString(R.string.dynamic_color_preference)).assertDoesNotExist()
composeTestRule.onNodeWithText(getString(R.string.dynamic_color_yes)).assertDoesNotExist()
composeTestRule.onNodeWithText(getString(R.string.dynamic_color_no)).assertDoesNotExist()
}
@Test
fun whenStateIsSuccess_usesAndroidBrand_DynamicColorOptionIsNotDisplayed() {
composeTestRule.setContent {
SettingsDialog(
settingsUiState = Success(
UserEditableSettings(
brand = ANDROID,
darkThemeConfig = DARK,
useDynamicColor = false,
)
),
onDismiss = {},
onChangeDynamicColorPreference = {},
onChangeThemeBrand = {},
onChangeDarkThemeConfig = {}
)
}
composeTestRule.onNodeWithText(getString(R.string.dynamic_color_preference)).assertDoesNotExist()
composeTestRule.onNodeWithText(getString(R.string.dynamic_color_yes)).assertDoesNotExist()
composeTestRule.onNodeWithText(getString(R.string.dynamic_color_no)).assertDoesNotExist()
}
@Test
fun whenStateIsSuccess_allLinksAreDisplayed() {
composeTestRule.setContent {
@ -88,10 +165,12 @@ class SettingsDialogTest {
settingsUiState = Success(
UserEditableSettings(
brand = ANDROID,
darkThemeConfig = DARK
darkThemeConfig = DARK,
useDynamicColor = false,
)
),
onDismiss = { },
onDismiss = {},
onChangeDynamicColorPreference = {},
onChangeThemeBrand = {},
onChangeDarkThemeConfig = {}
)

@ -26,6 +26,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.selection.selectable
import androidx.compose.foundation.selection.selectableGroup
@ -38,17 +39,21 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.DialogProperties
import androidx.core.content.ContextCompat
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.ExperimentalLifecycleComposeApi
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme
import com.google.samples.apps.nowinandroid.core.designsystem.theme.supportsDynamicTheming
import com.google.samples.apps.nowinandroid.core.model.data.DarkThemeConfig
import com.google.samples.apps.nowinandroid.core.model.data.DarkThemeConfig.DARK
import com.google.samples.apps.nowinandroid.core.model.data.DarkThemeConfig.FOLLOW_SYSTEM
@ -71,19 +76,33 @@ fun SettingsDialog(
onDismiss = onDismiss,
settingsUiState = settingsUiState,
onChangeThemeBrand = viewModel::updateThemeBrand,
onChangeDynamicColorPreference = viewModel::updateDynamicColorPreference,
onChangeDarkThemeConfig = viewModel::updateDarkThemeConfig,
)
}
@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun SettingsDialog(
settingsUiState: SettingsUiState,
supportDynamicColor: Boolean = supportsDynamicTheming(),
onDismiss: () -> Unit,
onChangeThemeBrand: (themeBrand: ThemeBrand) -> Unit,
onChangeDynamicColorPreference: (useDynamicColor: Boolean) -> Unit,
onChangeDarkThemeConfig: (darkThemeConfig: DarkThemeConfig) -> Unit
) {
val configuration = LocalConfiguration.current
/**
* usePlatformDefaultWidth = false is use as a temporary fix to allow
* height recalculation during recomposition. This, however, causes
* Dialog's to occupy full width in Compact mode. Therefore max width
* is configured below. This should be removed when there's fix to
* b/221643630
*/
AlertDialog(
properties = DialogProperties(usePlatformDefaultWidth = false),
modifier = Modifier.widthIn(max = configuration.screenWidthDp.dp - 80.dp),
onDismissRequest = { onDismiss() },
title = {
Text(
@ -101,10 +120,13 @@ fun SettingsDialog(
modifier = Modifier.padding(vertical = 16.dp)
)
}
is Success -> {
SettingsPanel(
settings = settingsUiState.settings,
supportDynamicColor = supportDynamicColor,
onChangeThemeBrand = onChangeThemeBrand,
onChangeDynamicColorPreference = onChangeDynamicColorPreference,
onChangeDarkThemeConfig = onChangeDarkThemeConfig
)
}
@ -129,7 +151,9 @@ fun SettingsDialog(
@Composable
private fun SettingsPanel(
settings: UserEditableSettings,
supportDynamicColor: Boolean,
onChangeThemeBrand: (themeBrand: ThemeBrand) -> Unit,
onChangeDynamicColorPreference: (useDynamicColor: Boolean) -> Unit,
onChangeDarkThemeConfig: (darkThemeConfig: DarkThemeConfig) -> Unit
) {
SettingsDialogSectionTitle(text = stringResource(string.theme))
@ -145,7 +169,22 @@ private fun SettingsPanel(
onClick = { onChangeThemeBrand(ANDROID) }
)
}
SettingsDialogSectionTitle(text = "Dark mode preference")
if (settings.brand == DEFAULT && supportDynamicColor) {
SettingsDialogSectionTitle(text = stringResource(R.string.dynamic_color_preference))
Column(Modifier.selectableGroup()) {
SettingsDialogThemeChooserRow(
text = stringResource(string.dynamic_color_yes),
selected = settings.useDynamicColor == true,
onClick = { onChangeDynamicColorPreference(true) }
)
SettingsDialogThemeChooserRow(
text = stringResource(string.dynamic_color_no),
selected = settings.useDynamicColor == false,
onClick = { onChangeDynamicColorPreference(false) }
)
}
}
SettingsDialogSectionTitle(text = stringResource(R.string.dark_mode_preference))
Column(Modifier.selectableGroup()) {
SettingsDialogThemeChooserRow(
text = stringResource(string.dark_mode_config_system_default),
@ -262,11 +301,13 @@ private fun PreviewSettingsDialog() {
settingsUiState = Success(
UserEditableSettings(
brand = DEFAULT,
darkThemeConfig = FOLLOW_SYSTEM
darkThemeConfig = FOLLOW_SYSTEM,
useDynamicColor = false
)
),
onChangeThemeBrand = { },
onChangeDarkThemeConfig = { }
onChangeThemeBrand = {},
onChangeDynamicColorPreference = {},
onChangeDarkThemeConfig = {}
)
}
}
@ -278,14 +319,17 @@ private fun PreviewSettingsDialogLoading() {
SettingsDialog(
onDismiss = {},
settingsUiState = Loading,
onChangeThemeBrand = { },
onChangeDarkThemeConfig = { }
onChangeThemeBrand = {},
onChangeDynamicColorPreference = {},
onChangeDarkThemeConfig = {}
)
}
}
/* ktlint-disable max-line-length */
private const val PRIVACY_POLICY_URL = "https://policies.google.com/privacy"
private const val LICENSES_URL = "https://github.com/android/nowinandroid/blob/main/app/LICENSES.md#open-source-licenses-and-copyright-notices"
private const val BRAND_GUIDELINES_URL = "https://developer.android.com/distribute/marketing-tools/brand-guidelines"
private const val LICENSES_URL =
"https://github.com/android/nowinandroid/blob/main/app/LICENSES.md#open-source-licenses-and-copyright-notices"
private const val BRAND_GUIDELINES_URL =
"https://developer.android.com/distribute/marketing-tools/brand-guidelines"
private const val FEEDBACK_URL = "https://goo.gle/nia-app-feedback"

@ -41,6 +41,7 @@ class SettingsViewModel @Inject constructor(
Success(
settings = UserEditableSettings(
brand = userData.themeBrand,
useDynamicColor = userData.useDynamicColor,
darkThemeConfig = userData.darkThemeConfig
)
)
@ -68,12 +69,22 @@ class SettingsViewModel @Inject constructor(
userDataRepository.setDarkThemeConfig(darkThemeConfig)
}
}
fun updateDynamicColorPreference(useDynamicColor: Boolean) {
viewModelScope.launch {
userDataRepository.setDynamicColorPreference(useDynamicColor)
}
}
}
/**
* Represents the settings which the user can edit within the app.
*/
data class UserEditableSettings(val brand: ThemeBrand, val darkThemeConfig: DarkThemeConfig)
data class UserEditableSettings(
val brand: ThemeBrand,
val useDynamicColor: Boolean,
val darkThemeConfig: DarkThemeConfig
)
sealed interface SettingsUiState {
object Loading : SettingsUiState

@ -25,8 +25,12 @@
<string name="theme">Theme</string>
<string name="brand_default">Default</string>
<string name="brand_android">Android</string>
<string name="dark_mode_preference">Dark mode preference</string>
<string name="dark_mode_config_system_default">System default</string>
<string name="dark_mode_config_light">Light</string>
<string name="dark_mode_config_dark">Dark</string>
<string name="dynamic_color_preference">Use Dynamic Color</string>
<string name="dynamic_color_yes">Yes</string>
<string name="dynamic_color_no">No</string>
<string name="dismiss_dialog_button_text">OK</string>
</resources>
</resources>

@ -63,7 +63,8 @@ class SettingsViewModelTest {
Success(
UserEditableSettings(
brand = ANDROID,
darkThemeConfig = DARK
darkThemeConfig = DARK,
useDynamicColor = false
)
),
viewModel.settingsUiState.value

@ -39,14 +39,13 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.ExperimentalLifecycleComposeApi
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import coil.compose.AsyncImage
import com.google.samples.apps.nowinandroid.core.designsystem.component.DynamicAsyncImage
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaBackground
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaFilterChip
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaLoadingWheel
@ -153,10 +152,9 @@ private fun TopicHeader(name: String, description: String, imageUrl: String) {
Column(
modifier = Modifier.padding(horizontal = 24.dp)
) {
AsyncImage(
model = imageUrl,
DynamicAsyncImage(
imageUrl = imageUrl,
contentDescription = null,
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.primary),
modifier = Modifier
.align(Alignment.CenterHorizontally)
.size(216.dp)

Loading…
Cancel
Save