blob: 3c35e4bbc84103e2a58a9c20b9b7b93636d29ff8 [file] [log] [blame]
/*
* 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.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.animation.AnimatorSet
import android.annotation.IntDef
import android.os.Process
import android.provider.DeviceConfig
import android.util.Log
import android.view.animation.PathInterpolator
import com.android.systemui.Dumpable
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.dump.DumpManager
import com.android.systemui.statusbar.policy.CallbackController
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
*/
@SysUISingleton
open class SystemStatusAnimationScheduler @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
) : CallbackController<SystemStatusAnimationCallback>, Dumpable {
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 var animationState: Int = IDLE
private set
/** True if the persistent privacy dot should be active */
var hasPersistentDot = false
protected set
private var scheduledEvent: StatusEvent? = null
private var cancelExecutionRunnable: Runnable? = null
val listeners = mutableSetOf<SystemStatusAnimationCallback>()
init {
coordinator.attachScheduler(this)
dumpManager.registerDumpable(TAG, this)
}
open 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")
}
}
}
private fun clearDotIfVisible() {
notifyHidePersistentDot()
}
fun setShouldShowPersistentPrivacyIndicator(should: Boolean) {
if (hasPersistentDot == should || !isImmersiveIndicatorEnabled()) {
return
}
hasPersistentDot = should
if (!hasPersistentDot) {
clearDotIfVisible()
}
}
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")
}
}
}
}
/**
* The general idea here is that this scheduler will run two value animators, and provide
* animator-like callbacks for each kind of animation. The SystemChrome animation is expected to
* create space for the chip animation to display. This means hiding the system elements in the
* status bar and keyguard.
*
* The value animators themselves are simple animators from 0.0 to 1.0. Listeners can apply any
* interpolation they choose but realistically these are most likely to be simple alpha transitions
*/
interface SystemStatusAnimationCallback {
/** Implement this method to return an [Animator] or [AnimatorSet] that presents the chip */
fun onSystemEventAnimationBegin(): Animator? { return null }
/** Implement this method to return an [Animator] or [AnimatorSet] that hides the chip */
fun onSystemEventAnimationFinish(hasPersistentDot: Boolean): Animator? { return null }
// Best method name, change my mind
@JvmDefault
fun onSystemStatusAnimationTransitionToPersistentDot(contentDescription: String?): Animator? {
return null
}
@JvmDefault fun onHidePersistentDot(): Animator? { return null }
}
/**
* Animation state IntDef
*/
@Retention(AnnotationRetention.SOURCE)
@IntDef(
value = [
IDLE,
ANIMATION_QUEUED,
ANIMATING_IN,
RUNNING_CHIP_ANIM,
ANIMATING_OUT,
SHOWING_PERSISTENT_DOT
]
)
annotation class SystemAnimationState
/** No animation is in progress */
const val IDLE = 0
/** An animation is queued, and awaiting the debounce period */
const val ANIMATION_QUEUED = 1
/** System is animating out, and chip is animating in */
const val ANIMATING_IN = 2
/** Chip has animated in and is awaiting exit animation, and optionally playing its own animation */
const val RUNNING_CHIP_ANIM = 3
/** Chip is animating away and system is animating back */
const val ANIMATING_OUT = 4
/** Chip has animated away, and the persistent dot is showing */
const val SHOWING_PERSISTENT_DOT = 5
/** Commonly-needed interpolators can go here */
@JvmField val STATUS_BAR_X_MOVE_OUT = PathInterpolator(0.33f, 0f, 0f, 1f)
@JvmField val STATUS_BAR_X_MOVE_IN = PathInterpolator(0f, 0f, 0f, 1f)
/**
* Status chip animation to dot have multiple stages of motion, the _1 and _2 interpolators should
* be used in succession
*/
val STATUS_CHIP_WIDTH_TO_DOT_KEYFRAME_1 = PathInterpolator(0.44f, 0f, 0.25f, 1f)
val STATUS_CHIP_WIDTH_TO_DOT_KEYFRAME_2 = PathInterpolator(0.3f, 0f, 0.26f, 1f)
val STATUS_CHIP_HEIGHT_TO_DOT_KEYFRAME_1 = PathInterpolator(0.4f, 0f, 0.17f, 1f)
val STATUS_CHIP_HEIGHT_TO_DOT_KEYFRAME_2 = PathInterpolator(0.3f, 0f, 0f, 1f)
val STATUS_CHIP_MOVE_TO_DOT = PathInterpolator(0f, 0f, 0.05f, 1f)
private const val TAG = "SystemStatusAnimationScheduler"
private const val DEBOUNCE_DELAY = 100L
/**
* The total time spent on the chip animation is 1500ms, broken up into 3 sections:
* - 500ms to animate the chip in (including animating system icons away)
* - 500ms holding the chip on screen
* - 500ms to animate the chip away (and system icons back)
*
* So DISPLAY_LENGTH should be the sum of the first 2 phases, while the final 500ms accounts for
* the actual animation
*/
private const val DISPLAY_LENGTH = 1000L
private const val MIN_UPTIME: Long = 5 * 1000
private const val DEBUG = false