| /* |
| * Copyright (C) 2021 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.systemui.statusbar.events |
| |
| import android.os.Process |
| import android.provider.DeviceConfig |
| import android.util.Log |
| import androidx.core.animation.Animator |
| import androidx.core.animation.AnimatorListenerAdapter |
| import androidx.core.animation.AnimatorSet |
| import com.android.systemui.dagger.qualifiers.Main |
| import com.android.systemui.dump.DumpManager |
| import com.android.systemui.statusbar.window.StatusBarWindowController |
| import com.android.systemui.util.Assert |
| import com.android.systemui.util.concurrency.DelayableExecutor |
| import com.android.systemui.util.time.SystemClock |
| import java.io.PrintWriter |
| import javax.inject.Inject |
| |
| /** |
| * Dead-simple scheduler for system status events. Obeys the following principles (all values TBD): |
| * ``` |
| * - Avoiding log spam by only allowing 12 events per minute (1event/5s) |
| * - Waits 100ms to schedule any event for debouncing/prioritization |
| * - Simple prioritization: Privacy > Battery > connectivity (encoded in [StatusEvent]) |
| * - Only schedules a single event, and throws away lowest priority events |
| * ``` |
| * |
| * There are 4 basic stages of animation at play here: |
| * ``` |
| * 1. System chrome animation OUT |
| * 2. Chip animation IN |
| * 3. Chip animation OUT; potentially into a dot |
| * 4. System chrome animation IN |
| * ``` |
| * |
| * Thus we can keep all animations synchronized with two separate ValueAnimators, one for system |
| * chrome and the other for the chip. These can animate from 0,1 and listeners can parameterize |
| * their respective views based on the progress of the animator. Interpolation differences TBD |
| */ |
| open class SystemStatusAnimationSchedulerLegacyImpl |
| @Inject |
| constructor( |
| private val coordinator: SystemEventCoordinator, |
| private val chipAnimationController: SystemEventChipAnimationController, |
| private val statusBarWindowController: StatusBarWindowController, |
| private val dumpManager: DumpManager, |
| private val systemClock: SystemClock, |
| @Main private val executor: DelayableExecutor |
| ) : SystemStatusAnimationScheduler { |
| |
| companion object { |
| private const val PROPERTY_ENABLE_IMMERSIVE_INDICATOR = "enable_immersive_indicator" |
| } |
| |
| fun isImmersiveIndicatorEnabled(): Boolean { |
| return DeviceConfig.getBoolean( |
| DeviceConfig.NAMESPACE_PRIVACY, |
| PROPERTY_ENABLE_IMMERSIVE_INDICATOR, |
| true |
| ) |
| } |
| |
| @SystemAnimationState private var animationState: Int = IDLE |
| |
| /** True if the persistent privacy dot should be active */ |
| var hasPersistentDot = false |
| protected set |
| |
| private var scheduledEvent: StatusEvent? = null |
| |
| val listeners = mutableSetOf<SystemStatusAnimationCallback>() |
| |
| init { |
| coordinator.attachScheduler(this) |
| dumpManager.registerDumpable(TAG, this) |
| } |
| |
| @SystemAnimationState override fun getAnimationState() = animationState |
| |
| override fun onStatusEvent(event: StatusEvent) { |
| // Ignore any updates until the system is up and running |
| if (isTooEarly() || !isImmersiveIndicatorEnabled()) { |
| return |
| } |
| |
| // Don't deal with threading for now (no need let's be honest) |
| Assert.isMainThread() |
| if ( |
| (event.priority > (scheduledEvent?.priority ?: -1)) && |
| animationState != ANIMATING_OUT && |
| animationState != SHOWING_PERSISTENT_DOT |
| ) { |
| // events can only be scheduled if a higher priority or no other event is in progress |
| if (DEBUG) { |
| Log.d(TAG, "scheduling event $event") |
| } |
| |
| scheduleEvent(event) |
| } else if (scheduledEvent?.shouldUpdateFromEvent(event) == true) { |
| if (DEBUG) { |
| Log.d(TAG, "updating current event from: $event. animationState=$animationState") |
| } |
| scheduledEvent?.updateFromEvent(event) |
| if (event.forceVisible) { |
| hasPersistentDot = true |
| // If we missed the chance to show the persistent dot, do it now |
| if (animationState == IDLE) { |
| notifyTransitionToPersistentDot() |
| } |
| } |
| } else { |
| if (DEBUG) { |
| Log.d(TAG, "ignoring event $event") |
| } |
| } |
| } |
| |
| override fun removePersistentDot() { |
| if (!hasPersistentDot || !isImmersiveIndicatorEnabled()) { |
| return |
| } |
| |
| hasPersistentDot = false |
| notifyHidePersistentDot() |
| return |
| } |
| |
| fun isTooEarly(): Boolean { |
| return systemClock.uptimeMillis() - Process.getStartUptimeMillis() < MIN_UPTIME |
| } |
| |
| /** Clear the scheduled event (if any) and schedule a new one */ |
| private fun scheduleEvent(event: StatusEvent) { |
| scheduledEvent = event |
| |
| if (event.forceVisible) { |
| hasPersistentDot = true |
| } |
| |
| // If animations are turned off, we'll transition directly to the dot |
| if (!event.showAnimation && event.forceVisible) { |
| notifyTransitionToPersistentDot() |
| scheduledEvent = null |
| return |
| } |
| |
| chipAnimationController.prepareChipAnimation(scheduledEvent!!.viewCreator) |
| animationState = ANIMATION_QUEUED |
| executor.executeDelayed({ runChipAnimation() }, DEBOUNCE_DELAY) |
| } |
| |
| /** |
| * 1. Define a total budget for the chip animation (1500ms) |
| * 2. Send out callbacks to listeners so that they can generate animations locally |
| * 3. Update the scheduler state so that clients know where we are |
| * 4. Maybe: provide scaffolding such as: dot location, margins, etc |
| * 5. Maybe: define a maximum animation length and enforce it. Probably only doable if we |
| * collect all of the animators and run them together. |
| */ |
| private fun runChipAnimation() { |
| statusBarWindowController.setForceStatusBarVisible(true) |
| animationState = ANIMATING_IN |
| |
| val animSet = collectStartAnimations() |
| if (animSet.totalDuration > 500) { |
| throw IllegalStateException( |
| "System animation total length exceeds budget. " + |
| "Expected: 500, actual: ${animSet.totalDuration}" |
| ) |
| } |
| animSet.addListener( |
| object : AnimatorListenerAdapter() { |
| override fun onAnimationEnd(animation: Animator) { |
| animationState = RUNNING_CHIP_ANIM |
| } |
| } |
| ) |
| animSet.start() |
| |
| executor.executeDelayed( |
| { |
| val animSet2 = collectFinishAnimations() |
| animationState = ANIMATING_OUT |
| animSet2.addListener( |
| object : AnimatorListenerAdapter() { |
| override fun onAnimationEnd(animation: Animator) { |
| animationState = |
| if (hasPersistentDot) { |
| SHOWING_PERSISTENT_DOT |
| } else { |
| IDLE |
| } |
| |
| statusBarWindowController.setForceStatusBarVisible(false) |
| } |
| } |
| ) |
| animSet2.start() |
| scheduledEvent = null |
| }, |
| DISPLAY_LENGTH |
| ) |
| } |
| |
| private fun collectStartAnimations(): AnimatorSet { |
| val animators = mutableListOf<Animator>() |
| listeners.forEach { listener -> |
| listener.onSystemEventAnimationBegin()?.let { anim -> animators.add(anim) } |
| } |
| animators.add(chipAnimationController.onSystemEventAnimationBegin()) |
| val animSet = AnimatorSet().also { it.playTogether(animators) } |
| |
| return animSet |
| } |
| |
| private fun collectFinishAnimations(): AnimatorSet { |
| val animators = mutableListOf<Animator>() |
| listeners.forEach { listener -> |
| listener.onSystemEventAnimationFinish(hasPersistentDot)?.let { anim -> |
| animators.add(anim) |
| } |
| } |
| animators.add(chipAnimationController.onSystemEventAnimationFinish(hasPersistentDot)) |
| if (hasPersistentDot) { |
| val dotAnim = notifyTransitionToPersistentDot() |
| if (dotAnim != null) { |
| animators.add(dotAnim) |
| } |
| } |
| val animSet = AnimatorSet().also { it.playTogether(animators) } |
| |
| return animSet |
| } |
| |
| private fun notifyTransitionToPersistentDot(): Animator? { |
| val anims: List<Animator> = |
| listeners.mapNotNull { |
| it.onSystemStatusAnimationTransitionToPersistentDot( |
| scheduledEvent?.contentDescription |
| ) |
| } |
| if (anims.isNotEmpty()) { |
| val aSet = AnimatorSet() |
| aSet.playTogether(anims) |
| return aSet |
| } |
| |
| return null |
| } |
| |
| private fun notifyHidePersistentDot(): Animator? { |
| val anims: List<Animator> = listeners.mapNotNull { it.onHidePersistentDot() } |
| |
| if (animationState == SHOWING_PERSISTENT_DOT) { |
| animationState = IDLE |
| } |
| |
| if (anims.isNotEmpty()) { |
| val aSet = AnimatorSet() |
| aSet.playTogether(anims) |
| return aSet |
| } |
| |
| return null |
| } |
| |
| override fun addCallback(listener: SystemStatusAnimationCallback) { |
| Assert.isMainThread() |
| |
| if (listeners.isEmpty()) { |
| coordinator.startObserving() |
| } |
| listeners.add(listener) |
| } |
| |
| override fun removeCallback(listener: SystemStatusAnimationCallback) { |
| Assert.isMainThread() |
| |
| listeners.remove(listener) |
| if (listeners.isEmpty()) { |
| coordinator.stopObserving() |
| } |
| } |
| |
| override fun dump(pw: PrintWriter, args: Array<out String>) { |
| pw.println("Scheduled event: $scheduledEvent") |
| pw.println("Has persistent privacy dot: $hasPersistentDot") |
| pw.println("Animation state: $animationState") |
| pw.println("Listeners:") |
| if (listeners.isEmpty()) { |
| pw.println("(none)") |
| } else { |
| listeners.forEach { pw.println(" $it") } |
| } |
| } |
| } |
| |
| private const val DEBUG = false |
| private const val TAG = "SystemStatusAnimationSchedulerLegacyImpl" |