diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java index 548fd27c55b2e..b59dda9a22e78 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java @@ -589,6 +589,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; @@ -1045,6 +1051,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 -> { @@ -1641,11 +1653,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 704c906911bd4..20af6cadeed53 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinator.kt @@ -39,8 +39,8 @@ 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.doOnCancel import com.android.systemui.util.doOnEnd +import com.android.systemui.util.doOnStart import java.io.PrintWriter import javax.inject.Inject import kotlin.math.max @@ -357,6 +357,11 @@ constructor( * [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, @@ -379,8 +384,13 @@ constructor( interpolator = InterpolatorsAndroidX.LINEAR duration = StackStateAnimator.ANIMATION_DURATION_WAKEUP.toLong() startDelay = WAKEUP_ANIMATION_DELAY_MS.toLong() - doOnEnd { delayedDozeAmountAnimator = null } - doOnCancel { delayedDozeAmountAnimator = null } + doOnStart { + wakeUpListeners.forEach { it.onDelayedDozeAmountAnimationRunning(true) } + } + doOnEnd { + delayedDozeAmountAnimator = null + wakeUpListeners.forEach { it.onDelayedDozeAmountAnimationRunning(false) } + } start() } } @@ -597,8 +607,8 @@ constructor( pw.println("canShowPulsingHuns: $canShowPulsingHuns") } - fun logClockTransitionAnimationStarting(delayWakeUpAnimation: Boolean) { - logger.logClockTransitionAnimationStarting(delayWakeUpAnimation) + fun logDelayingClockWakeUpAnimation(delayingAnimation: Boolean) { + logger.logDelayingClockWakeUpAnimation(delayingAnimation) } interface WakeUpListener { @@ -611,6 +621,12 @@ constructor( * @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 { 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 d3f935366f958..dd3c2a9df3e56 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinatorLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinatorLogger.kt @@ -193,12 +193,12 @@ constructor(@NotificationLockscreenLog private val buffer: LogBuffer) { ) } - fun logClockTransitionAnimationStarting(delayWakeUpAnimation: Boolean) { + fun logDelayingClockWakeUpAnimation(delayingAnimation: Boolean) { buffer.log( TAG, DEBUG, - { bool1 = delayWakeUpAnimation }, - { "clockTransitionAnimationStarting() withDelay=$bool1" } + { bool1 = delayingAnimation }, + { "logDelayingClockWakeUpAnimation($bool1)" } ) } } 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 1fb9c5b60b091..0ba546cbb2d02 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java @@ -3564,6 +3564,8 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { } } + mNotificationPanelViewController.setWillPlayDelayedDozeAmountAnimation( + mShouldDelayWakeUpAnimation); mWakeUpCoordinator.setWakingUp( /* wakingUp= */ true, mShouldDelayWakeUpAnimation); 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 b868018b7dda2..be0d933e2c5b5 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java @@ -436,6 +436,34 @@ public class NotificationPanelViewControllerTest extends NotificationPanelViewCo assertKeyguardStatusViewCentered(); } + @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/notification/NotificationWakeUpCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinatorTest.kt index 6df62bb539d71..aff705f1f3bfe 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 @@ -31,6 +31,7 @@ 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 @@ -43,6 +44,7 @@ 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 @@ -59,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 @@ -207,6 +210,76 @@ class NotificationWakeUpCoordinatorTest : SysuiTestCase() { 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()