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