blob: 6219164343966af8e15f46c6bafa8160d194d98d [file] [log] [blame]
/*
* Copyright (C) 2020 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
import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.animation.ValueAnimator
import android.os.SystemClock
import android.os.Trace
import android.util.IndentingPrintWriter
import android.util.Log
import android.util.MathUtils
import android.view.Choreographer
import android.view.View
import androidx.annotation.VisibleForTesting
import androidx.dynamicanimation.animation.FloatPropertyCompat
import androidx.dynamicanimation.animation.SpringAnimation
import androidx.dynamicanimation.animation.SpringForce
import com.android.systemui.Dumpable
import com.android.systemui.animation.Interpolators
import com.android.systemui.animation.ShadeInterpolation
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dump.DumpManager
import com.android.systemui.plugins.statusbar.StatusBarStateController
import com.android.systemui.statusbar.phone.BiometricUnlockController
import com.android.systemui.statusbar.phone.BiometricUnlockController.MODE_WAKE_AND_UNLOCK
import com.android.systemui.statusbar.phone.DozeParameters
import com.android.systemui.statusbar.phone.ScrimController
import com.android.systemui.statusbar.phone.panelstate.PanelExpansionListener
import com.android.systemui.statusbar.policy.KeyguardStateController
import com.android.systemui.util.WallpaperController
import java.io.PrintWriter
import javax.inject.Inject
import kotlin.math.max
import kotlin.math.sign
/**
* Controller responsible for statusbar window blur.
*/
@SysUISingleton
class NotificationShadeDepthController @Inject constructor(
private val statusBarStateController: StatusBarStateController,
private val blurUtils: BlurUtils,
private val biometricUnlockController: BiometricUnlockController,
private val keyguardStateController: KeyguardStateController,
private val choreographer: Choreographer,
private val wallpaperController: WallpaperController,
private val notificationShadeWindowController: NotificationShadeWindowController,
private val dozeParameters: DozeParameters,
dumpManager: DumpManager
) : PanelExpansionListener, Dumpable {
companion object {
private const val WAKE_UP_ANIMATION_ENABLED = true
private const val VELOCITY_SCALE = 100f
private const val MAX_VELOCITY = 3000f
private const val MIN_VELOCITY = -MAX_VELOCITY
private const val INTERACTION_BLUR_FRACTION = 0.8f
private const val ANIMATION_BLUR_FRACTION = 1f - INTERACTION_BLUR_FRACTION
private const val TAG = "DepthController"
}
lateinit var root: View
private var blurRoot: View? = null
private var keyguardAnimator: Animator? = null
private var notificationAnimator: Animator? = null
private var updateScheduled: Boolean = false
@VisibleForTesting
var shadeExpansion = 0f
private var isClosed: Boolean = true
private var isOpen: Boolean = false
private var isBlurred: Boolean = false
private var listeners = mutableListOf<DepthListener>()
private var prevTracking: Boolean = false
private var prevTimestamp: Long = -1
private var prevShadeDirection = 0
private var prevShadeVelocity = 0f
// Only for dumpsys
private var lastAppliedBlur = 0
// Shade expansion offset that happens when pulling down on a HUN.
var panelPullDownMinFraction = 0f
var shadeAnimation = DepthAnimation()
@VisibleForTesting
var brightnessMirrorSpring = DepthAnimation()
var brightnessMirrorVisible: Boolean = false
set(value) {
field = value
brightnessMirrorSpring.animateTo(if (value) blurUtils.blurRadiusOfRatio(1f).toInt()
else 0)
}
var qsPanelExpansion = 0f
set(value) {
if (value.isNaN()) {
Log.w(TAG, "Invalid qs expansion")
return
}
if (field == value) return
field = value
scheduleUpdate()
}
/**
* How much we're transitioning to the full shade
*/
var transitionToFullShadeProgress = 0f
set(value) {
if (field == value) return
field = value
scheduleUpdate()
}
/**
* When launching an app from the shade, the animations progress should affect how blurry the
* shade is, overriding the expansion amount.
*/
var blursDisabledForAppLaunch: Boolean = false
set(value) {
if (field == value) {
return
}
field = value
scheduleUpdate()
if (shadeExpansion == 0f && shadeAnimation.radius == 0f) {
return
}
// Do not remove blurs when we're re-enabling them
if (!value) {
return
}
shadeAnimation.animateTo(0)
shadeAnimation.finishIfRunning()
}
/**
* We're unlocking, and should not blur as the panel expansion changes.
*/
var blursDisabledForUnlock: Boolean = false
set(value) {
if (field == value) return
field = value
scheduleUpdate()
}
/**
* Force stop blur effect when necessary.
*/
private var scrimsVisible: Boolean = false
set(value) {
if (field == value) return
field = value
scheduleUpdate()
}
/**
* Blur radius of the wake-up animation on this frame.
*/
private var wakeAndUnlockBlurRadius = 0f
set(value) {
if (field == value) return
field = value
scheduleUpdate()
}
/**
* Callback that updates the window blur value and is called only once per frame.
*/
@VisibleForTesting
val updateBlurCallback = Choreographer.FrameCallback {
updateScheduled = false
val animationRadius = MathUtils.constrain(shadeAnimation.radius,
blurUtils.minBlurRadius.toFloat(), blurUtils.maxBlurRadius.toFloat())
val expansionRadius = blurUtils.blurRadiusOfRatio(
ShadeInterpolation.getNotificationScrimAlpha(
if (shouldApplyShadeBlur()) shadeExpansion else 0f))
var combinedBlur = (expansionRadius * INTERACTION_BLUR_FRACTION +
animationRadius * ANIMATION_BLUR_FRACTION)
val qsExpandedRatio = ShadeInterpolation.getNotificationScrimAlpha(qsPanelExpansion) *
shadeExpansion
combinedBlur = max(combinedBlur, blurUtils.blurRadiusOfRatio(qsExpandedRatio))
combinedBlur = max(combinedBlur, blurUtils.blurRadiusOfRatio(transitionToFullShadeProgress))
var shadeRadius = max(combinedBlur, wakeAndUnlockBlurRadius)
if (blursDisabledForAppLaunch || blursDisabledForUnlock) {
shadeRadius = 0f
}
var zoomOut = MathUtils.saturate(blurUtils.ratioOfBlurRadius(shadeRadius))
var blur = shadeRadius.toInt()
// Make blur be 0 if it is necessary to stop blur effect.
if (scrimsVisible) {
blur = 0
zoomOut = 0f
}
if (!blurUtils.supportsBlursOnWindows()) {
blur = 0
}
// Brightness slider removes blur, but doesn't affect zooms
blur = (blur * (1f - brightnessMirrorSpring.ratio)).toInt()
val opaque = scrimsVisible && !blursDisabledForAppLaunch
Trace.traceCounter(Trace.TRACE_TAG_APP, "shade_blur_radius", blur)
blurUtils.applyBlur(blurRoot?.viewRootImpl ?: root.viewRootImpl, blur, opaque)
lastAppliedBlur = blur
wallpaperController.setNotificationShadeZoom(zoomOut)
listeners.forEach {
it.onWallpaperZoomOutChanged(zoomOut)
it.onBlurRadiusChanged(blur)
}
notificationShadeWindowController.setBackgroundBlurRadius(blur)
}
/**
* Animate blurs when unlocking.
*/
private val keyguardStateCallback = object : KeyguardStateController.Callback {
override fun onKeyguardFadingAwayChanged() {
if (!keyguardStateController.isKeyguardFadingAway ||
biometricUnlockController.mode != MODE_WAKE_AND_UNLOCK) {
return
}
keyguardAnimator?.cancel()
keyguardAnimator = ValueAnimator.ofFloat(1f, 0f).apply {
// keyguardStateController.keyguardFadingAwayDuration might be zero when unlock by
// fingerprint due to there is no window container, see AppTransition#goodToGo.
// We use DozeParameters.wallpaperFadeOutDuration as an alternative.
duration = dozeParameters.wallpaperFadeOutDuration
startDelay = keyguardStateController.keyguardFadingAwayDelay
interpolator = Interpolators.FAST_OUT_SLOW_IN
addUpdateListener { animation: ValueAnimator ->
wakeAndUnlockBlurRadius =
blurUtils.blurRadiusOfRatio(animation.animatedValue as Float)
}
addListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator?) {
keyguardAnimator = null
wakeAndUnlockBlurRadius = 0f
scheduleUpdate()
}
})
start()
}
}
override fun onKeyguardShowingChanged() {
if (keyguardStateController.isShowing) {
keyguardAnimator?.cancel()
notificationAnimator?.cancel()
}
}
}
private val statusBarStateCallback = object : StatusBarStateController.StateListener {
override fun onStateChanged(newState: Int) {
updateShadeAnimationBlur(
shadeExpansion, prevTracking, prevShadeVelocity, prevShadeDirection)
scheduleUpdate()
}
override fun onDozingChanged(isDozing: Boolean) {
if (isDozing) {
shadeAnimation.finishIfRunning()
brightnessMirrorSpring.finishIfRunning()
}
}
override fun onDozeAmountChanged(linear: Float, eased: Float) {
wakeAndUnlockBlurRadius = blurUtils.blurRadiusOfRatio(eased)
scheduleUpdate()
}
}
init {
dumpManager.registerDumpable(javaClass.name, this)
if (WAKE_UP_ANIMATION_ENABLED) {
keyguardStateController.addCallback(keyguardStateCallback)
}
statusBarStateController.addCallback(statusBarStateCallback)
notificationShadeWindowController.setScrimsVisibilityListener {
// Stop blur effect when scrims is opaque to avoid unnecessary GPU composition.
visibility -> scrimsVisible = visibility == ScrimController.OPAQUE
}
shadeAnimation.setStiffness(SpringForce.STIFFNESS_LOW)
shadeAnimation.setDampingRatio(SpringForce.DAMPING_RATIO_NO_BOUNCY)
}
fun addListener(listener: DepthListener) {
listeners.add(listener)
}
fun removeListener(listener: DepthListener) {
listeners.remove(listener)
}
/**
* Update blurs when pulling down the shade
*/
override fun onPanelExpansionChanged(rawFraction: Float, expanded: Boolean, tracking: Boolean) {
val timestamp = SystemClock.elapsedRealtimeNanos()
val expansion = MathUtils.saturate(
(rawFraction - panelPullDownMinFraction) / (1f - panelPullDownMinFraction))
if (shadeExpansion == expansion && prevTracking == tracking) {
prevTimestamp = timestamp
return
}
var deltaTime = 1f
if (prevTimestamp < 0) {
prevTimestamp = timestamp
} else {
deltaTime = MathUtils.constrain(
((timestamp - prevTimestamp) / 1E9).toFloat(), 0.00001f, 1f)
}
val diff = expansion - shadeExpansion
val shadeDirection = sign(diff).toInt()
val shadeVelocity = MathUtils.constrain(
VELOCITY_SCALE * diff / deltaTime, MIN_VELOCITY, MAX_VELOCITY)
updateShadeAnimationBlur(expansion, tracking, shadeVelocity, shadeDirection)
prevShadeDirection = shadeDirection
prevShadeVelocity = shadeVelocity
shadeExpansion = expansion
prevTracking = tracking
prevTimestamp = timestamp
scheduleUpdate()
}
private fun updateShadeAnimationBlur(
expansion: Float,
tracking: Boolean,
velocity: Float,
direction: Int
) {
if (shouldApplyShadeBlur()) {
if (expansion > 0f) {
// Blur view if user starts animating in the shade.
if (isClosed) {
animateBlur(true, velocity)
isClosed = false
}
// If we were blurring out and the user stopped the animation, blur view.
if (tracking && !isBlurred) {
animateBlur(true, 0f)
}
// If shade is being closed and the user isn't interacting with it, un-blur.
if (!tracking && direction < 0 && isBlurred) {
animateBlur(false, velocity)
}
if (expansion == 1f) {
if (!isOpen) {
isOpen = true
// If shade is open and view is not blurred, blur.
if (!isBlurred) {
animateBlur(true, velocity)
}
}
} else {
isOpen = false
}
// Automatic animation when the user closes the shade.
} else if (!isClosed) {
isClosed = true
// If shade is closed and view is not blurred, blur.
if (isBlurred) {
animateBlur(false, velocity)
}
}
} else {
animateBlur(false, 0f)
isClosed = true
isOpen = false
}
}
private fun animateBlur(blur: Boolean, velocity: Float) {
isBlurred = blur
val targetBlurNormalized = if (blur && shouldApplyShadeBlur()) {
1f
} else {
0f
}
shadeAnimation.setStartVelocity(velocity)
shadeAnimation.animateTo(blurUtils.blurRadiusOfRatio(targetBlurNormalized).toInt())
}
private fun scheduleUpdate(viewToBlur: View? = null) {
if (updateScheduled) {
return
}
updateScheduled = true
blurRoot = viewToBlur
choreographer.postFrameCallback(updateBlurCallback)
}
/**
* Should blur be applied to the shade currently. This is mainly used to make sure that
* on the lockscreen, the wallpaper isn't blurred.
*/
private fun shouldApplyShadeBlur(): Boolean {
val state = statusBarStateController.state
return (state == StatusBarState.SHADE || state == StatusBarState.SHADE_LOCKED) &&
!keyguardStateController.isKeyguardFadingAway
}
override fun dump(pw: PrintWriter, args: Array<out String>) {
IndentingPrintWriter(pw, " ").let {
it.println("StatusBarWindowBlurController:")
it.increaseIndent()
it.println("shadeExpansion: $shadeExpansion")
it.println("shouldApplyShadeBlur: ${shouldApplyShadeBlur()}")
it.println("shadeAnimation: ${shadeAnimation.radius}")
it.println("brightnessMirrorRadius: ${brightnessMirrorSpring.radius}")
it.println("wakeAndUnlockBlur: $wakeAndUnlockBlurRadius")
it.println("blursDisabledForAppLaunch: $blursDisabledForAppLaunch")
it.println("qsPanelExpansion: $qsPanelExpansion")
it.println("transitionToFullShadeProgress: $transitionToFullShadeProgress")
it.println("lastAppliedBlur: $lastAppliedBlur")
}
}
/**
* Animation helper that smoothly animates the depth using a spring and deals with frame
* invalidation.
*/
inner class DepthAnimation() {
/**
* Blur radius visible on the UI, in pixels.
*/
var radius = 0f
/**
* Depth ratio of the current blur radius.
*/
val ratio
get() = blurUtils.ratioOfBlurRadius(radius)
/**
* Radius that we're animating to.
*/
private var pendingRadius = -1
/**
* View on {@link Surface} that wants depth.
*/
private var view: View? = null
private var springAnimation = SpringAnimation(this, object :
FloatPropertyCompat<DepthAnimation>("blurRadius") {
override fun setValue(rect: DepthAnimation?, value: Float) {
radius = value
scheduleUpdate(view)
}
override fun getValue(rect: DepthAnimation?): Float {
return radius
}
})
init {
springAnimation.spring = SpringForce(0.0f)
springAnimation.spring.dampingRatio = SpringForce.DAMPING_RATIO_NO_BOUNCY
springAnimation.spring.stiffness = SpringForce.STIFFNESS_HIGH
springAnimation.addEndListener { _, _, _, _ -> pendingRadius = -1 }
}
fun animateTo(newRadius: Int, viewToBlur: View? = null) {
if (pendingRadius == newRadius && view == viewToBlur) {
return
}
view = viewToBlur
pendingRadius = newRadius
springAnimation.animateToFinalPosition(newRadius.toFloat())
}
fun finishIfRunning() {
if (springAnimation.isRunning) {
springAnimation.skipToEnd()
}
}
fun setStiffness(stiffness: Float) {
springAnimation.spring.stiffness = stiffness
}
fun setDampingRatio(dampingRation: Float) {
springAnimation.spring.dampingRatio = dampingRation
}
fun setStartVelocity(velocity: Float) {
springAnimation.setStartVelocity(velocity)
}
}
/**
* Invoked when changes are needed in z-space
*/
interface DepthListener {
/**
* Current wallpaper zoom out, where 0 is the closest, and 1 the farthest
*/
fun onWallpaperZoomOutChanged(zoomOut: Float)
@JvmDefault
fun onBlurRadiusChanged(blurRadius: Int) {}
}
}