• 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.os.Process
20 import android.provider.DeviceConfig
21 import android.util.Log
22 import androidx.core.animation.Animator
23 import androidx.core.animation.AnimatorListenerAdapter
24 import androidx.core.animation.AnimatorSet
25 import com.android.systemui.dagger.qualifiers.Main
26 import com.android.systemui.dump.DumpManager
27 import com.android.systemui.statusbar.window.StatusBarWindowController
28 import com.android.systemui.util.Assert
29 import com.android.systemui.util.concurrency.DelayableExecutor
30 import com.android.systemui.util.time.SystemClock
31 import java.io.PrintWriter
32 import javax.inject.Inject
33 
34 /**
35  * Dead-simple scheduler for system status events. Obeys the following principles (all values TBD):
36  * ```
37  *      - Avoiding log spam by only allowing 12 events per minute (1event/5s)
38  *      - Waits 100ms to schedule any event for debouncing/prioritization
39  *      - Simple prioritization: Privacy > Battery > connectivity (encoded in [StatusEvent])
40  *      - Only schedules a single event, and throws away lowest priority events
41  * ```
42  *
43  * There are 4 basic stages of animation at play here:
44  * ```
45  *      1. System chrome animation OUT
46  *      2. Chip animation IN
47  *      3. Chip animation OUT; potentially into a dot
48  *      4. System chrome animation IN
49  * ```
50  *
51  * Thus we can keep all animations synchronized with two separate ValueAnimators, one for system
52  * chrome and the other for the chip. These can animate from 0,1 and listeners can parameterize
53  * their respective views based on the progress of the animator. Interpolation differences TBD
54  */
55 open class SystemStatusAnimationSchedulerLegacyImpl
56 @Inject
57 constructor(
58     private val coordinator: SystemEventCoordinator,
59     private val chipAnimationController: SystemEventChipAnimationController,
60     private val statusBarWindowController: StatusBarWindowController,
61     private val dumpManager: DumpManager,
62     private val systemClock: SystemClock,
63     @Main private val executor: DelayableExecutor
64 ) : SystemStatusAnimationScheduler {
65 
66     companion object {
67         private const val PROPERTY_ENABLE_IMMERSIVE_INDICATOR = "enable_immersive_indicator"
68     }
69 
70     fun isImmersiveIndicatorEnabled(): Boolean {
71         return DeviceConfig.getBoolean(
72             DeviceConfig.NAMESPACE_PRIVACY,
73             PROPERTY_ENABLE_IMMERSIVE_INDICATOR,
74             true
75         )
76     }
77 
78     @SystemAnimationState private var animationState: Int = IDLE
79 
80     /** True if the persistent privacy dot should be active */
81     var hasPersistentDot = false
82         protected set
83 
84     private var scheduledEvent: StatusEvent? = null
85 
86     val listeners = mutableSetOf<SystemStatusAnimationCallback>()
87 
88     init {
89         coordinator.attachScheduler(this)
90         dumpManager.registerDumpable(TAG, this)
91     }
92 
93     @SystemAnimationState override fun getAnimationState() = animationState
94 
95     override 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 (
104             (event.priority > (scheduledEvent?.priority ?: -1)) &&
105                 animationState != ANIMATING_OUT &&
106                 animationState != SHOWING_PERSISTENT_DOT
107         ) {
108             // events can only be scheduled if a higher priority or no other event is in progress
109             if (DEBUG) {
110                 Log.d(TAG, "scheduling event $event")
111             }
112 
113             scheduleEvent(event)
114         } else if (scheduledEvent?.shouldUpdateFromEvent(event) == true) {
115             if (DEBUG) {
116                 Log.d(TAG, "updating current event from: $event. animationState=$animationState")
117             }
118             scheduledEvent?.updateFromEvent(event)
119             if (event.forceVisible) {
120                 hasPersistentDot = true
121                 // If we missed the chance to show the persistent dot, do it now
122                 if (animationState == IDLE) {
123                     notifyTransitionToPersistentDot()
124                 }
125             }
126         } else {
127             if (DEBUG) {
128                 Log.d(TAG, "ignoring event $event")
129             }
130         }
131     }
132 
133     override fun removePersistentDot() {
134         if (!hasPersistentDot || !isImmersiveIndicatorEnabled()) {
135             return
136         }
137 
138         hasPersistentDot = false
139         notifyHidePersistentDot()
140         return
141     }
142 
143     fun isTooEarly(): Boolean {
144         return systemClock.uptimeMillis() - Process.getStartUptimeMillis() < MIN_UPTIME
145     }
146 
147     /** Clear the scheduled event (if any) and schedule a new one */
148     private fun scheduleEvent(event: StatusEvent) {
149         scheduledEvent = event
150 
151         if (event.forceVisible) {
152             hasPersistentDot = true
153         }
154 
155         // If animations are turned off, we'll transition directly to the dot
156         if (!event.showAnimation && event.forceVisible) {
157             notifyTransitionToPersistentDot()
158             scheduledEvent = null
159             return
160         }
161 
162         chipAnimationController.prepareChipAnimation(scheduledEvent!!.viewCreator)
163         animationState = ANIMATION_QUEUED
164         executor.executeDelayed({ runChipAnimation() }, DEBOUNCE_DELAY)
165     }
166 
167     /**
168      * 1. Define a total budget for the chip animation (1500ms)
169      * 2. Send out callbacks to listeners so that they can generate animations locally
170      * 3. Update the scheduler state so that clients know where we are
171      * 4. Maybe: provide scaffolding such as: dot location, margins, etc
172      * 5. Maybe: define a maximum animation length and enforce it. Probably only doable if we
173      *    collect all of the animators and run them together.
174      */
175     private fun runChipAnimation() {
176         statusBarWindowController.setForceStatusBarVisible(true)
177         animationState = ANIMATING_IN
178 
179         val animSet = collectStartAnimations()
180         if (animSet.totalDuration > 500) {
181             throw IllegalStateException(
182                 "System animation total length exceeds budget. " +
183                     "Expected: 500, actual: ${animSet.totalDuration}"
184             )
185         }
186         animSet.addListener(
187             object : AnimatorListenerAdapter() {
188                 override fun onAnimationEnd(animation: Animator) {
189                     animationState = RUNNING_CHIP_ANIM
190                 }
191             }
192         )
193         animSet.start()
194 
195         executor.executeDelayed(
196             {
197                 val animSet2 = collectFinishAnimations()
198                 animationState = ANIMATING_OUT
199                 animSet2.addListener(
200                     object : AnimatorListenerAdapter() {
201                         override fun onAnimationEnd(animation: Animator) {
202                             animationState =
203                                 if (hasPersistentDot) {
204                                     SHOWING_PERSISTENT_DOT
205                                 } else {
206                                     IDLE
207                                 }
208 
209                             statusBarWindowController.setForceStatusBarVisible(false)
210                         }
211                     }
212                 )
213                 animSet2.start()
214                 scheduledEvent = null
215             },
216             DISPLAY_LENGTH
217         )
218     }
219 
220     private fun collectStartAnimations(): AnimatorSet {
221         val animators = mutableListOf<Animator>()
222         listeners.forEach { listener ->
223             listener.onSystemEventAnimationBegin()?.let { anim -> animators.add(anim) }
224         }
225         animators.add(chipAnimationController.onSystemEventAnimationBegin())
226         val animSet = AnimatorSet().also { it.playTogether(animators) }
227 
228         return animSet
229     }
230 
231     private fun collectFinishAnimations(): AnimatorSet {
232         val animators = mutableListOf<Animator>()
233         listeners.forEach { listener ->
234             listener.onSystemEventAnimationFinish(hasPersistentDot)?.let { anim ->
235                 animators.add(anim)
236             }
237         }
238         animators.add(chipAnimationController.onSystemEventAnimationFinish(hasPersistentDot))
239         if (hasPersistentDot) {
240             val dotAnim = notifyTransitionToPersistentDot()
241             if (dotAnim != null) {
242                 animators.add(dotAnim)
243             }
244         }
245         val animSet = AnimatorSet().also { it.playTogether(animators) }
246 
247         return animSet
248     }
249 
250     private fun notifyTransitionToPersistentDot(): Animator? {
251         val anims: List<Animator> =
252             listeners.mapNotNull {
253                 it.onSystemStatusAnimationTransitionToPersistentDot(
254                     scheduledEvent?.contentDescription
255                 )
256             }
257         if (anims.isNotEmpty()) {
258             val aSet = AnimatorSet()
259             aSet.playTogether(anims)
260             return aSet
261         }
262 
263         return null
264     }
265 
266     private fun notifyHidePersistentDot(): Animator? {
267         val anims: List<Animator> = listeners.mapNotNull { it.onHidePersistentDot() }
268 
269         if (animationState == SHOWING_PERSISTENT_DOT) {
270             animationState = IDLE
271         }
272 
273         if (anims.isNotEmpty()) {
274             val aSet = AnimatorSet()
275             aSet.playTogether(anims)
276             return aSet
277         }
278 
279         return null
280     }
281 
282     override fun addCallback(listener: SystemStatusAnimationCallback) {
283         Assert.isMainThread()
284 
285         if (listeners.isEmpty()) {
286             coordinator.startObserving()
287         }
288         listeners.add(listener)
289     }
290 
291     override fun removeCallback(listener: SystemStatusAnimationCallback) {
292         Assert.isMainThread()
293 
294         listeners.remove(listener)
295         if (listeners.isEmpty()) {
296             coordinator.stopObserving()
297         }
298     }
299 
300     override fun dump(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 { pw.println("  $it") }
309         }
310     }
311 }
312 
313 private const val DEBUG = false
314 private const val TAG = "SystemStatusAnimationSchedulerLegacyImpl"
315