• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.text.format.DateFormat
24 import android.util.Log
25 import android.util.TypedValue
26 import android.view.View
27 import android.view.View.OnAttachStateChangeListener
28 import android.view.ViewTreeObserver
29 import android.view.ViewTreeObserver.OnGlobalLayoutListener
30 import android.widget.FrameLayout
31 import androidx.annotation.VisibleForTesting
32 import androidx.lifecycle.Lifecycle
33 import androidx.lifecycle.repeatOnLifecycle
34 import com.android.systemui.R
35 import com.android.systemui.broadcast.BroadcastDispatcher
36 import com.android.systemui.dagger.qualifiers.Background
37 import com.android.systemui.dagger.qualifiers.Main
38 import com.android.systemui.flags.FeatureFlags
39 import com.android.systemui.flags.Flags.DOZING_MIGRATION_1
40 import com.android.systemui.flags.Flags.REGION_SAMPLING
41 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
42 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
43 import com.android.systemui.keyguard.shared.model.TransitionState
44 import com.android.systemui.lifecycle.repeatWhenAttached
45 import com.android.systemui.log.LogBuffer
46 import com.android.systemui.log.core.LogLevel.DEBUG
47 import com.android.systemui.log.dagger.KeyguardLargeClockLog
48 import com.android.systemui.log.dagger.KeyguardSmallClockLog
49 import com.android.systemui.plugins.ClockController
50 import com.android.systemui.plugins.ClockFaceController
51 import com.android.systemui.plugins.ClockTickRate
52 import com.android.systemui.plugins.WeatherData
53 import com.android.systemui.shared.regionsampling.RegionSampler
54 import com.android.systemui.statusbar.policy.BatteryController
55 import com.android.systemui.statusbar.policy.BatteryController.BatteryStateChangeCallback
56 import com.android.systemui.statusbar.policy.ConfigurationController
57 import com.android.systemui.util.concurrency.DelayableExecutor
58 import kotlinx.coroutines.CoroutineScope
59 import kotlinx.coroutines.DisposableHandle
60 import kotlinx.coroutines.Job
61 import kotlinx.coroutines.flow.combine
62 import kotlinx.coroutines.flow.filter
63 import kotlinx.coroutines.launch
64 import java.util.Locale
65 import java.util.TimeZone
66 import java.util.concurrent.Executor
67 import javax.inject.Inject
68 
69 /**
70  * Controller for a Clock provided by the registry and used on the keyguard. Instantiated by
71  * [KeyguardClockSwitchController]. Functionality is forked from [AnimatableClockController].
72  */
73 open class ClockEventController
74 @Inject
75 constructor(
76     private val keyguardInteractor: KeyguardInteractor,
77     private val keyguardTransitionInteractor: KeyguardTransitionInteractor,
78     private val broadcastDispatcher: BroadcastDispatcher,
79     private val batteryController: BatteryController,
80     private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
81     private val configurationController: ConfigurationController,
82     @Main private val resources: Resources,
83     private val context: Context,
84     @Main private val mainExecutor: DelayableExecutor,
85     @Background private val bgExecutor: Executor,
86     @KeyguardSmallClockLog private val smallLogBuffer: LogBuffer?,
87     @KeyguardLargeClockLog private val largeLogBuffer: LogBuffer?,
88     private val featureFlags: FeatureFlags
89 ) {
90     var clock: ClockController? = null
91         set(value) {
92             smallClockOnAttachStateChangeListener?.let {
93                 field?.smallClock?.view?.removeOnAttachStateChangeListener(it)
94                 smallClockFrame?.viewTreeObserver
95                         ?.removeOnGlobalLayoutListener(onGlobalLayoutListener)
96             }
97             largeClockOnAttachStateChangeListener?.let {
98                 field?.largeClock?.view?.removeOnAttachStateChangeListener(it)
99             }
100 
101             field = value
102             if (value != null) {
103                 smallLogBuffer?.log(TAG, DEBUG, {}, { "New Clock" })
104                 value.smallClock.messageBuffer = smallLogBuffer
105                 largeLogBuffer?.log(TAG, DEBUG, {}, { "New Clock" })
106                 value.largeClock.messageBuffer = largeLogBuffer
107 
108                 value.initialize(resources, dozeAmount, 0f)
109 
110                 if (!regionSamplingEnabled) {
111                     updateColors()
112                 } else {
113                     clock?.let {
114                         smallRegionSampler = createRegionSampler(
115                                 it.smallClock.view,
116                                 mainExecutor,
117                                 bgExecutor,
118                                 regionSamplingEnabled,
119                                 isLockscreen = true,
120                                 ::updateColors
121                         )?.apply { startRegionSampler() }
122 
123                         largeRegionSampler = createRegionSampler(
124                                 it.largeClock.view,
125                                 mainExecutor,
126                                 bgExecutor,
127                                 regionSamplingEnabled,
128                                 isLockscreen = true,
129                                 ::updateColors
130                         )?.apply { startRegionSampler() }
131 
132                         updateColors()
133                     }
134                 }
135                 updateFontSizes()
136                 updateTimeListeners()
137                 cachedWeatherData?.let {
138                     if (WeatherData.DEBUG) {
139                         Log.i(TAG, "Pushing cached weather data to new clock: $it")
140                     }
141                     value.events.onWeatherDataChanged(it)
142                 }
143 
144                 smallClockOnAttachStateChangeListener =
145                     object : OnAttachStateChangeListener {
146                         var pastVisibility: Int? = null
147                         override fun onViewAttachedToWindow(view: View) {
148                             value.events.onTimeFormatChanged(DateFormat.is24HourFormat(context))
149                             if (view != null) {
150                                 smallClockFrame = view.parent as FrameLayout
151                                 smallClockFrame?.let {frame ->
152                                     pastVisibility = frame.visibility
153                                     onGlobalLayoutListener = OnGlobalLayoutListener {
154                                         val currentVisibility = frame.visibility
155                                         if (pastVisibility != currentVisibility) {
156                                             pastVisibility = currentVisibility
157                                             // when small clock  visible,
158                                             // recalculate bounds and sample
159                                             if (currentVisibility == View.VISIBLE) {
160                                                 smallRegionSampler?.stopRegionSampler()
161                                                 smallRegionSampler?.startRegionSampler()
162                                             }
163                                         }
164                                     }
165                                     frame.viewTreeObserver
166                                             .addOnGlobalLayoutListener(onGlobalLayoutListener)
167                                 }
168                             }
169                         }
170 
171                         override fun onViewDetachedFromWindow(p0: View) {
172                             smallClockFrame?.viewTreeObserver
173                                     ?.removeOnGlobalLayoutListener(onGlobalLayoutListener)
174                         }
175                 }
176                 value.smallClock.view
177                         .addOnAttachStateChangeListener(smallClockOnAttachStateChangeListener)
178 
179                 largeClockOnAttachStateChangeListener =
180                     object : OnAttachStateChangeListener {
181                         override fun onViewAttachedToWindow(p0: View) {
182                             value.events.onTimeFormatChanged(DateFormat.is24HourFormat(context))
183                         }
184                         override fun onViewDetachedFromWindow(p0: View) {
185                         }
186                 }
187                 value.largeClock.view
188                         .addOnAttachStateChangeListener(largeClockOnAttachStateChangeListener)
189             }
190         }
191 
192     @VisibleForTesting
193     var smallClockOnAttachStateChangeListener: OnAttachStateChangeListener? = null
194     @VisibleForTesting
195     var largeClockOnAttachStateChangeListener: OnAttachStateChangeListener? = null
196     private var smallClockFrame: FrameLayout? = null
197     private var onGlobalLayoutListener: OnGlobalLayoutListener? = null
198 
199     private var isDozing = false
200         private set
201 
202     private var isCharging = false
203     private var dozeAmount = 0f
204     private var isKeyguardVisible = false
205     private var isRegistered = false
206     private var disposableHandle: DisposableHandle? = null
207     private val regionSamplingEnabled = featureFlags.isEnabled(REGION_SAMPLING)
208 
209 
210     private fun updateColors() {
211         if (regionSamplingEnabled) {
212             clock?.let { clock ->
213                 smallRegionSampler?.let {
214                     smallClockIsDark = it.currentRegionDarkness().isDark
215                     clock.smallClock.events.onRegionDarknessChanged(smallClockIsDark)
216                 }
217 
218                 largeRegionSampler?.let {
219                     largeClockIsDark = it.currentRegionDarkness().isDark
220                     clock.largeClock.events.onRegionDarknessChanged(largeClockIsDark)
221                 }
222             }
223             return
224         }
225 
226         val isLightTheme = TypedValue()
227         context.theme.resolveAttribute(android.R.attr.isLightTheme, isLightTheme, true)
228         smallClockIsDark = isLightTheme.data == 0
229         largeClockIsDark = isLightTheme.data == 0
230 
231         clock?.run {
232             smallClock.events.onRegionDarknessChanged(smallClockIsDark)
233             largeClock.events.onRegionDarknessChanged(largeClockIsDark)
234         }
235     }
236     protected open fun createRegionSampler(
237         sampledView: View,
238         mainExecutor: Executor?,
239         bgExecutor: Executor?,
240         regionSamplingEnabled: Boolean,
241         isLockscreen: Boolean,
242         updateColors: () -> Unit
243     ): RegionSampler? {
244         return RegionSampler(
245             sampledView,
246             mainExecutor,
247             bgExecutor,
248             regionSamplingEnabled,
249             isLockscreen,
250         ) { updateColors() }
251     }
252 
253     var smallRegionSampler: RegionSampler? = null
254         private set
255     var largeRegionSampler: RegionSampler? = null
256         private set
257     var smallTimeListener: TimeListener? = null
258     var largeTimeListener: TimeListener? = null
259     val shouldTimeListenerRun: Boolean
260         get() = isKeyguardVisible && dozeAmount < DOZE_TICKRATE_THRESHOLD
261     private var cachedWeatherData: WeatherData? = null
262 
263     private var smallClockIsDark = true
264     private var largeClockIsDark = true
265 
266     private val configListener =
267         object : ConfigurationController.ConfigurationListener {
268             override fun onThemeChanged() {
269                 clock?.run { events.onColorPaletteChanged(resources) }
270                 updateColors()
271             }
272 
273             override fun onDensityOrFontScaleChanged() {
274                 updateFontSizes()
275             }
276         }
277 
278     private val batteryCallback =
279         object : BatteryStateChangeCallback {
280             override fun onBatteryLevelChanged(level: Int, pluggedIn: Boolean, charging: Boolean) {
281                 if (isKeyguardVisible && !isCharging && charging) {
282                     clock?.run {
283                         smallClock.animations.charge()
284                         largeClock.animations.charge()
285                     }
286                 }
287                 isCharging = charging
288             }
289         }
290 
291     private val localeBroadcastReceiver =
292         object : BroadcastReceiver() {
293             override fun onReceive(context: Context, intent: Intent) {
294                 clock?.run { events.onLocaleChanged(Locale.getDefault()) }
295             }
296         }
297 
298     private val keyguardUpdateMonitorCallback =
299         object : KeyguardUpdateMonitorCallback() {
300             override fun onKeyguardVisibilityChanged(visible: Boolean) {
301                 isKeyguardVisible = visible
302                 if (!featureFlags.isEnabled(DOZING_MIGRATION_1)) {
303                     if (!isKeyguardVisible) {
304                         clock?.run {
305                             smallClock.animations.doze(if (isDozing) 1f else 0f)
306                             largeClock.animations.doze(if (isDozing) 1f else 0f)
307                         }
308                     }
309                 }
310 
311                 smallTimeListener?.update(shouldTimeListenerRun)
312                 largeTimeListener?.update(shouldTimeListenerRun)
313             }
314 
315             override fun onTimeFormatChanged(timeFormat: String?) {
316                 clock?.run { events.onTimeFormatChanged(DateFormat.is24HourFormat(context)) }
317             }
318 
319             override fun onTimeZoneChanged(timeZone: TimeZone) {
320                 clock?.run { events.onTimeZoneChanged(timeZone) }
321             }
322 
323             override fun onUserSwitchComplete(userId: Int) {
324                 clock?.run { events.onTimeFormatChanged(DateFormat.is24HourFormat(context)) }
325             }
326 
327             override fun onWeatherDataChanged(data: WeatherData) {
328                 cachedWeatherData = data
329                 clock?.run { events.onWeatherDataChanged(data) }
330             }
331         }
332 
333     fun registerListeners(parent: View) {
334         if (isRegistered) {
335             return
336         }
337         isRegistered = true
338         broadcastDispatcher.registerReceiver(
339             localeBroadcastReceiver,
340             IntentFilter(Intent.ACTION_LOCALE_CHANGED)
341         )
342         configurationController.addCallback(configListener)
343         batteryController.addCallback(batteryCallback)
344         keyguardUpdateMonitor.registerCallback(keyguardUpdateMonitorCallback)
345         disposableHandle =
346             parent.repeatWhenAttached {
347                 repeatOnLifecycle(Lifecycle.State.STARTED) {
348                     listenForDozing(this)
349                     if (featureFlags.isEnabled(DOZING_MIGRATION_1)) {
350                         listenForDozeAmountTransition(this)
351                         listenForAnyStateToAodTransition(this)
352                     } else {
353                         listenForDozeAmount(this)
354                     }
355                 }
356             }
357         smallTimeListener?.update(shouldTimeListenerRun)
358         largeTimeListener?.update(shouldTimeListenerRun)
359     }
360 
361     fun unregisterListeners() {
362         if (!isRegistered) {
363             return
364         }
365         isRegistered = false
366 
367         disposableHandle?.dispose()
368         broadcastDispatcher.unregisterReceiver(localeBroadcastReceiver)
369         configurationController.removeCallback(configListener)
370         batteryController.removeCallback(batteryCallback)
371         keyguardUpdateMonitor.removeCallback(keyguardUpdateMonitorCallback)
372         smallRegionSampler?.stopRegionSampler()
373         largeRegionSampler?.stopRegionSampler()
374         smallTimeListener?.stop()
375         largeTimeListener?.stop()
376         clock?.smallClock?.view
377                 ?.removeOnAttachStateChangeListener(smallClockOnAttachStateChangeListener)
378         smallClockFrame?.viewTreeObserver
379                 ?.removeOnGlobalLayoutListener(onGlobalLayoutListener)
380         clock?.largeClock?.view
381                 ?.removeOnAttachStateChangeListener(largeClockOnAttachStateChangeListener)
382     }
383 
384     private fun updateTimeListeners() {
385         smallTimeListener?.stop()
386         largeTimeListener?.stop()
387 
388         smallTimeListener = null
389         largeTimeListener = null
390 
391         clock?.let {
392             smallTimeListener = TimeListener(it.smallClock, mainExecutor).apply {
393                 update(shouldTimeListenerRun)
394             }
395             largeTimeListener = TimeListener(it.largeClock, mainExecutor).apply {
396                 update(shouldTimeListenerRun)
397             }
398         }
399     }
400 
401     private fun updateFontSizes() {
402         clock?.run {
403             smallClock.events.onFontSettingChanged(
404                 resources.getDimensionPixelSize(R.dimen.small_clock_text_size).toFloat()
405             )
406             largeClock.events.onFontSettingChanged(
407                 resources.getDimensionPixelSize(R.dimen.large_clock_text_size).toFloat()
408             )
409         }
410     }
411 
412     private fun handleDoze(doze: Float) {
413         dozeAmount = doze
414         clock?.run {
415             smallClock.animations.doze(dozeAmount)
416             largeClock.animations.doze(dozeAmount)
417         }
418         smallTimeListener?.update(doze < DOZE_TICKRATE_THRESHOLD)
419         largeTimeListener?.update(doze < DOZE_TICKRATE_THRESHOLD)
420     }
421 
422     @VisibleForTesting
423     internal fun listenForDozeAmount(scope: CoroutineScope): Job {
424         return scope.launch { keyguardInteractor.dozeAmount.collect { handleDoze(it) } }
425     }
426 
427     @VisibleForTesting
428     internal fun listenForDozeAmountTransition(scope: CoroutineScope): Job {
429         return scope.launch {
430             keyguardTransitionInteractor.dozeAmountTransition.collect { handleDoze(it.value) }
431         }
432     }
433 
434     /**
435      * When keyguard is displayed again after being gone, the clock must be reset to full dozing.
436      */
437     @VisibleForTesting
438     internal fun listenForAnyStateToAodTransition(scope: CoroutineScope): Job {
439         return scope.launch {
440             keyguardTransitionInteractor.anyStateToAodTransition
441                 .filter { it.transitionState == TransitionState.FINISHED }
442                 .collect { handleDoze(1f) }
443         }
444     }
445 
446     @VisibleForTesting
447     internal fun listenForDozing(scope: CoroutineScope): Job {
448         return scope.launch {
449             combine(
450                     keyguardInteractor.dozeAmount,
451                     keyguardInteractor.isDozing,
452                 ) { localDozeAmount, localIsDozing ->
453                     localDozeAmount > dozeAmount || localIsDozing
454                 }
455                 .collect { localIsDozing -> isDozing = localIsDozing }
456         }
457     }
458 
459     class TimeListener(val clockFace: ClockFaceController, val executor: DelayableExecutor) {
460         val predrawListener =
461             ViewTreeObserver.OnPreDrawListener {
462                 clockFace.events.onTimeTick()
463                 true
464             }
465 
466         val secondsRunnable =
467             object : Runnable {
468                 override fun run() {
469                     if (!isRunning) {
470                         return
471                     }
472 
473                     executor.executeDelayed(this, 990)
474                     clockFace.events.onTimeTick()
475                 }
476             }
477 
478         var isRunning: Boolean = false
479             private set
480 
481         fun start() {
482             if (isRunning) {
483                 return
484             }
485 
486             isRunning = true
487             when (clockFace.config.tickRate) {
488                 ClockTickRate.PER_MINUTE -> {
489                     /* Handled by KeyguardClockSwitchController */
490                 }
491                 ClockTickRate.PER_SECOND -> executor.execute(secondsRunnable)
492                 ClockTickRate.PER_FRAME -> {
493                     clockFace.view.viewTreeObserver.addOnPreDrawListener(predrawListener)
494                     clockFace.view.invalidate()
495                 }
496             }
497         }
498 
499         fun stop() {
500             if (!isRunning) {
501                 return
502             }
503 
504             isRunning = false
505             clockFace.view.viewTreeObserver.removeOnPreDrawListener(predrawListener)
506         }
507 
508         fun update(shouldRun: Boolean) = if (shouldRun) start() else stop()
509     }
510 
511     companion object {
512         private val TAG = ClockEventController::class.simpleName!!
513         private val DOZE_TICKRATE_THRESHOLD = 0.99f
514     }
515 }
516