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.net.Uri 21 import android.os.UserHandle 22 import android.provider.Settings 23 import androidx.annotation.OpenForTesting 24 import com.android.systemui.log.LogBuffer 25 import com.android.systemui.log.core.LogLevel 26 import com.android.systemui.log.core.LogcatOnlyMessageBuffer 27 import com.android.systemui.log.core.Logger 28 import com.android.systemui.plugins.PluginLifecycleManager 29 import com.android.systemui.plugins.PluginListener 30 import com.android.systemui.plugins.PluginManager 31 import com.android.systemui.plugins.clocks.ClockController 32 import com.android.systemui.plugins.clocks.ClockId 33 import com.android.systemui.plugins.clocks.ClockMessageBuffers 34 import com.android.systemui.plugins.clocks.ClockMetadata 35 import com.android.systemui.plugins.clocks.ClockPickerConfig 36 import com.android.systemui.plugins.clocks.ClockProvider 37 import com.android.systemui.plugins.clocks.ClockProviderPlugin 38 import com.android.systemui.plugins.clocks.ClockSettings 39 import com.android.systemui.util.ThreadAssert 40 import java.io.PrintWriter 41 import java.util.concurrent.ConcurrentHashMap 42 import java.util.concurrent.atomic.AtomicBoolean 43 import kotlinx.coroutines.CoroutineDispatcher 44 import kotlinx.coroutines.CoroutineScope 45 import kotlinx.coroutines.launch 46 import kotlinx.coroutines.withContext 47 import org.json.JSONObject 48 49 private val KEY_TIMESTAMP = "appliedTimestamp" 50 private val KNOWN_PLUGINS = 51 mapOf<String, List<ClockMetadata>>( 52 "com.android.systemui.clocks.bignum" to listOf(ClockMetadata("ANALOG_CLOCK_BIGNUM")), 53 "com.android.systemui.clocks.calligraphy" to 54 listOf(ClockMetadata("DIGITAL_CLOCK_CALLIGRAPHY")), 55 "com.android.systemui.clocks.flex" to listOf(ClockMetadata("DIGITAL_CLOCK_FLEX")), 56 "com.android.systemui.clocks.growth" to listOf(ClockMetadata("DIGITAL_CLOCK_GROWTH")), 57 "com.android.systemui.clocks.handwritten" to 58 listOf(ClockMetadata("DIGITAL_CLOCK_HANDWRITTEN")), 59 "com.android.systemui.clocks.inflate" to listOf(ClockMetadata("DIGITAL_CLOCK_INFLATE")), 60 "com.android.systemui.clocks.metro" to listOf(ClockMetadata("DIGITAL_CLOCK_METRO")), 61 "com.android.systemui.clocks.numoverlap" to 62 listOf(ClockMetadata("DIGITAL_CLOCK_NUMBEROVERLAP")), 63 "com.android.systemui.clocks.weather" to listOf(ClockMetadata("DIGITAL_CLOCK_WEATHER")), 64 ) 65 66 private fun <TKey : Any, TVal : Any> ConcurrentHashMap<TKey, TVal>.concurrentGetOrPut( 67 key: TKey, 68 value: TVal, 69 onNew: (TVal) -> Unit, 70 ): TVal { 71 val result = this.putIfAbsent(key, value) 72 if (result == null) { 73 onNew(value) 74 } 75 return result ?: value 76 } 77 78 /** ClockRegistry aggregates providers and plugins */ 79 open class ClockRegistry( 80 val context: Context, 81 val pluginManager: PluginManager, 82 val scope: CoroutineScope, 83 val mainDispatcher: CoroutineDispatcher, 84 val bgDispatcher: CoroutineDispatcher, 85 val isEnabled: Boolean, 86 val handleAllUsers: Boolean, 87 defaultClockProvider: ClockProvider, 88 val fallbackClockId: ClockId = DEFAULT_CLOCK_ID, 89 val clockBuffers: ClockMessageBuffers? = null, 90 val keepAllLoaded: Boolean, 91 subTag: String, 92 val assert: ThreadAssert = ThreadAssert(), 93 ) { 94 private val TAG = "${ClockRegistry::class.simpleName} ($subTag)" 95 private val logger: Logger = 96 Logger(clockBuffers?.infraMessageBuffer ?: LogcatOnlyMessageBuffer(LogLevel.DEBUG), TAG) 97 98 interface ClockChangeListener { 99 // Called when the active clock changes onCurrentClockChangednull100 fun onCurrentClockChanged() {} 101 102 // Called when the list of available clocks changes onAvailableClocksChangednull103 fun onAvailableClocksChanged() {} 104 } 105 106 private val availableClocks = ConcurrentHashMap<ClockId, ClockInfo>() 107 private val clockChangeListeners = mutableListOf<ClockChangeListener>() 108 private val settingObserver = 109 object : ContentObserver(null) { onChangenull110 override fun onChange( 111 selfChange: Boolean, 112 uris: Collection<Uri>, 113 flags: Int, 114 userId: Int, 115 ) { 116 scope.launch(bgDispatcher) { querySettings() } 117 } 118 } 119 120 private val pluginListener = 121 object : PluginListener<ClockProviderPlugin> { onPluginAttachednull122 override fun onPluginAttached( 123 manager: PluginLifecycleManager<ClockProviderPlugin> 124 ): Boolean { 125 manager.setLogFunc({ tag, msg -> 126 (clockBuffers?.infraMessageBuffer as LogBuffer?)?.log(tag, LogLevel.DEBUG, msg) 127 }) 128 if (keepAllLoaded) { 129 // Always load new plugins if requested 130 return true 131 } 132 133 val knownClocks = KNOWN_PLUGINS.get(manager.getPackage()) 134 if (knownClocks == null) { 135 logger.w({ "Loading unrecognized clock package: $str1" }) { 136 str1 = manager.getPackage() 137 } 138 return true 139 } 140 141 logger.i({ "Skipping initial load of known clock package package: $str1" }) { 142 str1 = manager.getPackage() 143 } 144 145 var isCurrentClock = false 146 var isClockListChanged = false 147 for (metadata in knownClocks) { 148 isCurrentClock = isCurrentClock || currentClockId == metadata.clockId 149 val id = metadata.clockId 150 val info = 151 availableClocks.concurrentGetOrPut(id, ClockInfo(metadata, null, manager)) { 152 isClockListChanged = true 153 onConnected(it) 154 } 155 156 if (manager != info.manager) { 157 logger.e({ 158 "Clock Id conflict on attach: " + 159 "$str1 is double registered by $str2 and $str3" 160 }) { 161 str1 = id 162 str2 = info.manager.toString() 163 str3 = manager.toString() 164 } 165 continue 166 } 167 168 info.provider = null 169 } 170 171 if (isClockListChanged) { 172 triggerOnAvailableClocksChanged() 173 } 174 verifyLoadedProviders() 175 176 // Load immediately if it's the current clock, otherwise let verifyLoadedProviders 177 // load and unload clocks as necessary on the background thread. 178 return isCurrentClock 179 } 180 onPluginLoadednull181 override fun onPluginLoaded( 182 plugin: ClockProviderPlugin, 183 pluginContext: Context, 184 manager: PluginLifecycleManager<ClockProviderPlugin>, 185 ) { 186 plugin.initialize(clockBuffers) 187 188 var isClockListChanged = false 189 for (clock in plugin.getClocks()) { 190 val id = clock.clockId 191 val info = 192 availableClocks.concurrentGetOrPut(id, ClockInfo(clock, plugin, manager)) { 193 isClockListChanged = true 194 onConnected(it) 195 } 196 197 if (manager != info.manager) { 198 logger.e({ 199 "Clock Id conflict on load: " + 200 "$str1 is double registered by $str2 and $str3" 201 }) { 202 str1 = id 203 str2 = info.manager.toString() 204 str3 = manager.toString() 205 } 206 manager.unloadPlugin() 207 continue 208 } 209 210 info.provider = plugin 211 onLoaded(info) 212 } 213 214 if (isClockListChanged) { 215 triggerOnAvailableClocksChanged() 216 } 217 verifyLoadedProviders() 218 } 219 onPluginUnloadednull220 override fun onPluginUnloaded( 221 plugin: ClockProviderPlugin, 222 manager: PluginLifecycleManager<ClockProviderPlugin>, 223 ) { 224 for (clock in plugin.getClocks()) { 225 val id = clock.clockId 226 val info = availableClocks[id] 227 if (info?.manager != manager) { 228 logger.e({ 229 "Clock Id conflict on unload: " + 230 "$str1 is double registered by $str2 and $str3" 231 }) { 232 str1 = id 233 str2 = info?.manager.toString() 234 str3 = manager.toString() 235 } 236 continue 237 } 238 info.provider = null 239 onUnloaded(info) 240 } 241 242 verifyLoadedProviders() 243 } 244 onPluginDetachednull245 override fun onPluginDetached(manager: PluginLifecycleManager<ClockProviderPlugin>) { 246 val removed = mutableListOf<ClockInfo>() 247 availableClocks.entries.removeAll { 248 if (it.value.manager != manager) { 249 return@removeAll false 250 } 251 252 removed.add(it.value) 253 return@removeAll true 254 } 255 256 removed.forEach(::onDisconnected) 257 if (removed.size > 0) { 258 triggerOnAvailableClocksChanged() 259 } 260 } 261 } 262 263 private val userSwitchObserver = 264 object : UserSwitchObserver() { onUserSwitchCompletenull265 override fun onUserSwitchComplete(newUserId: Int) { 266 scope.launch(bgDispatcher) { querySettings() } 267 } 268 } 269 270 // TODO(b/267372164): Migrate to flows 271 var settings: ClockSettings? = null 272 get() = field 273 protected set(value) { 274 if (field != value) { 275 field = value 276 verifyLoadedProviders() 277 triggerOnCurrentClockChanged() 278 } 279 } 280 281 var isRegistered: Boolean = false 282 private set 283 284 @OpenForTesting querySettingsnull285 open fun querySettings() { 286 assert.isNotMainThread() 287 val result = 288 try { 289 val json = 290 if (handleAllUsers) { 291 Settings.Secure.getStringForUser( 292 context.contentResolver, 293 Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE, 294 ActivityManager.getCurrentUser(), 295 ) 296 } else { 297 Settings.Secure.getString( 298 context.contentResolver, 299 Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE, 300 ) 301 } 302 303 ClockSettings.fromJson(JSONObject(json)) 304 } catch (ex: Exception) { 305 logger.e("Failed to parse clock settings", ex) 306 null 307 } 308 settings = result 309 } 310 311 @OpenForTesting applySettingsnull312 open fun applySettings(value: ClockSettings?) { 313 assert.isNotMainThread() 314 315 try { 316 val json = 317 value?.let { 318 it.metadata.put(KEY_TIMESTAMP, System.currentTimeMillis()) 319 ClockSettings.toJson(it) 320 } ?: "" 321 322 if (handleAllUsers) { 323 Settings.Secure.putStringForUser( 324 context.contentResolver, 325 Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE, 326 json.toString(), 327 ActivityManager.getCurrentUser(), 328 ) 329 } else { 330 Settings.Secure.putString( 331 context.contentResolver, 332 Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE, 333 json.toString(), 334 ) 335 } 336 } catch (ex: Exception) { 337 logger.e("Failed to set clock settings", ex) 338 } 339 settings = value 340 } 341 342 private var isClockChanged = AtomicBoolean(false) 343 triggerOnCurrentClockChangednull344 private fun triggerOnCurrentClockChanged() { 345 val shouldSchedule = isClockChanged.compareAndSet(false, true) 346 if (!shouldSchedule) { 347 return 348 } 349 350 scope.launch(mainDispatcher) { 351 assert.isMainThread() 352 isClockChanged.set(false) 353 clockChangeListeners.forEach { it.onCurrentClockChanged() } 354 } 355 } 356 357 private var isClockListChanged = AtomicBoolean(false) 358 triggerOnAvailableClocksChangednull359 private fun triggerOnAvailableClocksChanged() { 360 val shouldSchedule = isClockListChanged.compareAndSet(false, true) 361 if (!shouldSchedule) { 362 return 363 } 364 365 scope.launch(mainDispatcher) { 366 assert.isMainThread() 367 isClockListChanged.set(false) 368 clockChangeListeners.forEach { it.onAvailableClocksChanged() } 369 } 370 } 371 mutateSettingnull372 public suspend fun mutateSetting(mutator: (ClockSettings) -> ClockSettings) { 373 withContext(bgDispatcher) { applySettings(mutator(settings ?: ClockSettings())) } 374 } 375 376 var currentClockId: ClockId 377 get() = settings?.clockId ?: fallbackClockId 378 set(value) { <lambda>null379 scope.launch(bgDispatcher) { mutateSetting { it.copy(clockId = value) } } 380 } 381 382 var seedColor: Int? 383 get() = settings?.seedColor 384 set(value) { <lambda>null385 scope.launch(bgDispatcher) { mutateSetting { it.copy(seedColor = value) } } 386 } 387 388 // Returns currentClockId if clock is connected, otherwise DEFAULT_CLOCK_ID. Since this 389 // is dependent on which clocks are connected, it may change when a clock is installed or 390 // removed from the device (unlike currentClockId). 391 // TODO: Merge w/ CurrentClockId when we convert to a flow. We shouldn't need both behaviors. 392 val activeClockId: String 393 get() { 394 if (!availableClocks.containsKey(currentClockId)) { 395 return DEFAULT_CLOCK_ID 396 } 397 return currentClockId 398 } 399 400 init { 401 // Initialize & register default clock designs 402 defaultClockProvider.initialize(clockBuffers) 403 for (clock in defaultClockProvider.getClocks()) { 404 availableClocks[clock.clockId] = ClockInfo(clock, defaultClockProvider, null) 405 } 406 407 // Something has gone terribly wrong if the default clock isn't present 408 if (!availableClocks.containsKey(DEFAULT_CLOCK_ID)) { 409 throw IllegalArgumentException( 410 "$defaultClockProvider did not register clock at $DEFAULT_CLOCK_ID" 411 ) 412 } 413 } 414 registerListenersnull415 fun registerListeners() { 416 if (!isEnabled || isRegistered) { 417 return 418 } 419 420 isRegistered = true 421 422 pluginManager.addPluginListener( 423 pluginListener, 424 ClockProviderPlugin::class.java, 425 /*allowMultiple=*/ true, 426 ) 427 428 scope.launch(bgDispatcher) { querySettings() } 429 if (handleAllUsers) { 430 context.contentResolver.registerContentObserver( 431 Settings.Secure.getUriFor(Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE), 432 /*notifyForDescendants=*/ false, 433 settingObserver, 434 UserHandle.USER_ALL, 435 ) 436 437 ActivityManager.getService().registerUserSwitchObserver(userSwitchObserver, TAG) 438 } else { 439 context.contentResolver.registerContentObserver( 440 Settings.Secure.getUriFor(Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE), 441 /*notifyForDescendants=*/ false, 442 settingObserver, 443 ) 444 } 445 } 446 unregisterListenersnull447 fun unregisterListeners() { 448 if (!isRegistered) { 449 return 450 } 451 452 isRegistered = false 453 454 pluginManager.removePluginListener(pluginListener) 455 context.contentResolver.unregisterContentObserver(settingObserver) 456 if (handleAllUsers) { 457 ActivityManager.getService().unregisterUserSwitchObserver(userSwitchObserver) 458 } 459 } 460 461 private var isQueued = AtomicBoolean(false) 462 verifyLoadedProvidersnull463 fun verifyLoadedProviders() { 464 val shouldSchedule = isQueued.compareAndSet(false, true) 465 if (!shouldSchedule) { 466 logger.v("verifyLoadedProviders: shouldSchedule=false") 467 return 468 } 469 470 scope.launch(bgDispatcher) { 471 // TODO(b/267372164): Use better threading approach when converting to flows 472 synchronized(availableClocks) { 473 isQueued.set(false) 474 if (keepAllLoaded) { 475 logger.i("verifyLoadedProviders: keepAllLoaded=true") 476 // Enforce that all plugins are loaded if requested 477 for ((_, info) in availableClocks) { 478 info.manager?.loadPlugin() 479 } 480 return@launch 481 } 482 483 val currentClock = availableClocks[currentClockId] 484 if (currentClock == null) { 485 logger.i("verifyLoadedProviders: currentClock=null") 486 // Current Clock missing, load no plugins and use default 487 for ((_, info) in availableClocks) { 488 info.manager?.unloadPlugin() 489 } 490 return@launch 491 } 492 493 logger.i("verifyLoadedProviders: load currentClock") 494 val currentManager = currentClock.manager 495 currentManager?.loadPlugin() 496 497 for ((_, info) in availableClocks) { 498 val manager = info.manager 499 if (manager != null && currentManager != manager) { 500 manager.unloadPlugin() 501 } 502 } 503 } 504 } 505 } 506 onConnectednull507 private fun onConnected(info: ClockInfo) { 508 val isCurrent = currentClockId == info.metadata.clockId 509 logger.log( 510 if (isCurrent) LogLevel.INFO else LogLevel.DEBUG, 511 { "Connected $str1 @$str2" + if (bool1) " (Current Clock)" else "" }, 512 ) { 513 str1 = info.metadata.clockId 514 str2 = info.manager.toString() 515 bool1 = isCurrent 516 } 517 } 518 onLoadednull519 private fun onLoaded(info: ClockInfo) { 520 val isCurrent = currentClockId == info.metadata.clockId 521 logger.log( 522 if (isCurrent) LogLevel.INFO else LogLevel.DEBUG, 523 { "Loaded $str1 @$str2" + if (bool1) " (Current Clock)" else "" }, 524 ) { 525 str1 = info.metadata.clockId 526 str2 = info.manager.toString() 527 bool1 = isCurrent 528 } 529 530 if (isCurrent) { 531 triggerOnCurrentClockChanged() 532 } 533 } 534 onUnloadednull535 private fun onUnloaded(info: ClockInfo) { 536 val isCurrent = currentClockId == info.metadata.clockId 537 logger.log( 538 if (isCurrent) LogLevel.WARNING else LogLevel.DEBUG, 539 { "Unloaded $str1 @$str2" + if (bool1) " (Current Clock)" else "" }, 540 ) { 541 str1 = info.metadata.clockId 542 str2 = info.manager.toString() 543 bool1 = isCurrent 544 } 545 546 if (isCurrent) { 547 triggerOnCurrentClockChanged() 548 } 549 } 550 onDisconnectednull551 private fun onDisconnected(info: ClockInfo) { 552 val isCurrent = currentClockId == info.metadata.clockId 553 logger.log( 554 if (isCurrent) LogLevel.INFO else LogLevel.DEBUG, 555 { "Disconnected $str1 @$str2" + if (bool1) " (Current Clock)" else "" }, 556 ) { 557 str1 = info.metadata.clockId 558 str2 = info.manager.toString() 559 bool1 = isCurrent 560 } 561 } 562 getClocksnull563 fun getClocks(): List<ClockMetadata> { 564 if (!isEnabled) return listOf(availableClocks[DEFAULT_CLOCK_ID]!!.metadata) 565 return availableClocks.map { (_, clock) -> clock.metadata } 566 } 567 getClockPickerConfignull568 fun getClockPickerConfig(clockId: ClockId): ClockPickerConfig? { 569 val clockSettings = 570 settings?.let { if (clockId == it.clockId) it else null } ?: ClockSettings(clockId) 571 return availableClocks[clockId]?.provider?.getClockPickerConfig(clockSettings) 572 } 573 createExampleClocknull574 fun createExampleClock(clockId: ClockId): ClockController? = createClock(clockId) 575 576 /** 577 * Adds [listener] to receive future clock changes. 578 * 579 * Calling from main thread to make sure the access is thread safe. 580 */ 581 fun registerClockChangeListener(listener: ClockChangeListener) { 582 assert.isMainThread() 583 clockChangeListeners.add(listener) 584 } 585 586 /** 587 * Removes [listener] from future clock changes. 588 * 589 * Calling from main thread to make sure the access is thread safe. 590 */ unregisterClockChangeListenernull591 fun unregisterClockChangeListener(listener: ClockChangeListener) { 592 assert.isMainThread() 593 clockChangeListeners.remove(listener) 594 } 595 createCurrentClocknull596 fun createCurrentClock(): ClockController { 597 val clockId = currentClockId 598 if (isEnabled && clockId.isNotEmpty()) { 599 val clock = createClock(clockId) 600 if (clock != null) { 601 logger.i({ "Rendering clock $str1" }) { str1 = clockId } 602 return clock 603 } else if (availableClocks.containsKey(clockId)) { 604 logger.w({ "Clock $str1 not loaded; using default" }) { str1 = clockId } 605 verifyLoadedProviders() 606 } else { 607 logger.e({ "Clock $str1 not found; using default" }) { str1 = clockId } 608 } 609 } 610 611 return createClock(DEFAULT_CLOCK_ID)!! 612 } 613 createClocknull614 private fun createClock(targetClockId: ClockId): ClockController? { 615 var settings = this.settings ?: ClockSettings() 616 if (targetClockId != settings.clockId) { 617 settings = settings.copy(clockId = targetClockId) 618 } 619 return availableClocks[targetClockId]?.provider?.createClock(settings) 620 } 621 dumpnull622 fun dump(pw: PrintWriter, args: Array<out String>) { 623 pw.println("ClockRegistry:") 624 pw.println(" settings = $settings") 625 for ((id, info) in availableClocks) { 626 pw.println(" availableClocks[$id] = $info") 627 } 628 } 629 630 private data class ClockInfo( 631 val metadata: ClockMetadata, 632 var provider: ClockProvider?, 633 val manager: PluginLifecycleManager<ClockProviderPlugin>?, 634 ) 635 } 636