diff --git a/app/src/main/java/com/google/samples/apps/nowinandroid/MainActivity.kt b/app/src/main/java/com/google/samples/apps/nowinandroid/MainActivity.kt index 2cfc7142f..06cbf9c84 100644 --- a/app/src/main/java/com/google/samples/apps/nowinandroid/MainActivity.kt +++ b/app/src/main/java/com/google/samples/apps/nowinandroid/MainActivity.kt @@ -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. diff --git a/core/data/src/main/java/com/google/samples/apps/nowinandroid/core/data/repository/OfflineFirstUserDataRepository.kt b/core/data/src/main/java/com/google/samples/apps/nowinandroid/core/data/repository/OfflineFirstUserDataRepository.kt index 14a419b67..954779a3f 100644 --- a/core/data/src/main/java/com/google/samples/apps/nowinandroid/core/data/repository/OfflineFirstUserDataRepository.kt +++ b/core/data/src/main/java/com/google/samples/apps/nowinandroid/core/data/repository/OfflineFirstUserDataRepository.kt @@ -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) } diff --git a/core/data/src/main/java/com/google/samples/apps/nowinandroid/core/data/repository/UserDataRepository.kt b/core/data/src/main/java/com/google/samples/apps/nowinandroid/core/data/repository/UserDataRepository.kt index d645e0194..ea093852f 100644 --- a/core/data/src/main/java/com/google/samples/apps/nowinandroid/core/data/repository/UserDataRepository.kt +++ b/core/data/src/main/java/com/google/samples/apps/nowinandroid/core/data/repository/UserDataRepository.kt @@ -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. */ diff --git a/core/data/src/main/java/com/google/samples/apps/nowinandroid/core/data/repository/fake/FakeUserDataRepository.kt b/core/data/src/main/java/com/google/samples/apps/nowinandroid/core/data/repository/fake/FakeUserDataRepository.kt index 5d4d17d0b..d7920cabc 100644 --- a/core/data/src/main/java/com/google/samples/apps/nowinandroid/core/data/repository/fake/FakeUserDataRepository.kt +++ b/core/data/src/main/java/com/google/samples/apps/nowinandroid/core/data/repository/fake/FakeUserDataRepository.kt @@ -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) } diff --git a/core/data/src/test/java/com/google/samples/apps/nowinandroid/core/data/repository/OfflineFirstUserDataRepositoryTest.kt b/core/data/src/test/java/com/google/samples/apps/nowinandroid/core/data/repository/OfflineFirstUserDataRepositoryTest.kt index 531f1004a..926052ea8 100644 --- a/core/data/src/test/java/com/google/samples/apps/nowinandroid/core/data/repository/OfflineFirstUserDataRepositoryTest.kt +++ b/core/data/src/test/java/com/google/samples/apps/nowinandroid/core/data/repository/OfflineFirstUserDataRepositoryTest.kt @@ -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 { diff --git a/core/datastore/src/main/java/com/google/samples/apps/nowinandroid/core/datastore/NiaPreferencesDataSource.kt b/core/datastore/src/main/java/com/google/samples/apps/nowinandroid/core/datastore/NiaPreferencesDataSource.kt index 841ad9e2e..e82b13950 100644 --- a/core/datastore/src/main/java/com/google/samples/apps/nowinandroid/core/datastore/NiaPreferencesDataSource.kt +++ b/core/datastore/src/main/java/com/google/samples/apps/nowinandroid/core/datastore/NiaPreferencesDataSource.kt @@ -29,7 +29,6 @@ import kotlinx.coroutines.flow.map class NiaPreferencesDataSource @Inject constructor( private val userPreferences: DataStore ) { - 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 { diff --git a/core/datastore/src/main/proto/com/google/samples/apps/nowinandroid/data/user_preferences.proto b/core/datastore/src/main/proto/com/google/samples/apps/nowinandroid/data/user_preferences.proto index b7d33dcaf..5288c04ea 100644 --- a/core/datastore/src/main/proto/com/google/samples/apps/nowinandroid/data/user_preferences.proto +++ b/core/datastore/src/main/proto/com/google/samples/apps/nowinandroid/data/user_preferences.proto @@ -45,4 +45,6 @@ message UserPreferences { DarkThemeConfigProto dark_theme_config = 17; bool should_hide_onboarding = 18; + + bool use_dynamic_color = 19; } diff --git a/core/datastore/src/test/java/com/google/samples/apps/nowinandroid/core/datastore/NiaPreferencesDataSourceTest.kt b/core/datastore/src/test/java/com/google/samples/apps/nowinandroid/core/datastore/NiaPreferencesDataSourceTest.kt index cfca4b83a..62a70de11 100644 --- a/core/datastore/src/test/java/com/google/samples/apps/nowinandroid/core/datastore/NiaPreferencesDataSourceTest.kt +++ b/core/datastore/src/test/java/com/google/samples/apps/nowinandroid/core/datastore/NiaPreferencesDataSourceTest.kt @@ -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) + } } diff --git a/core/designsystem/build.gradle.kts b/core/designsystem/build.gradle.kts index 503ac2d19..1bcc9d65c 100644 --- a/core/designsystem/build.gradle.kts +++ b/core/designsystem/build.gradle.kts @@ -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")) -} \ No newline at end of file +} diff --git a/core/designsystem/src/androidTest/java/com/google/samples/apps/nowinandroid/core/designsystem/ThemeTest.kt b/core/designsystem/src/androidTest/java/com/google/samples/apps/nowinandroid/core/designsystem/ThemeTest.kt index bd73cd7c2..a047d838b 100644 --- a/core/designsystem/src/androidTest/java/com/google/samples/apps/nowinandroid/core/designsystem/ThemeTest.kt +++ b/core/designsystem/src/androidTest/java/com/google/samples/apps/nowinandroid/core/designsystem/ThemeTest.kt @@ -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 = dynamicTintThemeWithFallback(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 = dynamicTintThemeWithFallback(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 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. */ diff --git a/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/Background.kt b/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/Background.kt index 40eae962f..f81d2e36d 100644 --- a/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/Background.kt +++ b/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/Background.kt @@ -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 = {}) } } diff --git a/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/DynamicAsyncImage.kt b/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/DynamicAsyncImage.kt new file mode 100644 index 000000000..26f80989f --- /dev/null +++ b/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/DynamicAsyncImage.kt @@ -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 + ) +} diff --git a/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/theme/Theme.kt b/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/theme/Theme.kt index cc405421e..e7be17c99 100644 --- a/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/theme/Theme.kt +++ b/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/theme/Theme.kt @@ -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 diff --git a/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/theme/Tint.kt b/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/theme/Tint.kt new file mode 100644 index 000000000..848c8d8f5 --- /dev/null +++ b/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/theme/Tint.kt @@ -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() } diff --git a/core/domain/src/test/java/com/google/samples/apps/nowinandroid/core/domain/UserNewsResourceTest.kt b/core/domain/src/test/java/com/google/samples/apps/nowinandroid/core/domain/UserNewsResourceTest.kt index d1ea7b569..d06a6d5ac 100644 --- a/core/domain/src/test/java/com/google/samples/apps/nowinandroid/core/domain/UserNewsResourceTest.kt +++ b/core/domain/src/test/java/com/google/samples/apps/nowinandroid/core/domain/UserNewsResourceTest.kt @@ -72,6 +72,7 @@ class UserNewsResourceTest { followedTopics = setOf("T1"), themeBrand = DEFAULT, darkThemeConfig = FOLLOW_SYSTEM, + useDynamicColor = false, shouldHideOnboarding = true ) diff --git a/core/model/src/main/java/com/google/samples/apps/nowinandroid/core/model/data/UserData.kt b/core/model/src/main/java/com/google/samples/apps/nowinandroid/core/model/data/UserData.kt index dcea61b5b..56e3fa522 100644 --- a/core/model/src/main/java/com/google/samples/apps/nowinandroid/core/model/data/UserData.kt +++ b/core/model/src/main/java/com/google/samples/apps/nowinandroid/core/model/data/UserData.kt @@ -24,5 +24,6 @@ data class UserData( val followedTopics: Set, val themeBrand: ThemeBrand, val darkThemeConfig: DarkThemeConfig, + val useDynamicColor: Boolean, val shouldHideOnboarding: Boolean ) diff --git a/core/testing/src/main/java/com/google/samples/apps/nowinandroid/core/testing/repository/TestUserDataRepository.kt b/core/testing/src/main/java/com/google/samples/apps/nowinandroid/core/testing/repository/TestUserDataRepository.kt index 8dba1bfc7..c4c6e126f 100644 --- a/core/testing/src/main/java/com/google/samples/apps/nowinandroid/core/testing/repository/TestUserDataRepository.kt +++ b/core/testing/src/main/java/com/google/samples/apps/nowinandroid/core/testing/repository/TestUserDataRepository.kt @@ -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)) diff --git a/feature/bookmarks/src/main/java/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksScreen.kt b/feature/bookmarks/src/main/java/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksScreen.kt index d5d70e9f7..4531942bb 100644 --- a/feature/bookmarks/src/main/java/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksScreen.kt +++ b/feature/bookmarks/src/main/java/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksScreen.kt @@ -40,6 +40,7 @@ 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.platform.testTag import androidx.compose.ui.res.painterResource 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.collectAsStateWithLifecycle 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.domain.model.previewUserNewsResources import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState @@ -142,9 +144,11 @@ private fun EmptyState(modifier: Modifier = Modifier) { verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally ) { + val iconTint = LocalTintTheme.current.iconTint Image( modifier = Modifier.fillMaxWidth(), painter = painterResource(id = R.drawable.img_empty_bookmarks), + colorFilter = if (iconTint != null) ColorFilter.tint(iconTint) else null, contentDescription = null ) diff --git a/feature/bookmarks/src/main/res/drawable/img_empty_bookmarks.xml b/feature/bookmarks/src/main/res/drawable/img_empty_bookmarks.xml index b9e2f2963..64bbfbd23 100644 --- a/feature/bookmarks/src/main/res/drawable/img_empty_bookmarks.xml +++ b/feature/bookmarks/src/main/res/drawable/img_empty_bookmarks.xml @@ -1,6 +1,6 @@ - - + + + + + + + + + + + + + + + + diff --git a/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreen.kt b/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreen.kt index 8c705f070..ef714308b 100644 --- a/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreen.kt +++ b/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreen.kt @@ -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 @@ -376,12 +375,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) diff --git a/feature/interests/src/main/java/com/google/samples/apps/nowinandroid/feature/interests/InterestsItem.kt b/feature/interests/src/main/java/com/google/samples/apps/nowinandroid/feature/interests/InterestsItem.kt index de04f59f1..19ace59f6 100644 --- a/feature/interests/src/main/java/com/google/samples/apps/nowinandroid/feature/interests/InterestsItem.kt +++ b/feature/interests/src/main/java/com/google/samples/apps/nowinandroid/feature/interests/InterestsItem.kt @@ -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 ) } diff --git a/feature/settings/src/androidTest/java/com/google/samples/apps/nowinandroid/feature/settings/SettingsDialogTest.kt b/feature/settings/src/androidTest/java/com/google/samples/apps/nowinandroid/feature/settings/SettingsDialogTest.kt index 07b6b272c..4643362ff 100644 --- a/feature/settings/src/androidTest/java/com/google/samples/apps/nowinandroid/feature/settings/SettingsDialogTest.kt +++ b/feature/settings/src/androidTest/java/com/google/samples/apps/nowinandroid/feature/settings/SettingsDialogTest.kt @@ -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,81 @@ 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 +167,12 @@ class SettingsDialogTest { settingsUiState = Success( UserEditableSettings( brand = ANDROID, - darkThemeConfig = DARK + darkThemeConfig = DARK, + useDynamicColor = false, ) ), - onDismiss = { }, + onDismiss = {}, + onChangeDynamicColorPreference = {}, onChangeThemeBrand = {}, onChangeDarkThemeConfig = {} ) diff --git a/feature/settings/src/main/java/com/google/samples/apps/nowinandroid/feature/settings/SettingsDialog.kt b/feature/settings/src/main/java/com/google/samples/apps/nowinandroid/feature/settings/SettingsDialog.kt index 01c7cb0f5..a824c1db3 100644 --- a/feature/settings/src/main/java/com/google/samples/apps/nowinandroid/feature/settings/SettingsDialog.kt +++ b/feature/settings/src/main/java/com/google/samples/apps/nowinandroid/feature/settings/SettingsDialog.kt @@ -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 + * https://issuetracker.google.com/issues/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, + 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()) { 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" diff --git a/feature/settings/src/main/java/com/google/samples/apps/nowinandroid/feature/settings/SettingsViewModel.kt b/feature/settings/src/main/java/com/google/samples/apps/nowinandroid/feature/settings/SettingsViewModel.kt index 85a7feb91..c1eac1eee 100644 --- a/feature/settings/src/main/java/com/google/samples/apps/nowinandroid/feature/settings/SettingsViewModel.kt +++ b/feature/settings/src/main/java/com/google/samples/apps/nowinandroid/feature/settings/SettingsViewModel.kt @@ -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 diff --git a/feature/settings/src/main/res/values/strings.xml b/feature/settings/src/main/res/values/strings.xml index b5265f650..5efaeb577 100644 --- a/feature/settings/src/main/res/values/strings.xml +++ b/feature/settings/src/main/res/values/strings.xml @@ -25,8 +25,12 @@ Theme Default Android + Dark mode preference System default Light Dark + Use Dynamic Color + Yes + No OK - \ No newline at end of file + diff --git a/feature/settings/src/test/java/com/google/samples/apps/nowinandroid/feature/settings/SettingsViewModelTest.kt b/feature/settings/src/test/java/com/google/samples/apps/nowinandroid/feature/settings/SettingsViewModelTest.kt index 6d8cf8b13..0377490b3 100644 --- a/feature/settings/src/test/java/com/google/samples/apps/nowinandroid/feature/settings/SettingsViewModelTest.kt +++ b/feature/settings/src/test/java/com/google/samples/apps/nowinandroid/feature/settings/SettingsViewModelTest.kt @@ -63,7 +63,8 @@ class SettingsViewModelTest { Success( UserEditableSettings( brand = ANDROID, - darkThemeConfig = DARK + darkThemeConfig = DARK, + useDynamicColor = false ) ), viewModel.settingsUiState.value diff --git a/feature/topic/src/main/java/com/google/samples/apps/nowinandroid/feature/topic/TopicScreen.kt b/feature/topic/src/main/java/com/google/samples/apps/nowinandroid/feature/topic/TopicScreen.kt index b3263839f..2200fc035 100644 --- a/feature/topic/src/main/java/com/google/samples/apps/nowinandroid/feature/topic/TopicScreen.kt +++ b/feature/topic/src/main/java/com/google/samples/apps/nowinandroid/feature/topic/TopicScreen.kt @@ -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)