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 replacementMap = ConcurrentHashMap<ClockId, ClockId>() 107 private val availableClocks = ConcurrentHashMap<ClockId, ClockInfo>() 108 private val clockChangeListeners = mutableListOf<ClockChangeListener>() 109 private val settingObserver = 110 object : ContentObserver(null) { onChangenull111 override fun onChange( 112 selfChange: Boolean, 113 uris: Collection<Uri>, 114 flags: Int, 115 userId: Int, 116 ) { 117 scope.launch(bgDispatcher) { querySettings() } 118 } 119 } 120 121 private val pluginListener = 122 object : PluginListener<ClockProviderPlugin> { onPluginAttachednull123 override fun onPluginAttached( 124 manager: PluginLifecycleManager<ClockProviderPlugin> 125 ): Boolean { 126 manager.setLogFunc({ tag, msg -> 127 (clockBuffers?.infraMessageBuffer as LogBuffer?)?.log(tag, LogLevel.DEBUG, msg) 128 }) 129 if (keepAllLoaded) { 130 // Always load new plugins if requested 131 return true 132 } 133 134 val knownClocks = KNOWN_PLUGINS.get(manager.getPackage()) 135 if (knownClocks == null) { 136 logger.w({ "Loading unrecognized clock package: $str1" }) { 137 str1 = manager.getPackage() 138 } 139 return true 140 } 141 142 logger.i({ "Skipping initial load of known clock package package: $str1" }) { 143 str1 = manager.getPackage() 144 } 145 146 var isCurrentClock = false 147 var isClockListChanged = false 148 for (metadata in knownClocks) { 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 "Using $str2 since it was attached first." 161 }) { 162 str1 = id 163 str2 = info.manager?.toString() ?: info.provider?.toString() 164 str3 = manager.toString() 165 } 166 continue 167 } 168 169 isCurrentClock = isCurrentClock || currentClockId == metadata.clockId 170 info.provider = null 171 } 172 173 if (isClockListChanged) { 174 triggerOnAvailableClocksChanged() 175 } 176 verifyLoadedProviders() 177 178 // Load immediately if it's the current clock, otherwise let verifyLoadedProviders 179 // load and unload clocks as necessary on the background thread. 180 return isCurrentClock 181 } 182 onPluginLoadednull183 override fun onPluginLoaded( 184 plugin: ClockProviderPlugin, 185 pluginContext: Context, 186 manager: PluginLifecycleManager<ClockProviderPlugin>, 187 ) { 188 plugin.initialize(clockBuffers) 189 190 var isClockListChanged = false 191 for (clock in plugin.getClocks()) { 192 val id = clock.clockId 193 val info = 194 availableClocks.concurrentGetOrPut(id, ClockInfo(clock, plugin, manager)) { 195 isClockListChanged = true 196 onConnected(it) 197 } 198 199 if (manager != info.manager) { 200 logger.e({ 201 "Clock Id conflict on load: " + 202 "$str1 is double registered by $str2 and $str3. " + 203 "Using $str2 since it was attached first." 204 }) { 205 str1 = id 206 str2 = info.manager?.toString() ?: info.provider?.toString() 207 str3 = manager.toString() 208 } 209 manager.unloadPlugin() 210 continue 211 } 212 213 clock.replacementTarget?.let { replacementMap[id] = it } 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 "Using $str2 since it was attached first." 236 }) { 237 str1 = id 238 str2 = info?.manager?.toString() ?: info?.provider?.toString() 239 str3 = manager.toString() 240 } 241 continue 242 } 243 info.provider = null 244 onUnloaded(info) 245 } 246 247 verifyLoadedProviders() 248 } 249 onPluginDetachednull250 override fun onPluginDetached(manager: PluginLifecycleManager<ClockProviderPlugin>) { 251 val removed = mutableListOf<ClockInfo>() 252 availableClocks.entries.removeAll { 253 if (it.value.manager != manager) { 254 return@removeAll false 255 } 256 257 removed.add(it.value) 258 return@removeAll true 259 } 260 261 removed.forEach(::onDisconnected) 262 if (removed.size > 0) { 263 triggerOnAvailableClocksChanged() 264 } 265 } 266 } 267 268 private val userSwitchObserver = 269 object : UserSwitchObserver() { onUserSwitchCompletenull270 override fun onUserSwitchComplete(newUserId: Int) { 271 scope.launch(bgDispatcher) { querySettings() } 272 } 273 } 274 275 // TODO(b/267372164): Migrate to flows 276 var settings: ClockSettings? = null 277 get() = field 278 protected set(value) { 279 if (field != value) { 280 field = value 281 verifyLoadedProviders() 282 triggerOnCurrentClockChanged() 283 } 284 } 285 286 var isRegistered: Boolean = false 287 private set 288 289 @OpenForTesting querySettingsnull290 open fun querySettings() { 291 assert.isNotMainThread() 292 val result = 293 try { 294 val json = 295 if (handleAllUsers) { 296 Settings.Secure.getStringForUser( 297 context.contentResolver, 298 Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE, 299 ActivityManager.getCurrentUser(), 300 ) 301 } else { 302 Settings.Secure.getString( 303 context.contentResolver, 304 Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE, 305 ) 306 } 307 json?.let { ClockSettings.fromJson(JSONObject(it)) } 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 val json = 321 value?.let { 322 it.metadata.put(KEY_TIMESTAMP, System.currentTimeMillis()) 323 ClockSettings.toJson(it) 324 } ?: "" 325 326 if (handleAllUsers) { 327 Settings.Secure.putStringForUser( 328 context.contentResolver, 329 Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE, 330 json.toString(), 331 ActivityManager.getCurrentUser(), 332 ) 333 } else { 334 Settings.Secure.putString( 335 context.contentResolver, 336 Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE, 337 json.toString(), 338 ) 339 } 340 } catch (ex: Exception) { 341 logger.e("Failed to set clock settings", ex) 342 } 343 settings = value 344 } 345 346 private var isClockChanged = AtomicBoolean(false) 347 triggerOnCurrentClockChangednull348 private fun triggerOnCurrentClockChanged() { 349 val shouldSchedule = isClockChanged.compareAndSet(false, true) 350 if (!shouldSchedule) { 351 return 352 } 353 354 scope.launch(mainDispatcher) { 355 assert.isMainThread() 356 isClockChanged.set(false) 357 clockChangeListeners.forEach { it.onCurrentClockChanged() } 358 } 359 } 360 361 private var isClockListChanged = AtomicBoolean(false) 362 triggerOnAvailableClocksChangednull363 private fun triggerOnAvailableClocksChanged() { 364 val shouldSchedule = isClockListChanged.compareAndSet(false, true) 365 if (!shouldSchedule) { 366 return 367 } 368 369 scope.launch(mainDispatcher) { 370 assert.isMainThread() 371 isClockListChanged.set(false) 372 clockChangeListeners.forEach { it.onAvailableClocksChanged() } 373 } 374 } 375 mutateSettingnull376 public suspend fun mutateSetting(mutator: (ClockSettings) -> ClockSettings) { 377 withContext(bgDispatcher) { applySettings(mutator(settings ?: ClockSettings())) } 378 } 379 380 var currentClockId: ClockId 381 get() = settings?.clockId ?: fallbackClockId 382 set(value) { <lambda>null383 scope.launch(bgDispatcher) { mutateSetting { it.copy(clockId = value) } } 384 } 385 386 var seedColor: Int? 387 get() = settings?.seedColor 388 set(value) { <lambda>null389 scope.launch(bgDispatcher) { mutateSetting { it.copy(seedColor = value) } } 390 } 391 392 // Returns currentClockId if clock is connected, otherwise DEFAULT_CLOCK_ID. Since this 393 // is dependent on which clocks are connected, it may change when a clock is installed or 394 // removed from the device (unlike currentClockId). 395 // TODO: Merge w/ CurrentClockId when we convert to a flow. We shouldn't need both behaviors. 396 val activeClockId: String 397 get() { 398 var id = currentClockId 399 if (!availableClocks.containsKey(id)) { 400 return DEFAULT_CLOCK_ID 401 } 402 return replacementMap[id] ?: id 403 } 404 405 init { 406 // Initialize & register default clock designs 407 defaultClockProvider.initialize(clockBuffers) 408 for (clock in defaultClockProvider.getClocks()) { 409 availableClocks[clock.clockId] = ClockInfo(clock, defaultClockProvider, null) <lambda>null410 clock.replacementTarget?.let { replacementMap[clock.clockId] = it } 411 } 412 413 // Something has gone terribly wrong if the default clock isn't present 414 if (!availableClocks.containsKey(DEFAULT_CLOCK_ID)) { 415 throw IllegalArgumentException( 416 "$defaultClockProvider did not register clock at $DEFAULT_CLOCK_ID" 417 ) 418 } 419 } 420 registerListenersnull421 fun registerListeners() { 422 if (!isEnabled || isRegistered) { 423 return 424 } 425 426 isRegistered = true 427 428 pluginManager.addPluginListener( 429 pluginListener, 430 ClockProviderPlugin::class.java, 431 /*allowMultiple=*/ true, 432 ) 433 434 scope.launch(bgDispatcher) { querySettings() } 435 if (handleAllUsers) { 436 context.contentResolver.registerContentObserver( 437 Settings.Secure.getUriFor(Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE), 438 /*notifyForDescendants=*/ false, 439 settingObserver, 440 UserHandle.USER_ALL, 441 ) 442 443 ActivityManager.getService().registerUserSwitchObserver(userSwitchObserver, TAG) 444 } else { 445 context.contentResolver.registerContentObserver( 446 Settings.Secure.getUriFor(Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE), 447 /*notifyForDescendants=*/ false, 448 settingObserver, 449 ) 450 } 451 } 452 unregisterListenersnull453 fun unregisterListeners() { 454 if (!isRegistered) { 455 return 456 } 457 458 isRegistered = false 459 460 pluginManager.removePluginListener(pluginListener) 461 context.contentResolver.unregisterContentObserver(settingObserver) 462 if (handleAllUsers) { 463 ActivityManager.getService().unregisterUserSwitchObserver(userSwitchObserver) 464 } 465 } 466 467 private var isQueued = AtomicBoolean(false) 468 verifyLoadedProvidersnull469 fun verifyLoadedProviders() { 470 val shouldSchedule = isQueued.compareAndSet(false, true) 471 if (!shouldSchedule) { 472 logger.v("verifyLoadedProviders: shouldSchedule=false") 473 return 474 } 475 476 scope.launch(bgDispatcher) { 477 // TODO(b/267372164): Use better threading approach when converting to flows 478 synchronized(availableClocks) { 479 isQueued.set(false) 480 if (keepAllLoaded) { 481 logger.i("verifyLoadedProviders: keepAllLoaded=true") 482 // Enforce that all plugins are loaded if requested 483 for ((_, info) in availableClocks) { 484 info.manager?.loadPlugin() 485 } 486 return@launch 487 } 488 489 val currentClock = availableClocks[currentClockId] 490 if (currentClock == null) { 491 logger.i("verifyLoadedProviders: currentClock=null") 492 // Current Clock missing, load no plugins and use default 493 for ((_, info) in availableClocks) { 494 info.manager?.unloadPlugin() 495 } 496 return@launch 497 } 498 499 logger.i("verifyLoadedProviders: load currentClock") 500 val currentManager = currentClock.manager 501 currentManager?.loadPlugin() 502 503 for ((_, info) in availableClocks) { 504 val manager = info.manager 505 if (manager != null && currentManager != manager) { 506 manager.unloadPlugin() 507 } 508 } 509 } 510 } 511 } 512 onConnectednull513 private fun onConnected(info: ClockInfo) { 514 val isCurrent = currentClockId == info.metadata.clockId 515 logger.log( 516 if (isCurrent) LogLevel.INFO else LogLevel.DEBUG, 517 { "Connected $str1 @$str2" + if (bool1) " (Current Clock)" else "" }, 518 ) { 519 str1 = info.metadata.clockId 520 str2 = info.manager.toString() 521 bool1 = isCurrent 522 } 523 } 524 onLoadednull525 private fun onLoaded(info: ClockInfo) { 526 val isCurrent = currentClockId == info.metadata.clockId 527 logger.log( 528 if (isCurrent) LogLevel.INFO else LogLevel.DEBUG, 529 { "Loaded $str1 @$str2" + if (bool1) " (Current Clock)" else "" }, 530 ) { 531 str1 = info.metadata.clockId 532 str2 = info.manager.toString() 533 bool1 = isCurrent 534 } 535 536 if (isCurrent) { 537 triggerOnCurrentClockChanged() 538 } 539 } 540 onUnloadednull541 private fun onUnloaded(info: ClockInfo) { 542 val isCurrent = currentClockId == info.metadata.clockId 543 logger.log( 544 if (isCurrent) LogLevel.WARNING else LogLevel.DEBUG, 545 { "Unloaded $str1 @$str2" + if (bool1) " (Current Clock)" else "" }, 546 ) { 547 str1 = info.metadata.clockId 548 str2 = info.manager.toString() 549 bool1 = isCurrent 550 } 551 552 if (isCurrent) { 553 triggerOnCurrentClockChanged() 554 } 555 } 556 onDisconnectednull557 private fun onDisconnected(info: ClockInfo) { 558 val isCurrent = currentClockId == info.metadata.clockId 559 logger.log( 560 if (isCurrent) LogLevel.INFO else LogLevel.DEBUG, 561 { "Disconnected $str1 @$str2" + if (bool1) " (Current Clock)" else "" }, 562 ) { 563 str1 = info.metadata.clockId 564 str2 = info.manager.toString() 565 bool1 = isCurrent 566 } 567 } 568 getClocksnull569 fun getClocks(includeDeprecated: Boolean = false): List<ClockMetadata> { 570 return when { 571 !isEnabled -> listOf(availableClocks[DEFAULT_CLOCK_ID]!!.metadata) 572 includeDeprecated -> availableClocks.map { (_, clock) -> clock.metadata } 573 else -> availableClocks.map { (_, clock) -> clock.metadata }.filter { !it.isDeprecated } 574 } 575 } 576 getClockPickerConfignull577 fun getClockPickerConfig(clockId: ClockId): ClockPickerConfig? { 578 val clockSettings = 579 settings?.let { if (clockId == it.clockId) it else null } ?: ClockSettings(clockId) 580 return availableClocks[clockId]?.provider?.getClockPickerConfig(clockSettings) 581 } 582 createExampleClocknull583 fun createExampleClock(clockId: ClockId): ClockController? = createClock(clockId) 584 585 /** 586 * Adds [listener] to receive future clock changes. 587 * 588 * Calling from main thread to make sure the access is thread safe. 589 */ 590 fun registerClockChangeListener(listener: ClockChangeListener) { 591 assert.isMainThread() 592 clockChangeListeners.add(listener) 593 } 594 595 /** 596 * Removes [listener] from future clock changes. 597 * 598 * Calling from main thread to make sure the access is thread safe. 599 */ unregisterClockChangeListenernull600 fun unregisterClockChangeListener(listener: ClockChangeListener) { 601 assert.isMainThread() 602 clockChangeListeners.remove(listener) 603 } 604 createCurrentClocknull605 fun createCurrentClock(): ClockController { 606 val clockId = currentClockId 607 if (isEnabled && clockId.isNotEmpty()) { 608 val clock = createClock(clockId) 609 if (clock != null) { 610 logger.i({ "Rendering clock $str1" }) { str1 = clockId } 611 return clock 612 } else if (availableClocks.containsKey(clockId)) { 613 logger.w({ "Clock $str1 not loaded; using default" }) { str1 = clockId } 614 verifyLoadedProviders() 615 } else { 616 logger.e({ "Clock $str1 not found; using default" }) { str1 = clockId } 617 } 618 } 619 620 return createClock(DEFAULT_CLOCK_ID)!! 621 } 622 createClocknull623 private fun createClock(targetClockId: ClockId): ClockController? { 624 var settings = this.settings ?: ClockSettings() 625 if (targetClockId != settings.clockId) { 626 settings = settings.copy(clockId = targetClockId) 627 } 628 return availableClocks[targetClockId]?.provider?.createClock(settings) 629 } 630 dumpnull631 fun dump(pw: PrintWriter, args: Array<out String>) { 632 pw.println("ClockRegistry:") 633 pw.println(" settings = $settings") 634 for ((id, info) in availableClocks) { 635 pw.println(" availableClocks[$id] = $info") 636 } 637 } 638 639 private data class ClockInfo( 640 val metadata: ClockMetadata, 641 var provider: ClockProvider?, 642 val manager: PluginLifecycleManager<ClockProviderPlugin>?, 643 ) 644 } 645