1 /*
<lambda>null2  * Copyright (C) 2023 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 package com.android.customization.picker.clock.ui.viewmodel
17 
18 import android.content.res.Resources
19 import android.graphics.Color
20 import androidx.lifecycle.ViewModel
21 import androidx.lifecycle.ViewModelProvider
22 import androidx.lifecycle.viewModelScope
23 import com.android.customization.module.logging.ThemesUserEventLogger
24 import com.android.customization.picker.clock.domain.interactor.ClockPickerInteractor
25 import com.android.customization.picker.clock.shared.ClockSize
26 import com.android.customization.picker.clock.ui.view.ClockViewFactory
27 import com.android.themepicker.R
28 import kotlinx.coroutines.CoroutineDispatcher
29 import kotlinx.coroutines.ExperimentalCoroutinesApi
30 import kotlinx.coroutines.Job
31 import kotlinx.coroutines.delay
32 import kotlinx.coroutines.flow.Flow
33 import kotlinx.coroutines.flow.SharingStarted
34 import kotlinx.coroutines.flow.StateFlow
35 import kotlinx.coroutines.flow.flatMapLatest
36 import kotlinx.coroutines.flow.flowOn
37 import kotlinx.coroutines.flow.map
38 import kotlinx.coroutines.flow.mapLatest
39 import kotlinx.coroutines.flow.mapNotNull
40 import kotlinx.coroutines.flow.stateIn
41 import kotlinx.coroutines.launch
42 
43 /**
44  * Clock carousel view model that provides data for the carousel of clock previews. When there is
45  * only one item, we should show a single clock preview instead of a carousel.
46  */
47 class ClockCarouselViewModel(
48     private val interactor: ClockPickerInteractor,
49     private val backgroundDispatcher: CoroutineDispatcher,
50     private val clockViewFactory: ClockViewFactory,
51     private val resources: Resources,
52     private val logger: ThemesUserEventLogger,
53 ) : ViewModel() {
54     @OptIn(ExperimentalCoroutinesApi::class)
55     val allClocks: StateFlow<List<ClockCarouselItemViewModel>> =
56         interactor.allClocks
57             .mapLatest { allClocks ->
58                 // Delay to avoid the case that the full list of clocks is not initiated.
59                 delay(CLOCKS_EVENT_UPDATE_DELAY_MILLIS)
60                 allClocks.map {
61                     val contentDescription =
62                         resources.getString(
63                             R.string.select_clock_action_description,
64                             clockViewFactory.getController(it.clockId).config.description
65                         )
66                     ClockCarouselItemViewModel(it.clockId, it.isSelected, contentDescription)
67                 }
68             }
69             // makes sure that the operations above this statement are executed on I/O dispatcher
70             // while parallelism limits the number of threads this can run on which makes sure that
71             // the flows run sequentially
72             .flowOn(backgroundDispatcher.limitedParallelism(1))
73             .stateIn(viewModelScope, SharingStarted.Eagerly, emptyList())
74 
75     val selectedClockSize: Flow<ClockSize> = interactor.selectedClockSize
76 
77     val seedColor: Flow<Int?> = interactor.seedColor
78 
79     fun getClockCardColorResId(isDarkThemeEnabled: Boolean): Flow<Int> {
80         return interactor.seedColor.map {
81             it.let { seedColor ->
82                 // if seedColor is null, default clock color is selected
83                 if (seedColor == null) {
84                     if (isDarkThemeEnabled) {
85                         // In dark mode, use darkest surface container color
86                         com.android.wallpaper.R.color.system_surface_container_high
87                     } else {
88                         // In light mode, use lightest surface container color
89                         com.android.wallpaper.R.color.system_surface_bright
90                     }
91                 } else {
92                     val luminance = Color.luminance(seedColor)
93                     if (isDarkThemeEnabled) {
94                         if (luminance <= CARD_COLOR_CHANGE_LUMINANCE_THRESHOLD_DARK_THEME) {
95                             com.android.wallpaper.R.color.system_surface_bright
96                         } else {
97                             com.android.wallpaper.R.color.system_surface_container_high
98                         }
99                     } else {
100                         if (luminance <= CARD_COLOR_CHANGE_LUMINANCE_THRESHOLD_LIGHT_THEME) {
101                             com.android.wallpaper.R.color.system_surface_bright
102                         } else {
103                             com.android.wallpaper.R.color.system_surface_container_highest
104                         }
105                     }
106                 }
107             }
108         }
109     }
110 
111     @OptIn(ExperimentalCoroutinesApi::class)
112     val selectedIndex: Flow<Int> =
113         allClocks
114             .flatMapLatest { allClockIds ->
115                 interactor.selectedClockId.map { selectedClockId ->
116                     val index = allClockIds.indexOfFirst { it.clockId == selectedClockId }
117                     /** Making sure there is no active [setSelectedClockJob] */
118                     val isSetClockIdJobActive = setSelectedClockJob?.isActive == true
119                     if (index >= 0 && !isSetClockIdJobActive) {
120                         index
121                     } else {
122                         null
123                     }
124                 }
125             }
126             .mapNotNull { it }
127 
128     private var setSelectedClockJob: Job? = null
129     fun setSelectedClock(clockId: String) {
130         setSelectedClockJob?.cancel()
131         setSelectedClockJob =
132             viewModelScope.launch(backgroundDispatcher) {
133                 interactor.setSelectedClock(clockId)
134                 logger.logClockApplied(clockId)
135             }
136     }
137 
138     class Factory(
139         private val interactor: ClockPickerInteractor,
140         private val backgroundDispatcher: CoroutineDispatcher,
141         private val clockViewFactory: ClockViewFactory,
142         private val resources: Resources,
143         private val logger: ThemesUserEventLogger,
144     ) : ViewModelProvider.Factory {
145         override fun <T : ViewModel> create(modelClass: Class<T>): T {
146             @Suppress("UNCHECKED_CAST")
147             return ClockCarouselViewModel(
148                 interactor = interactor,
149                 backgroundDispatcher = backgroundDispatcher,
150                 clockViewFactory = clockViewFactory,
151                 resources = resources,
152                 logger = logger,
153             )
154                 as T
155         }
156     }
157 
158     companion object {
159         const val CLOCKS_EVENT_UPDATE_DELAY_MILLIS: Long = 100
160         const val CARD_COLOR_CHANGE_LUMINANCE_THRESHOLD_LIGHT_THEME: Float = 0.85f
161         const val CARD_COLOR_CHANGE_LUMINANCE_THRESHOLD_DARK_THEME: Float = 0.03f
162     }
163 }
164