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