• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * Copyright (C) 2022 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
5  * except in compliance with the License. You may obtain a copy of the License at
6  *
7  *      http://www.apache.org/licenses/LICENSE-2.0
8  *
9  * Unless required by applicable law or agreed to in writing, software distributed under the
10  * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
11  * KIND, either express or implied. See the License for the specific language governing
12  * permissions and limitations under the License.
13  */
14 package com.android.systemui.shared.clocks
15 
16 import android.app.ActivityManager
17 import android.app.UserSwitchObserver
18 import android.content.Context
19 import android.database.ContentObserver
20 import android.net.Uri
21 import android.os.UserHandle
22 import android.provider.Settings
23 import androidx.annotation.OpenForTesting
24 import com.android.systemui.log.LogBuffer
25 import com.android.systemui.log.core.LogLevel
26 import com.android.systemui.log.core.LogcatOnlyMessageBuffer
27 import com.android.systemui.log.core.Logger
28 import com.android.systemui.plugins.PluginLifecycleManager
29 import com.android.systemui.plugins.PluginListener
30 import com.android.systemui.plugins.PluginManager
31 import com.android.systemui.plugins.clocks.ClockController
32 import com.android.systemui.plugins.clocks.ClockId
33 import com.android.systemui.plugins.clocks.ClockMessageBuffers
34 import com.android.systemui.plugins.clocks.ClockMetadata
35 import com.android.systemui.plugins.clocks.ClockPickerConfig
36 import com.android.systemui.plugins.clocks.ClockProvider
37 import com.android.systemui.plugins.clocks.ClockProviderPlugin
38 import com.android.systemui.plugins.clocks.ClockSettings
39 import com.android.systemui.util.ThreadAssert
40 import java.io.PrintWriter
41 import java.util.concurrent.ConcurrentHashMap
42 import java.util.concurrent.atomic.AtomicBoolean
43 import kotlinx.coroutines.CoroutineDispatcher
44 import kotlinx.coroutines.CoroutineScope
45 import kotlinx.coroutines.launch
46 import kotlinx.coroutines.withContext
47 import org.json.JSONObject
48 
49 private val KEY_TIMESTAMP = "appliedTimestamp"
50 private val KNOWN_PLUGINS =
51     mapOf<String, List<ClockMetadata>>(
52         "com.android.systemui.clocks.bignum" to listOf(ClockMetadata("ANALOG_CLOCK_BIGNUM")),
53         "com.android.systemui.clocks.calligraphy" to
54             listOf(ClockMetadata("DIGITAL_CLOCK_CALLIGRAPHY")),
55         "com.android.systemui.clocks.flex" to listOf(ClockMetadata("DIGITAL_CLOCK_FLEX")),
56         "com.android.systemui.clocks.growth" to listOf(ClockMetadata("DIGITAL_CLOCK_GROWTH")),
57         "com.android.systemui.clocks.handwritten" to
58             listOf(ClockMetadata("DIGITAL_CLOCK_HANDWRITTEN")),
59         "com.android.systemui.clocks.inflate" to listOf(ClockMetadata("DIGITAL_CLOCK_INFLATE")),
60         "com.android.systemui.clocks.metro" to listOf(ClockMetadata("DIGITAL_CLOCK_METRO")),
61         "com.android.systemui.clocks.numoverlap" to
62             listOf(ClockMetadata("DIGITAL_CLOCK_NUMBEROVERLAP")),
63         "com.android.systemui.clocks.weather" to listOf(ClockMetadata("DIGITAL_CLOCK_WEATHER")),
64     )
65 
66 private fun <TKey : Any, TVal : Any> ConcurrentHashMap<TKey, TVal>.concurrentGetOrPut(
67     key: TKey,
68     value: TVal,
69     onNew: (TVal) -> Unit,
70 ): TVal {
71     val result = this.putIfAbsent(key, value)
72     if (result == null) {
73         onNew(value)
74     }
75     return result ?: value
76 }
77 
78 /** ClockRegistry aggregates providers and plugins */
79 open class ClockRegistry(
80     val context: Context,
81     val pluginManager: PluginManager,
82     val scope: CoroutineScope,
83     val mainDispatcher: CoroutineDispatcher,
84     val bgDispatcher: CoroutineDispatcher,
85     val isEnabled: Boolean,
86     val handleAllUsers: Boolean,
87     defaultClockProvider: ClockProvider,
88     val fallbackClockId: ClockId = DEFAULT_CLOCK_ID,
89     val clockBuffers: ClockMessageBuffers? = null,
90     val keepAllLoaded: Boolean,
91     subTag: String,
92     val assert: ThreadAssert = ThreadAssert(),
93 ) {
94     private val TAG = "${ClockRegistry::class.simpleName} ($subTag)"
95     private val logger: Logger =
96         Logger(clockBuffers?.infraMessageBuffer ?: LogcatOnlyMessageBuffer(LogLevel.DEBUG), TAG)
97 
98     interface ClockChangeListener {
99         // Called when the active clock changes
onCurrentClockChangednull100         fun onCurrentClockChanged() {}
101 
102         // Called when the list of available clocks changes
onAvailableClocksChangednull103         fun onAvailableClocksChanged() {}
104     }
105 
106     private val replacementMap = ConcurrentHashMap<ClockId, ClockId>()
107     private val availableClocks = ConcurrentHashMap<ClockId, ClockInfo>()
108     private val clockChangeListeners = mutableListOf<ClockChangeListener>()
109     private val settingObserver =
110         object : ContentObserver(null) {
onChangenull111             override fun onChange(
112                 selfChange: Boolean,
113                 uris: Collection<Uri>,
114                 flags: Int,
115                 userId: Int,
116             ) {
117                 scope.launch(bgDispatcher) { querySettings() }
118             }
119         }
120 
121     private val pluginListener =
122         object : PluginListener<ClockProviderPlugin> {
onPluginAttachednull123             override fun onPluginAttached(
124                 manager: PluginLifecycleManager<ClockProviderPlugin>
125             ): Boolean {
126                 manager.setLogFunc({ tag, msg ->
127                     (clockBuffers?.infraMessageBuffer as LogBuffer?)?.log(tag, LogLevel.DEBUG, msg)
128                 })
129                 if (keepAllLoaded) {
130                     // Always load new plugins if requested
131                     return true
132                 }
133 
134                 val knownClocks = KNOWN_PLUGINS.get(manager.getPackage())
135                 if (knownClocks == null) {
136                     logger.w({ "Loading unrecognized clock package: $str1" }) {
137                         str1 = manager.getPackage()
138                     }
139                     return true
140                 }
141 
142                 logger.i({ "Skipping initial load of known clock package package: $str1" }) {
143                     str1 = manager.getPackage()
144                 }
145 
146                 var isCurrentClock = false
147                 var isClockListChanged = false
148                 for (metadata in knownClocks) {
149                     val id = metadata.clockId
150                     val info =
151                         availableClocks.concurrentGetOrPut(id, ClockInfo(metadata, null, manager)) {
152                             isClockListChanged = true
153                             onConnected(it)
154                         }
155 
156                     if (manager != info.manager) {
157                         logger.e({
158                             "Clock Id conflict on attach: " +
159                                 "$str1 is double registered by $str2 and $str3. " +
160                                 "Using $str2 since it was attached first."
161                         }) {
162                             str1 = id
163                             str2 = info.manager?.toString() ?: info.provider?.toString()
164                             str3 = manager.toString()
165                         }
166                         continue
167                     }
168 
169                     isCurrentClock = isCurrentClock || currentClockId == metadata.clockId
170                     info.provider = null
171                 }
172 
173                 if (isClockListChanged) {
174                     triggerOnAvailableClocksChanged()
175                 }
176                 verifyLoadedProviders()
177 
178                 // Load immediately if it's the current clock, otherwise let verifyLoadedProviders
179                 // load and unload clocks as necessary on the background thread.
180                 return isCurrentClock
181             }
182 
onPluginLoadednull183             override fun onPluginLoaded(
184                 plugin: ClockProviderPlugin,
185                 pluginContext: Context,
186                 manager: PluginLifecycleManager<ClockProviderPlugin>,
187             ) {
188                 plugin.initialize(clockBuffers)
189 
190                 var isClockListChanged = false
191                 for (clock in plugin.getClocks()) {
192                     val id = clock.clockId
193                     val info =
194                         availableClocks.concurrentGetOrPut(id, ClockInfo(clock, plugin, manager)) {
195                             isClockListChanged = true
196                             onConnected(it)
197                         }
198 
199                     if (manager != info.manager) {
200                         logger.e({
201                             "Clock Id conflict on load: " +
202                                 "$str1 is double registered by $str2 and $str3. " +
203                                 "Using $str2 since it was attached first."
204                         }) {
205                             str1 = id
206                             str2 = info.manager?.toString() ?: info.provider?.toString()
207                             str3 = manager.toString()
208                         }
209                         manager.unloadPlugin()
210                         continue
211                     }
212 
213                     clock.replacementTarget?.let { replacementMap[id] = it }
214                     info.provider = plugin
215                     onLoaded(info)
216                 }
217 
218                 if (isClockListChanged) {
219                     triggerOnAvailableClocksChanged()
220                 }
221                 verifyLoadedProviders()
222             }
223 
onPluginUnloadednull224             override fun onPluginUnloaded(
225                 plugin: ClockProviderPlugin,
226                 manager: PluginLifecycleManager<ClockProviderPlugin>,
227             ) {
228                 for (clock in plugin.getClocks()) {
229                     val id = clock.clockId
230                     val info = availableClocks[id]
231                     if (info?.manager != manager) {
232                         logger.e({
233                             "Clock Id conflict on unload: " +
234                                 "$str1 is double registered by $str2 and $str3. " +
235                                 "Using $str2 since it was attached first."
236                         }) {
237                             str1 = id
238                             str2 = info?.manager?.toString() ?: info?.provider?.toString()
239                             str3 = manager.toString()
240                         }
241                         continue
242                     }
243                     info.provider = null
244                     onUnloaded(info)
245                 }
246 
247                 verifyLoadedProviders()
248             }
249 
onPluginDetachednull250             override fun onPluginDetached(manager: PluginLifecycleManager<ClockProviderPlugin>) {
251                 val removed = mutableListOf<ClockInfo>()
252                 availableClocks.entries.removeAll {
253                     if (it.value.manager != manager) {
254                         return@removeAll false
255                     }
256 
257                     removed.add(it.value)
258                     return@removeAll true
259                 }
260 
261                 removed.forEach(::onDisconnected)
262                 if (removed.size > 0) {
263                     triggerOnAvailableClocksChanged()
264                 }
265             }
266         }
267 
268     private val userSwitchObserver =
269         object : UserSwitchObserver() {
onUserSwitchCompletenull270             override fun onUserSwitchComplete(newUserId: Int) {
271                 scope.launch(bgDispatcher) { querySettings() }
272             }
273         }
274 
275     // TODO(b/267372164): Migrate to flows
276     var settings: ClockSettings? = null
277         get() = field
278         protected set(value) {
279             if (field != value) {
280                 field = value
281                 verifyLoadedProviders()
282                 triggerOnCurrentClockChanged()
283             }
284         }
285 
286     var isRegistered: Boolean = false
287         private set
288 
289     @OpenForTesting
querySettingsnull290     open fun querySettings() {
291         assert.isNotMainThread()
292         val result =
293             try {
294                 val json =
295                     if (handleAllUsers) {
296                         Settings.Secure.getStringForUser(
297                             context.contentResolver,
298                             Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE,
299                             ActivityManager.getCurrentUser(),
300                         )
301                     } else {
302                         Settings.Secure.getString(
303                             context.contentResolver,
304                             Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE,
305                         )
306                     }
307                 json?.let { ClockSettings.fromJson(JSONObject(it)) }
308             } catch (ex: Exception) {
309                 logger.e("Failed to parse clock settings", ex)
310                 null
311             }
312         settings = result
313     }
314 
315     @OpenForTesting
applySettingsnull316     open fun applySettings(value: ClockSettings?) {
317         assert.isNotMainThread()
318 
319         try {
320             val json =
321                 value?.let {
322                     it.metadata.put(KEY_TIMESTAMP, System.currentTimeMillis())
323                     ClockSettings.toJson(it)
324                 } ?: ""
325 
326             if (handleAllUsers) {
327                 Settings.Secure.putStringForUser(
328                     context.contentResolver,
329                     Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE,
330                     json.toString(),
331                     ActivityManager.getCurrentUser(),
332                 )
333             } else {
334                 Settings.Secure.putString(
335                     context.contentResolver,
336                     Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE,
337                     json.toString(),
338                 )
339             }
340         } catch (ex: Exception) {
341             logger.e("Failed to set clock settings", ex)
342         }
343         settings = value
344     }
345 
346     private var isClockChanged = AtomicBoolean(false)
347 
triggerOnCurrentClockChangednull348     private fun triggerOnCurrentClockChanged() {
349         val shouldSchedule = isClockChanged.compareAndSet(false, true)
350         if (!shouldSchedule) {
351             return
352         }
353 
354         scope.launch(mainDispatcher) {
355             assert.isMainThread()
356             isClockChanged.set(false)
357             clockChangeListeners.forEach { it.onCurrentClockChanged() }
358         }
359     }
360 
361     private var isClockListChanged = AtomicBoolean(false)
362 
triggerOnAvailableClocksChangednull363     private fun triggerOnAvailableClocksChanged() {
364         val shouldSchedule = isClockListChanged.compareAndSet(false, true)
365         if (!shouldSchedule) {
366             return
367         }
368 
369         scope.launch(mainDispatcher) {
370             assert.isMainThread()
371             isClockListChanged.set(false)
372             clockChangeListeners.forEach { it.onAvailableClocksChanged() }
373         }
374     }
375 
mutateSettingnull376     public suspend fun mutateSetting(mutator: (ClockSettings) -> ClockSettings) {
377         withContext(bgDispatcher) { applySettings(mutator(settings ?: ClockSettings())) }
378     }
379 
380     var currentClockId: ClockId
381         get() = settings?.clockId ?: fallbackClockId
382         set(value) {
<lambda>null383             scope.launch(bgDispatcher) { mutateSetting { it.copy(clockId = value) } }
384         }
385 
386     var seedColor: Int?
387         get() = settings?.seedColor
388         set(value) {
<lambda>null389             scope.launch(bgDispatcher) { mutateSetting { it.copy(seedColor = value) } }
390         }
391 
392     // Returns currentClockId if clock is connected, otherwise DEFAULT_CLOCK_ID. Since this
393     // is dependent on which clocks are connected, it may change when a clock is installed or
394     // removed from the device (unlike currentClockId).
395     // TODO: Merge w/ CurrentClockId when we convert to a flow. We shouldn't need both behaviors.
396     val activeClockId: String
397         get() {
398             var id = currentClockId
399             if (!availableClocks.containsKey(id)) {
400                 return DEFAULT_CLOCK_ID
401             }
402             return replacementMap[id] ?: id
403         }
404 
405     init {
406         // Initialize & register default clock designs
407         defaultClockProvider.initialize(clockBuffers)
408         for (clock in defaultClockProvider.getClocks()) {
409             availableClocks[clock.clockId] = ClockInfo(clock, defaultClockProvider, null)
<lambda>null410             clock.replacementTarget?.let { replacementMap[clock.clockId] = it }
411         }
412 
413         // Something has gone terribly wrong if the default clock isn't present
414         if (!availableClocks.containsKey(DEFAULT_CLOCK_ID)) {
415             throw IllegalArgumentException(
416                 "$defaultClockProvider did not register clock at $DEFAULT_CLOCK_ID"
417             )
418         }
419     }
420 
registerListenersnull421     fun registerListeners() {
422         if (!isEnabled || isRegistered) {
423             return
424         }
425 
426         isRegistered = true
427 
428         pluginManager.addPluginListener(
429             pluginListener,
430             ClockProviderPlugin::class.java,
431             /*allowMultiple=*/ true,
432         )
433 
434         scope.launch(bgDispatcher) { querySettings() }
435         if (handleAllUsers) {
436             context.contentResolver.registerContentObserver(
437                 Settings.Secure.getUriFor(Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE),
438                 /*notifyForDescendants=*/ false,
439                 settingObserver,
440                 UserHandle.USER_ALL,
441             )
442 
443             ActivityManager.getService().registerUserSwitchObserver(userSwitchObserver, TAG)
444         } else {
445             context.contentResolver.registerContentObserver(
446                 Settings.Secure.getUriFor(Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE),
447                 /*notifyForDescendants=*/ false,
448                 settingObserver,
449             )
450         }
451     }
452 
unregisterListenersnull453     fun unregisterListeners() {
454         if (!isRegistered) {
455             return
456         }
457 
458         isRegistered = false
459 
460         pluginManager.removePluginListener(pluginListener)
461         context.contentResolver.unregisterContentObserver(settingObserver)
462         if (handleAllUsers) {
463             ActivityManager.getService().unregisterUserSwitchObserver(userSwitchObserver)
464         }
465     }
466 
467     private var isQueued = AtomicBoolean(false)
468 
verifyLoadedProvidersnull469     fun verifyLoadedProviders() {
470         val shouldSchedule = isQueued.compareAndSet(false, true)
471         if (!shouldSchedule) {
472             logger.v("verifyLoadedProviders: shouldSchedule=false")
473             return
474         }
475 
476         scope.launch(bgDispatcher) {
477             // TODO(b/267372164): Use better threading approach when converting to flows
478             synchronized(availableClocks) {
479                 isQueued.set(false)
480                 if (keepAllLoaded) {
481                     logger.i("verifyLoadedProviders: keepAllLoaded=true")
482                     // Enforce that all plugins are loaded if requested
483                     for ((_, info) in availableClocks) {
484                         info.manager?.loadPlugin()
485                     }
486                     return@launch
487                 }
488 
489                 val currentClock = availableClocks[currentClockId]
490                 if (currentClock == null) {
491                     logger.i("verifyLoadedProviders: currentClock=null")
492                     // Current Clock missing, load no plugins and use default
493                     for ((_, info) in availableClocks) {
494                         info.manager?.unloadPlugin()
495                     }
496                     return@launch
497                 }
498 
499                 logger.i("verifyLoadedProviders: load currentClock")
500                 val currentManager = currentClock.manager
501                 currentManager?.loadPlugin()
502 
503                 for ((_, info) in availableClocks) {
504                     val manager = info.manager
505                     if (manager != null && currentManager != manager) {
506                         manager.unloadPlugin()
507                     }
508                 }
509             }
510         }
511     }
512 
onConnectednull513     private fun onConnected(info: ClockInfo) {
514         val isCurrent = currentClockId == info.metadata.clockId
515         logger.log(
516             if (isCurrent) LogLevel.INFO else LogLevel.DEBUG,
517             { "Connected $str1 @$str2" + if (bool1) " (Current Clock)" else "" },
518         ) {
519             str1 = info.metadata.clockId
520             str2 = info.manager.toString()
521             bool1 = isCurrent
522         }
523     }
524 
onLoadednull525     private fun onLoaded(info: ClockInfo) {
526         val isCurrent = currentClockId == info.metadata.clockId
527         logger.log(
528             if (isCurrent) LogLevel.INFO else LogLevel.DEBUG,
529             { "Loaded $str1 @$str2" + if (bool1) " (Current Clock)" else "" },
530         ) {
531             str1 = info.metadata.clockId
532             str2 = info.manager.toString()
533             bool1 = isCurrent
534         }
535 
536         if (isCurrent) {
537             triggerOnCurrentClockChanged()
538         }
539     }
540 
onUnloadednull541     private fun onUnloaded(info: ClockInfo) {
542         val isCurrent = currentClockId == info.metadata.clockId
543         logger.log(
544             if (isCurrent) LogLevel.WARNING else LogLevel.DEBUG,
545             { "Unloaded $str1 @$str2" + if (bool1) " (Current Clock)" else "" },
546         ) {
547             str1 = info.metadata.clockId
548             str2 = info.manager.toString()
549             bool1 = isCurrent
550         }
551 
552         if (isCurrent) {
553             triggerOnCurrentClockChanged()
554         }
555     }
556 
onDisconnectednull557     private fun onDisconnected(info: ClockInfo) {
558         val isCurrent = currentClockId == info.metadata.clockId
559         logger.log(
560             if (isCurrent) LogLevel.INFO else LogLevel.DEBUG,
561             { "Disconnected $str1 @$str2" + if (bool1) " (Current Clock)" else "" },
562         ) {
563             str1 = info.metadata.clockId
564             str2 = info.manager.toString()
565             bool1 = isCurrent
566         }
567     }
568 
getClocksnull569     fun getClocks(includeDeprecated: Boolean = false): List<ClockMetadata> {
570         return when {
571             !isEnabled -> listOf(availableClocks[DEFAULT_CLOCK_ID]!!.metadata)
572             includeDeprecated -> availableClocks.map { (_, clock) -> clock.metadata }
573             else -> availableClocks.map { (_, clock) -> clock.metadata }.filter { !it.isDeprecated }
574         }
575     }
576 
getClockPickerConfignull577     fun getClockPickerConfig(clockId: ClockId): ClockPickerConfig? {
578         val clockSettings =
579             settings?.let { if (clockId == it.clockId) it else null } ?: ClockSettings(clockId)
580         return availableClocks[clockId]?.provider?.getClockPickerConfig(clockSettings)
581     }
582 
createExampleClocknull583     fun createExampleClock(clockId: ClockId): ClockController? = createClock(clockId)
584 
585     /**
586      * Adds [listener] to receive future clock changes.
587      *
588      * Calling from main thread to make sure the access is thread safe.
589      */
590     fun registerClockChangeListener(listener: ClockChangeListener) {
591         assert.isMainThread()
592         clockChangeListeners.add(listener)
593     }
594 
595     /**
596      * Removes [listener] from future clock changes.
597      *
598      * Calling from main thread to make sure the access is thread safe.
599      */
unregisterClockChangeListenernull600     fun unregisterClockChangeListener(listener: ClockChangeListener) {
601         assert.isMainThread()
602         clockChangeListeners.remove(listener)
603     }
604 
createCurrentClocknull605     fun createCurrentClock(): ClockController {
606         val clockId = currentClockId
607         if (isEnabled && clockId.isNotEmpty()) {
608             val clock = createClock(clockId)
609             if (clock != null) {
610                 logger.i({ "Rendering clock $str1" }) { str1 = clockId }
611                 return clock
612             } else if (availableClocks.containsKey(clockId)) {
613                 logger.w({ "Clock $str1 not loaded; using default" }) { str1 = clockId }
614                 verifyLoadedProviders()
615             } else {
616                 logger.e({ "Clock $str1 not found; using default" }) { str1 = clockId }
617             }
618         }
619 
620         return createClock(DEFAULT_CLOCK_ID)!!
621     }
622 
createClocknull623     private fun createClock(targetClockId: ClockId): ClockController? {
624         var settings = this.settings ?: ClockSettings()
625         if (targetClockId != settings.clockId) {
626             settings = settings.copy(clockId = targetClockId)
627         }
628         return availableClocks[targetClockId]?.provider?.createClock(settings)
629     }
630 
dumpnull631     fun dump(pw: PrintWriter, args: Array<out String>) {
632         pw.println("ClockRegistry:")
633         pw.println("  settings = $settings")
634         for ((id, info) in availableClocks) {
635             pw.println("  availableClocks[$id] = $info")
636         }
637     }
638 
639     private data class ClockInfo(
640         val metadata: ClockMetadata,
641         var provider: ClockProvider?,
642         val manager: PluginLifecycleManager<ClockProviderPlugin>?,
643     )
644 }
645