1 /* <lambda>null2 * Copyright (C) 2024 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.wallpaper.picker.customization.ui.viewmodel 18 19 import android.annotation.ColorInt 20 import android.content.Context 21 import com.android.customization.picker.mode.data.repository.DarkModeStateRepository 22 import com.android.systemui.monet.ColorScheme 23 import com.android.systemui.monet.Style 24 import com.android.wallpaper.R 25 import com.google.ux.material.libmonet.dynamiccolor.DynamicColor 26 import com.google.ux.material.libmonet.dynamiccolor.DynamicScheme 27 import com.google.ux.material.libmonet.dynamiccolor.MaterialDynamicColors 28 import dagger.hilt.android.ActivityRetainedLifecycle 29 import dagger.hilt.android.lifecycle.RetainedLifecycle 30 import dagger.hilt.android.qualifiers.ApplicationContext 31 import dagger.hilt.android.scopes.ActivityRetainedScoped 32 import javax.inject.Inject 33 import kotlin.coroutines.CoroutineContext 34 import kotlinx.coroutines.CoroutineScope 35 import kotlinx.coroutines.Dispatchers 36 import kotlinx.coroutines.SupervisorJob 37 import kotlinx.coroutines.cancel 38 import kotlinx.coroutines.flow.Flow 39 import kotlinx.coroutines.flow.MutableSharedFlow 40 import kotlinx.coroutines.flow.MutableStateFlow 41 import kotlinx.coroutines.flow.asSharedFlow 42 import kotlinx.coroutines.flow.combine 43 import kotlinx.coroutines.flow.distinctUntilChanged 44 import kotlinx.coroutines.launch 45 46 @ActivityRetainedScoped 47 class ColorUpdateViewModel 48 @Inject 49 constructor( 50 @ApplicationContext private val context: Context, 51 activityRetainedLifecycle: ActivityRetainedLifecycle, 52 private val darkModeStateRepository: DarkModeStateRepository, 53 ) { 54 private val coroutineScope = RetainedLifecycleCoroutineScope(activityRetainedLifecycle) 55 56 private val isPreviewEnabled = MutableStateFlow(false) 57 58 /** 59 * Flow that emits an event whenever the system colors are updated. This flow has a replay of 1, 60 * so it will emit the last event to new subscribers. 61 */ 62 private val _systemColorsUpdated: MutableSharedFlow<Unit> = 63 MutableSharedFlow<Unit>(replay = 1).also { it.tryEmit(Unit) } 64 val systemColorsUpdated = _systemColorsUpdated.asSharedFlow() 65 66 /** 67 * Flow that emits an event whenever the system colors are updated. This flow does not have a 68 * replay, so it will not emit the last event to new subscribers. 69 */ 70 private val _systemColorsUpdatedNoReplay: MutableSharedFlow<Unit> = MutableSharedFlow() 71 val systemColorsUpdatedNoReplay = _systemColorsUpdatedNoReplay.asSharedFlow() 72 73 private val previewingIsDarkMode: MutableStateFlow<Boolean?> = MutableStateFlow(null) 74 private val systemIsDarkMode = darkModeStateRepository.isDarkMode 75 val isDarkMode = 76 combine(previewingIsDarkMode, systemIsDarkMode, isPreviewEnabled) { 77 previewingIsDarkMode, 78 systemIsDarkMode, 79 isPreviewEnabled -> 80 if (previewingIsDarkMode != null && isPreviewEnabled) previewingIsDarkMode 81 else systemIsDarkMode 82 } 83 .distinctUntilChanged() 84 85 private val previewingColorScheme: MutableStateFlow<DynamicScheme?> = MutableStateFlow(null) 86 private val colors: MutableList<Color> = mutableListOf() 87 88 private inner class Color(private val colorResId: Int, dynamicColor: DynamicColor) { 89 private val color = MutableStateFlow(context.getColor(colorResId)) 90 val colorFlow = 91 combine(color, previewingColorScheme, isPreviewEnabled) { 92 systemColor, 93 previewScheme, 94 isEnabled -> 95 if (previewScheme != null && isEnabled) { 96 previewScheme.getArgb(dynamicColor) 97 } else systemColor 98 } 99 100 fun update() { 101 color.value = context.getColor(colorResId) 102 } 103 } 104 105 private fun createColorFlow(colorResId: Int, dynamicColor: DynamicColor): Flow<Int> { 106 val color = Color(colorResId, dynamicColor) 107 colors.add(color) 108 return color.colorFlow 109 } 110 111 val colorPrimary = createColorFlow(R.color.system_primary, MaterialDynamicColors().primary()) 112 val colorPrimaryContainer = 113 createColorFlow( 114 R.color.system_primary_container, 115 MaterialDynamicColors().primaryContainer(), 116 ) 117 val colorPrimaryFixedDim = 118 createColorFlow(R.color.system_primary_fixed_dim, MaterialDynamicColors().primaryFixedDim()) 119 val colorOnPrimary = 120 createColorFlow(R.color.system_on_primary, MaterialDynamicColors().onPrimary()) 121 val colorOnPrimaryContainer = 122 createColorFlow( 123 R.color.system_on_primary_container, 124 MaterialDynamicColors().onPrimaryContainer(), 125 ) 126 val colorOnPrimaryFixed = 127 createColorFlow(R.color.system_on_primary_fixed, MaterialDynamicColors().onPrimaryFixed()) 128 val colorOnPrimaryFixedVariant = 129 createColorFlow( 130 R.color.system_on_primary_fixed_variant, 131 MaterialDynamicColors().onPrimaryFixedVariant(), 132 ) 133 val colorSecondary = 134 createColorFlow(R.color.system_secondary, MaterialDynamicColors().secondary()) 135 val colorSecondaryContainer = 136 createColorFlow( 137 R.color.system_secondary_container, 138 MaterialDynamicColors().secondaryContainer(), 139 ) 140 val colorOnSecondaryContainer = 141 createColorFlow( 142 R.color.system_on_secondary_container, 143 MaterialDynamicColors().onSecondaryContainer(), 144 ) 145 val colorSurfaceContainer = 146 createColorFlow( 147 R.color.system_surface_container, 148 MaterialDynamicColors().surfaceContainer(), 149 ) 150 val colorOnSurface = 151 createColorFlow(R.color.system_on_surface, MaterialDynamicColors().onSurface()) 152 val colorOnSurfaceVariant = 153 createColorFlow( 154 R.color.system_on_surface_variant, 155 MaterialDynamicColors().onSurfaceVariant(), 156 ) 157 val colorSurfaceContainerHigh = 158 createColorFlow( 159 R.color.system_surface_container_high, 160 MaterialDynamicColors().surfaceContainerHigh(), 161 ) 162 val colorSurfaceContainerHighest = 163 createColorFlow( 164 R.color.system_surface_container_highest, 165 MaterialDynamicColors().surfaceContainerHighest(), 166 ) 167 val colorSurfaceBright = 168 createColorFlow(R.color.system_surface_bright, MaterialDynamicColors().surfaceBright()) 169 val colorOutline = createColorFlow(R.color.system_outline, MaterialDynamicColors().outline()) 170 171 // Custom day/night color pairing 172 val floatingToolbarBackground = 173 createColorFlow( 174 R.color.floating_toolbar_background, 175 if (!context.resources.configuration.isNightModeActive) { 176 MaterialDynamicColors().surfaceBright() 177 } else { 178 MaterialDynamicColors().surfaceContainerHigh() 179 }, 180 ) 181 182 fun previewColors(@ColorInt colorSeed: Int, @Style.Type style: Int, isDarkMode: Boolean) { 183 previewingColorScheme.value = ColorScheme(colorSeed, isDarkMode, style).materialScheme 184 previewingIsDarkMode.value = isDarkMode 185 } 186 187 fun resetPreview() { 188 previewingColorScheme.value = null 189 previewingIsDarkMode.value = null 190 } 191 192 fun setPreviewEnabled(isEnabled: Boolean) { 193 isPreviewEnabled.value = isEnabled 194 } 195 196 fun updateColors() { 197 // Launch a coroutine scope and use emit rather than tryEmit to make sure the update always 198 // emits successfully. 199 coroutineScope.launch { 200 _systemColorsUpdated.emit(Unit) 201 _systemColorsUpdatedNoReplay.emit(Unit) 202 } 203 colors.forEach { it.update() } 204 } 205 206 fun updateDarkModeAndColors() { 207 darkModeStateRepository.refreshIsDarkMode() 208 // Colors always need an update when dark mode is updated 209 updateColors() 210 } 211 212 class RetainedLifecycleCoroutineScope(val lifecycle: RetainedLifecycle) : 213 CoroutineScope, RetainedLifecycle.OnClearedListener { 214 215 override val coroutineContext: CoroutineContext = 216 SupervisorJob() + Dispatchers.Main.immediate 217 218 init { 219 lifecycle.addOnClearedListener(this) 220 } 221 222 override fun onCleared() { 223 coroutineContext.cancel() 224 } 225 } 226 } 227