1 /* <lambda>null2 * Copyright (C) 2022 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file 5 * except in compliance with the License. You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software distributed under the 10 * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 11 * KIND, either express or implied. See the License for the specific language governing 12 * permissions and limitations under the License. 13 */ 14 package com.android.systemui.shared.clocks 15 16 import android.app.ActivityManager 17 import android.app.UserSwitchObserver 18 import android.content.Context 19 import android.database.ContentObserver 20 import android.graphics.drawable.Drawable 21 import android.net.Uri 22 import android.os.UserHandle 23 import android.provider.Settings 24 import android.util.Log 25 import androidx.annotation.OpenForTesting 26 import com.android.systemui.plugins.ClockController 27 import com.android.systemui.plugins.ClockId 28 import com.android.systemui.plugins.ClockMetadata 29 import com.android.systemui.plugins.ClockProvider 30 import com.android.systemui.plugins.ClockProviderPlugin 31 import com.android.systemui.plugins.ClockSettings 32 import com.android.systemui.plugins.PluginListener 33 import com.android.systemui.plugins.PluginManager 34 import com.android.systemui.util.Assert 35 import kotlinx.coroutines.CoroutineDispatcher 36 import kotlinx.coroutines.CoroutineScope 37 import kotlinx.coroutines.launch 38 39 private val TAG = ClockRegistry::class.simpleName!! 40 private const val DEBUG = true 41 private val KEY_TIMESTAMP = "appliedTimestamp" 42 43 /** ClockRegistry aggregates providers and plugins */ 44 open class ClockRegistry( 45 val context: Context, 46 val pluginManager: PluginManager, 47 val scope: CoroutineScope, 48 val mainDispatcher: CoroutineDispatcher, 49 val bgDispatcher: CoroutineDispatcher, 50 val isEnabled: Boolean, 51 val handleAllUsers: Boolean, 52 defaultClockProvider: ClockProvider, 53 val fallbackClockId: ClockId = DEFAULT_CLOCK_ID, 54 ) { 55 interface ClockChangeListener { 56 // Called when the active clock changes 57 fun onCurrentClockChanged() {} 58 59 // Called when the list of available clocks changes 60 fun onAvailableClocksChanged() {} 61 } 62 63 private val availableClocks = mutableMapOf<ClockId, ClockInfo>() 64 private val clockChangeListeners = mutableListOf<ClockChangeListener>() 65 private val settingObserver = 66 object : ContentObserver(null) { 67 override fun onChange( 68 selfChange: Boolean, 69 uris: Collection<Uri>, 70 flags: Int, 71 userId: Int 72 ) { 73 scope.launch(bgDispatcher) { querySettings() } 74 } 75 } 76 77 private val pluginListener = 78 object : PluginListener<ClockProviderPlugin> { 79 override fun onPluginConnected(plugin: ClockProviderPlugin, context: Context) = 80 connectClocks(plugin) 81 82 override fun onPluginDisconnected(plugin: ClockProviderPlugin) = 83 disconnectClocks(plugin) 84 } 85 86 private val userSwitchObserver = 87 object : UserSwitchObserver() { 88 override fun onUserSwitchComplete(newUserId: Int) { 89 scope.launch(bgDispatcher) { querySettings() } 90 } 91 } 92 93 // TODO(b/267372164): Migrate to flows 94 var settings: ClockSettings? = null 95 get() = field 96 protected set(value) { 97 if (field != value) { 98 field = value 99 scope.launch(mainDispatcher) { onClockChanged { it.onCurrentClockChanged() } } 100 } 101 } 102 103 var isRegistered: Boolean = false 104 private set 105 106 @OpenForTesting 107 open fun querySettings() { 108 assertNotMainThread() 109 val result = 110 try { 111 val json = 112 if (handleAllUsers) { 113 Settings.Secure.getStringForUser( 114 context.contentResolver, 115 Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE, 116 ActivityManager.getCurrentUser() 117 ) 118 } else { 119 Settings.Secure.getString( 120 context.contentResolver, 121 Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE 122 ) 123 } 124 125 ClockSettings.deserialize(json) 126 } catch (ex: Exception) { 127 Log.e(TAG, "Failed to parse clock settings", ex) 128 null 129 } 130 settings = result 131 } 132 133 @OpenForTesting 134 open fun applySettings(value: ClockSettings?) { 135 assertNotMainThread() 136 137 try { 138 value?.metadata?.put(KEY_TIMESTAMP, System.currentTimeMillis()) 139 140 val json = ClockSettings.serialize(value) 141 if (handleAllUsers) { 142 Settings.Secure.putStringForUser( 143 context.contentResolver, 144 Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE, 145 json, 146 ActivityManager.getCurrentUser() 147 ) 148 } else { 149 Settings.Secure.putString( 150 context.contentResolver, 151 Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE, 152 json 153 ) 154 } 155 } catch (ex: Exception) { 156 Log.e(TAG, "Failed to set clock settings", ex) 157 } 158 settings = value 159 } 160 161 @OpenForTesting 162 protected open fun assertMainThread() { 163 Assert.isMainThread() 164 } 165 166 @OpenForTesting 167 protected open fun assertNotMainThread() { 168 Assert.isNotMainThread() 169 } 170 171 private fun onClockChanged(func: (ClockChangeListener) -> Unit) { 172 assertMainThread() 173 clockChangeListeners.forEach(func) 174 } 175 176 public fun mutateSetting(mutator: (ClockSettings) -> ClockSettings) { 177 scope.launch(bgDispatcher) { applySettings(mutator(settings ?: ClockSettings())) } 178 } 179 180 var currentClockId: ClockId 181 get() = settings?.clockId ?: fallbackClockId 182 set(value) { 183 mutateSetting { it.copy(clockId = value) } 184 } 185 186 var seedColor: Int? 187 get() = settings?.seedColor 188 set(value) { 189 mutateSetting { it.copy(seedColor = value) } 190 } 191 192 init { 193 connectClocks(defaultClockProvider) 194 if (!availableClocks.containsKey(DEFAULT_CLOCK_ID)) { 195 throw IllegalArgumentException( 196 "$defaultClockProvider did not register clock at $DEFAULT_CLOCK_ID" 197 ) 198 } 199 } 200 201 fun registerListeners() { 202 if (!isEnabled || isRegistered) { 203 return 204 } 205 206 isRegistered = true 207 208 pluginManager.addPluginListener( 209 pluginListener, 210 ClockProviderPlugin::class.java, 211 /*allowMultiple=*/ true 212 ) 213 214 scope.launch(bgDispatcher) { querySettings() } 215 if (handleAllUsers) { 216 context.contentResolver.registerContentObserver( 217 Settings.Secure.getUriFor(Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE), 218 /*notifyForDescendants=*/ false, 219 settingObserver, 220 UserHandle.USER_ALL 221 ) 222 223 ActivityManager.getService().registerUserSwitchObserver(userSwitchObserver, TAG) 224 } else { 225 context.contentResolver.registerContentObserver( 226 Settings.Secure.getUriFor(Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE), 227 /*notifyForDescendants=*/ false, 228 settingObserver 229 ) 230 } 231 } 232 233 fun unregisterListeners() { 234 if (!isRegistered) { 235 return 236 } 237 238 isRegistered = false 239 240 pluginManager.removePluginListener(pluginListener) 241 context.contentResolver.unregisterContentObserver(settingObserver) 242 if (handleAllUsers) { 243 ActivityManager.getService().unregisterUserSwitchObserver(userSwitchObserver) 244 } 245 } 246 247 private fun connectClocks(provider: ClockProvider) { 248 var isAvailableChanged = false 249 val currentId = currentClockId 250 for (clock in provider.getClocks()) { 251 val id = clock.clockId 252 val current = availableClocks[id] 253 if (current != null) { 254 Log.e( 255 TAG, 256 "Clock Id conflict: $id is registered by both " + 257 "${provider::class.simpleName} and ${current.provider::class.simpleName}" 258 ) 259 continue 260 } 261 262 availableClocks[id] = ClockInfo(clock, provider) 263 isAvailableChanged = true 264 if (DEBUG) { 265 Log.i(TAG, "Added ${clock.clockId}") 266 } 267 268 if (currentId == id) { 269 if (DEBUG) { 270 Log.i(TAG, "Current clock ($currentId) was connected") 271 } 272 onClockChanged { it.onCurrentClockChanged() } 273 } 274 } 275 276 if (isAvailableChanged) { 277 onClockChanged { it.onAvailableClocksChanged() } 278 } 279 } 280 281 private fun disconnectClocks(provider: ClockProvider) { 282 var isAvailableChanged = false 283 val currentId = currentClockId 284 for (clock in provider.getClocks()) { 285 availableClocks.remove(clock.clockId) 286 isAvailableChanged = true 287 288 if (DEBUG) { 289 Log.i(TAG, "Removed ${clock.clockId}") 290 } 291 292 if (currentId == clock.clockId) { 293 Log.w(TAG, "Current clock ($currentId) was disconnected") 294 onClockChanged { it.onCurrentClockChanged() } 295 } 296 } 297 298 if (isAvailableChanged) { 299 onClockChanged { it.onAvailableClocksChanged() } 300 } 301 } 302 303 fun getClocks(): List<ClockMetadata> { 304 if (!isEnabled) { 305 return listOf(availableClocks[DEFAULT_CLOCK_ID]!!.metadata) 306 } 307 return availableClocks.map { (_, clock) -> clock.metadata } 308 } 309 310 fun getClockThumbnail(clockId: ClockId): Drawable? = 311 availableClocks[clockId]?.provider?.getClockThumbnail(clockId) 312 313 fun createExampleClock(clockId: ClockId): ClockController? = createClock(clockId) 314 315 fun registerClockChangeListener(listener: ClockChangeListener) = 316 clockChangeListeners.add(listener) 317 318 fun unregisterClockChangeListener(listener: ClockChangeListener) = 319 clockChangeListeners.remove(listener) 320 321 fun createCurrentClock(): ClockController { 322 val clockId = currentClockId 323 if (isEnabled && clockId.isNotEmpty()) { 324 val clock = createClock(clockId) 325 if (clock != null) { 326 if (DEBUG) { 327 Log.i(TAG, "Rendering clock $clockId") 328 } 329 return clock 330 } else { 331 Log.e(TAG, "Clock $clockId not found; using default") 332 } 333 } 334 335 return createClock(DEFAULT_CLOCK_ID)!! 336 } 337 338 private fun createClock(targetClockId: ClockId): ClockController? { 339 var settings = this.settings ?: ClockSettings() 340 if (targetClockId != settings.clockId) { 341 settings = settings.copy(clockId = targetClockId) 342 } 343 return availableClocks[targetClockId]?.provider?.createClock(settings) 344 } 345 346 private data class ClockInfo( 347 val metadata: ClockMetadata, 348 val provider: ClockProvider, 349 ) 350 } 351