diff options
| author | 2023-03-29 20:03:21 +0000 | |
|---|---|---|
| committer | 2023-03-29 20:03:21 +0000 | |
| commit | e29ff09035efca8c6c470bc619570a193e49651c (patch) | |
| tree | 9d01f85185f33a39b9751a1e5bd8f5f470871303 | |
| parent | 1a033981f7741ee929cfcffab44a628b814cea00 (diff) | |
| parent | 6de9be1669aea06ede10c0b62b1edbd1a960725c (diff) | |
Merge changes from topic "tsuji-wakeup" into tm-qpr-dev
* changes:
Create AnimatorTestRule2 which avoids the test interaction bugs with the original as it currently exists
Keep the clock centered until the delayed doze animation plays.
Delay the dozeAmount change animation.
Run ktfmt on NotificationWakeUpCoordinator.kt
Restructure the NotificationWakeUpCoordinator's dozeAmount recalculation.
Tell NotificationWakeupCoordinator if we're waking from power button w/ SFPS press-to-unlock.
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 { |