Fold to AOD animation: part 2, add fold to aod controller

Adds animation that animates AOD content
from left to right when folding a foldable
device.

There are still a couple of issue that should be
addressed in the follow up CLs:
* Navigation bar handle sometimes displayed on AOD
* Showing keyguard when folding from unlocked state
sometimes takes too much time so the screen blocker
times out which leads to flickering

Bug: 202844967
Test: fold from lockscreen, fold from unlocked device,
 aod enabled/disabled, gesture nav enabled/disabled
Test: screen off animation when unlocked
Test: screen off animation when locked
Test: screen off animation when timed out
Test: fingerprint unlock from aod
Change-Id: Ie44f82119ac770ce34faecc60402044d4109dddf
This commit is contained in:
Nick Chameyev
2021-11-19 17:45:57 +00:00
parent 07ae9943a2
commit 418683f500
21 changed files with 980 additions and 418 deletions

View File

@@ -168,6 +168,12 @@ public class AnimatableClockController extends ViewController<AnimatableClockVie
if (!mIsDozing) mView.animateAppearOnLockscreen();
}
/** Animate the clock appearance when a foldable device goes from fully-open/half-open state to
* fully folded state and it goes to sleep (always on display screen) */
public void animateFoldAppear() {
mView.animateFoldAppear();
}
/**
* Updates the time for the view.
*/

View File

@@ -1,315 +0,0 @@
/*
* Copyright (C) 2020 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.annotation.FloatRange;
import android.annotation.IntRange;
import android.content.Context;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.text.format.DateFormat;
import android.util.AttributeSet;
import android.widget.TextView;
import com.android.systemui.R;
import java.util.Calendar;
import java.util.Locale;
import java.util.TimeZone;
import kotlin.Unit;
/**
* 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.
*/
public class AnimatableClockView extends TextView {
private static final CharSequence DOUBLE_LINE_FORMAT_12_HOUR = "hh\nmm";
private static final CharSequence DOUBLE_LINE_FORMAT_24_HOUR = "HH\nmm";
private static final long DOZE_ANIM_DURATION = 300;
private static final long APPEAR_ANIM_DURATION = 350;
private static final long CHARGE_ANIM_DURATION_PHASE_0 = 500;
private static final long CHARGE_ANIM_DURATION_PHASE_1 = 1000;
private final Calendar mTime = Calendar.getInstance();
private final int mDozingWeight;
private final int mLockScreenWeight;
private CharSequence mFormat;
private CharSequence mDescFormat;
private int mDozingColor;
private int mLockScreenColor;
private float mLineSpacingScale = 1f;
private int mChargeAnimationDelay = 0;
private TextAnimator mTextAnimator = null;
private Runnable mOnTextAnimatorInitialized;
private boolean mIsSingleLine;
public AnimatableClockView(Context context) {
this(context, null, 0, 0);
}
public AnimatableClockView(Context context, AttributeSet attrs) {
this(context, attrs, 0, 0);
}
public AnimatableClockView(Context context, AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}
public AnimatableClockView(Context context, AttributeSet attrs, int defStyleAttr,
int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
TypedArray ta = context.obtainStyledAttributes(
attrs, R.styleable.AnimatableClockView, defStyleAttr, defStyleRes);
try {
mDozingWeight = ta.getInt(R.styleable.AnimatableClockView_dozeWeight, 100);
mLockScreenWeight = ta.getInt(R.styleable.AnimatableClockView_lockScreenWeight, 300);
mChargeAnimationDelay = ta.getInt(
R.styleable.AnimatableClockView_chargeAnimationDelay, 200);
} finally {
ta.recycle();
}
ta = context.obtainStyledAttributes(
attrs, android.R.styleable.TextView, defStyleAttr, defStyleRes);
try {
mIsSingleLine = ta.getBoolean(android.R.styleable.TextView_singleLine, false);
} finally {
ta.recycle();
}
refreshFormat();
}
@Override
public void onAttachedToWindow() {
super.onAttachedToWindow();
refreshFormat();
}
@Override
public void onDetachedFromWindow() {
super.onDetachedFromWindow();
}
int getDozingWeight() {
if (useBoldedVersion()) {
return mDozingWeight + 100;
}
return mDozingWeight;
}
int getLockScreenWeight() {
if (useBoldedVersion()) {
return mLockScreenWeight + 100;
}
return mLockScreenWeight;
}
/**
* Whether to use a bolded version based on the user specified fontWeightAdjustment.
*/
boolean useBoldedVersion() {
// "Bold text" fontWeightAdjustment is 300.
return getResources().getConfiguration().fontWeightAdjustment > 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();
}
}

View File

@@ -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

View File

@@ -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;

View File

@@ -292,17 +292,24 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS
* Set which clock should be displayed on the keyguard. The other one will be automatically
* hidden.
*/
public void displayClock(@KeyguardClockSwitch.ClockSize int clockSize) {
public void displayClock(@KeyguardClockSwitch.ClockSize int clockSize, boolean animate) {
if (!mCanShowDoubleLineClock && clockSize == KeyguardClockSwitch.LARGE) {
return;
}
boolean appeared = mView.switchToClock(clockSize);
if (appeared && clockSize == LARGE) {
boolean appeared = mView.switchToClock(clockSize, animate);
if (animate && appeared && clockSize == LARGE) {
mLargeClockViewController.animateAppear();
}
}
public void animateFoldToAod() {
if (mClockViewController != null) {
mClockViewController.animateFoldAppear();
mLargeClockViewController.animateFoldAppear();
}
}
/**
* If we're presenting a custom clock of just the default one.
*/
@@ -445,7 +452,7 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS
Settings.Secure.LOCKSCREEN_USE_DOUBLE_LINE_CLOCK, 1) != 0;
if (!mCanShowDoubleLineClock) {
mUiExecutor.execute(() -> displayClock(KeyguardClockSwitch.SMALL));
mUiExecutor.execute(() -> displayClock(KeyguardClockSwitch.SMALL, /* animate */ true));
}
}
}

View File

@@ -123,8 +123,17 @@ public class KeyguardStatusViewController extends ViewController<KeyguardStatusV
* Set which clock should be displayed on the keyguard. The other one will be automatically
* hidden.
*/
public void displayClock(@ClockSize int clockSize) {
mKeyguardClockSwitchController.displayClock(clockSize);
public void displayClock(@ClockSize int clockSize, boolean animate) {
mKeyguardClockSwitchController.displayClock(clockSize, animate);
}
/**
* Performs fold to aod animation of the clocks (changes font weight from bold to thin).
* This animation is played when AOD is enabled and foldable device is fully folded, it is
* displayed on the outer screen
*/
public void animateFoldToAod() {
mKeyguardClockSwitchController.animateFoldToAod();
}
/**

View File

@@ -37,10 +37,14 @@ 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.FoldAodAnimationController.FoldAodAnimationStatus;
import com.android.systemui.unfold.SysUIUnfoldComponent;
import com.android.systemui.util.AlarmTimeout;
import com.android.systemui.util.wakelock.WakeLock;
import java.util.Calendar;
import java.util.Optional;
import javax.inject.Inject;
@@ -49,7 +53,8 @@ import javax.inject.Inject;
*/
@DozeScope
public class DozeUi implements DozeMachine.Part, TunerService.Tunable,
ConfigurationController.ConfigurationListener, StatusBarStateController.StateListener {
ConfigurationController.ConfigurationListener, FoldAodAnimationStatus,
StatusBarStateController.StateListener {
// if enabled, calls dozeTimeTick() whenever the time changes:
private static final boolean BURN_IN_TESTING_ENABLED = false;
private static final long TIME_TICK_DEADLINE_MILLIS = 90 * 1000; // 1.5min
@@ -57,6 +62,7 @@ public class DozeUi implements DozeMachine.Part, TunerService.Tunable,
private final DozeHost mHost;
private final Handler mHandler;
private final WakeLock mWakeLock;
private final FoldAodAnimationController mFoldAodAnimationController;
private DozeMachine mMachine;
private final AlarmTimeout mTimeTicker;
private final boolean mCanAnimateTransition;
@@ -100,6 +106,7 @@ public class DozeUi implements DozeMachine.Part, TunerService.Tunable,
DozeParameters params, KeyguardUpdateMonitor keyguardUpdateMonitor,
DozeLog dozeLog, TunerService tunerService,
StatusBarStateController statusBarStateController,
Optional<SysUIUnfoldComponent> 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();
}
}

View File

@@ -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<UnfoldLightRevealOverlayAnimation> mUnfoldLightRevealAnimation;
private final AtomicInteger mPendingDrawnTasks = new AtomicInteger();
private final Optional<FoldAodAnimationController> mFoldAodAnimationController;
private final PendingDrawnTasksContainer mPendingDrawnTasks = new PendingDrawnTasksContainer();
private final KeyguardStateController mKeyguardStateController;
private final Lazy<KeyguardUnlockAnimationController> 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);
}
/**

View File

@@ -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<Runnable> = 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

View File

@@ -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;

View File

@@ -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

View File

@@ -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(

View File

@@ -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<SysUIUnfoldComponent>,
unlockedScreenOffAnimation: UnlockedScreenOffAnimationController,
private val wakefulnessLifecycle: WakefulnessLifecycle,
) : WakefulnessLifecycle.Observer {
// TODO(b/202844967) add fold to aod animation here
private val animations: List<ScreenOffAnimation> = listOf(unlockedScreenOffAnimation)
private val foldToAodAnimation: FoldAodAnimationController? = sysUiUnfoldComponent
.orElse(null)?.getFoldAodAnimationController()
private val animations: List<ScreenOffAnimation> =
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
}

View File

@@ -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();

View File

@@ -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<KeyguardViewMediator>,
private val wakefulnessLifecycle: WakefulnessLifecycle,
private val globalSettings: GlobalSettings
) : ScreenLifecycle.Observer,
CallbackController<FoldAodAnimationStatus>,
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<FoldAodAnimationStatus>()
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()
}
}

View File

@@ -78,6 +78,8 @@ interface SysUIUnfoldComponent {
fun getStatusBarMoveFromCenterAnimationController(): StatusBarMoveFromCenterAnimationController
fun getFoldAodAnimationController(): FoldAodAnimationController
fun getUnfoldTransitionWallpaperController(): UnfoldTransitionWallpaperController
fun getUnfoldLightRevealOverlayAnimation(): UnfoldLightRevealOverlayAnimation

View File

@@ -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) {

View File

@@ -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();
}
}

View File

@@ -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.

View File

@@ -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<SysUIUnfoldComponent> mSysUIUnfoldComponent;
private @Mock Optional<UnfoldLightRevealOverlayAnimation> 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<SysUIUnfoldComponent> 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.<Function<SysUIUnfoldComponent, UnfoldLightRevealOverlayAnimation>>
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<Runnable> 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();
}
}

View File

@@ -424,6 +424,7 @@ public class NotificationPanelViewControllerTest extends SysuiTestCase {
.thenReturn(mKeyguardUserSwitcherComponent);
when(mKeyguardUserSwitcherComponent.getKeyguardUserSwitcherController())
.thenReturn(mKeyguardUserSwitcherController);
when(mScreenOffAnimationController.shouldAnimateClockChange()).thenReturn(true);
doAnswer((Answer<Void>) 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