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.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
<lambda>null80 private val TMP_MESSAGE: LogMessage by lazy { LogMessageImpl.Factory.create() }
81
tryLognull82 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
onCurrentClockChangednull125 fun onCurrentClockChanged() {}
126
127 // Called when the list of available clocks changes
onAvailableClocksChangednull128 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) {
onChangenull136 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> {
onPluginAttachednull148 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
onPluginLoadednull214 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
onPluginUnloadednull260 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
onPluginDetachednull290 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() {
onUserSwitchCompletenull310 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
querySettingsnull330 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
applySettingsnull357 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
assertMainThreadnull385 protected open fun assertMainThread() {
386 Assert.isMainThread()
387 }
388
389 @OpenForTesting
assertNotMainThreadnull390 protected open fun assertNotMainThread() {
391 Assert.isNotMainThread()
392 }
393
394 private var isClockChanged = AtomicBoolean(false)
triggerOnCurrentClockChangednull395 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)
triggerOnAvailableClocksChangednull409 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
mutateSettingnull422 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) {
<lambda>null429 scope.launch(bgDispatcher) { mutateSetting { it.copy(clockId = value) } }
430 }
431
432 var seedColor: Int?
433 get() = settings?.seedColor
434 set(value) {
<lambda>null435 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
registerListenersnull464 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
unregisterListenersnull496 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)
verifyLoadedProvidersnull511 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
onConnectednull576 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
onLoadednull590 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
onUnloadednull608 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
onDisconnectednull626 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
getClocksnull640 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
getClockThumbnailnull647 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 */
unregisterClockChangeListenernull667 fun unregisterClockChangeListener(listener: ClockChangeListener) {
668 assertMainThread()
669 clockChangeListeners.remove(listener)
670 }
671
createCurrentClocknull672 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
createClocknull700 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
dumpnull708 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