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