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