blob: 76abad8ae8631b063a0b336e0c8e9162e4bcafec [file] [log] [blame]
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.keyguard
import com.android.systemui.keyguard.shared.model.KeyguardState.AOD
import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.res.Resources
import android.text.format.DateFormat
import android.util.Log
import android.util.TypedValue
import android.view.View
import android.view.View.OnAttachStateChangeListener
import android.view.ViewGroup
import android.view.ViewTreeObserver
import android.view.ViewTreeObserver.OnGlobalLayoutListener
import androidx.annotation.VisibleForTesting
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.repeatOnLifecycle
import com.android.systemui.broadcast.BroadcastDispatcher
import com.android.systemui.customization.R
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dagger.qualifiers.DisplaySpecific
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.flags.Flags.REGION_SAMPLING
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
import com.android.systemui.keyguard.shared.KeyguardShadeMigrationNssl
import com.android.systemui.keyguard.shared.model.TransitionState
import com.android.systemui.lifecycle.repeatWhenAttached
import com.android.systemui.log.LogBuffer
import com.android.systemui.log.core.LogLevel.DEBUG
import com.android.systemui.log.dagger.KeyguardLargeClockLog
import com.android.systemui.log.dagger.KeyguardSmallClockLog
import com.android.systemui.plugins.clocks.ClockController
import com.android.systemui.plugins.clocks.ClockFaceController
import com.android.systemui.plugins.clocks.ClockTickRate
import com.android.systemui.plugins.clocks.AlarmData
import com.android.systemui.plugins.clocks.WeatherData
import com.android.systemui.plugins.clocks.ZenData
import com.android.systemui.plugins.clocks.ZenData.ZenMode
import com.android.systemui.res.R as SysuiR
import com.android.systemui.shared.regionsampling.RegionSampler
import com.android.systemui.statusbar.policy.BatteryController
import com.android.systemui.statusbar.policy.BatteryController.BatteryStateChangeCallback
import com.android.systemui.statusbar.policy.ConfigurationController
import com.android.systemui.statusbar.policy.ZenModeController
import com.android.systemui.util.concurrency.DelayableExecutor
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DisposableHandle
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.launch
import java.util.Locale
import java.util.TimeZone
import java.util.concurrent.Executor
import javax.inject.Inject
/**
* Controller for a Clock provided by the registry and used on the keyguard. Instantiated by
* [KeyguardClockSwitchController]. Functionality is forked from [AnimatableClockController].
*/
open class ClockEventController
@Inject
constructor(
private val keyguardInteractor: KeyguardInteractor,
private val keyguardTransitionInteractor: KeyguardTransitionInteractor,
private val broadcastDispatcher: BroadcastDispatcher,
private val batteryController: BatteryController,
private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
private val configurationController: ConfigurationController,
@DisplaySpecific private val resources: Resources,
private val context: Context,
@Main private val mainExecutor: DelayableExecutor,
@Background private val bgExecutor: Executor,
@KeyguardSmallClockLog private val smallLogBuffer: LogBuffer?,
@KeyguardLargeClockLog private val largeLogBuffer: LogBuffer?,
private val featureFlags: FeatureFlags,
private val zenModeController: ZenModeController,
) {
var clock: ClockController? = null
set(value) {
smallClockOnAttachStateChangeListener?.let {
field?.smallClock?.view?.removeOnAttachStateChangeListener(it)
smallClockFrame?.viewTreeObserver
?.removeOnGlobalLayoutListener(onGlobalLayoutListener)
}
largeClockOnAttachStateChangeListener?.let {
field?.largeClock?.view?.removeOnAttachStateChangeListener(it)
}
field = value
if (value != null) {
smallLogBuffer?.log(TAG, DEBUG, {}, { "New Clock" })
value.smallClock.messageBuffer = smallLogBuffer
largeLogBuffer?.log(TAG, DEBUG, {}, { "New Clock" })
value.largeClock.messageBuffer = largeLogBuffer
value.initialize(resources, dozeAmount, 0f)
if (!regionSamplingEnabled) {
updateColors()
} else {
clock?.let {
smallRegionSampler = createRegionSampler(
it.smallClock.view,
mainExecutor,
bgExecutor,
regionSamplingEnabled,
isLockscreen = true,
::updateColors
)?.apply { startRegionSampler() }
largeRegionSampler = createRegionSampler(
it.largeClock.view,
mainExecutor,
bgExecutor,
regionSamplingEnabled,
isLockscreen = true,
::updateColors
)?.apply { startRegionSampler() }
updateColors()
}
}
updateFontSizes()
updateTimeListeners()
weatherData?.let {
if (WeatherData.DEBUG) {
Log.i(TAG, "Pushing cached weather data to new clock: $it")
}
value.events.onWeatherDataChanged(it)
}
zenData?.let {
value.events.onZenDataChanged(it)
}
alarmData?.let {
value.events.onAlarmDataChanged(it)
}
smallClockOnAttachStateChangeListener =
object : OnAttachStateChangeListener {
var pastVisibility: Int? = null
override fun onViewAttachedToWindow(view: View) {
value.events.onTimeFormatChanged(DateFormat.is24HourFormat(context))
// Match the asing for view.parent's layout classes.
smallClockFrame = view.parent as ViewGroup
smallClockFrame?.let { frame ->
pastVisibility = frame.visibility
onGlobalLayoutListener = OnGlobalLayoutListener {
val currentVisibility = frame.visibility
if (pastVisibility != currentVisibility) {
pastVisibility = currentVisibility
// when small clock is visible,
// recalculate bounds and sample
if (currentVisibility == View.VISIBLE) {
smallRegionSampler?.stopRegionSampler()
smallRegionSampler?.startRegionSampler()
}
}
}
frame.viewTreeObserver
.addOnGlobalLayoutListener(onGlobalLayoutListener)
}
}
override fun onViewDetachedFromWindow(p0: View) {
smallClockFrame?.viewTreeObserver
?.removeOnGlobalLayoutListener(onGlobalLayoutListener)
}
}
value.smallClock.view
.addOnAttachStateChangeListener(smallClockOnAttachStateChangeListener)
largeClockOnAttachStateChangeListener =
object : OnAttachStateChangeListener {
override fun onViewAttachedToWindow(p0: View) {
value.events.onTimeFormatChanged(DateFormat.is24HourFormat(context))
}
override fun onViewDetachedFromWindow(p0: View) {
}
}
value.largeClock.view
.addOnAttachStateChangeListener(largeClockOnAttachStateChangeListener)
}
}
@VisibleForTesting
var smallClockOnAttachStateChangeListener: OnAttachStateChangeListener? = null
@VisibleForTesting
var largeClockOnAttachStateChangeListener: OnAttachStateChangeListener? = null
private var smallClockFrame: ViewGroup? = null
private var onGlobalLayoutListener: OnGlobalLayoutListener? = null
private var isDozing = false
private set
private var isCharging = false
private var dozeAmount = 0f
private var isKeyguardVisible = false
private var isRegistered = false
private var disposableHandle: DisposableHandle? = null
private val regionSamplingEnabled = featureFlags.isEnabled(REGION_SAMPLING)
private var largeClockOnSecondaryDisplay = false
private fun updateColors() {
if (regionSamplingEnabled) {
clock?.let { clock ->
smallRegionSampler?.let {
val isRegionDark = it.currentRegionDarkness().isDark
clock.smallClock.events.onRegionDarknessChanged(isRegionDark)
}
largeRegionSampler?.let {
val isRegionDark = it.currentRegionDarkness().isDark
clock.largeClock.events.onRegionDarknessChanged(isRegionDark)
}
}
return
}
val isLightTheme = TypedValue()
context.theme.resolveAttribute(android.R.attr.isLightTheme, isLightTheme, true)
val isRegionDark = isLightTheme.data == 0
clock?.run {
Log.i(TAG, "Region isDark: $isRegionDark")
smallClock.events.onRegionDarknessChanged(isRegionDark)
largeClock.events.onRegionDarknessChanged(isRegionDark)
}
}
protected open fun createRegionSampler(
sampledView: View,
mainExecutor: Executor?,
bgExecutor: Executor?,
regionSamplingEnabled: Boolean,
isLockscreen: Boolean,
updateColors: () -> Unit
): RegionSampler? {
return RegionSampler(
sampledView,
mainExecutor,
bgExecutor,
regionSamplingEnabled,
isLockscreen,
) { updateColors() }
}
var smallRegionSampler: RegionSampler? = null
private set
var largeRegionSampler: RegionSampler? = null
private set
var smallTimeListener: TimeListener? = null
var largeTimeListener: TimeListener? = null
val shouldTimeListenerRun: Boolean
get() = isKeyguardVisible && dozeAmount < DOZE_TICKRATE_THRESHOLD
private var weatherData: WeatherData? = null
private var zenData: ZenData? = null
private var alarmData: AlarmData? = null
private val configListener =
object : ConfigurationController.ConfigurationListener {
override fun onThemeChanged() {
clock?.run { events.onColorPaletteChanged(resources) }
updateColors()
}
override fun onDensityOrFontScaleChanged() {
updateFontSizes()
}
}
private val batteryCallback =
object : BatteryStateChangeCallback {
override fun onBatteryLevelChanged(level: Int, pluggedIn: Boolean, charging: Boolean) {
if (isKeyguardVisible && !isCharging && charging) {
clock?.run {
smallClock.animations.charge()
largeClock.animations.charge()
}
}
isCharging = charging
}
}
private val localeBroadcastReceiver =
object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
clock?.run { events.onLocaleChanged(Locale.getDefault()) }
}
}
private val keyguardUpdateMonitorCallback =
object : KeyguardUpdateMonitorCallback() {
override fun onKeyguardVisibilityChanged(visible: Boolean) {
isKeyguardVisible = visible
if (!KeyguardShadeMigrationNssl.isEnabled) {
if (!isKeyguardVisible) {
clock?.run {
smallClock.animations.doze(if (isDozing) 1f else 0f)
largeClock.animations.doze(if (isDozing) 1f else 0f)
}
}
}
smallTimeListener?.update(shouldTimeListenerRun)
largeTimeListener?.update(shouldTimeListenerRun)
}
override fun onTimeFormatChanged(timeFormat: String?) {
clock?.run { events.onTimeFormatChanged(DateFormat.is24HourFormat(context)) }
}
override fun onTimeZoneChanged(timeZone: TimeZone) {
clock?.run { events.onTimeZoneChanged(timeZone) }
}
override fun onUserSwitchComplete(userId: Int) {
clock?.run { events.onTimeFormatChanged(DateFormat.is24HourFormat(context)) }
zenModeCallback.onNextAlarmChanged()
}
override fun onWeatherDataChanged(data: WeatherData) {
weatherData = data
clock?.run { events.onWeatherDataChanged(data) }
}
}
private val zenModeCallback = object : ZenModeController.Callback {
override fun onZenChanged(zen: Int) {
var mode = ZenMode.fromInt(zen)
if (mode == null) {
Log.e(TAG, "Failed to get zen mode from int: $zen")
return
}
zenData = ZenData(
mode,
if (mode == ZenMode.OFF) SysuiR.string::dnd_is_off.name
else SysuiR.string::dnd_is_on.name
).also { data ->
clock?.run { events.onZenDataChanged(data) }
}
}
override fun onNextAlarmChanged() {
val nextAlarmMillis = zenModeController.getNextAlarm()
alarmData = AlarmData(
if (nextAlarmMillis > 0) nextAlarmMillis else null,
SysuiR.string::status_bar_alarm.name
).also { data ->
clock?.run { events.onAlarmDataChanged(data) }
}
}
}
fun registerListeners(parent: View) {
if (isRegistered) {
return
}
isRegistered = true
broadcastDispatcher.registerReceiver(
localeBroadcastReceiver,
IntentFilter(Intent.ACTION_LOCALE_CHANGED)
)
configurationController.addCallback(configListener)
batteryController.addCallback(batteryCallback)
keyguardUpdateMonitor.registerCallback(keyguardUpdateMonitorCallback)
zenModeController.addCallback(zenModeCallback)
disposableHandle =
parent.repeatWhenAttached {
repeatOnLifecycle(Lifecycle.State.CREATED) {
listenForDozing(this)
if (KeyguardShadeMigrationNssl.isEnabled) {
listenForDozeAmountTransition(this)
listenForAnyStateToAodTransition(this)
} else {
listenForDozeAmount(this)
}
}
}
smallTimeListener?.update(shouldTimeListenerRun)
largeTimeListener?.update(shouldTimeListenerRun)
// Query ZenMode data
zenModeCallback.onZenChanged(zenModeController.zen)
zenModeCallback.onNextAlarmChanged()
}
fun unregisterListeners() {
if (!isRegistered) {
return
}
isRegistered = false
disposableHandle?.dispose()
broadcastDispatcher.unregisterReceiver(localeBroadcastReceiver)
configurationController.removeCallback(configListener)
batteryController.removeCallback(batteryCallback)
keyguardUpdateMonitor.removeCallback(keyguardUpdateMonitorCallback)
zenModeController.removeCallback(zenModeCallback)
smallRegionSampler?.stopRegionSampler()
largeRegionSampler?.stopRegionSampler()
smallTimeListener?.stop()
largeTimeListener?.stop()
clock?.smallClock?.view
?.removeOnAttachStateChangeListener(smallClockOnAttachStateChangeListener)
smallClockFrame?.viewTreeObserver
?.removeOnGlobalLayoutListener(onGlobalLayoutListener)
clock?.largeClock?.view
?.removeOnAttachStateChangeListener(largeClockOnAttachStateChangeListener)
}
/**
* Sets this clock as showing in a secondary display.
*
* Not that this is not necessarily needed, as we could get the displayId from [Context]
* directly and infere [largeClockOnSecondaryDisplay] from the id being different than the
* default display one. However, if we do so, current screenshot tests would not work, as they
* pass an activity context always from the default display.
*/
fun setLargeClockOnSecondaryDisplay(onSecondaryDisplay: Boolean) {
largeClockOnSecondaryDisplay = onSecondaryDisplay
updateFontSizes()
}
private fun updateTimeListeners() {
smallTimeListener?.stop()
largeTimeListener?.stop()
smallTimeListener = null
largeTimeListener = null
clock?.let {
smallTimeListener = TimeListener(it.smallClock, mainExecutor).apply {
update(shouldTimeListenerRun)
}
largeTimeListener = TimeListener(it.largeClock, mainExecutor).apply {
update(shouldTimeListenerRun)
}
}
}
private fun updateFontSizes() {
clock?.run {
smallClock.events.onFontSettingChanged(
resources.getDimensionPixelSize(R.dimen.small_clock_text_size).toFloat()
)
largeClock.events.onFontSettingChanged(getLargeClockSizePx())
}
}
private fun getLargeClockSizePx(): Float {
return if (largeClockOnSecondaryDisplay) {
resources.getDimensionPixelSize(R.dimen.presentation_clock_text_size).toFloat()
} else {
resources.getDimensionPixelSize(R.dimen.large_clock_text_size).toFloat()
}
}
private fun handleDoze(doze: Float) {
dozeAmount = doze
clock?.run {
smallClock.animations.doze(dozeAmount)
largeClock.animations.doze(dozeAmount)
}
smallTimeListener?.update(doze < DOZE_TICKRATE_THRESHOLD)
largeTimeListener?.update(doze < DOZE_TICKRATE_THRESHOLD)
}
@VisibleForTesting
internal fun listenForDozeAmount(scope: CoroutineScope): Job {
return scope.launch { keyguardInteractor.dozeAmount.collect { handleDoze(it) } }
}
@VisibleForTesting
internal fun listenForDozeAmountTransition(scope: CoroutineScope): Job {
return scope.launch {
keyguardTransitionInteractor.dozeAmountTransition.collect { handleDoze(it.value) }
}
}
/**
* When keyguard is displayed again after being gone, the clock must be reset to full dozing.
*/
@VisibleForTesting
internal fun listenForAnyStateToAodTransition(scope: CoroutineScope): Job {
return scope.launch {
keyguardTransitionInteractor.transitionStepsToState(AOD)
.filter { it.transitionState == TransitionState.STARTED }
.filter { it.from != LOCKSCREEN }
.collect { handleDoze(1f) }
}
}
@VisibleForTesting
internal fun listenForDozing(scope: CoroutineScope): Job {
return scope.launch {
combine(
keyguardInteractor.dozeAmount,
keyguardInteractor.isDozing,
) { localDozeAmount, localIsDozing ->
localDozeAmount > dozeAmount || localIsDozing
}
.collect { localIsDozing -> isDozing = localIsDozing }
}
}
class TimeListener(val clockFace: ClockFaceController, val executor: DelayableExecutor) {
val predrawListener =
ViewTreeObserver.OnPreDrawListener {
clockFace.events.onTimeTick()
true
}
val secondsRunnable =
object : Runnable {
override fun run() {
if (!isRunning) {
return
}
executor.executeDelayed(this, 990)
clockFace.events.onTimeTick()
}
}
var isRunning: Boolean = false
private set
fun start() {
if (isRunning) {
return
}
isRunning = true
when (clockFace.config.tickRate) {
ClockTickRate.PER_MINUTE -> {
/* Handled by KeyguardClockSwitchController */
}
ClockTickRate.PER_SECOND -> executor.execute(secondsRunnable)
ClockTickRate.PER_FRAME -> {
clockFace.view.viewTreeObserver.addOnPreDrawListener(predrawListener)
clockFace.view.invalidate()
}
}
}
fun stop() {
if (!isRunning) {
return
}
isRunning = false
clockFace.view.viewTreeObserver.removeOnPreDrawListener(predrawListener)
}
fun update(shouldRun: Boolean) = if (shouldRun) start() else stop()
}
companion object {
private val TAG = ClockEventController::class.simpleName!!
private val DOZE_TICKRATE_THRESHOLD = 0.99f
}
}