summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Selim Cinek <cinek@google.com> 2021-05-19 14:59:29 +0200
committer Selim Cinek <cinek@google.com> 2021-05-26 16:23:26 +0200
commit67563f5ce01fb0ad6a409caed0180e14d6d4620b (patch)
treefa9bb63ed9c6036466ac01d7dce18f6be6b52b1f
parent48f518618e3c75ac793703da678e3c4ba9b05b64 (diff)
Transitioning media on lockscreen with a fade
UX wise a fade was much preferred, so we built in the capability for media transitions to fade from location to location. We're now also fading the media when transitioning between QS and Lockscreen. This also polishes the animation further. Bug: 184946919 Test: atest SystemUITests Change-Id: Id9fc58469bebe69ad7a0189e4c4acd36523cdeed
-rw-r--r--packages/SystemUI/res/layout/media_carousel.xml1
-rw-r--r--packages/SystemUI/res/values/dimens.xml15
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt327
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/QSFragment.java16
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java4
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java31
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithm.java4
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java49
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/media/MediaHierarchyManagerTest.kt77
9 files changed, 426 insertions, 98 deletions
diff --git a/packages/SystemUI/res/layout/media_carousel.xml b/packages/SystemUI/res/layout/media_carousel.xml
index 87acfd088939..52132e881c43 100644
--- a/packages/SystemUI/res/layout/media_carousel.xml
+++ b/packages/SystemUI/res/layout/media_carousel.xml
@@ -22,6 +22,7 @@
android:layout_height="wrap_content"
android:clipChildren="false"
android:clipToPadding="false"
+ android:forceHasOverlappingRendering="false"
android:theme="@style/MediaPlayer">
<com.android.systemui.media.MediaScrollView
android:id="@+id/media_carousel_scroller"
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index 915b62791f58..dc278fb0c58e 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -1409,10 +1409,6 @@
<dimen name="media_output_dialog_icon_corner_radius">16dp</dimen>
<dimen name="media_output_dialog_title_anim_y_delta">12.5dp</dimen>
- <!-- Delay after which the media will start transitioning to the full shade on
- the lockscreen -->
- <dimen name="lockscreen_shade_media_transition_start_delay">40dp</dimen>
-
<!-- Distance that the full shade transition takes in order for qs to fully transition to the
shade -->
<dimen name="lockscreen_shade_qs_transition_distance">200dp</dimen>
@@ -1421,13 +1417,16 @@
the shade (in alpha) -->
<dimen name="lockscreen_shade_scrim_transition_distance">80dp</dimen>
- <!-- Extra inset for the notifications when accounting for media during the lockscreen to
- shade transition to compensate for the disappearing media -->
- <dimen name="lockscreen_shade_transition_extra_media_inset">-48dp</dimen>
+ <!-- Distance that the full shade transition takes in order for media to fully transition to
+ the shade -->
+ <dimen name="lockscreen_shade_media_transition_distance">140dp</dimen>
<!-- Maximum overshoot for the topPadding of notifications when transitioning to the full
shade -->
- <dimen name="lockscreen_shade_max_top_overshoot">32dp</dimen>
+ <dimen name="lockscreen_shade_notification_movement">24dp</dimen>
+
+ <!-- Maximum overshoot for the pulse expansion -->
+ <dimen name="pulse_expansion_max_top_overshoot">16dp</dimen>
<dimen name="people_space_widget_radius">28dp</dimen>
<dimen name="people_space_image_radius">20dp</dimen>
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt
index 73dfe5e68d9a..075bc700cfa0 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt
@@ -26,21 +26,23 @@ import android.util.MathUtils
import android.view.View
import android.view.ViewGroup
import android.view.ViewGroupOverlay
+import androidx.annotation.VisibleForTesting
import com.android.systemui.R
import com.android.systemui.animation.Interpolators
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.keyguard.WakefulnessLifecycle
import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.statusbar.CrossFadeHelper
import com.android.systemui.statusbar.NotificationLockscreenUserManager
import com.android.systemui.statusbar.StatusBarState
import com.android.systemui.statusbar.SysuiStatusBarStateController
import com.android.systemui.statusbar.notification.stack.StackStateAnimator
import com.android.systemui.statusbar.phone.KeyguardBypassController
+import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager
import com.android.systemui.statusbar.policy.ConfigurationController
import com.android.systemui.statusbar.policy.KeyguardStateController
import com.android.systemui.util.animation.UniqueObjectHostView
import javax.inject.Inject
-import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager
/**
* Similarly to isShown but also excludes views that have 0 alpha
@@ -80,6 +82,7 @@ class MediaHierarchyManager @Inject constructor(
wakefulnessLifecycle: WakefulnessLifecycle,
private val statusBarKeyguardViewManager: StatusBarKeyguardViewManager
) {
+
/**
* The root overlay of the hierarchy. This is where the media notification is attached to
* whenever the view is transitioning from one host to another. It also make sure that the
@@ -90,6 +93,30 @@ class MediaHierarchyManager @Inject constructor(
private var rootView: View? = null
private var currentBounds = Rect()
private var animationStartBounds: Rect = Rect()
+
+ /**
+ * The cross fade progress at the start of the animation. 0.5f means it's just switching between
+ * the start and the end location and the content is fully faded, while 0.75f means that we're
+ * halfway faded in again in the target state.
+ */
+ private var animationStartCrossFadeProgress = 0.0f
+
+ /**
+ * The starting alpha of the animation
+ */
+ private var animationStartAlpha = 0.0f
+
+ /**
+ * The starting location of the cross fade if an animation is running right now.
+ */
+ @MediaLocation
+ private var crossFadeAnimationStartLocation = -1
+
+ /**
+ * The end location of the cross fade if an animation is running right now.
+ */
+ @MediaLocation
+ private var crossFadeAnimationEndLocation = -1
private var targetBounds: Rect = Rect()
private val mediaFrame
get() = mediaCarouselController.mediaFrame
@@ -98,9 +125,22 @@ class MediaHierarchyManager @Inject constructor(
interpolator = Interpolators.FAST_OUT_SLOW_IN
addUpdateListener {
updateTargetState()
- interpolateBounds(animationStartBounds, targetBounds, animatedFraction,
+ val currentAlpha: Float
+ var boundsProgress = animatedFraction
+ if (isCrossFadeAnimatorRunning) {
+ animationCrossFadeProgress = MathUtils.lerp(animationStartCrossFadeProgress, 1.0f,
+ animatedFraction)
+ // When crossfading, let's keep the bounds at the right location during fading
+ boundsProgress = if (animationCrossFadeProgress < 0.5f) 0.0f else 1.0f
+ currentAlpha = calculateAlphaFromCrossFade(animationCrossFadeProgress,
+ instantlyShowAtEnd = false)
+ } else {
+ // If we're not crossfading, let's interpolate from the start alpha to 1.0f
+ currentAlpha = MathUtils.lerp(animationStartAlpha, 1.0f, animatedFraction)
+ }
+ interpolateBounds(animationStartBounds, targetBounds, boundsProgress,
result = currentBounds)
- applyState(currentBounds)
+ applyState(currentBounds, currentAlpha)
}
addListener(object : AnimatorListenerAdapter() {
private var cancelled: Boolean = false
@@ -112,6 +152,7 @@ class MediaHierarchyManager @Inject constructor(
}
override fun onAnimationEnd(animation: Animator?) {
+ isCrossFadeAnimatorRunning = false
if (!cancelled) {
applyTargetStateIfNotAnimating()
}
@@ -192,11 +233,6 @@ class MediaHierarchyManager @Inject constructor(
private var distanceForFullShadeTransition = 0
/**
- * Delay after which the media will start transitioning to the full shade on the lockscreen.
- */
- private var fullShadeTransitionDelay = 0
-
- /**
* The amount of progress we are currently in if we're transitioning to the full shade.
* 0.0f means we're not transitioning yet, while 1 means we're all the way in the full
* shade.
@@ -207,18 +243,33 @@ class MediaHierarchyManager @Inject constructor(
return
}
field = value
- if (bypassController.bypassEnabled) {
+ if (bypassController.bypassEnabled || statusbarState != StatusBarState.KEYGUARD) {
+ // No need to do all the calculations / updates below if we're not on the lockscreen
+ // or if we're bypassing.
return
}
- updateDesiredLocation()
+ updateDesiredLocation(forceNoAnimation = isCurrentlyFading())
if (value >= 0) {
updateTargetState()
+ // Setting the alpha directly, as the below call will use it to update the alpha
+ carouselAlpha = calculateAlphaFromCrossFade(field, instantlyShowAtEnd = true)
applyTargetStateIfNotAnimating()
}
}
+ /**
+ * Is there currently a cross-fade animation running driven by an animator?
+ */
+ private var isCrossFadeAnimatorRunning = false
+
+ /**
+ * Are we currently transitionioning from the lockscreen to the full shade
+ * [StatusBarState.SHADE_LOCKED] or [StatusBarState.SHADE]. Once the user has dragged down and
+ * the transition starts, this will no longer return true.
+ */
private val isTransitioningToFullShade: Boolean
- get() = fullShadeTransitionProgress != 0f && !bypassController.bypassEnabled
+ get() = fullShadeTransitionProgress != 0f && !bypassController.bypassEnabled &&
+ statusbarState == StatusBarState.KEYGUARD
/**
* Set the amount of pixels we have currently dragged down if we're transitioning to the full
@@ -227,14 +278,8 @@ class MediaHierarchyManager @Inject constructor(
fun setTransitionToFullShadeAmount(value: Float) {
// If we're transitioning starting on the shade_locked, we don't want any delay and rather
// have it aligned with the rest of the animation
- val delay = if (statusbarState == StatusBarState.KEYGUARD) {
- fullShadeTransitionDelay
- } else {
- 0
- }
- val progress = MathUtils.saturate((value - delay) /
- (distanceForFullShadeTransition - delay))
- fullShadeTransitionProgress = Interpolators.FAST_OUT_SLOW_IN.getInterpolation(progress)
+ val progress = MathUtils.saturate(value / distanceForFullShadeTransition)
+ fullShadeTransitionProgress = progress
}
/**
@@ -296,6 +341,49 @@ class MediaHierarchyManager @Inject constructor(
}
}
+ /**
+ * The current cross fade progress. 0.5f means it's just switching
+ * between the start and the end location and the content is fully faded, while 0.75f means
+ * that we're halfway faded in again in the target state.
+ * This is only valid while [isCrossFadeAnimatorRunning] is true.
+ */
+ private var animationCrossFadeProgress = 1.0f
+
+ /**
+ * The current carousel Alpha.
+ */
+ private var carouselAlpha: Float = 1.0f
+ set(value) {
+ if (field == value) {
+ return
+ }
+ field = value
+ CrossFadeHelper.fadeIn(mediaFrame, value)
+ }
+
+ /**
+ * Calculate the alpha of the view when given a cross-fade progress.
+ *
+ * @param crossFadeProgress The current cross fade progress. 0.5f means it's just switching
+ * between the start and the end location and the content is fully faded, while 0.75f means
+ * that we're halfway faded in again in the target state.
+ *
+ * @param instantlyShowAtEnd should the view be instantly shown at the end. This is needed
+ * to avoid fadinging in when the target was hidden anyway.
+ */
+ private fun calculateAlphaFromCrossFade(
+ crossFadeProgress: Float,
+ instantlyShowAtEnd: Boolean
+ ): Float {
+ if (crossFadeProgress <= 0.5f) {
+ return 1.0f - crossFadeProgress / 0.5f
+ } else if (instantlyShowAtEnd) {
+ return 1.0f
+ } else {
+ return (crossFadeProgress - 0.5f) / 0.5f
+ }
+ }
+
init {
updateConfiguration()
configurationController.addCallback(object : ConfigurationController.ConfigurationListener {
@@ -375,9 +463,7 @@ class MediaHierarchyManager @Inject constructor(
private fun updateConfiguration() {
distanceForFullShadeTransition = context.resources.getDimensionPixelSize(
- R.dimen.lockscreen_shade_qs_transition_distance)
- fullShadeTransitionDelay = context.resources.getDimensionPixelSize(
- R.dimen.lockscreen_shade_media_transition_start_delay)
+ R.dimen.lockscreen_shade_media_transition_distance)
}
/**
@@ -449,8 +535,13 @@ class MediaHierarchyManager @Inject constructor(
shouldAnimateTransition(desiredLocation, previousLocation)
val (animDuration, delay) = getAnimationParams(previousLocation, desiredLocation)
val host = getHost(desiredLocation)
- mediaCarouselController.onDesiredLocationChanged(desiredLocation, host, animate,
- animDuration, delay)
+ val willFade = calculateTransformationType() == TRANSFORMATION_TYPE_FADE
+ if (!willFade || isCurrentlyInGuidedTransformation() || !animate) {
+ // if we're fading, we want the desired location / measurement only to change
+ // once fully faded. This is happening in the host attachment
+ mediaCarouselController.onDesiredLocationChanged(desiredLocation, host,
+ animate, animDuration, delay)
+ }
performTransitionToNewLocation(isNewView, animate)
}
}
@@ -470,6 +561,8 @@ class MediaHierarchyManager @Inject constructor(
if (isCurrentlyInGuidedTransformation()) {
applyTargetStateIfNotAnimating()
} else if (animate) {
+ val wasCrossFading = isCrossFadeAnimatorRunning
+ val previewsCrossFadeProgress = animationCrossFadeProgress
animator.cancel()
if (currentAttachmentLocation != previousLocation ||
!previousHost.hostView.isAttachedToWindow) {
@@ -482,6 +575,42 @@ class MediaHierarchyManager @Inject constructor(
// be outdated
animationStartBounds.set(previousHost.currentBounds)
}
+ val transformationType = calculateTransformationType()
+ var needsCrossFade = transformationType == TRANSFORMATION_TYPE_FADE
+ var crossFadeStartProgress = 0.0f
+ // The alpha is only relevant when not cross fading
+ var newCrossFadeStartLocation = previousLocation
+ if (wasCrossFading) {
+ if (currentAttachmentLocation == crossFadeAnimationEndLocation) {
+ if (needsCrossFade) {
+ // We were previously crossFading and we've already reached
+ // the end view, Let's start crossfading from the same position there
+ crossFadeStartProgress = 1.0f - previewsCrossFadeProgress
+ }
+ // Otherwise let's fade in from the current alpha, but not cross fade
+ } else {
+ // We haven't reached the previous location yet, let's still cross fade from
+ // where we were.
+ newCrossFadeStartLocation = crossFadeAnimationStartLocation
+ if (newCrossFadeStartLocation == desiredLocation) {
+ // we're crossFading back to where we were, let's start at the end position
+ crossFadeStartProgress = 1.0f - previewsCrossFadeProgress
+ } else {
+ // Let's start from where we are right now
+ crossFadeStartProgress = previewsCrossFadeProgress
+ // We need to force cross fading as we haven't reached the end location yet
+ needsCrossFade = true
+ }
+ }
+ } else if (needsCrossFade) {
+ // let's not flicker and start with the same alpha
+ crossFadeStartProgress = (1.0f - carouselAlpha) / 2.0f
+ }
+ isCrossFadeAnimatorRunning = needsCrossFade
+ crossFadeAnimationStartLocation = newCrossFadeStartLocation
+ crossFadeAnimationEndLocation = desiredLocation
+ animationStartAlpha = carouselAlpha
+ animationStartCrossFadeProgress = crossFadeStartProgress
adjustAnimatorForTransition(desiredLocation, previousLocation)
if (!animationPending) {
rootView?.let {
@@ -518,6 +647,17 @@ class MediaHierarchyManager @Inject constructor(
// non-trivial reattaching logic happening that will make the view not-shown earlier
return true
}
+
+ if (statusbarState == StatusBarState.KEYGUARD) {
+ if (currentLocation == LOCATION_LOCKSCREEN &&
+ previousLocation == LOCATION_QS ||
+ (currentLocation == LOCATION_QS &&
+ previousLocation == LOCATION_LOCKSCREEN)) {
+ // We're always fading from lockscreen to keyguard in situations where the player
+ // is already fully hidden
+ return false
+ }
+ }
return mediaFrame.isShownNotFaded || animator.isRunning || animationPending
}
@@ -538,7 +678,7 @@ class MediaHierarchyManager @Inject constructor(
keyguardStateController.isKeyguardFadingAway) {
delay = keyguardStateController.keyguardFadingAwayDelay
}
- animDuration = StackStateAnimator.ANIMATION_DURATION_GO_TO_FULL_SHADE.toLong()
+ animDuration = (StackStateAnimator.ANIMATION_DURATION_GO_TO_FULL_SHADE / 2f).toLong()
} else if (previousLocation == LOCATION_QQS && desiredLocation == LOCATION_LOCKSCREEN) {
animDuration = StackStateAnimator.ANIMATION_DURATION_APPEAR_DISAPPEAR.toLong()
}
@@ -550,7 +690,7 @@ class MediaHierarchyManager @Inject constructor(
// Let's immediately apply the target state (which is interpolated) if there is
// no animation running. Otherwise the animation update will already update
// the location
- applyState(targetBounds)
+ applyState(targetBounds, carouselAlpha)
}
}
@@ -558,7 +698,7 @@ class MediaHierarchyManager @Inject constructor(
* Updates the bounds that the view wants to be in at the end of the animation.
*/
private fun updateTargetState() {
- if (isCurrentlyInGuidedTransformation()) {
+ if (isCurrentlyInGuidedTransformation() && !isCurrentlyFading()) {
val progress = getTransformationProgress()
var endHost = getHost(desiredLocation)!!
var starthost = getHost(previousLocation)!!
@@ -606,12 +746,33 @@ class MediaHierarchyManager @Inject constructor(
}
/**
+ * Calculate the transformation type for the current animation
+ */
+ @VisibleForTesting
+ @TransformationType
+ fun calculateTransformationType(): Int {
+ if (isTransitioningToFullShade) {
+ return TRANSFORMATION_TYPE_FADE
+ }
+ if (previousLocation == LOCATION_LOCKSCREEN && desiredLocation == LOCATION_QS ||
+ previousLocation == LOCATION_QS && desiredLocation == LOCATION_LOCKSCREEN) {
+ // animating between ls and qs should fade, as QS is clipped.
+ return TRANSFORMATION_TYPE_FADE
+ }
+ if (previousLocation == LOCATION_LOCKSCREEN && desiredLocation == LOCATION_QQS) {
+ // animating between ls and qqs should fade when dragging down via e.g. expand button
+ return TRANSFORMATION_TYPE_FADE
+ }
+ return TRANSFORMATION_TYPE_TRANSITION
+ }
+
+ /**
* @return the current transformation progress if we're in a guided transformation and -1
* otherwise
*/
private fun getTransformationProgress(): Float {
val progress = getQSTransformationProgress()
- if (progress >= 0) {
+ if (statusbarState != StatusBarState.KEYGUARD && progress >= 0) {
return progress
}
if (isTransitioningToFullShade) {
@@ -643,19 +804,20 @@ class MediaHierarchyManager @Inject constructor(
private fun cancelAnimationAndApplyDesiredState() {
animator.cancel()
getHost(desiredLocation)?.let {
- applyState(it.currentBounds, immediately = true)
+ applyState(it.currentBounds, alpha = 1.0f, immediately = true)
}
}
/**
* Apply the current state to the view, updating it's bounds and desired state
*/
- private fun applyState(bounds: Rect, immediately: Boolean = false) {
+ private fun applyState(bounds: Rect, alpha: Float, immediately: Boolean = false) {
currentBounds.set(bounds)
- val currentlyInGuidedTransformation = isCurrentlyInGuidedTransformation()
- val startLocation = if (currentlyInGuidedTransformation) previousLocation else -1
- val progress = if (currentlyInGuidedTransformation) getTransformationProgress() else 1.0f
- val endLocation = desiredLocation
+ carouselAlpha = if (isCurrentlyFading()) alpha else 1.0f
+ val onlyUseEndState = !isCurrentlyInGuidedTransformation() || isCurrentlyFading()
+ val startLocation = if (onlyUseEndState) -1 else previousLocation
+ val progress = if (onlyUseEndState) 1.0f else getTransformationProgress()
+ val endLocation = resolveLocationForFading()
mediaCarouselController.setCurrentState(startLocation, endLocation, progress, immediately)
updateHostAttachment()
if (currentAttachmentLocation == IN_OVERLAY) {
@@ -668,8 +830,19 @@ class MediaHierarchyManager @Inject constructor(
}
private fun updateHostAttachment() {
- val inOverlay = isTransitionRunning() && rootOverlay != null
- val newLocation = if (inOverlay) IN_OVERLAY else desiredLocation
+ var newLocation = resolveLocationForFading()
+ var canUseOverlay = !isCurrentlyFading()
+ if (isCrossFadeAnimatorRunning) {
+ if (getHost(newLocation)?.visible == true &&
+ getHost(newLocation)?.hostView?.isShown == false &&
+ newLocation != desiredLocation) {
+ // We're crossfading but the view is already hidden. Let's move to the overlay
+ // instead. This happens when animating to the full shade using a button click.
+ canUseOverlay = true
+ }
+ }
+ val inOverlay = isTransitionRunning() && rootOverlay != null && canUseOverlay
+ newLocation = if (inOverlay) IN_OVERLAY else newLocation
if (currentAttachmentLocation != newLocation) {
currentAttachmentLocation = newLocation
@@ -677,10 +850,10 @@ class MediaHierarchyManager @Inject constructor(
(mediaFrame.parent as ViewGroup?)?.removeView(mediaFrame)
// Add it to the new one
- val targetHost = getHost(desiredLocation)!!.hostView
if (inOverlay) {
rootOverlay!!.add(mediaFrame)
} else {
+ val targetHost = getHost(newLocation)!!.hostView
// When adding back to the host, let's make sure to reset the bounds.
// Usually adding the view will trigger a layout that does this automatically,
// but we sometimes suppress this.
@@ -693,7 +866,37 @@ class MediaHierarchyManager @Inject constructor(
left + currentBounds.width(),
top + currentBounds.height())
}
+ if (isCrossFadeAnimatorRunning) {
+ // When cross-fading with an animation, we only notify the media carousel of the
+ // location change, once the view is reattached to the new place and not immediately
+ // when the desired location changes. This callback will update the measurement
+ // of the carousel, only once we've faded out at the old location and then reattach
+ // to fade it in at the new location.
+ mediaCarouselController.onDesiredLocationChanged(
+ newLocation,
+ getHost(newLocation),
+ animate = false
+ )
+ }
+ }
+ }
+
+ /**
+ * Calculate the location when cross fading between locations. While fading out,
+ * the content should remain in the previous location, while after the switch it should
+ * be at the desired location.
+ */
+ private fun resolveLocationForFading(): Int {
+ if (isCrossFadeAnimatorRunning) {
+ // When animating between two hosts with a fade, let's keep ourselves in the old
+ // location for the first half, and then switch over to the end location
+ if (animationCrossFadeProgress > 0.5 || previousLocation == -1) {
+ return crossFadeAnimationEndLocation
+ } else {
+ return crossFadeAnimationStartLocation
+ }
}
+ return desiredLocation
}
private fun isTransitionRunning(): Boolean {
@@ -708,29 +911,29 @@ class MediaHierarchyManager @Inject constructor(
return desiredLocation
}
val onLockscreen = (!bypassController.bypassEnabled &&
- (statusbarState == StatusBarState.KEYGUARD ||
- statusbarState == StatusBarState.FULLSCREEN_USER_SWITCHER))
+ (statusbarState == StatusBarState.KEYGUARD ||
+ statusbarState == StatusBarState.FULLSCREEN_USER_SWITCHER))
val allowedOnLockscreen = notifLockscreenUserManager.shouldShowLockscreenNotifications()
val location = when {
qsExpansion > 0.0f && !onLockscreen -> LOCATION_QS
qsExpansion > 0.4f && onLockscreen -> LOCATION_QS
- onLockscreen && isTransitioningToFullShade -> LOCATION_QQS
+ onLockscreen && isTransformingToFullShadeAndInQQS() -> LOCATION_QQS
onLockscreen && allowedOnLockscreen -> LOCATION_LOCKSCREEN
else -> LOCATION_QQS
}
// When we're on lock screen and the player is not active, we should keep it in QS.
// Otherwise it will try to animate a transition that doesn't make sense.
if (location == LOCATION_LOCKSCREEN && getHost(location)?.visible != true &&
- !statusBarStateController.isDozing) {
+ !statusBarStateController.isDozing) {
return LOCATION_QS
}
if (location == LOCATION_LOCKSCREEN && desiredLocation == LOCATION_QS &&
- collapsingShadeFromQS) {
+ collapsingShadeFromQS) {
// When collapsing on the lockscreen, we want to remain in QS
return LOCATION_QS
}
if (location != LOCATION_LOCKSCREEN && desiredLocation == LOCATION_LOCKSCREEN &&
- !fullyAwake) {
+ !fullyAwake) {
// When unlocking from dozing / while waking up, the media shouldn't be transitioning
// in an animated way. Let's keep it in the lockscreen until we're fully awake and
// reattach it without an animation
@@ -740,6 +943,26 @@ class MediaHierarchyManager @Inject constructor(
}
/**
+ * Are we currently transforming to the full shade and already in QQS
+ */
+ private fun isTransformingToFullShadeAndInQQS(): Boolean {
+ if (!isTransitioningToFullShade) {
+ return false
+ }
+ return fullShadeTransitionProgress > 0.5f
+ }
+
+ /**
+ * Is the current transformationType fading
+ */
+ private fun isCurrentlyFading(): Boolean {
+ if (isTransitioningToFullShade) {
+ return true
+ }
+ return isCrossFadeAnimatorRunning
+ }
+
+ /**
* Returns true when the media card could be visible to the user if existed.
*/
private fun isVisibleToUser(): Boolean {
@@ -789,9 +1012,27 @@ class MediaHierarchyManager @Inject constructor(
* Attached at the root of the hierarchy in an overlay
*/
const val IN_OVERLAY = -1000
+
+ /**
+ * The default transformation type where the hosts transform into each other using a direct
+ * transition
+ */
+ const val TRANSFORMATION_TYPE_TRANSITION = 0
+
+ /**
+ * A transformation type where content fades from one place to another instead of
+ * transitioning
+ */
+ const val TRANSFORMATION_TYPE_FADE = 1
}
}
+@IntDef(prefix = ["TRANSFORMATION_TYPE_"], value = [
+ MediaHierarchyManager.TRANSFORMATION_TYPE_TRANSITION,
+ MediaHierarchyManager.TRANSFORMATION_TYPE_FADE])
+@Retention(AnnotationRetention.SOURCE)
+private annotation class TransformationType
+
@IntDef(prefix = ["LOCATION_"], value = [MediaHierarchyManager.LOCATION_QS,
MediaHierarchyManager.LOCATION_QQS, MediaHierarchyManager.LOCATION_LOCKSCREEN])
@Retention(AnnotationRetention.SOURCE)
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java b/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java
index 34c654c9135d..1c5fa439d0ee 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java
@@ -95,7 +95,7 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca
private boolean mLastKeyguardAndExpanded;
/**
* The last received state from the controller. This should not be used directly to check if
- * we're on keyguard but use {@link #isKeyguardShowing()} instead since that is more accurate
+ * we're on keyguard but use {@link #isKeyguardState()} instead since that is more accurate
* during state transitions which often call into us.
*/
private int mState;
@@ -326,7 +326,7 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca
|| mHeaderAnimating;
mQSPanelController.setExpanded(mQsExpanded);
mQSDetail.setExpanded(mQsExpanded);
- boolean keyguardShowing = isKeyguardShowing();
+ boolean keyguardShowing = isKeyguardState();
mHeader.setVisibility((mQsExpanded || !keyguardShowing || mHeaderAnimating
|| mShowCollapsedOnKeyguard)
? View.VISIBLE
@@ -344,7 +344,7 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca
!mQsDisabled && expandVisually ? View.VISIBLE : View.INVISIBLE);
}
- private boolean isKeyguardShowing() {
+ private boolean isKeyguardState() {
// We want the freshest state here since otherwise we'll have some weirdness if earlier
// listeners trigger updates
return mStatusBarStateController.getState() == StatusBarState.KEYGUARD;
@@ -366,7 +366,7 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca
if (mQSAnimator != null) {
mQSAnimator.setShowCollapsedOnKeyguard(showCollapsed);
}
- if (!showCollapsed && isKeyguardShowing()) {
+ if (!showCollapsed && isKeyguardState()) {
setQsExpansion(mLastQSExpansion, 0);
}
}
@@ -457,7 +457,7 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca
mContainer.setExpansion(expansion);
final float translationScaleY = (mTranslateWhileExpanding
? 1 : QSAnimator.SHORT_PARALLAX_AMOUNT) * (expansion - 1);
- boolean onKeyguardAndExpanded = isKeyguardShowing() && !mShowCollapsedOnKeyguard;
+ boolean onKeyguardAndExpanded = isKeyguardState() && !mShowCollapsedOnKeyguard;
if (!mHeaderAnimating && !headerWillBeAnimating()) {
getView().setTranslationY(
onKeyguardAndExpanded
@@ -531,7 +531,6 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca
// The Media can be scrolled off screen by default, let's offset it
float expandedMediaPosition = absoluteBottomPosition - mQSPanelScrollView.getScrollY()
+ mQSPanelScrollView.getScrollRange();
- // The expanded media host should never move below the laid out position
pinToBottom(expandedMediaPosition, mQsMediaHost, true /* expanded */);
// The expanded media host should never move above the laid out position
pinToBottom(absoluteBottomPosition, mQqsMediaHost, false /* expanded */);
@@ -540,7 +539,8 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca
private void pinToBottom(float absoluteBottomPosition, MediaHost mediaHost, boolean expanded) {
View hostView = mediaHost.getHostView();
- if (mLastQSExpansion > 0) {
+ // on keyguard we cross-fade to expanded, so no need to pin it.
+ if (mLastQSExpansion > 0 && !isKeyguardState()) {
float targetPosition = absoluteBottomPosition - getTotalBottomMargin(hostView)
- hostView.getHeight();
float currentPosition = mediaHost.getCurrentBounds().top
@@ -573,7 +573,7 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca
private boolean headerWillBeAnimating() {
return mState == StatusBarState.KEYGUARD && mShowCollapsedOnKeyguard
- && !isKeyguardShowing();
+ && !isKeyguardState();
}
@Override
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
index a804ae6a5cd8..f5daa867394d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
@@ -5147,8 +5147,8 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable
}
/**
- * Sets the extra top inset for the full shade transition. This is needed to compensate for
- * media transitioning to quick settings
+ * Sets the extra top inset for the full shade transition. This moves notifications down
+ * during the drag down.
*/
public void setExtraTopInsetForFullShadeTransition(float inset) {
mExtraTopInsetForFullShadeTransition = inset;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
index 0d42428dd11d..0ce452e565ed 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
@@ -185,8 +185,17 @@ public class NotificationStackScrollLayoutController {
private ColorExtractor.OnColorsChangedListener mOnColorsChangedListener;
+ /**
+ * The total distance in pixels that the full shade transition takes to transition entirely to
+ * the full shade.
+ */
private int mTotalDistanceForFullShadeTransition;
- private int mTotalExtraMediaInsetFullShadeTransition;
+
+ /**
+ * The amount of movement the notifications do when transitioning to the full shade before
+ * reaching the overstrech
+ */
+ private int mNotificationDragDownMovement;
@VisibleForTesting
final View.OnAttachStateChangeListener mOnAttachStateChangeListener =
@@ -255,8 +264,8 @@ public class NotificationStackScrollLayoutController {
};
private void updateResources() {
- mTotalExtraMediaInsetFullShadeTransition = mResources.getDimensionPixelSize(
- R.dimen.lockscreen_shade_transition_extra_media_inset);
+ mNotificationDragDownMovement = mResources.getDimensionPixelSize(
+ R.dimen.lockscreen_shade_notification_movement);
mTotalDistanceForFullShadeTransition = mResources.getDimensionPixelSize(
R.dimen.lockscreen_shade_qs_transition_distance);
}
@@ -1415,15 +1424,13 @@ public class NotificationStackScrollLayoutController {
* shade. 0.0f means we're not transitioning yet.
*/
public void setTransitionToFullShadeAmount(float amount) {
- float extraTopInset;
- MediaHeaderView view = mKeyguardMediaController.getSinglePaneContainer();
- if (view == null || view.getHeight() == 0
- || mStatusBarStateController.getState() != KEYGUARD) {
- extraTopInset = 0;
- } else {
- extraTopInset = MathUtils.saturate(amount / mTotalDistanceForFullShadeTransition);
- extraTopInset = Interpolators.FAST_OUT_SLOW_IN.getInterpolation(extraTopInset);
- extraTopInset = extraTopInset * mTotalExtraMediaInsetFullShadeTransition;
+ float extraTopInset = 0.0f;
+ if (mStatusBarStateController.getState() == KEYGUARD) {
+ float overallProgress = MathUtils.saturate(amount / mView.getHeight());
+ float transitionProgress = Interpolators.getOvershootInterpolation(overallProgress,
+ 0.6f,
+ (float) mTotalDistanceForFullShadeTransition / (float) mView.getHeight());
+ extraTopInset = transitionProgress * mNotificationDragDownMovement;
}
mView.setExtraTopInsetForFullShadeTransition(extraTopInset);
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithm.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithm.java
index f4710f49524d..7c2723d724ce 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithm.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithm.java
@@ -325,7 +325,9 @@ public class KeyguardClockPositionAlgorithm {
*/
private float getClockAlpha(int y) {
float alphaKeyguard = Math.max(0, y / Math.max(1f, getClockY(1f, mDarkAmount)));
- alphaKeyguard *= (1f - mQsExpansion);
+ float qsAlphaFactor = MathUtils.saturate(mQsExpansion / 0.3f);
+ qsAlphaFactor = 1f - qsAlphaFactor;
+ alphaKeyguard *= qsAlphaFactor;
alphaKeyguard = Interpolators.ACCELERATE.getInterpolation(alphaKeyguard);
return MathUtils.lerp(alphaKeyguard, 1f, mDarkAmount);
}
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 9d8a9bfafe49..4d35e9a59c41 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java
@@ -507,6 +507,13 @@ public class NotificationPanelViewController extends PanelViewController {
private float mSectionPadding;
/**
+ * The padding between the start of notifications and the qs boundary on the lockscreen.
+ * On lockscreen, notifications aren't inset this extra amount, but we still want the
+ * qs boundary to be padded.
+ */
+ private int mLockscreenNotificationQSPadding;
+
+ /**
* The amount of progress we are currently in if we're transitioning to the full shade.
* 0.0f means we're not transitioning yet, while 1 means we're all the way in the full
* shade. This value can also go beyond 1.1 when we're overshooting!
@@ -528,7 +535,7 @@ public class NotificationPanelViewController extends PanelViewController {
/**
* The maximum overshoot allowed for the top padding for the full shade transition
*/
- private int mMaxOverscrollAmountForDragDown;
+ private int mMaxOverscrollAmountForPulse;
/**
* Should we animate the next bounds update
@@ -811,7 +818,7 @@ public class NotificationPanelViewController extends PanelViewController {
amount -> {
float progress = amount / mView.getHeight();
float overstretch = Interpolators.getOvershootInterpolation(progress,
- (float) mMaxOverscrollAmountForDragDown / mView.getHeight(),
+ (float) mMaxOverscrollAmountForPulse / mView.getHeight(),
0.2f);
setOverStrechAmount(overstretch);
});
@@ -872,14 +879,16 @@ public class NotificationPanelViewController extends PanelViewController {
R.dimen.heads_up_status_bar_padding);
mDistanceForQSFullShadeTransition = mResources.getDimensionPixelSize(
R.dimen.lockscreen_shade_qs_transition_distance);
- mMaxOverscrollAmountForDragDown = mResources.getDimensionPixelSize(
- R.dimen.lockscreen_shade_max_top_overshoot);
+ mMaxOverscrollAmountForPulse = mResources.getDimensionPixelSize(
+ R.dimen.pulse_expansion_max_top_overshoot);
mScrimCornerRadius = mResources.getDimensionPixelSize(
R.dimen.notification_scrim_corner_radius);
mScreenCornerRadius = mResources.getDimensionPixelSize(
com.android.internal.R.dimen.rounded_corner_radius);
mNotificationScrimPadding = mResources.getDimensionPixelSize(
R.dimen.notification_side_paddings);
+ mLockscreenNotificationQSPadding = mResources.getDimensionPixelSize(
+ R.dimen.notification_side_paddings);
}
private void updateViewControllers(KeyguardStatusView keyguardStatusView,
@@ -2363,7 +2372,6 @@ public class NotificationPanelViewController extends PanelViewController {
public void setTransitionToFullShadeAmount(float pxAmount, boolean animate, long delay) {
mAnimateNextNotificationBounds = animate && !mShouldUseSplitNotificationShade;
mNotificationBoundsAnimationDelay = delay;
- float progress = MathUtils.saturate(pxAmount / mView.getHeight());
float endPosition = 0;
if (pxAmount > 0.0f) {
@@ -2371,29 +2379,28 @@ public class NotificationPanelViewController extends PanelViewController {
&& !mMediaDataManager.hasActiveMedia()) {
// No notifications are visible, let's animate to the height of qs instead
if (mQs != null) {
- // Let's interpolate to the header height
- endPosition = mQs.getHeader().getHeight();
+ // Let's interpolate to the header height instead of the top padding,
+ // because the toppadding is way too low because of the large clock.
+ // we still want to take into account the edgePosition though as that nicely
+ // overshoots in the stackscroller
+ endPosition = getQSEdgePosition()
+ - mNotificationStackScrollLayoutController.getTopPadding()
+ + mQs.getHeader().getHeight();
}
} else {
// Interpolating to the new bottom edge position!
- endPosition = getQSEdgePosition() - mOverStretchAmount;
-
- // If we have media, we need to put the boundary below it, as the media header
- // still uses the space during the transition.
- endPosition +=
- mNotificationStackScrollLayoutController.getFullShadeTransitionInset();
+ endPosition = getQSEdgePosition()
+ + mNotificationStackScrollLayoutController.getFullShadeTransitionInset();
+ if (isOnKeyguard()) {
+ endPosition -= mLockscreenNotificationQSPadding;
+ }
}
}
// Calculate the overshoot amount such that we're reaching the target after our desired
// distance, but only reach it fully once we drag a full shade length.
- float transitionProgress = 0;
- if (endPosition != 0 && progress != 0) {
- transitionProgress = Interpolators.getOvershootInterpolation(progress,
- mMaxOverscrollAmountForDragDown / endPosition,
- (float) mDistanceForQSFullShadeTransition / (float) mView.getHeight());
- }
- mTransitioningToFullShadeProgress = transitionProgress;
+ mTransitioningToFullShadeProgress = Interpolators.FAST_OUT_SLOW_IN.getInterpolation(
+ MathUtils.saturate(pxAmount / mDistanceForQSFullShadeTransition));
int position = (int) MathUtils.lerp((float) 0, endPosition,
mTransitioningToFullShadeProgress);
@@ -2401,8 +2408,6 @@ public class NotificationPanelViewController extends PanelViewController {
// we want at least 1 pixel otherwise the panel won't be clipped
position = Math.max(1, position);
}
- float overStretchAmount = Math.max(position - endPosition, 0.0f);
- setOverStrechAmount(overStretchAmount);
mTransitionToFullShadeQSPosition = position;
updateQsExpansion();
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaHierarchyManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaHierarchyManagerTest.kt
index c6aef4a18373..bf87a4a59c49 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaHierarchyManagerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaHierarchyManagerTest.kt
@@ -20,6 +20,7 @@ import android.graphics.Rect
import android.testing.AndroidTestingRunner
import android.testing.TestableLooper
import android.view.ViewGroup
+import android.widget.FrameLayout
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.controls.controller.ControlsControllerImplTest.Companion.eq
@@ -33,6 +34,7 @@ import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager
import com.android.systemui.statusbar.policy.ConfigurationController
import com.android.systemui.statusbar.policy.KeyguardStateController
import com.android.systemui.util.animation.UniqueObjectHostView
+import junit.framework.Assert
import org.junit.Assert.assertNotNull
import org.junit.Before
import org.junit.Rule
@@ -65,8 +67,6 @@ class MediaHierarchyManagerTest : SysuiTestCase() {
@Mock
private lateinit var bypassController: KeyguardBypassController
@Mock
- private lateinit var mediaFrame: ViewGroup
- @Mock
private lateinit var keyguardStateController: KeyguardStateController
@Mock
private lateinit var statusBarStateController: SysuiStatusBarStateController
@@ -90,9 +90,11 @@ class MediaHierarchyManagerTest : SysuiTestCase() {
@Rule
val mockito = MockitoJUnit.rule()
private lateinit var mediaHiearchyManager: MediaHierarchyManager
+ private lateinit var mediaFrame: ViewGroup
@Before
fun setup() {
+ mediaFrame = FrameLayout(context)
`when`(mediaCarouselController.mediaFrame).thenReturn(mediaFrame)
mediaHiearchyManager = MediaHierarchyManager(
context,
@@ -112,6 +114,9 @@ class MediaHierarchyManagerTest : SysuiTestCase() {
`when`(statusBarStateController.state).thenReturn(StatusBarState.SHADE)
`when`(mediaCarouselController.mediaCarouselScrollHandler)
.thenReturn(mediaCarouselScrollHandler)
+ val observer = wakefullnessObserver.value
+ assertNotNull("lifecycle observer wasn't registered", observer)
+ observer.onFinishedWakingUp()
// We'll use the viewmanager to verify a few calls below, let's reset this.
clearInvocations(mediaCarouselController)
}
@@ -120,6 +125,7 @@ class MediaHierarchyManagerTest : SysuiTestCase() {
`when`(host.location).thenReturn(location)
`when`(host.currentBounds).thenReturn(Rect())
`when`(host.hostView).thenReturn(UniqueObjectHostView(context))
+ `when`(host.visible).thenReturn(true)
mediaHiearchyManager.register(host)
}
@@ -160,6 +166,73 @@ class MediaHierarchyManagerTest : SysuiTestCase() {
}
@Test
+ fun testGoingToFullShade() {
+ // Let's set it onto Lock screen
+ `when`(statusBarStateController.state).thenReturn(StatusBarState.KEYGUARD)
+ `when`(notificationLockscreenUserManager.shouldShowLockscreenNotifications()).thenReturn(
+ true)
+ statusBarCallback.value.onStatePreChange(StatusBarState.SHADE, StatusBarState.KEYGUARD)
+ clearInvocations(mediaCarouselController)
+
+ // Let's transition all the way to full shade
+ mediaHiearchyManager.setTransitionToFullShadeAmount(100000f)
+ verify(mediaCarouselController).onDesiredLocationChanged(
+ eq(MediaHierarchyManager.LOCATION_QQS),
+ any(MediaHostState::class.java),
+ eq(false),
+ anyLong(),
+ anyLong())
+ clearInvocations(mediaCarouselController)
+
+ // Let's go back to the lock screen
+ mediaHiearchyManager.setTransitionToFullShadeAmount(0.0f)
+ verify(mediaCarouselController).onDesiredLocationChanged(
+ eq(MediaHierarchyManager.LOCATION_LOCKSCREEN),
+ any(MediaHostState::class.java),
+ eq(false),
+ anyLong(),
+ anyLong())
+
+ // Let's make sure alpha is set
+ mediaHiearchyManager.setTransitionToFullShadeAmount(2.0f)
+ Assert.assertTrue("alpha should not be 1.0f when cross fading", mediaFrame.alpha != 1.0f)
+ }
+
+ @Test
+ fun testTransformationOnLockScreenIsFading() {
+ // Let's set it onto Lock screen
+ `when`(statusBarStateController.state).thenReturn(StatusBarState.KEYGUARD)
+ `when`(notificationLockscreenUserManager.shouldShowLockscreenNotifications()).thenReturn(
+ true)
+ statusBarCallback.value.onStatePreChange(StatusBarState.SHADE, StatusBarState.KEYGUARD)
+ clearInvocations(mediaCarouselController)
+
+ // Let's transition from lockscreen to qs
+ mediaHiearchyManager.qsExpansion = 1.0f
+ val transformType = mediaHiearchyManager.calculateTransformationType()
+ Assert.assertTrue("media isn't transforming to qs with a fade",
+ transformType == MediaHierarchyManager.TRANSFORMATION_TYPE_FADE)
+ }
+
+ @Test
+ fun testTransformationOnLockScreenToQQSisFading() {
+ // Let's set it onto Lock screen
+ `when`(statusBarStateController.state).thenReturn(StatusBarState.KEYGUARD)
+ `when`(notificationLockscreenUserManager.shouldShowLockscreenNotifications()).thenReturn(
+ true)
+ statusBarCallback.value.onStatePreChange(StatusBarState.SHADE, StatusBarState.KEYGUARD)
+ clearInvocations(mediaCarouselController)
+
+ // Let's transition from lockscreen to qs
+ `when`(statusBarStateController.state).thenReturn(StatusBarState.SHADE_LOCKED)
+ statusBarCallback.value.onStatePreChange(StatusBarState.KEYGUARD,
+ StatusBarState.SHADE_LOCKED)
+ val transformType = mediaHiearchyManager.calculateTransformationType()
+ Assert.assertTrue("media isn't transforming to qqswith a fade",
+ transformType == MediaHierarchyManager.TRANSFORMATION_TYPE_FADE)
+ }
+
+ @Test
fun testCloseGutsRelayToCarousel() {
mediaHiearchyManager.closeGuts()