Merge branch 'github/main'

pull/591/head^2
Automerger 2 years ago
commit 6371cda677

@ -106,7 +106,8 @@ class MainActivity : ComponentActivity() {
NiaTheme( NiaTheme(
darkTheme = darkTheme, darkTheme = darkTheme,
androidTheme = shouldUseAndroidTheme(uiState) androidTheme = shouldUseAndroidTheme(uiState),
disableDynamicTheming = shouldDisableDynamicTheming(uiState)
) { ) {
NiaApp( NiaApp(
networkMonitor = networkMonitor, 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 * Returns `true` if dark theme should be used, as a function of the [uiState] and the
* current system context. * current system context.

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

@ -53,6 +53,11 @@ interface UserDataRepository {
*/ */
suspend fun setDarkThemeConfig(darkThemeConfig: DarkThemeConfig) 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. * Sets whether the user has completed the onboarding process.
*/ */

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

@ -60,6 +60,7 @@ class OfflineFirstUserDataRepositoryTest {
followedTopics = emptySet(), followedTopics = emptySet(),
themeBrand = ThemeBrand.DEFAULT, themeBrand = ThemeBrand.DEFAULT,
darkThemeConfig = DarkThemeConfig.FOLLOW_SYSTEM, darkThemeConfig = DarkThemeConfig.FOLLOW_SYSTEM,
useDynamicColor = false,
shouldHideOnboarding = false shouldHideOnboarding = false
), ),
subject.userData.first() 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 @Test
fun offlineFirstUserDataRepository_set_dark_theme_config_delegates_to_nia_preferences() = fun offlineFirstUserDataRepository_set_dark_theme_config_delegates_to_nia_preferences() =
runTest { runTest {

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

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

@ -77,4 +77,15 @@ class NiaPreferencesDataSourceTest {
// Then: onboarding should be shown again // Then: onboarding should be shown again
assertFalse(subject.userData.first().shouldHideOnboarding) 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 { dependencies {
implementation(libs.androidx.core.ktx) implementation(libs.androidx.core.ktx)
implementation(libs.coil.kt.compose)
api(libs.androidx.compose.foundation) api(libs.androidx.compose.foundation)
api(libs.androidx.compose.foundation.layout) api(libs.androidx.compose.foundation.layout)
api(libs.androidx.compose.material.iconsExtended) api(libs.androidx.compose.material.iconsExtended)
@ -41,4 +42,4 @@ dependencies {
api(libs.androidx.compose.runtime) api(libs.androidx.compose.runtime)
lintPublish(project(":lint")) lintPublish(project(":lint"))
androidTestImplementation(project(":core:testing")) 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.LightDefaultColorScheme
import com.google.samples.apps.nowinandroid.core.designsystem.theme.LocalBackgroundTheme 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.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.NiaTheme
import com.google.samples.apps.nowinandroid.core.designsystem.theme.TintTheme
import kotlin.test.assertEquals import kotlin.test.assertEquals
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
@ -70,6 +72,8 @@ class ThemeTest {
assertEquals(gradientColors, LocalGradientColors.current) assertEquals(gradientColors, LocalGradientColors.current)
val backgroundTheme = defaultBackgroundTheme(colorScheme) val backgroundTheme = defaultBackgroundTheme(colorScheme)
assertEquals(backgroundTheme, LocalBackgroundTheme.current) assertEquals(backgroundTheme, LocalBackgroundTheme.current)
val tintTheme = defaultTintTheme()
assertEquals(tintTheme, LocalTintTheme.current)
} }
} }
} }
@ -88,6 +92,8 @@ class ThemeTest {
assertEquals(gradientColors, LocalGradientColors.current) assertEquals(gradientColors, LocalGradientColors.current)
val backgroundTheme = defaultBackgroundTheme(colorScheme) val backgroundTheme = defaultBackgroundTheme(colorScheme)
assertEquals(backgroundTheme, LocalBackgroundTheme.current) assertEquals(backgroundTheme, LocalBackgroundTheme.current)
val tintTheme = defaultTintTheme()
assertEquals(tintTheme, LocalTintTheme.current)
} }
} }
} }
@ -97,6 +103,7 @@ class ThemeTest {
composeTestRule.setContent { composeTestRule.setContent {
NiaTheme( NiaTheme(
darkTheme = false, darkTheme = false,
disableDynamicTheming = false,
androidTheme = false androidTheme = false
) { ) {
val colorScheme = dynamicLightColorSchemeWithFallback() val colorScheme = dynamicLightColorSchemeWithFallback()
@ -105,6 +112,8 @@ class ThemeTest {
assertEquals(gradientColors, LocalGradientColors.current) assertEquals(gradientColors, LocalGradientColors.current)
val backgroundTheme = defaultBackgroundTheme(colorScheme) val backgroundTheme = defaultBackgroundTheme(colorScheme)
assertEquals(backgroundTheme, LocalBackgroundTheme.current) assertEquals(backgroundTheme, LocalBackgroundTheme.current)
val tintTheme = dynamicTintThemeWithFallback(colorScheme)
assertEquals(tintTheme, LocalTintTheme.current)
} }
} }
} }
@ -114,6 +123,7 @@ class ThemeTest {
composeTestRule.setContent { composeTestRule.setContent {
NiaTheme( NiaTheme(
darkTheme = true, darkTheme = true,
disableDynamicTheming = false,
androidTheme = false androidTheme = false
) { ) {
val colorScheme = dynamicDarkColorSchemeWithFallback() val colorScheme = dynamicDarkColorSchemeWithFallback()
@ -122,6 +132,8 @@ class ThemeTest {
assertEquals(gradientColors, LocalGradientColors.current) assertEquals(gradientColors, LocalGradientColors.current)
val backgroundTheme = defaultBackgroundTheme(colorScheme) val backgroundTheme = defaultBackgroundTheme(colorScheme)
assertEquals(backgroundTheme, LocalBackgroundTheme.current) assertEquals(backgroundTheme, LocalBackgroundTheme.current)
val tintTheme = dynamicTintThemeWithFallback(colorScheme)
assertEquals(tintTheme, LocalTintTheme.current)
} }
} }
} }
@ -140,6 +152,8 @@ class ThemeTest {
assertEquals(gradientColors, LocalGradientColors.current) assertEquals(gradientColors, LocalGradientColors.current)
val backgroundTheme = LightAndroidBackgroundTheme val backgroundTheme = LightAndroidBackgroundTheme
assertEquals(backgroundTheme, LocalBackgroundTheme.current) assertEquals(backgroundTheme, LocalBackgroundTheme.current)
val tintTheme = defaultTintTheme()
assertEquals(tintTheme, LocalTintTheme.current)
} }
} }
} }
@ -158,6 +172,8 @@ class ThemeTest {
assertEquals(gradientColors, LocalGradientColors.current) assertEquals(gradientColors, LocalGradientColors.current)
val backgroundTheme = DarkAndroidBackgroundTheme val backgroundTheme = DarkAndroidBackgroundTheme
assertEquals(backgroundTheme, LocalBackgroundTheme.current) assertEquals(backgroundTheme, LocalBackgroundTheme.current)
val tintTheme = defaultTintTheme()
assertEquals(tintTheme, LocalTintTheme.current)
} }
} }
} }
@ -167,6 +183,7 @@ class ThemeTest {
composeTestRule.setContent { composeTestRule.setContent {
NiaTheme( NiaTheme(
darkTheme = false, darkTheme = false,
disableDynamicTheming = false,
androidTheme = true androidTheme = true
) { ) {
val colorScheme = LightAndroidColorScheme val colorScheme = LightAndroidColorScheme
@ -175,6 +192,8 @@ class ThemeTest {
assertEquals(gradientColors, LocalGradientColors.current) assertEquals(gradientColors, LocalGradientColors.current)
val backgroundTheme = LightAndroidBackgroundTheme val backgroundTheme = LightAndroidBackgroundTheme
assertEquals(backgroundTheme, LocalBackgroundTheme.current) assertEquals(backgroundTheme, LocalBackgroundTheme.current)
val tintTheme = defaultTintTheme()
assertEquals(tintTheme, LocalTintTheme.current)
} }
} }
} }
@ -184,6 +203,7 @@ class ThemeTest {
composeTestRule.setContent { composeTestRule.setContent {
NiaTheme( NiaTheme(
darkTheme = true, darkTheme = true,
disableDynamicTheming = false,
androidTheme = true androidTheme = true
) { ) {
val colorScheme = DarkAndroidColorScheme val colorScheme = DarkAndroidColorScheme
@ -192,6 +212,8 @@ class ThemeTest {
assertEquals(gradientColors, LocalGradientColors.current) assertEquals(gradientColors, LocalGradientColors.current)
val backgroundTheme = DarkAndroidBackgroundTheme val backgroundTheme = DarkAndroidBackgroundTheme
assertEquals(backgroundTheme, LocalBackgroundTheme.current) 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 dynamicTintThemeWithFallback(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. * Workaround for the fact that the NiA design system specify all color scheme values.
*/ */

@ -158,7 +158,7 @@ fun BackgroundDefault() {
@ThemePreviews @ThemePreviews
@Composable @Composable
fun BackgroundDynamic() { fun BackgroundDynamic() {
NiaTheme { NiaTheme(disableDynamicTheming = false) {
NiaBackground(Modifier.size(100.dp), content = {}) NiaBackground(Modifier.size(100.dp), content = {})
} }
} }
@ -182,7 +182,7 @@ fun GradientBackgroundDefault() {
@ThemePreviews @ThemePreviews
@Composable @Composable
fun GradientBackgroundDynamic() { fun GradientBackgroundDynamic() {
NiaTheme { NiaTheme(disableDynamicTheming = false) {
NiaGradientBackground(Modifier.size(100.dp), content = {}) 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 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 * @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. * default theme.
* @param disableDynamicTheming If `true`, disables the use of dynamic theming, even when it is * @param disableDynamicTheming If `true`, disables the use of dynamic theming, even when it is
* supported. This parameter has no effect if [androidTheme] is `true`. * supported. This parameter has no effect if [androidTheme] is `true`.
*/ */
@Composable @Composable
internal fun NiaTheme( fun NiaTheme(
darkTheme: Boolean = isSystemInDarkTheme(), darkTheme: Boolean = isSystemInDarkTheme(),
androidTheme: Boolean = false, androidTheme: Boolean = false,
disableDynamicTheming: Boolean, disableDynamicTheming: Boolean = true,
content: @Composable () -> Unit content: @Composable () -> Unit
) { ) {
// Color scheme // Color scheme
@ -223,6 +203,7 @@ internal fun NiaTheme(
val context = LocalContext.current val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
} }
else -> if (darkTheme) DarkDefaultColorScheme else LightDefaultColorScheme else -> if (darkTheme) DarkDefaultColorScheme else LightDefaultColorScheme
} }
// Gradient colors // Gradient colors
@ -246,10 +227,16 @@ internal fun NiaTheme(
androidTheme -> if (darkTheme) DarkAndroidBackgroundTheme else LightAndroidBackgroundTheme androidTheme -> if (darkTheme) DarkAndroidBackgroundTheme else LightAndroidBackgroundTheme
else -> defaultBackgroundTheme else -> defaultBackgroundTheme
} }
val tintTheme = when {
androidTheme -> TintTheme()
!disableDynamicTheming && supportsDynamicTheming() -> TintTheme(colorScheme.primary)
else -> TintTheme()
}
// Composition locals // Composition locals
CompositionLocalProvider( CompositionLocalProvider(
LocalGradientColors provides gradientColors, LocalGradientColors provides gradientColors,
LocalBackgroundTheme provides backgroundTheme LocalBackgroundTheme provides backgroundTheme,
LocalTintTheme provides tintTheme
) { ) {
MaterialTheme( MaterialTheme(
colorScheme = colorScheme, colorScheme = colorScheme,
@ -260,4 +247,4 @@ internal fun NiaTheme(
} }
@ChecksSdkIntAtLeast(api = Build.VERSION_CODES.S) @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"), followedTopics = setOf("T1"),
themeBrand = DEFAULT, themeBrand = DEFAULT,
darkThemeConfig = FOLLOW_SYSTEM, darkThemeConfig = FOLLOW_SYSTEM,
useDynamicColor = false,
shouldHideOnboarding = true shouldHideOnboarding = true
) )

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

@ -30,6 +30,7 @@ val emptyUserData = UserData(
followedTopics = emptySet(), followedTopics = emptySet(),
themeBrand = ThemeBrand.DEFAULT, themeBrand = ThemeBrand.DEFAULT,
darkThemeConfig = DarkThemeConfig.FOLLOW_SYSTEM, darkThemeConfig = DarkThemeConfig.FOLLOW_SYSTEM,
useDynamicColor = false,
shouldHideOnboarding = 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) { override suspend fun setShouldHideOnboarding(shouldHideOnboarding: Boolean) {
currentUserData.let { current -> currentUserData.let { current ->
_userData.tryEmit(current.copy(shouldHideOnboarding = shouldHideOnboarding)) _userData.tryEmit(current.copy(shouldHideOnboarding = shouldHideOnboarding))

@ -40,6 +40,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.platform.testTag import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
@ -51,6 +52,7 @@ import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.ExperimentalLifecycleComposeApi import androidx.lifecycle.compose.ExperimentalLifecycleComposeApi
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaLoadingWheel import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaLoadingWheel
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.NiaTheme
import com.google.samples.apps.nowinandroid.core.domain.model.previewUserNewsResources import com.google.samples.apps.nowinandroid.core.domain.model.previewUserNewsResources
import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState
@ -142,9 +144,11 @@ private fun EmptyState(modifier: Modifier = Modifier) {
verticalArrangement = Arrangement.Center, verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
val iconTint = LocalTintTheme.current.iconTint
Image( Image(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
painter = painterResource(id = R.drawable.img_empty_bookmarks), painter = painterResource(id = R.drawable.img_empty_bookmarks),
colorFilter = if (iconTint != null) ColorFilter.tint(iconTint) else null,
contentDescription = null contentDescription = null
) )

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!-- <!--
Copyright 2022 The Android Open Source Project Copyright 2023 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -15,17 +15,43 @@
limitations under the License. limitations under the License.
--> -->
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="192dp" xmlns:aapt="http://schemas.android.com/aapt"
android:height="192dp" android:width="64dp"
android:height="64dp"
android:viewportWidth="64" android:viewportWidth="64"
android:viewportHeight="64"> android:viewportHeight="64">
<path <path
android:fillColor="#8B418F" android:pathData="M16,2H55C57.209,2 59,3.791 59,6V52"
android:pathData="M16,1C15.448,1 15,1.448 15,2C15,2.552 15.448,3 16,3V1ZM58,52C58,52.552 58.448,53 59,53C59.552,53 60,52.552 60,52H58ZM16,3H55V1H16V3ZM58,6V52H60V6H58ZM55,3C56.657,3 58,4.343 58,6H60C60,3.239 57.761,1 55,1V3Z" /> android:strokeWidth="2"
<path android:fillColor="#00000000"
android:fillColor="#00000000" android:strokeLineCap="round">
android:pathData="M45,10H9C6.791,10 5,11.791 5,14V55.854C5,59.177 8.817,61.051 11.446,59.019L24.554,48.89C25.995,47.777 28.005,47.777 29.446,48.89L42.554,59.019C45.183,61.051 49,59.177 49,55.854V14C49,11.791 47.209,10 45,10Z" <aapt:attr name="android:strokeColor">
android:strokeColor="#8B418F" <gradient
android:strokeLineCap="round" android:startX="8"
android:strokeWidth="2" /> android:startY="8"
android:endX="56"
android:endY="56"
android:type="linear">
<item android:offset="0" android:color="#FFFFA8FF"/>
<item android:offset="1" android:color="#FFFF8B5E"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M45,10H9C6.791,10 5,11.791 5,14V55.854C5,59.177 8.817,61.051 11.446,59.019L24.554,48.89C25.995,47.777 28.005,47.777 29.446,48.89L42.554,59.019C45.183,61.051 49,59.177 49,55.854V14C49,11.791 47.209,10 45,10Z"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeLineCap="round">
<aapt:attr name="android:strokeColor">
<gradient
android:startX="8"
android:startY="8"
android:endX="56"
android:endY="56"
android:type="linear">
<item android:offset="0" android:color="#FFFFA8FF"/>
<item android:offset="1" android:color="#FFFF8B5E"/>
</gradient>
</aapt:attr>
</path>
</vector> </vector>

@ -60,7 +60,6 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.layout.layout import androidx.compose.ui.layout.layout
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalView import androidx.compose.ui.platform.LocalView
@ -76,7 +75,7 @@ import androidx.core.view.doOnPreDraw
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.ExperimentalLifecycleComposeApi import androidx.lifecycle.compose.ExperimentalLifecycleComposeApi
import androidx.lifecycle.compose.collectAsStateWithLifecycle 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.NiaButton
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaIconToggleButton import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaIconToggleButton
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaOverlayLoadingWheel import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaOverlayLoadingWheel
@ -304,7 +303,10 @@ private fun TopicSelection(
.fillMaxWidth() .fillMaxWidth()
.testTag(topicSelectionTestTag) .testTag(topicSelectionTestTag)
) { ) {
items(onboardingUiState.topics) { items(
items = onboardingUiState.topics,
key = { it.topic.id }
) {
SingleTopicButton( SingleTopicButton(
name = it.topic.name, name = it.topic.name,
topicId = it.topic.id, topicId = it.topic.id,
@ -376,12 +378,11 @@ fun TopicIcon(
imageUrl: String, imageUrl: String,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
AsyncImage( DynamicAsyncImage(
// TODO b/228077205, show loading image visual instead of static placeholder // TODO b/228077205, show loading image visual instead of static placeholder
placeholder = painterResource(R.drawable.ic_icon_placeholder), placeholder = painterResource(R.drawable.ic_icon_placeholder),
model = imageUrl, imageUrl = imageUrl,
contentDescription = null, // decorative contentDescription = null, // decorative
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.primary),
modifier = modifier modifier = modifier
.padding(10.dp) .padding(10.dp)
.size(32.dp) .size(32.dp)

@ -31,12 +31,11 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
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.component.NiaIconToggleButton
import com.google.samples.apps.nowinandroid.core.designsystem.icon.NiaIcons import com.google.samples.apps.nowinandroid.core.designsystem.icon.NiaIcons
import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme 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 contentDescription = null, // decorative image
) )
} else { } else {
AsyncImage( DynamicAsyncImage(
model = topicImageUrl, imageUrl = topicImageUrl,
contentDescription = null, contentDescription = null,
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.primary),
modifier = modifier modifier = modifier
) )
} }

@ -22,6 +22,7 @@ import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onNodeWithText 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.DarkThemeConfig.DARK
import com.google.samples.apps.nowinandroid.core.model.data.ThemeBrand.ANDROID 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.Loading
import com.google.samples.apps.nowinandroid.feature.settings.SettingsUiState.Success import com.google.samples.apps.nowinandroid.feature.settings.SettingsUiState.Success
import org.junit.Rule import org.junit.Rule
@ -40,7 +41,8 @@ class SettingsDialogTest {
composeTestRule.setContent { composeTestRule.setContent {
SettingsDialog( SettingsDialog(
settingsUiState = Loading, settingsUiState = Loading,
onDismiss = { }, onDismiss = {},
onChangeDynamicColorPreference = {},
onChangeThemeBrand = {}, onChangeThemeBrand = {},
onChangeDarkThemeConfig = {} onChangeDarkThemeConfig = {}
) )
@ -52,16 +54,18 @@ class SettingsDialogTest {
} }
@Test @Test
fun whenStateIsSuccess_allSettingsAreDisplayed() { fun whenStateIsSuccess_allDefaultSettingsAreDisplayed() {
composeTestRule.setContent { composeTestRule.setContent {
SettingsDialog( SettingsDialog(
settingsUiState = Success( settingsUiState = Success(
UserEditableSettings( UserEditableSettings(
brand = ANDROID, brand = ANDROID,
useDynamicColor = false,
darkThemeConfig = DARK darkThemeConfig = DARK
) )
), ),
onDismiss = { }, onDismiss = { },
onChangeDynamicColorPreference = {},
onChangeThemeBrand = {}, onChangeThemeBrand = {},
onChangeDarkThemeConfig = {} onChangeDarkThemeConfig = {}
) )
@ -81,6 +85,81 @@ class SettingsDialogTest {
composeTestRule.onNodeWithText(getString(R.string.dark_mode_config_dark)).assertIsSelected() 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 @Test
fun whenStateIsSuccess_allLinksAreDisplayed() { fun whenStateIsSuccess_allLinksAreDisplayed() {
composeTestRule.setContent { composeTestRule.setContent {
@ -88,10 +167,12 @@ class SettingsDialogTest {
settingsUiState = Success( settingsUiState = Success(
UserEditableSettings( UserEditableSettings(
brand = ANDROID, brand = ANDROID,
darkThemeConfig = DARK darkThemeConfig = DARK,
useDynamicColor = false,
) )
), ),
onDismiss = { }, onDismiss = {},
onChangeDynamicColorPreference = {},
onChangeThemeBrand = {}, onChangeThemeBrand = {},
onChangeDarkThemeConfig = {} onChangeDarkThemeConfig = {}
) )

@ -26,6 +26,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.selection.selectable import androidx.compose.foundation.selection.selectable
import androidx.compose.foundation.selection.selectableGroup import androidx.compose.foundation.selection.selectableGroup
@ -38,17 +39,21 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.Role
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.DialogProperties
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.ExperimentalLifecycleComposeApi import androidx.lifecycle.compose.ExperimentalLifecycleComposeApi
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme 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
import com.google.samples.apps.nowinandroid.core.model.data.DarkThemeConfig.DARK import com.google.samples.apps.nowinandroid.core.model.data.DarkThemeConfig.DARK
import com.google.samples.apps.nowinandroid.core.model.data.DarkThemeConfig.FOLLOW_SYSTEM import com.google.samples.apps.nowinandroid.core.model.data.DarkThemeConfig.FOLLOW_SYSTEM
@ -71,19 +76,33 @@ fun SettingsDialog(
onDismiss = onDismiss, onDismiss = onDismiss,
settingsUiState = settingsUiState, settingsUiState = settingsUiState,
onChangeThemeBrand = viewModel::updateThemeBrand, onChangeThemeBrand = viewModel::updateThemeBrand,
onChangeDynamicColorPreference = viewModel::updateDynamicColorPreference,
onChangeDarkThemeConfig = viewModel::updateDarkThemeConfig, onChangeDarkThemeConfig = viewModel::updateDarkThemeConfig,
) )
} }
@OptIn(ExperimentalComposeUiApi::class)
@Composable @Composable
fun SettingsDialog( fun SettingsDialog(
settingsUiState: SettingsUiState, settingsUiState: SettingsUiState,
supportDynamicColor: Boolean = supportsDynamicTheming(),
onDismiss: () -> Unit, onDismiss: () -> Unit,
onChangeThemeBrand: (themeBrand: ThemeBrand) -> Unit, onChangeThemeBrand: (themeBrand: ThemeBrand) -> Unit,
onChangeDynamicColorPreference: (useDynamicColor: Boolean) -> Unit,
onChangeDarkThemeConfig: (darkThemeConfig: DarkThemeConfig) -> 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
* https://issuetracker.google.com/issues/221643630
*/
AlertDialog( AlertDialog(
properties = DialogProperties(usePlatformDefaultWidth = false),
modifier = Modifier.widthIn(max = configuration.screenWidthDp.dp - 80.dp),
onDismissRequest = { onDismiss() }, onDismissRequest = { onDismiss() },
title = { title = {
Text( Text(
@ -101,10 +120,13 @@ fun SettingsDialog(
modifier = Modifier.padding(vertical = 16.dp) modifier = Modifier.padding(vertical = 16.dp)
) )
} }
is Success -> { is Success -> {
SettingsPanel( SettingsPanel(
settings = settingsUiState.settings, settings = settingsUiState.settings,
supportDynamicColor = supportDynamicColor,
onChangeThemeBrand = onChangeThemeBrand, onChangeThemeBrand = onChangeThemeBrand,
onChangeDynamicColorPreference = onChangeDynamicColorPreference,
onChangeDarkThemeConfig = onChangeDarkThemeConfig onChangeDarkThemeConfig = onChangeDarkThemeConfig
) )
} }
@ -129,7 +151,9 @@ fun SettingsDialog(
@Composable @Composable
private fun SettingsPanel( private fun SettingsPanel(
settings: UserEditableSettings, settings: UserEditableSettings,
supportDynamicColor: Boolean,
onChangeThemeBrand: (themeBrand: ThemeBrand) -> Unit, onChangeThemeBrand: (themeBrand: ThemeBrand) -> Unit,
onChangeDynamicColorPreference: (useDynamicColor: Boolean) -> Unit,
onChangeDarkThemeConfig: (darkThemeConfig: DarkThemeConfig) -> Unit onChangeDarkThemeConfig: (darkThemeConfig: DarkThemeConfig) -> Unit
) { ) {
SettingsDialogSectionTitle(text = stringResource(string.theme)) SettingsDialogSectionTitle(text = stringResource(string.theme))
@ -145,7 +169,22 @@ private fun SettingsPanel(
onClick = { onChangeThemeBrand(ANDROID) } 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,
onClick = { onChangeDynamicColorPreference(true) }
)
SettingsDialogThemeChooserRow(
text = stringResource(string.dynamic_color_no),
selected = !settings.useDynamicColor,
onClick = { onChangeDynamicColorPreference(false) }
)
}
}
SettingsDialogSectionTitle(text = stringResource(R.string.dark_mode_preference))
Column(Modifier.selectableGroup()) { Column(Modifier.selectableGroup()) {
SettingsDialogThemeChooserRow( SettingsDialogThemeChooserRow(
text = stringResource(string.dark_mode_config_system_default), text = stringResource(string.dark_mode_config_system_default),
@ -262,11 +301,13 @@ private fun PreviewSettingsDialog() {
settingsUiState = Success( settingsUiState = Success(
UserEditableSettings( UserEditableSettings(
brand = DEFAULT, brand = DEFAULT,
darkThemeConfig = FOLLOW_SYSTEM darkThemeConfig = FOLLOW_SYSTEM,
useDynamicColor = false
) )
), ),
onChangeThemeBrand = { }, onChangeThemeBrand = {},
onChangeDarkThemeConfig = { } onChangeDynamicColorPreference = {},
onChangeDarkThemeConfig = {}
) )
} }
} }
@ -278,14 +319,17 @@ private fun PreviewSettingsDialogLoading() {
SettingsDialog( SettingsDialog(
onDismiss = {}, onDismiss = {},
settingsUiState = Loading, settingsUiState = Loading,
onChangeThemeBrand = { }, onChangeThemeBrand = {},
onChangeDarkThemeConfig = { } onChangeDynamicColorPreference = {},
onChangeDarkThemeConfig = {}
) )
} }
} }
/* ktlint-disable max-line-length */ /* ktlint-disable max-line-length */
private const val PRIVACY_POLICY_URL = "https://policies.google.com/privacy" 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 LICENSES_URL =
private const val BRAND_GUIDELINES_URL = "https://developer.android.com/distribute/marketing-tools/brand-guidelines" "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" private const val FEEDBACK_URL = "https://goo.gle/nia-app-feedback"

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

@ -25,8 +25,12 @@
<string name="theme">Theme</string> <string name="theme">Theme</string>
<string name="brand_default">Default</string> <string name="brand_default">Default</string>
<string name="brand_android">Android</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_system_default">System default</string>
<string name="dark_mode_config_light">Light</string> <string name="dark_mode_config_light">Light</string>
<string name="dark_mode_config_dark">Dark</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> <string name="dismiss_dialog_button_text">OK</string>
</resources> </resources>

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

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

Loading…
Cancel
Save