blob: aa2d20b422bc33453620679ddb491a67170fae5c [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 android.app.WallpaperManager
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.TypedValue
import android.util.Log
import android.view.View
import android.view.View.OnAttachStateChangeListener
import android.view.ViewTreeObserver
import androidx.annotation.VisibleForTesting
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.repeatOnLifecycle
import com.android.systemui.customization.R
import com.android.systemui.broadcast.BroadcastDispatcher
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.flags.Flags.DOZING_MIGRATION_1
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.model.TransitionState
import com.android.systemui.lifecycle.repeatWhenAttached
import com.android.systemui.log.LogBuffer
import com.android.systemui.log.LogLevel.DEBUG
import com.android.systemui.log.dagger.KeyguardLargeClockLog
import com.android.systemui.log.dagger.KeyguardSmallClockLog
import com.android.systemui.plugins.ClockController
import com.android.systemui.plugins.ClockFaceController
import com.android.systemui.plugins.ClockTickRate
import com.android.systemui.plugins.WeatherData
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.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,
@Main 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
) {
var clock: ClockController? = null
set(value) {
field = value
if (value != null) {
smallLogBuffer?.log(TAG, DEBUG, {}, { "New Clock" })
value.smallClock.logBuffer = smallLogBuffer
largeLogBuffer?.log(TAG, DEBUG, {}, { "New Clock" })
value.largeClock.logBuffer = largeLogBuffer
value.initialize(resources, dozeAmount, 0f)
if (!regionSamplingEnabled) {
updateColors()
}
updateFontSizes()
updateTimeListeners()
cachedWeatherData?.let {
if (WeatherData.DEBUG) {
Log.i(TAG, "Pushing cached weather data to new clock: $it")
}
value.events.onWeatherDataChanged(it)
}
value.smallClock.view.addOnAttachStateChangeListener(
object : OnAttachStateChangeListener {
override fun onViewAttachedToWindow(p0: View?) {
value.events.onTimeFormatChanged(DateFormat.is24HourFormat(context))
}
override fun onViewDetachedFromWindow(p0: View?) {
}
})
value.largeClock.view.addOnAttachStateChangeListener(
object : OnAttachStateChangeListener {
override fun onViewAttachedToWindow(p0: View?) {
value.events.onTimeFormatChanged(DateFormat.is24HourFormat(context))
}
override fun onViewDetachedFromWindow(p0: View?) {
}
})
}
}
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 fun updateColors() {
val wallpaperManager = WallpaperManager.getInstance(context)
if (regionSamplingEnabled) {
regionSampler?.let { regionSampler ->
clock?.let { clock ->
if (regionSampler.sampledView == clock.smallClock.view) {
smallClockIsDark = regionSampler.currentRegionDarkness().isDark
clock.smallClock.events.onRegionDarknessChanged(smallClockIsDark)
return@updateColors
} else if (regionSampler.sampledView == clock.largeClock.view) {
largeClockIsDark = regionSampler.currentRegionDarkness().isDark
clock.largeClock.events.onRegionDarknessChanged(largeClockIsDark)
return@updateColors
}
}
}
}
val isLightTheme = TypedValue()
context.theme.resolveAttribute(android.R.attr.isLightTheme, isLightTheme, true)
smallClockIsDark = isLightTheme.data == 0
largeClockIsDark = isLightTheme.data == 0
clock?.run {
smallClock.events.onRegionDarknessChanged(smallClockIsDark)
largeClock.events.onRegionDarknessChanged(largeClockIsDark)
}
}
private fun updateRegionSampler(sampledRegion: View) {
regionSampler?.stopRegionSampler()
regionSampler =
createRegionSampler(
sampledRegion,
mainExecutor,
bgExecutor,
regionSamplingEnabled,
isLockscreen = true,
::updateColors
)
?.apply { startRegionSampler() }
updateColors()
}
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 regionSampler: RegionSampler? = null
var smallTimeListener: TimeListener? = null
var largeTimeListener: TimeListener? = null
val shouldTimeListenerRun: Boolean
get() = isKeyguardVisible && dozeAmount < DOZE_TICKRATE_THRESHOLD
private var cachedWeatherData: WeatherData? = null
private var smallClockIsDark = true
private var largeClockIsDark = true
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 (!featureFlags.isEnabled(DOZING_MIGRATION_1)) {
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)) }
}
override fun onWeatherDataChanged(data: WeatherData) {
cachedWeatherData = data
clock?.run { events.onWeatherDataChanged(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)
disposableHandle =
parent.repeatWhenAttached {
repeatOnLifecycle(Lifecycle.State.STARTED) {
listenForDozing(this)
if (featureFlags.isEnabled(DOZING_MIGRATION_1)) {
listenForDozeAmountTransition(this)
listenForAnyStateToAodTransition(this)
} else {
listenForDozeAmount(this)
}
}
}
smallTimeListener?.update(shouldTimeListenerRun)
largeTimeListener?.update(shouldTimeListenerRun)
}
fun unregisterListeners() {
if (!isRegistered) {
return
}
isRegistered = false
disposableHandle?.dispose()
broadcastDispatcher.unregisterReceiver(localeBroadcastReceiver)
configurationController.removeCallback(configListener)
batteryController.removeCallback(batteryCallback)
keyguardUpdateMonitor.removeCallback(keyguardUpdateMonitorCallback)
regionSampler?.stopRegionSampler()
smallTimeListener?.stop()
largeTimeListener?.stop()
}
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(
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.anyStateToAodTransition
.filter { it.transitionState == TransitionState.FINISHED }
.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
}
}