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