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 androidx.annotation.OpenForTesting 25 import com.android.systemui.log.LogBuffer 26 import com.android.systemui.log.core.LogLevel 27 import com.android.systemui.log.core.LogcatOnlyMessageBuffer 28 import com.android.systemui.log.core.Logger 29 import com.android.systemui.plugins.PluginLifecycleManager 30 import com.android.systemui.plugins.PluginListener 31 import com.android.systemui.plugins.PluginManager 32 import com.android.systemui.plugins.clocks.ClockController 33 import com.android.systemui.plugins.clocks.ClockId 34 import com.android.systemui.plugins.clocks.ClockMessageBuffers 35 import com.android.systemui.plugins.clocks.ClockMetadata 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 48 private val KEY_TIMESTAMP = "appliedTimestamp" 49 private val KNOWN_PLUGINS = 50 mapOf<String, List<ClockMetadata>>( 51 "com.android.systemui.clocks.bignum" to listOf(ClockMetadata("ANALOG_CLOCK_BIGNUM")), 52 "com.android.systemui.clocks.calligraphy" to 53 listOf(ClockMetadata("DIGITAL_CLOCK_CALLIGRAPHY")), 54 "com.android.systemui.clocks.flex" to listOf(ClockMetadata("DIGITAL_CLOCK_FLEX")), 55 "com.android.systemui.clocks.growth" to listOf(ClockMetadata("DIGITAL_CLOCK_GROWTH")), 56 "com.android.systemui.clocks.handwritten" to 57 listOf(ClockMetadata("DIGITAL_CLOCK_HANDWRITTEN")), 58 "com.android.systemui.clocks.inflate" to listOf(ClockMetadata("DIGITAL_CLOCK_INFLATE")), 59 "com.android.systemui.clocks.metro" to listOf(ClockMetadata("DIGITAL_CLOCK_METRO")), 60 "com.android.systemui.clocks.numoverlap" to 61 listOf(ClockMetadata("DIGITAL_CLOCK_NUMBEROVERLAP")), 62 "com.android.systemui.clocks.weather" to listOf(ClockMetadata("DIGITAL_CLOCK_WEATHER")), 63 ) 64 65 private fun <TKey : Any, TVal : Any> ConcurrentHashMap<TKey, TVal>.concurrentGetOrPut( 66 key: TKey, 67 value: TVal, 68 onNew: (TVal) -> Unit 69 ): TVal { 70 val result = this.putIfAbsent(key, value) 71 if (result == null) { 72 onNew(value) 73 } 74 return result ?: value 75 } 76 77 /** ClockRegistry aggregates providers and plugins */ 78 open class ClockRegistry( 79 val context: Context, 80 val pluginManager: PluginManager, 81 val scope: CoroutineScope, 82 val mainDispatcher: CoroutineDispatcher, 83 val bgDispatcher: CoroutineDispatcher, 84 val isEnabled: Boolean, 85 val handleAllUsers: Boolean, 86 defaultClockProvider: ClockProvider, 87 val fallbackClockId: ClockId = DEFAULT_CLOCK_ID, 88 val clockBuffers: ClockMessageBuffers? = null, 89 val keepAllLoaded: Boolean, 90 subTag: String, 91 var isTransitClockEnabled: Boolean = false, 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 if (!isTransitClockEnabled && id == "DIGITAL_CLOCK_METRO") { 192 continue 193 } 194 195 val info = 196 availableClocks.concurrentGetOrPut(id, ClockInfo(clock, plugin, manager)) { 197 isClockListChanged = true 198 onConnected(it) 199 } 200 201 if (manager != info.manager) { 202 logger.e({ 203 "Clock Id conflict on load: " + 204 "$str1 is double registered by $str2 and $str3" 205 }) { 206 str1 = id 207 str2 = info.manager.toString() 208 str3 = manager.toString() 209 } 210 manager.unloadPlugin() 211 continue 212 } 213 214 info.provider = plugin 215 onLoaded(info) 216 } 217 218 if (isClockListChanged) { 219 triggerOnAvailableClocksChanged() 220 } 221 verifyLoadedProviders() 222 } 223 onPluginUnloadednull224 override fun onPluginUnloaded( 225 plugin: ClockProviderPlugin, 226 manager: PluginLifecycleManager<ClockProviderPlugin> 227 ) { 228 for (clock in plugin.getClocks()) { 229 val id = clock.clockId 230 val info = availableClocks[id] 231 if (info?.manager != manager) { 232 logger.e({ 233 "Clock Id conflict on unload: " + 234 "$str1 is double registered by $str2 and $str3" 235 }) { 236 str1 = id 237 str2 = info?.manager.toString() 238 str3 = manager.toString() 239 } 240 continue 241 } 242 info.provider = null 243 onUnloaded(info) 244 } 245 246 verifyLoadedProviders() 247 } 248 onPluginDetachednull249 override fun onPluginDetached(manager: PluginLifecycleManager<ClockProviderPlugin>) { 250 val removed = mutableListOf<ClockInfo>() 251 availableClocks.entries.removeAll { 252 if (it.value.manager != manager) { 253 return@removeAll false 254 } 255 256 removed.add(it.value) 257 return@removeAll true 258 } 259 260 removed.forEach(::onDisconnected) 261 if (removed.size > 0) { 262 triggerOnAvailableClocksChanged() 263 } 264 } 265 } 266 267 private val userSwitchObserver = 268 object : UserSwitchObserver() { onUserSwitchCompletenull269 override fun onUserSwitchComplete(newUserId: Int) { 270 scope.launch(bgDispatcher) { querySettings() } 271 } 272 } 273 274 // TODO(b/267372164): Migrate to flows 275 var settings: ClockSettings? = null 276 get() = field 277 protected set(value) { 278 if (field != value) { 279 field = value 280 verifyLoadedProviders() 281 triggerOnCurrentClockChanged() 282 } 283 } 284 285 var isRegistered: Boolean = false 286 private set 287 288 @OpenForTesting querySettingsnull289 open fun querySettings() { 290 assert.isNotMainThread() 291 val result = 292 try { 293 val json = 294 if (handleAllUsers) { 295 Settings.Secure.getStringForUser( 296 context.contentResolver, 297 Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE, 298 ActivityManager.getCurrentUser() 299 ) 300 } else { 301 Settings.Secure.getString( 302 context.contentResolver, 303 Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE 304 ) 305 } 306 307 ClockSettings.deserialize(json) 308 } catch (ex: Exception) { 309 logger.e("Failed to parse clock settings", ex) 310 null 311 } 312 settings = result 313 } 314 315 @OpenForTesting applySettingsnull316 open fun applySettings(value: ClockSettings?) { 317 assert.isNotMainThread() 318 319 try { 320 value?.metadata?.put(KEY_TIMESTAMP, System.currentTimeMillis()) 321 322 val json = ClockSettings.serialize(value) 323 if (handleAllUsers) { 324 Settings.Secure.putStringForUser( 325 context.contentResolver, 326 Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE, 327 json, 328 ActivityManager.getCurrentUser() 329 ) 330 } else { 331 Settings.Secure.putString( 332 context.contentResolver, 333 Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE, 334 json 335 ) 336 } 337 } catch (ex: Exception) { 338 logger.e("Failed to set clock settings", ex) 339 } 340 settings = value 341 } 342 343 private var isClockChanged = AtomicBoolean(false) 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) triggerOnAvailableClocksChangednull358 private fun triggerOnAvailableClocksChanged() { 359 val shouldSchedule = isClockListChanged.compareAndSet(false, true) 360 if (!shouldSchedule) { 361 return 362 } 363 364 scope.launch(mainDispatcher) { 365 assert.isMainThread() 366 isClockListChanged.set(false) 367 clockChangeListeners.forEach { it.onAvailableClocksChanged() } 368 } 369 } 370 mutateSettingnull371 public suspend fun mutateSetting(mutator: (ClockSettings) -> ClockSettings) { 372 withContext(bgDispatcher) { applySettings(mutator(settings ?: ClockSettings())) } 373 } 374 375 var currentClockId: ClockId 376 get() = settings?.clockId ?: fallbackClockId 377 set(value) { <lambda>null378 scope.launch(bgDispatcher) { mutateSetting { it.copy(clockId = value) } } 379 } 380 381 var seedColor: Int? 382 get() = settings?.seedColor 383 set(value) { <lambda>null384 scope.launch(bgDispatcher) { mutateSetting { it.copy(seedColor = value) } } 385 } 386 387 // Returns currentClockId if clock is connected, otherwise DEFAULT_CLOCK_ID. Since this 388 // is dependent on which clocks are connected, it may change when a clock is installed or 389 // removed from the device (unlike currentClockId). 390 // TODO: Merge w/ CurrentClockId when we convert to a flow. We shouldn't need both behaviors. 391 val activeClockId: String 392 get() { 393 if (!availableClocks.containsKey(currentClockId)) { 394 return DEFAULT_CLOCK_ID 395 } 396 return currentClockId 397 } 398 399 init { 400 // Initialize & register default clock designs 401 defaultClockProvider.initialize(clockBuffers) 402 for (clock in defaultClockProvider.getClocks()) { 403 availableClocks[clock.clockId] = ClockInfo(clock, defaultClockProvider, null) 404 } 405 406 // Something has gone terribly wrong if the default clock isn't present 407 if (!availableClocks.containsKey(DEFAULT_CLOCK_ID)) { 408 throw IllegalArgumentException( 409 "$defaultClockProvider did not register clock at $DEFAULT_CLOCK_ID" 410 ) 411 } 412 } 413 registerListenersnull414 fun registerListeners() { 415 if (!isEnabled || isRegistered) { 416 return 417 } 418 419 isRegistered = true 420 421 pluginManager.addPluginListener( 422 pluginListener, 423 ClockProviderPlugin::class.java, 424 /*allowMultiple=*/ true 425 ) 426 427 scope.launch(bgDispatcher) { querySettings() } 428 if (handleAllUsers) { 429 context.contentResolver.registerContentObserver( 430 Settings.Secure.getUriFor(Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE), 431 /*notifyForDescendants=*/ false, 432 settingObserver, 433 UserHandle.USER_ALL 434 ) 435 436 ActivityManager.getService().registerUserSwitchObserver(userSwitchObserver, TAG) 437 } else { 438 context.contentResolver.registerContentObserver( 439 Settings.Secure.getUriFor(Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE), 440 /*notifyForDescendants=*/ false, 441 settingObserver 442 ) 443 } 444 } 445 unregisterListenersnull446 fun unregisterListeners() { 447 if (!isRegistered) { 448 return 449 } 450 451 isRegistered = false 452 453 pluginManager.removePluginListener(pluginListener) 454 context.contentResolver.unregisterContentObserver(settingObserver) 455 if (handleAllUsers) { 456 ActivityManager.getService().unregisterUserSwitchObserver(userSwitchObserver) 457 } 458 } 459 460 private var isQueued = AtomicBoolean(false) verifyLoadedProvidersnull461 fun verifyLoadedProviders() { 462 val shouldSchedule = isQueued.compareAndSet(false, true) 463 if (!shouldSchedule) { 464 logger.v("verifyLoadedProviders: shouldSchedule=false") 465 return 466 } 467 468 scope.launch(bgDispatcher) { 469 // TODO(b/267372164): Use better threading approach when converting to flows 470 synchronized(availableClocks) { 471 isQueued.set(false) 472 if (keepAllLoaded) { 473 logger.i("verifyLoadedProviders: keepAllLoaded=true") 474 // Enforce that all plugins are loaded if requested 475 for ((_, info) in availableClocks) { 476 info.manager?.loadPlugin() 477 } 478 return@launch 479 } 480 481 val currentClock = availableClocks[currentClockId] 482 if (currentClock == null) { 483 logger.i("verifyLoadedProviders: currentClock=null") 484 // Current Clock missing, load no plugins and use default 485 for ((_, info) in availableClocks) { 486 info.manager?.unloadPlugin() 487 } 488 return@launch 489 } 490 491 logger.i("verifyLoadedProviders: load currentClock") 492 val currentManager = currentClock.manager 493 currentManager?.loadPlugin() 494 495 for ((_, info) in availableClocks) { 496 val manager = info.manager 497 if (manager != null && currentManager != manager) { 498 manager.unloadPlugin() 499 } 500 } 501 } 502 } 503 } 504 onConnectednull505 private fun onConnected(info: ClockInfo) { 506 val isCurrent = currentClockId == info.metadata.clockId 507 logger.log( 508 if (isCurrent) LogLevel.INFO else LogLevel.DEBUG, 509 { "Connected $str1 @$str2" + if (bool1) " (Current Clock)" else "" } 510 ) { 511 str1 = info.metadata.clockId 512 str2 = info.manager.toString() 513 bool1 = isCurrent 514 } 515 } 516 onLoadednull517 private fun onLoaded(info: ClockInfo) { 518 val isCurrent = currentClockId == info.metadata.clockId 519 logger.log( 520 if (isCurrent) LogLevel.INFO else LogLevel.DEBUG, 521 { "Loaded $str1 @$str2" + if (bool1) " (Current Clock)" else "" } 522 ) { 523 str1 = info.metadata.clockId 524 str2 = info.manager.toString() 525 bool1 = isCurrent 526 } 527 528 if (isCurrent) { 529 triggerOnCurrentClockChanged() 530 } 531 } 532 onUnloadednull533 private fun onUnloaded(info: ClockInfo) { 534 val isCurrent = currentClockId == info.metadata.clockId 535 logger.log( 536 if (isCurrent) LogLevel.WARNING else LogLevel.DEBUG, 537 { "Unloaded $str1 @$str2" + if (bool1) " (Current Clock)" else "" } 538 ) { 539 str1 = info.metadata.clockId 540 str2 = info.manager.toString() 541 bool1 = isCurrent 542 } 543 544 if (isCurrent) { 545 triggerOnCurrentClockChanged() 546 } 547 } 548 onDisconnectednull549 private fun onDisconnected(info: ClockInfo) { 550 val isCurrent = currentClockId == info.metadata.clockId 551 logger.log( 552 if (isCurrent) LogLevel.INFO else LogLevel.DEBUG, 553 { "Disconnected $str1 @$str2" + if (bool1) " (Current Clock)" else "" } 554 ) { 555 str1 = info.metadata.clockId 556 str2 = info.manager.toString() 557 bool1 = isCurrent 558 } 559 } 560 getClocksnull561 fun getClocks(): List<ClockMetadata> { 562 if (!isEnabled) { 563 return listOf(availableClocks[DEFAULT_CLOCK_ID]!!.metadata) 564 } 565 return availableClocks.map { (_, clock) -> clock.metadata } 566 } 567 getClockThumbnailnull568 fun getClockThumbnail(clockId: ClockId): Drawable? = 569 availableClocks[clockId]?.provider?.getClockThumbnail(clockId) 570 571 fun createExampleClock(clockId: ClockId): ClockController? = createClock(clockId) 572 573 /** 574 * Adds [listener] to receive future clock changes. 575 * 576 * Calling from main thread to make sure the access is thread safe. 577 */ 578 fun registerClockChangeListener(listener: ClockChangeListener) { 579 assert.isMainThread() 580 clockChangeListeners.add(listener) 581 } 582 583 /** 584 * Removes [listener] from future clock changes. 585 * 586 * Calling from main thread to make sure the access is thread safe. 587 */ unregisterClockChangeListenernull588 fun unregisterClockChangeListener(listener: ClockChangeListener) { 589 assert.isMainThread() 590 clockChangeListeners.remove(listener) 591 } 592 createCurrentClocknull593 fun createCurrentClock(): ClockController { 594 val clockId = currentClockId 595 if (isEnabled && clockId.isNotEmpty()) { 596 val clock = createClock(clockId) 597 if (clock != null) { 598 logger.i({ "Rendering clock $str1" }) { str1 = clockId } 599 return clock 600 } else if (availableClocks.containsKey(clockId)) { 601 logger.w({ "Clock $str1 not loaded; using default" }) { str1 = clockId } 602 verifyLoadedProviders() 603 } else { 604 logger.e({ "Clock $str1 not found; using default" }) { str1 = clockId } 605 } 606 } 607 608 return createClock(DEFAULT_CLOCK_ID)!! 609 } 610 createClocknull611 private fun createClock(targetClockId: ClockId): ClockController? { 612 var settings = this.settings ?: ClockSettings() 613 if (targetClockId != settings.clockId) { 614 settings = settings.copy(clockId = targetClockId) 615 } 616 return availableClocks[targetClockId]?.provider?.createClock(settings) 617 } 618 dumpnull619 fun dump(pw: PrintWriter, args: Array<out String>) { 620 pw.println("ClockRegistry:") 621 pw.println(" settings = $settings") 622 for ((id, info) in availableClocks) { 623 pw.println(" availableClocks[$id] = $info") 624 } 625 } 626 627 private data class ClockInfo( 628 val metadata: ClockMetadata, 629 var provider: ClockProvider?, 630 val manager: PluginLifecycleManager<ClockProviderPlugin>?, 631 ) 632 } 633