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