• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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