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