diff options
9 files changed, 366 insertions, 18 deletions
diff --git a/libs/dream/lowlight/src/com/android/dream/lowlight/LowLightDreamManager.java b/libs/dream/lowlight/src/com/android/dream/lowlight/LowLightDreamManager.java index 5ecec4ddd1ad..3125f088c72b 100644 --- a/libs/dream/lowlight/src/com/android/dream/lowlight/LowLightDreamManager.java +++ b/libs/dream/lowlight/src/com/android/dream/lowlight/LowLightDreamManager.java @@ -72,6 +72,7 @@ public final class LowLightDreamManager { public static final int AMBIENT_LIGHT_MODE_LOW_LIGHT = 2; private final DreamManager mDreamManager; + private final LowLightTransitionCoordinator mLowLightTransitionCoordinator; @Nullable private final ComponentName mLowLightDreamComponent; @@ -81,8 +82,10 @@ public final class LowLightDreamManager { @Inject public LowLightDreamManager( DreamManager dreamManager, + LowLightTransitionCoordinator lowLightTransitionCoordinator, @Named(LOW_LIGHT_DREAM_COMPONENT) @Nullable ComponentName lowLightDreamComponent) { mDreamManager = dreamManager; + mLowLightTransitionCoordinator = lowLightTransitionCoordinator; mLowLightDreamComponent = lowLightDreamComponent; } @@ -111,7 +114,9 @@ public final class LowLightDreamManager { mAmbientLightMode = ambientLightMode; - mDreamManager.setSystemDreamComponent(mAmbientLightMode == AMBIENT_LIGHT_MODE_LOW_LIGHT - ? mLowLightDreamComponent : null); + boolean shouldEnterLowLight = mAmbientLightMode == AMBIENT_LIGHT_MODE_LOW_LIGHT; + mLowLightTransitionCoordinator.notifyBeforeLowLightTransition(shouldEnterLowLight, + () -> mDreamManager.setSystemDreamComponent( + shouldEnterLowLight ? mLowLightDreamComponent : null)); } } diff --git a/libs/dream/lowlight/src/com/android/dream/lowlight/LowLightTransitionCoordinator.java b/libs/dream/lowlight/src/com/android/dream/lowlight/LowLightTransitionCoordinator.java new file mode 100644 index 000000000000..874a2d5af75e --- /dev/null +++ b/libs/dream/lowlight/src/com/android/dream/lowlight/LowLightTransitionCoordinator.java @@ -0,0 +1,111 @@ +/* + * 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 com.android.dream.lowlight; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.annotation.Nullable; + +import javax.inject.Inject; +import javax.inject.Singleton; + +/** + * Helper class that allows listening and running animations before entering or exiting low light. + */ +@Singleton +public class LowLightTransitionCoordinator { + /** + * Listener that is notified before low light entry. + */ + public interface LowLightEnterListener { + /** + * Callback that is notified before the device enters low light. + * + * @return an optional animator that will be waited upon before entering low light. + */ + Animator onBeforeEnterLowLight(); + } + + /** + * Listener that is notified before low light exit. + */ + public interface LowLightExitListener { + /** + * Callback that is notified before the device exits low light. + * + * @return an optional animator that will be waited upon before exiting low light. + */ + Animator onBeforeExitLowLight(); + } + + private LowLightEnterListener mLowLightEnterListener; + private LowLightExitListener mLowLightExitListener; + + @Inject + public LowLightTransitionCoordinator() { + } + + /** + * Sets the listener for the low light enter event. + * + * Only one listener can be set at a time. This method will overwrite any previously set + * listener. Null can be used to unset the listener. + */ + public void setLowLightEnterListener(@Nullable LowLightEnterListener lowLightEnterListener) { + mLowLightEnterListener = lowLightEnterListener; + } + + /** + * Sets the listener for the low light exit event. + * + * Only one listener can be set at a time. This method will overwrite any previously set + * listener. Null can be used to unset the listener. + */ + public void setLowLightExitListener(@Nullable LowLightExitListener lowLightExitListener) { + mLowLightExitListener = lowLightExitListener; + } + + /** + * Notifies listeners that the device is about to enter or exit low light. + * + * @param entering true if listeners should be notified before entering low light, false if this + * is notifying before exiting. + * @param callback callback that will be run after listeners complete. + */ + void notifyBeforeLowLightTransition(boolean entering, Runnable callback) { + Animator animator = null; + + if (entering && mLowLightEnterListener != null) { + animator = mLowLightEnterListener.onBeforeEnterLowLight(); + } else if (!entering && mLowLightExitListener != null) { + animator = mLowLightExitListener.onBeforeExitLowLight(); + } + + // If the listener returned an animator to indicate it was running an animation, run the + // callback after the animation completes, otherwise call the callback directly. + if (animator != null) { + animator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animator) { + callback.run(); + } + }); + } else { + callback.run(); + } + } +} diff --git a/libs/dream/lowlight/tests/src/com.android.dream.lowlight/LowLightDreamManagerTest.java b/libs/dream/lowlight/tests/src/com.android.dream.lowlight/LowLightDreamManagerTest.java index 91a170f7ae14..4b95d8c84bac 100644 --- a/libs/dream/lowlight/tests/src/com.android.dream.lowlight/LowLightDreamManagerTest.java +++ b/libs/dream/lowlight/tests/src/com.android.dream.lowlight/LowLightDreamManagerTest.java @@ -21,7 +21,10 @@ import static com.android.dream.lowlight.LowLightDreamManager.AMBIENT_LIGHT_MODE import static com.android.dream.lowlight.LowLightDreamManager.AMBIENT_LIGHT_MODE_UNKNOWN; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.clearInvocations; +import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; @@ -44,44 +47,52 @@ public class LowLightDreamManagerTest { private DreamManager mDreamManager; @Mock + private LowLightTransitionCoordinator mTransitionCoordinator; + + @Mock private ComponentName mDreamComponent; + LowLightDreamManager mLowLightDreamManager; + @Before public void setUp() { MockitoAnnotations.initMocks(this); + + // Automatically run any provided Runnable to mTransitionCoordinator to simplify testing. + doAnswer(invocation -> { + ((Runnable) invocation.getArgument(1)).run(); + return null; + }).when(mTransitionCoordinator).notifyBeforeLowLightTransition(anyBoolean(), + any(Runnable.class)); + + mLowLightDreamManager = new LowLightDreamManager(mDreamManager, mTransitionCoordinator, + mDreamComponent); } @Test public void setAmbientLightMode_lowLight_setSystemDream() { - final LowLightDreamManager lowLightDreamManager = new LowLightDreamManager(mDreamManager, - mDreamComponent); - - lowLightDreamManager.setAmbientLightMode(AMBIENT_LIGHT_MODE_LOW_LIGHT); + mLowLightDreamManager.setAmbientLightMode(AMBIENT_LIGHT_MODE_LOW_LIGHT); + verify(mTransitionCoordinator).notifyBeforeLowLightTransition(eq(true), any()); verify(mDreamManager).setSystemDreamComponent(mDreamComponent); } @Test public void setAmbientLightMode_regularLight_clearSystemDream() { - final LowLightDreamManager lowLightDreamManager = new LowLightDreamManager(mDreamManager, - mDreamComponent); - - lowLightDreamManager.setAmbientLightMode(AMBIENT_LIGHT_MODE_REGULAR); + mLowLightDreamManager.setAmbientLightMode(AMBIENT_LIGHT_MODE_REGULAR); + verify(mTransitionCoordinator).notifyBeforeLowLightTransition(eq(false), any()); verify(mDreamManager).setSystemDreamComponent(null); } @Test public void setAmbientLightMode_defaultUnknownMode_clearSystemDream() { - final LowLightDreamManager lowLightDreamManager = new LowLightDreamManager(mDreamManager, - mDreamComponent); - // Set to low light first. - lowLightDreamManager.setAmbientLightMode(AMBIENT_LIGHT_MODE_LOW_LIGHT); + mLowLightDreamManager.setAmbientLightMode(AMBIENT_LIGHT_MODE_LOW_LIGHT); clearInvocations(mDreamManager); // Return to default unknown mode. - lowLightDreamManager.setAmbientLightMode(AMBIENT_LIGHT_MODE_UNKNOWN); + mLowLightDreamManager.setAmbientLightMode(AMBIENT_LIGHT_MODE_UNKNOWN); verify(mDreamManager).setSystemDreamComponent(null); } @@ -89,7 +100,7 @@ public class LowLightDreamManagerTest { @Test public void setAmbientLightMode_dreamComponentNotSet_doNothing() { final LowLightDreamManager lowLightDreamManager = new LowLightDreamManager(mDreamManager, - null /*dream component*/); + mTransitionCoordinator, null /*dream component*/); lowLightDreamManager.setAmbientLightMode(AMBIENT_LIGHT_MODE_LOW_LIGHT); diff --git a/libs/dream/lowlight/tests/src/com.android.dream.lowlight/LowLightTransitionCoordinatorTest.java b/libs/dream/lowlight/tests/src/com.android.dream.lowlight/LowLightTransitionCoordinatorTest.java new file mode 100644 index 000000000000..81e1e33d6220 --- /dev/null +++ b/libs/dream/lowlight/tests/src/com.android.dream.lowlight/LowLightTransitionCoordinatorTest.java @@ -0,0 +1,113 @@ +/* + * 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 com.android.dream.lowlight; + +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.Mockito.when; + +import android.animation.Animator; +import android.testing.AndroidTestingRunner; + +import androidx.test.filters.SmallTest; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +@SmallTest +@RunWith(AndroidTestingRunner.class) +public class LowLightTransitionCoordinatorTest { + @Mock + private LowLightTransitionCoordinator.LowLightEnterListener mEnterListener; + + @Mock + private LowLightTransitionCoordinator.LowLightExitListener mExitListener; + + @Mock + private Animator mAnimator; + + @Captor + private ArgumentCaptor<Animator.AnimatorListener> mAnimatorListenerCaptor; + + @Mock + private Runnable mRunnable; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + } + + @Test + public void onEnterCalledOnListeners() { + LowLightTransitionCoordinator coordinator = new LowLightTransitionCoordinator(); + + coordinator.setLowLightEnterListener(mEnterListener); + + coordinator.notifyBeforeLowLightTransition(true, mRunnable); + + verify(mEnterListener).onBeforeEnterLowLight(); + verify(mRunnable).run(); + } + + @Test + public void onExitCalledOnListeners() { + LowLightTransitionCoordinator coordinator = new LowLightTransitionCoordinator(); + + coordinator.setLowLightExitListener(mExitListener); + + coordinator.notifyBeforeLowLightTransition(false, mRunnable); + + verify(mExitListener).onBeforeExitLowLight(); + verify(mRunnable).run(); + } + + @Test + public void listenerNotCalledAfterRemoval() { + LowLightTransitionCoordinator coordinator = new LowLightTransitionCoordinator(); + + coordinator.setLowLightEnterListener(mEnterListener); + coordinator.setLowLightEnterListener(null); + + coordinator.notifyBeforeLowLightTransition(true, mRunnable); + + verifyZeroInteractions(mEnterListener); + verify(mRunnable).run(); + } + + @Test + public void runnableCalledAfterAnimationEnds() { + when(mEnterListener.onBeforeEnterLowLight()).thenReturn(mAnimator); + + LowLightTransitionCoordinator coordinator = new LowLightTransitionCoordinator(); + coordinator.setLowLightEnterListener(mEnterListener); + + coordinator.notifyBeforeLowLightTransition(true, mRunnable); + + // Animator listener is added and the runnable is not run yet. + verify(mAnimator).addListener(mAnimatorListenerCaptor.capture()); + verifyZeroInteractions(mRunnable); + + // Runnable is run once the animation ends. + mAnimatorListenerCaptor.getValue().onAnimationEnd(null); + verify(mRunnable).run(); + } +} diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml index 3f84ddb2a067..f545dae05b56 100644 --- a/packages/SystemUI/res/values/config.xml +++ b/packages/SystemUI/res/values/config.xml @@ -774,7 +774,7 @@ <!-- Duration in milliseconds of the dream in complications fade-in animation. --> <integer name="config_dreamOverlayInComplicationsDurationMs">250</integer> <!-- Duration in milliseconds of the y-translation animation when entering a dream --> - <integer name="config_dreamOverlayInTranslationYDurationMs">917</integer> + <integer name="config_dreamOverlayInTranslationYDurationMs">1167</integer> <!-- Delay in milliseconds before switching to the dock user and dreaming if a secondary user is active when the device is locked and docked. 0 indicates disabled. Default is 1 minute. --> diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayAnimationsController.kt b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayAnimationsController.kt index 7ecf63ead4e2..d0a92f0846d0 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayAnimationsController.kt +++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayAnimationsController.kt @@ -174,6 +174,71 @@ constructor( } } + /** + * Starts the dream content and dream overlay exit animations. + * + * This should only be used when the low light dream is entering, animations to/from other SysUI + * views is controlled by `transitionViewModel`. + */ + // TODO(b/256916668): integrate with the keyguard transition model once dream surfaces work is + // done. + @JvmOverloads + fun startExitAnimations(animatorBuilder: () -> AnimatorSet = { AnimatorSet() }): Animator { + cancelAnimations() + + mAnimator = + animatorBuilder().apply { + playTogether( + translationYAnimator( + from = 0f, + to = -mDreamInTranslationYDistance.toFloat(), + durationMs = mDreamInTranslationYDurationMs, + delayMs = 0, + interpolator = Interpolators.EMPHASIZED + ), + alphaAnimator( + from = + mCurrentAlphaAtPosition.getOrDefault( + key = POSITION_BOTTOM, + defaultValue = 1f + ), + to = 0f, + durationMs = mDreamInComplicationsAnimDurationMs, + delayMs = 0, + positions = POSITION_BOTTOM + ) + .apply { + doOnEnd { + // The logical end of the animation is once the alpha and blur + // animations finish, end the animation so that any listeners are + // notified. The Y translation animation is much longer than all of + // the other animations due to how the spec is defined, but is not + // expected to run to completion. + mAnimator?.end() + } + }, + alphaAnimator( + from = + mCurrentAlphaAtPosition.getOrDefault( + key = POSITION_TOP, + defaultValue = 1f + ), + to = 0f, + durationMs = mDreamInComplicationsAnimDurationMs, + delayMs = 0, + positions = POSITION_TOP + ) + ) + doOnEnd { + mAnimator = null + mOverlayStateController.setExitAnimationsRunning(false) + } + start() + } + mOverlayStateController.setExitAnimationsRunning(true) + return mAnimator as AnimatorSet + } + /** Starts the dream content and dream overlay exit animations. */ fun wakeUp(doneCallback: Runnable, executor: DelayableExecutor) { cancelAnimations() diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayContainerViewController.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayContainerViewController.java index 87396db31c91..4b478cdca9f9 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayContainerViewController.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayContainerViewController.java @@ -23,6 +23,7 @@ import static com.android.systemui.doze.util.BurnInHelperKt.getBurnInOffset; import static com.android.systemui.dreams.complication.ComplicationLayoutParams.POSITION_BOTTOM; import static com.android.systemui.dreams.complication.ComplicationLayoutParams.POSITION_TOP; +import android.animation.Animator; import android.content.res.Resources; import android.os.Handler; import android.util.MathUtils; @@ -31,6 +32,7 @@ import android.view.ViewGroup; import androidx.annotation.NonNull; +import com.android.dream.lowlight.LowLightTransitionCoordinator; import com.android.systemui.R; import com.android.systemui.animation.Interpolators; import com.android.systemui.dagger.qualifiers.Main; @@ -54,11 +56,14 @@ import javax.inject.Named; * View controller for {@link DreamOverlayContainerView}. */ @DreamOverlayComponent.DreamOverlayScope -public class DreamOverlayContainerViewController extends ViewController<DreamOverlayContainerView> { +public class DreamOverlayContainerViewController extends + ViewController<DreamOverlayContainerView> implements + LowLightTransitionCoordinator.LowLightEnterListener { private final DreamOverlayStatusBarViewController mStatusBarViewController; private final BlurUtils mBlurUtils; private final DreamOverlayAnimationsController mDreamOverlayAnimationsController; private final DreamOverlayStateController mStateController; + private final LowLightTransitionCoordinator mLowLightTransitionCoordinator; private final ComplicationHostViewController mComplicationHostViewController; @@ -164,6 +169,7 @@ public class DreamOverlayContainerViewController extends ViewController<DreamOve ComplicationHostViewController complicationHostViewController, @Named(DreamOverlayModule.DREAM_OVERLAY_CONTENT_VIEW) ViewGroup contentView, DreamOverlayStatusBarViewController statusBarViewController, + LowLightTransitionCoordinator lowLightTransitionCoordinator, BlurUtils blurUtils, @Main Handler handler, @Main Resources resources, @@ -181,6 +187,7 @@ public class DreamOverlayContainerViewController extends ViewController<DreamOve mBlurUtils = blurUtils; mDreamOverlayAnimationsController = animationsController; mStateController = stateController; + mLowLightTransitionCoordinator = lowLightTransitionCoordinator; mBouncerlessScrimController = bouncerlessScrimController; mBouncerlessScrimController.addCallback(mBouncerlessExpansionCallback); @@ -207,6 +214,7 @@ public class DreamOverlayContainerViewController extends ViewController<DreamOve mStatusBarViewController.init(); mComplicationHostViewController.init(); mDreamOverlayAnimationsController.init(mView); + mLowLightTransitionCoordinator.setLowLightEnterListener(this); } @Override @@ -305,4 +313,12 @@ public class DreamOverlayContainerViewController extends ViewController<DreamOve mDreamOverlayAnimationsController.wakeUp(onAnimationEnd, callbackExecutor); } + + @Override + public Animator onBeforeEnterLowLight() { + // Return the animator so that the transition coordinator waits for the overlay exit + // animations to finish before entering low light, as otherwise the default DreamActivity + // animation plays immediately and there's no time for this animation to play. + return mDreamOverlayAnimationsController.startExitAnimations(); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayAnimationsControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayAnimationsControllerTest.kt index b8a08dfbb999..0a9470617a5f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayAnimationsControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayAnimationsControllerTest.kt @@ -1,5 +1,6 @@ package com.android.systemui.dreams +import android.animation.Animator import android.animation.AnimatorSet import android.animation.ValueAnimator import android.testing.AndroidTestingRunner @@ -11,6 +12,7 @@ import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransition import com.android.systemui.statusbar.BlurUtils import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.util.concurrency.DelayableExecutor +import com.android.systemui.util.mockito.argumentCaptor import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever import kotlinx.coroutines.Dispatchers @@ -74,6 +76,19 @@ class DreamOverlayAnimationsControllerTest : SysuiTestCase() { } @Test + fun testExitAnimationUpdatesState() { + controller.startExitAnimations(animatorBuilder = { mockAnimator }) + + verify(stateController).setExitAnimationsRunning(true) + + val captor = argumentCaptor<Animator.AnimatorListener>() + verify(mockAnimator).addListener(captor.capture()) + + captor.value.onAnimationEnd(mockAnimator) + verify(stateController).setExitAnimationsRunning(false) + } + + @Test fun testWakeUpCallsExecutor() { val mockExecutor: DelayableExecutor = mock() val mockCallback: Runnable = mock() diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayContainerViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayContainerViewControllerTest.java index 550993e6b3b8..2a72e7d85d3c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayContainerViewControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayContainerViewControllerTest.java @@ -34,6 +34,7 @@ import android.view.ViewTreeObserver; import androidx.test.filters.SmallTest; +import com.android.dream.lowlight.LowLightTransitionCoordinator; import com.android.keyguard.BouncerPanelExpansionCalculator; import com.android.systemui.SysuiTestCase; import com.android.systemui.dreams.complication.ComplicationHostViewController; @@ -66,6 +67,9 @@ public class DreamOverlayContainerViewControllerTest extends SysuiTestCase { DreamOverlayStatusBarViewController mDreamOverlayStatusBarViewController; @Mock + LowLightTransitionCoordinator mLowLightTransitionCoordinator; + + @Mock DreamOverlayContainerView mDreamOverlayContainerView; @Mock @@ -110,6 +114,7 @@ public class DreamOverlayContainerViewControllerTest extends SysuiTestCase { mComplicationHostViewController, mDreamOverlayContentView, mDreamOverlayStatusBarViewController, + mLowLightTransitionCoordinator, mBlurUtils, mHandler, mResources, @@ -235,6 +240,13 @@ public class DreamOverlayContainerViewControllerTest extends SysuiTestCase { } @Test + public void testStartsExitAnimationsBeforeEnteringLowLight() { + mController.onBeforeEnterLowLight(); + + verify(mAnimationsController).startExitAnimations(); + } + + @Test public void testCancelDreamEntryAnimationsOnDetached() { mController.onViewAttached(); mController.onViewDetached(); |