From 79c7aaa06e197f0faf6aa1bf2d278e8355cdf888 Mon Sep 17 00:00:00 2001 From: Matt Pietal Date: Tue, 6 Dec 2022 19:26:36 +0000 Subject: [PATCH] [DO NOT MERGE] Coordinated DREAMING->LOCKSCREEN animation Part 1: Update dream side. Shift to utilize KeyguardTransitionRepository as the source for all motion between states. DreamingToLockscreenTransitionViewModel contains all logic that subdivides the total animation time into flowables that emit events during only their proper subsection. This means one animator now runs the view animations across all spaces. As part of this, add DreamCallbackController as a data source for KeyguardRepository. The dream activity will signal the transition to begin via onWakeUp(), which will start the view motions, then run the unocclude transition, and then finally the lockscreen view motions. Part 2 coming next... lockscreen tweaks Test: atest KeyguardRepositoryImplTest DreamOverlayContainerViewControllerTest DreamOverlayAnimationsControllerTest DreamingToLockscreenTransitionViewModelTest DreamOverlayServiceTest Bug: 260631205 Change-Id: I9bcf877927d1e92e8c751d0b83d2dd70f15912f1 --- packages/SystemUI/res/values/config.xml | 21 --- .../dreams/DreamCallbackController.kt | 45 +++++ .../DreamOverlayAnimationsController.kt | 164 +++++++++--------- .../DreamOverlayContainerViewController.java | 13 +- .../systemui/dreams/DreamOverlayService.java | 13 +- .../dreams/dagger/DreamOverlayModule.java | 76 -------- .../keyguard/KeyguardViewMediator.java | 4 +- .../data/repository/KeyguardRepository.kt | 45 +++-- .../KeyguardTransitionRepository.kt | 4 +- .../DreamingTransitionInteractor.kt | 12 +- .../KeyguardTransitionInteractor.kt | 24 +++ .../keyguard/shared/model/AnimationParams.kt | 25 +++ ...DreamingToLockscreenTransitionViewModel.kt | 79 +++++++++ .../dreams/DreamCallbackControllerTest.kt | 63 +++++++ .../DreamOverlayAnimationsControllerTest.kt | 78 +++------ ...eamOverlayContainerViewControllerTest.java | 4 +- .../dreams/DreamOverlayServiceTest.java | 7 +- .../repository/KeyguardRepositoryImplTest.kt | 26 ++- ...mingToLockscreenTransitionViewModelTest.kt | 127 ++++++++++++++ 19 files changed, 565 insertions(+), 265 deletions(-) create mode 100644 packages/SystemUI/src/com/android/systemui/dreams/DreamCallbackController.kt create mode 100644 packages/SystemUI/src/com/android/systemui/keyguard/shared/model/AnimationParams.kt create mode 100644 packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModel.kt create mode 100644 packages/SystemUI/tests/src/com/android/systemui/dreams/DreamCallbackControllerTest.kt create mode 100644 packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModelTest.kt diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml index 2d756ae25b3c4..f0cc42e541312 100644 --- a/packages/SystemUI/res/values/config.xml +++ b/packages/SystemUI/res/values/config.xml @@ -743,27 +743,6 @@ 240000 - - 333 - - 33 - - 117 - - 200 - - 217 - - 133 - - 250 - 500 500 diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamCallbackController.kt b/packages/SystemUI/src/com/android/systemui/dreams/DreamCallbackController.kt new file mode 100644 index 0000000000000..ab4632b08fa19 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamCallbackController.kt @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.dreams + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.statusbar.policy.CallbackController +import javax.inject.Inject + +/** Dream-related callback information */ +@SysUISingleton +class DreamCallbackController @Inject constructor() : + CallbackController { + + private val callbacks = mutableSetOf() + + override fun addCallback(callback: DreamCallbackController.DreamCallback) { + callbacks.add(callback) + } + + override fun removeCallback(callback: DreamCallbackController.DreamCallback) { + callbacks.remove(callback) + } + + fun onWakeUp() { + callbacks.forEach { it.onWakeUp() } + } + + interface DreamCallback { + fun onWakeUp() + } +} diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayAnimationsController.kt b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayAnimationsController.kt index 9b8ef71882e93..abe9355d33752 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayAnimationsController.kt +++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayAnimationsController.kt @@ -22,6 +22,9 @@ import android.animation.ValueAnimator import android.view.View import android.view.animation.Interpolator import androidx.core.animation.doOnEnd +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.repeatOnLifecycle +import com.android.systemui.R import com.android.systemui.animation.Interpolators import com.android.systemui.dreams.complication.ComplicationHostViewController import com.android.systemui.dreams.complication.ComplicationLayoutParams @@ -29,10 +32,20 @@ import com.android.systemui.dreams.complication.ComplicationLayoutParams.POSITIO import com.android.systemui.dreams.complication.ComplicationLayoutParams.POSITION_TOP import com.android.systemui.dreams.complication.ComplicationLayoutParams.Position import com.android.systemui.dreams.dagger.DreamOverlayModule +import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel +import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel.Companion.DREAM_ANIMATION_DURATION +import com.android.systemui.lifecycle.repeatWhenAttached import com.android.systemui.statusbar.BlurUtils import com.android.systemui.statusbar.CrossFadeHelper +import com.android.systemui.statusbar.policy.ConfigurationController +import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener +import com.android.systemui.util.concurrency.DelayableExecutor import javax.inject.Inject import javax.inject.Named +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.launch /** Controller for dream overlay animations. */ class DreamOverlayAnimationsController @@ -43,6 +56,8 @@ constructor( private val mStatusBarViewController: DreamOverlayStatusBarViewController, private val mOverlayStateController: DreamOverlayStateController, @Named(DreamOverlayModule.DREAM_BLUR_RADIUS) private val mDreamBlurRadius: Int, + private val transitionViewModel: DreamingToLockscreenTransitionViewModel, + private val configController: ConfigurationController, @Named(DreamOverlayModule.DREAM_IN_BLUR_ANIMATION_DURATION) private val mDreamInBlurAnimDurationMs: Long, @Named(DreamOverlayModule.DREAM_IN_COMPLICATIONS_ANIMATION_DURATION) @@ -51,22 +66,10 @@ constructor( private val mDreamInTranslationYDistance: Int, @Named(DreamOverlayModule.DREAM_IN_TRANSLATION_Y_DURATION) private val mDreamInTranslationYDurationMs: Long, - @Named(DreamOverlayModule.DREAM_OUT_TRANSLATION_Y_DISTANCE) - private val mDreamOutTranslationYDistance: Int, - @Named(DreamOverlayModule.DREAM_OUT_TRANSLATION_Y_DURATION) - private val mDreamOutTranslationYDurationMs: Long, - @Named(DreamOverlayModule.DREAM_OUT_TRANSLATION_Y_DELAY_BOTTOM) - private val mDreamOutTranslationYDelayBottomMs: Long, - @Named(DreamOverlayModule.DREAM_OUT_TRANSLATION_Y_DELAY_TOP) - private val mDreamOutTranslationYDelayTopMs: Long, - @Named(DreamOverlayModule.DREAM_OUT_ALPHA_DURATION) private val mDreamOutAlphaDurationMs: Long, - @Named(DreamOverlayModule.DREAM_OUT_ALPHA_DELAY_BOTTOM) - private val mDreamOutAlphaDelayBottomMs: Long, - @Named(DreamOverlayModule.DREAM_OUT_ALPHA_DELAY_TOP) private val mDreamOutAlphaDelayTopMs: Long, - @Named(DreamOverlayModule.DREAM_OUT_BLUR_DURATION) private val mDreamOutBlurDurationMs: Long ) { private var mAnimator: Animator? = null + private lateinit var view: View /** * Store the current alphas at the various positions. This is so that we may resume an animation @@ -76,9 +79,63 @@ constructor( private var mCurrentBlurRadius: Float = 0f + fun init(view: View) { + this.view = view + + view.repeatWhenAttached { + val configurationBasedDimensions = MutableStateFlow(loadFromResources(view)) + val configCallback = + object : ConfigurationListener { + override fun onDensityOrFontScaleChanged() { + configurationBasedDimensions.value = loadFromResources(view) + } + } + + configController.addCallback(configCallback) + + repeatOnLifecycle(Lifecycle.State.CREATED) { + /* Translation animations, when moving from DREAMING->LOCKSCREEN state */ + launch { + configurationBasedDimensions + .flatMapLatest { + transitionViewModel.dreamOverlayTranslationY(it.translationYPx) + } + .collect { px -> + setElementsTranslationYAtPosition( + px, + ComplicationLayoutParams.POSITION_TOP + ) + setElementsTranslationYAtPosition( + px, + ComplicationLayoutParams.POSITION_BOTTOM + ) + } + } + + /* Alpha animations, when moving from DREAMING->LOCKSCREEN state */ + launch { + transitionViewModel.dreamOverlayAlpha.collect { alpha -> + setElementsAlphaAtPosition( + alpha = alpha, + position = ComplicationLayoutParams.POSITION_TOP, + fadingOut = true, + ) + setElementsAlphaAtPosition( + alpha = alpha, + position = ComplicationLayoutParams.POSITION_BOTTOM, + fadingOut = true, + ) + } + } + } + + configController.removeCallback(configCallback) + } + } + /** Starts the dream content and dream overlay entry animations. */ @JvmOverloads - fun startEntryAnimations(view: View, animatorBuilder: () -> AnimatorSet = { AnimatorSet() }) { + fun startEntryAnimations(animatorBuilder: () -> AnimatorSet = { AnimatorSet() }) { cancelAnimations() mAnimator = @@ -113,73 +170,9 @@ constructor( } /** Starts the dream content and dream overlay exit animations. */ - @JvmOverloads - fun startExitAnimations( - view: View, - doneCallback: () -> Unit, - animatorBuilder: () -> AnimatorSet = { AnimatorSet() } - ) { + fun wakeUp(doneCallback: Runnable, executor: DelayableExecutor) { cancelAnimations() - - mAnimator = - animatorBuilder().apply { - playTogether( - blurAnimator( - view = view, - // Start the blurring wherever the entry animation ended, in - // case it was cancelled early. - fromBlurRadius = mCurrentBlurRadius, - toBlurRadius = mDreamBlurRadius.toFloat(), - durationMs = mDreamOutBlurDurationMs, - interpolator = Interpolators.EMPHASIZED_ACCELERATE - ), - translationYAnimator( - from = 0f, - to = mDreamOutTranslationYDistance.toFloat(), - durationMs = mDreamOutTranslationYDurationMs, - delayMs = mDreamOutTranslationYDelayBottomMs, - positions = POSITION_BOTTOM, - interpolator = Interpolators.EMPHASIZED_ACCELERATE - ), - translationYAnimator( - from = 0f, - to = mDreamOutTranslationYDistance.toFloat(), - durationMs = mDreamOutTranslationYDurationMs, - delayMs = mDreamOutTranslationYDelayTopMs, - positions = POSITION_TOP, - interpolator = Interpolators.EMPHASIZED_ACCELERATE - ), - alphaAnimator( - from = - mCurrentAlphaAtPosition.getOrDefault( - key = POSITION_BOTTOM, - defaultValue = 1f - ), - to = 0f, - durationMs = mDreamOutAlphaDurationMs, - delayMs = mDreamOutAlphaDelayBottomMs, - positions = POSITION_BOTTOM - ), - alphaAnimator( - from = - mCurrentAlphaAtPosition.getOrDefault( - key = POSITION_TOP, - defaultValue = 1f - ), - to = 0f, - durationMs = mDreamOutAlphaDurationMs, - delayMs = mDreamOutAlphaDelayTopMs, - positions = POSITION_TOP - ) - ) - doOnEnd { - mAnimator = null - mOverlayStateController.setExitAnimationsRunning(false) - doneCallback() - } - start() - } - mOverlayStateController.setExitAnimationsRunning(true) + executor.executeDelayed(doneCallback, DREAM_ANIMATION_DURATION.inWholeMilliseconds) } /** Cancels the dream content and dream overlay animations, if they're currently running. */ @@ -288,4 +281,15 @@ constructor( mStatusBarViewController.setTranslationY(translationY) } } + + private fun loadFromResources(view: View): ConfigurationBasedDimensions { + return ConfigurationBasedDimensions( + translationYPx = + view.resources.getDimensionPixelSize(R.dimen.dream_overlay_exit_y_offset), + ) + } + + private data class ConfigurationBasedDimensions( + val translationYPx: Int, + ) } diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayContainerViewController.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayContainerViewController.java index 9d7ad305c7e50..3106173d81b22 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayContainerViewController.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayContainerViewController.java @@ -42,9 +42,9 @@ import com.android.systemui.statusbar.BlurUtils; import com.android.systemui.statusbar.phone.KeyguardBouncer; import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager; import com.android.systemui.util.ViewController; +import com.android.systemui.util.concurrency.DelayableExecutor; import java.util.Arrays; -import java.util.concurrent.Executor; import javax.inject.Inject; import javax.inject.Named; @@ -170,6 +170,7 @@ public class DreamOverlayContainerViewController extends ViewController { - callbackExecutor.execute(onAnimationEnd); - return null; - }); + public void wakeUp(@NonNull Runnable onAnimationEnd, + @NonNull DelayableExecutor callbackExecutor) { + mDreamOverlayAnimationsController.wakeUp(onAnimationEnd, callbackExecutor); } } diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java index e76d5b30d3e61..1be9cd1986046 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java @@ -43,8 +43,7 @@ import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dreams.complication.Complication; import com.android.systemui.dreams.dagger.DreamOverlayComponent; import com.android.systemui.dreams.touch.DreamOverlayTouchMonitor; - -import java.util.concurrent.Executor; +import com.android.systemui.util.concurrency.DelayableExecutor; import javax.inject.Inject; import javax.inject.Named; @@ -61,10 +60,11 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ // The Context is used to construct the hosting constraint layout and child overlay views. private final Context mContext; // The Executor ensures actions and ui updates happen on the same thread. - private final Executor mExecutor; + private final DelayableExecutor mExecutor; // A controller for the dream overlay container view (which contains both the status bar and the // content area). private DreamOverlayContainerViewController mDreamOverlayContainerViewController; + private final DreamCallbackController mDreamCallbackController; private final KeyguardUpdateMonitor mKeyguardUpdateMonitor; @Nullable private final ComponentName mLowLightDreamComponent; @@ -126,14 +126,15 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ @Inject public DreamOverlayService( Context context, - @Main Executor executor, + @Main DelayableExecutor executor, WindowManager windowManager, DreamOverlayComponent.Factory dreamOverlayComponentFactory, DreamOverlayStateController stateController, KeyguardUpdateMonitor keyguardUpdateMonitor, UiEventLogger uiEventLogger, @Nullable @Named(LowLightDreamModule.LOW_LIGHT_DREAM_COMPONENT) - ComponentName lowLightDreamComponent) { + ComponentName lowLightDreamComponent, + DreamCallbackController dreamCallbackController) { mContext = context; mExecutor = executor; mWindowManager = windowManager; @@ -142,6 +143,7 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ mKeyguardUpdateMonitor.registerCallback(mKeyguardCallback); mStateController = stateController; mUiEventLogger = uiEventLogger; + mDreamCallbackController = dreamCallbackController; final ViewModelStore viewModelStore = new ViewModelStore(); final Complication.Host host = @@ -217,6 +219,7 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ public void onWakeUp(@NonNull Runnable onCompletedCallback) { mExecutor.execute(() -> { if (mDreamOverlayContainerViewController != null) { + mDreamCallbackController.onWakeUp(); mDreamOverlayContainerViewController.wakeUp(onCompletedCallback, mExecutor); } }); diff --git a/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamOverlayModule.java b/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamOverlayModule.java index 4f1ac1a8abd5e..0d58a69d498c6 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamOverlayModule.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamOverlayModule.java @@ -55,22 +55,6 @@ public abstract class DreamOverlayModule { "dream_in_complications_translation_y"; public static final String DREAM_IN_TRANSLATION_Y_DURATION = "dream_in_complications_translation_y_duration"; - public static final String DREAM_OUT_TRANSLATION_Y_DISTANCE = - "dream_out_complications_translation_y"; - public static final String DREAM_OUT_TRANSLATION_Y_DURATION = - "dream_out_complications_translation_y_duration"; - public static final String DREAM_OUT_TRANSLATION_Y_DELAY_BOTTOM = - "dream_out_complications_translation_y_delay_bottom"; - public static final String DREAM_OUT_TRANSLATION_Y_DELAY_TOP = - "dream_out_complications_translation_y_delay_top"; - public static final String DREAM_OUT_ALPHA_DURATION = - "dream_out_complications_alpha_duration"; - public static final String DREAM_OUT_ALPHA_DELAY_BOTTOM = - "dream_out_complications_alpha_delay_bottom"; - public static final String DREAM_OUT_ALPHA_DELAY_TOP = - "dream_out_complications_alpha_delay_top"; - public static final String DREAM_OUT_BLUR_DURATION = - "dream_out_blur_duration"; /** */ @Provides @@ -185,66 +169,6 @@ public abstract class DreamOverlayModule { return (long) resources.getInteger(R.integer.config_dreamOverlayInTranslationYDurationMs); } - /** - * Provides the number of pixels to translate complications when waking up from dream. - */ - @Provides - @Named(DREAM_OUT_TRANSLATION_Y_DISTANCE) - @DreamOverlayComponent.DreamOverlayScope - static int providesDreamOutComplicationsTranslationY(@Main Resources resources) { - return resources.getDimensionPixelSize(R.dimen.dream_overlay_exit_y_offset); - } - - @Provides - @Named(DREAM_OUT_TRANSLATION_Y_DURATION) - @DreamOverlayComponent.DreamOverlayScope - static long providesDreamOutComplicationsTranslationYDuration(@Main Resources resources) { - return (long) resources.getInteger(R.integer.config_dreamOverlayOutTranslationYDurationMs); - } - - @Provides - @Named(DREAM_OUT_TRANSLATION_Y_DELAY_BOTTOM) - @DreamOverlayComponent.DreamOverlayScope - static long providesDreamOutComplicationsTranslationYDelayBottom(@Main Resources resources) { - return (long) resources.getInteger( - R.integer.config_dreamOverlayOutTranslationYDelayBottomMs); - } - - @Provides - @Named(DREAM_OUT_TRANSLATION_Y_DELAY_TOP) - @DreamOverlayComponent.DreamOverlayScope - static long providesDreamOutComplicationsTranslationYDelayTop(@Main Resources resources) { - return (long) resources.getInteger(R.integer.config_dreamOverlayOutTranslationYDelayTopMs); - } - - @Provides - @Named(DREAM_OUT_ALPHA_DURATION) - @DreamOverlayComponent.DreamOverlayScope - static long providesDreamOutComplicationsAlphaDuration(@Main Resources resources) { - return (long) resources.getInteger(R.integer.config_dreamOverlayOutAlphaDurationMs); - } - - @Provides - @Named(DREAM_OUT_ALPHA_DELAY_BOTTOM) - @DreamOverlayComponent.DreamOverlayScope - static long providesDreamOutComplicationsAlphaDelayBottom(@Main Resources resources) { - return (long) resources.getInteger(R.integer.config_dreamOverlayOutAlphaDelayBottomMs); - } - - @Provides - @Named(DREAM_OUT_ALPHA_DELAY_TOP) - @DreamOverlayComponent.DreamOverlayScope - static long providesDreamOutComplicationsAlphaDelayTop(@Main Resources resources) { - return (long) resources.getInteger(R.integer.config_dreamOverlayOutAlphaDelayTopMs); - } - - @Provides - @Named(DREAM_OUT_BLUR_DURATION) - @DreamOverlayComponent.DreamOverlayScope - static long providesDreamOutBlurDuration(@Main Resources resources) { - return (long) resources.getInteger(R.integer.config_dreamOverlayOutBlurDurationMs); - } - @Provides @DreamOverlayComponent.DreamOverlayScope static LifecycleOwner providesLifecycleOwner(Lazy lifecycleRegistryLazy) { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java index d231870997f31..ae714fbaa58b2 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java @@ -33,6 +33,7 @@ import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STR import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_TIMEOUT; import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_FOR_UNATTENDED_UPDATE; import static com.android.systemui.DejankUtils.whitelistIpcs; +import static com.android.systemui.keyguard.domain.interactor.DreamingTransitionInteractor.TO_LOCKSCREEN_DURATION_MS; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; @@ -1231,8 +1232,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, mDreamOpenAnimationDuration = context.getResources().getInteger( com.android.internal.R.integer.config_dreamOpenAnimationDuration); - mDreamCloseAnimationDuration = context.getResources().getInteger( - com.android.internal.R.integer.config_dreamCloseAnimationDuration); + mDreamCloseAnimationDuration = (int) TO_LOCKSCREEN_DURATION_MS; } public void userActivity() { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt index 148792ba779b3..9a0fbbf60f60c 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt @@ -29,6 +29,8 @@ import com.android.systemui.doze.DozeHost import com.android.systemui.doze.DozeMachine import com.android.systemui.doze.DozeTransitionCallback import com.android.systemui.doze.DozeTransitionListener +import com.android.systemui.dreams.DreamCallbackController +import com.android.systemui.dreams.DreamCallbackController.DreamCallback import com.android.systemui.keyguard.WakefulnessLifecycle import com.android.systemui.keyguard.shared.model.BiometricUnlockModel import com.android.systemui.keyguard.shared.model.BiometricUnlockSource @@ -47,6 +49,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.merge /** Defines interface for classes that encapsulate application state for the keyguard. */ interface KeyguardRepository { @@ -176,6 +179,7 @@ constructor( private val keyguardUpdateMonitor: KeyguardUpdateMonitor, private val dozeTransitionListener: DozeTransitionListener, private val authController: AuthController, + private val dreamCallbackController: DreamCallbackController, ) : KeyguardRepository { private val _animateBottomAreaDozingTransitions = MutableStateFlow(false) override val animateBottomAreaDozingTransitions = @@ -276,22 +280,35 @@ constructor( .distinctUntilChanged() override val isDreaming: Flow = - conflatedCallbackFlow { - val callback = - object : KeyguardUpdateMonitorCallback() { - override fun onDreamingStateChanged(isDreaming: Boolean) { - trySendWithFailureLogging(isDreaming, TAG, "updated isDreaming") + merge( + conflatedCallbackFlow { + val callback = + object : KeyguardUpdateMonitorCallback() { + override fun onDreamingStateChanged(isDreaming: Boolean) { + trySendWithFailureLogging(isDreaming, TAG, "updated isDreaming") + } } - } - keyguardUpdateMonitor.registerCallback(callback) - trySendWithFailureLogging( - keyguardUpdateMonitor.isDreaming, - TAG, - "initial isDreaming", - ) + keyguardUpdateMonitor.registerCallback(callback) + trySendWithFailureLogging( + keyguardUpdateMonitor.isDreaming, + TAG, + "initial isDreaming", + ) - awaitClose { keyguardUpdateMonitor.removeCallback(callback) } - } + awaitClose { keyguardUpdateMonitor.removeCallback(callback) } + }, + conflatedCallbackFlow { + val callback = + object : DreamCallback { + override fun onWakeUp() { + trySendWithFailureLogging(false, TAG, "updated isDreaming") + } + } + dreamCallbackController.addCallback(callback) + + awaitClose { dreamCallbackController.removeCallback(callback) } + } + ) .distinctUntilChanged() override val linearDozeAmount: Flow = conflatedCallbackFlow { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt index 5bb586e489e55..d72d7183b0f0b 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt @@ -66,8 +66,8 @@ interface KeyguardTransitionRepository { } /** - * Begin a transition from one state to another. Will not start if another transition is in - * progress. + * Begin a transition from one state to another. Transitions are interruptible, and will issue a + * [TransitionStep] with state = [TransitionState.CANCELED] before beginning the next one. */ fun startTransition(info: TransitionInfo): UUID? diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/DreamingTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/DreamingTransitionInteractor.kt index b73ce9e5689c9..4d60579a75ac6 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/DreamingTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/DreamingTransitionInteractor.kt @@ -28,6 +28,8 @@ import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.TransitionInfo import com.android.systemui.util.kotlin.sample import javax.inject.Inject +import kotlin.time.Duration +import kotlin.time.Duration.Companion.milliseconds import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.combine @@ -110,7 +112,7 @@ constructor( name, KeyguardState.DREAMING, KeyguardState.LOCKSCREEN, - getAnimator(), + getAnimator(TO_LOCKSCREEN_DURATION), ) ) } @@ -167,14 +169,16 @@ constructor( } } - private fun getAnimator(): ValueAnimator { + private fun getAnimator(duration: Duration = DEFAULT_DURATION): ValueAnimator { return ValueAnimator().apply { setInterpolator(Interpolators.LINEAR) - setDuration(TRANSITION_DURATION_MS) + setDuration(duration.inWholeMilliseconds) } } companion object { - private const val TRANSITION_DURATION_MS = 500L + private val DEFAULT_DURATION = 500.milliseconds + val TO_LOCKSCREEN_DURATION = 1183.milliseconds + @JvmField val TO_LOCKSCREEN_DURATION_MS = TO_LOCKSCREEN_DURATION.inWholeMilliseconds } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt index 54a4f493d21d7..3b9d6f59a8e76 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt @@ -19,12 +19,15 @@ package com.android.systemui.keyguard.domain.interactor import com.android.systemui.dagger.SysUISingleton import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository +import com.android.systemui.keyguard.shared.model.AnimationParams import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.KeyguardState.AOD +import com.android.systemui.keyguard.shared.model.KeyguardState.DREAMING import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.keyguard.shared.model.TransitionStep import javax.inject.Inject +import kotlin.time.Duration import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.map @@ -43,6 +46,10 @@ constructor( /** LOCKSCREEN->AOD transition information. */ val lockscreenToAodTransition: Flow = repository.transition(LOCKSCREEN, AOD) + /** DREAMING->LOCKSCREEN transition information. */ + val dreamingToLockscreenTransition: Flow = + repository.transition(DREAMING, LOCKSCREEN) + /** (any)->AOD transition information */ val anyStateToAodTransition: Flow = repository.transitions.filter { step -> step.to == KeyguardState.AOD } @@ -72,4 +79,21 @@ constructor( /* The last completed [KeyguardState] transition */ val finishedKeyguardState: Flow = finishedKeyguardTransitionStep.map { step -> step.to } + + /** + * Transitions will occur over a [totalDuration] with [TransitionStep]s being emitted in the + * range of [0, 1]. View animations should begin and end within a subset of this range. This + * function maps the [startTime] and [duration] into [0, 1], when this subset is valid. + */ + fun transitionStepAnimation( + flow: Flow, + params: AnimationParams, + totalDuration: Duration, + ): Flow { + val start = (params.startTime / totalDuration).toFloat() + val chunks = (totalDuration / params.duration).toFloat() + return flow + .map { step -> (step.value - start) * chunks } + .filter { value -> value >= 0f && value <= 1f } + } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/AnimationParams.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/AnimationParams.kt new file mode 100644 index 0000000000000..67733e9052683 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/AnimationParams.kt @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ +package com.android.systemui.keyguard.shared.model + +import kotlin.time.Duration +import kotlin.time.Duration.Companion.milliseconds + +/** Animation parameters */ +data class AnimationParams( + val startTime: Duration = 0.milliseconds, + val duration: Duration, +) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModel.kt new file mode 100644 index 0000000000000..9b36e8c6a8af9 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModel.kt @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyguard.ui.viewmodel + +import com.android.systemui.animation.Interpolators.EMPHASIZED_ACCELERATE +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.keyguard.domain.interactor.DreamingTransitionInteractor.Companion.TO_LOCKSCREEN_DURATION +import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor +import com.android.systemui.keyguard.shared.model.AnimationParams +import javax.inject.Inject +import kotlin.time.Duration.Companion.milliseconds +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +/** + * Breaks down DREAMING->LOCKSCREEN transition into discrete steps for corresponding views to + * consume. + */ +@SysUISingleton +class DreamingToLockscreenTransitionViewModel +@Inject +constructor( + private val interactor: KeyguardTransitionInteractor, +) { + + /** Dream overlay y-translation on exit */ + fun dreamOverlayTranslationY(translatePx: Int): Flow { + return flowForAnimation(DREAM_OVERLAY_TRANSLATION_Y).map { value -> + EMPHASIZED_ACCELERATE.getInterpolation(value) * translatePx + } + } + /** Dream overlay views alpha - fade out */ + val dreamOverlayAlpha: Flow = flowForAnimation(DREAM_OVERLAY_ALPHA).map { 1f - it } + + /** Dream background alpha - fade out */ + val dreamBackgroundAlpha: Flow = flowForAnimation(DREAM_BACKGROUND_ALPHA).map { 1f - it } + + /** Lockscreen views y-translation */ + fun lockscreenTranslationY(translatePx: Int): Flow { + return flowForAnimation(LOCKSCREEN_TRANSLATION_Y).map { it * translatePx } + } + + /** Lockscreen views alpha */ + val lockscreenAlpha: Flow = flowForAnimation(LOCKSCREEN_ALPHA) + + private fun flowForAnimation(params: AnimationParams): Flow { + return interactor.transitionStepAnimation( + interactor.dreamingToLockscreenTransition, + params, + totalDuration = TO_LOCKSCREEN_DURATION + ) + } + + companion object { + /* Length of time before ending the dream activity, in order to start unoccluding */ + val DREAM_ANIMATION_DURATION = 250.milliseconds + + val DREAM_OVERLAY_TRANSLATION_Y = AnimationParams(duration = 500.milliseconds) + val DREAM_OVERLAY_ALPHA = AnimationParams(duration = 250.milliseconds) + val DREAM_BACKGROUND_ALPHA = AnimationParams(duration = 250.milliseconds) + val LOCKSCREEN_TRANSLATION_Y = AnimationParams(duration = TO_LOCKSCREEN_DURATION) + val LOCKSCREEN_ALPHA = + AnimationParams(startTime = 233.milliseconds, duration = 250.milliseconds) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamCallbackControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamCallbackControllerTest.kt new file mode 100644 index 0000000000000..003efbfdc4d7e --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamCallbackControllerTest.kt @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.systemui.dreams + +import android.testing.AndroidTestingRunner +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito.never +import org.mockito.Mockito.reset +import org.mockito.Mockito.times +import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations + +@SmallTest +@RunWith(AndroidTestingRunner::class) +class DreamCallbackControllerTest : SysuiTestCase() { + + @Mock private lateinit var callback: DreamCallbackController.DreamCallback + + private lateinit var underTest: DreamCallbackController + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + underTest = DreamCallbackController() + } + + @Test + fun testOnWakeUpInvokesCallback() { + underTest.addCallback(callback) + underTest.onWakeUp() + verify(callback).onWakeUp() + + // Adding twice should not invoke twice + reset(callback) + underTest.addCallback(callback) + underTest.onWakeUp() + verify(callback, times(1)).onWakeUp() + + // After remove, no call to callback + reset(callback) + underTest.removeCallback(callback) + underTest.onWakeUp() + verify(callback, never()).onWakeUp() + } +} 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 8e689cf8f17e8..6c23254941a83 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayAnimationsControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayAnimationsControllerTest.kt @@ -1,18 +1,25 @@ package com.android.systemui.dreams -import android.animation.Animator import android.animation.AnimatorSet import android.testing.AndroidTestingRunner +import android.view.View import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.dreams.complication.ComplicationHostViewController +import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel import com.android.systemui.statusbar.BlurUtils -import com.android.systemui.util.mockito.argumentCaptor +import com.android.systemui.statusbar.policy.ConfigurationController +import com.android.systemui.util.concurrency.DelayableExecutor import com.android.systemui.util.mockito.mock +import com.android.systemui.util.mockito.whenever +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runBlocking import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock +import org.mockito.Mockito.anyLong +import org.mockito.Mockito.eq import org.mockito.Mockito.never import org.mockito.Mockito.times import org.mockito.Mockito.verify @@ -28,14 +35,6 @@ class DreamOverlayAnimationsControllerTest : SysuiTestCase() { private const val DREAM_IN_COMPLICATIONS_ANIMATION_DURATION = 3L private const val DREAM_IN_TRANSLATION_Y_DISTANCE = 6 private const val DREAM_IN_TRANSLATION_Y_DURATION = 7L - private const val DREAM_OUT_TRANSLATION_Y_DISTANCE = 6 - private const val DREAM_OUT_TRANSLATION_Y_DURATION = 7L - private const val DREAM_OUT_TRANSLATION_Y_DELAY_BOTTOM = 8L - private const val DREAM_OUT_TRANSLATION_Y_DELAY_TOP = 9L - private const val DREAM_OUT_ALPHA_DURATION = 10L - private const val DREAM_OUT_ALPHA_DELAY_BOTTOM = 11L - private const val DREAM_OUT_ALPHA_DELAY_TOP = 12L - private const val DREAM_OUT_BLUR_DURATION = 13L } @Mock private lateinit var mockAnimator: AnimatorSet @@ -43,6 +42,8 @@ class DreamOverlayAnimationsControllerTest : SysuiTestCase() { @Mock private lateinit var hostViewController: ComplicationHostViewController @Mock private lateinit var statusBarViewController: DreamOverlayStatusBarViewController @Mock private lateinit var stateController: DreamOverlayStateController + @Mock private lateinit var configController: ConfigurationController + @Mock private lateinit var transitionViewModel: DreamingToLockscreenTransitionViewModel private lateinit var controller: DreamOverlayAnimationsController @Before @@ -55,71 +56,48 @@ class DreamOverlayAnimationsControllerTest : SysuiTestCase() { statusBarViewController, stateController, DREAM_BLUR_RADIUS, + transitionViewModel, + configController, DREAM_IN_BLUR_ANIMATION_DURATION, DREAM_IN_COMPLICATIONS_ANIMATION_DURATION, DREAM_IN_TRANSLATION_Y_DISTANCE, DREAM_IN_TRANSLATION_Y_DURATION, - DREAM_OUT_TRANSLATION_Y_DISTANCE, - DREAM_OUT_TRANSLATION_Y_DURATION, - DREAM_OUT_TRANSLATION_Y_DELAY_BOTTOM, - DREAM_OUT_TRANSLATION_Y_DELAY_TOP, - DREAM_OUT_ALPHA_DURATION, - DREAM_OUT_ALPHA_DELAY_BOTTOM, - DREAM_OUT_ALPHA_DELAY_TOP, - DREAM_OUT_BLUR_DURATION ) + + val mockView: View = mock() + whenever(mockView.resources).thenReturn(mContext.resources) + + runBlocking(Dispatchers.Main.immediate) { controller.init(mockView) } } @Test - fun testExitAnimationOnEnd() { - val mockCallback: () -> Unit = mock() + fun testWakeUpCallsExecutor() { + val mockExecutor: DelayableExecutor = mock() + val mockCallback: Runnable = mock() - controller.startExitAnimations( - view = mock(), + controller.wakeUp( doneCallback = mockCallback, - animatorBuilder = { mockAnimator } + executor = mockExecutor, ) - val captor = argumentCaptor() - verify(mockAnimator).addListener(captor.capture()) - val listener = captor.value - - verify(mockCallback, never()).invoke() - listener.onAnimationEnd(mockAnimator) - verify(mockCallback, times(1)).invoke() + verify(mockExecutor).executeDelayed(eq(mockCallback), anyLong()) } @Test - fun testCancellation() { - controller.startExitAnimations( - view = mock(), - doneCallback = mock(), - animatorBuilder = { mockAnimator } - ) - - verify(mockAnimator, never()).cancel() - controller.cancelAnimations() - verify(mockAnimator, times(1)).cancel() - } - - @Test - fun testExitAfterStartWillCancel() { + fun testWakeUpAfterStartWillCancel() { val mockStartAnimator: AnimatorSet = mock() - val mockExitAnimator: AnimatorSet = mock() - controller.startEntryAnimations(view = mock(), animatorBuilder = { mockStartAnimator }) + controller.startEntryAnimations(animatorBuilder = { mockStartAnimator }) verify(mockStartAnimator, never()).cancel() - controller.startExitAnimations( - view = mock(), + controller.wakeUp( doneCallback = mock(), - animatorBuilder = { mockExitAnimator } + executor = mock(), ) // Verify that we cancelled the start animator in favor of the exit // animator. verify(mockStartAnimator, times(1)).cancel() - verify(mockExitAnimator, never()).cancel() } } 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 73c226d11bc43..2799a25316d06 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayContainerViewControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayContainerViewControllerTest.java @@ -203,7 +203,7 @@ public class DreamOverlayContainerViewControllerTest extends SysuiTestCase { mController.onViewAttached(); - verify(mAnimationsController).startEntryAnimations(mDreamOverlayContainerView); + verify(mAnimationsController).startEntryAnimations(); verify(mAnimationsController, never()).cancelAnimations(); } @@ -213,7 +213,7 @@ public class DreamOverlayContainerViewControllerTest extends SysuiTestCase { mController.onViewAttached(); - verify(mAnimationsController, never()).startEntryAnimations(mDreamOverlayContainerView); + verify(mAnimationsController, never()).startEntryAnimations(); } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java index ffb8342a56a54..d6f8deabc270a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java @@ -113,6 +113,9 @@ public class DreamOverlayServiceTest extends SysuiTestCase { @Mock UiEventLogger mUiEventLogger; + @Mock + DreamCallbackController mDreamCallbackController; + @Captor ArgumentCaptor mViewCaptor; @@ -141,7 +144,8 @@ public class DreamOverlayServiceTest extends SysuiTestCase { mStateController, mKeyguardUpdateMonitor, mUiEventLogger, - LOW_LIGHT_COMPONENT); + LOW_LIGHT_COMPONENT, + mDreamCallbackController); } @Test @@ -353,6 +357,7 @@ public class DreamOverlayServiceTest extends SysuiTestCase { mService.onWakeUp(callback); mMainExecutor.runAllReady(); verify(mDreamOverlayContainerViewController).wakeUp(callback, mMainExecutor); + verify(mDreamCallbackController).onWakeUp(); } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt index 5deac1924ab78..563d44d3f15fd 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt @@ -28,6 +28,8 @@ import com.android.systemui.doze.DozeHost import com.android.systemui.doze.DozeMachine import com.android.systemui.doze.DozeTransitionCallback import com.android.systemui.doze.DozeTransitionListener +import com.android.systemui.dreams.DreamCallbackController +import com.android.systemui.dreams.DreamCallbackController.DreamCallback import com.android.systemui.keyguard.WakefulnessLifecycle import com.android.systemui.keyguard.shared.model.BiometricUnlockModel import com.android.systemui.keyguard.shared.model.BiometricUnlockSource @@ -66,6 +68,7 @@ class KeyguardRepositoryImplTest : SysuiTestCase() { @Mock private lateinit var dozeTransitionListener: DozeTransitionListener @Mock private lateinit var authController: AuthController @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor + @Mock private lateinit var dreamCallbackController: DreamCallbackController private lateinit var underTest: KeyguardRepositoryImpl @@ -83,6 +86,7 @@ class KeyguardRepositoryImplTest : SysuiTestCase() { keyguardUpdateMonitor, dozeTransitionListener, authController, + dreamCallbackController, ) } @@ -318,7 +322,7 @@ class KeyguardRepositoryImplTest : SysuiTestCase() { } @Test - fun isDreaming() = + fun isDreamingFromKeyguardUpdateMonitor() = runTest(UnconfinedTestDispatcher()) { whenever(keyguardUpdateMonitor.isDreaming()).thenReturn(false) var latest: Boolean? = null @@ -338,6 +342,26 @@ class KeyguardRepositoryImplTest : SysuiTestCase() { job.cancel() } + @Test + fun isDreamingFromDreamCallbackController() = + runTest(UnconfinedTestDispatcher()) { + whenever(keyguardUpdateMonitor.isDreaming()).thenReturn(true) + var latest: Boolean? = null + val job = underTest.isDreaming.onEach { latest = it }.launchIn(this) + + assertThat(latest).isTrue() + + val listener = + withArgCaptor { + verify(dreamCallbackController).addCallback(capture()) + } + + listener.onWakeUp() + assertThat(latest).isFalse() + + job.cancel() + } + @Test fun biometricUnlockState() = runTest(UnconfinedTestDispatcher()) { diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModelTest.kt new file mode 100644 index 0000000000000..81950f1c22e5c --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModelTest.kt @@ -0,0 +1,127 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyguard.ui.viewmodel + +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.animation.Interpolators.EMPHASIZED_ACCELERATE +import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository +import com.android.systemui.keyguard.domain.interactor.DreamingTransitionInteractor.Companion.TO_LOCKSCREEN_DURATION +import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor +import com.android.systemui.keyguard.shared.model.AnimationParams +import com.android.systemui.keyguard.shared.model.KeyguardState +import com.android.systemui.keyguard.shared.model.TransitionState +import com.android.systemui.keyguard.shared.model.TransitionStep +import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel.Companion.DREAM_OVERLAY_ALPHA +import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel.Companion.DREAM_OVERLAY_TRANSLATION_Y +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +@SmallTest +@RunWith(JUnit4::class) +class DreamingToLockscreenTransitionViewModelTest : SysuiTestCase() { + private lateinit var underTest: DreamingToLockscreenTransitionViewModel + private lateinit var repository: FakeKeyguardTransitionRepository + + @Before + fun setUp() { + repository = FakeKeyguardTransitionRepository() + val interactor = KeyguardTransitionInteractor(repository) + underTest = DreamingToLockscreenTransitionViewModel(interactor) + } + + @Test + fun dreamOverlayTranslationY() = + runTest(UnconfinedTestDispatcher()) { + val values = mutableListOf() + + val pixels = 100 + val job = + underTest.dreamOverlayTranslationY(pixels).onEach { values.add(it) }.launchIn(this) + + repository.sendTransitionStep(step(0f)) + repository.sendTransitionStep(step(0.3f)) + repository.sendTransitionStep(step(0.5f)) + repository.sendTransitionStep(step(1f)) + + // Only two values should be present, since the dream overlay runs for a small fraction + // of + // the overall animation time + assertThat(values.size).isEqualTo(2) + assertThat(values[0]) + .isEqualTo( + EMPHASIZED_ACCELERATE.getInterpolation( + animValue(0f, DREAM_OVERLAY_TRANSLATION_Y) + ) * pixels + ) + assertThat(values[1]) + .isEqualTo( + EMPHASIZED_ACCELERATE.getInterpolation( + animValue(0.3f, DREAM_OVERLAY_TRANSLATION_Y) + ) * pixels + ) + + job.cancel() + } + + @Test + fun dreamOverlayFadeOut() = + runTest(UnconfinedTestDispatcher()) { + val values = mutableListOf() + + val job = underTest.dreamOverlayAlpha.onEach { values.add(it) }.launchIn(this) + + repository.sendTransitionStep(step(0f)) + repository.sendTransitionStep(step(0.1f)) + repository.sendTransitionStep(step(0.5f)) + repository.sendTransitionStep(step(1f)) + + // Only two values should be present, since the dream overlay runs for a small fraction + // of + // the overall animation time + assertThat(values.size).isEqualTo(2) + assertThat(values[0]).isEqualTo(1f - animValue(0f, DREAM_OVERLAY_ALPHA)) + assertThat(values[1]).isEqualTo(1f - animValue(0.1f, DREAM_OVERLAY_ALPHA)) + + job.cancel() + } + + private fun animValue(stepValue: Float, params: AnimationParams): Float { + val totalDuration = TO_LOCKSCREEN_DURATION + val startValue = (params.startTime / totalDuration).toFloat() + + val multiplier = (totalDuration / params.duration).toFloat() + return (stepValue - startValue) * multiplier + } + + private fun step(value: Float): TransitionStep { + return TransitionStep( + from = KeyguardState.DREAMING, + to = KeyguardState.LOCKSCREEN, + value = value, + transitionState = TransitionState.RUNNING, + ownerName = "DreamingToLockscreenTransitionViewModelTest" + ) + } +}