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