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