diff options
14 files changed, 1034 insertions, 178 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java index ae669dcf80fa..7388c27ed313 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java @@ -254,6 +254,7 @@ public final class NotificationPanelViewController implements Dumpable { public static final float FLING_SPEED_UP_FACTOR = 0.6f; public static final float FLING_CLOSING_MAX_LENGTH_SECONDS = 0.6f; public static final float FLING_CLOSING_SPEED_UP_FACTOR = 0.6f; + public static final int WAKEUP_ANIMATION_DELAY_MS = 250; private static final boolean DEBUG_LOGCAT = Compile.IS_DEBUG && Log.isLoggable(TAG, Log.DEBUG); private static final boolean SPEW_LOGCAT = Compile.IS_DEBUG && Log.isLoggable(TAG, Log.VERBOSE); private static final boolean DEBUG_DRAWABLE = false; @@ -276,6 +277,7 @@ public final class NotificationPanelViewController implements Dumpable { private static final int NO_FIXED_DURATION = -1; private static final long SHADE_OPEN_SPRING_OUT_DURATION = 350L; private static final long SHADE_OPEN_SPRING_BACK_DURATION = 400L; + /** * The factor of the usual high velocity that is needed in order to reach the maximum overshoot * when flinging. A low value will make it that most flings will reach the maximum overshoot. @@ -588,6 +590,12 @@ public final class NotificationPanelViewController implements Dumpable { private boolean mGestureWaitForTouchSlop; private boolean mIgnoreXTouchSlop; private boolean mExpandLatencyTracking; + /** + * Whether we're waking up and will play the delayed doze animation in + * {@link NotificationWakeUpCoordinator}. If so, we'll want to keep the clock centered until the + * delayed doze animation starts. + */ + private boolean mWillPlayDelayedDozeAmountAnimation = false; private final DreamingToLockscreenTransitionViewModel mDreamingToLockscreenTransitionViewModel; private final OccludedToLockscreenTransitionViewModel mOccludedToLockscreenTransitionViewModel; private final LockscreenToDreamingTransitionViewModel mLockscreenToDreamingTransitionViewModel; @@ -1044,6 +1052,12 @@ public final class NotificationPanelViewController implements Dumpable { requestScrollerTopPaddingUpdate(false /* animate */); } } + + @Override + public void onDelayedDozeAmountAnimationRunning(boolean running) { + // On running OR finished, the animation is no longer waiting to play + setWillPlayDelayedDozeAmountAnimation(false); + } }); mView.setRtlChangeListener(layoutDirection -> { @@ -1640,11 +1654,28 @@ public final class NotificationPanelViewController implements Dumpable { // Pulsing notification appears on the right. Move clock left to avoid overlap. return false; } + if (mWillPlayDelayedDozeAmountAnimation) { + return true; + } // "Visible" notifications are actually not visible on AOD (unless pulsing), so it is safe // to center the clock without overlap. return isOnAod(); } + /** + * Notify us that {@link NotificationWakeUpCoordinator} is going to play the doze wakeup + * animation after a delay. If so, we'll keep the clock centered until that animation starts. + */ + public void setWillPlayDelayedDozeAmountAnimation(boolean willPlay) { + if (mWillPlayDelayedDozeAmountAnimation == willPlay) return; + + mWillPlayDelayedDozeAmountAnimation = willPlay; + mWakeUpCoordinator.logDelayingClockWakeUpAnimation(willPlay); + + // Once changing this value, see if we should move the clock. + positionClockAndNotifications(); + } + private boolean isOnAod() { return mDozing && mDozeParameters.getAlwaysOn(); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinator.kt index 8874f59d6c17..20af6cadeed5 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinator.kt @@ -16,14 +16,17 @@ package com.android.systemui.statusbar.notification -import android.animation.ObjectAnimator import android.util.FloatProperty +import android.view.animation.Interpolator import androidx.annotation.VisibleForTesting +import androidx.core.animation.ObjectAnimator import com.android.systemui.Dumpable import com.android.systemui.animation.Interpolators +import com.android.systemui.animation.InterpolatorsAndroidX import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dump.DumpManager import com.android.systemui.plugins.statusbar.StatusBarStateController +import com.android.systemui.shade.NotificationPanelViewController.WAKEUP_ANIMATION_DELAY_MS import com.android.systemui.shade.ShadeExpansionChangeEvent import com.android.systemui.shade.ShadeExpansionListener import com.android.systemui.statusbar.StatusBarState @@ -36,12 +39,17 @@ import com.android.systemui.statusbar.phone.KeyguardBypassController.OnBypassSta import com.android.systemui.statusbar.phone.ScreenOffAnimationController import com.android.systemui.statusbar.policy.HeadsUpManager import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener +import com.android.systemui.util.doOnEnd +import com.android.systemui.util.doOnStart import java.io.PrintWriter import javax.inject.Inject +import kotlin.math.max import kotlin.math.min @SysUISingleton -class NotificationWakeUpCoordinator @Inject constructor( +class NotificationWakeUpCoordinator +@Inject +constructor( dumpManager: DumpManager, private val mHeadsUpManager: HeadsUpManager, private val statusBarStateController: StatusBarStateController, @@ -49,27 +57,25 @@ class NotificationWakeUpCoordinator @Inject constructor( private val dozeParameters: DozeParameters, private val screenOffAnimationController: ScreenOffAnimationController, private val logger: NotificationWakeUpCoordinatorLogger, -) : OnHeadsUpChangedListener, StatusBarStateController.StateListener, ShadeExpansionListener, +) : + OnHeadsUpChangedListener, + StatusBarStateController.StateListener, + ShadeExpansionListener, Dumpable { - - private val mNotificationVisibility = object : FloatProperty<NotificationWakeUpCoordinator>( - "notificationVisibility") { - - override fun setValue(coordinator: NotificationWakeUpCoordinator, value: Float) { - coordinator.setVisibilityAmount(value) - } - - override fun get(coordinator: NotificationWakeUpCoordinator): Float? { - return coordinator.mLinearVisibilityAmount - } - } private lateinit var mStackScrollerController: NotificationStackScrollLayoutController private var mVisibilityInterpolator = Interpolators.FAST_OUT_SLOW_IN_REVERSE - private var mLinearDozeAmount: Float = 0.0f - private var mDozeAmount: Float = 0.0f - private var mDozeAmountSource: String = "init" - private var mNotifsHiddenByDozeAmountOverride: Boolean = false + private var inputLinearDozeAmount: Float = 0.0f + private var inputEasedDozeAmount: Float = 0.0f + private var delayedDozeAmountOverride: Float = 0.0f + private var delayedDozeAmountAnimator: ObjectAnimator? = null + /** Valid values: {1f, 0f, null} null => use input */ + private var hardDozeAmountOverride: Float? = null + private var hardDozeAmountOverrideSource: String = "n/a" + private var outputLinearDozeAmount: Float = 0.0f + private var outputEasedDozeAmount: Float = 0.0f + @VisibleForTesting val dozeAmountInterpolator: Interpolator = Interpolators.FAST_OUT_SLOW_IN + private var mNotificationVisibleAmount = 0.0f private var mNotificationsVisible = false private var mNotificationsVisibleForExpansion = false @@ -84,27 +90,32 @@ class NotificationWakeUpCoordinator @Inject constructor( var fullyAwake: Boolean = false var wakingUp = false - set(value) { + private set(value) { field = value willWakeUp = false if (value) { - if (mNotificationsVisible && !mNotificationsVisibleForExpansion && - !bypassController.bypassEnabled) { + if ( + mNotificationsVisible && + !mNotificationsVisibleForExpansion && + !bypassController.bypassEnabled + ) { // We're waking up while pulsing, let's make sure the animation looks nice mStackScrollerController.wakeUpFromPulse() } if (bypassController.bypassEnabled && !mNotificationsVisible) { // Let's make sure our huns become visible once we are waking up in case // they were blocked by the proximity sensor - updateNotificationVisibility(animate = shouldAnimateVisibility(), - increaseSpeed = false) + updateNotificationVisibility( + animate = shouldAnimateVisibility(), + increaseSpeed = false + ) } } } var willWakeUp = false set(value) { - if (!value || mDozeAmount != 0.0f) { + if (!value || outputLinearDozeAmount != 0.0f) { field = value } } @@ -118,8 +129,10 @@ class NotificationWakeUpCoordinator @Inject constructor( // Only when setting pulsing to true we want an immediate update, since we get // this already when the doze service finishes which is usually before we get // the waking up callback - updateNotificationVisibility(animate = shouldAnimateVisibility(), - increaseSpeed = false) + updateNotificationVisibility( + animate = shouldAnimateVisibility(), + increaseSpeed = false + ) } } @@ -133,17 +146,17 @@ class NotificationWakeUpCoordinator @Inject constructor( } } - /** - * True if we can show pulsing heads up notifications - */ + /** True if we can show pulsing heads up notifications */ var canShowPulsingHuns: Boolean = false private set get() { var canShow = pulsing if (bypassController.bypassEnabled) { // We also allow pulsing on the lock screen! - canShow = canShow || (wakingUp || willWakeUp || fullyAwake) && - statusBarStateController.state == StatusBarState.KEYGUARD + canShow = + canShow || + (wakingUp || willWakeUp || fullyAwake) && + statusBarStateController.state == StatusBarState.KEYGUARD // We want to hide the notifications when collapsed too much if (collapsedEnoughToHide) { canShow = false @@ -152,30 +165,38 @@ class NotificationWakeUpCoordinator @Inject constructor( return canShow } - private val bypassStateChangedListener = object : OnBypassStateChangedListener { - override fun onBypassStateChanged(isEnabled: Boolean) { - // When the bypass state changes, we have to check whether we should re-show the - // notifications by clearing the doze amount override which hides them. - maybeClearDozeAmountOverrideHidingNotifs() + private val bypassStateChangedListener = + object : OnBypassStateChangedListener { + override fun onBypassStateChanged(isEnabled: Boolean) { + // When the bypass state changes, we have to check whether we should re-show the + // notifications by clearing the doze amount override which hides them. + maybeClearHardDozeAmountOverrideHidingNotifs() + } } - } init { dumpManager.registerDumpable(this) mHeadsUpManager.addListener(this) statusBarStateController.addCallback(this) bypassController.registerOnBypassStateChangedListener(bypassStateChangedListener) - addListener(object : WakeUpListener { - override fun onFullyHiddenChanged(isFullyHidden: Boolean) { - if (isFullyHidden && mNotificationsVisibleForExpansion) { - // When the notification becomes fully invisible, let's make sure our expansion - // flag also changes. This can happen if the bouncer shows when dragging down - // and then the screen turning off, where we don't reset this state. - setNotificationsVisibleForExpansion(visible = false, animate = false, - increaseSpeed = false) + addListener( + object : WakeUpListener { + override fun onFullyHiddenChanged(isFullyHidden: Boolean) { + if (isFullyHidden && mNotificationsVisibleForExpansion) { + // When the notification becomes fully invisible, let's make sure our + // expansion + // flag also changes. This can happen if the bouncer shows when dragging + // down + // and then the screen turning off, where we don't reset this state. + setNotificationsVisibleForExpansion( + visible = false, + animate = false, + increaseSpeed = false + ) + } } } - }) + ) } fun setStackScroller(stackScrollerController: NotificationStackScrollLayoutController) { @@ -221,15 +242,17 @@ class NotificationWakeUpCoordinator @Inject constructor( wakeUpListeners.remove(listener) } - private fun updateNotificationVisibility( - animate: Boolean, - increaseSpeed: Boolean - ) { + private fun updateNotificationVisibility(animate: Boolean, increaseSpeed: Boolean) { // TODO: handle Lockscreen wakeup for bypass when we're not pulsing anymore var visible = mNotificationsVisibleForExpansion || mHeadsUpManager.hasNotifications() visible = visible && canShowPulsingHuns - if (!visible && mNotificationsVisible && (wakingUp || willWakeUp) && mDozeAmount != 0.0f) { + if ( + !visible && + mNotificationsVisible && + (wakingUp || willWakeUp) && + outputLinearDozeAmount != 0.0f + ) { // let's not make notifications invisible while waking up, otherwise the animation // is strange return @@ -257,7 +280,9 @@ class NotificationWakeUpCoordinator @Inject constructor( override fun onDozeAmountChanged(linear: Float, eased: Float) { logger.logOnDozeAmountChanged(linear = linear, eased = eased) - if (overrideDozeAmountIfAnimatingScreenOff(linear)) { + inputLinearDozeAmount = linear + inputEasedDozeAmount = eased + if (overrideDozeAmountIfAnimatingScreenOff()) { return } @@ -265,35 +290,111 @@ class NotificationWakeUpCoordinator @Inject constructor( return } - if (linear != 1.0f && linear != 0.0f && - (mLinearDozeAmount == 0.0f || mLinearDozeAmount == 1.0f)) { - // Let's notify the scroller that an animation started - notifyAnimationStart(mLinearDozeAmount == 1.0f) + if (clearHardDozeAmountOverride()) { + return } - setDozeAmount(linear, eased, source = "StatusBar") + + updateDozeAmount() } - fun setDozeAmount( - linear: Float, - eased: Float, - source: String, - hidesNotifsByOverride: Boolean = false - ) { - val changed = linear != mLinearDozeAmount - logger.logSetDozeAmount(linear, eased, source, statusBarStateController.state, changed) - mLinearDozeAmount = linear - mDozeAmount = eased - mDozeAmountSource = source - mNotifsHiddenByDozeAmountOverride = hidesNotifsByOverride - mStackScrollerController.setDozeAmount(mDozeAmount) + private fun setHardDozeAmountOverride(dozing: Boolean, source: String) { + logger.logSetDozeAmountOverride(dozing = dozing, source = source) + hardDozeAmountOverride = if (dozing) 1f else 0f + hardDozeAmountOverrideSource = source + updateDozeAmount() + } + + private fun clearHardDozeAmountOverride(): Boolean { + if (hardDozeAmountOverride == null) return false + hardDozeAmountOverride = null + hardDozeAmountOverrideSource = "Cleared: $hardDozeAmountOverrideSource" + updateDozeAmount() + return true + } + + private fun updateDozeAmount() { + // Calculate new doze amount (linear) + val newOutputLinearDozeAmount = + hardDozeAmountOverride ?: max(inputLinearDozeAmount, delayedDozeAmountOverride) + val changed = outputLinearDozeAmount != newOutputLinearDozeAmount + + // notify when the animation is starting + if ( + newOutputLinearDozeAmount != 1.0f && + newOutputLinearDozeAmount != 0.0f && + (outputLinearDozeAmount == 0.0f || outputLinearDozeAmount == 1.0f) + ) { + // Let's notify the scroller that an animation started + notifyAnimationStart(outputLinearDozeAmount == 1.0f) + } + + // Update output doze amount + outputLinearDozeAmount = newOutputLinearDozeAmount + outputEasedDozeAmount = dozeAmountInterpolator.getInterpolation(outputLinearDozeAmount) + logger.logUpdateDozeAmount( + inputLinear = inputLinearDozeAmount, + delayLinear = delayedDozeAmountOverride, + hardOverride = hardDozeAmountOverride, + outputLinear = outputLinearDozeAmount, + state = statusBarStateController.state, + changed = changed + ) + mStackScrollerController.setDozeAmount(outputEasedDozeAmount) updateHideAmount() - if (changed && linear == 0.0f) { + if (changed && outputLinearDozeAmount == 0.0f) { setNotificationsVisible(visible = false, animate = false, increaseSpeed = false) - setNotificationsVisibleForExpansion(visible = false, animate = false, - increaseSpeed = false) + setNotificationsVisibleForExpansion( + visible = false, + animate = false, + increaseSpeed = false + ) + } + } + + /** + * Notifies the wakeup coordinator that we're waking up. + * + * [requestDelayedAnimation] is used to request that we delay the start of the wakeup animation + * in order to wait for a potential fingerprint authentication to arrive, since unlocking during + * the wakeup animation looks chaotic. + * + * If called with [wakingUp] and [requestDelayedAnimation] both `true`, the [WakeUpListener]s + * are guaranteed to receive at least one [WakeUpListener.onDelayedDozeAmountAnimationRunning] + * call with `false` at some point in the near future. A call with `true` before that will + * happen if the animation is not already running. + */ + fun setWakingUp( + wakingUp: Boolean, + requestDelayedAnimation: Boolean, + ) { + logger.logSetWakingUp(wakingUp, requestDelayedAnimation) + this.wakingUp = wakingUp + if (wakingUp && requestDelayedAnimation) { + scheduleDelayedDozeAmountAnimation() } } + private fun scheduleDelayedDozeAmountAnimation() { + val alreadyRunning = delayedDozeAmountAnimator != null + logger.logStartDelayedDozeAmountAnimation(alreadyRunning) + if (alreadyRunning) return + delayedDozeAmount.setValue(this, 1.0f) + delayedDozeAmountAnimator = + ObjectAnimator.ofFloat(this, delayedDozeAmount, 0.0f).apply { + interpolator = InterpolatorsAndroidX.LINEAR + duration = StackStateAnimator.ANIMATION_DURATION_WAKEUP.toLong() + startDelay = WAKEUP_ANIMATION_DELAY_MS.toLong() + doOnStart { + wakeUpListeners.forEach { it.onDelayedDozeAmountAnimationRunning(true) } + } + doOnEnd { + delayedDozeAmountAnimator = null + wakeUpListeners.forEach { it.onDelayedDozeAmountAnimationRunning(false) } + } + start() + } + } + override fun onStateChanged(newState: Int) { logger.logOnStateChanged(newState = newState, storedState = state) if (state == StatusBarState.SHADE && newState == StatusBarState.SHADE) { @@ -302,12 +403,15 @@ class NotificationWakeUpCoordinator @Inject constructor( // undefined state, so it's an indication that we should do state cleanup. We override // the doze amount to 0f (not dozing) so that the notifications are no longer hidden. // See: UnlockedScreenOffAnimationController.onFinishedWakingUp() - setDozeAmount(0f, 0f, source = "Override: Shade->Shade (lock cancelled by unlock)") + setHardDozeAmountOverride( + dozing = false, + source = "Override: Shade->Shade (lock cancelled by unlock)" + ) this.state = newState return } - if (overrideDozeAmountIfAnimatingScreenOff(mLinearDozeAmount)) { + if (overrideDozeAmountIfAnimatingScreenOff()) { this.state = newState return } @@ -317,7 +421,7 @@ class NotificationWakeUpCoordinator @Inject constructor( return } - maybeClearDozeAmountOverrideHidingNotifs() + maybeClearHardDozeAmountOverrideHidingNotifs() this.state = newState } @@ -340,15 +444,14 @@ class NotificationWakeUpCoordinator @Inject constructor( /** * @return Whether the doze amount was overridden because bypass is enabled. If true, the - * original doze amount should be ignored. + * original doze amount should be ignored. */ private fun overrideDozeAmountIfBypass(): Boolean { if (bypassController.bypassEnabled) { if (statusBarStateController.state == StatusBarState.KEYGUARD) { - setDozeAmount(1f, 1f, source = "Override: bypass (keyguard)", - hidesNotifsByOverride = true) + setHardDozeAmountOverride(dozing = true, source = "Override: bypass (keyguard)") } else { - setDozeAmount(0f, 0f, source = "Override: bypass (shade)") + setHardDozeAmountOverride(dozing = false, source = "Override: bypass (shade)") } return true } @@ -362,26 +465,28 @@ class NotificationWakeUpCoordinator @Inject constructor( * This fixes bugs where the bypass state changing could result in stale overrides, hiding * notifications either on the inside screen or even after unlock. */ - private fun maybeClearDozeAmountOverrideHidingNotifs() { - if (mNotifsHiddenByDozeAmountOverride) { + private fun maybeClearHardDozeAmountOverrideHidingNotifs() { + if (hardDozeAmountOverride == 1f) { val onKeyguard = statusBarStateController.state == StatusBarState.KEYGUARD val dozing = statusBarStateController.isDozing val bypass = bypassController.bypassEnabled val animating = - screenOffAnimationController.overrideNotificationsFullyDozingOnKeyguard() + screenOffAnimationController.overrideNotificationsFullyDozingOnKeyguard() // Overrides are set by [overrideDozeAmountIfAnimatingScreenOff] and // [overrideDozeAmountIfBypass] based on 'animating' and 'bypass' respectively, so only // clear the override if both those conditions are cleared. But also require either // !dozing or !onKeyguard because those conditions should indicate that we intend // notifications to be visible, and thus it is safe to unhide them. val willRemove = (!onKeyguard || !dozing) && !bypass && !animating - logger.logMaybeClearDozeAmountOverrideHidingNotifs( - willRemove = willRemove, - onKeyguard = onKeyguard, dozing = dozing, - bypass = bypass, animating = animating, + logger.logMaybeClearHardDozeAmountOverrideHidingNotifs( + willRemove = willRemove, + onKeyguard = onKeyguard, + dozing = dozing, + bypass = bypass, + animating = animating, ) if (willRemove) { - setDozeAmount(0f, 0f, source = "Removed: $mDozeAmountSource") + clearHardDozeAmountOverride() } } } @@ -392,12 +497,11 @@ class NotificationWakeUpCoordinator @Inject constructor( * off and dozeAmount goes from 1f to 0f. * * @return Whether the doze amount was overridden because we are playing the screen off - * animation. If true, the original doze amount should be ignored. + * animation. If true, the original doze amount should be ignored. */ - private fun overrideDozeAmountIfAnimatingScreenOff(linearDozeAmount: Float): Boolean { + private fun overrideDozeAmountIfAnimatingScreenOff(): Boolean { if (screenOffAnimationController.overrideNotificationsFullyDozingOnKeyguard()) { - setDozeAmount(1f, 1f, source = "Override: animating screen off", - hidesNotifsByOverride = true) + setHardDozeAmountOverride(dozing = true, source = "Override: animating screen off") return true } @@ -406,41 +510,41 @@ class NotificationWakeUpCoordinator @Inject constructor( private fun startVisibilityAnimation(increaseSpeed: Boolean) { if (mNotificationVisibleAmount == 0f || mNotificationVisibleAmount == 1f) { - mVisibilityInterpolator = if (mNotificationsVisible) - Interpolators.TOUCH_RESPONSE - else - Interpolators.FAST_OUT_SLOW_IN_REVERSE + mVisibilityInterpolator = + if (mNotificationsVisible) Interpolators.TOUCH_RESPONSE + else Interpolators.FAST_OUT_SLOW_IN_REVERSE } val target = if (mNotificationsVisible) 1.0f else 0.0f - val visibilityAnimator = ObjectAnimator.ofFloat(this, mNotificationVisibility, target) - visibilityAnimator.setInterpolator(Interpolators.LINEAR) + val visibilityAnimator = ObjectAnimator.ofFloat(this, notificationVisibility, target) + visibilityAnimator.interpolator = InterpolatorsAndroidX.LINEAR var duration = StackStateAnimator.ANIMATION_DURATION_WAKEUP.toLong() if (increaseSpeed) { duration = (duration.toFloat() / 1.5F).toLong() } - visibilityAnimator.setDuration(duration) + visibilityAnimator.duration = duration visibilityAnimator.start() mVisibilityAnimator = visibilityAnimator } private fun setVisibilityAmount(visibilityAmount: Float) { + logger.logSetVisibilityAmount(visibilityAmount) mLinearVisibilityAmount = visibilityAmount - mVisibilityAmount = mVisibilityInterpolator.getInterpolation( - visibilityAmount) + mVisibilityAmount = mVisibilityInterpolator.getInterpolation(visibilityAmount) handleAnimationFinished() updateHideAmount() } private fun handleAnimationFinished() { - if (mLinearDozeAmount == 0.0f || mLinearVisibilityAmount == 0.0f) { + if (outputLinearDozeAmount == 0.0f || mLinearVisibilityAmount == 0.0f) { mEntrySetToClearWhenFinished.forEach { it.setHeadsUpAnimatingAway(false) } mEntrySetToClearWhenFinished.clear() } } private fun updateHideAmount() { - val linearAmount = min(1.0f - mLinearVisibilityAmount, mLinearDozeAmount) - val amount = min(1.0f - mVisibilityAmount, mDozeAmount) + val linearAmount = min(1.0f - mLinearVisibilityAmount, outputLinearDozeAmount) + val amount = min(1.0f - mVisibilityAmount, outputEasedDozeAmount) + logger.logSetHideAmount(linearAmount) mStackScrollerController.setHideAmount(linearAmount, amount) notificationsFullyHidden = linearAmount == 1.0f } @@ -458,7 +562,7 @@ class NotificationWakeUpCoordinator @Inject constructor( override fun onHeadsUpStateChanged(entry: NotificationEntry, isHeadsUp: Boolean) { var animate = shouldAnimateVisibility() if (!isHeadsUp) { - if (mLinearDozeAmount != 0.0f && mLinearVisibilityAmount != 0.0f) { + if (outputLinearDozeAmount != 0.0f && mLinearVisibilityAmount != 0.0f) { if (entry.isRowDismissed) { // if we animate, we see the shelf briefly visible. Instead we fully animate // the notification and its background out @@ -477,13 +581,16 @@ class NotificationWakeUpCoordinator @Inject constructor( } private fun shouldAnimateVisibility() = - dozeParameters.alwaysOn && !dozeParameters.displayNeedsBlanking + dozeParameters.alwaysOn && !dozeParameters.displayNeedsBlanking override fun dump(pw: PrintWriter, args: Array<out String>) { - pw.println("mLinearDozeAmount: $mLinearDozeAmount") - pw.println("mDozeAmount: $mDozeAmount") - pw.println("mDozeAmountSource: $mDozeAmountSource") - pw.println("mNotifsHiddenByDozeAmountOverride: $mNotifsHiddenByDozeAmountOverride") + pw.println("inputLinearDozeAmount: $inputLinearDozeAmount") + pw.println("inputEasedDozeAmount: $inputEasedDozeAmount") + pw.println("delayedDozeAmountOverride: $delayedDozeAmountOverride") + pw.println("hardDozeAmountOverride: $hardDozeAmountOverride") + pw.println("hardDozeAmountOverrideSource: $hardDozeAmountOverrideSource") + pw.println("outputLinearDozeAmount: $outputLinearDozeAmount") + pw.println("outputEasedDozeAmount: $outputEasedDozeAmount") pw.println("mNotificationVisibleAmount: $mNotificationVisibleAmount") pw.println("mNotificationsVisible: $mNotificationsVisible") pw.println("mNotificationsVisibleForExpansion: $mNotificationsVisibleForExpansion") @@ -500,16 +607,53 @@ class NotificationWakeUpCoordinator @Inject constructor( pw.println("canShowPulsingHuns: $canShowPulsingHuns") } + fun logDelayingClockWakeUpAnimation(delayingAnimation: Boolean) { + logger.logDelayingClockWakeUpAnimation(delayingAnimation) + } + interface WakeUpListener { - /** - * Called whenever the notifications are fully hidden or shown - */ + /** Called whenever the notifications are fully hidden or shown */ @JvmDefault fun onFullyHiddenChanged(isFullyHidden: Boolean) {} /** * Called whenever the pulseExpansion changes + * * @param expandingChanged if the user has started or stopped expanding */ @JvmDefault fun onPulseExpansionChanged(expandingChanged: Boolean) {} + + /** + * Called when the animator started by [scheduleDelayedDozeAmountAnimation] begins running + * after the start delay, or after it ends/is cancelled. + */ + @JvmDefault fun onDelayedDozeAmountAnimationRunning(running: Boolean) {} + } + + companion object { + private val notificationVisibility = + object : FloatProperty<NotificationWakeUpCoordinator>("notificationVisibility") { + + override fun setValue(coordinator: NotificationWakeUpCoordinator, value: Float) { + coordinator.setVisibilityAmount(value) + } + + override fun get(coordinator: NotificationWakeUpCoordinator): Float { + return coordinator.mLinearVisibilityAmount + } + } + + private val delayedDozeAmount = + object : FloatProperty<NotificationWakeUpCoordinator>("delayedDozeAmount") { + + override fun setValue(coordinator: NotificationWakeUpCoordinator, value: Float) { + coordinator.delayedDozeAmountOverride = value + coordinator.logger.logSetDelayDozeAmountOverride(value) + coordinator.updateDozeAmount() + } + + override fun get(coordinator: NotificationWakeUpCoordinator): Float { + return coordinator.delayedDozeAmountOverride + } + } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinatorLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinatorLogger.kt index 88d9ffcdcf3e..dd3c2a9df3e5 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinatorLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinatorLogger.kt @@ -22,50 +22,75 @@ import javax.inject.Inject class NotificationWakeUpCoordinatorLogger @Inject constructor(@NotificationLockscreenLog private val buffer: LogBuffer) { - private var lastSetDozeAmountLogWasFractional = false + private var allowThrottle = true + private var lastSetDozeAmountLogInputWasFractional = false + private var lastSetDozeAmountLogDelayWasFractional = false private var lastSetDozeAmountLogState = -1 - private var lastSetDozeAmountLogSource = "undefined" + private var lastSetHardOverride: Float? = null private var lastOnDozeAmountChangedLogWasFractional = false + private var lastSetDelayDozeAmountOverrideLogWasFractional = false + private var lastSetVisibilityAmountLogWasFractional = false + private var lastSetHideAmountLogWasFractional = false + private var lastSetHideAmount = -1f - fun logSetDozeAmount( - linear: Float, - eased: Float, - source: String, + fun logUpdateDozeAmount( + inputLinear: Float, + delayLinear: Float, + hardOverride: Float?, + outputLinear: Float, state: Int, changed: Boolean, ) { // Avoid logging on every frame of the animation if important values are not changing - val isFractional = linear != 1f && linear != 0f + val isInputFractional = inputLinear != 1f && inputLinear != 0f + val isDelayFractional = delayLinear != 1f && delayLinear != 0f if ( - lastSetDozeAmountLogWasFractional && - isFractional && + (isInputFractional || isDelayFractional) && + lastSetDozeAmountLogInputWasFractional == isInputFractional && + lastSetDozeAmountLogDelayWasFractional == isDelayFractional && lastSetDozeAmountLogState == state && - lastSetDozeAmountLogSource == source + lastSetHardOverride == hardOverride && + allowThrottle ) { return } - lastSetDozeAmountLogWasFractional = isFractional + lastSetDozeAmountLogInputWasFractional = isInputFractional + lastSetDozeAmountLogDelayWasFractional = isDelayFractional lastSetDozeAmountLogState = state - lastSetDozeAmountLogSource = source + lastSetHardOverride = hardOverride buffer.log( TAG, DEBUG, { - double1 = linear.toDouble() - str2 = eased.toString() - str3 = source + double1 = inputLinear.toDouble() + str1 = hardOverride.toString() + str2 = outputLinear.toString() + str3 = delayLinear.toString() int1 = state bool1 = changed }, { - "setDozeAmount(linear=$double1, eased=$str2, source=$str3)" + + "updateDozeAmount() inputLinear=$double1 delayLinear=$str3" + + " hardOverride=$str1 outputLinear=$str2" + " state=${StatusBarState.toString(int1)} changed=$bool1" } ) } - fun logMaybeClearDozeAmountOverrideHidingNotifs( + fun logSetDozeAmountOverride(dozing: Boolean, source: String) { + buffer.log( + TAG, + DEBUG, + { + bool1 = dozing + str1 = source + }, + { "setDozeAmountOverride(dozing=$bool1, source=\"$str1\")" } + ) + } + + fun logMaybeClearHardDozeAmountOverrideHidingNotifs( willRemove: Boolean, onKeyguard: Boolean, dozing: Boolean, @@ -80,14 +105,14 @@ constructor(@NotificationLockscreenLog private val buffer: LogBuffer) { "willRemove=$willRemove onKeyguard=$onKeyguard dozing=$dozing" + " bypass=$bypass animating=$animating" }, - { "maybeClearDozeAmountOverrideHidingNotifs() $str1" } + { "maybeClearHardDozeAmountOverrideHidingNotifs() $str1" } ) } fun logOnDozeAmountChanged(linear: Float, eased: Float) { // Avoid logging on every frame of the animation when values are fractional val isFractional = linear != 1f && linear != 0f - if (lastOnDozeAmountChangedLogWasFractional && isFractional) return + if (lastOnDozeAmountChangedLogWasFractional && isFractional && allowThrottle) return lastOnDozeAmountChangedLogWasFractional = isFractional buffer.log( TAG, @@ -100,6 +125,47 @@ constructor(@NotificationLockscreenLog private val buffer: LogBuffer) { ) } + fun logSetDelayDozeAmountOverride(linear: Float) { + // Avoid logging on every frame of the animation when values are fractional + val isFractional = linear != 1f && linear != 0f + if (lastSetDelayDozeAmountOverrideLogWasFractional && isFractional && allowThrottle) return + lastSetDelayDozeAmountOverrideLogWasFractional = isFractional + buffer.log( + TAG, + DEBUG, + { double1 = linear.toDouble() }, + { "setDelayDozeAmountOverride($double1)" } + ) + } + + fun logSetVisibilityAmount(linear: Float) { + // Avoid logging on every frame of the animation when values are fractional + val isFractional = linear != 1f && linear != 0f + if (lastSetVisibilityAmountLogWasFractional && isFractional && allowThrottle) return + lastSetVisibilityAmountLogWasFractional = isFractional + buffer.log(TAG, DEBUG, { double1 = linear.toDouble() }, { "setVisibilityAmount($double1)" }) + } + + fun logSetHideAmount(linear: Float) { + // Avoid logging the same value repeatedly + if (lastSetHideAmount == linear && allowThrottle) return + lastSetHideAmount = linear + // Avoid logging on every frame of the animation when values are fractional + val isFractional = linear != 1f && linear != 0f + if (lastSetHideAmountLogWasFractional && isFractional && allowThrottle) return + lastSetHideAmountLogWasFractional = isFractional + buffer.log(TAG, DEBUG, { double1 = linear.toDouble() }, { "setHideAmount($double1)" }) + } + + fun logStartDelayedDozeAmountAnimation(alreadyRunning: Boolean) { + buffer.log( + TAG, + DEBUG, + { bool1 = alreadyRunning }, + { "startDelayedDozeAmountAnimation() alreadyRunning=$bool1" } + ) + } + fun logOnStateChanged(newState: Int, storedState: Int) { buffer.log( TAG, @@ -114,6 +180,27 @@ constructor(@NotificationLockscreenLog private val buffer: LogBuffer) { } ) } + + fun logSetWakingUp(wakingUp: Boolean, requestDelayedAnimation: Boolean) { + buffer.log( + TAG, + DEBUG, + { + bool1 = wakingUp + bool2 = requestDelayedAnimation + }, + { "setWakingUp(wakingUp=$bool1, requestDelayedAnimation=$bool2)" } + ) + } + + fun logDelayingClockWakeUpAnimation(delayingAnimation: Boolean) { + buffer.log( + TAG, + DEBUG, + { bool1 = delayingAnimation }, + { "logDelayingClockWakeUpAnimation($bool1)" } + ) + } } private const val TAG = "NotificationWakeUpCoordinator" diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java index 2c088fa66a8a..300587627c18 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java @@ -5565,6 +5565,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable public void setDozeAmount(float dozeAmount) { mAmbientState.setDozeAmount(dozeAmount); updateContinuousBackgroundDrawing(); + updateStackPosition(); requestChildrenUpdate(); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java index 311728fff6aa..b62450b22889 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java @@ -53,6 +53,7 @@ import com.android.systemui.shade.NotificationShadeWindowView; import com.android.systemui.shade.NotificationShadeWindowViewController; import com.android.systemui.statusbar.LightRevealScrim; import com.android.systemui.statusbar.NotificationPresenter; +import com.android.systemui.util.Compile; import java.io.PrintWriter; @@ -71,6 +72,7 @@ public interface CentralSurfaces extends Dumpable, ActivityStarter, LifecycleOwn boolean DEBUG_MEDIA_FAKE_ARTWORK = false; boolean DEBUG_CAMERA_LIFT = false; boolean DEBUG_WINDOW_STATE = false; + boolean DEBUG_WAKEUP_DELAY = Compile.IS_DEBUG; // additional instrumentation for testing purposes; intended to be left on during development boolean CHATTY = DEBUG; boolean SHOW_LOCKSCREEN_MEDIA_ARTWORK = true; @@ -536,6 +538,8 @@ public interface CentralSurfaces extends Dumpable, ActivityStarter, LifecycleOwn void extendDozePulse(); + boolean shouldDelayWakeUpAnimation(); + public static class KeyboardShortcutsMessage { final int mDeviceId; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java index de129a3a2eb4..6cc99beecd03 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java @@ -69,6 +69,7 @@ import android.content.pm.ResolveInfo; import android.content.res.Configuration; import android.graphics.Point; import android.hardware.devicestate.DeviceStateManager; +import android.hardware.fingerprint.FingerprintManager; import android.metrics.LogMaker; import android.net.Uri; import android.os.Binder; @@ -257,6 +258,7 @@ import java.util.concurrent.Executor; import javax.inject.Inject; import javax.inject.Named; +import javax.inject.Provider; import dagger.Lazy; @@ -446,10 +448,20 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { private final KeyguardUpdateMonitor mKeyguardUpdateMonitor; @VisibleForTesting DozeServiceHost mDozeServiceHost; - private boolean mWakeUpComingFromTouch; private LightRevealScrim mLightRevealScrim; private PowerButtonReveal mPowerButtonReveal; + private boolean mWakeUpComingFromTouch; + + /** + * Whether we should delay the wakeup animation (which shows the notifications and moves the + * clock view). This is typically done when waking up from a 'press to unlock' gesture on a + * device with a side fingerprint sensor, so that if the fingerprint scan is successful, we + * can play the unlock animation directly rather than interrupting the wakeup animation part + * way through. + */ + private boolean mShouldDelayWakeUpAnimation = false; + private final Object mQueueLock = new Object(); private final PulseExpansionHandler mPulseExpansionHandler; @@ -510,6 +522,7 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { private final MessageRouter mMessageRouter; private final WallpaperManager mWallpaperManager; private final UserTracker mUserTracker; + private final Provider<FingerprintManager> mFingerprintManager; private CentralSurfacesComponent mCentralSurfacesComponent; @@ -753,7 +766,8 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { Lazy<CameraLauncher> cameraLauncherLazy, Lazy<LightRevealScrimViewModel> lightRevealScrimViewModelLazy, AlternateBouncerInteractor alternateBouncerInteractor, - UserTracker userTracker + UserTracker userTracker, + Provider<FingerprintManager> fingerprintManager ) { mContext = context; mNotificationsController = notificationsController; @@ -834,6 +848,7 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { mCameraLauncherLazy = cameraLauncherLazy; mAlternateBouncerInteractor = alternateBouncerInteractor; mUserTracker = userTracker; + mFingerprintManager = fingerprintManager; mLockscreenShadeTransitionController = lockscreenShadeTransitionController; mStartingSurfaceOptional = startingSurfaceOptional; @@ -3137,6 +3152,10 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { } } + public boolean shouldDelayWakeUpAnimation() { + return mShouldDelayWakeUpAnimation; + } + private void updateDozingState() { Trace.traceCounter(Trace.TRACE_TAG_APP, "dozing", mDozing ? 1 : 0); Trace.beginSection("CentralSurfaces#updateDozingState"); @@ -3147,11 +3166,8 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { boolean keyguardVisibleOrWillBe = keyguardVisible || (mDozing && mDozeParameters.shouldDelayKeyguardShow()); - boolean wakeAndUnlock = mBiometricUnlockController.getMode() - == BiometricUnlockController.MODE_WAKE_AND_UNLOCK; - boolean animate = (!mDozing && mDozeServiceHost.shouldAnimateWakeup() && !wakeAndUnlock) - || (mDozing && mDozeParameters.shouldControlScreenOff() - && keyguardVisibleOrWillBe); + boolean animate = (!mDozing && shouldAnimateDozeWakeup()) + || (mDozing && mDozeParameters.shouldControlScreenOff() && keyguardVisibleOrWillBe); mNotificationPanelViewController.setDozing(mDozing, animate); updateQsExpansionEnabled(); @@ -3504,7 +3520,44 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { DejankUtils.startDetectingBlockingIpcs(tag); mNotificationShadeWindowController.batchApplyWindowLayoutParams(()-> { mDeviceInteractive = true; - mWakeUpCoordinator.setWakingUp(true); + + if (shouldAnimateDozeWakeup()) { + // If this is false, the power button must be physically pressed in order to + // trigger fingerprint authentication. + final boolean touchToUnlockAnytime = Settings.Secure.getIntForUser( + mContext.getContentResolver(), + Settings.Secure.SFPS_PERFORMANT_AUTH_ENABLED, + -1, + mUserTracker.getUserId()) > 0; + + // Delay if we're waking up, not mid-doze animation (which means we are + // cancelling a sleep), from the power button, on a device with a power button + // FPS, and 'press to unlock' is required. + mShouldDelayWakeUpAnimation = + !isPulsing() + && mStatusBarStateController.getDozeAmount() == 1f + && mWakefulnessLifecycle.getLastWakeReason() + == PowerManager.WAKE_REASON_POWER_BUTTON + && mFingerprintManager.get().isPowerbuttonFps() + && mFingerprintManager.get().hasEnrolledFingerprints() + && !touchToUnlockAnytime; + if (DEBUG_WAKEUP_DELAY) { + Log.d(TAG, "mShouldDelayWakeUpAnimation=" + mShouldDelayWakeUpAnimation); + } + } else { + // If we're not animating anyway, we do not need to delay it. + mShouldDelayWakeUpAnimation = false; + if (DEBUG_WAKEUP_DELAY) { + Log.d(TAG, "mShouldDelayWakeUpAnimation CLEARED"); + } + } + + mNotificationPanelViewController.setWillPlayDelayedDozeAmountAnimation( + mShouldDelayWakeUpAnimation); + mWakeUpCoordinator.setWakingUp( + /* wakingUp= */ true, + mShouldDelayWakeUpAnimation); + if (!mKeyguardBypassController.getBypassEnabled()) { mHeadsUpManager.releaseAllImmediately(); } @@ -3531,7 +3584,7 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { @Override public void onFinishedWakingUp() { mWakeUpCoordinator.setFullyAwake(true); - mWakeUpCoordinator.setWakingUp(false); + mWakeUpCoordinator.setWakingUp(false, false); if (mKeyguardStateController.isOccluded() && !mDozeParameters.canControlUnlockedScreenOff()) { // When the keyguard is occluded we don't use the KEYGUARD state which would @@ -4395,4 +4448,15 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { } return mUserTracker.getUserHandle(); } + + /** + * Whether we want to animate the wake animation AOD to lockscreen. This is done only if the + * doze service host says we can, and also we're not wake and unlocking (in which case the + * AOD instantly hides). + */ + private boolean shouldAnimateDozeWakeup() { + return mDozeServiceHost.shouldAnimateWakeup() + && mBiometricUnlockController.getMode() + != BiometricUnlockController.MODE_WAKE_AND_UNLOCK; + } } diff --git a/packages/SystemUI/tests/src/androidx/core/animation/AnimatorTestRule2.java b/packages/SystemUI/tests/src/androidx/core/animation/AnimatorTestRule2.java new file mode 100644 index 000000000000..e93e86291535 --- /dev/null +++ b/packages/SystemUI/tests/src/androidx/core/animation/AnimatorTestRule2.java @@ -0,0 +1,174 @@ +/* + * Copyright (C) 2023 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 androidx.core.animation; + +import android.os.Looper; +import android.os.SystemClock; +import android.util.AndroidRuntimeException; + +import androidx.annotation.NonNull; + +import org.junit.rules.TestRule; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; + +import java.util.ArrayList; +import java.util.List; + +/** + * NOTE: this is a copy of the {@link androidx.core.animation.AnimatorTestRule} which attempts to + * circumvent the problems with {@link androidx.core.animation.AnimationHandler} having a static + * list of callbacks. + * + * TODO(b/275602127): remove this and use the original rule once we have the updated androidx code. + */ +public final class AnimatorTestRule2 implements TestRule { + + class TestAnimationHandler extends AnimationHandler { + TestAnimationHandler() { + super(new TestProvider()); + } + + List<AnimationFrameCallback> animationCallbacks = new ArrayList<>(); + + @Override + void addAnimationFrameCallback(AnimationFrameCallback callback) { + animationCallbacks.add(callback); + callback.doAnimationFrame(getCurrentTime()); + } + + @Override + public void removeCallback(AnimationFrameCallback callback) { + int id = animationCallbacks.indexOf(callback); + if (id >= 0) { + animationCallbacks.set(id, null); + } + } + + void onAnimationFrame(long frameTime) { + for (int i = 0; i < animationCallbacks.size(); i++) { + final AnimationFrameCallback callback = animationCallbacks.get(i); + if (callback == null) { + continue; + } + callback.doAnimationFrame(frameTime); + } + } + + @Override + void autoCancelBasedOn(ObjectAnimator objectAnimator) { + for (int i = animationCallbacks.size() - 1; i >= 0; i--) { + AnimationFrameCallback cb = animationCallbacks.get(i); + if (cb == null) { + continue; + } + if (objectAnimator.shouldAutoCancel(cb)) { + ((Animator) animationCallbacks.get(i)).cancel(); + } + } + } + } + + final TestAnimationHandler mTestHandler; + final long mStartTime; + private long mTotalTimeDelta = 0; + private final Object mLock = new Object(); + + public AnimatorTestRule2() { + mStartTime = SystemClock.uptimeMillis(); + mTestHandler = new TestAnimationHandler(); + } + + @NonNull + @Override + public Statement apply(@NonNull final Statement base, @NonNull Description description) { + return new Statement() { + @Override + public void evaluate() throws Throwable { + AnimationHandler.setTestHandler(mTestHandler); + try { + base.evaluate(); + } finally { + AnimationHandler.setTestHandler(null); + } + } + }; + } + + /** + * Advances the animation clock by the given amount of delta in milliseconds. This call will + * produce an animation frame to all the ongoing animations. This method needs to be + * called on the same thread as {@link Animator#start()}. + * + * @param timeDelta the amount of milliseconds to advance + */ + public void advanceTimeBy(long timeDelta) { + if (Looper.myLooper() == null) { + // Throw an exception + throw new AndroidRuntimeException("AnimationTestRule#advanceTimeBy(long) may only be" + + "called on Looper threads"); + } + synchronized (mLock) { + // Advance time & pulse a frame + mTotalTimeDelta += timeDelta < 0 ? 0 : timeDelta; + } + // produce a frame + mTestHandler.onAnimationFrame(getCurrentTime()); + } + + + /** + * Returns the current time in milliseconds tracked by AnimationHandler. Note that this is a + * different time than the time tracked by {@link SystemClock} This method needs to be called on + * the same thread as {@link Animator#start()}. + */ + public long getCurrentTime() { + if (Looper.myLooper() == null) { + // Throw an exception + throw new AndroidRuntimeException("AnimationTestRule#getCurrentTime() may only be" + + "called on Looper threads"); + } + synchronized (mLock) { + return mStartTime + mTotalTimeDelta; + } + } + + + private class TestProvider implements AnimationHandler.AnimationFrameCallbackProvider { + TestProvider() { + } + + @Override + public void onNewCallbackAdded(AnimationHandler.AnimationFrameCallback callback) { + callback.doAnimationFrame(getCurrentTime()); + } + + @Override + public void postFrameCallback() { + } + + @Override + public void setFrameDelay(long delay) { + } + + @Override + public long getFrameDelay() { + return 0; + } + } +} + diff --git a/packages/SystemUI/tests/src/androidx/core/animation/AnimatorTestRuleTest.kt b/packages/SystemUI/tests/src/androidx/core/animation/AnimatorTestRuleTest.kt new file mode 100644 index 000000000000..bddd60b5970a --- /dev/null +++ b/packages/SystemUI/tests/src/androidx/core/animation/AnimatorTestRuleTest.kt @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2023 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 androidx.core.animation + +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper.RunWithLooper +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.util.doOnEnd +import com.google.common.truth.Truth.assertThat +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidTestingRunner::class) +@SmallTest +@RunWithLooper(setAsMainLooper = true) +class AnimatorTestRuleTest : SysuiTestCase() { + + @get:Rule val animatorTestRule = AnimatorTestRule2() + + @Test + fun testA() { + didTouchA = false + didTouchB = false + ObjectAnimator.ofFloat(0f, 1f).apply { + duration = 100 + doOnEnd { didTouchA = true } + start() + } + ObjectAnimator.ofFloat(0f, 1f).apply { + duration = 150 + doOnEnd { didTouchA = true } + start() + } + animatorTestRule.advanceTimeBy(100) + assertThat(didTouchA).isTrue() + assertThat(didTouchB).isFalse() + } + + @Test + fun testB() { + didTouchA = false + didTouchB = false + ObjectAnimator.ofFloat(0f, 1f).apply { + duration = 100 + doOnEnd { didTouchB = true } + start() + } + ObjectAnimator.ofFloat(0f, 1f).apply { + duration = 150 + doOnEnd { didTouchB = true } + start() + } + animatorTestRule.advanceTimeBy(100) + assertThat(didTouchA).isFalse() + assertThat(didTouchB).isTrue() + } + + companion object { + var didTouchA = false + var didTouchB = false + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java index b868018b7dda..be0d933e2c5b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java @@ -437,6 +437,34 @@ public class NotificationPanelViewControllerTest extends NotificationPanelViewCo } @Test + public void keyguardStatusView_willPlayDelayedDoze_isCentered_thenNot() { + when(mNotificationStackScrollLayoutController.getVisibleNotificationCount()).thenReturn(2); + mStatusBarStateController.setState(KEYGUARD); + enableSplitShade(/* enabled= */ true); + + mNotificationPanelViewController.setWillPlayDelayedDozeAmountAnimation(true); + setDozing(/* dozing= */ false, /* dozingAlwaysOn= */ false); + assertKeyguardStatusViewCentered(); + + mNotificationPanelViewController.setWillPlayDelayedDozeAmountAnimation(false); + assertKeyguardStatusViewNotCentered(); + } + + @Test + public void keyguardStatusView_willPlayDelayedDoze_isCentered_thenStillCenteredIfNoNotifs() { + when(mNotificationStackScrollLayoutController.getVisibleNotificationCount()).thenReturn(0); + mStatusBarStateController.setState(KEYGUARD); + enableSplitShade(/* enabled= */ true); + + mNotificationPanelViewController.setWillPlayDelayedDozeAmountAnimation(true); + setDozing(/* dozing= */ false, /* dozingAlwaysOn= */ false); + assertKeyguardStatusViewCentered(); + + mNotificationPanelViewController.setWillPlayDelayedDozeAmountAnimation(false); + assertKeyguardStatusViewCentered(); + } + + @Test public void testCanCollapsePanelOnTouch_trueForKeyGuard() { mStatusBarStateController.setState(KEYGUARD); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerImplTest.kt index 08a9f3139d71..7b59cc284181 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerImplTest.kt @@ -22,7 +22,7 @@ import android.testing.AndroidTestingRunner import android.testing.TestableLooper.RunWithLooper import android.view.View import android.widget.FrameLayout -import androidx.core.animation.AnimatorTestRule +import androidx.core.animation.AnimatorTestRule2 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.dump.DumpManager @@ -70,7 +70,7 @@ class SystemStatusAnimationSchedulerImplTest : SysuiTestCase() { private lateinit var systemStatusAnimationScheduler: SystemStatusAnimationScheduler private val fakeFeatureFlags = FakeFeatureFlags() - @get:Rule val animatorTestRule = AnimatorTestRule() + @get:Rule val animatorTestRule = AnimatorTestRule2() @Before fun setup() { diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinatorLoggerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinatorLoggerTest.kt index 7a6779684fc5..bef9fcb5697c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinatorLoggerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinatorLoggerTest.kt @@ -18,7 +18,7 @@ package com.android.systemui.statusbar.notification import android.testing.AndroidTestingRunner import androidx.test.filters.SmallTest -import com.android.systemui.SysuiTestCase; +import com.android.systemui.SysuiTestCase import com.android.systemui.plugins.log.LogBuffer import com.android.systemui.plugins.log.LogLevel import com.android.systemui.plugins.log.LogcatEchoTracker @@ -45,47 +45,130 @@ class NotificationWakeUpCoordinatorLoggerTest : SysuiTestCase() { } @Test - fun setDozeAmountWillThrottleFractionalUpdates() { - logger.logSetDozeAmount(0f, 0f, "source1", StatusBarState.SHADE, changed = false) + fun updateVisibilityThrottleFractionalUpdates() { + logger.logSetVisibilityAmount(0f) verifyDidLog(1) - logger.logSetDozeAmount(0.1f, 0.1f, "source1", StatusBarState.SHADE, changed = true) + logger.logSetVisibilityAmount(0.1f) verifyDidLog(1) - logger.logSetDozeAmount(0.2f, 0.2f, "source1", StatusBarState.SHADE, changed = true) - logger.logSetDozeAmount(0.3f, 0.3f, "source1", StatusBarState.SHADE, changed = true) - logger.logSetDozeAmount(0.4f, 0.4f, "source1", StatusBarState.SHADE, changed = true) - logger.logSetDozeAmount(0.5f, 0.5f, "source1", StatusBarState.SHADE, changed = true) + logger.logSetVisibilityAmount(0.2f) + logger.logSetVisibilityAmount(0.3f) + logger.logSetVisibilityAmount(0.4f) + logger.logSetVisibilityAmount(0.5f) verifyDidLog(0) - logger.logSetDozeAmount(1f, 1f, "source1", StatusBarState.SHADE, changed = true) + logger.logSetVisibilityAmount(1f) verifyDidLog(1) } @Test - fun setDozeAmountWillIncludeFractionalUpdatesWhenStateChanges() { - logger.logSetDozeAmount(0f, 0f, "source1", StatusBarState.SHADE, changed = false) + fun updateHideAmountThrottleFractionalOrRepeatedUpdates() { + logger.logSetHideAmount(0f) verifyDidLog(1) - logger.logSetDozeAmount(0.1f, 0.1f, "source1", StatusBarState.SHADE, changed = true) + logger.logSetHideAmount(0f) + logger.logSetHideAmount(0f) + verifyDidLog(0) + logger.logSetHideAmount(0.1f) + verifyDidLog(1) + logger.logSetHideAmount(0.2f) + logger.logSetHideAmount(0.3f) + logger.logSetHideAmount(0.4f) + logger.logSetHideAmount(0.5f) + logger.logSetHideAmount(0.5f) + logger.logSetHideAmount(0.5f) + verifyDidLog(0) + logger.logSetHideAmount(1f) + verifyDidLog(1) + logger.logSetHideAmount(1f) + logger.logSetHideAmount(1f) + verifyDidLog(0) + } + + @Test + fun updateDozeAmountWillThrottleFractionalInputUpdates() { + logger.logUpdateDozeAmount(0f, 0f, null, 0f, StatusBarState.SHADE, changed = false) + verifyDidLog(1) + logger.logUpdateDozeAmount(0.1f, 0f, null, 0.1f, StatusBarState.SHADE, changed = true) + verifyDidLog(1) + logger.logUpdateDozeAmount(0.2f, 0f, null, 0.2f, StatusBarState.SHADE, changed = true) + logger.logUpdateDozeAmount(0.3f, 0f, null, 0.3f, StatusBarState.SHADE, changed = true) + logger.logUpdateDozeAmount(0.4f, 0f, null, 0.4f, StatusBarState.SHADE, changed = true) + logger.logUpdateDozeAmount(0.5f, 0f, null, 0.5f, StatusBarState.SHADE, changed = true) + verifyDidLog(0) + logger.logUpdateDozeAmount(1f, 0f, null, 1f, StatusBarState.SHADE, changed = true) + verifyDidLog(1) + } + + @Test + fun updateDozeAmountWillThrottleFractionalDelayUpdates() { + logger.logUpdateDozeAmount(0f, 0f, null, 0f, StatusBarState.SHADE, changed = false) + verifyDidLog(1) + logger.logUpdateDozeAmount(0f, 0.1f, null, 0.1f, StatusBarState.SHADE, changed = true) + verifyDidLog(1) + logger.logUpdateDozeAmount(0f, 0.2f, null, 0.2f, StatusBarState.SHADE, changed = true) + logger.logUpdateDozeAmount(0f, 0.3f, null, 0.3f, StatusBarState.SHADE, changed = true) + logger.logUpdateDozeAmount(0f, 0.4f, null, 0.4f, StatusBarState.SHADE, changed = true) + logger.logUpdateDozeAmount(0f, 0.5f, null, 0.5f, StatusBarState.SHADE, changed = true) + verifyDidLog(0) + logger.logUpdateDozeAmount(0f, 1f, null, 1f, StatusBarState.SHADE, changed = true) verifyDidLog(1) - logger.logSetDozeAmount(0.2f, 0.2f, "source1", StatusBarState.SHADE, changed = true) - logger.logSetDozeAmount(0.3f, 0.3f, "source1", StatusBarState.SHADE, changed = true) - logger.logSetDozeAmount(0.4f, 0.4f, "source1", StatusBarState.SHADE, changed = true) - logger.logSetDozeAmount(0.5f, 0.5f, "source1", StatusBarState.SHADE, changed = true) + } + + @Test + fun updateDozeAmountWillIncludeFractionalUpdatesWhenOtherInputChangesFractionality() { + logger.logUpdateDozeAmount(0.0f, 1.0f, 1f, 1f, StatusBarState.SHADE, changed = false) + verifyDidLog(1) + logger.logUpdateDozeAmount(0.1f, 1.0f, 1f, 1f, StatusBarState.SHADE, changed = false) + verifyDidLog(1) + logger.logUpdateDozeAmount(0.2f, 1.0f, 1f, 1f, StatusBarState.SHADE, changed = false) + logger.logUpdateDozeAmount(0.3f, 1.0f, 1f, 1f, StatusBarState.SHADE, changed = false) + logger.logUpdateDozeAmount(0.4f, 1.0f, 1f, 1f, StatusBarState.SHADE, changed = false) + verifyDidLog(0) + logger.logUpdateDozeAmount(0.5f, 0.9f, 1f, 1f, StatusBarState.SHADE, changed = false) + verifyDidLog(1) + logger.logUpdateDozeAmount(0.6f, 0.8f, 1f, 1f, StatusBarState.SHADE, changed = false) + logger.logUpdateDozeAmount(0.8f, 0.6f, 1f, 1f, StatusBarState.SHADE, changed = false) + logger.logUpdateDozeAmount(0.9f, 0.5f, 1f, 1f, StatusBarState.SHADE, changed = false) + verifyDidLog(0) + logger.logUpdateDozeAmount(1.0f, 0.4f, 1f, 1f, StatusBarState.SHADE, changed = false) + verifyDidLog(1) + logger.logUpdateDozeAmount(1.0f, 0.3f, 1f, 1f, StatusBarState.SHADE, changed = false) + logger.logUpdateDozeAmount(1.0f, 0.2f, 1f, 1f, StatusBarState.SHADE, changed = false) + logger.logUpdateDozeAmount(1.0f, 0.1f, 1f, 1f, StatusBarState.SHADE, changed = false) verifyDidLog(0) - logger.logSetDozeAmount(0.5f, 0.5f, "source1", StatusBarState.KEYGUARD, changed = false) + logger.logUpdateDozeAmount(1.0f, 0.0f, 1f, 1f, StatusBarState.SHADE, changed = false) verifyDidLog(1) } @Test - fun setDozeAmountWillIncludeFractionalUpdatesWhenSourceChanges() { - logger.logSetDozeAmount(0f, 0f, "source1", StatusBarState.SHADE, changed = false) + fun updateDozeAmountWillIncludeFractionalUpdatesWhenStateChanges() { + logger.logUpdateDozeAmount(0f, 0f, null, 0f, StatusBarState.SHADE, changed = false) verifyDidLog(1) - logger.logSetDozeAmount(0.1f, 0.1f, "source1", StatusBarState.SHADE, changed = true) + logger.logUpdateDozeAmount(0.1f, 0f, null, 0.1f, StatusBarState.SHADE, changed = true) verifyDidLog(1) - logger.logSetDozeAmount(0.2f, 0.2f, "source1", StatusBarState.SHADE, changed = true) - logger.logSetDozeAmount(0.3f, 0.3f, "source1", StatusBarState.SHADE, changed = true) - logger.logSetDozeAmount(0.4f, 0.4f, "source1", StatusBarState.SHADE, changed = true) - logger.logSetDozeAmount(0.5f, 0.5f, "source1", StatusBarState.SHADE, changed = true) + logger.logUpdateDozeAmount(0.2f, 0f, null, 0.2f, StatusBarState.SHADE, changed = true) + logger.logUpdateDozeAmount(0.3f, 0f, null, 0.3f, StatusBarState.SHADE, changed = true) + logger.logUpdateDozeAmount(0.4f, 0f, null, 0.4f, StatusBarState.SHADE, changed = true) + logger.logUpdateDozeAmount(0.5f, 0f, null, 0.5f, StatusBarState.SHADE, changed = true) verifyDidLog(0) - logger.logSetDozeAmount(0.5f, 0.5f, "source2", StatusBarState.SHADE, changed = false) + logger.logUpdateDozeAmount(0.5f, 0f, null, 0.5f, StatusBarState.KEYGUARD, changed = false) + verifyDidLog(1) + } + + @Test + fun updateDozeAmountWillIncludeFractionalUpdatesWhenHardOverrideChanges() { + logger.logUpdateDozeAmount(0f, 0f, null, 0f, StatusBarState.SHADE, changed = false) + verifyDidLog(1) + logger.logUpdateDozeAmount(0.1f, 0f, null, 0.1f, StatusBarState.SHADE, changed = true) + verifyDidLog(1) + logger.logUpdateDozeAmount(0.2f, 0f, null, 0.2f, StatusBarState.SHADE, changed = true) + logger.logUpdateDozeAmount(0.3f, 0f, null, 0.3f, StatusBarState.SHADE, changed = true) + logger.logUpdateDozeAmount(0.4f, 0f, null, 0.4f, StatusBarState.SHADE, changed = true) + logger.logUpdateDozeAmount(0.5f, 0f, null, 0.5f, StatusBarState.SHADE, changed = true) + verifyDidLog(0) + logger.logUpdateDozeAmount(0.5f, 0f, 1f, 1f, StatusBarState.SHADE, changed = true) + verifyDidLog(1) + logger.logUpdateDozeAmount(0.5f, 0f, 0f, 0f, StatusBarState.SHADE, changed = true) + verifyDidLog(1) + logger.logUpdateDozeAmount(0.5f, 0f, null, 0.5f, StatusBarState.SHADE, changed = true) verifyDidLog(1) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinatorTest.kt index 95591a4b321c..be3b7234a1a2 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinatorTest.kt @@ -17,31 +17,42 @@ package com.android.systemui.statusbar.notification import android.testing.AndroidTestingRunner +import android.testing.TestableLooper +import androidx.core.animation.AnimatorTestRule2 import androidx.test.filters.SmallTest -import com.android.systemui.SysuiTestCase; +import com.android.systemui.SysuiTestCase import com.android.systemui.dump.DumpManager import com.android.systemui.plugins.statusbar.StatusBarStateController +import com.android.systemui.shade.NotificationPanelViewController.WAKEUP_ANIMATION_DELAY_MS import com.android.systemui.statusbar.StatusBarState import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController +import com.android.systemui.statusbar.notification.stack.StackStateAnimator.ANIMATION_DURATION_WAKEUP import com.android.systemui.statusbar.phone.DozeParameters import com.android.systemui.statusbar.phone.KeyguardBypassController import com.android.systemui.statusbar.phone.ScreenOffAnimationController import com.android.systemui.statusbar.policy.HeadsUpManager +import com.android.systemui.util.mockito.eq import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever import com.android.systemui.util.mockito.withArgCaptor import com.google.common.truth.Truth.assertThat import org.junit.Before +import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mockito.anyFloat import org.mockito.Mockito.clearInvocations +import org.mockito.Mockito.never import org.mockito.Mockito.verify +import org.mockito.Mockito.verifyNoMoreInteractions @RunWith(AndroidTestingRunner::class) @SmallTest +@TestableLooper.RunWithLooper(setAsMainLooper = true) class NotificationWakeUpCoordinatorTest : SysuiTestCase() { + @get:Rule val animatorTestRule = AnimatorTestRule2() + private val dumpManager: DumpManager = mock() private val headsUpManager: HeadsUpManager = mock() private val statusBarStateController: StatusBarStateController = mock() @@ -50,6 +61,7 @@ class NotificationWakeUpCoordinatorTest : SysuiTestCase() { private val screenOffAnimationController: ScreenOffAnimationController = mock() private val logger: NotificationWakeUpCoordinatorLogger = mock() private val stackScrollerController: NotificationStackScrollLayoutController = mock() + private val wakeUpListener: NotificationWakeUpCoordinator.WakeUpListener = mock() private lateinit var notificationWakeUpCoordinator: NotificationWakeUpCoordinator private lateinit var statusBarStateCallback: StatusBarStateController.StateListener @@ -57,7 +69,8 @@ class NotificationWakeUpCoordinatorTest : SysuiTestCase() { private var bypassEnabled: Boolean = false private var statusBarState: Int = StatusBarState.KEYGUARD - private var dozeAmount: Float = 0f + private fun eased(dozeAmount: Float) = + notificationWakeUpCoordinator.dozeAmountInterpolator.getInterpolation(dozeAmount) private fun setBypassEnabled(enabled: Boolean) { bypassEnabled = enabled @@ -70,7 +83,6 @@ class NotificationWakeUpCoordinatorTest : SysuiTestCase() { } private fun setDozeAmount(dozeAmount: Float) { - this.dozeAmount = dozeAmount statusBarStateCallback.onDozeAmountChanged(dozeAmount, dozeAmount) } @@ -129,7 +141,7 @@ class NotificationWakeUpCoordinatorTest : SysuiTestCase() { fun setDozeToZeroWithBypassWillFullyHideNotifications() { bypassEnabled = true setDozeAmount(0f) - verifyStackScrollerDozeAndHideAmount(dozeAmount = 01f, hideAmount = 1f) + verifyStackScrollerDozeAndHideAmount(dozeAmount = 1f, hideAmount = 1f) assertThat(notificationWakeUpCoordinator.notificationsFullyHidden).isTrue() } @@ -152,12 +164,161 @@ class NotificationWakeUpCoordinatorTest : SysuiTestCase() { assertThat(notificationWakeUpCoordinator.statusBarState).isEqualTo(StatusBarState.SHADE) } + private val delayedDozeDelay = WAKEUP_ANIMATION_DELAY_MS.toLong() + private val delayedDozeDuration = ANIMATION_DURATION_WAKEUP.toLong() + + @Test + fun dozeAmountOutputClampsTo1WhenDelayStarts() { + notificationWakeUpCoordinator.setWakingUp(true, requestDelayedAnimation = true) + verifyStackScrollerDozeAndHideAmount(dozeAmount = 1f, hideAmount = 1f) + assertThat(notificationWakeUpCoordinator.notificationsFullyHidden).isTrue() + + // verify further doze amount changes have no effect on output + setDozeAmount(0.5f) + verifyStackScrollerDozeAndHideAmount(dozeAmount = 1f, hideAmount = 1f) + assertThat(notificationWakeUpCoordinator.notificationsFullyHidden).isTrue() + } + + @Test + fun verifyDozeAmountOutputTracksDelay() { + dozeAmountOutputClampsTo1WhenDelayStarts() + + // Animator waiting the delay amount should not yet affect the output + animatorTestRule.advanceTimeBy(delayedDozeDelay) + verifyStackScrollerDozeAndHideAmount(dozeAmount = 1f, hideAmount = 1f) + assertThat(notificationWakeUpCoordinator.notificationsFullyHidden).isTrue() + + // input doze amount change to 0 has no effect + setDozeAmount(0.0f) + verifyStackScrollerDozeAndHideAmount(dozeAmount = 1f, hideAmount = 1f) + assertThat(notificationWakeUpCoordinator.notificationsFullyHidden).isTrue() + + // Advancing the delay to 50% will cause the 50% output + animatorTestRule.advanceTimeBy(delayedDozeDuration / 2) + verifyStackScrollerDozeAndHideAmount(dozeAmount = 0.5f, hideAmount = 0.5f) + assertThat(notificationWakeUpCoordinator.notificationsFullyHidden).isFalse() + + // Now advance delay to 100% completion; notifications become fully visible + animatorTestRule.advanceTimeBy(delayedDozeDuration / 2) + verifyStackScrollerDozeAndHideAmount(dozeAmount = 0f, hideAmount = 0f) + assertThat(notificationWakeUpCoordinator.notificationsFullyHidden).isFalse() + + // Now advance delay to 200% completion -- should not invoke anything else + animatorTestRule.advanceTimeBy(delayedDozeDuration) + verify(stackScrollerController, never()).setDozeAmount(anyFloat()) + verify(stackScrollerController, never()).setHideAmount(anyFloat(), anyFloat()) + assertThat(notificationWakeUpCoordinator.notificationsFullyHidden).isFalse() + } + + @Test + fun verifyWakeUpListenerCallbacksWhenDozing() { + // prime internal state as dozing, then add the listener + setDozeAmount(1f) + notificationWakeUpCoordinator.addListener(wakeUpListener) + + setDozeAmount(0.5f) + verify(wakeUpListener).onFullyHiddenChanged(eq(false)) + verifyNoMoreInteractions(wakeUpListener) + clearInvocations(wakeUpListener) + + setDozeAmount(0f) + verifyNoMoreInteractions(wakeUpListener) + + setDozeAmount(0.5f) + verifyNoMoreInteractions(wakeUpListener) + + setDozeAmount(1f) + verify(wakeUpListener).onFullyHiddenChanged(eq(true)) + verifyNoMoreInteractions(wakeUpListener) + } + + @Test + fun verifyWakeUpListenerCallbacksWhenDelayingAnimation() { + // prime internal state as dozing, then add the listener + setDozeAmount(1f) + notificationWakeUpCoordinator.addListener(wakeUpListener) + + // setWakingUp() doesn't do anything yet + notificationWakeUpCoordinator.setWakingUp(true, requestDelayedAnimation = true) + verifyNoMoreInteractions(wakeUpListener) + + // verify further doze amount changes have no effect + setDozeAmount(0.5f) + verifyNoMoreInteractions(wakeUpListener) + + // advancing to just before the start time should not invoke the listener + animatorTestRule.advanceTimeBy(delayedDozeDelay - 1) + verifyNoMoreInteractions(wakeUpListener) + + animatorTestRule.advanceTimeBy(1) + verify(wakeUpListener).onDelayedDozeAmountAnimationRunning(eq(true)) + verifyNoMoreInteractions(wakeUpListener) + clearInvocations(wakeUpListener) + + // input doze amount change to 0 has no effect + setDozeAmount(0.0f) + verifyNoMoreInteractions(wakeUpListener) + + // Advancing the delay to 50% will cause notifications to no longer be fully hidden + animatorTestRule.advanceTimeBy(delayedDozeDuration / 2) + verify(wakeUpListener).onFullyHiddenChanged(eq(false)) + verifyNoMoreInteractions(wakeUpListener) + clearInvocations(wakeUpListener) + + // Now advance delay to 99.x% completion; notifications become fully visible + animatorTestRule.advanceTimeBy(delayedDozeDuration / 2 - 1) + verifyNoMoreInteractions(wakeUpListener) + + // advance to 100%; animation no longer running + animatorTestRule.advanceTimeBy(1) + verify(wakeUpListener).onDelayedDozeAmountAnimationRunning(eq(false)) + verifyNoMoreInteractions(wakeUpListener) + clearInvocations(wakeUpListener) + + // Now advance delay to 200% completion -- should not invoke anything else + animatorTestRule.advanceTimeBy(delayedDozeDuration) + verifyNoMoreInteractions(wakeUpListener) + } + + @Test + fun verifyDelayedDozeAmountCanBeOverridden() { + dozeAmountOutputClampsTo1WhenDelayStarts() + + // input doze amount change to 0 has no effect + setDozeAmount(0.0f) + verifyStackScrollerDozeAndHideAmount(dozeAmount = 1f, hideAmount = 1f) + assertThat(notificationWakeUpCoordinator.notificationsFullyHidden).isTrue() + + // Advancing the delay to 50% will cause the 50% output + animatorTestRule.advanceTimeBy(delayedDozeDelay + delayedDozeDuration / 2) + verifyStackScrollerDozeAndHideAmount(dozeAmount = 0.5f, hideAmount = 0.5f) + assertThat(notificationWakeUpCoordinator.notificationsFullyHidden).isFalse() + + // Enabling bypass and showing keyguard will override back to fully dozing/hidden + setBypassEnabled(true) + setStatusBarState(StatusBarState.KEYGUARD) + verifyStackScrollerDozeAndHideAmount(dozeAmount = 1f, hideAmount = 1f) + assertThat(notificationWakeUpCoordinator.notificationsFullyHidden).isTrue() + } + + @Test + fun verifyRemovingOverrideRestoresOtherwiseCalculatedDozeAmount() { + verifyDelayedDozeAmountCanBeOverridden() + + // Disabling bypass will return back to the 50% value + setBypassEnabled(false) + verifyStackScrollerDozeAndHideAmount(dozeAmount = 0.5f, hideAmount = 0.5f) + assertThat(notificationWakeUpCoordinator.notificationsFullyHidden).isFalse() + } + private fun verifyStackScrollerDozeAndHideAmount(dozeAmount: Float, hideAmount: Float) { // First verify that we did in-fact receive the correct values - verify(stackScrollerController).setDozeAmount(dozeAmount) - verify(stackScrollerController).setHideAmount(hideAmount, hideAmount) + verify(stackScrollerController).setDozeAmount(eased(dozeAmount)) + verify(stackScrollerController).setHideAmount(hideAmount, eased(hideAmount)) // Now verify that there was just this ONE call to each of these methods verify(stackScrollerController).setDozeAmount(anyFloat()) verify(stackScrollerController).setHideAmount(anyFloat(), anyFloat()) + // clear for next check + clearInvocations(stackScrollerController) } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java index 33a813f5b0ea..64f071039c74 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java @@ -311,6 +311,7 @@ public class CentralSurfacesImplTest extends SysuiTestCase { @Mock private ViewRootImpl mViewRootImpl; @Mock private WindowOnBackInvokedDispatcher mOnBackInvokedDispatcher; @Mock private UserTracker mUserTracker; + @Mock private FingerprintManager mFingerprintManager; @Captor private ArgumentCaptor<OnBackInvokedCallback> mOnBackInvokedCallback; @Mock IPowerManager mPowerManagerService; @@ -521,7 +522,8 @@ public class CentralSurfacesImplTest extends SysuiTestCase { mCameraLauncherLazy, () -> mLightRevealScrimViewModel, mAlternateBouncerInteractor, - mUserTracker + mUserTracker, + () -> mFingerprintManager ) { @Override protected ViewRootImpl getViewRootImpl() { diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java index 0cca7b2aa38c..1880c2bd16a2 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java @@ -62,7 +62,7 @@ import android.window.OnBackInvokedDispatcher; import android.window.WindowOnBackInvokedDispatcher; import androidx.annotation.NonNull; -import androidx.core.animation.AnimatorTestRule; +import androidx.core.animation.AnimatorTestRule2; import androidx.test.filters.SmallTest; import com.android.internal.logging.UiEventLogger; @@ -110,7 +110,7 @@ public class RemoteInputViewTest extends SysuiTestCase { private final UiEventLoggerFake mUiEventLoggerFake = new UiEventLoggerFake(); @ClassRule - public static AnimatorTestRule mAnimatorTestRule = new AnimatorTestRule(); + public static AnimatorTestRule2 mAnimatorTestRule = new AnimatorTestRule2(); @Before public void setUp() throws Exception { |