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