1 /*
2  * 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. However, for important events that
97         // request to be force visible (like privacy), ignore whether it's too early.
98         if ((isTooEarly() && !event.forceVisible) || !isImmersiveIndicatorEnabled()) {
99             return
100         }
101 
102         // Don't deal with threading for now (no need let's be honest)
103         Assert.isMainThread()
104         if (
105             (event.priority > (scheduledEvent?.priority ?: -1)) &&
106                 animationState != ANIMATING_OUT &&
107                 animationState != SHOWING_PERSISTENT_DOT
108         ) {
109             // events can only be scheduled if a higher priority or no other event is in progress
110             if (DEBUG) {
111                 Log.d(TAG, "scheduling event $event")
112             }
113 
114             scheduleEvent(event)
115         } else if (scheduledEvent?.shouldUpdateFromEvent(event) == true) {
116             if (DEBUG) {
117                 Log.d(TAG, "updating current event from: $event. animationState=$animationState")
118             }
119             scheduledEvent?.updateFromEvent(event)
120             if (event.forceVisible) {
121                 hasPersistentDot = true
122                 // If we missed the chance to show the persistent dot, do it now
123                 if (animationState == IDLE) {
124                     notifyTransitionToPersistentDot()
125                 }
126             }
127         } else {
128             if (DEBUG) {
129                 Log.d(TAG, "ignoring event $event")
130             }
131         }
132     }
133 
134     override fun removePersistentDot() {
135         if (!hasPersistentDot || !isImmersiveIndicatorEnabled()) {
136             return
137         }
138 
139         hasPersistentDot = false
140         notifyHidePersistentDot()
141         return
142     }
143 
144     fun isTooEarly(): Boolean {
145         return systemClock.uptimeMillis() - Process.getStartUptimeMillis() < MIN_UPTIME
146     }
147 
148     /** Clear the scheduled event (if any) and schedule a new one */
149     private fun scheduleEvent(event: StatusEvent) {
150         scheduledEvent = event
151 
152         if (event.forceVisible) {
153             hasPersistentDot = true
154         }
155 
156         // If animations are turned off, we'll transition directly to the dot
157         if (!event.showAnimation && event.forceVisible) {
158             notifyTransitionToPersistentDot()
159             scheduledEvent = null
160             return
161         }
162 
163         chipAnimationController.prepareChipAnimation(scheduledEvent!!.viewCreator)
164         animationState = ANIMATION_QUEUED
165         executor.executeDelayed({ runChipAnimation() }, DEBOUNCE_DELAY)
166     }
167 
168     /**
169      * 1. Define a total budget for the chip animation (1500ms)
170      * 2. Send out callbacks to listeners so that they can generate animations locally
171      * 3. Update the scheduler state so that clients know where we are
172      * 4. Maybe: provide scaffolding such as: dot location, margins, etc
173      * 5. Maybe: define a maximum animation length and enforce it. Probably only doable if we
174      *    collect all of the animators and run them together.
175      */
176     private fun runChipAnimation() {
177         statusBarWindowController.setForceStatusBarVisible(true)
178         animationState = ANIMATING_IN
179 
180         val animSet = collectStartAnimations()
181         if (animSet.totalDuration > 500) {
182             throw IllegalStateException(
183                 "System animation total length exceeds budget. " +
184                     "Expected: 500, actual: ${animSet.totalDuration}"
185             )
186         }
187         animSet.addListener(
188             object : AnimatorListenerAdapter() {
189                 override fun onAnimationEnd(animation: Animator) {
190                     animationState = RUNNING_CHIP_ANIM
191                 }
192             }
193         )
194         animSet.start()
195 
196         executor.executeDelayed(
197             {
198                 val animSet2 = collectFinishAnimations()
199                 animationState = ANIMATING_OUT
200                 animSet2.addListener(
201                     object : AnimatorListenerAdapter() {
202                         override fun onAnimationEnd(animation: Animator) {
203                             animationState =
204                                 if (hasPersistentDot) {
205                                     SHOWING_PERSISTENT_DOT
206                                 } else {
207                                     IDLE
208                                 }
209 
210                             statusBarWindowController.setForceStatusBarVisible(false)
211                         }
212                     }
213                 )
214                 animSet2.start()
215                 scheduledEvent = null
216             },
217             DISPLAY_LENGTH
218         )
219     }
220 
221     private fun collectStartAnimations(): AnimatorSet {
222         val animators = mutableListOf<Animator>()
223         listeners.forEach { listener ->
224             listener.onSystemEventAnimationBegin()?.let { anim -> animators.add(anim) }
225         }
226         animators.add(chipAnimationController.onSystemEventAnimationBegin())
227         val animSet = AnimatorSet().also { it.playTogether(animators) }
228 
229         return animSet
230     }
231 
232     private fun collectFinishAnimations(): AnimatorSet {
233         val animators = mutableListOf<Animator>()
234         listeners.forEach { listener ->
235             listener.onSystemEventAnimationFinish(hasPersistentDot)?.let { anim ->
236                 animators.add(anim)
237             }
238         }
239         animators.add(chipAnimationController.onSystemEventAnimationFinish(hasPersistentDot))
240         if (hasPersistentDot) {
241             val dotAnim = notifyTransitionToPersistentDot()
242             if (dotAnim != null) {
243                 animators.add(dotAnim)
244             }
245         }
246         val animSet = AnimatorSet().also { it.playTogether(animators) }
247 
248         return animSet
249     }
250 
251     private fun notifyTransitionToPersistentDot(): Animator? {
252         val anims: List<Animator> =
253             listeners.mapNotNull {
254                 it.onSystemStatusAnimationTransitionToPersistentDot(
255                     scheduledEvent?.contentDescription
256                 )
257             }
258         if (anims.isNotEmpty()) {
259             val aSet = AnimatorSet()
260             aSet.playTogether(anims)
261             return aSet
262         }
263 
264         return null
265     }
266 
267     private fun notifyHidePersistentDot(): Animator? {
268         val anims: List<Animator> = listeners.mapNotNull { it.onHidePersistentDot() }
269 
270         if (animationState == SHOWING_PERSISTENT_DOT) {
271             animationState = IDLE
272         }
273 
274         if (anims.isNotEmpty()) {
275             val aSet = AnimatorSet()
276             aSet.playTogether(anims)
277             return aSet
278         }
279 
280         return null
281     }
282 
283     override fun addCallback(listener: SystemStatusAnimationCallback) {
284         Assert.isMainThread()
285 
286         if (listeners.isEmpty()) {
287             coordinator.startObserving()
288         }
289         listeners.add(listener)
290     }
291 
292     override fun removeCallback(listener: SystemStatusAnimationCallback) {
293         Assert.isMainThread()
294 
295         listeners.remove(listener)
296         if (listeners.isEmpty()) {
297             coordinator.stopObserving()
298         }
299     }
300 
301     override fun dump(pw: PrintWriter, args: Array<out String>) {
302         pw.println("Scheduled event: $scheduledEvent")
303         pw.println("Has persistent privacy dot: $hasPersistentDot")
304         pw.println("Animation state: $animationState")
305         pw.println("Listeners:")
306         if (listeners.isEmpty()) {
307             pw.println("(none)")
308         } else {
309             listeners.forEach { pw.println("  $it") }
310         }
311     }
312 }
313 
314 private const val DEBUG = false
315 private const val TAG = "SystemStatusAnimationSchedulerLegacyImpl"
316