From 091205468b28dc2f220f68d481c133b907d16e56 Mon Sep 17 00:00:00 2001 From: Lucas Silva Date: Thu, 3 Nov 2022 15:04:56 -0400 Subject: [PATCH] Implement dream out overlay transition The overlay should blur and animate the complications when the dream is waking up. This adds a new API to DreamOverlayService to get notified of dreams waking, so that it can run the animation. When the animation is done, we communicate back to the DreamService so that it may finish itself. A delay was added to the touch handler which hides the overlay when a touch happens, in order to ensure the exit animation has a chance to complete first. Bug: 220311554 Test: atest DreamOverlayAnimationsControllerTest Test: atest HideComplicationTouchHandlerTest Test: atest DreamOverlayContainerViewControllerTest Test: atest DreamOverlayServiceTest Test: flashed device and verified animation looks correct when tapping dream to exit Change-Id: Ibccd45aefbf9b8aeb462d843794c3434c49a6bf2 --- .../service/dreams/DreamOverlayService.java | 22 ++ .../android/service/dreams/DreamService.java | 22 +- .../android/service/dreams/IDreamOverlay.aidl | 3 + .../service/dreams/IDreamOverlayCallback.aidl | 3 + core/res/res/values/config.xml | 2 +- packages/SystemUI/res/values/config.xml | 44 +-- packages/SystemUI/res/values/dimens.xml | 1 + .../DreamOverlayAnimationsController.kt | 275 +++++++++++++----- .../DreamOverlayContainerViewController.java | 18 +- .../systemui/dreams/DreamOverlayService.java | 9 + .../dreams/DreamOverlayStateController.java | 18 ++ .../DreamOverlayStatusBarViewController.java | 18 +- .../dreams/complication/Complication.java | 6 +- .../ComplicationLayoutEngine.java | 33 +-- .../ComplicationLayoutParams.java | 2 +- .../dagger/ComplicationHostViewModule.java | 11 + .../dreams/dagger/DreamOverlayModule.java | 97 +++++- .../touch/HideComplicationTouchHandler.java | 75 ++++- .../keyguard/KeyguardViewMediator.java | 10 +- .../DreamOverlayAnimationsControllerTest.kt | 125 ++++++++ ...eamOverlayContainerViewControllerTest.java | 4 +- .../dreams/DreamOverlayServiceTest.java | 25 ++ .../HideComplicationTouchHandlerTest.java | 52 ++-- 23 files changed, 704 insertions(+), 171 deletions(-) create mode 100644 packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayAnimationsControllerTest.kt diff --git a/core/java/android/service/dreams/DreamOverlayService.java b/core/java/android/service/dreams/DreamOverlayService.java index aa45c20a8e135..6e8198ba0cd1a 100644 --- a/core/java/android/service/dreams/DreamOverlayService.java +++ b/core/java/android/service/dreams/DreamOverlayService.java @@ -49,6 +49,17 @@ public abstract class DreamOverlayService extends Service { mShowComplications = shouldShowComplications; onStartDream(layoutParams); } + + @Override + public void wakeUp() { + onWakeUp(() -> { + try { + mDreamOverlayCallback.onWakeUpComplete(); + } catch (RemoteException e) { + Log.e(TAG, "Could not notify dream of wakeUp:" + e); + } + }); + } }; IDreamOverlayCallback mDreamOverlayCallback; @@ -70,6 +81,17 @@ public abstract class DreamOverlayService extends Service { */ public abstract void onStartDream(@NonNull WindowManager.LayoutParams layoutParams); + /** + * This method is overridden by implementations to handle when the dream has been requested + * to wakeup. This allows any overlay animations to run. + * + * @param onCompleteCallback The callback to trigger to notify the dream service that the + * overlay has completed waking up. + * @hide + */ + public void onWakeUp(@NonNull Runnable onCompleteCallback) { + } + /** * This method is invoked to request the dream exit. */ diff --git a/core/java/android/service/dreams/DreamService.java b/core/java/android/service/dreams/DreamService.java index 32bdf79622730..cff20391103ba 100644 --- a/core/java/android/service/dreams/DreamService.java +++ b/core/java/android/service/dreams/DreamService.java @@ -312,7 +312,14 @@ public class DreamService extends Service implements Window.Callback { @Override public void onExitRequested() { // Simply finish dream when exit is requested. - finish(); + mHandler.post(() -> finish()); + } + + @Override + public void onWakeUpComplete() { + // Finish the dream once overlay animations are complete. Execute on handler since + // this is coming in on the overlay binder. + mHandler.post(() -> finish()); } }; @@ -975,7 +982,18 @@ public class DreamService extends Service implements Window.Callback { *

*/ public void onWakeUp() { - finish(); + if (mOverlayConnection != null) { + mOverlayConnection.addConsumer(overlay -> { + try { + overlay.wakeUp(); + } catch (RemoteException e) { + Slog.e(TAG, "Error waking the overlay service", e); + finish(); + } + }); + } else { + finish(); + } } /** {@inheritDoc} */ diff --git a/core/java/android/service/dreams/IDreamOverlay.aidl b/core/java/android/service/dreams/IDreamOverlay.aidl index 05ebbfe98c9fc..7aeceb2ce5380 100644 --- a/core/java/android/service/dreams/IDreamOverlay.aidl +++ b/core/java/android/service/dreams/IDreamOverlay.aidl @@ -38,4 +38,7 @@ interface IDreamOverlay { */ void startDream(in LayoutParams params, in IDreamOverlayCallback callback, in String dreamComponent, in boolean shouldShowComplications); + + /** Called when the dream is waking, to do any exit animations */ + void wakeUp(); } diff --git a/core/java/android/service/dreams/IDreamOverlayCallback.aidl b/core/java/android/service/dreams/IDreamOverlayCallback.aidl index ec76a334d5b2d..4ad63f1317d15 100644 --- a/core/java/android/service/dreams/IDreamOverlayCallback.aidl +++ b/core/java/android/service/dreams/IDreamOverlayCallback.aidl @@ -28,4 +28,7 @@ interface IDreamOverlayCallback { * Invoked to request the dream exit. */ void onExitRequested(); + + /** Invoked when the dream overlay wakeUp animation is complete. */ + void onWakeUpComplete(); } \ No newline at end of file diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index 2a9e60a469725..b9bcf559f63bb 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -2485,7 +2485,7 @@ 250 - 100 + 300 diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml index 55d637916d0cd..f4d802bf745ef 100644 --- a/packages/SystemUI/res/values/config.xml +++ b/packages/SystemUI/res/values/config.xml @@ -743,12 +743,35 @@ 240000 + + 333 + + 33 + + 117 + + 200 + + 217 + + 133 + + 250 + 500 500 1000 + 200 + 249 @@ -780,27 +803,6 @@ com.android.systemui - - - -1 - 50 - 100 - 150 - 200 - 300 - - - - - @color/dream_overlay_aqi_good - @color/dream_overlay_aqi_moderate - @color/dream_overlay_aqi_unhealthy_sensitive - @color/dream_overlay_aqi_unhealthy - @color/dream_overlay_aqi_very_unhealthy - @color/dream_overlay_aqi_hazardous - - true diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index c78d36dd36853..7cda9d70ea49a 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -1534,6 +1534,7 @@ 0dp 80dp + 40dp 28dp 6dp diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayAnimationsController.kt b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayAnimationsController.kt index d8dd6a21d4c16..0087c84393708 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayAnimationsController.kt +++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayAnimationsController.kt @@ -17,17 +17,19 @@ package com.android.systemui.dreams import android.animation.Animator -import android.animation.AnimatorListenerAdapter import android.animation.AnimatorSet import android.animation.ValueAnimator import android.view.View +import android.view.animation.Interpolator +import androidx.annotation.FloatRange import androidx.core.animation.doOnEnd import com.android.systemui.animation.Interpolators import com.android.systemui.dreams.complication.ComplicationHostViewController import com.android.systemui.dreams.complication.ComplicationLayoutParams +import com.android.systemui.dreams.complication.ComplicationLayoutParams.Position import com.android.systemui.dreams.dagger.DreamOverlayModule import com.android.systemui.statusbar.BlurUtils -import java.util.function.Consumer +import com.android.systemui.statusbar.CrossFadeHelper import javax.inject.Inject import javax.inject.Named @@ -40,108 +42,239 @@ constructor( private val mStatusBarViewController: DreamOverlayStatusBarViewController, private val mOverlayStateController: DreamOverlayStateController, @Named(DreamOverlayModule.DREAM_IN_BLUR_ANIMATION_DURATION) - private val mDreamInBlurAnimDuration: Int, - @Named(DreamOverlayModule.DREAM_IN_BLUR_ANIMATION_DELAY) private val mDreamInBlurAnimDelay: Int, + private val mDreamInBlurAnimDurationMs: Long, + @Named(DreamOverlayModule.DREAM_IN_BLUR_ANIMATION_DELAY) + private val mDreamInBlurAnimDelayMs: Long, @Named(DreamOverlayModule.DREAM_IN_COMPLICATIONS_ANIMATION_DURATION) - private val mDreamInComplicationsAnimDuration: Int, + private val mDreamInComplicationsAnimDurationMs: Long, @Named(DreamOverlayModule.DREAM_IN_TOP_COMPLICATIONS_ANIMATION_DELAY) - private val mDreamInTopComplicationsAnimDelay: Int, + private val mDreamInTopComplicationsAnimDelayMs: Long, @Named(DreamOverlayModule.DREAM_IN_BOTTOM_COMPLICATIONS_ANIMATION_DELAY) - private val mDreamInBottomComplicationsAnimDelay: Int + private val mDreamInBottomComplicationsAnimDelayMs: 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 ) { - var mEntryAnimations: AnimatorSet? = null + private var mAnimator: Animator? = null + + /** + * Store the current alphas at the various positions. This is so that we may resume an animation + * at the current alpha. + */ + private var mCurrentAlphaAtPosition = mutableMapOf() + + @FloatRange(from = 0.0, to = 1.0) private var mBlurProgress: Float = 0f /** Starts the dream content and dream overlay entry animations. */ - fun startEntryAnimations(view: View) { - cancelRunningEntryAnimations() + @JvmOverloads + fun startEntryAnimations(view: View, animatorBuilder: () -> AnimatorSet = { AnimatorSet() }) { + cancelAnimations() - mEntryAnimations = AnimatorSet() - mEntryAnimations?.apply { - playTogether( - buildDreamInBlurAnimator(view), - buildDreamInTopComplicationsAnimator(), - buildDreamInBottomComplicationsAnimator() - ) - doOnEnd { mOverlayStateController.setEntryAnimationsFinished(true) } - start() - } + mAnimator = + animatorBuilder().apply { + playTogether( + blurAnimator( + view = view, + from = 1f, + to = 0f, + durationMs = mDreamInBlurAnimDurationMs, + delayMs = mDreamInBlurAnimDelayMs + ), + alphaAnimator( + from = 0f, + to = 1f, + durationMs = mDreamInComplicationsAnimDurationMs, + delayMs = mDreamInTopComplicationsAnimDelayMs, + position = ComplicationLayoutParams.POSITION_TOP + ), + alphaAnimator( + from = 0f, + to = 1f, + durationMs = mDreamInComplicationsAnimDurationMs, + delayMs = mDreamInBottomComplicationsAnimDelayMs, + position = ComplicationLayoutParams.POSITION_BOTTOM + ) + ) + doOnEnd { + mAnimator = null + mOverlayStateController.setEntryAnimationsFinished(true) + } + start() + } + } + + /** Starts the dream content and dream overlay exit animations. */ + @JvmOverloads + fun startExitAnimations( + view: View, + doneCallback: () -> Unit, + animatorBuilder: () -> AnimatorSet = { AnimatorSet() } + ) { + cancelAnimations() + + mAnimator = + animatorBuilder().apply { + playTogether( + blurAnimator( + view = view, + // Start the blurring wherever the entry animation ended, in + // case it was cancelled early. + from = mBlurProgress, + to = 1f, + durationMs = mDreamOutBlurDurationMs + ), + translationYAnimator( + from = 0f, + to = mDreamOutTranslationYDistance.toFloat(), + durationMs = mDreamOutTranslationYDurationMs, + delayMs = mDreamOutTranslationYDelayBottomMs, + position = ComplicationLayoutParams.POSITION_BOTTOM, + animInterpolator = Interpolators.EMPHASIZED_ACCELERATE + ), + translationYAnimator( + from = 0f, + to = mDreamOutTranslationYDistance.toFloat(), + durationMs = mDreamOutTranslationYDurationMs, + delayMs = mDreamOutTranslationYDelayTopMs, + position = ComplicationLayoutParams.POSITION_TOP, + animInterpolator = Interpolators.EMPHASIZED_ACCELERATE + ), + alphaAnimator( + from = + mCurrentAlphaAtPosition.getOrDefault( + key = ComplicationLayoutParams.POSITION_BOTTOM, + defaultValue = 1f + ), + to = 0f, + durationMs = mDreamOutAlphaDurationMs, + delayMs = mDreamOutAlphaDelayBottomMs, + position = ComplicationLayoutParams.POSITION_BOTTOM + ), + alphaAnimator( + from = + mCurrentAlphaAtPosition.getOrDefault( + key = ComplicationLayoutParams.POSITION_TOP, + defaultValue = 1f + ), + to = 0f, + durationMs = mDreamOutAlphaDurationMs, + delayMs = mDreamOutAlphaDelayTopMs, + position = ComplicationLayoutParams.POSITION_TOP + ) + ) + doOnEnd { + mAnimator = null + mOverlayStateController.setExitAnimationsRunning(false) + doneCallback() + } + start() + } + mOverlayStateController.setExitAnimationsRunning(true) } /** Cancels the dream content and dream overlay animations, if they're currently running. */ - fun cancelRunningEntryAnimations() { - if (mEntryAnimations?.isRunning == true) { - mEntryAnimations?.cancel() - } - mEntryAnimations = null + fun cancelAnimations() { + mAnimator = + mAnimator?.let { + it.cancel() + null + } } - private fun buildDreamInBlurAnimator(view: View): Animator { - return ValueAnimator.ofFloat(1f, 0f).apply { - duration = mDreamInBlurAnimDuration.toLong() - startDelay = mDreamInBlurAnimDelay.toLong() + private fun blurAnimator( + view: View, + from: Float, + to: Float, + durationMs: Long, + delayMs: Long = 0 + ): Animator { + return ValueAnimator.ofFloat(from, to).apply { + duration = durationMs + startDelay = delayMs interpolator = Interpolators.LINEAR addUpdateListener { animator: ValueAnimator -> + mBlurProgress = animator.animatedValue as Float mBlurUtils.applyBlur( - view.viewRootImpl, - mBlurUtils.blurRadiusOfRatio(animator.animatedValue as Float).toInt(), - false /*opaque*/ + viewRootImpl = view.viewRootImpl, + radius = mBlurUtils.blurRadiusOfRatio(mBlurProgress).toInt(), + opaque = false ) } } } - private fun buildDreamInTopComplicationsAnimator(): Animator { - return ValueAnimator.ofFloat(0f, 1f).apply { - duration = mDreamInComplicationsAnimDuration.toLong() - startDelay = mDreamInTopComplicationsAnimDelay.toLong() + private fun alphaAnimator( + from: Float, + to: Float, + durationMs: Long, + delayMs: Long, + @Position position: Int + ): Animator { + return ValueAnimator.ofFloat(from, to).apply { + duration = durationMs + startDelay = delayMs interpolator = Interpolators.LINEAR addUpdateListener { va: ValueAnimator -> - setTopElementsAlpha(va.animatedValue as Float) + setElementsAlphaAtPosition( + alpha = va.animatedValue as Float, + position = position, + fadingOut = to < from + ) } } } - private fun buildDreamInBottomComplicationsAnimator(): Animator { - return ValueAnimator.ofFloat(0f, 1f).apply { - duration = mDreamInComplicationsAnimDuration.toLong() - startDelay = mDreamInBottomComplicationsAnimDelay.toLong() - interpolator = Interpolators.LINEAR + private fun translationYAnimator( + from: Float, + to: Float, + durationMs: Long, + delayMs: Long, + @Position position: Int, + animInterpolator: Interpolator + ): Animator { + return ValueAnimator.ofFloat(from, to).apply { + duration = durationMs + startDelay = delayMs + interpolator = animInterpolator addUpdateListener { va: ValueAnimator -> - setBottomElementsAlpha(va.animatedValue as Float) + setElementsTranslationYAtPosition(va.animatedValue as Float, position) } - addListener( - object : AnimatorListenerAdapter() { - override fun onAnimationStart(animation: Animator) { - mComplicationHostViewController - .getViewsAtPosition(ComplicationLayoutParams.POSITION_BOTTOM) - .forEach(Consumer { v: View -> v.visibility = View.VISIBLE }) - } - } - ) } } - /** Sets alpha of top complications and the status bar. */ - private fun setTopElementsAlpha(alpha: Float) { - mComplicationHostViewController - .getViewsAtPosition(ComplicationLayoutParams.POSITION_TOP) - .forEach(Consumer { v: View -> setAlphaAndEnsureVisible(v, alpha) }) - mStatusBarViewController.setAlpha(alpha) - } - - /** Sets alpha of bottom complications. */ - private fun setBottomElementsAlpha(alpha: Float) { - mComplicationHostViewController - .getViewsAtPosition(ComplicationLayoutParams.POSITION_BOTTOM) - .forEach(Consumer { v: View -> setAlphaAndEnsureVisible(v, alpha) }) - } - - private fun setAlphaAndEnsureVisible(view: View, alpha: Float) { - if (alpha > 0 && view.visibility != View.VISIBLE) { - view.visibility = View.VISIBLE + /** Sets alpha of complications at the specified position. */ + private fun setElementsAlphaAtPosition(alpha: Float, position: Int, fadingOut: Boolean) { + mCurrentAlphaAtPosition[position] = alpha + mComplicationHostViewController.getViewsAtPosition(position).forEach { view -> + if (fadingOut) { + CrossFadeHelper.fadeOut(view, 1 - alpha, /* remap= */ false) + } else { + CrossFadeHelper.fadeIn(view, alpha, /* remap= */ false) + } } + if (position == ComplicationLayoutParams.POSITION_TOP) { + mStatusBarViewController.setFadeAmount(alpha, fadingOut) + } + } - view.alpha = alpha + /** Sets y translation of complications at the specified position. */ + private fun setElementsTranslationYAtPosition(translationY: Float, position: Int) { + mComplicationHostViewController.getViewsAtPosition(position).forEach { v -> + v.translationY = translationY + } + if (position == ComplicationLayoutParams.POSITION_TOP) { + mStatusBarViewController.setTranslationY(translationY) + } } } diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayContainerViewController.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayContainerViewController.java index 5c6d24813570b..9d7ad305c7e50 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayContainerViewController.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayContainerViewController.java @@ -29,6 +29,8 @@ import android.util.MathUtils; import android.view.View; import android.view.ViewGroup; +import androidx.annotation.NonNull; + import com.android.systemui.R; import com.android.systemui.animation.Interpolators; import com.android.systemui.dagger.qualifiers.Main; @@ -42,6 +44,7 @@ import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager; import com.android.systemui.util.ViewController; import java.util.Arrays; +import java.util.concurrent.Executor; import javax.inject.Inject; import javax.inject.Named; @@ -194,7 +197,7 @@ public class DreamOverlayContainerViewController extends ViewController { + callbackExecutor.execute(onAnimationEnd); + return null; + }); + } } diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java index 8542412f82f86..e76d5b30d3e61 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java @@ -213,6 +213,15 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ mLifecycleRegistry.setCurrentState(state); } + @Override + public void onWakeUp(@NonNull Runnable onCompletedCallback) { + mExecutor.execute(() -> { + if (mDreamOverlayContainerViewController != null) { + mDreamOverlayContainerViewController.wakeUp(onCompletedCallback, mExecutor); + } + }); + } + /** * Inserts {@link Window} to host the dream overlay into the dream's parent window. Must be * called from the main executing thread. The window attributes closely mirror those that are diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStateController.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStateController.java index e80d0beabf2b0..5f942b6fb834f 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStateController.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStateController.java @@ -52,6 +52,7 @@ public class DreamOverlayStateController implements public static final int STATE_DREAM_OVERLAY_ACTIVE = 1 << 0; public static final int STATE_LOW_LIGHT_ACTIVE = 1 << 1; public static final int STATE_DREAM_ENTRY_ANIMATIONS_FINISHED = 1 << 2; + public static final int STATE_DREAM_EXIT_ANIMATIONS_RUNNING = 1 << 3; private static final int OP_CLEAR_STATE = 1; private static final int OP_SET_STATE = 2; @@ -211,6 +212,14 @@ public class DreamOverlayStateController implements return containsState(STATE_DREAM_ENTRY_ANIMATIONS_FINISHED); } + /** + * Returns whether the dream content and dream overlay exit animations are running. + * @return {@code true} if animations are running, {@code false} otherwise. + */ + public boolean areExitAnimationsRunning() { + return containsState(STATE_DREAM_EXIT_ANIMATIONS_RUNNING); + } + private boolean containsState(int state) { return (mState & state) != 0; } @@ -256,6 +265,15 @@ public class DreamOverlayStateController implements STATE_DREAM_ENTRY_ANIMATIONS_FINISHED); } + /** + * Sets whether dream content and dream overlay exit animations are running. + * @param running {@code true} if exit animations are running, {@code false} otherwise. + */ + public void setExitAnimationsRunning(boolean running) { + modifyState(running ? OP_SET_STATE : OP_CLEAR_STATE, + STATE_DREAM_EXIT_ANIMATIONS_RUNNING); + } + /** * Returns the available complication types. */ diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarViewController.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarViewController.java index d17fbe31c8d2c..f1bb156199ef5 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarViewController.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarViewController.java @@ -37,6 +37,7 @@ import com.android.systemui.R; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dreams.DreamOverlayStatusBarItemsProvider.StatusBarItem; import com.android.systemui.dreams.dagger.DreamOverlayComponent; +import com.android.systemui.statusbar.CrossFadeHelper; import com.android.systemui.statusbar.policy.IndividualSensorPrivacyController; import com.android.systemui.statusbar.policy.NextAlarmController; import com.android.systemui.statusbar.policy.ZenModeController; @@ -217,18 +218,29 @@ public class DreamOverlayStatusBarViewController extends ViewController mCancelCallbacks = new ArrayDeque<>(); + private final Runnable mRestoreComplications = new Runnable() { @Override public void run() { - mVisibilityController.setVisibility(View.VISIBLE, true); + mVisibilityController.setVisibility(View.VISIBLE); + mHidden = false; + } + }; + + private final Runnable mHideComplications = new Runnable() { + @Override + public void run() { + if (mOverlayStateController.areExitAnimationsRunning()) { + // Avoid interfering with the exit animations. + return; + } + mVisibilityController.setVisibility(View.INVISIBLE); + mHidden = true; + if (mHiddenCallback != null) { + mHiddenCallback.run(); + mHiddenCallback = null; + } } }; @Inject HideComplicationTouchHandler(Complication.VisibilityController visibilityController, @Named(COMPLICATIONS_RESTORE_TIMEOUT) int restoreTimeout, + @Named(COMPLICATIONS_FADE_OUT_DELAY) int fadeOutDelay, TouchInsetManager touchInsetManager, StatusBarKeyguardViewManager statusBarKeyguardViewManager, - @Main Executor executor, - @Main Handler handler) { + @Main DelayableExecutor executor, + DreamOverlayStateController overlayStateController) { mVisibilityController = visibilityController; mRestoreTimeout = restoreTimeout; + mFadeOutDelay = fadeOutDelay; mStatusBarKeyguardViewManager = statusBarKeyguardViewManager; - mHandler = handler; mTouchInsetManager = touchInsetManager; mExecutor = executor; + mOverlayStateController = overlayStateController; } @Override @@ -87,7 +116,8 @@ public class HideComplicationTouchHandler implements DreamTouchHandler { final boolean bouncerShowing = mStatusBarKeyguardViewManager.isBouncerShowing(); // If other sessions are interested in this touch, do not fade out elements. - if (session.getActiveSessionCount() > 1 || bouncerShowing) { + if (session.getActiveSessionCount() > 1 || bouncerShowing + || mOverlayStateController.areExitAnimationsRunning()) { if (DEBUG) { Log.d(TAG, "not fading. Active session count: " + session.getActiveSessionCount() + ". Bouncer showing: " + bouncerShowing); @@ -115,8 +145,11 @@ public class HideComplicationTouchHandler implements DreamTouchHandler { touchCheck.addListener(() -> { try { if (!touchCheck.get()) { - mHandler.removeCallbacks(mRestoreComplications); - mVisibilityController.setVisibility(View.INVISIBLE, true); + // Cancel all pending callbacks. + while (!mCancelCallbacks.isEmpty()) mCancelCallbacks.pop().run(); + mCancelCallbacks.add( + mExecutor.executeDelayed( + mHideComplications, mFadeOutDelay)); } else { // If a touch occurred inside the dream overlay touch insets, do not // handle the touch. @@ -130,7 +163,23 @@ public class HideComplicationTouchHandler implements DreamTouchHandler { || motionEvent.getAction() == MotionEvent.ACTION_UP) { // End session and initiate delayed reappearance of the complications. session.pop(); - mHandler.postDelayed(mRestoreComplications, mRestoreTimeout); + runAfterHidden(() -> mCancelCallbacks.add( + mExecutor.executeDelayed(mRestoreComplications, + mRestoreTimeout))); + } + }); + } + + /** + * Triggers a runnable after complications have been hidden. Will override any previously set + * runnable currently waiting for hide to happen. + */ + private void runAfterHidden(Runnable runnable) { + mExecutor.execute(() -> { + if (mHidden) { + runnable.run(); + } else { + mHiddenCallback = runnable; } }); } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java index 7cd3843fd5066..5ed0bff99b614 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java @@ -408,6 +408,11 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, */ private final int mDreamOpenAnimationDuration; + /** + * The duration in milliseconds of the dream close animation. + */ + private final int mDreamCloseAnimationDuration; + /** * The animation used for hiding keyguard. This is used to fetch the animation timings if * WindowManager is not providing us with them. @@ -1054,7 +1059,8 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, } mUnoccludeAnimator = ValueAnimator.ofFloat(1f, 0f); - mUnoccludeAnimator.setDuration(UNOCCLUDE_ANIMATION_DURATION); + mUnoccludeAnimator.setDuration(isDream ? mDreamCloseAnimationDuration + : UNOCCLUDE_ANIMATION_DURATION); mUnoccludeAnimator.setInterpolator(Interpolators.TOUCH_RESPONSE); mUnoccludeAnimator.addUpdateListener( animation -> { @@ -1204,6 +1210,8 @@ 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); } public void userActivity() { diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayAnimationsControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayAnimationsControllerTest.kt new file mode 100644 index 0000000000000..99406ed446060 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayAnimationsControllerTest.kt @@ -0,0 +1,125 @@ +package com.android.systemui.dreams + +import android.animation.Animator +import android.animation.AnimatorSet +import android.testing.AndroidTestingRunner +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.dreams.complication.ComplicationHostViewController +import com.android.systemui.statusbar.BlurUtils +import com.android.systemui.util.mockito.argumentCaptor +import com.android.systemui.util.mockito.mock +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.times +import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations + +@SmallTest +@RunWith(AndroidTestingRunner::class) +class DreamOverlayAnimationsControllerTest : SysuiTestCase() { + + companion object { + private const val DREAM_IN_BLUR_ANIMATION_DURATION = 1L + private const val DREAM_IN_BLUR_ANIMATION_DELAY = 2L + private const val DREAM_IN_COMPLICATIONS_ANIMATION_DURATION = 3L + private const val DREAM_IN_TOP_COMPLICATIONS_ANIMATION_DELAY = 4L + private const val DREAM_IN_BOTTOM_COMPLICATIONS_ANIMATION_DELAY = 5L + 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 + @Mock private lateinit var blurUtils: BlurUtils + @Mock private lateinit var hostViewController: ComplicationHostViewController + @Mock private lateinit var statusBarViewController: DreamOverlayStatusBarViewController + @Mock private lateinit var stateController: DreamOverlayStateController + private lateinit var controller: DreamOverlayAnimationsController + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + controller = + DreamOverlayAnimationsController( + blurUtils, + hostViewController, + statusBarViewController, + stateController, + DREAM_IN_BLUR_ANIMATION_DURATION, + DREAM_IN_BLUR_ANIMATION_DELAY, + DREAM_IN_COMPLICATIONS_ANIMATION_DURATION, + DREAM_IN_TOP_COMPLICATIONS_ANIMATION_DELAY, + DREAM_IN_BOTTOM_COMPLICATIONS_ANIMATION_DELAY, + 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 + ) + } + + @Test + fun testExitAnimationOnEnd() { + val mockCallback: () -> Unit = mock() + + controller.startExitAnimations( + view = mock(), + doneCallback = mockCallback, + animatorBuilder = { mockAnimator } + ) + + val captor = argumentCaptor() + verify(mockAnimator).addListener(captor.capture()) + val listener = captor.value + + verify(mockCallback, never()).invoke() + listener.onAnimationEnd(mockAnimator) + verify(mockCallback, times(1)).invoke() + } + + @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() { + val mockStartAnimator: AnimatorSet = mock() + val mockExitAnimator: AnimatorSet = mock() + + controller.startEntryAnimations(view = mock(), animatorBuilder = { mockStartAnimator }) + + verify(mockStartAnimator, never()).cancel() + + controller.startExitAnimations( + view = mock(), + doneCallback = mock(), + animatorBuilder = { mockExitAnimator } + ) + + // 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 517804db2a70a..73c226d11bc43 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayContainerViewControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayContainerViewControllerTest.java @@ -204,7 +204,7 @@ public class DreamOverlayContainerViewControllerTest extends SysuiTestCase { mController.onViewAttached(); verify(mAnimationsController).startEntryAnimations(mDreamOverlayContainerView); - verify(mAnimationsController, never()).cancelRunningEntryAnimations(); + verify(mAnimationsController, never()).cancelAnimations(); } @Test @@ -221,6 +221,6 @@ public class DreamOverlayContainerViewControllerTest extends SysuiTestCase { mController.onViewAttached(); mController.onViewDetached(); - verify(mAnimationsController).cancelRunningEntryAnimations(); + verify(mAnimationsController).cancelAnimations(); } } 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 f04a37f4c3fa0..ffb8342a56a54 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java @@ -20,6 +20,7 @@ import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.clearInvocations; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -337,4 +338,28 @@ public class DreamOverlayServiceTest extends SysuiTestCase { verify(mDreamOverlayComponent).getDreamOverlayContainerViewController(); verify(mDreamOverlayComponent).getDreamOverlayTouchMonitor(); } + + @Test + public void testWakeUp() throws RemoteException { + final IBinder proxy = mService.onBind(new Intent()); + final IDreamOverlay overlay = IDreamOverlay.Stub.asInterface(proxy); + + // Inform the overlay service of dream starting. + overlay.startDream(mWindowParams, mDreamOverlayCallback, DREAM_COMPONENT, + true /*shouldShowComplication*/); + mMainExecutor.runAllReady(); + + final Runnable callback = mock(Runnable.class); + mService.onWakeUp(callback); + mMainExecutor.runAllReady(); + verify(mDreamOverlayContainerViewController).wakeUp(callback, mMainExecutor); + } + + @Test + public void testWakeUpBeforeStartDoesNothing() { + final Runnable callback = mock(Runnable.class); + mService.onWakeUp(callback); + mMainExecutor.runAllReady(); + verify(mDreamOverlayContainerViewController, never()).wakeUp(callback, mMainExecutor); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/touch/HideComplicationTouchHandlerTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/touch/HideComplicationTouchHandlerTest.java index 14a5702c8e5b0..4e3aca7108845 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/dreams/touch/HideComplicationTouchHandlerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/touch/HideComplicationTouchHandlerTest.java @@ -16,8 +16,6 @@ package com.android.systemui.dreams.touch; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.never; @@ -33,6 +31,7 @@ import androidx.concurrent.futures.CallbackToFutureAdapter; import androidx.test.filters.SmallTest; import com.android.systemui.SysuiTestCase; +import com.android.systemui.dreams.DreamOverlayStateController; import com.android.systemui.dreams.complication.Complication; import com.android.systemui.shared.system.InputChannelCompat; import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager; @@ -52,6 +51,7 @@ import org.mockito.MockitoAnnotations; @RunWith(AndroidTestingRunner.class) public class HideComplicationTouchHandlerTest extends SysuiTestCase { private static final int RESTORE_TIMEOUT = 1000; + private static final int HIDE_DELAY = 500; @Mock Complication.VisibilityController mVisibilityController; @@ -71,11 +71,18 @@ public class HideComplicationTouchHandlerTest extends SysuiTestCase { @Mock DreamTouchHandler.TouchSession mSession; - FakeExecutor mFakeExecutor = new FakeExecutor(new FakeSystemClock()); + @Mock + DreamOverlayStateController mStateController; + + FakeSystemClock mClock; + + FakeExecutor mFakeExecutor; @Before public void setup() { MockitoAnnotations.initMocks(this); + mClock = new FakeSystemClock(); + mFakeExecutor = new FakeExecutor(mClock); } /** @@ -86,10 +93,11 @@ public class HideComplicationTouchHandlerTest extends SysuiTestCase { final HideComplicationTouchHandler touchHandler = new HideComplicationTouchHandler( mVisibilityController, RESTORE_TIMEOUT, + HIDE_DELAY, mTouchInsetManager, mStatusBarKeyguardViewManager, mFakeExecutor, - mHandler); + mStateController); // Report multiple active sessions. when(mSession.getActiveSessionCount()).thenReturn(2); @@ -103,8 +111,10 @@ public class HideComplicationTouchHandlerTest extends SysuiTestCase { // Verify session end. verify(mSession).pop(); + mClock.advanceTime(HIDE_DELAY); + // Verify no interaction with visibility controller. - verify(mVisibilityController, never()).setVisibility(anyInt(), anyBoolean()); + verify(mVisibilityController, never()).setVisibility(anyInt()); } /** @@ -115,10 +125,11 @@ public class HideComplicationTouchHandlerTest extends SysuiTestCase { final HideComplicationTouchHandler touchHandler = new HideComplicationTouchHandler( mVisibilityController, RESTORE_TIMEOUT, + HIDE_DELAY, mTouchInsetManager, mStatusBarKeyguardViewManager, mFakeExecutor, - mHandler); + mStateController); // Report one session. when(mSession.getActiveSessionCount()).thenReturn(1); @@ -132,8 +143,10 @@ public class HideComplicationTouchHandlerTest extends SysuiTestCase { // Verify session end. verify(mSession).pop(); + mClock.advanceTime(HIDE_DELAY); + // Verify no interaction with visibility controller. - verify(mVisibilityController, never()).setVisibility(anyInt(), anyBoolean()); + verify(mVisibilityController, never()).setVisibility(anyInt()); } /** @@ -144,10 +157,11 @@ public class HideComplicationTouchHandlerTest extends SysuiTestCase { final HideComplicationTouchHandler touchHandler = new HideComplicationTouchHandler( mVisibilityController, RESTORE_TIMEOUT, + HIDE_DELAY, mTouchInsetManager, mStatusBarKeyguardViewManager, mFakeExecutor, - mHandler); + mStateController); // Report one session when(mSession.getActiveSessionCount()).thenReturn(1); @@ -177,8 +191,10 @@ public class HideComplicationTouchHandlerTest extends SysuiTestCase { // Verify session ended. verify(mSession).pop(); + mClock.advanceTime(HIDE_DELAY); + // Verify no interaction with visibility controller. - verify(mVisibilityController, never()).setVisibility(anyInt(), anyBoolean()); + verify(mVisibilityController, never()).setVisibility(anyInt()); } /** @@ -189,10 +205,11 @@ public class HideComplicationTouchHandlerTest extends SysuiTestCase { final HideComplicationTouchHandler touchHandler = new HideComplicationTouchHandler( mVisibilityController, RESTORE_TIMEOUT, + HIDE_DELAY, mTouchInsetManager, mStatusBarKeyguardViewManager, mFakeExecutor, - mHandler); + mStateController); // Report one session when(mSession.getActiveSessionCount()).thenReturn(1); @@ -221,11 +238,11 @@ public class HideComplicationTouchHandlerTest extends SysuiTestCase { inputEventListenerCaptor.getValue().onInputEvent(mMotionEvent); mFakeExecutor.runAllReady(); - // Verify callback to restore visibility cancelled. - verify(mHandler).removeCallbacks(any()); - + // Verify visibility controller doesn't hide until after timeout + verify(mVisibilityController, never()).setVisibility(eq(View.INVISIBLE)); + mClock.advanceTime(HIDE_DELAY); // Verify visibility controller told to hide complications. - verify(mVisibilityController).setVisibility(eq(View.INVISIBLE), anyBoolean()); + verify(mVisibilityController).setVisibility(eq(View.INVISIBLE)); Mockito.clearInvocations(mVisibilityController, mHandler); @@ -235,11 +252,8 @@ public class HideComplicationTouchHandlerTest extends SysuiTestCase { mFakeExecutor.runAllReady(); // Verify visibility controller told to show complications. - ArgumentCaptor delayRunnableCaptor = ArgumentCaptor.forClass(Runnable.class); - verify(mHandler).postDelayed(delayRunnableCaptor.capture(), - eq(Long.valueOf(RESTORE_TIMEOUT))); - delayRunnableCaptor.getValue().run(); - verify(mVisibilityController).setVisibility(eq(View.VISIBLE), anyBoolean()); + mClock.advanceTime(RESTORE_TIMEOUT); + verify(mVisibilityController).setVisibility(eq(View.VISIBLE)); // Verify session ended. verify(mSession).pop();