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 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                     val info =
192                         availableClocks.concurrentGetOrPut(id, ClockInfo(clock, plugin, manager)) {
193                             isClockListChanged = true
194                             onConnected(it)
195                         }
196 
197                     if (manager != info.manager) {
198                         logger.e({
199                             "Clock Id conflict on load: " +
200                                 "$str1 is double registered by $str2 and $str3"
201                         }) {
202                             str1 = id
203                             str2 = info.manager.toString()
204                             str3 = manager.toString()
205                         }
206                         manager.unloadPlugin()
207                         continue
208                     }
209 
210                     info.provider = plugin
211                     onLoaded(info)
212                 }
213 
214                 if (isClockListChanged) {
215                     triggerOnAvailableClocksChanged()
216                 }
217                 verifyLoadedProviders()
218             }
219 
onPluginUnloadednull220             override fun onPluginUnloaded(
221                 plugin: ClockProviderPlugin,
222                 manager: PluginLifecycleManager<ClockProviderPlugin>,
223             ) {
224                 for (clock in plugin.getClocks()) {
225                     val id = clock.clockId
226                     val info = availableClocks[id]
227                     if (info?.manager != manager) {
228                         logger.e({
229                             "Clock Id conflict on unload: " +
230                                 "$str1 is double registered by $str2 and $str3"
231                         }) {
232                             str1 = id
233                             str2 = info?.manager.toString()
234                             str3 = manager.toString()
235                         }
236                         continue
237                     }
238                     info.provider = null
239                     onUnloaded(info)
240                 }
241 
242                 verifyLoadedProviders()
243             }
244 
onPluginDetachednull245             override fun onPluginDetached(manager: PluginLifecycleManager<ClockProviderPlugin>) {
246                 val removed = mutableListOf<ClockInfo>()
247                 availableClocks.entries.removeAll {
248                     if (it.value.manager != manager) {
249                         return@removeAll false
250                     }
251 
252                     removed.add(it.value)
253                     return@removeAll true
254                 }
255 
256                 removed.forEach(::onDisconnected)
257                 if (removed.size > 0) {
258                     triggerOnAvailableClocksChanged()
259                 }
260             }
261         }
262 
263     private val userSwitchObserver =
264         object : UserSwitchObserver() {
onUserSwitchCompletenull265             override fun onUserSwitchComplete(newUserId: Int) {
266                 scope.launch(bgDispatcher) { querySettings() }
267             }
268         }
269 
270     // TODO(b/267372164): Migrate to flows
271     var settings: ClockSettings? = null
272         get() = field
273         protected set(value) {
274             if (field != value) {
275                 field = value
276                 verifyLoadedProviders()
277                 triggerOnCurrentClockChanged()
278             }
279         }
280 
281     var isRegistered: Boolean = false
282         private set
283 
284     @OpenForTesting
querySettingsnull285     open fun querySettings() {
286         assert.isNotMainThread()
287         val result =
288             try {
289                 val json =
290                     if (handleAllUsers) {
291                         Settings.Secure.getStringForUser(
292                             context.contentResolver,
293                             Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE,
294                             ActivityManager.getCurrentUser(),
295                         )
296                     } else {
297                         Settings.Secure.getString(
298                             context.contentResolver,
299                             Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE,
300                         )
301                     }
302 
303                 ClockSettings.fromJson(JSONObject(json))
304             } catch (ex: Exception) {
305                 logger.e("Failed to parse clock settings", ex)
306                 null
307             }
308         settings = result
309     }
310 
311     @OpenForTesting
applySettingsnull312     open fun applySettings(value: ClockSettings?) {
313         assert.isNotMainThread()
314 
315         try {
316             val json =
317                 value?.let {
318                     it.metadata.put(KEY_TIMESTAMP, System.currentTimeMillis())
319                     ClockSettings.toJson(it)
320                 } ?: ""
321 
322             if (handleAllUsers) {
323                 Settings.Secure.putStringForUser(
324                     context.contentResolver,
325                     Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE,
326                     json.toString(),
327                     ActivityManager.getCurrentUser(),
328                 )
329             } else {
330                 Settings.Secure.putString(
331                     context.contentResolver,
332                     Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE,
333                     json.toString(),
334                 )
335             }
336         } catch (ex: Exception) {
337             logger.e("Failed to set clock settings", ex)
338         }
339         settings = value
340     }
341 
342     private var isClockChanged = AtomicBoolean(false)
343 
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)
358 
triggerOnAvailableClocksChangednull359     private fun triggerOnAvailableClocksChanged() {
360         val shouldSchedule = isClockListChanged.compareAndSet(false, true)
361         if (!shouldSchedule) {
362             return
363         }
364 
365         scope.launch(mainDispatcher) {
366             assert.isMainThread()
367             isClockListChanged.set(false)
368             clockChangeListeners.forEach { it.onAvailableClocksChanged() }
369         }
370     }
371 
mutateSettingnull372     public suspend fun mutateSetting(mutator: (ClockSettings) -> ClockSettings) {
373         withContext(bgDispatcher) { applySettings(mutator(settings ?: ClockSettings())) }
374     }
375 
376     var currentClockId: ClockId
377         get() = settings?.clockId ?: fallbackClockId
378         set(value) {
<lambda>null379             scope.launch(bgDispatcher) { mutateSetting { it.copy(clockId = value) } }
380         }
381 
382     var seedColor: Int?
383         get() = settings?.seedColor
384         set(value) {
<lambda>null385             scope.launch(bgDispatcher) { mutateSetting { it.copy(seedColor = value) } }
386         }
387 
388     // Returns currentClockId if clock is connected, otherwise DEFAULT_CLOCK_ID. Since this
389     // is dependent on which clocks are connected, it may change when a clock is installed or
390     // removed from the device (unlike currentClockId).
391     // TODO: Merge w/ CurrentClockId when we convert to a flow. We shouldn't need both behaviors.
392     val activeClockId: String
393         get() {
394             if (!availableClocks.containsKey(currentClockId)) {
395                 return DEFAULT_CLOCK_ID
396             }
397             return currentClockId
398         }
399 
400     init {
401         // Initialize & register default clock designs
402         defaultClockProvider.initialize(clockBuffers)
403         for (clock in defaultClockProvider.getClocks()) {
404             availableClocks[clock.clockId] = ClockInfo(clock, defaultClockProvider, null)
405         }
406 
407         // Something has gone terribly wrong if the default clock isn't present
408         if (!availableClocks.containsKey(DEFAULT_CLOCK_ID)) {
409             throw IllegalArgumentException(
410                 "$defaultClockProvider did not register clock at $DEFAULT_CLOCK_ID"
411             )
412         }
413     }
414 
registerListenersnull415     fun registerListeners() {
416         if (!isEnabled || isRegistered) {
417             return
418         }
419 
420         isRegistered = true
421 
422         pluginManager.addPluginListener(
423             pluginListener,
424             ClockProviderPlugin::class.java,
425             /*allowMultiple=*/ true,
426         )
427 
428         scope.launch(bgDispatcher) { querySettings() }
429         if (handleAllUsers) {
430             context.contentResolver.registerContentObserver(
431                 Settings.Secure.getUriFor(Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE),
432                 /*notifyForDescendants=*/ false,
433                 settingObserver,
434                 UserHandle.USER_ALL,
435             )
436 
437             ActivityManager.getService().registerUserSwitchObserver(userSwitchObserver, TAG)
438         } else {
439             context.contentResolver.registerContentObserver(
440                 Settings.Secure.getUriFor(Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE),
441                 /*notifyForDescendants=*/ false,
442                 settingObserver,
443             )
444         }
445     }
446 
unregisterListenersnull447     fun unregisterListeners() {
448         if (!isRegistered) {
449             return
450         }
451 
452         isRegistered = false
453 
454         pluginManager.removePluginListener(pluginListener)
455         context.contentResolver.unregisterContentObserver(settingObserver)
456         if (handleAllUsers) {
457             ActivityManager.getService().unregisterUserSwitchObserver(userSwitchObserver)
458         }
459     }
460 
461     private var isQueued = AtomicBoolean(false)
462 
verifyLoadedProvidersnull463     fun verifyLoadedProviders() {
464         val shouldSchedule = isQueued.compareAndSet(false, true)
465         if (!shouldSchedule) {
466             logger.v("verifyLoadedProviders: shouldSchedule=false")
467             return
468         }
469 
470         scope.launch(bgDispatcher) {
471             // TODO(b/267372164): Use better threading approach when converting to flows
472             synchronized(availableClocks) {
473                 isQueued.set(false)
474                 if (keepAllLoaded) {
475                     logger.i("verifyLoadedProviders: keepAllLoaded=true")
476                     // Enforce that all plugins are loaded if requested
477                     for ((_, info) in availableClocks) {
478                         info.manager?.loadPlugin()
479                     }
480                     return@launch
481                 }
482 
483                 val currentClock = availableClocks[currentClockId]
484                 if (currentClock == null) {
485                     logger.i("verifyLoadedProviders: currentClock=null")
486                     // Current Clock missing, load no plugins and use default
487                     for ((_, info) in availableClocks) {
488                         info.manager?.unloadPlugin()
489                     }
490                     return@launch
491                 }
492 
493                 logger.i("verifyLoadedProviders: load currentClock")
494                 val currentManager = currentClock.manager
495                 currentManager?.loadPlugin()
496 
497                 for ((_, info) in availableClocks) {
498                     val manager = info.manager
499                     if (manager != null && currentManager != manager) {
500                         manager.unloadPlugin()
501                     }
502                 }
503             }
504         }
505     }
506 
onConnectednull507     private fun onConnected(info: ClockInfo) {
508         val isCurrent = currentClockId == info.metadata.clockId
509         logger.log(
510             if (isCurrent) LogLevel.INFO else LogLevel.DEBUG,
511             { "Connected $str1 @$str2" + if (bool1) " (Current Clock)" else "" },
512         ) {
513             str1 = info.metadata.clockId
514             str2 = info.manager.toString()
515             bool1 = isCurrent
516         }
517     }
518 
onLoadednull519     private fun onLoaded(info: ClockInfo) {
520         val isCurrent = currentClockId == info.metadata.clockId
521         logger.log(
522             if (isCurrent) LogLevel.INFO else LogLevel.DEBUG,
523             { "Loaded $str1 @$str2" + if (bool1) " (Current Clock)" else "" },
524         ) {
525             str1 = info.metadata.clockId
526             str2 = info.manager.toString()
527             bool1 = isCurrent
528         }
529 
530         if (isCurrent) {
531             triggerOnCurrentClockChanged()
532         }
533     }
534 
onUnloadednull535     private fun onUnloaded(info: ClockInfo) {
536         val isCurrent = currentClockId == info.metadata.clockId
537         logger.log(
538             if (isCurrent) LogLevel.WARNING else LogLevel.DEBUG,
539             { "Unloaded $str1 @$str2" + if (bool1) " (Current Clock)" else "" },
540         ) {
541             str1 = info.metadata.clockId
542             str2 = info.manager.toString()
543             bool1 = isCurrent
544         }
545 
546         if (isCurrent) {
547             triggerOnCurrentClockChanged()
548         }
549     }
550 
onDisconnectednull551     private fun onDisconnected(info: ClockInfo) {
552         val isCurrent = currentClockId == info.metadata.clockId
553         logger.log(
554             if (isCurrent) LogLevel.INFO else LogLevel.DEBUG,
555             { "Disconnected $str1 @$str2" + if (bool1) " (Current Clock)" else "" },
556         ) {
557             str1 = info.metadata.clockId
558             str2 = info.manager.toString()
559             bool1 = isCurrent
560         }
561     }
562 
getClocksnull563     fun getClocks(): List<ClockMetadata> {
564         if (!isEnabled) return listOf(availableClocks[DEFAULT_CLOCK_ID]!!.metadata)
565         return availableClocks.map { (_, clock) -> clock.metadata }
566     }
567 
getClockPickerConfignull568     fun getClockPickerConfig(clockId: ClockId): ClockPickerConfig? {
569         val clockSettings =
570             settings?.let { if (clockId == it.clockId) it else null } ?: ClockSettings(clockId)
571         return availableClocks[clockId]?.provider?.getClockPickerConfig(clockSettings)
572     }
573 
createExampleClocknull574     fun createExampleClock(clockId: ClockId): ClockController? = createClock(clockId)
575 
576     /**
577      * Adds [listener] to receive future clock changes.
578      *
579      * Calling from main thread to make sure the access is thread safe.
580      */
581     fun registerClockChangeListener(listener: ClockChangeListener) {
582         assert.isMainThread()
583         clockChangeListeners.add(listener)
584     }
585 
586     /**
587      * Removes [listener] from future clock changes.
588      *
589      * Calling from main thread to make sure the access is thread safe.
590      */
unregisterClockChangeListenernull591     fun unregisterClockChangeListener(listener: ClockChangeListener) {
592         assert.isMainThread()
593         clockChangeListeners.remove(listener)
594     }
595 
createCurrentClocknull596     fun createCurrentClock(): ClockController {
597         val clockId = currentClockId
598         if (isEnabled && clockId.isNotEmpty()) {
599             val clock = createClock(clockId)
600             if (clock != null) {
601                 logger.i({ "Rendering clock $str1" }) { str1 = clockId }
602                 return clock
603             } else if (availableClocks.containsKey(clockId)) {
604                 logger.w({ "Clock $str1 not loaded; using default" }) { str1 = clockId }
605                 verifyLoadedProviders()
606             } else {
607                 logger.e({ "Clock $str1 not found; using default" }) { str1 = clockId }
608             }
609         }
610 
611         return createClock(DEFAULT_CLOCK_ID)!!
612     }
613 
createClocknull614     private fun createClock(targetClockId: ClockId): ClockController? {
615         var settings = this.settings ?: ClockSettings()
616         if (targetClockId != settings.clockId) {
617             settings = settings.copy(clockId = targetClockId)
618         }
619         return availableClocks[targetClockId]?.provider?.createClock(settings)
620     }
621 
dumpnull622     fun dump(pw: PrintWriter, args: Array<out String>) {
623         pw.println("ClockRegistry:")
624         pw.println("  settings = $settings")
625         for ((id, info) in availableClocks) {
626             pw.println("  availableClocks[$id] = $info")
627         }
628     }
629 
630     private data class ClockInfo(
631         val metadata: ClockMetadata,
632         var provider: ClockProvider?,
633         val manager: PluginLifecycleManager<ClockProviderPlugin>?,
634     )
635 }
636