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 */ 17 package com.android.customization.picker.clock.data.repository 18 19 import android.graphics.drawable.Drawable 20 import android.provider.Settings 21 import androidx.annotation.ColorInt 22 import androidx.annotation.IntRange 23 import com.android.customization.picker.clock.shared.ClockSize 24 import com.android.customization.picker.clock.shared.model.ClockMetadataModel 25 import com.android.systemui.plugins.clocks.AxisPresetConfig 26 import com.android.systemui.plugins.clocks.ClockAxisStyle 27 import com.android.systemui.plugins.clocks.ClockId 28 import com.android.systemui.plugins.clocks.ClockMetadata 29 import com.android.systemui.shared.clocks.ClockRegistry 30 import com.android.systemui.shared.settings.data.repository.SecureSettingsRepository 31 import com.android.wallpaper.picker.di.modules.MainDispatcher 32 import javax.inject.Inject 33 import javax.inject.Singleton 34 import kotlinx.coroutines.CoroutineDispatcher 35 import kotlinx.coroutines.CoroutineScope 36 import kotlinx.coroutines.ExperimentalCoroutinesApi 37 import kotlinx.coroutines.channels.awaitClose 38 import kotlinx.coroutines.delay 39 import kotlinx.coroutines.flow.Flow 40 import kotlinx.coroutines.flow.SharedFlow 41 import kotlinx.coroutines.flow.SharingStarted 42 import kotlinx.coroutines.flow.callbackFlow 43 import kotlinx.coroutines.flow.distinctUntilChanged 44 import kotlinx.coroutines.flow.flowOn 45 import kotlinx.coroutines.flow.map 46 import kotlinx.coroutines.flow.mapLatest 47 import kotlinx.coroutines.flow.mapNotNull 48 import kotlinx.coroutines.flow.shareIn 49 import org.json.JSONObject 50 51 /** Implementation of [ClockPickerRepository], using [ClockRegistry]. */ 52 @Singleton 53 class ClockPickerRepositoryImpl 54 @Inject 55 constructor( 56 private val secureSettingsRepository: SecureSettingsRepository, 57 private val registry: ClockRegistry, 58 @MainDispatcher mainScope: CoroutineScope, 59 @MainDispatcher mainDispatcher: CoroutineDispatcher, 60 ) : ClockPickerRepository { 61 62 @OptIn(ExperimentalCoroutinesApi::class) 63 override val allClocks: Flow<List<ClockMetadataModel>> = 64 callbackFlow { 65 fun send() { 66 val activeClockId = registry.activeClockId 67 val allClocks = 68 registry.getClocks().mapNotNull { 69 val clockConfig = registry.getClockPickerConfig(it.clockId) 70 if (clockConfig != null) { 71 it.toModel( 72 isSelected = it.clockId == activeClockId, 73 description = clockConfig.description, 74 thumbnail = clockConfig.thumbnail, 75 isReactiveToTone = clockConfig.isReactiveToTone, 76 axisPresetConfig = clockConfig.presetConfig, 77 ) 78 } else { 79 null 80 } 81 } 82 83 trySend(allClocks) 84 } 85 86 val listener = 87 object : ClockRegistry.ClockChangeListener { 88 override fun onCurrentClockChanged() { 89 send() 90 } 91 92 override fun onAvailableClocksChanged() { 93 send() 94 } 95 } 96 registry.registerClockChangeListener(listener) 97 send() 98 awaitClose { registry.unregisterClockChangeListener(listener) } 99 } 100 .flowOn(mainDispatcher) 101 .mapLatest { allClocks -> 102 // Loading list of clock plugins can cause many consecutive calls of 103 // onAvailableClocksChanged(). We only care about the final fully-initiated clock 104 // list. Delay to avoid unnecessary too many emits. 105 delay(100) 106 allClocks 107 } 108 109 /** The currently-selected clock. This also emits the clock color information. */ 110 override val selectedClock: Flow<ClockMetadataModel> = 111 callbackFlow<ClockMetadataModel?> { 112 fun send() { 113 val activeClockId = registry.activeClockId 114 val metadata = registry.settings?.metadata 115 val clockConfig = registry.getClockPickerConfig(activeClockId) 116 val model = 117 clockConfig?.let { 118 registry 119 .getClocks() 120 .find { clockMetadata -> clockMetadata.clockId == activeClockId } 121 ?.toModel( 122 isSelected = true, 123 description = it.description, 124 thumbnail = it.thumbnail, 125 isReactiveToTone = it.isReactiveToTone, 126 axisPresetConfig = it.presetConfig, 127 selectedColorId = metadata?.getSelectedColorId(), 128 colorTone = 129 metadata?.getColorTone() 130 ?: ClockMetadataModel.DEFAULT_COLOR_TONE_PROGRESS, 131 seedColor = registry.seedColor, 132 ) 133 } 134 trySend(model) 135 } 136 137 val listener = 138 object : ClockRegistry.ClockChangeListener { 139 override fun onCurrentClockChanged() { 140 send() 141 } 142 143 override fun onAvailableClocksChanged() { 144 send() 145 } 146 } 147 registry.registerClockChangeListener(listener) 148 send() 149 awaitClose { registry.unregisterClockChangeListener(listener) } 150 } 151 .flowOn(mainDispatcher) 152 .mapNotNull { it } 153 // Make this a shared flow to prevent ClockRegistry.registerClockChangeListener from 154 // being called every time this flow is collected, since ClockRegistry is a singleton. 155 .shareIn(mainScope, SharingStarted.WhileSubscribed(), 1) 156 157 override suspend fun setSelectedClock(clockId: String) { 158 registry.mutateSetting { oldSettings -> 159 val newSettings = oldSettings.copy(clockId = clockId) 160 newSettings.metadata = oldSettings.metadata 161 newSettings 162 } 163 } 164 165 override suspend fun setClockColor( 166 selectedColorId: String?, 167 @IntRange(from = 0, to = 100) colorToneProgress: Int, 168 @ColorInt seedColor: Int?, 169 ) { 170 registry.mutateSetting { oldSettings -> 171 val newSettings = oldSettings.copy(seedColor = seedColor) 172 newSettings.metadata = 173 oldSettings.metadata 174 .put(KEY_METADATA_SELECTED_COLOR_ID, selectedColorId) 175 .put(KEY_METADATA_COLOR_TONE_PROGRESS, colorToneProgress) 176 newSettings 177 } 178 } 179 180 override val selectedClockSize: SharedFlow<ClockSize> = 181 secureSettingsRepository 182 .intSetting( 183 name = Settings.Secure.LOCKSCREEN_USE_DOUBLE_LINE_CLOCK, 184 defaultValue = DEFAULT_CLOCK_SIZE, 185 ) 186 .map { setting -> setting == 1 } 187 .map { isDynamic -> if (isDynamic) ClockSize.DYNAMIC else ClockSize.SMALL } 188 .distinctUntilChanged() 189 .shareIn(scope = mainScope, started = SharingStarted.Eagerly, replay = 1) 190 191 override suspend fun setClockSize(size: ClockSize) { 192 secureSettingsRepository.setInt( 193 name = Settings.Secure.LOCKSCREEN_USE_DOUBLE_LINE_CLOCK, 194 value = if (size == ClockSize.DYNAMIC) 1 else 0, 195 ) 196 } 197 198 override suspend fun setClockAxisStyle(axisStyle: ClockAxisStyle) { 199 registry.mutateSetting { oldSettings -> 200 val newSettings = oldSettings.copy(axes = axisStyle) 201 newSettings.metadata = oldSettings.metadata 202 newSettings 203 } 204 } 205 206 override fun isReactiveToTone(clockId: ClockId): Boolean? { 207 return registry.getClockPickerConfig(clockId)?.isReactiveToTone 208 } 209 210 private fun JSONObject.getSelectedColorId(): String? { 211 return if (this.isNull(KEY_METADATA_SELECTED_COLOR_ID)) { 212 null 213 } else { 214 this.getString(KEY_METADATA_SELECTED_COLOR_ID) 215 } 216 } 217 218 private fun JSONObject.getColorTone(): Int { 219 return this.optInt( 220 KEY_METADATA_COLOR_TONE_PROGRESS, 221 ClockMetadataModel.DEFAULT_COLOR_TONE_PROGRESS, 222 ) 223 } 224 225 /** By default, [ClockMetadataModel] has no color information unless specified. */ 226 private fun ClockMetadata.toModel( 227 isSelected: Boolean, 228 description: String, 229 thumbnail: Drawable, 230 isReactiveToTone: Boolean, 231 axisPresetConfig: AxisPresetConfig?, 232 selectedColorId: String? = null, 233 @IntRange(from = 0, to = 100) colorTone: Int = 0, 234 @ColorInt seedColor: Int? = null, 235 ): ClockMetadataModel { 236 return ClockMetadataModel( 237 clockId = clockId, 238 isSelected = isSelected, 239 description = description, 240 thumbnail = thumbnail, 241 isReactiveToTone = isReactiveToTone, 242 axisPresetConfig = axisPresetConfig, 243 selectedColorId = selectedColorId, 244 colorToneProgress = colorTone, 245 seedColor = seedColor, 246 ) 247 } 248 249 companion object { 250 // The selected color in the color option list 251 private const val KEY_METADATA_SELECTED_COLOR_ID = "metadataSelectedColorId" 252 253 // The color tone to apply to the selected color 254 private const val KEY_METADATA_COLOR_TONE_PROGRESS = "metadataColorToneProgress" 255 256 // The default clock size is 1, which means dynamic 257 private const val DEFAULT_CLOCK_SIZE = 1 258 } 259 } 260