diff --git a/packages/SystemUI/src/com/android/keyguard/AnimatableClockController.java b/packages/SystemUI/src/com/android/keyguard/AnimatableClockController.java index ac463ebc1c973..1571913020100 100644 --- a/packages/SystemUI/src/com/android/keyguard/AnimatableClockController.java +++ b/packages/SystemUI/src/com/android/keyguard/AnimatableClockController.java @@ -168,6 +168,12 @@ public class AnimatableClockController extends ViewController 100; - } - - void refreshTime() { - mTime.setTimeInMillis(System.currentTimeMillis()); - setText(DateFormat.format(mFormat, mTime)); - setContentDescription(DateFormat.format(mDescFormat, mTime)); - } - - void onTimeZoneChanged(TimeZone timeZone) { - mTime.setTimeZone(timeZone); - refreshFormat(); - } - - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - super.onMeasure(widthMeasureSpec, heightMeasureSpec); - if (mTextAnimator == null) { - mTextAnimator = new TextAnimator( - getLayout(), - () -> { - invalidate(); - return Unit.INSTANCE; - }); - if (mOnTextAnimatorInitialized != null) { - mOnTextAnimatorInitialized.run(); - mOnTextAnimatorInitialized = null; - } - } else { - mTextAnimator.updateLayout(getLayout()); - } - } - - @Override - protected void onDraw(Canvas canvas) { - mTextAnimator.draw(canvas); - } - - void setLineSpacingScale(float scale) { - mLineSpacingScale = scale; - setLineSpacing(0, mLineSpacingScale); - } - - void setColors(int dozingColor, int lockScreenColor) { - mDozingColor = dozingColor; - mLockScreenColor = lockScreenColor; - } - - void animateAppearOnLockscreen() { - if (mTextAnimator == null) { - return; - } - - setTextStyle( - getDozingWeight(), - -1 /* text size, no update */, - mLockScreenColor, - false /* animate */, - 0 /* duration */, - 0 /* delay */, - null /* onAnimationEnd */); - - setTextStyle( - getLockScreenWeight(), - -1 /* text size, no update */, - mLockScreenColor, - true, /* animate */ - APPEAR_ANIM_DURATION, - 0 /* delay */, - null /* onAnimationEnd */); - } - - void animateCharge(DozeStateGetter dozeStateGetter) { - if (mTextAnimator == null || mTextAnimator.isRunning()) { - // Skip charge animation if dozing animation is already playing. - return; - } - Runnable startAnimPhase2 = () -> setTextStyle( - dozeStateGetter.isDozing() ? getDozingWeight() : getLockScreenWeight() /* weight */, - -1, - null, - true /* animate */, - CHARGE_ANIM_DURATION_PHASE_1, - 0 /* delay */, - null /* onAnimationEnd */); - setTextStyle(dozeStateGetter.isDozing() - ? getLockScreenWeight() - : getDozingWeight()/* weight */, - -1, - null, - true /* animate */, - CHARGE_ANIM_DURATION_PHASE_0, - mChargeAnimationDelay, - startAnimPhase2); - } - - void animateDoze(boolean isDozing, boolean animate) { - setTextStyle(isDozing ? getDozingWeight() : getLockScreenWeight() /* weight */, - -1, - isDozing ? mDozingColor : mLockScreenColor, - animate, - DOZE_ANIM_DURATION, - 0 /* delay */, - null /* onAnimationEnd */); - } - - /** - * Set text style with an optional animation. - * - * By passing -1 to weight, the view preserves its current weight. - * By passing -1 to textSize, the view preserves its current text size. - * - * @param weight text weight. - * @param textSize font size. - * @param animate true to animate the text style change, otherwise false. - */ - private void setTextStyle( - @IntRange(from = 0, to = 1000) int weight, - @FloatRange(from = 0) float textSize, - Integer color, - boolean animate, - long duration, - long delay, - Runnable onAnimationEnd) { - if (mTextAnimator != null) { - mTextAnimator.setTextStyle(weight, textSize, color, animate, duration, null, - delay, onAnimationEnd); - } else { - // when the text animator is set, update its start values - mOnTextAnimatorInitialized = - () -> mTextAnimator.setTextStyle( - weight, textSize, color, false, duration, null, - delay, onAnimationEnd); - } - } - - void refreshFormat() { - Patterns.update(mContext); - - final boolean use24HourFormat = DateFormat.is24HourFormat(getContext()); - if (mIsSingleLine && use24HourFormat) { - mFormat = Patterns.sClockView24; - } else if (!mIsSingleLine && use24HourFormat) { - mFormat = DOUBLE_LINE_FORMAT_24_HOUR; - } else if (mIsSingleLine && !use24HourFormat) { - mFormat = Patterns.sClockView12; - } else { - mFormat = DOUBLE_LINE_FORMAT_12_HOUR; - } - - mDescFormat = use24HourFormat ? Patterns.sClockView24 : Patterns.sClockView12; - refreshTime(); - } - - // DateFormat.getBestDateTimePattern is extremely expensive, and refresh is called often. - // This is an optimization to ensure we only recompute the patterns when the inputs change. - private static final class Patterns { - static String sClockView12; - static String sClockView24; - static String sCacheKey; - - static void update(Context context) { - final Locale locale = Locale.getDefault(); - final Resources res = context.getResources(); - final String clockView12Skel = res.getString(R.string.clock_12hr_format); - final String clockView24Skel = res.getString(R.string.clock_24hr_format); - final String key = locale.toString() + clockView12Skel + clockView24Skel; - if (key.equals(sCacheKey)) return; - sClockView12 = DateFormat.getBestDateTimePattern(locale, clockView12Skel); - - // CLDR insists on adding an AM/PM indicator even though it wasn't in the skeleton - // format. The following code removes the AM/PM indicator if we didn't want it. - if (!clockView12Skel.contains("a")) { - sClockView12 = sClockView12.replaceAll("a", "").trim(); - } - sClockView24 = DateFormat.getBestDateTimePattern(locale, clockView24Skel); - sCacheKey = key; - } - } - - interface DozeStateGetter { - boolean isDozing(); - } -} diff --git a/packages/SystemUI/src/com/android/keyguard/AnimatableClockView.kt b/packages/SystemUI/src/com/android/keyguard/AnimatableClockView.kt new file mode 100644 index 0000000000000..357be2503a3a8 --- /dev/null +++ b/packages/SystemUI/src/com/android/keyguard/AnimatableClockView.kt @@ -0,0 +1,370 @@ +/* + * Copyright (C) 2021 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.keyguard + +import android.animation.TimeInterpolator +import android.annotation.ColorInt +import android.annotation.FloatRange +import android.annotation.IntRange +import android.annotation.SuppressLint +import android.content.Context +import android.graphics.Canvas +import android.text.format.DateFormat +import android.util.AttributeSet +import android.widget.TextView +import com.android.systemui.R +import com.android.systemui.animation.Interpolators +import com.android.systemui.statusbar.notification.stack.StackStateAnimator +import java.util.Calendar +import java.util.Locale +import java.util.TimeZone + +/** + * Displays the time with the hour positioned above the minutes. (ie: 09 above 30 is 9:30) + * The time's text color is a gradient that changes its colors based on its controller. + */ +class AnimatableClockView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0, + defStyleRes: Int = 0 +) : TextView(context, attrs, defStyleAttr, defStyleRes) { + + private val time = Calendar.getInstance() + + private val dozingWeightInternal: Int + private val lockScreenWeightInternal: Int + private val isSingleLineInternal: Boolean + + private var format: CharSequence? = null + private var descFormat: CharSequence? = null + + @ColorInt + private var dozingColor = 0 + + @ColorInt + private var lockScreenColor = 0 + + private var lineSpacingScale = 1f + private val chargeAnimationDelay: Int + private var textAnimator: TextAnimator? = null + private var onTextAnimatorInitialized: Runnable? = null + + val dozingWeight: Int + get() = if (useBoldedVersion()) dozingWeightInternal + 100 else dozingWeightInternal + + val lockScreenWeight: Int + get() = if (useBoldedVersion()) lockScreenWeightInternal + 100 else lockScreenWeightInternal + + init { + val animatableClockViewAttributes = context.obtainStyledAttributes( + attrs, R.styleable.AnimatableClockView, defStyleAttr, defStyleRes + ) + + try { + dozingWeightInternal = animatableClockViewAttributes.getInt( + R.styleable.AnimatableClockView_dozeWeight, + 100 + ) + lockScreenWeightInternal = animatableClockViewAttributes.getInt( + R.styleable.AnimatableClockView_lockScreenWeight, + 300 + ) + chargeAnimationDelay = animatableClockViewAttributes.getInt( + R.styleable.AnimatableClockView_chargeAnimationDelay, 200 + ) + } finally { + animatableClockViewAttributes.recycle() + } + + val textViewAttributes = context.obtainStyledAttributes( + attrs, android.R.styleable.TextView, + defStyleAttr, defStyleRes + ) + + isSingleLineInternal = + try { + textViewAttributes.getBoolean(android.R.styleable.TextView_singleLine, false) + } finally { + textViewAttributes.recycle() + } + + refreshFormat() + } + + override fun onAttachedToWindow() { + super.onAttachedToWindow() + refreshFormat() + } + + /** + * Whether to use a bolded version based on the user specified fontWeightAdjustment. + */ + fun useBoldedVersion(): Boolean { + // "Bold text" fontWeightAdjustment is 300. + return resources.configuration.fontWeightAdjustment > 100 + } + + fun refreshTime() { + time.timeInMillis = System.currentTimeMillis() + text = DateFormat.format(format, time) + contentDescription = DateFormat.format(descFormat, time) + } + + fun onTimeZoneChanged(timeZone: TimeZone?) { + time.timeZone = timeZone + refreshFormat() + } + + @SuppressLint("DrawAllocation") + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec) + val animator = textAnimator + if (animator == null) { + textAnimator = TextAnimator(layout) { invalidate() } + onTextAnimatorInitialized?.run() + onTextAnimatorInitialized = null + } else { + animator.updateLayout(layout) + } + } + + override fun onDraw(canvas: Canvas) { + textAnimator?.draw(canvas) + } + + fun setLineSpacingScale(scale: Float) { + lineSpacingScale = scale + setLineSpacing(0f, lineSpacingScale) + } + + fun setColors(@ColorInt dozingColor: Int, lockScreenColor: Int) { + this.dozingColor = dozingColor + this.lockScreenColor = lockScreenColor + } + + fun animateAppearOnLockscreen() { + if (textAnimator == null) { + return + } + setTextStyle( + weight = dozingWeight, + textSize = -1f, + color = lockScreenColor, + animate = false, + duration = 0, + delay = 0, + onAnimationEnd = null + ) + setTextStyle( + weight = lockScreenWeight, + textSize = -1f, + color = lockScreenColor, + animate = true, + duration = APPEAR_ANIM_DURATION, + delay = 0, + onAnimationEnd = null + ) + } + + fun animateFoldAppear() { + if (textAnimator == null) { + return + } + setTextStyle( + weight = lockScreenWeightInternal, + textSize = -1f, + color = lockScreenColor, + animate = false, + duration = 0, + delay = 0, + onAnimationEnd = null + ) + setTextStyle( + weight = dozingWeightInternal, + textSize = -1f, + color = dozingColor, + animate = true, + interpolator = Interpolators.EMPHASIZED_DECELERATE, + duration = StackStateAnimator.ANIMATION_DURATION_FOLD_TO_AOD.toLong(), + delay = 0, + onAnimationEnd = null + ) + } + + fun animateCharge(dozeStateGetter: DozeStateGetter) { + if (textAnimator == null || textAnimator!!.isRunning()) { + // Skip charge animation if dozing animation is already playing. + return + } + val startAnimPhase2 = Runnable { + setTextStyle( + weight = if (dozeStateGetter.isDozing) dozingWeight else lockScreenWeight, + textSize = -1f, + color = null, + animate = true, + duration = CHARGE_ANIM_DURATION_PHASE_1, + delay = 0, + onAnimationEnd = null + ) + } + setTextStyle( + weight = if (dozeStateGetter.isDozing) lockScreenWeight else dozingWeight, + textSize = -1f, + color = null, + animate = true, + duration = CHARGE_ANIM_DURATION_PHASE_0, + delay = chargeAnimationDelay.toLong(), + onAnimationEnd = startAnimPhase2 + ) + } + + fun animateDoze(isDozing: Boolean, animate: Boolean) { + setTextStyle( + weight = if (isDozing) dozingWeight else lockScreenWeight, + textSize = -1f, + color = if (isDozing) dozingColor else lockScreenColor, + animate = animate, + duration = DOZE_ANIM_DURATION, + delay = 0, + onAnimationEnd = null + ) + } + + /** + * Set text style with an optional animation. + * + * By passing -1 to weight, the view preserves its current weight. + * By passing -1 to textSize, the view preserves its current text size. + * + * @param weight text weight. + * @param textSize font size. + * @param animate true to animate the text style change, otherwise false. + */ + private fun setTextStyle( + @IntRange(from = 0, to = 1000) weight: Int, + @FloatRange(from = 0.0) textSize: Float, + color: Int?, + animate: Boolean, + interpolator: TimeInterpolator?, + duration: Long, + delay: Long, + onAnimationEnd: Runnable? + ) { + if (textAnimator != null) { + textAnimator?.setTextStyle( + weight = weight, + textSize = textSize, + color = color, + animate = animate, + duration = duration, + interpolator = interpolator, + delay = delay, + onAnimationEnd = onAnimationEnd + ) + } else { + // when the text animator is set, update its start values + onTextAnimatorInitialized = Runnable { + textAnimator?.setTextStyle( + weight = weight, + textSize = textSize, + color = color, + animate = false, + duration = duration, + interpolator = interpolator, + delay = delay, + onAnimationEnd = onAnimationEnd + ) + } + } + } + + private fun setTextStyle( + @IntRange(from = 0, to = 1000) weight: Int, + @FloatRange(from = 0.0) textSize: Float, + color: Int?, + animate: Boolean, + duration: Long, + delay: Long, + onAnimationEnd: Runnable? + ) { + setTextStyle( + weight = weight, + textSize = textSize, + color = color, + animate = animate, + interpolator = null, + duration = duration, + delay = delay, + onAnimationEnd = onAnimationEnd + ) + } + + fun refreshFormat() { + Patterns.update(context) + val use24HourFormat = DateFormat.is24HourFormat(context) + + format = when { + isSingleLineInternal && use24HourFormat -> Patterns.sClockView24 + !isSingleLineInternal && use24HourFormat -> DOUBLE_LINE_FORMAT_24_HOUR + isSingleLineInternal && !use24HourFormat -> Patterns.sClockView12 + else -> DOUBLE_LINE_FORMAT_12_HOUR + } + + descFormat = if (use24HourFormat) Patterns.sClockView24 else Patterns.sClockView12 + + refreshTime() + } + + // DateFormat.getBestDateTimePattern is extremely expensive, and refresh is called often. + // This is an optimization to ensure we only recompute the patterns when the inputs change. + private object Patterns { + var sClockView12: String? = null + var sClockView24: String? = null + var sCacheKey: String? = null + + fun update(context: Context) { + val locale = Locale.getDefault() + val res = context.resources + val clockView12Skel = res.getString(R.string.clock_12hr_format) + val clockView24Skel = res.getString(R.string.clock_24hr_format) + val key = locale.toString() + clockView12Skel + clockView24Skel + if (key == sCacheKey) return + + val clockView12 = DateFormat.getBestDateTimePattern(locale, clockView12Skel) + sClockView12 = clockView12 + + // CLDR insists on adding an AM/PM indicator even though it wasn't in the skeleton + // format. The following code removes the AM/PM indicator if we didn't want it. + if (!clockView12Skel.contains("a")) { + sClockView12 = clockView12.replace("a".toRegex(), "").trim { it <= ' ' } + } + sClockView24 = DateFormat.getBestDateTimePattern(locale, clockView24Skel) + sCacheKey = key + } + } + + interface DozeStateGetter { + val isDozing: Boolean + } +} + +private const val DOUBLE_LINE_FORMAT_12_HOUR = "hh\nmm" +private const val DOUBLE_LINE_FORMAT_24_HOUR = "HH\nmm" +private const val DOZE_ANIM_DURATION: Long = 300 +private const val APPEAR_ANIM_DURATION: Long = 350 +private const val CHARGE_ANIM_DURATION_PHASE_0: Long = 500 +private const val CHARGE_ANIM_DURATION_PHASE_1: Long = 1000 diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java index 9238b8226bbc1..25dcdf9aa5610 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java @@ -190,11 +190,15 @@ public class KeyguardClockSwitch extends RelativeLayout { } } - private void animateClockChange(boolean useLargeClock) { + private void updateClockViews(boolean useLargeClock, boolean animate) { if (mClockInAnim != null) mClockInAnim.cancel(); if (mClockOutAnim != null) mClockOutAnim.cancel(); if (mStatusAreaAnim != null) mStatusAreaAnim.cancel(); + mClockInAnim = null; + mClockOutAnim = null; + mStatusAreaAnim = null; + View in, out; int direction = 1; float statusAreaYTranslation; @@ -214,6 +218,14 @@ public class KeyguardClockSwitch extends RelativeLayout { removeView(out); } + if (!animate) { + out.setAlpha(0f); + in.setAlpha(1f); + in.setVisibility(VISIBLE); + mStatusArea.setTranslationY(statusAreaYTranslation); + return; + } + mClockOutAnim = new AnimatorSet(); mClockOutAnim.setDuration(CLOCK_OUT_MILLIS); mClockOutAnim.setInterpolator(Interpolators.FAST_OUT_LINEAR_IN); @@ -273,7 +285,7 @@ public class KeyguardClockSwitch extends RelativeLayout { * * @return true if desired clock appeared and false if it was already visible */ - boolean switchToClock(@ClockSize int clockSize) { + boolean switchToClock(@ClockSize int clockSize, boolean animate) { if (mDisplayedClockSize != null && clockSize == mDisplayedClockSize) { return false; } @@ -281,7 +293,7 @@ public class KeyguardClockSwitch extends RelativeLayout { // let's make sure clock is changed only after all views were laid out so we can // translate them properly if (mChildrenAreLaidOut) { - animateClockChange(clockSize == LARGE); + updateClockViews(clockSize == LARGE, animate); } mDisplayedClockSize = clockSize; @@ -293,7 +305,7 @@ public class KeyguardClockSwitch extends RelativeLayout { super.onLayout(changed, l, t, r, b); if (mDisplayedClockSize != null && !mChildrenAreLaidOut) { - animateClockChange(mDisplayedClockSize == LARGE); + updateClockViews(mDisplayedClockSize == LARGE, /* animate */ true); } mChildrenAreLaidOut = true; diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java index c628d4401bb11..a4830561489b1 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java @@ -292,17 +292,24 @@ public class KeyguardClockSwitchController extends ViewController displayClock(KeyguardClockSwitch.SMALL)); + mUiExecutor.execute(() -> displayClock(KeyguardClockSwitch.SMALL, /* animate */ true)); } } } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java index 986d0debb56af..8bf890d7df504 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java @@ -123,8 +123,17 @@ public class KeyguardStatusViewController extends ViewController sysUiUnfoldComponent, ConfigurationController configurationController) { mContext = context; mWakeLock = wakeLock; @@ -118,12 +125,23 @@ public class DozeUi implements DozeMachine.Part, TunerService.Tunable, mConfigurationController = configurationController; mConfigurationController.addCallback(this); + + mFoldAodAnimationController = sysUiUnfoldComponent + .map(SysUIUnfoldComponent::getFoldAodAnimationController).orElse(null); + + if (mFoldAodAnimationController != null) { + mFoldAodAnimationController.addCallback(this); + } } @Override public void destroy() { mTunerService.removeTunable(this); mConfigurationController.removeCallback(this); + + if (mFoldAodAnimationController != null) { + mFoldAodAnimationController.removeCallback(this); + } } @Override @@ -142,7 +160,8 @@ public class DozeUi implements DozeMachine.Part, TunerService.Tunable, && (mKeyguardShowing || mDozeParameters.shouldControlUnlockedScreenOff()) && !mHost.isPowerSaveActive(); mDozeParameters.setControlScreenOffAnimation(controlScreenOff); - mHost.setAnimateScreenOff(controlScreenOff); + mHost.setAnimateScreenOff(controlScreenOff + && mDozeParameters.shouldAnimateDozingChange()); } } @@ -299,4 +318,9 @@ public class DozeUi implements DozeMachine.Part, TunerService.Tunable, public void onStatePostChange() { updateAnimateScreenOff(); } + + @Override + public void onFoldToAodAnimationChanged() { + updateAnimateScreenOff(); + } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java index c14d32ed5a6e7..217244938ee9a 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java @@ -122,6 +122,7 @@ import com.android.systemui.statusbar.phone.StatusBar; import com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManager; import com.android.systemui.statusbar.policy.KeyguardStateController; import com.android.systemui.statusbar.policy.UserSwitcherController; +import com.android.systemui.unfold.FoldAodAnimationController; import com.android.systemui.unfold.SysUIUnfoldComponent; import com.android.systemui.unfold.UnfoldLightRevealOverlayAnimation; import com.android.systemui.util.DeviceConfigProxy; @@ -131,7 +132,6 @@ import java.io.PrintWriter; import java.util.ArrayList; import java.util.Optional; import java.util.concurrent.Executor; -import java.util.concurrent.atomic.AtomicInteger; import dagger.Lazy; @@ -439,7 +439,7 @@ public class KeyguardViewMediator extends CoreStartable implements Dumpable, private boolean mInGestureNavigationMode; private boolean mWakeAndUnlocking; - private IKeyguardDrawnCallback mDrawnCallback; + private Runnable mWakeAndUnlockingDrawnCallback; private CharSequence mCustomMessage; /** @@ -817,7 +817,8 @@ public class KeyguardViewMediator extends CoreStartable implements Dumpable, private DozeParameters mDozeParameters; private final Optional mUnfoldLightRevealAnimation; - private final AtomicInteger mPendingDrawnTasks = new AtomicInteger(); + private final Optional mFoldAodAnimationController; + private final PendingDrawnTasksContainer mPendingDrawnTasks = new PendingDrawnTasksContainer(); private final KeyguardStateController mKeyguardStateController; private final Lazy mKeyguardUnlockAnimationControllerLazy; @@ -877,8 +878,12 @@ public class KeyguardViewMediator extends CoreStartable implements Dumpable, mInGestureNavigationMode = QuickStepContract.isGesturalMode(mode); })); mDozeParameters = dozeParameters; - mUnfoldLightRevealAnimation = unfoldComponent.map( - c -> c.getUnfoldLightRevealOverlayAnimation()); + + mUnfoldLightRevealAnimation = unfoldComponent + .map(SysUIUnfoldComponent::getUnfoldLightRevealOverlayAnimation); + mFoldAodAnimationController = unfoldComponent + .map(SysUIUnfoldComponent::getFoldAodAnimationController); + mStatusBarStateController = statusBarStateController; statusBarStateController.addCallback(this); @@ -1069,7 +1074,7 @@ public class KeyguardViewMediator extends CoreStartable implements Dumpable, mDeviceInteractive = false; mGoingToSleep = false; mWakeAndUnlocking = false; - mAnimatingScreenOff = mDozeParameters.shouldControlUnlockedScreenOff(); + mAnimatingScreenOff = mDozeParameters.shouldAnimateDozingChange(); resetKeyguardDonePendingLocked(); mHideAnimationRun = false; @@ -2221,14 +2226,14 @@ public class KeyguardViewMediator extends CoreStartable implements Dumpable, IRemoteAnimationRunner runner = mKeyguardExitAnimationRunner; mKeyguardExitAnimationRunner = null; - if (mWakeAndUnlocking && mDrawnCallback != null) { + if (mWakeAndUnlocking && mWakeAndUnlockingDrawnCallback != null) { // Hack level over 9000: To speed up wake-and-unlock sequence, force it to report // the next draw from here so we don't have to wait for window manager to signal // this to our ViewRootImpl. mKeyguardViewControllerLazy.get().getViewRootImpl().setReportNextDraw(); - notifyDrawn(mDrawnCallback); - mDrawnCallback = null; + mWakeAndUnlockingDrawnCallback.run(); + mWakeAndUnlockingDrawnCallback = null; } LatencyTracker.getInstance(mContext) @@ -2566,31 +2571,27 @@ public class KeyguardViewMediator extends CoreStartable implements Dumpable, synchronized (KeyguardViewMediator.this) { if (DEBUG) Log.d(TAG, "handleNotifyScreenTurningOn"); - if (mUnfoldLightRevealAnimation.isPresent()) { - mPendingDrawnTasks.set(2); // unfold overlay and keyguard drawn + mPendingDrawnTasks.reset(); + if (mUnfoldLightRevealAnimation.isPresent()) { mUnfoldLightRevealAnimation.get() - .onScreenTurningOn(() -> { - if (mPendingDrawnTasks.decrementAndGet() == 0) { - try { - callback.onDrawn(); - } catch (RemoteException e) { - Slog.w(TAG, "Exception calling onDrawn():", e); - } - } - }); - } else { - mPendingDrawnTasks.set(1); // only keyguard drawn + .onScreenTurningOn(mPendingDrawnTasks.registerTask("unfold-reveal")); + } + + if (mFoldAodAnimationController.isPresent()) { + mFoldAodAnimationController.get() + .onScreenTurningOn(mPendingDrawnTasks.registerTask("fold-to-aod")); } mKeyguardViewControllerLazy.get().onScreenTurningOn(); if (callback != null) { if (mWakeAndUnlocking) { - mDrawnCallback = callback; - } else { - notifyDrawn(callback); + mWakeAndUnlockingDrawnCallback = + mPendingDrawnTasks.registerTask("wake-and-unlocking"); } } + + mPendingDrawnTasks.onTasksComplete(() -> notifyDrawn(callback)); } Trace.endSection(); } @@ -2599,6 +2600,8 @@ public class KeyguardViewMediator extends CoreStartable implements Dumpable, Trace.beginSection("KeyguardViewMediator#handleNotifyScreenTurnedOn"); synchronized (this) { if (DEBUG) Log.d(TAG, "handleNotifyScreenTurnedOn"); + + mPendingDrawnTasks.reset(); mKeyguardViewControllerLazy.get().onScreenTurnedOn(); } Trace.endSection(); @@ -2607,18 +2610,18 @@ public class KeyguardViewMediator extends CoreStartable implements Dumpable, private void handleNotifyScreenTurnedOff() { synchronized (this) { if (DEBUG) Log.d(TAG, "handleNotifyScreenTurnedOff"); - mDrawnCallback = null; + mWakeAndUnlockingDrawnCallback = null; } } private void notifyDrawn(final IKeyguardDrawnCallback callback) { Trace.beginSection("KeyguardViewMediator#notifyDrawn"); - if (mPendingDrawnTasks.decrementAndGet() == 0) { - try { + try { + if (callback != null) { callback.onDrawn(); - } catch (RemoteException e) { - Slog.w(TAG, "Exception calling onDrawn():", e); } + } catch (RemoteException e) { + Slog.w(TAG, "Exception calling onDrawn():", e); } Trace.endSection(); } @@ -2777,9 +2780,9 @@ public class KeyguardViewMediator extends CoreStartable implements Dumpable, pw.print(" mHideAnimationRun: "); pw.println(mHideAnimationRun); pw.print(" mPendingReset: "); pw.println(mPendingReset); pw.print(" mPendingLock: "); pw.println(mPendingLock); - pw.print(" mPendingDrawnTasks: "); pw.println(mPendingDrawnTasks.get()); + pw.print(" mPendingDrawnTasks: "); pw.println(mPendingDrawnTasks.getPendingCount()); pw.print(" mWakeAndUnlocking: "); pw.println(mWakeAndUnlocking); - pw.print(" mDrawnCallback: "); pw.println(mDrawnCallback); + pw.print(" mDrawnCallback: "); pw.println(mWakeAndUnlockingDrawnCallback); } /** diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/PendingDrawnTasksContainer.kt b/packages/SystemUI/src/com/android/systemui/keyguard/PendingDrawnTasksContainer.kt new file mode 100644 index 0000000000000..bccd106db8362 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/PendingDrawnTasksContainer.kt @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2021 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 + +import android.os.Trace +import java.util.concurrent.atomic.AtomicInteger +import java.util.concurrent.atomic.AtomicReference + +/** + * Allows to wait for multiple callbacks and notify when the last one is executed + */ +class PendingDrawnTasksContainer { + + private lateinit var pendingDrawnTasksCount: AtomicInteger + private var completionCallback: AtomicReference = AtomicReference() + + /** + * Registers a task that we should wait for + * @return a runnable that should be invoked when the task is finished + */ + fun registerTask(name: String): Runnable { + pendingDrawnTasksCount.incrementAndGet() + + if (ENABLE_TRACE) { + Trace.beginAsyncSection("PendingDrawnTasksContainer#$name", 0) + } + + return Runnable { + if (pendingDrawnTasksCount.decrementAndGet() == 0) { + val onComplete = completionCallback.getAndSet(null) + onComplete?.run() + + if (ENABLE_TRACE) { + Trace.endAsyncSection("PendingDrawnTasksContainer#$name", 0) + } + } + } + } + + /** + * Clears state and initializes the container + */ + fun reset() { + // Create new objects in case if there are pending callbacks from the previous invocations + completionCallback = AtomicReference() + pendingDrawnTasksCount = AtomicInteger(0) + } + + /** + * Starts waiting for all tasks to be completed + * When all registered tasks complete it will invoke the [onComplete] callback + */ + fun onTasksComplete(onComplete: Runnable) { + completionCallback.set(onComplete) + + if (pendingDrawnTasksCount.get() == 0) { + val currentOnComplete = completionCallback.getAndSet(null) + currentOnComplete?.run() + } + } + + /** + * Returns current pending tasks count + */ + fun getPendingCount(): Int = pendingDrawnTasksCount.get() +} + +private const val ENABLE_TRACE = false diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateAnimator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateAnimator.java index e3a4bf0170fb0..2dc92764a4afd 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateAnimator.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateAnimator.java @@ -51,6 +51,7 @@ public class StackStateAnimator { public static final int ANIMATION_DURATION_CLOSE_REMOTE_INPUT = 150; public static final int ANIMATION_DURATION_HEADS_UP_APPEAR = 400; public static final int ANIMATION_DURATION_HEADS_UP_DISAPPEAR = 400; + public static final int ANIMATION_DURATION_FOLD_TO_AOD = 600; public static final int ANIMATION_DURATION_PULSE_APPEAR = KeyguardSliceView.DEFAULT_ANIM_DURATION; public static final int ANIMATION_DURATION_BLOCKING_HELPER_FADE = 240; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeParameters.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeParameters.java index 4b8b580eba0b3..d87a02493a231 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeParameters.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeParameters.java @@ -243,6 +243,10 @@ public class DozeParameters implements return mScreenOffAnimationController.shouldShowLightRevealScrim(); } + public boolean shouldAnimateDozingChange() { + return mScreenOffAnimationController.shouldAnimateDozingChange(); + } + /** * Whether we're capable of controlling the screen off animation if we want to. This isn't * possible if AOD isn't even enabled or if the flag is disabled. @@ -324,6 +328,7 @@ public class DozeParameters implements for (Callback callback : mCallbacks) { callback.onAlwaysOnChange(); } + mScreenOffAnimationController.onAlwaysOnChanged(getAlwaysOn()); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java index 715b3cfce7d04..3e6d204ac1007 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java @@ -26,6 +26,7 @@ import static androidx.constraintlayout.widget.ConstraintSet.TOP; import static com.android.internal.jank.InteractionJankMonitor.CUJ_NOTIFICATION_SHADE_QS_EXPAND_COLLAPSE; import static com.android.keyguard.KeyguardClockSwitch.LARGE; import static com.android.keyguard.KeyguardClockSwitch.SMALL; +import static com.android.systemui.animation.Interpolators.EMPHASIZED_DECELERATE; import static com.android.systemui.classifier.Classifier.QS_COLLAPSE; import static com.android.systemui.classifier.Classifier.QUICK_SETTINGS; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED; @@ -34,6 +35,7 @@ import static com.android.systemui.statusbar.StatusBarState.KEYGUARD; import static com.android.systemui.statusbar.StatusBarState.SHADE; import static com.android.systemui.statusbar.StatusBarState.SHADE_LOCKED; import static com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.ROWS_ALL; +import static com.android.systemui.statusbar.notification.stack.StackStateAnimator.ANIMATION_DURATION_FOLD_TO_AOD; import static com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManagerKt.STATE_CLOSED; import static com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManagerKt.STATE_OPEN; import static com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManagerKt.STATE_OPENING; @@ -1425,11 +1427,12 @@ public class NotificationPanelViewController extends PanelViewController { .getVisibleNotificationCount() != 0 || mMediaDataManager.hasActiveMedia(); boolean splitShadeWithActiveMedia = mShouldUseSplitNotificationShade && mMediaDataManager.hasActiveMedia(); + boolean shouldAnimateClockChange = mScreenOffAnimationController.shouldAnimateClockChange(); if ((hasVisibleNotifications && !mShouldUseSplitNotificationShade) || (splitShadeWithActiveMedia && !mDozing)) { - mKeyguardStatusViewController.displayClock(SMALL); + mKeyguardStatusViewController.displayClock(SMALL, shouldAnimateClockChange); } else { - mKeyguardStatusViewController.displayClock(LARGE); + mKeyguardStatusViewController.displayClock(LARGE, shouldAnimateClockChange); } updateKeyguardStatusViewAlignment(true /* animate */); int userIconHeight = mKeyguardQsUserSwitchController != null @@ -1465,7 +1468,7 @@ public class NotificationPanelViewController extends PanelViewController { mKeyguardStatusViewController.isClockTopAligned()); mClockPositionAlgorithm.run(mClockPositionResult); boolean animate = mNotificationStackScrollLayoutController.isAddOrRemoveAnimationPending(); - boolean animateClock = animate || mAnimateNextPositionUpdate; + boolean animateClock = (animate || mAnimateNextPositionUpdate) && shouldAnimateClockChange; mKeyguardStatusViewController.updatePosition( mClockPositionResult.clockX, mClockPositionResult.clockY, mClockPositionResult.clockScale, animateClock); @@ -3826,6 +3829,45 @@ public class NotificationPanelViewController extends PanelViewController { } } + /** + * Updates the views to the initial state for the fold to AOD animation + */ + public void prepareFoldToAodAnimation() { + // Force show AOD UI even if we are not locked + showAodUi(); + + // Move the content of the AOD all the way to the left + // so we can animate to the initial position + final int translationAmount = mView.getResources().getDimensionPixelSize( + R.dimen.below_clock_padding_start); + mView.setTranslationX(-translationAmount); + mView.setAlpha(0); + } + + /** + * Starts fold to AOD animation + */ + public void startFoldToAodAnimation(Runnable endAction) { + mView.animate() + .translationX(0) + .alpha(1f) + .setDuration(ANIMATION_DURATION_FOLD_TO_AOD) + .setInterpolator(EMPHASIZED_DECELERATE) + .setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationCancel(Animator animation) { + endAction.run(); + } + @Override + public void onAnimationEnd(Animator animation) { + endAction.run(); + } + }) + .start(); + + mKeyguardStatusViewController.animateFoldToAod(); + } + /** */ public void setImportantForAccessibility(int mode) { mView.setImportantForAccessibility(mode); @@ -3940,6 +3982,10 @@ public class NotificationPanelViewController extends PanelViewController { mView.setAlpha(alpha); } + public void resetTranslation() { + mView.setTranslationX(0f); + } + public ViewPropertyAnimator fadeOut(long startDelayMs, long durationMs, Runnable endAction) { return mView.animate().alpha(0).setStartDelay(startDelayMs).setDuration( durationMs).setInterpolator(Interpolators.ALPHA_OUT).withLayer().withEndAction( diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScreenOffAnimationController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScreenOffAnimationController.kt index 497e7d78ac4ef..e806ca0d9005d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScreenOffAnimationController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScreenOffAnimationController.kt @@ -19,16 +19,23 @@ import android.view.View import com.android.systemui.dagger.SysUISingleton import com.android.systemui.keyguard.WakefulnessLifecycle import com.android.systemui.statusbar.LightRevealScrim +import com.android.systemui.unfold.FoldAodAnimationController +import com.android.systemui.unfold.SysUIUnfoldComponent +import java.util.Optional import javax.inject.Inject @SysUISingleton class ScreenOffAnimationController @Inject constructor( + sysUiUnfoldComponent: Optional, unlockedScreenOffAnimation: UnlockedScreenOffAnimationController, private val wakefulnessLifecycle: WakefulnessLifecycle, ) : WakefulnessLifecycle.Observer { - // TODO(b/202844967) add fold to aod animation here - private val animations: List = listOf(unlockedScreenOffAnimation) + private val foldToAodAnimation: FoldAodAnimationController? = sysUiUnfoldComponent + .orElse(null)?.getFoldAodAnimationController() + + private val animations: List = + listOfNotNull(foldToAodAnimation, unlockedScreenOffAnimation) fun initialize(statusBar: StatusBar, lightRevealScrim: LightRevealScrim) { animations.forEach { it.initialize(statusBar, lightRevealScrim) } @@ -42,6 +49,19 @@ class ScreenOffAnimationController @Inject constructor( animations.firstOrNull { it.startAnimation() } } + /** + * Called when opaqueness of the light reveal scrim has change + * When [isOpaque] is true then scrim is visible and covers the screen + */ + fun onScrimOpaqueChanged(isOpaque: Boolean) = + animations.forEach { it.onScrimOpaqueChanged(isOpaque) } + + /** + * Called when always on display setting changed + */ + fun onAlwaysOnChanged(alwaysOn: Boolean) = + animations.forEach { it.onAlwaysOnChanged(alwaysOn) } + /** * If returns true we are taking over the screen off animation from display manager to SysUI. * We can play our custom animation instead of default fade out animation. @@ -102,6 +122,12 @@ class ScreenOffAnimationController @Inject constructor( fun isKeyguardShowDelayed(): Boolean = animations.any { it.isAnimationPlaying() } + /** + * Return true to ignore requests to hide keyguard + */ + fun isKeyguardHideDelayed(): Boolean = + animations.any { it.isKeyguardHideDelayed() } + /** * Return true to make the StatusBar expanded so we can animate [LightRevealScrim] */ @@ -145,6 +171,19 @@ class ScreenOffAnimationController @Inject constructor( */ fun shouldAnimateAodIcons(): Boolean = animations.all { it.shouldAnimateAodIcons() } + + /** + * Return true to animate doze state change, if returns false dozing will be applied without + * animation (sends only 0.0f or 1.0f dozing progress) + */ + fun shouldAnimateDozingChange(): Boolean = + animations.all { it.shouldAnimateDozingChange() } + + /** + * Return true to animate large <-> small clock transition + */ + fun shouldAnimateClockChange(): Boolean = + animations.all { it.shouldAnimateClockChange() } } interface ScreenOffAnimation { @@ -158,11 +197,17 @@ interface ScreenOffAnimation { fun shouldPlayAnimation(): Boolean = false fun isAnimationPlaying(): Boolean = false + fun onScrimOpaqueChanged(isOpaque: Boolean) {} + fun onAlwaysOnChanged(alwaysOn: Boolean) {} + fun shouldAnimateInKeyguard(): Boolean = false fun animateInKeyguard(keyguardView: View, after: Runnable) = after.run() + fun isKeyguardHideDelayed(): Boolean = false fun shouldHideScrimOnWakeUp(): Boolean = false fun overrideNotificationsDozeAmount(): Boolean = false fun shouldShowAodIconsWhenShade(): Boolean = false fun shouldAnimateAodIcons(): Boolean = true + fun shouldAnimateDozingChange(): Boolean = true + fun shouldAnimateClockChange(): Boolean = true } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java index f50bf8eb19c69..818f7c3ef871e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java @@ -1234,6 +1234,8 @@ public class StatusBar extends CoreStartable implements Runnable updateOpaqueness = () -> { mNotificationShadeWindowController.setLightRevealScrimOpaque( mLightRevealScrim.isScrimOpaque()); + mScreenOffAnimationController + .onScrimOpaqueChanged(mLightRevealScrim.isScrimOpaque()); }; if (opaque) { // Delay making the view opaque for a frame, because it needs some time to render @@ -2955,7 +2957,17 @@ public class StatusBar extends CoreStartable implements showKeyguardImpl(); } } else { - return hideKeyguardImpl(force); + // During folding a foldable device this might be called as a result of + // 'onScreenTurnedOff' call for the inner display. + // In this case: + // * When phone is locked on folding: it doesn't make sense to hide keyguard as it + // will be immediately locked again + // * When phone is unlocked: we still don't want to execute hiding of the keyguard + // as the animation could prepare 'fake AOD' interface (without actually + // transitioning to keyguard state) and this might reset the view states + if (!mScreenOffAnimationController.isKeyguardHideDelayed()) { + return hideKeyguardImpl(force); + } } return false; } @@ -3118,6 +3130,7 @@ public class StatusBar extends CoreStartable implements mNotificationPanelViewController.onAffordanceLaunchEnded(); mNotificationPanelViewController.cancelAnimation(); mNotificationPanelViewController.setAlpha(1f); + mNotificationPanelViewController.resetTranslation(); mNotificationPanelViewController.resetViewGroupFade(); updateDozingState(); updateScrimController(); diff --git a/packages/SystemUI/src/com/android/systemui/unfold/FoldAodAnimationController.kt b/packages/SystemUI/src/com/android/systemui/unfold/FoldAodAnimationController.kt new file mode 100644 index 0000000000000..fb9df01a02514 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/unfold/FoldAodAnimationController.kt @@ -0,0 +1,173 @@ +/* + * Copyright (C) 2021 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.unfold + +import android.os.PowerManager +import android.provider.Settings +import com.android.systemui.keyguard.KeyguardViewMediator +import com.android.systemui.keyguard.ScreenLifecycle +import com.android.systemui.keyguard.WakefulnessLifecycle +import com.android.systemui.statusbar.LightRevealScrim +import com.android.systemui.statusbar.phone.ScreenOffAnimation +import com.android.systemui.statusbar.phone.StatusBar +import com.android.systemui.statusbar.policy.CallbackController +import com.android.systemui.unfold.FoldAodAnimationController.FoldAodAnimationStatus +import com.android.systemui.util.settings.GlobalSettings +import dagger.Lazy +import javax.inject.Inject + +/** + * Controls folding to AOD animation: when AOD is enabled and foldable device is folded + * we play a special AOD animation on the outer screen + */ +@SysUIUnfoldScope +class FoldAodAnimationController @Inject constructor( + private val screenLifecycle: ScreenLifecycle, + private val keyguardViewMediatorLazy: Lazy, + private val wakefulnessLifecycle: WakefulnessLifecycle, + private val globalSettings: GlobalSettings +) : ScreenLifecycle.Observer, + CallbackController, + ScreenOffAnimation, + WakefulnessLifecycle.Observer { + + private var alwaysOnEnabled: Boolean = false + private var isScrimOpaque: Boolean = false + private lateinit var statusBar: StatusBar + private var pendingScrimReadyCallback: Runnable? = null + + private var shouldPlayAnimation = false + private val statusListeners = arrayListOf() + + private var isAnimationPlaying = false + + override fun initialize(statusBar: StatusBar, lightRevealScrim: LightRevealScrim) { + this.statusBar = statusBar + + screenLifecycle.addObserver(this) + wakefulnessLifecycle.addObserver(this) + } + + /** + * Returns true if we should run fold to AOD animation + */ + override fun shouldPlayAnimation(): Boolean = + shouldPlayAnimation + + override fun startAnimation(): Boolean = + if (alwaysOnEnabled && + wakefulnessLifecycle.lastSleepReason == PowerManager.GO_TO_SLEEP_REASON_DEVICE_FOLD && + globalSettings.getString(Settings.Global.ANIMATOR_DURATION_SCALE) != "0" + ) { + shouldPlayAnimation = true + + isAnimationPlaying = true + statusBar.notificationPanelViewController.prepareFoldToAodAnimation() + + statusListeners.forEach(FoldAodAnimationStatus::onFoldToAodAnimationChanged) + + true + } else { + shouldPlayAnimation = false + false + } + + override fun onStartedWakingUp() { + shouldPlayAnimation = false + isAnimationPlaying = false + } + + /** + * Called when screen starts turning on, the contents of the screen might not be visible yet. + * This method reports back that the animation is ready in [onReady] callback. + * + * @param onReady callback when the animation is ready + * @see [com.android.systemui.keyguard.KeyguardViewMediator] + */ + fun onScreenTurningOn(onReady: Runnable) { + if (shouldPlayAnimation) { + if (isScrimOpaque) { + onReady.run() + } else { + pendingScrimReadyCallback = onReady + } + } else { + // No animation, call ready callback immediately + onReady.run() + } + } + + /** + * Called when keyguard scrim opaque changed + */ + override fun onScrimOpaqueChanged(isOpaque: Boolean) { + isScrimOpaque = isOpaque + + if (isOpaque) { + pendingScrimReadyCallback?.run() + pendingScrimReadyCallback = null + } + } + + override fun onScreenTurnedOn() { + if (shouldPlayAnimation) { + statusBar.notificationPanelViewController.startFoldToAodAnimation { + // End action + isAnimationPlaying = false + keyguardViewMediatorLazy.get().maybeHandlePendingLock() + } + shouldPlayAnimation = false + } + } + + override fun isAnimationPlaying(): Boolean = + isAnimationPlaying + + override fun isKeyguardHideDelayed(): Boolean = + isAnimationPlaying() + + override fun shouldShowAodIconsWhenShade(): Boolean = + shouldPlayAnimation() + + override fun shouldAnimateAodIcons(): Boolean = + !shouldPlayAnimation() + + override fun shouldAnimateDozingChange(): Boolean = + !shouldPlayAnimation() + + override fun shouldAnimateClockChange(): Boolean = + !isAnimationPlaying() + + /** + * Called when AOD status is changed + */ + override fun onAlwaysOnChanged(alwaysOn: Boolean) { + alwaysOnEnabled = alwaysOn + } + + override fun addCallback(listener: FoldAodAnimationStatus) { + statusListeners += listener + } + + override fun removeCallback(listener: FoldAodAnimationStatus) { + statusListeners.remove(listener) + } + + interface FoldAodAnimationStatus { + fun onFoldToAodAnimationChanged() + } +} diff --git a/packages/SystemUI/src/com/android/systemui/unfold/SysUIUnfoldModule.kt b/packages/SystemUI/src/com/android/systemui/unfold/SysUIUnfoldModule.kt index b53ab210424f7..ccde3162b1775 100644 --- a/packages/SystemUI/src/com/android/systemui/unfold/SysUIUnfoldModule.kt +++ b/packages/SystemUI/src/com/android/systemui/unfold/SysUIUnfoldModule.kt @@ -78,6 +78,8 @@ interface SysUIUnfoldComponent { fun getStatusBarMoveFromCenterAnimationController(): StatusBarMoveFromCenterAnimationController + fun getFoldAodAnimationController(): FoldAodAnimationController + fun getUnfoldTransitionWallpaperController(): UnfoldTransitionWallpaperController fun getUnfoldLightRevealOverlayAnimation(): UnfoldLightRevealOverlayAnimation diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java index e967033b69a27..74e0f40020269 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java @@ -264,7 +264,7 @@ public class KeyguardClockSwitchControllerTest extends SysuiTestCase { reset(mView); observer.onChange(true); mExecutor.runAllReady(); - verify(mView).switchToClock(KeyguardClockSwitch.SMALL); + verify(mView).switchToClock(KeyguardClockSwitch.SMALL, /* animate */ true); } private void verifyAttachment(VerificationMode times) { diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchTest.java index e4336fe07dbb9..8717a0eaf57fd 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchTest.java @@ -253,8 +253,8 @@ public class KeyguardClockSwitchTest extends SysuiTestCase { } @Test - public void switchingToBigClock_makesSmallClockDisappear() { - mKeyguardClockSwitch.switchToClock(LARGE); + public void switchingToBigClockWithAnimation_makesSmallClockDisappear() { + mKeyguardClockSwitch.switchToClock(LARGE, /* animate */ true); mKeyguardClockSwitch.mClockInAnim.end(); mKeyguardClockSwitch.mClockOutAnim.end(); @@ -265,8 +265,17 @@ public class KeyguardClockSwitchTest extends SysuiTestCase { } @Test - public void switchingToSmallClock_makesBigClockDisappear() { - mKeyguardClockSwitch.switchToClock(SMALL); + public void switchingToBigClockNoAnimation_makesSmallClockDisappear() { + mKeyguardClockSwitch.switchToClock(LARGE, /* animate */ false); + + assertThat(mLargeClockFrame.getAlpha()).isEqualTo(1); + assertThat(mLargeClockFrame.getVisibility()).isEqualTo(VISIBLE); + assertThat(mClockFrame.getAlpha()).isEqualTo(0); + } + + @Test + public void switchingToSmallClockWithAnimation_makesBigClockDisappear() { + mKeyguardClockSwitch.switchToClock(SMALL, /* animate */ true); mKeyguardClockSwitch.mClockInAnim.end(); mKeyguardClockSwitch.mClockOutAnim.end(); @@ -278,9 +287,20 @@ public class KeyguardClockSwitchTest extends SysuiTestCase { assertThat(mLargeClockFrame.getAlpha()).isEqualTo(0); } + @Test + public void switchingToSmallClockNoAnimation_makesBigClockDisappear() { + mKeyguardClockSwitch.switchToClock(SMALL, false); + + assertThat(mClockFrame.getAlpha()).isEqualTo(1); + assertThat(mClockFrame.getVisibility()).isEqualTo(VISIBLE); + // only big clock is removed at switch + assertThat(mLargeClockFrame.getParent()).isNull(); + assertThat(mLargeClockFrame.getAlpha()).isEqualTo(0); + } + @Test public void switchingToBigClock_returnsTrueOnlyWhenItWasNotVisibleBefore() { - assertThat(mKeyguardClockSwitch.switchToClock(LARGE)).isTrue(); - assertThat(mKeyguardClockSwitch.switchToClock(LARGE)).isFalse(); + assertThat(mKeyguardClockSwitch.switchToClock(LARGE, /* animate */ true)).isTrue(); + assertThat(mKeyguardClockSwitch.switchToClock(LARGE, /* animate */ true)).isFalse(); } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeUiTest.java b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeUiTest.java index 873f7a45571b2..55af51d3fddf4 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeUiTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeUiTest.java @@ -45,6 +45,8 @@ import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.statusbar.phone.DozeParameters; import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.tuner.TunerService; +import com.android.systemui.unfold.FoldAodAnimationController; +import com.android.systemui.unfold.SysUIUnfoldComponent; import com.android.systemui.util.wakelock.WakeLockFake; import org.junit.After; @@ -54,6 +56,8 @@ import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import java.util.Optional; + @RunWith(AndroidJUnit4.class) @SmallTest public class DozeUiTest extends SysuiTestCase { @@ -79,6 +83,10 @@ public class DozeUiTest extends SysuiTestCase { @Mock private StatusBarStateController mStatusBarStateController; @Mock + private FoldAodAnimationController mFoldAodAnimationController; + @Mock + private SysUIUnfoldComponent mSysUIUnfoldComponent; + @Mock private ConfigurationController mConfigurationController; @Before @@ -90,9 +98,13 @@ public class DozeUiTest extends SysuiTestCase { mWakeLock = new WakeLockFake(); mHandler = mHandlerThread.getThreadHandler(); + when(mSysUIUnfoldComponent.getFoldAodAnimationController()) + .thenReturn(mFoldAodAnimationController); + mDozeUi = new DozeUi(mContext, mAlarmManager, mWakeLock, mHost, mHandler, mDozeParameters, mKeyguardUpdateMonitor, mDozeLog, mTunerService, - mStatusBarStateController, mConfigurationController); + mStatusBarStateController, Optional.of(mSysUIUnfoldComponent), + mConfigurationController); mDozeUi.setDozeMachine(mMachine); } @@ -121,6 +133,7 @@ public class DozeUiTest extends SysuiTestCase { reset(mHost); when(mDozeParameters.getAlwaysOn()).thenReturn(false); when(mDozeParameters.getDisplayNeedsBlanking()).thenReturn(false); + when(mDozeParameters.shouldAnimateDozingChange()).thenReturn(true); mDozeUi.getKeyguardCallback().onKeyguardVisibilityChanged(false); verify(mHost).setAnimateScreenOff(eq(false)); @@ -131,6 +144,7 @@ public class DozeUiTest extends SysuiTestCase { reset(mHost); when(mDozeParameters.getAlwaysOn()).thenReturn(true); when(mDozeParameters.getDisplayNeedsBlanking()).thenReturn(false); + when(mDozeParameters.shouldAnimateDozingChange()).thenReturn(true); // Take over when the keyguard is visible. mDozeUi.getKeyguardCallback().onKeyguardVisibilityChanged(true); @@ -141,6 +155,18 @@ public class DozeUiTest extends SysuiTestCase { verify(mHost).setAnimateScreenOff(eq(false)); } + @Test + public void propagatesAnimateScreenOff_alwaysOn_shouldAnimateDozingChangeIsFalse() { + reset(mHost); + when(mDozeParameters.getAlwaysOn()).thenReturn(true); + when(mDozeParameters.getDisplayNeedsBlanking()).thenReturn(false); + when(mDozeParameters.shouldAnimateDozingChange()).thenReturn(false); + + // Take over when the keyguard is visible. + mDozeUi.getKeyguardCallback().onKeyguardVisibilityChanged(true); + verify(mHost).setAnimateScreenOff(eq(false)); + } + @Test public void neverAnimateScreenOff_whenNotSupported() { // Re-initialize DozeParameters saying that the display requires blanking. @@ -149,7 +175,8 @@ public class DozeUiTest extends SysuiTestCase { when(mDozeParameters.getDisplayNeedsBlanking()).thenReturn(true); mDozeUi = new DozeUi(mContext, mAlarmManager, mWakeLock, mHost, mHandler, mDozeParameters, mKeyguardUpdateMonitor, mDozeLog, mTunerService, - mStatusBarStateController, mConfigurationController); + mStatusBarStateController, Optional.of(mSysUIUnfoldComponent), + mConfigurationController); mDozeUi.setDozeMachine(mMachine); // Never animate if display doesn't support it. diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java index 8d329c56af367..27fcb11b8dfc4 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java @@ -58,6 +58,7 @@ import com.android.systemui.statusbar.phone.ScreenOffAnimationController; import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager; import com.android.systemui.statusbar.policy.KeyguardStateController; import com.android.systemui.statusbar.policy.UserSwitcherController; +import com.android.systemui.unfold.FoldAodAnimationController; import com.android.systemui.unfold.SysUIUnfoldComponent; import com.android.systemui.unfold.UnfoldLightRevealOverlayAnimation; import com.android.systemui.util.DeviceConfigProxy; @@ -69,7 +70,6 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; -import org.mockito.ArgumentMatchers; import org.mockito.Mock; import org.mockito.MockitoAnnotations; @@ -95,62 +95,40 @@ public class KeyguardViewMediatorTest extends SysuiTestCase { private @Mock NavigationModeController mNavigationModeController; private @Mock KeyguardDisplayManager mKeyguardDisplayManager; private @Mock DozeParameters mDozeParameters; - private @Mock Optional mSysUIUnfoldComponent; - private @Mock Optional mUnfoldAnimationOptional; + private @Mock SysUIUnfoldComponent mSysUIUnfoldComponent; private @Mock UnfoldLightRevealOverlayAnimation mUnfoldAnimation; private @Mock SysuiStatusBarStateController mStatusBarStateController; private @Mock KeyguardStateController mKeyguardStateController; private @Mock NotificationShadeDepthController mNotificationShadeDepthController; private @Mock KeyguardUnlockAnimationController mKeyguardUnlockAnimationController; private @Mock ScreenOffAnimationController mScreenOffAnimationController; + private @Mock FoldAodAnimationController mFoldAodAnimationController; private @Mock IKeyguardDrawnCallback mKeyguardDrawnCallback; private @Mock InteractionJankMonitor mInteractionJankMonitor; private DeviceConfigProxy mDeviceConfig = new DeviceConfigProxyFake(); private FakeExecutor mUiBgExecutor = new FakeExecutor(new FakeSystemClock()); + private Optional mSysUiUnfoldComponentOptional; + private FalsingCollectorFake mFalsingCollector; @Before public void setUp() throws Exception { MockitoAnnotations.initMocks(this); mFalsingCollector = new FalsingCollectorFake(); + mSysUiUnfoldComponentOptional = Optional.of(mSysUIUnfoldComponent); when(mLockPatternUtils.getDevicePolicyManager()).thenReturn(mDevicePolicyManager); when(mPowerManager.newWakeLock(anyInt(), any())).thenReturn(mock(WakeLock.class)); - when(mSysUIUnfoldComponent.map( - ArgumentMatchers.> - any())) - .thenReturn(mUnfoldAnimationOptional); - when(mUnfoldAnimationOptional.isPresent()).thenReturn(true); - when(mUnfoldAnimationOptional.get()).thenReturn(mUnfoldAnimation); + when(mSysUIUnfoldComponent.getUnfoldLightRevealOverlayAnimation()) + .thenReturn(mUnfoldAnimation); + when(mSysUIUnfoldComponent.getFoldAodAnimationController()) + .thenReturn(mFoldAodAnimationController); + when(mInteractionJankMonitor.begin(any(), anyInt())).thenReturn(true); when(mInteractionJankMonitor.end(anyInt())).thenReturn(true); - mViewMediator = new KeyguardViewMediator( - mContext, - mFalsingCollector, - mLockPatternUtils, - mBroadcastDispatcher, - () -> mStatusBarKeyguardViewManager, - mDismissCallbackRegistry, - mUpdateMonitor, - mDumpManager, - mUiBgExecutor, - mPowerManager, - mTrustManager, - mUserSwitcherController, - mDeviceConfig, - mNavigationModeController, - mKeyguardDisplayManager, - mDozeParameters, - mSysUIUnfoldComponent, - mStatusBarStateController, - mKeyguardStateController, - () -> mKeyguardUnlockAnimationController, - mScreenOffAnimationController, - () -> mNotificationShadeDepthController, - mInteractionJankMonitor); - mViewMediator.start(); + createAndStartViewMediator(); } @Test @@ -179,6 +157,7 @@ public class KeyguardViewMediatorTest extends SysuiTestCase { mViewMediator.onScreenTurningOn(mKeyguardDrawnCallback); TestableLooper.get(this).processAllMessages(); onUnfoldOverlayReady(); + onFoldAodReady(); // Should be called when both unfold overlay and keyguard drawn ready verify(mKeyguardDrawnCallback).onDrawn(); @@ -188,7 +167,8 @@ public class KeyguardViewMediatorTest extends SysuiTestCase { @TestableLooper.RunWithLooper(setAsMainLooper = true) public void testUnfoldTransitionDisabledDrawnTasksReady_onScreenTurningOn_callsDrawnCallback() throws RemoteException { - when(mUnfoldAnimationOptional.isPresent()).thenReturn(false); + mSysUiUnfoldComponentOptional = Optional.empty(); + createAndStartViewMediator(); mViewMediator.onScreenTurningOn(mKeyguardDrawnCallback); TestableLooper.get(this).processAllMessages(); @@ -200,6 +180,7 @@ public class KeyguardViewMediatorTest extends SysuiTestCase { @Test public void testIsAnimatingScreenOff() { when(mDozeParameters.shouldControlUnlockedScreenOff()).thenReturn(true); + when(mDozeParameters.shouldAnimateDozingChange()).thenReturn(true); mViewMediator.onFinishedGoingToSleep(OFF_BECAUSE_OF_USER, false); mViewMediator.setDozing(true); @@ -244,4 +225,39 @@ public class KeyguardViewMediatorTest extends SysuiTestCase { overlayReadyCaptor.getValue().run(); TestableLooper.get(this).processAllMessages(); } + + private void onFoldAodReady() { + ArgumentCaptor ready = ArgumentCaptor.forClass(Runnable.class); + verify(mFoldAodAnimationController).onScreenTurningOn(ready.capture()); + ready.getValue().run(); + TestableLooper.get(this).processAllMessages(); + } + + private void createAndStartViewMediator() { + mViewMediator = new KeyguardViewMediator( + mContext, + mFalsingCollector, + mLockPatternUtils, + mBroadcastDispatcher, + () -> mStatusBarKeyguardViewManager, + mDismissCallbackRegistry, + mUpdateMonitor, + mDumpManager, + mUiBgExecutor, + mPowerManager, + mTrustManager, + mUserSwitcherController, + mDeviceConfig, + mNavigationModeController, + mKeyguardDisplayManager, + mDozeParameters, + mSysUiUnfoldComponentOptional, + mStatusBarStateController, + mKeyguardStateController, + () -> mKeyguardUnlockAnimationController, + mScreenOffAnimationController, + () -> mNotificationShadeDepthController, + mInteractionJankMonitor); + mViewMediator.start(); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationPanelViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationPanelViewControllerTest.java index 22e7746614ef1..5a3d43a21f8be 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationPanelViewControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationPanelViewControllerTest.java @@ -424,6 +424,7 @@ public class NotificationPanelViewControllerTest extends SysuiTestCase { .thenReturn(mKeyguardUserSwitcherComponent); when(mKeyguardUserSwitcherComponent.getKeyguardUserSwitcherController()) .thenReturn(mKeyguardUserSwitcherController); + when(mScreenOffAnimationController.shouldAnimateClockChange()).thenReturn(true); doAnswer((Answer) invocation -> { mTouchHandler = invocation.getArgument(0); @@ -879,11 +880,11 @@ public class NotificationPanelViewControllerTest extends SysuiTestCase { when(mNotificationStackScrollLayoutController.getVisibleNotificationCount()).thenReturn(0); triggerPositionClockAndNotifications(); - verify(mKeyguardStatusViewController).displayClock(LARGE); + verify(mKeyguardStatusViewController).displayClock(LARGE, /* animate */ true); when(mNotificationStackScrollLayoutController.getVisibleNotificationCount()).thenReturn(1); mNotificationPanelViewController.closeQs(); - verify(mKeyguardStatusViewController).displayClock(SMALL); + verify(mKeyguardStatusViewController).displayClock(SMALL, /* animate */ true); } @Test @@ -893,12 +894,14 @@ public class NotificationPanelViewControllerTest extends SysuiTestCase { when(mNotificationStackScrollLayoutController.getVisibleNotificationCount()).thenReturn(0); triggerPositionClockAndNotifications(); - verify(mKeyguardStatusViewController).displayClock(LARGE); + verify(mKeyguardStatusViewController).displayClock(LARGE, /* animate */ true); when(mNotificationStackScrollLayoutController.getVisibleNotificationCount()).thenReturn(1); triggerPositionClockAndNotifications(); - verify(mKeyguardStatusViewController, times(2)).displayClock(LARGE); - verify(mKeyguardStatusViewController, never()).displayClock(SMALL); + verify(mKeyguardStatusViewController, times(2)) + .displayClock(LARGE, /* animate */ true); + verify(mKeyguardStatusViewController, never()) + .displayClock(SMALL, /* animate */ true); } @Test @@ -910,7 +913,20 @@ public class NotificationPanelViewControllerTest extends SysuiTestCase { mNotificationPanelViewController.setDozing(true, false, null); - verify(mKeyguardStatusViewController).displayClock(LARGE); + verify(mKeyguardStatusViewController).displayClock(LARGE, /* animate */ true); + } + + @Test + public void testSwitchesToBigClockInSplitShadeOnAodAnimateDisabled() { + when(mScreenOffAnimationController.shouldAnimateClockChange()).thenReturn(false); + mStatusBarStateController.setState(KEYGUARD); + enableSplitShade(/* enabled= */ true); + when(mMediaDataManager.hasActiveMedia()).thenReturn(true); + when(mNotificationStackScrollLayoutController.getVisibleNotificationCount()).thenReturn(2); + + mNotificationPanelViewController.setDozing(true, false, null); + + verify(mKeyguardStatusViewController).displayClock(LARGE, /* animate */ false); } @Test @@ -922,13 +938,13 @@ public class NotificationPanelViewControllerTest extends SysuiTestCase { // one notification + media player visible when(mNotificationStackScrollLayoutController.getVisibleNotificationCount()).thenReturn(1); triggerPositionClockAndNotifications(); - verify(mKeyguardStatusViewController).displayClock(SMALL); + verify(mKeyguardStatusViewController).displayClock(SMALL, /* animate */ true); // only media player visible when(mNotificationStackScrollLayoutController.getVisibleNotificationCount()).thenReturn(0); triggerPositionClockAndNotifications(); - verify(mKeyguardStatusViewController, times(2)).displayClock(SMALL); - verify(mKeyguardStatusViewController, never()).displayClock(LARGE); + verify(mKeyguardStatusViewController, times(2)).displayClock(SMALL, true); + verify(mKeyguardStatusViewController, never()).displayClock(LARGE, /* animate */ true); } @Test