xref: /aosp_15_r20/frameworks/base/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt (revision d57664e9bc4670b3ecf6748a746a57c557b6bc9e)
1 /*
<lambda>null2  * Copyright (C) 2022 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.android.keyguard
17 
18 import android.app.NotificationManager.zenModeFromInterruptionFilter
19 import android.content.BroadcastReceiver
20 import android.content.Context
21 import android.content.Intent
22 import android.content.IntentFilter
23 import android.content.res.Resources
24 import android.os.Trace
25 import android.provider.Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS
26 import android.provider.Settings.Global.ZEN_MODE_OFF
27 import android.text.format.DateFormat
28 import android.util.Log
29 import android.util.TypedValue
30 import android.view.View
31 import android.view.View.OnAttachStateChangeListener
32 import android.view.ViewGroup
33 import android.view.ViewTreeObserver
34 import android.view.ViewTreeObserver.OnGlobalLayoutListener
35 import androidx.annotation.VisibleForTesting
36 import androidx.lifecycle.Lifecycle
37 import androidx.lifecycle.repeatOnLifecycle
38 import com.android.app.tracing.coroutines.launchTraced as launch
39 import com.android.systemui.broadcast.BroadcastDispatcher
40 import com.android.systemui.customization.R
41 import com.android.systemui.dagger.qualifiers.Background
42 import com.android.systemui.dagger.qualifiers.DisplaySpecific
43 import com.android.systemui.dagger.qualifiers.Main
44 import com.android.systemui.flags.FeatureFlagsClassic
45 import com.android.systemui.flags.Flags.REGION_SAMPLING
46 import com.android.systemui.keyguard.MigrateClocksToBlueprint
47 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
48 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
49 import com.android.systemui.keyguard.shared.model.Edge
50 import com.android.systemui.keyguard.shared.model.KeyguardState.AOD
51 import com.android.systemui.keyguard.shared.model.KeyguardState.DOZING
52 import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN
53 import com.android.systemui.keyguard.shared.model.TransitionState
54 import com.android.systemui.lifecycle.repeatWhenAttached
55 import com.android.systemui.log.core.Logger
56 import com.android.systemui.modes.shared.ModesUi
57 import com.android.systemui.plugins.clocks.AlarmData
58 import com.android.systemui.plugins.clocks.ClockController
59 import com.android.systemui.plugins.clocks.ClockFaceController
60 import com.android.systemui.plugins.clocks.ClockMessageBuffers
61 import com.android.systemui.plugins.clocks.ClockTickRate
62 import com.android.systemui.plugins.clocks.WeatherData
63 import com.android.systemui.plugins.clocks.ZenData
64 import com.android.systemui.plugins.clocks.ZenData.ZenMode
65 import com.android.systemui.res.R as SysuiR
66 import com.android.systemui.scene.shared.flag.SceneContainerFlag
67 import com.android.systemui.settings.UserTracker
68 import com.android.systemui.shared.regionsampling.RegionSampler
69 import com.android.systemui.statusbar.policy.BatteryController
70 import com.android.systemui.statusbar.policy.BatteryController.BatteryStateChangeCallback
71 import com.android.systemui.statusbar.policy.ConfigurationController
72 import com.android.systemui.statusbar.policy.ZenModeController
73 import com.android.systemui.statusbar.policy.domain.interactor.ZenModeInteractor
74 import com.android.systemui.util.concurrency.DelayableExecutor
75 import java.util.Locale
76 import java.util.TimeZone
77 import java.util.concurrent.Executor
78 import javax.inject.Inject
79 import kotlinx.coroutines.CoroutineScope
80 import kotlinx.coroutines.DisposableHandle
81 import kotlinx.coroutines.Job
82 import kotlinx.coroutines.flow.combine
83 import kotlinx.coroutines.flow.filter
84 import kotlinx.coroutines.flow.map
85 import kotlinx.coroutines.flow.merge
86 
87 /**
88  * Controller for a Clock provided by the registry and used on the keyguard. Instantiated by
89  * [KeyguardClockSwitchController]. Functionality is forked from [AnimatableClockController].
90  */
91 open class ClockEventController
92 @Inject
93 constructor(
94     private val keyguardInteractor: KeyguardInteractor,
95     private val keyguardTransitionInteractor: KeyguardTransitionInteractor,
96     private val broadcastDispatcher: BroadcastDispatcher,
97     private val batteryController: BatteryController,
98     private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
99     // TODO b/362719719 - We should use the configuration controller associated with the display.
100     private val configurationController: ConfigurationController,
101     @DisplaySpecific private val resources: Resources,
102     @DisplaySpecific val context: Context,
103     @Main private val mainExecutor: DelayableExecutor,
104     @Background private val bgExecutor: Executor,
105     private val clockBuffers: ClockMessageBuffers,
106     private val featureFlags: FeatureFlagsClassic,
107     private val zenModeController: ZenModeController,
108     private val zenModeInteractor: ZenModeInteractor,
109     private val userTracker: UserTracker,
110 ) {
111     var loggers =
112         listOf(
113                 clockBuffers.infraMessageBuffer,
114                 clockBuffers.smallClockMessageBuffer,
115                 clockBuffers.largeClockMessageBuffer,
116             )
117             .map { Logger(it, TAG) }
118 
119     var clock: ClockController? = null
120         get() = field
121         set(value) {
122             disconnectClock(field)
123             field = value
124             connectClock(value)
125         }
126 
127     private fun is24HourFormat(userId: Int? = null): Boolean {
128         return DateFormat.is24HourFormat(context, userId ?: userTracker.userId)
129     }
130 
131     private fun disconnectClock(clock: ClockController?) {
132         if (clock == null) {
133             return
134         }
135         smallClockOnAttachStateChangeListener?.let {
136             clock.smallClock.view.removeOnAttachStateChangeListener(it)
137             smallClockFrame?.viewTreeObserver?.removeOnGlobalLayoutListener(onGlobalLayoutListener)
138         }
139         largeClockOnAttachStateChangeListener?.let {
140             clock.largeClock.view.removeOnAttachStateChangeListener(it)
141         }
142     }
143 
144     private fun connectClock(clock: ClockController?) {
145         if (clock == null) {
146             return
147         }
148         val clockStr = clock.toString()
149         loggers.forEach { it.d({ "New Clock: $str1" }) { str1 = clockStr } }
150 
151         clock.initialize(isDarkTheme(), dozeAmount, 0f)
152 
153         if (!regionSamplingEnabled) {
154             updateColors()
155         } else {
156             smallRegionSampler =
157                 createRegionSampler(
158                         clock.smallClock.view,
159                         mainExecutor,
160                         bgExecutor,
161                         regionSamplingEnabled,
162                         isLockscreen = true,
163                         ::updateColors,
164                     )
165                     .apply { startRegionSampler() }
166 
167             largeRegionSampler =
168                 createRegionSampler(
169                         clock.largeClock.view,
170                         mainExecutor,
171                         bgExecutor,
172                         regionSamplingEnabled,
173                         isLockscreen = true,
174                         ::updateColors,
175                     )
176                     .apply { startRegionSampler() }
177 
178             updateColors()
179         }
180         updateFontSizes()
181         updateTimeListeners()
182 
183         weatherData?.let {
184             if (WeatherData.DEBUG) {
185                 Log.i(TAG, "Pushing cached weather data to new clock: $it")
186             }
187             clock.events.onWeatherDataChanged(it)
188         }
189         zenData?.let { clock.events.onZenDataChanged(it) }
190         alarmData?.let { clock.events.onAlarmDataChanged(it) }
191 
192         smallClockOnAttachStateChangeListener =
193             object : OnAttachStateChangeListener {
194                 var pastVisibility: Int? = null
195 
196                 override fun onViewAttachedToWindow(view: View) {
197                     clock.events.onTimeFormatChanged(is24HourFormat())
198                     // Match the asing for view.parent's layout classes.
199                     smallClockFrame =
200                         (view.parent as ViewGroup)?.also { frame ->
201                             pastVisibility = frame.visibility
202                             onGlobalLayoutListener = OnGlobalLayoutListener {
203                                 val currentVisibility = frame.visibility
204                                 if (pastVisibility != currentVisibility) {
205                                     pastVisibility = currentVisibility
206                                     // when small clock is visible,
207                                     // recalculate bounds and sample
208                                     if (currentVisibility == View.VISIBLE) {
209                                         smallRegionSampler?.stopRegionSampler()
210                                         smallRegionSampler?.startRegionSampler()
211                                     }
212                                 }
213                             }
214                             frame.viewTreeObserver.addOnGlobalLayoutListener(onGlobalLayoutListener)
215                         }
216                 }
217 
218                 override fun onViewDetachedFromWindow(p0: View) {
219                     smallClockFrame
220                         ?.viewTreeObserver
221                         ?.removeOnGlobalLayoutListener(onGlobalLayoutListener)
222                 }
223             }
224         clock.smallClock.view.addOnAttachStateChangeListener(smallClockOnAttachStateChangeListener)
225 
226         largeClockOnAttachStateChangeListener =
227             object : OnAttachStateChangeListener {
228                 override fun onViewAttachedToWindow(p0: View) {
229                     clock.events.onTimeFormatChanged(is24HourFormat())
230                 }
231 
232                 override fun onViewDetachedFromWindow(p0: View) {}
233             }
234         clock.largeClock.view.addOnAttachStateChangeListener(largeClockOnAttachStateChangeListener)
235     }
236 
237     @VisibleForTesting
238     var smallClockOnAttachStateChangeListener: OnAttachStateChangeListener? = null
239     @VisibleForTesting
240     var largeClockOnAttachStateChangeListener: OnAttachStateChangeListener? = null
241     private var smallClockFrame: ViewGroup? = null
242     private var onGlobalLayoutListener: OnGlobalLayoutListener? = null
243 
244     private var isDozing = false
245         private set
246 
247     private var isCharging = false
248     private var dozeAmount = 0f
249     private var isKeyguardVisible = false
250     private var isRegistered = false
251     private var disposableHandle: DisposableHandle? = null
252     private val regionSamplingEnabled = featureFlags.isEnabled(REGION_SAMPLING)
253     private var largeClockOnSecondaryDisplay = false
254 
255     private fun isDarkTheme(): Boolean {
256         val isLightTheme = TypedValue()
257         context.theme.resolveAttribute(android.R.attr.isLightTheme, isLightTheme, true)
258         return isLightTheme.data == 0
259     }
260 
261     private fun updateColors() {
262         val isDarkTheme = isDarkTheme()
263         if (regionSamplingEnabled) {
264             clock?.smallClock?.run {
265                 val isDark = smallRegionSampler?.currentRegionDarkness()?.isDark ?: isDarkTheme
266                 events.onThemeChanged(theme.copy(isDarkTheme = isDark))
267             }
268             clock?.largeClock?.run {
269                 val isDark = largeRegionSampler?.currentRegionDarkness()?.isDark ?: isDarkTheme
270                 events.onThemeChanged(theme.copy(isDarkTheme = isDark))
271             }
272             return
273         }
274 
275         clock?.run {
276             Log.i(TAG, "isThemeDark: $isDarkTheme")
277             smallClock.events.onThemeChanged(smallClock.theme.copy(isDarkTheme = isDarkTheme))
278             largeClock.events.onThemeChanged(largeClock.theme.copy(isDarkTheme = isDarkTheme))
279         }
280     }
281 
282     protected open fun createRegionSampler(
283         sampledView: View,
284         mainExecutor: Executor?,
285         bgExecutor: Executor?,
286         regionSamplingEnabled: Boolean,
287         isLockscreen: Boolean,
288         updateColors: () -> Unit,
289     ): RegionSampler {
290         return RegionSampler(
291             sampledView,
292             mainExecutor,
293             bgExecutor,
294             regionSamplingEnabled,
295             isLockscreen,
296         ) {
297             updateColors()
298         }
299     }
300 
301     var smallRegionSampler: RegionSampler? = null
302         private set
303 
304     var largeRegionSampler: RegionSampler? = null
305         private set
306 
307     var smallTimeListener: TimeListener? = null
308     var largeTimeListener: TimeListener? = null
309     val shouldTimeListenerRun: Boolean
310         get() = isKeyguardVisible && dozeAmount < DOZE_TICKRATE_THRESHOLD
311 
312     private var weatherData: WeatherData? = null
313     private var zenData: ZenData? = null
314     private var alarmData: AlarmData? = null
315 
316     private val configListener =
317         object : ConfigurationController.ConfigurationListener {
318             override fun onThemeChanged() {
319                 updateColors()
320             }
321 
322             override fun onDensityOrFontScaleChanged() {
323                 updateFontSizes()
324             }
325         }
326 
327     private val batteryCallback =
328         object : BatteryStateChangeCallback {
329             override fun onBatteryLevelChanged(level: Int, pluggedIn: Boolean, charging: Boolean) {
330                 if (isKeyguardVisible && !isCharging && charging) {
331                     clock?.run {
332                         smallClock.animations.charge()
333                         largeClock.animations.charge()
334                     }
335                 }
336                 isCharging = charging
337             }
338         }
339 
340     private val localeBroadcastReceiver =
341         object : BroadcastReceiver() {
342             override fun onReceive(context: Context, intent: Intent) {
343                 clock?.run { events.onLocaleChanged(Locale.getDefault()) }
344             }
345         }
346 
347     private val keyguardUpdateMonitorCallback =
348         object : KeyguardUpdateMonitorCallback() {
349             override fun onKeyguardVisibilityChanged(visible: Boolean) {
350                 isKeyguardVisible = visible
351                 if (!MigrateClocksToBlueprint.isEnabled) {
352                     if (!isKeyguardVisible) {
353                         clock?.run {
354                             smallClock.animations.doze(if (isDozing) 1f else 0f)
355                             largeClock.animations.doze(if (isDozing) 1f else 0f)
356                         }
357                     }
358                 }
359 
360                 if (visible) {
361                     refreshTime()
362                 }
363 
364                 smallTimeListener?.update(shouldTimeListenerRun)
365                 largeTimeListener?.update(shouldTimeListenerRun)
366             }
367 
368             override fun onTimeFormatChanged(timeFormat: String?) {
369                 clock?.run { events.onTimeFormatChanged(is24HourFormat()) }
370             }
371 
372             override fun onTimeZoneChanged(timeZone: TimeZone) {
373                 clock?.run { events.onTimeZoneChanged(timeZone) }
374             }
375 
376             override fun onUserSwitchComplete(userId: Int) {
377                 clock?.run { events.onTimeFormatChanged(is24HourFormat(userId)) }
378                 zenModeCallback.onNextAlarmChanged()
379             }
380 
381             override fun onWeatherDataChanged(data: WeatherData) {
382                 weatherData = data
383                 clock?.run { events.onWeatherDataChanged(data) }
384             }
385 
386             override fun onTimeChanged() {
387                 refreshTime()
388             }
389 
390             private fun refreshTime() {
391                 if (!MigrateClocksToBlueprint.isEnabled) {
392                     return
393                 }
394 
395                 clock?.smallClock?.events?.onTimeTick()
396                 clock?.largeClock?.events?.onTimeTick()
397             }
398         }
399 
400     @VisibleForTesting
401     internal fun listenForDnd(scope: CoroutineScope): Job {
402         ModesUi.assertInNewMode()
403         return scope.launch {
404             zenModeInteractor.dndMode.collect {
405                 val zenMode =
406                     if (it != null && it.isActive)
407                         zenModeFromInterruptionFilter(
408                             it.interruptionFilter,
409                             ZEN_MODE_IMPORTANT_INTERRUPTIONS,
410                         )
411                     else ZEN_MODE_OFF
412 
413                 handleZenMode(zenMode)
414             }
415         }
416     }
417 
418     private val zenModeCallback =
419         object : ZenModeController.Callback {
420             override fun onZenChanged(zen: Int) {
421                 if (!ModesUi.isEnabled) {
422                     handleZenMode(zen)
423                 }
424             }
425 
426             override fun onNextAlarmChanged() {
427                 val nextAlarmMillis = zenModeController.getNextAlarm()
428                 alarmData =
429                     AlarmData(
430                             if (nextAlarmMillis > 0) nextAlarmMillis else null,
431                             SysuiR.string::status_bar_alarm.name,
432                         )
433                         .also { data ->
434                             mainExecutor.execute { clock?.run { events.onAlarmDataChanged(data) } }
435                         }
436             }
437         }
438 
439     private fun handleZenMode(zen: Int) {
440         val mode = ZenMode.fromInt(zen)
441         if (mode == null) {
442             Log.e(TAG, "Failed to get zen mode from int: $zen")
443             return
444         }
445 
446         zenData =
447             ZenData(
448                     mode,
449                     if (mode == ZenMode.OFF) SysuiR.string::dnd_is_off.name
450                     else SysuiR.string::dnd_is_on.name,
451                 )
452                 .also { data ->
453                     mainExecutor.execute { clock?.run { events.onZenDataChanged(data) } }
454                 }
455     }
456 
457     fun registerListeners(parent: View) {
458         if (isRegistered) {
459             return
460         }
461         isRegistered = true
462         broadcastDispatcher.registerReceiver(
463             localeBroadcastReceiver,
464             IntentFilter(Intent.ACTION_LOCALE_CHANGED),
465         )
466         configurationController.addCallback(configListener)
467         batteryController.addCallback(batteryCallback)
468         keyguardUpdateMonitor.registerCallback(keyguardUpdateMonitorCallback)
469         zenModeController.addCallback(zenModeCallback)
470         if (SceneContainerFlag.isEnabled) {
471             handleDoze(
472                 when (AOD) {
473                     keyguardTransitionInteractor.getCurrentState() -> 1f
474                     keyguardTransitionInteractor.getStartedState() -> 1f
475                     else -> 0f
476                 }
477             )
478         }
479         disposableHandle =
480             parent.repeatWhenAttached {
481                 repeatOnLifecycle(Lifecycle.State.CREATED) {
482                     listenForDozing(this)
483                     if (ModesUi.isEnabled) {
484                         listenForDnd(this)
485                     }
486                     if (MigrateClocksToBlueprint.isEnabled) {
487                         listenForDozeAmountTransition(this)
488                         listenForAnyStateToAodTransition(this)
489                         listenForAnyStateToLockscreenTransition(this)
490                         listenForAnyStateToDozingTransition(this)
491                     } else {
492                         listenForDozeAmount(this)
493                     }
494                 }
495             }
496         smallTimeListener?.update(shouldTimeListenerRun)
497         largeTimeListener?.update(shouldTimeListenerRun)
498 
499         bgExecutor.execute {
500             // Query ZenMode data
501             if (!ModesUi.isEnabled) {
502                 zenModeCallback.onZenChanged(zenModeController.zen)
503             }
504             zenModeCallback.onNextAlarmChanged()
505         }
506     }
507 
508     fun unregisterListeners() {
509         if (!isRegistered) {
510             return
511         }
512         isRegistered = false
513 
514         disposableHandle?.dispose()
515         broadcastDispatcher.unregisterReceiver(localeBroadcastReceiver)
516         configurationController.removeCallback(configListener)
517         batteryController.removeCallback(batteryCallback)
518         keyguardUpdateMonitor.removeCallback(keyguardUpdateMonitorCallback)
519         zenModeController.removeCallback(zenModeCallback)
520         smallRegionSampler?.stopRegionSampler()
521         largeRegionSampler?.stopRegionSampler()
522         smallTimeListener?.stop()
523         largeTimeListener?.stop()
524         clock?.run {
525             smallClock.view.removeOnAttachStateChangeListener(smallClockOnAttachStateChangeListener)
526             largeClock.view.removeOnAttachStateChangeListener(largeClockOnAttachStateChangeListener)
527         }
528         smallClockFrame?.viewTreeObserver?.removeOnGlobalLayoutListener(onGlobalLayoutListener)
529     }
530 
531     fun setFallbackWeatherData(data: WeatherData) {
532         if (weatherData != null) return
533         weatherData = data
534         clock?.run { events.onWeatherDataChanged(data) }
535     }
536 
537     /**
538      * Sets this clock as showing in a secondary display.
539      *
540      * Not that this is not necessarily needed, as we could get the displayId from [Context]
541      * directly and infere [largeClockOnSecondaryDisplay] from the id being different than the
542      * default display one. However, if we do so, current screenshot tests would not work, as they
543      * pass an activity context always from the default display.
544      */
545     fun setLargeClockOnSecondaryDisplay(onSecondaryDisplay: Boolean) {
546         largeClockOnSecondaryDisplay = onSecondaryDisplay
547         updateFontSizes()
548     }
549 
550     private fun updateTimeListeners() {
551         smallTimeListener?.stop()
552         largeTimeListener?.stop()
553 
554         smallTimeListener = null
555         largeTimeListener = null
556 
557         clock?.let {
558             smallTimeListener =
559                 TimeListener(it.smallClock, mainExecutor).apply { update(shouldTimeListenerRun) }
560             largeTimeListener =
561                 TimeListener(it.largeClock, mainExecutor).apply { update(shouldTimeListenerRun) }
562         }
563     }
564 
565     fun updateFontSizes() {
566         clock?.run {
567             smallClock.events.onFontSettingChanged(getSmallClockSizePx())
568             largeClock.events.onFontSettingChanged(getLargeClockSizePx())
569         }
570     }
571 
572     private fun getSmallClockSizePx(): Float {
573         return resources.getDimensionPixelSize(R.dimen.small_clock_text_size).toFloat()
574     }
575 
576     private fun getLargeClockSizePx(): Float {
577         return if (largeClockOnSecondaryDisplay) {
578             resources.getDimensionPixelSize(R.dimen.presentation_clock_text_size).toFloat()
579         } else {
580             resources.getDimensionPixelSize(R.dimen.large_clock_text_size).toFloat()
581         }
582     }
583 
584     private fun handleDoze(doze: Float) {
585         dozeAmount = doze
586         clock?.run {
587             Trace.beginSection("$TAG#smallClock.animations.doze")
588             smallClock.animations.doze(dozeAmount)
589             Trace.endSection()
590             Trace.beginSection("$TAG#largeClock.animations.doze")
591             largeClock.animations.doze(dozeAmount)
592             Trace.endSection()
593         }
594         smallTimeListener?.update(doze < DOZE_TICKRATE_THRESHOLD)
595         largeTimeListener?.update(doze < DOZE_TICKRATE_THRESHOLD)
596     }
597 
598     @VisibleForTesting
599     internal fun listenForDozeAmount(scope: CoroutineScope): Job {
600         return scope.launch { keyguardInteractor.dozeAmount.collect { handleDoze(it) } }
601     }
602 
603     @VisibleForTesting
604     internal fun listenForDozeAmountTransition(scope: CoroutineScope): Job {
605         return scope.launch {
606             merge(
607                     keyguardTransitionInteractor.transition(Edge.create(AOD, LOCKSCREEN)).map {
608                         it.copy(value = 1f - it.value)
609                     },
610                     keyguardTransitionInteractor.transition(Edge.create(LOCKSCREEN, AOD)),
611                 )
612                 .filter { it.transitionState != TransitionState.FINISHED }
613                 .collect { handleDoze(it.value) }
614         }
615     }
616 
617     /**
618      * When keyguard is displayed again after being gone, the clock must be reset to full dozing.
619      */
620     @VisibleForTesting
621     internal fun listenForAnyStateToAodTransition(scope: CoroutineScope): Job {
622         return scope.launch {
623             keyguardTransitionInteractor
624                 .transition(Edge.create(to = AOD))
625                 .filter { it.transitionState == TransitionState.STARTED }
626                 .filter { it.from != LOCKSCREEN }
627                 .collect { handleDoze(1f) }
628         }
629     }
630 
631     @VisibleForTesting
632     internal fun listenForAnyStateToLockscreenTransition(scope: CoroutineScope): Job {
633         return scope.launch {
634             keyguardTransitionInteractor
635                 .transition(Edge.create(to = LOCKSCREEN))
636                 .filter { it.transitionState == TransitionState.STARTED }
637                 .filter { it.from != AOD }
638                 .collect { handleDoze(0f) }
639         }
640     }
641 
642     /**
643      * When keyguard is displayed due to pulsing notifications when AOD is off, we should make sure
644      * clock is in dozing state instead of LS state
645      */
646     @VisibleForTesting
647     internal fun listenForAnyStateToDozingTransition(scope: CoroutineScope): Job {
648         return scope.launch {
649             keyguardTransitionInteractor
650                 .transition(Edge.create(to = DOZING))
651                 .filter { it.transitionState == TransitionState.FINISHED }
652                 .collect { handleDoze(1f) }
653         }
654     }
655 
656     @VisibleForTesting
657     internal fun listenForDozing(scope: CoroutineScope): Job {
658         return scope.launch {
659             combine(keyguardInteractor.dozeAmount, keyguardInteractor.isDozing) {
660                     localDozeAmount,
661                     localIsDozing ->
662                     localDozeAmount > dozeAmount || localIsDozing
663                 }
664                 .collect { localIsDozing -> isDozing = localIsDozing }
665         }
666     }
667 
668     class TimeListener(val clockFace: ClockFaceController, val executor: DelayableExecutor) {
669         val predrawListener =
670             ViewTreeObserver.OnPreDrawListener {
671                 clockFace.events.onTimeTick()
672                 true
673             }
674 
675         val secondsRunnable =
676             object : Runnable {
677                 override fun run() {
678                     if (!isRunning) {
679                         return
680                     }
681 
682                     executor.executeDelayed(this, 990)
683                     clockFace.events.onTimeTick()
684                 }
685             }
686 
687         var isRunning: Boolean = false
688             private set
689 
690         fun start() {
691             if (isRunning) {
692                 return
693             }
694 
695             isRunning = true
696             when (clockFace.config.tickRate) {
697                 ClockTickRate.PER_MINUTE -> {
698                     // Handled by KeyguardClockSwitchController and
699                     // by KeyguardUpdateMonitorCallback#onTimeChanged.
700                 }
701                 ClockTickRate.PER_SECOND -> executor.execute(secondsRunnable)
702                 ClockTickRate.PER_FRAME -> {
703                     clockFace.view.viewTreeObserver.addOnPreDrawListener(predrawListener)
704                     clockFace.view.invalidate()
705                 }
706             }
707         }
708 
709         fun stop() {
710             if (!isRunning) {
711                 return
712             }
713 
714             isRunning = false
715             clockFace.view.viewTreeObserver.removeOnPreDrawListener(predrawListener)
716         }
717 
718         fun update(shouldRun: Boolean) = if (shouldRun) start() else stop()
719     }
720 
721     companion object {
722         private const val TAG = "ClockEventController"
723         private const val DOZE_TICKRATE_THRESHOLD = 0.99f
724     }
725 }
726