• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * Copyright (C) 2021 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 
17 package com.android.systemui.statusbar.events
18 
19 import android.animation.Animator
20 import android.animation.AnimatorListenerAdapter
21 import android.animation.AnimatorSet
22 import android.animation.ValueAnimator
23 import android.annotation.IntDef
24 import android.content.Context
25 import android.os.Process
26 import android.provider.DeviceConfig
27 import android.util.Log
28 import android.view.View
29 import com.android.systemui.Dumpable
30 
31 import com.android.systemui.dagger.SysUISingleton
32 import com.android.systemui.dagger.qualifiers.Main
33 import com.android.systemui.dump.DumpManager
34 import com.android.systemui.statusbar.phone.StatusBarWindowController
35 import com.android.systemui.statusbar.policy.CallbackController
36 import com.android.systemui.util.Assert
37 import com.android.systemui.util.concurrency.DelayableExecutor
38 import com.android.systemui.util.time.SystemClock
39 import java.io.FileDescriptor
40 import java.io.PrintWriter
41 
42 import javax.inject.Inject
43 
44 /**
45  * Dead-simple scheduler for system status events. Obeys the following principles (all values TBD):
46  *      - Avoiding log spam by only allowing 12 events per minute (1event/5s)
47  *      - Waits 100ms to schedule any event for debouncing/prioritization
48  *      - Simple prioritization: Privacy > Battery > connectivity (encoded in StatusEvent)
49  *      - Only schedules a single event, and throws away lowest priority events
50  *
51  * There are 4 basic stages of animation at play here:
52  *      1. System chrome animation OUT
53  *      2. Chip animation IN
54  *      3. Chip animation OUT; potentially into a dot
55  *      4. System chrome animation IN
56  *
57  * Thus we can keep all animations synchronized with two separate ValueAnimators, one for system
58  * chrome and the other for the chip. These can animate from 0,1 and listeners can parameterize
59  * their respective views based on the progress of the animator. Interpolation differences TBD
60  */
61 @SysUISingleton
62 class SystemStatusAnimationScheduler @Inject constructor(
63     private val coordinator: SystemEventCoordinator,
64     private val chipAnimationController: SystemEventChipAnimationController,
65     private val statusBarWindowController: StatusBarWindowController,
66     private val dumpManager: DumpManager,
67     private val systemClock: SystemClock,
68     @Main private val executor: DelayableExecutor
69 ) : CallbackController<SystemStatusAnimationCallback>, Dumpable {
70 
71     companion object {
72         private const val PROPERTY_ENABLE_IMMERSIVE_INDICATOR = "enable_immersive_indicator"
73     }
74     private fun isImmersiveIndicatorEnabled(): Boolean {
75         return DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_PRIVACY,
76                 PROPERTY_ENABLE_IMMERSIVE_INDICATOR, true)
77     }
78 
79     @SystemAnimationState var animationState: Int = IDLE
80         private set
81 
82     /** True if the persistent privacy dot should be active */
83     var hasPersistentDot = false
84         private set
85 
86     private var scheduledEvent: StatusEvent? = null
87     private var cancelExecutionRunnable: Runnable? = null
88     private val listeners = mutableSetOf<SystemStatusAnimationCallback>()
89 
90     init {
91         coordinator.attachScheduler(this)
92         dumpManager.registerDumpable(TAG, this)
93     }
94 
95     fun onStatusEvent(event: StatusEvent) {
96         // Ignore any updates until the system is up and running
97         if (isTooEarly() || !isImmersiveIndicatorEnabled()) {
98             return
99         }
100 
101         // Don't deal with threading for now (no need let's be honest)
102         Assert.isMainThread()
103         if ((event.priority > scheduledEvent?.priority ?: -1) &&
104                 animationState != ANIMATING_OUT &&
105                 (animationState != SHOWING_PERSISTENT_DOT && event.forceVisible)) {
106             // events can only be scheduled if a higher priority or no other event is in progress
107             if (DEBUG) {
108                 Log.d(TAG, "scheduling event $event")
109             }
110 
111             scheduleEvent(event)
112         } else if (scheduledEvent?.shouldUpdateFromEvent(event) == true) {
113             if (DEBUG) {
114                 Log.d(TAG, "updating current event from: $event")
115             }
116             scheduledEvent?.updateFromEvent(event)
117             if (event.forceVisible) {
118                 hasPersistentDot = true
119                 notifyTransitionToPersistentDot()
120             }
121         } else {
122             if (DEBUG) {
123                 Log.d(TAG, "ignoring event $event")
124             }
125         }
126     }
127 
128     private fun clearDotIfVisible() {
129         notifyHidePersistentDot()
130     }
131 
132     fun setShouldShowPersistentPrivacyIndicator(should: Boolean) {
133         if (hasPersistentDot == should || !isImmersiveIndicatorEnabled()) {
134             return
135         }
136 
137         hasPersistentDot = should
138 
139         if (!hasPersistentDot) {
140             clearDotIfVisible()
141         }
142     }
143 
144     private fun isTooEarly(): Boolean {
145         return systemClock.uptimeMillis() - Process.getStartUptimeMillis() < MIN_UPTIME
146     }
147 
148     /**
149      * Clear the scheduled event (if any) and schedule a new one
150      */
151     private fun scheduleEvent(event: StatusEvent) {
152         scheduledEvent = event
153 
154         if (event.forceVisible) {
155             hasPersistentDot = true
156         }
157 
158         // If animations are turned off, we'll transition directly to the dot
159         if (!event.showAnimation && event.forceVisible) {
160             notifyTransitionToPersistentDot()
161             scheduledEvent = null
162             return
163         }
164 
165         // Schedule the animation to start after a debounce period
166         cancelExecutionRunnable = executor.executeDelayed({
167             cancelExecutionRunnable = null
168             animationState = ANIMATING_IN
169             statusBarWindowController.setForceStatusBarVisible(true)
170 
171             val entranceAnimator = ValueAnimator.ofFloat(1f, 0f)
172             entranceAnimator.duration = ENTRANCE_ANIM_LENGTH
173             entranceAnimator.addListener(systemAnimatorAdapter)
174             entranceAnimator.addUpdateListener(systemUpdateListener)
175 
176             val chipAnimator = ValueAnimator.ofFloat(0f, 1f)
177             chipAnimator.duration = CHIP_ANIM_LENGTH
178             chipAnimator.addListener(
179                     ChipAnimatorAdapter(RUNNING_CHIP_ANIM, scheduledEvent!!.viewCreator))
180             chipAnimator.addUpdateListener(chipUpdateListener)
181 
182             val aSet2 = AnimatorSet()
183             aSet2.playSequentially(entranceAnimator, chipAnimator)
184             aSet2.start()
185 
186             executor.executeDelayed({
187                 animationState = ANIMATING_OUT
188 
189                 val systemAnimator = ValueAnimator.ofFloat(0f, 1f)
190                 systemAnimator.duration = ENTRANCE_ANIM_LENGTH
191                 systemAnimator.addListener(systemAnimatorAdapter)
192                 systemAnimator.addUpdateListener(systemUpdateListener)
193 
194                 val chipAnimator = ValueAnimator.ofFloat(1f, 0f)
195                 chipAnimator.duration = CHIP_ANIM_LENGTH
196                 val endState = if (hasPersistentDot) {
197                     SHOWING_PERSISTENT_DOT
198                 } else {
199                     IDLE
200                 }
201                 chipAnimator.addListener(
202                     ChipAnimatorAdapter(endState, scheduledEvent!!.viewCreator))
203                 chipAnimator.addUpdateListener(chipUpdateListener)
204 
205                 val aSet2 = AnimatorSet()
206 
207                 aSet2.play(chipAnimator).before(systemAnimator)
208                 if (hasPersistentDot) {
209                     val dotAnim = notifyTransitionToPersistentDot()
210                     if (dotAnim != null) aSet2.playTogether(systemAnimator, dotAnim)
211                 }
212 
213                 aSet2.start()
214 
215                 statusBarWindowController.setForceStatusBarVisible(false)
216                 scheduledEvent = null
217             }, DISPLAY_LENGTH)
218         }, DELAY)
219     }
220 
221     private fun notifyTransitionToPersistentDot(): Animator? {
222         val anims: List<Animator> = listeners.mapNotNull {
223             it.onSystemStatusAnimationTransitionToPersistentDot(scheduledEvent?.contentDescription)
224         }
225         if (anims.isNotEmpty()) {
226             val aSet = AnimatorSet()
227             aSet.playTogether(anims)
228             return aSet
229         }
230 
231         return null
232     }
233 
234     private fun notifyHidePersistentDot(): Animator? {
235         val anims: List<Animator> = listeners.mapNotNull {
236             it.onHidePersistentDot()
237         }
238 
239         if (animationState == SHOWING_PERSISTENT_DOT) {
240             animationState = IDLE
241         }
242 
243         if (anims.isNotEmpty()) {
244             val aSet = AnimatorSet()
245             aSet.playTogether(anims)
246             return aSet
247         }
248 
249         return null
250     }
251 
252     private fun notifySystemStart() {
253         listeners.forEach { it.onSystemChromeAnimationStart() }
254     }
255 
256     private fun notifySystemFinish() {
257         listeners.forEach { it.onSystemChromeAnimationEnd() }
258     }
259 
260     private fun notifySystemAnimationUpdate(anim: ValueAnimator) {
261         listeners.forEach { it.onSystemChromeAnimationUpdate(anim) }
262     }
263 
264     override fun addCallback(listener: SystemStatusAnimationCallback) {
265         Assert.isMainThread()
266 
267         if (listeners.isEmpty()) {
268             coordinator.startObserving()
269         }
270         listeners.add(listener)
271     }
272 
273     override fun removeCallback(listener: SystemStatusAnimationCallback) {
274         Assert.isMainThread()
275 
276         listeners.remove(listener)
277         if (listeners.isEmpty()) {
278             coordinator.stopObserving()
279         }
280     }
281 
282     private val systemUpdateListener = ValueAnimator.AnimatorUpdateListener {
283         anim -> notifySystemAnimationUpdate(anim)
284     }
285 
286     private val systemAnimatorAdapter = object : AnimatorListenerAdapter() {
287         override fun onAnimationEnd(p0: Animator?) {
288             notifySystemFinish()
289         }
290 
291         override fun onAnimationStart(p0: Animator?) {
292             notifySystemStart()
293         }
294     }
295 
296     private val chipUpdateListener = ValueAnimator.AnimatorUpdateListener {
297         anim -> chipAnimationController.onChipAnimationUpdate(anim, animationState)
298     }
299 
300     override fun dump(fd: FileDescriptor, pw: PrintWriter, args: Array<out String>) {
301         pw.println("Scheduled event: $scheduledEvent")
302         pw.println("Has persistent privacy dot: $hasPersistentDot")
303         pw.println("Animation state: $animationState")
304         pw.println("Listeners:")
305         if (listeners.isEmpty()) {
306             pw.println("(none)")
307         } else {
308             listeners.forEach {
309                 pw.println("  $it")
310             }
311         }
312     }
313 
314     inner class ChipAnimatorAdapter(
315         @SystemAnimationState val endState: Int,
316         val viewCreator: (context: Context) -> View
317     ) : AnimatorListenerAdapter() {
318         override fun onAnimationEnd(p0: Animator?) {
319             chipAnimationController.onChipAnimationEnd(animationState)
320             animationState = if (endState == SHOWING_PERSISTENT_DOT && !hasPersistentDot) {
321                 IDLE
322             } else {
323                 endState
324             }
325         }
326 
327         override fun onAnimationStart(p0: Animator?) {
328             chipAnimationController.onChipAnimationStart(viewCreator, animationState)
329         }
330     }
331 }
332 
333 /**
334  * The general idea here is that this scheduler will run two value animators, and provide
335  * animator-like callbacks for each kind of animation. The SystemChrome animation is expected to
336  * create space for the chip animation to display. This means hiding the system elements in the
337  * status bar and keyguard.
338  *
339  * TODO: the chip animation really only has one client, we can probably remove it from this
340  * interface
341  *
342  * The value animators themselves are simple animators from 0.0 to 1.0. Listeners can apply any
343  * interpolation they choose but realistically these are most likely to be simple alpha transitions
344  */
345 interface SystemStatusAnimationCallback {
onSystemChromeAnimationUpdatenull346     @JvmDefault fun onSystemChromeAnimationUpdate(animator: ValueAnimator) {}
onSystemChromeAnimationStartnull347     @JvmDefault fun onSystemChromeAnimationStart() {}
onSystemChromeAnimationEndnull348     @JvmDefault fun onSystemChromeAnimationEnd() {}
349 
350     // Best method name, change my mind
351     @JvmDefault
onSystemStatusAnimationTransitionToPersistentDotnull352     fun onSystemStatusAnimationTransitionToPersistentDot(contentDescription: String?): Animator? {
353         return null
354     }
onHidePersistentDotnull355     @JvmDefault fun onHidePersistentDot(): Animator? { return null }
356 }
357 
358 interface SystemStatusChipAnimationCallback {
onChipAnimationUpdatenull359     fun onChipAnimationUpdate(animator: ValueAnimator, @SystemAnimationState state: Int) {}
360 
onChipAnimationStartnull361     fun onChipAnimationStart(
362         viewCreator: (context: Context) -> View,
363         @SystemAnimationState state: Int
364     ) {}
365 
onChipAnimationEndnull366     fun onChipAnimationEnd(@SystemAnimationState state: Int) {}
367 }
368 
369 /**
370  */
371 @Retention(AnnotationRetention.SOURCE)
372 @IntDef(
373         value = [
374             IDLE, ANIMATING_IN, RUNNING_CHIP_ANIM, ANIMATING_OUT
375         ]
376 )
377 annotation class SystemAnimationState
378 
379 /** No animation is in progress */
380 const val IDLE = 0
381 /** System is animating out, and chip is animating in */
382 const val ANIMATING_IN = 1
383 /** Chip has animated in and is awaiting exit animation, and optionally playing its own animation */
384 const val RUNNING_CHIP_ANIM = 2
385 /** Chip is animating away and system is animating back */
386 const val ANIMATING_OUT = 3
387 /** Chip has animated away, and the persistent dot is showing */
388 const val SHOWING_PERSISTENT_DOT = 4
389 
390 private const val TAG = "SystemStatusAnimationScheduler"
391 private const val DELAY = 0L
392 
393 /**
394  * The total time spent animation should be 1500ms. The entrance animation is how much time
395  * we give to the system to animate system elements out of the way. Total chip animation length
396  * will be equivalent to 2*chip_anim_length + display_length
397  */
398 private const val ENTRANCE_ANIM_LENGTH = 250L
399 private const val CHIP_ANIM_LENGTH = 250L
400 // 1s + entrance time + chip anim_length
401 private const val DISPLAY_LENGTH = 1500L
402 
403 private const val MIN_UPTIME: Long = 5 * 1000
404 
405 private const val DEBUG = false
406