Merge changes from topic "lockscreen_shade_transition" into sc-dev

* changes:
  Implemented Lockscreen to shade transition
  Convert DragDownHelper to Kotlin
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/Interpolators.java b/packages/SystemUI/animation/src/com/android/systemui/animation/Interpolators.java
index 0a052df..044b5ed 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/Interpolators.java
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/Interpolators.java
@@ -16,6 +16,7 @@
 
 package com.android.systemui.animation;
 
+import android.util.MathUtils;
 import android.view.animation.AccelerateDecelerateInterpolator;
 import android.view.animation.AccelerateInterpolator;
 import android.view.animation.BounceInterpolator;
@@ -72,6 +73,27 @@
             new PathInterpolator(0.9f, 0f, 0.7f, 1f);
 
     /**
+     * Calculate the amount of overshoot using an exponential falloff function with desired
+     * properties, where the overshoot smoothly transitions at the 1.0f boundary into the
+     * overshoot, retaining its acceleration.
+     *
+     * @param progress a progress value going from 0 to 1
+     * @param overshootAmount the amount > 0 of overshoot desired. A value of 0.1 means the max
+     *                        value of the overall progress will be at 1.1.
+     * @param overshootStart the point in (0,1] where the result should reach 1
+     * @return the interpolated overshoot
+     */
+    public static float getOvershootInterpolation(float progress, float overshootAmount,
+            float overshootStart) {
+        if (overshootAmount == 0.0f || overshootStart == 0.0f) {
+            throw new IllegalArgumentException("Invalid values for overshoot");
+        }
+        float b = MathUtils.log((overshootAmount + 1) / (overshootAmount)) / overshootStart;
+        return MathUtils.max(0.0f,
+                (float) (1.0f - Math.exp(-b * progress)) * (overshootAmount + 1.0f));
+    }
+
+    /**
      * Interpolate alpha for notifications background scrim during shade expansion.
      * @param fraction Shade expansion fraction
      */
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QS.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QS.java
index 4d4c909..98ef9e2 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QS.java
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QS.java
@@ -33,7 +33,7 @@
 
     String ACTION = "com.android.systemui.action.PLUGIN_QS";
 
-    int VERSION = 8;
+    int VERSION = 9;
 
     String TAG = "QS";
 
@@ -50,8 +50,13 @@
     void setListening(boolean listening);
     boolean isShowingDetail();
     void closeDetail();
-    default void setShowCollapsedOnKeyguard(boolean showCollapsedOnKeyguard) {}
-    void animateHeaderSlidingIn(long delay);
+
+    /**
+     * Set that we're currently pulse expanding
+     *
+     * @param pulseExpanding if we're currently expanding during pulsing
+     */
+    default void setPulseExpanding(boolean pulseExpanding) {}
     void animateHeaderSlidingOut();
     void setQsExpansion(float qsExpansionFraction, float headerTranslation);
     void setHeaderListening(boolean listening);
@@ -79,10 +84,23 @@
     void setTranslateWhileExpanding(boolean shouldTranslate);
 
     /**
+     * Set the amount of pixels we have currently dragged down if we're transitioning to the full
+     * shade. 0.0f means we're not transitioning yet.
+     */
+    default void setTransitionToFullShadeAmount(float pxAmount, boolean animated) {}
+
+    /**
      * A rounded corner clipping that makes QS feel as if it were behind everything.
      */
     void setFancyClipping(int top, int bottom, int cornerRadius, boolean visible);
 
+    /**
+     * @return if quick settings is fully collapsed currently
+     */
+    default boolean isFullyCollapsed() {
+        return true;
+    }
+
     @ProvidesInterface(version = HeightListener.VERSION)
     interface HeightListener {
         int VERSION = 1;
diff --git a/packages/SystemUI/res/layout/keyguard_bottom_area.xml b/packages/SystemUI/res/layout/keyguard_bottom_area.xml
index 95483f1..0dc1473 100644
--- a/packages/SystemUI/res/layout/keyguard_bottom_area.xml
+++ b/packages/SystemUI/res/layout/keyguard_bottom_area.xml
@@ -21,8 +21,7 @@
     android:id="@+id/keyguard_bottom_area"
     android:layout_height="match_parent"
     android:layout_width="match_parent"
-    android:outlineProvider="none"
-    android:elevation="5dp" > <!-- Put it above the status bar header -->
+    android:outlineProvider="none" > <!-- Put it above the status bar header -->
 
     <LinearLayout
         android:id="@+id/keyguard_indication_area"
@@ -58,12 +57,6 @@
 
     </LinearLayout>
 
-    <FrameLayout
-        android:id="@+id/preview_container"
-        android:layout_width="match_parent"
-        android:layout_height="match_parent">
-    </FrameLayout>
-
     <com.android.systemui.statusbar.KeyguardAffordanceView
         android:id="@+id/camera_button"
         android:layout_height="@dimen/keyguard_affordance_height"
diff --git a/packages/SystemUI/res/layout/status_bar_expanded.xml b/packages/SystemUI/res/layout/status_bar_expanded.xml
index f4cb3b1..3543fd1 100644
--- a/packages/SystemUI/res/layout/status_bar_expanded.xml
+++ b/packages/SystemUI/res/layout/status_bar_expanded.xml
@@ -37,6 +37,25 @@
         android:layout_height="match_parent"
         android:layout_width="match_parent" />
 
+    <include
+        layout="@layout/keyguard_bottom_area"
+        android:visibility="gone" />
+
+    <ViewStub
+        android:id="@+id/keyguard_user_switcher_stub"
+        android:layout="@layout/keyguard_user_switcher"
+        android:layout_height="match_parent"
+        android:layout_width="match_parent" />
+
+    <include layout="@layout/status_bar_expanded_plugin_frame"/>
+
+    <include layout="@layout/dock_info_bottom_area_overlay" />
+
+    <com.android.keyguard.LockIconView
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:id="@+id/lock_icon_view" />
+
     <com.android.systemui.statusbar.phone.NotificationsQuickSettingsContainer
         android:layout_width="match_parent"
         android:layout_height="match_parent"
@@ -49,6 +68,18 @@
             layout="@layout/keyguard_status_view"
             android:visibility="gone"/>
 
+        <com.android.systemui.scrim.ScrimView
+            android:id="@+id/scrim_notifications"
+            android:layout_width="0dp"
+            android:layout_height="0dp"
+            android:importantForAccessibility="no"
+            systemui:ignoreRightInset="true"
+            systemui:layout_constraintStart_toStartOf="parent"
+            systemui:layout_constraintEnd_toEndOf="parent"
+            systemui:layout_constraintTop_toTopOf="parent"
+            systemui:layout_constraintBottom_toBottomOf="parent"
+            />
+
         <include layout="@layout/dock_info_overlay" />
 
         <FrameLayout
@@ -101,22 +132,9 @@
 
     </com.android.systemui.statusbar.phone.NotificationsQuickSettingsContainer>
 
-    <include layout="@layout/dock_info_bottom_area_overlay" />
-
-    <include
-        layout="@layout/keyguard_bottom_area"
-        android:visibility="gone" />
-
-    <ViewStub
-        android:id="@+id/keyguard_user_switcher_stub"
-        android:layout="@layout/keyguard_user_switcher"
-        android:layout_height="match_parent"
-        android:layout_width="match_parent" />
-
-    <include layout="@layout/status_bar_expanded_plugin_frame"/>
-
-    <com.android.keyguard.LockIconView
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:id="@+id/lock_icon_view" />
+    <FrameLayout
+        android:id="@+id/preview_container"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent">
+    </FrameLayout>
 </com.android.systemui.statusbar.phone.NotificationPanelView>
diff --git a/packages/SystemUI/res/layout/super_notification_shade.xml b/packages/SystemUI/res/layout/super_notification_shade.xml
index bea50e8..08284a0 100644
--- a/packages/SystemUI/res/layout/super_notification_shade.xml
+++ b/packages/SystemUI/res/layout/super_notification_shade.xml
@@ -51,14 +51,6 @@
         sysui:ignoreRightInset="true"
         />
 
-    <com.android.systemui.scrim.ScrimView
-        android:id="@+id/scrim_notifications"
-        android:layout_width="match_parent"
-        android:layout_height="match_parent"
-        android:importantForAccessibility="no"
-        sysui:ignoreRightInset="true"
-        />
-
     <com.android.systemui.statusbar.LightRevealScrim
             android:id="@+id/light_reveal_scrim"
             android:layout_width="match_parent"
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index 87a669f..e7d714e 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -1409,6 +1409,26 @@
     <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>
+
+    <!-- Distance that the full shade transition takes in order for scrim to fully transition to
+         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>
+
+    <!-- Maximum overshoot for the topPadding of notifications when transitioning to the full
+         shade -->
+    <dimen name="lockscreen_shade_max_top_overshoot">32dp</dimen>
+
     <dimen name="people_space_widget_radius">28dp</dimen>
     <dimen name="people_space_image_radius">20dp</dimen>
     <dimen name="people_space_messages_count_radius">12dp</dimen>
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java
index 3a3f2fc..388c085 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java
@@ -198,6 +198,13 @@
     }
 
     /**
+     * @return {@code true} if we are currently animating the screen off from unlock
+     */
+    public boolean isAnimatingScreenOffFromUnlocked() {
+        return mKeyguardVisibilityHelper.isAnimatingScreenOffFromUnlocked();
+    }
+
+    /**
      * Set the visibility of the keyguard status view based on some new state.
      */
     public void setKeyguardStatusViewVisibility(
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardVisibilityHelper.java b/packages/SystemUI/src/com/android/keyguard/KeyguardVisibilityHelper.java
index 6aca5a7..b6a58dc 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardVisibilityHelper.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardVisibilityHelper.java
@@ -22,6 +22,9 @@
 
 import com.android.systemui.animation.Interpolators;
 import com.android.systemui.statusbar.StatusBarState;
+import com.android.systemui.statusbar.notification.AnimatableProperty;
+import com.android.systemui.statusbar.notification.PropertyAnimator;
+import com.android.systemui.statusbar.notification.stack.AnimationProperties;
 import com.android.systemui.statusbar.notification.stack.StackStateAnimator;
 import com.android.systemui.statusbar.phone.DozeParameters;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
@@ -37,6 +40,8 @@
     private final DozeParameters mDozeParameters;
     private boolean mKeyguardViewVisibilityAnimating;
     private boolean mLastOccludedState = false;
+    private boolean mAnimatingScreenOff;
+    private final AnimationProperties mAnimationProperties = new AnimationProperties();
 
     public KeyguardVisibilityHelper(View view, KeyguardStateController keyguardStateController,
             DozeParameters dozeParameters) {
@@ -89,12 +94,21 @@
         } else if (statusBarState == KEYGUARD) {
             if (keyguardFadingAway) {
                 mKeyguardViewVisibilityAnimating = true;
+                float target = mView.getY() - mView.getHeight() * 0.05f;
+                int delay = 0;
+                int duration = 125;
+                // We animate the Y properly separately using the PropertyAnimator, as the panel
+                // view als needs to update the end position.
+                mAnimationProperties.setDuration(duration).setDelay(delay);
+                PropertyAnimator.cancelAnimation(mView, AnimatableProperty.Y);
+                PropertyAnimator.setProperty(mView, AnimatableProperty.Y, target,
+                        mAnimationProperties,
+                        true /* animate */);
                 mView.animate()
                         .alpha(0)
-                        .translationYBy(-mView.getHeight() * 0.05f)
                         .setInterpolator(Interpolators.FAST_OUT_LINEAR_IN)
-                        .setDuration(125)
-                        .setStartDelay(0)
+                        .setDuration(duration)
+                        .setStartDelay(delay)
                         .withEndAction(mAnimateKeyguardStatusViewInvisibleEndRunnable)
                         .start();
             } else if (mLastOccludedState && !isOccluded) {
@@ -110,20 +124,30 @@
                         .start();
             } else if (mDozeParameters.shouldControlUnlockedScreenOff()) {
                 mKeyguardViewVisibilityAnimating = true;
+                mAnimatingScreenOff = true;
 
                 mView.setVisibility(View.VISIBLE);
                 mView.setAlpha(0f);
+                float currentY = mView.getY();
+                mView.setY(currentY - mView.getHeight() * 0.1f);
+                int duration = StackStateAnimator.ANIMATION_DURATION_WAKEUP;
+                int delay = (int) (duration * .6f);
+                // We animate the Y properly separately using the PropertyAnimator, as the panel
+                // view als needs to update the end position.
+                mAnimationProperties.setDuration(duration).setDelay(delay);
+                PropertyAnimator.cancelAnimation(mView, AnimatableProperty.Y);
+                PropertyAnimator.setProperty(mView, AnimatableProperty.Y, currentY,
+                        mAnimationProperties,
+                        true /* animate */);
 
-                float curTranslationY = mView.getTranslationY();
-                mView.setTranslationY(curTranslationY - mView.getHeight() * 0.1f);
                 mView.animate()
-                        .setStartDelay((int) (StackStateAnimator.ANIMATION_DURATION_WAKEUP * .6f))
-                        .setDuration(StackStateAnimator.ANIMATION_DURATION_WAKEUP)
+                        .setStartDelay(delay)
+                        .setDuration(duration)
                         .setInterpolator(Interpolators.FAST_OUT_SLOW_IN)
                         .alpha(1f)
-                        .translationY(curTranslationY)
                         .withEndAction(mAnimateKeyguardStatusViewVisibleEndRunnable)
                         .start();
+
             } else {
                 mView.setVisibility(View.VISIBLE);
                 mView.setAlpha(1f);
@@ -148,5 +172,13 @@
 
     private final Runnable mAnimateKeyguardStatusViewVisibleEndRunnable = () -> {
         mKeyguardViewVisibilityAnimating = false;
+        mAnimatingScreenOff = false;
     };
+
+    /**
+     * @return {@code true} if we are currently animating the screen off from unlock
+     */
+    public boolean isAnimatingScreenOffFromUnlocked() {
+        return mAnimatingScreenOff;
+    }
 }
diff --git a/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java b/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java
index b367bdf..7127444 100644
--- a/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java
@@ -145,7 +145,13 @@
         final boolean hasUdfps = mAuthController.getUdfpsSensorLocation() != null;
         mHasUdfpsOrFaceAuthFeatures = hasFaceAuth || hasUdfps;
         if (!mHasUdfpsOrFaceAuthFeatures) {
-            ((ViewGroup) mView.getParent()).removeView(mView);
+            // Posting since removing a view in the middle of onAttach can lead to a crash in the
+            // iteration loop when the view isn't last
+            mView.setVisibility(View.GONE);
+            mView.post(() -> {
+                mView.setVisibility(View.VISIBLE);
+                ((ViewGroup) mView.getParent()).removeView(mView);
+            });
             return;
         }
 
diff --git a/packages/SystemUI/src/com/android/systemui/media/KeyguardMediaController.kt b/packages/SystemUI/src/com/android/systemui/media/KeyguardMediaController.kt
index b668e88..241c2a9 100644
--- a/packages/SystemUI/src/com/android/systemui/media/KeyguardMediaController.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/KeyguardMediaController.kt
@@ -64,6 +64,12 @@
         mediaHost.init(MediaHierarchyManager.LOCATION_LOCKSCREEN)
     }
 
+    /**
+     * Is the media player visible?
+     */
+    var visible = false
+        private set
+
     var visibilityChangedListener: ((Boolean) -> Unit)? = null
 
     /**
@@ -106,11 +112,11 @@
         val keyguardOrUserSwitcher = (statusBarStateController.state == StatusBarState.KEYGUARD ||
                 statusBarStateController.state == StatusBarState.FULLSCREEN_USER_SWITCHER)
         // mediaHost.visible required for proper animations handling
-        val shouldBeVisible = mediaHost.visible &&
+        visible = mediaHost.visible &&
                 !bypassController.bypassEnabled &&
                 keyguardOrUserSwitcher &&
                 notifLockscreenUserManager.shouldShowLockscreenNotifications()
-        if (shouldBeVisible) {
+        if (visible) {
             showMediaPlayer()
         } else {
             hideMediaPlayer()
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt
index 60e832a..73dfe5e 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt
@@ -26,6 +26,7 @@
 import android.view.View
 import android.view.ViewGroup
 import android.view.ViewGroupOverlay
+import com.android.systemui.R
 import com.android.systemui.animation.Interpolators
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.keyguard.WakefulnessLifecycle
@@ -35,6 +36,7 @@
 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.policy.ConfigurationController
 import com.android.systemui.statusbar.policy.KeyguardStateController
 import com.android.systemui.util.animation.UniqueObjectHostView
 import javax.inject.Inject
@@ -74,6 +76,7 @@
     private val bypassController: KeyguardBypassController,
     private val mediaCarouselController: MediaCarouselController,
     private val notifLockscreenUserManager: NotificationLockscreenUserManager,
+    configurationController: ConfigurationController,
     wakefulnessLifecycle: WakefulnessLifecycle,
     private val statusBarKeyguardViewManager: StatusBarKeyguardViewManager
 ) {
@@ -183,6 +186,58 @@
         }
 
     /**
+     * distance that the full shade transition takes in order for media to fully transition to the
+     * shade
+     */
+    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.
+     */
+    private var fullShadeTransitionProgress = 0f
+        set(value) {
+            if (field == value) {
+                return
+            }
+            field = value
+            if (bypassController.bypassEnabled) {
+                return
+            }
+            updateDesiredLocation()
+            if (value >= 0) {
+                updateTargetState()
+                applyTargetStateIfNotAnimating()
+            }
+        }
+
+    private val isTransitioningToFullShade: Boolean
+        get() = fullShadeTransitionProgress != 0f && !bypassController.bypassEnabled
+
+    /**
+     * Set the amount of pixels we have currently dragged down if we're transitioning to the full
+     * shade. 0.0f means we're not transitioning yet.
+     */
+    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)
+    }
+
+    /**
      * Is the shade currently collapsing from the expanded qs? If we're on the lockscreen and in qs,
      * we wouldn't want to transition in that case.
      */
@@ -242,6 +297,12 @@
         }
 
     init {
+        updateConfiguration()
+        configurationController.addCallback(object : ConfigurationController.ConfigurationListener {
+            override fun onDensityOrFontScaleChanged() {
+                updateConfiguration()
+            }
+        })
         statusBarStateController.addCallback(object : StatusBarStateController.StateListener {
             override fun onStatePreChange(oldState: Int, newState: Int) {
                 // We're updating the location before the state change happens, since we want the
@@ -312,6 +373,13 @@
         })
     }
 
+    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)
+    }
+
     /**
      * Register a media host and create a view can be attached to a view hierarchy
      * and where the players will be placed in when the host is the currently desired state.
@@ -546,6 +614,9 @@
         if (progress >= 0) {
             return progress
         }
+        if (isTransitioningToFullShade) {
+            return fullShadeTransitionProgress
+        }
         return -1.0f
     }
 
@@ -643,6 +714,7 @@
         val location = when {
             qsExpansion > 0.0f && !onLockscreen -> LOCATION_QS
             qsExpansion > 0.4f && onLockscreen -> LOCATION_QS
+            onLockscreen && isTransitioningToFullShade -> LOCATION_QQS
             onLockscreen && allowedOnLockscreen -> LOCATION_LOCKSCREEN
             else -> LOCATION_QQS
         }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java b/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java
index 53b4d5f..34c654c 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java
@@ -112,6 +112,17 @@
      * otherwise.
      */
     private boolean mTranslateWhileExpanding;
+    private boolean mPulseExpanding;
+
+    /**
+     * Are we currently transitioning from lockscreen to the full shade?
+     */
+    private boolean mTransitioningToFullShade;
+
+    /**
+     * Whether the next Quick settings
+     */
+    private boolean mAnimateNextQsUpdate;
 
     @Inject
     public QSFragment(RemoteInputQuickSettingsDisabler remoteInputQsDisabler,
@@ -265,6 +276,11 @@
         }
     }
 
+    @Override
+    public boolean isFullyCollapsed() {
+        return mLastQSExpansion == 0.0f || mLastQSExpansion == -1;
+    }
+
     private void setEditLocation(View view) {
         View edit = view.findViewById(android.R.id.edit);
         int[] loc = edit.getLocationOnScreen();
@@ -335,14 +351,22 @@
     }
 
     @Override
-    public void setShowCollapsedOnKeyguard(boolean showCollapsedOnKeyguard) {
-        if (showCollapsedOnKeyguard != mShowCollapsedOnKeyguard) {
-            mShowCollapsedOnKeyguard = showCollapsedOnKeyguard;
+    public void setPulseExpanding(boolean pulseExpanding) {
+        if (pulseExpanding != mPulseExpanding) {
+            mPulseExpanding = pulseExpanding;
+            updateShowCollapsedOnKeyguard();
+        }
+    }
+
+    private void updateShowCollapsedOnKeyguard() {
+        boolean showCollapsed = mPulseExpanding || mTransitioningToFullShade;
+        if (showCollapsed != mShowCollapsedOnKeyguard) {
+            mShowCollapsedOnKeyguard = showCollapsed;
             updateQsState();
             if (mQSAnimator != null) {
-                mQSAnimator.setShowCollapsedOnKeyguard(showCollapsedOnKeyguard);
+                mQSAnimator.setShowCollapsedOnKeyguard(showCollapsed);
             }
-            if (!showCollapsedOnKeyguard && isKeyguardShowing()) {
+            if (!showCollapsed && isKeyguardShowing()) {
                 setQsExpansion(mLastQSExpansion, 0);
             }
         }
@@ -411,13 +435,24 @@
     }
 
     @Override
-    public void setQsExpansion(float expansion, float headerTranslation) {
-        if (DEBUG) Log.d(TAG, "setQSExpansion " + expansion + " " + headerTranslation);
+    public void setTransitionToFullShadeAmount(float pxAmount, boolean animated) {
+        boolean isTransitioningToFullShade = pxAmount > 0;
+        if (isTransitioningToFullShade != mTransitioningToFullShade) {
+            mTransitioningToFullShade = isTransitioningToFullShade;
+            updateShowCollapsedOnKeyguard();
+            setQsExpansion(mLastQSExpansion, mLastHeaderTranslation);
+        }
+    }
 
+    @Override
+    public void setQsExpansion(float expansion, float proposedTranslation) {
+        if (DEBUG) Log.d(TAG, "setQSExpansion " + expansion + " " + proposedTranslation);
+        float headerTranslation = mTransitioningToFullShade ? 0 : proposedTranslation;
         if (mQSAnimator != null) {
             final boolean showQSOnLockscreen = expansion > 0;
             final boolean showQSUnlocked = headerTranslation == 0 || !mTranslateWhileExpanding;
-            mQSAnimator.startAlphaAnimation(showQSOnLockscreen || showQSUnlocked);
+            mQSAnimator.startAlphaAnimation(showQSOnLockscreen || showQSUnlocked
+                    || mTransitioningToFullShade);
         }
         mContainer.setExpansion(expansion);
         final float translationScaleY = (mTranslateWhileExpanding
@@ -542,18 +577,6 @@
     }
 
     @Override
-    public void animateHeaderSlidingIn(long delay) {
-        if (DEBUG) Log.d(TAG, "animateHeaderSlidingIn");
-        // If the QS is already expanded we don't need to slide in the header as it's already
-        // visible.
-        if (!mQsExpanded && getView().getTranslationY() != 0) {
-            mHeaderAnimating = true;
-            mDelay = delay;
-            getView().getViewTreeObserver().addOnPreDrawListener(mStartHeaderSlidingIn);
-        }
-    }
-
-    @Override
     public void animateHeaderSlidingOut() {
         if (DEBUG) Log.d(TAG, "animateHeaderSlidingOut");
         if (getView().getY() == -mHeader.getHeight()) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/DragDownHelper.java b/packages/SystemUI/src/com/android/systemui/statusbar/DragDownHelper.java
deleted file mode 100644
index 0378123..0000000
--- a/packages/SystemUI/src/com/android/systemui/statusbar/DragDownHelper.java
+++ /dev/null
@@ -1,292 +0,0 @@
-/*
- * Copyright (C) 2014 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.statusbar;
-
-import static com.android.systemui.classifier.Classifier.NOTIFICATION_DRAG_DOWN;
-
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.ObjectAnimator;
-import android.animation.ValueAnimator;
-import android.content.Context;
-import android.view.MotionEvent;
-import android.view.View;
-import android.view.ViewConfiguration;
-
-import com.android.systemui.ExpandHelper;
-import com.android.systemui.Gefingerpoken;
-import com.android.systemui.R;
-import com.android.systemui.animation.Interpolators;
-import com.android.systemui.classifier.FalsingCollector;
-import com.android.systemui.plugins.FalsingManager;
-import com.android.systemui.statusbar.notification.row.ExpandableView;
-
-/**
- * A utility class to enable the downward swipe on the lockscreen to go to the full shade and expand
- * the notification where the drag started.
- */
-public class DragDownHelper implements Gefingerpoken {
-
-    private static final float RUBBERBAND_FACTOR_EXPANDABLE = 0.5f;
-    private static final float RUBBERBAND_FACTOR_STATIC = 0.15f;
-
-    private static final int SPRING_BACK_ANIMATION_LENGTH_MS = 375;
-
-    private int mMinDragDistance;
-    private final FalsingManager mFalsingManager;
-    private ExpandHelper.Callback mCallback;
-    private float mInitialTouchX;
-    private float mInitialTouchY;
-    private boolean mDraggingDown;
-    private final float mTouchSlop;
-    private final float mSlopMultiplier;
-    private DragDownCallback mDragDownCallback;
-    private View mHost;
-    private final int[] mTemp2 = new int[2];
-    private boolean mDraggedFarEnough;
-    private ExpandableView mStartingChild;
-    private float mLastHeight;
-    private FalsingCollector mFalsingCollector;
-
-    public DragDownHelper(Context context, View host, ExpandHelper.Callback callback,
-            DragDownCallback dragDownCallback, FalsingManager falsingManager,
-            FalsingCollector falsingCollector) {
-        mMinDragDistance = context.getResources().getDimensionPixelSize(
-                R.dimen.keyguard_drag_down_min_distance);
-        mFalsingManager = falsingManager;
-        final ViewConfiguration configuration = ViewConfiguration.get(context);
-        mTouchSlop = configuration.getScaledTouchSlop();
-        mSlopMultiplier = configuration.getScaledAmbiguousGestureMultiplier();
-        mCallback = callback;
-        mDragDownCallback = dragDownCallback;
-        mHost = host;
-        mFalsingCollector = falsingCollector;
-    }
-
-    @Override
-    public boolean onInterceptTouchEvent(MotionEvent event) {
-        final float x = event.getX();
-        final float y = event.getY();
-
-        switch (event.getActionMasked()) {
-            case MotionEvent.ACTION_DOWN:
-                mDraggedFarEnough = false;
-                mDraggingDown = false;
-                mStartingChild = null;
-                mInitialTouchY = y;
-                mInitialTouchX = x;
-                break;
-
-            case MotionEvent.ACTION_MOVE:
-                final float h = y - mInitialTouchY;
-                // Adjust the touch slop if another gesture may be being performed.
-                final float touchSlop =
-                        event.getClassification() == MotionEvent.CLASSIFICATION_AMBIGUOUS_GESTURE
-                        ? mTouchSlop * mSlopMultiplier
-                        : mTouchSlop;
-                if (h > touchSlop && h > Math.abs(x - mInitialTouchX)) {
-                    mFalsingCollector.onNotificationStartDraggingDown();
-                    mDraggingDown = true;
-                    captureStartingChild(mInitialTouchX, mInitialTouchY);
-                    mInitialTouchY = y;
-                    mInitialTouchX = x;
-                    mDragDownCallback.onTouchSlopExceeded();
-                    return mStartingChild != null || mDragDownCallback.isDragDownAnywhereEnabled();
-                }
-                break;
-        }
-        return false;
-    }
-
-    @Override
-    public boolean onTouchEvent(MotionEvent event) {
-        if (!mDraggingDown) {
-            return false;
-        }
-        final float x = event.getX();
-        final float y = event.getY();
-
-        switch (event.getActionMasked()) {
-            case MotionEvent.ACTION_MOVE:
-                mLastHeight = y - mInitialTouchY;
-                captureStartingChild(mInitialTouchX, mInitialTouchY);
-                if (mStartingChild != null) {
-                    handleExpansion(mLastHeight, mStartingChild);
-                } else {
-                    mDragDownCallback.setEmptyDragAmount(mLastHeight);
-                }
-                if (mLastHeight > mMinDragDistance) {
-                    if (!mDraggedFarEnough) {
-                        mDraggedFarEnough = true;
-                        mDragDownCallback.onCrossedThreshold(true);
-                    }
-                } else {
-                    if (mDraggedFarEnough) {
-                        mDraggedFarEnough = false;
-                        mDragDownCallback.onCrossedThreshold(false);
-                    }
-                }
-                return true;
-            case MotionEvent.ACTION_UP:
-                if (!mFalsingManager.isUnlockingDisabled() && mDragDownCallback.canDragDown()
-                        && !isFalseTouch()) {
-                    mDragDownCallback.onDraggedDown(mStartingChild, (int) (y - mInitialTouchY));
-                    if (mStartingChild == null) {
-                        cancelExpansion();
-                    } else {
-                        mCallback.setUserLockedChild(mStartingChild, false);
-                        mStartingChild = null;
-                    }
-                    mDraggingDown = false;
-                } else {
-                    stopDragging();
-                    return false;
-                }
-                break;
-            case MotionEvent.ACTION_CANCEL:
-                stopDragging();
-                return false;
-        }
-        return false;
-    }
-
-    private boolean isFalseTouch() {
-        if (!mDragDownCallback.isFalsingCheckNeeded()) {
-            return false;
-        }
-        return mFalsingManager.isFalseTouch(NOTIFICATION_DRAG_DOWN) || !mDraggedFarEnough;
-    }
-
-    private void captureStartingChild(float x, float y) {
-        if (mStartingChild == null) {
-            mStartingChild = findView(x, y);
-            if (mStartingChild != null) {
-                if (mDragDownCallback.isDragDownEnabledForView(mStartingChild)) {
-                    mCallback.setUserLockedChild(mStartingChild, true);
-                } else {
-                    mStartingChild = null;
-                }
-            }
-        }
-    }
-
-    private void handleExpansion(float heightDelta, ExpandableView child) {
-        if (heightDelta < 0) {
-            heightDelta = 0;
-        }
-        boolean expandable = child.isContentExpandable();
-        float rubberbandFactor = expandable
-                ? RUBBERBAND_FACTOR_EXPANDABLE
-                : RUBBERBAND_FACTOR_STATIC;
-        float rubberband = heightDelta * rubberbandFactor;
-        if (expandable
-                && (rubberband + child.getCollapsedHeight()) > child.getMaxContentHeight()) {
-            float overshoot =
-                    (rubberband + child.getCollapsedHeight()) - child.getMaxContentHeight();
-            overshoot *= (1 - RUBBERBAND_FACTOR_STATIC);
-            rubberband -= overshoot;
-        }
-        child.setActualHeight((int) (child.getCollapsedHeight() + rubberband));
-    }
-
-    private void cancelExpansion(final ExpandableView child) {
-        if (child.getActualHeight() == child.getCollapsedHeight()) {
-            mCallback.setUserLockedChild(child, false);
-            return;
-        }
-        ObjectAnimator anim = ObjectAnimator.ofInt(child, "actualHeight",
-                child.getActualHeight(), child.getCollapsedHeight());
-        anim.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
-        anim.setDuration(SPRING_BACK_ANIMATION_LENGTH_MS);
-        anim.addListener(new AnimatorListenerAdapter() {
-            @Override
-            public void onAnimationEnd(Animator animation) {
-                mCallback.setUserLockedChild(child, false);
-            }
-        });
-        anim.start();
-    }
-
-    private void cancelExpansion() {
-        ValueAnimator anim = ValueAnimator.ofFloat(mLastHeight, 0);
-        anim.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
-        anim.setDuration(SPRING_BACK_ANIMATION_LENGTH_MS);
-        anim.addUpdateListener(animation -> {
-            mDragDownCallback.setEmptyDragAmount((Float) animation.getAnimatedValue());
-        });
-        anim.start();
-    }
-
-    private void stopDragging() {
-        mFalsingCollector.onNotificationStopDraggingDown();
-        if (mStartingChild != null) {
-            cancelExpansion(mStartingChild);
-            mStartingChild = null;
-        } else {
-            cancelExpansion();
-        }
-        mDraggingDown = false;
-        mDragDownCallback.onDragDownReset();
-    }
-
-    private ExpandableView findView(float x, float y) {
-        mHost.getLocationOnScreen(mTemp2);
-        x += mTemp2[0];
-        y += mTemp2[1];
-        return mCallback.getChildAtRawPosition(x, y);
-    }
-
-    public boolean isDraggingDown() {
-        return mDraggingDown;
-    }
-
-    public boolean isDragDownEnabled() {
-        return mDragDownCallback.isDragDownEnabledForView(null);
-    }
-
-    public interface DragDownCallback {
-
-        /**
-         * @return true if the interaction is accepted, false if it should be cancelled
-         */
-        boolean canDragDown();
-
-        /** Call when a view has been dragged. */
-        void onDraggedDown(View startingChild, int dragLengthY);
-        void onDragDownReset();
-
-        /**
-         * The user has dragged either above or below the threshold
-         * @param above whether he dragged above it
-         */
-        void onCrossedThreshold(boolean above);
-        void onTouchSlopExceeded();
-        void setEmptyDragAmount(float amount);
-        boolean isFalsingCheckNeeded();
-
-        /**
-         * Is dragging down enabled on a given view
-         * @param view The view to check or {@code null} to check if it's enabled at all
-         */
-        boolean isDragDownEnabledForView(ExpandableView view);
-
-        /**
-         * @return if drag down is enabled anywhere, not just on selected views.
-         */
-        boolean isDragDownAnywhereEnabled();
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt
new file mode 100644
index 0000000..12f569c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt
@@ -0,0 +1,667 @@
+package com.android.systemui.statusbar
+
+import android.animation.Animator
+import android.animation.AnimatorListenerAdapter
+import android.animation.ObjectAnimator
+import android.animation.ValueAnimator
+import android.content.Context
+import android.content.res.Configuration
+import android.os.SystemClock
+import android.util.DisplayMetrics
+import android.util.MathUtils
+import android.view.MotionEvent
+import android.view.View
+import android.view.ViewConfiguration
+import androidx.annotation.VisibleForTesting
+import com.android.internal.logging.nano.MetricsProto.MetricsEvent
+import com.android.systemui.ExpandHelper
+import com.android.systemui.Gefingerpoken
+import com.android.systemui.R
+import com.android.systemui.animation.Interpolators
+import com.android.systemui.classifier.Classifier
+import com.android.systemui.classifier.FalsingCollector
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.media.MediaHierarchyManager
+import com.android.systemui.plugins.ActivityStarter.OnDismissAction
+import com.android.systemui.plugins.FalsingManager
+import com.android.systemui.plugins.qs.QS
+import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
+import com.android.systemui.statusbar.notification.row.ExpandableView
+import com.android.systemui.statusbar.notification.stack.AmbientState
+import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
+import com.android.systemui.statusbar.phone.KeyguardBypassController
+import com.android.systemui.statusbar.phone.LockscreenGestureLogger
+import com.android.systemui.statusbar.phone.LockscreenGestureLogger.LockscreenUiEvent
+import com.android.systemui.statusbar.phone.NotificationPanelViewController
+import com.android.systemui.statusbar.phone.ScrimController
+import com.android.systemui.statusbar.phone.StatusBar
+import com.android.systemui.statusbar.policy.ConfigurationController
+import com.android.systemui.util.Utils
+import javax.inject.Inject
+
+private const val SPRING_BACK_ANIMATION_LENGTH_MS = 375L
+private const val RUBBERBAND_FACTOR_STATIC = 0.15f
+private const val RUBBERBAND_FACTOR_EXPANDABLE = 0.5f
+
+/**
+ * A class that controls the lockscreen to shade transition
+ */
+@SysUISingleton
+class LockscreenShadeTransitionController @Inject constructor(
+    private val statusBarStateController: SysuiStatusBarStateController,
+    private val lockscreenGestureLogger: LockscreenGestureLogger,
+    private val keyguardBypassController: KeyguardBypassController,
+    private val lockScreenUserManager: NotificationLockscreenUserManager,
+    private val falsingCollector: FalsingCollector,
+    private val ambientState: AmbientState,
+    private val displayMetrics: DisplayMetrics,
+    private val mediaHierarchyManager: MediaHierarchyManager,
+    private val scrimController: ScrimController,
+    private val featureFlags: FeatureFlags,
+    private val context: Context,
+    configurationController: ConfigurationController,
+    falsingManager: FalsingManager
+) {
+    private var useSplitShade: Boolean = false
+    private lateinit var nsslController: NotificationStackScrollLayoutController
+    lateinit var notificationPanelController: NotificationPanelViewController
+    lateinit var statusbar: StatusBar
+    lateinit var qS: QS
+
+    /**
+     * A handler that handles the next keyguard dismiss animation.
+     */
+    private var animationHandlerOnKeyguardDismiss: ((Long) -> Unit)? = null
+
+    /**
+     * The entry that was just dragged down on.
+     */
+    private var draggedDownEntry: NotificationEntry? = null
+
+    /**
+     * The current animator if any
+     */
+    @VisibleForTesting
+    internal var dragDownAnimator: ValueAnimator? = null
+
+    /**
+     * Distance that the full shade transition takes in order for scrim to fully transition to
+     * the shade (in alpha)
+     */
+    private var scrimTransitionDistance = 0
+
+    /**
+     * Distance that the full transition takes in order for us to fully transition to the shade
+     */
+    private var fullTransitionDistance = 0
+
+    /**
+     * Flag to make sure that the dragDownAmount is applied to the listeners even when in the
+     * locked down shade.
+     */
+    private var forceApplyAmount = false
+
+    /**
+     * A flag to suppress the default animation when unlocking in the locked down shade.
+     */
+    private var nextHideKeyguardNeedsNoAnimation = false
+
+    /**
+     * The touch helper responsible for the drag down animation.
+     */
+    val touchHelper = DragDownHelper(falsingManager, falsingCollector, this, context)
+
+    init {
+        updateResources()
+        configurationController.addCallback(object : ConfigurationController.ConfigurationListener {
+            override fun onConfigChanged(newConfig: Configuration?) {
+                updateResources()
+                touchHelper.updateResources(context)
+            }
+        })
+    }
+
+    private fun updateResources() {
+        scrimTransitionDistance = context.resources.getDimensionPixelSize(
+                R.dimen.lockscreen_shade_scrim_transition_distance)
+        fullTransitionDistance = context.resources.getDimensionPixelSize(
+                R.dimen.lockscreen_shade_qs_transition_distance)
+        useSplitShade = Utils.shouldUseSplitNotificationShade(featureFlags, context.resources)
+    }
+
+    fun setStackScroller(nsslController: NotificationStackScrollLayoutController) {
+        this.nsslController = nsslController
+        touchHelper.host = nsslController.view
+        touchHelper.expandCallback = nsslController.expandHelperCallback
+    }
+
+    /**
+     * Initialize the shelf controller such that clicks on it will expand the shade
+     */
+    fun bindController(notificationShelfController: NotificationShelfController) {
+        // Bind the click listener of the shelf to go to the full shade
+        notificationShelfController.setOnClickListener {
+            if (statusBarStateController.state == StatusBarState.KEYGUARD) {
+                statusbar.wakeUpIfDozing(SystemClock.uptimeMillis(), it, "SHADE_CLICK")
+                goToLockedShade(it)
+            }
+        }
+    }
+
+    /**
+     * @return true if the interaction is accepted, false if it should be cancelled
+     */
+    internal fun canDragDown(): Boolean {
+        return (statusBarStateController.state == StatusBarState.KEYGUARD ||
+                nsslController.isInLockedDownShade()) &&
+                qS.isFullyCollapsed
+    }
+
+    /**
+     * Called by the touch helper when when a gesture has completed all the way and released.
+     */
+    internal fun onDraggedDown(startingChild: View?, dragLengthY: Int) {
+        if (canDragDown()) {
+            if (nsslController.isInLockedDownShade()) {
+                statusBarStateController.setLeaveOpenOnKeyguardHide(true)
+                statusbar.dismissKeyguardThenExecute(OnDismissAction {
+                    nextHideKeyguardNeedsNoAnimation = true
+                    false
+                },
+                        null /* cancelRunnable */, false /* afterKeyguardGone */)
+            } else {
+                lockscreenGestureLogger.write(
+                        MetricsEvent.ACTION_LS_SHADE,
+                        (dragLengthY / displayMetrics.density).toInt(),
+                        0 /* velocityDp */)
+                lockscreenGestureLogger.log(LockscreenUiEvent.LOCKSCREEN_PULL_SHADE_OPEN)
+                if (!ambientState.isDozing() || startingChild != null) {
+                    // go to locked shade while animating the drag down amount from its current
+                    // value
+                    val animationHandler = { delay: Long ->
+                        if (startingChild is ExpandableNotificationRow) {
+                            startingChild.onExpandedByGesture(
+                                    true /* drag down is always an open */)
+                        }
+                        notificationPanelController.animateToFullShade(delay)
+                        notificationPanelController.setTransitionToFullShadeAmount(0f,
+                                true /* animated */, delay)
+
+                        // Let's reset ourselves, ready for the next animation
+
+                        // changing to shade locked will make isInLockDownShade true, so let's
+                        // override that
+                        forceApplyAmount = true
+                        // Reset the behavior. At this point the animation is already started
+                        dragDownAmount = 0f
+                        forceApplyAmount = false
+                    }
+                    val cancelRunnable = Runnable { setDragDownAmountAnimated(0f) }
+                    goToLockedShadeInternal(startingChild, animationHandler, cancelRunnable)
+                }
+            }
+        } else {
+            setDragDownAmountAnimated(0f)
+        }
+    }
+
+    /**
+     * Called by the touch helper when the drag down was aborted and should be reset.
+     */
+    internal fun onDragDownReset() {
+        nsslController.setDimmed(true /* dimmed */, true /* animated */)
+        nsslController.resetScrollPosition()
+        nsslController.resetCheckSnoozeLeavebehind()
+        setDragDownAmountAnimated(0f)
+    }
+
+    /**
+     * The user has dragged either above or below the threshold which changes the dimmed state.
+     * @param above whether they dragged above it
+     */
+    internal fun onCrossedThreshold(above: Boolean) {
+        nsslController.setDimmed(!above /* dimmed */, true /* animate */)
+    }
+
+    /**
+     * Called by the touch helper when the drag down was started
+     */
+    internal fun onDragDownStarted() {
+        nsslController.cancelLongPress()
+        nsslController.checkSnoozeLeavebehind()
+        dragDownAnimator?.cancel()
+    }
+
+    /**
+     * Do we need a falsing check currently?
+     */
+    internal val isFalsingCheckNeeded: Boolean
+        get() = statusBarStateController.state == StatusBarState.KEYGUARD
+
+    /**
+     * Is dragging down enabled on a given view
+     * @param view The view to check or `null` to check if it's enabled at all
+     */
+    internal fun isDragDownEnabledForView(view: ExpandableView?): Boolean {
+        if (isDragDownAnywhereEnabled) {
+            return true
+        }
+        if (nsslController.isInLockedDownShade()) {
+            if (view == null) {
+                // Dragging down is allowed in general
+                return true
+            }
+            if (view is ExpandableNotificationRow) {
+                // Only drag down on sensitive views, otherwise the ExpandHelper will take this
+                return view.entry.isSensitive
+            }
+        }
+        return false
+    }
+
+    /**
+     * @return if drag down is enabled anywhere, not just on selected views.
+     */
+    internal val isDragDownAnywhereEnabled: Boolean
+        get() = (statusBarStateController.getState() == StatusBarState.KEYGUARD &&
+                !keyguardBypassController.bypassEnabled &&
+                qS.isFullyCollapsed)
+
+    /**
+     * The amount in pixels that the user has dragged down.
+     */
+    internal var dragDownAmount = 0f
+        set(value) {
+            if (field != value || forceApplyAmount) {
+                field = value
+                if (!nsslController.isInLockedDownShade() || forceApplyAmount) {
+                    nsslController.setTransitionToFullShadeAmount(field)
+                    notificationPanelController.setTransitionToFullShadeAmount(field,
+                            false /* animate */, 0 /* delay */)
+                    mediaHierarchyManager.setTransitionToFullShadeAmount(field)
+                    val scrimProgress = MathUtils.saturate(field / scrimTransitionDistance)
+                    scrimController.setTransitionToFullShadeProgress(scrimProgress)
+                    // TODO: appear qs also in split shade
+                    val qsAmount = if (useSplitShade) 0f else field
+                    qS.setTransitionToFullShadeAmount(qsAmount, false /* animate */)
+                }
+            }
+        }
+
+    private fun setDragDownAmountAnimated(
+        target: Float,
+        delay: Long = 0,
+        endlistener: (() -> Unit)? = null
+    ) {
+        val dragDownAnimator = ValueAnimator.ofFloat(dragDownAmount, target)
+        dragDownAnimator.interpolator = Interpolators.FAST_OUT_SLOW_IN
+        dragDownAnimator.duration = SPRING_BACK_ANIMATION_LENGTH_MS
+        dragDownAnimator.addUpdateListener { animation: ValueAnimator ->
+            dragDownAmount = animation.animatedValue as Float
+        }
+        if (delay > 0) {
+            dragDownAnimator.startDelay = delay
+        }
+        if (endlistener != null) {
+            dragDownAnimator.addListener(object : AnimatorListenerAdapter() {
+                override fun onAnimationEnd(animation: Animator?) {
+                    endlistener.invoke()
+                }
+            })
+        }
+        dragDownAnimator.start()
+        this.dragDownAnimator = dragDownAnimator
+    }
+
+    /**
+     * Animate appear the drag down amount.
+     */
+    private fun animateAppear(delay: Long = 0) {
+        // changing to shade locked will make isInLockDownShade true, so let's override
+        // that
+        forceApplyAmount = true
+
+        // we set the value initially to 1 pixel, since that will make sure we're
+        // transitioning to the full shade. this is important to avoid flickering,
+        // as the below animation only starts once the shade is unlocked, which can
+        // be a couple of frames later. if we're setting it to 0, it will use the
+        // default inset and therefore flicker
+        dragDownAmount = 1f
+        setDragDownAmountAnimated(fullTransitionDistance.toFloat(), delay = delay) {
+            // End listener:
+            // Reset
+            dragDownAmount = 0f
+            forceApplyAmount = false
+        }
+    }
+
+    /**
+     * Ask this controller to go to the locked shade, changing the state change and doing
+     * an animation, where the qs appears from 0 from the top
+     *
+     * If secure with redaction: Show bouncer, go to unlocked shade.
+     * If secure without redaction or no security: Go to [StatusBarState.SHADE_LOCKED].
+     *
+     * @param expandView The view to expand after going to the shade
+     * @param needsQSAnimation if this needs the quick settings to slide in from the top or if
+     *                         that's already handled separately
+     */
+    @JvmOverloads
+    fun goToLockedShade(expandedView: View?, needsQSAnimation: Boolean = true) {
+        if (statusBarStateController.state == StatusBarState.KEYGUARD) {
+            val animationHandler: ((Long) -> Unit)?
+            if (needsQSAnimation) {
+                // Let's use the default animation
+                animationHandler = null
+            } else {
+                // Let's only animate notifications
+                animationHandler = { delay: Long ->
+                    notificationPanelController.animateToFullShade(delay)
+                }
+            }
+            goToLockedShadeInternal(expandedView, animationHandler,
+                    cancelAction = null)
+        }
+    }
+
+    /**
+     * If secure with redaction: Show bouncer, go to unlocked shade.
+     *
+     * If secure without redaction or no security: Go to [StatusBarState.SHADE_LOCKED].
+     *
+     * @param expandView The view to expand after going to the shade.
+     * @param animationHandler The handler which performs the go to full shade animation. If null,
+     *                         the default handler will do the animation, otherwise the caller is
+     *                         responsible for the animation. The input value is a Long for the
+     *                         delay for the animation.
+     * @param cancelAction The runnable to invoke when the transition is aborted. This happens if
+     *                     the user goes to the bouncer and goes back.
+     */
+    private fun goToLockedShadeInternal(
+        expandView: View?,
+        animationHandler: ((Long) -> Unit)? = null,
+        cancelAction: Runnable? = null
+    ) {
+        if (statusbar.isShadeDisabled) {
+            cancelAction?.run()
+            return
+        }
+        var userId: Int = lockScreenUserManager.getCurrentUserId()
+        var entry: NotificationEntry? = null
+        if (expandView is ExpandableNotificationRow) {
+            entry = expandView.entry
+            entry.setUserExpanded(true /* userExpanded */, true /* allowChildExpansion */)
+            // Indicate that the group expansion is changing at this time -- this way the group
+            // and children backgrounds / divider animations will look correct.
+            entry.setGroupExpansionChanging(true)
+            userId = entry.sbn.userId
+        }
+        var fullShadeNeedsBouncer = (!lockScreenUserManager.userAllowsPrivateNotificationsInPublic(
+                lockScreenUserManager.getCurrentUserId()) ||
+                !lockScreenUserManager.shouldShowLockscreenNotifications() ||
+                falsingCollector.shouldEnforceBouncer())
+        if (keyguardBypassController.bypassEnabled) {
+            fullShadeNeedsBouncer = false
+        }
+        if (lockScreenUserManager.isLockscreenPublicMode(userId) && fullShadeNeedsBouncer) {
+            statusBarStateController.setLeaveOpenOnKeyguardHide(true)
+            var onDismissAction: OnDismissAction? = null
+            if (animationHandler != null) {
+                onDismissAction = OnDismissAction {
+                    // We're waiting on keyguard to hide before triggering the action,
+                    // as that will make the animation work properly
+                    animationHandlerOnKeyguardDismiss = animationHandler
+                    false
+                }
+            }
+            val cancelHandler = Runnable {
+                draggedDownEntry?.apply {
+                    setUserLocked(false)
+                    notifyHeightChanged(false /* needsAnimation */)
+                    draggedDownEntry = null
+                }
+                cancelAction?.run()
+            }
+            statusbar.showBouncerWithDimissAndCancelIfKeyguard(onDismissAction, cancelHandler)
+            draggedDownEntry = entry
+        } else {
+            statusBarStateController.setState(StatusBarState.SHADE_LOCKED)
+            // This call needs to be after updating the shade state since otherwise
+            // the scrimstate resets too early
+            if (animationHandler != null) {
+                animationHandler.invoke(0 /* delay */)
+            } else {
+                performDefaultGoToFullShadeAnimation(0)
+            }
+        }
+    }
+
+    /**
+     * Notify this handler that the keyguard was just dismissed and that a animation to
+     * the full shade should happen.
+     */
+    fun onHideKeyguard(delay: Long) {
+        if (animationHandlerOnKeyguardDismiss != null) {
+            animationHandlerOnKeyguardDismiss!!.invoke(delay)
+            animationHandlerOnKeyguardDismiss = null
+        } else {
+            if (nextHideKeyguardNeedsNoAnimation) {
+                nextHideKeyguardNeedsNoAnimation = false
+            } else {
+                performDefaultGoToFullShadeAnimation(delay)
+            }
+        }
+        draggedDownEntry?.apply {
+            setUserLocked(false)
+            draggedDownEntry = null
+        }
+    }
+
+    /**
+     * Perform the default appear animation when going to the full shade. This is called when
+     * not triggered by gestures, e.g. when clicking on the shelf or expand button.
+     */
+    private fun performDefaultGoToFullShadeAnimation(delay: Long) {
+        notificationPanelController.animateToFullShade(delay)
+        animateAppear(delay)
+    }
+}
+
+/**
+ * A utility class to enable the downward swipe on the lockscreen to go to the full shade and expand
+ * the notification where the drag started.
+ */
+class DragDownHelper(
+    private val falsingManager: FalsingManager,
+    private val falsingCollector: FalsingCollector,
+    private val dragDownCallback: LockscreenShadeTransitionController,
+    context: Context
+) : Gefingerpoken {
+
+    private var dragDownAmountOnStart = 0.0f
+    lateinit var expandCallback: ExpandHelper.Callback
+    lateinit var host: View
+
+    private var minDragDistance = 0
+    private var initialTouchX = 0f
+    private var initialTouchY = 0f
+    private var touchSlop = 0f
+    private var slopMultiplier = 0f
+    private val temp2 = IntArray(2)
+    private var draggedFarEnough = false
+    private var startingChild: ExpandableView? = null
+    private var lastHeight = 0f
+    var isDraggingDown = false
+        private set
+
+    private val isFalseTouch: Boolean
+        get() {
+            return if (!dragDownCallback.isFalsingCheckNeeded) {
+                false
+            } else {
+                falsingManager.isFalseTouch(Classifier.NOTIFICATION_DRAG_DOWN) || !draggedFarEnough
+            }
+        }
+
+    val isDragDownEnabled: Boolean
+        get() = dragDownCallback.isDragDownEnabledForView(null)
+
+    init {
+        updateResources(context)
+    }
+
+    fun updateResources(context: Context) {
+        minDragDistance = context.resources.getDimensionPixelSize(
+                R.dimen.keyguard_drag_down_min_distance)
+        val configuration = ViewConfiguration.get(context)
+        touchSlop = configuration.scaledTouchSlop.toFloat()
+        slopMultiplier = configuration.scaledAmbiguousGestureMultiplier
+    }
+
+    override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
+        val x = event.x
+        val y = event.y
+        when (event.actionMasked) {
+            MotionEvent.ACTION_DOWN -> {
+                draggedFarEnough = false
+                isDraggingDown = false
+                startingChild = null
+                initialTouchY = y
+                initialTouchX = x
+            }
+            MotionEvent.ACTION_MOVE -> {
+                val h = y - initialTouchY
+                // Adjust the touch slop if another gesture may be being performed.
+                val touchSlop = if (event.classification
+                        == MotionEvent.CLASSIFICATION_AMBIGUOUS_GESTURE)
+                    touchSlop * slopMultiplier
+                else
+                    touchSlop
+                if (h > touchSlop && h > Math.abs(x - initialTouchX)) {
+                    falsingCollector.onNotificationStartDraggingDown()
+                    isDraggingDown = true
+                    captureStartingChild(initialTouchX, initialTouchY)
+                    initialTouchY = y
+                    initialTouchX = x
+                    dragDownCallback.onDragDownStarted()
+                    dragDownAmountOnStart = dragDownCallback.dragDownAmount
+                    return startingChild != null || dragDownCallback.isDragDownAnywhereEnabled
+                }
+            }
+        }
+        return false
+    }
+
+    override fun onTouchEvent(event: MotionEvent): Boolean {
+        if (!isDraggingDown) {
+            return false
+        }
+        val x = event.x
+        val y = event.y
+        when (event.actionMasked) {
+            MotionEvent.ACTION_MOVE -> {
+                lastHeight = y - initialTouchY
+                captureStartingChild(initialTouchX, initialTouchY)
+                dragDownCallback.dragDownAmount = lastHeight + dragDownAmountOnStart
+                if (startingChild != null) {
+                    handleExpansion(lastHeight, startingChild!!)
+                }
+                if (lastHeight > minDragDistance) {
+                    if (!draggedFarEnough) {
+                        draggedFarEnough = true
+                        dragDownCallback.onCrossedThreshold(true)
+                    }
+                } else {
+                    if (draggedFarEnough) {
+                        draggedFarEnough = false
+                        dragDownCallback.onCrossedThreshold(false)
+                    }
+                }
+                return true
+            }
+            MotionEvent.ACTION_UP -> if (!falsingManager.isUnlockingDisabled && !isFalseTouch &&
+                    dragDownCallback.canDragDown()) {
+                dragDownCallback.onDraggedDown(startingChild, (y - initialTouchY).toInt())
+                if (startingChild != null) {
+                    expandCallback.setUserLockedChild(startingChild, false)
+                    startingChild = null
+                }
+                isDraggingDown = false
+            } else {
+                stopDragging()
+                return false
+            }
+            MotionEvent.ACTION_CANCEL -> {
+                stopDragging()
+                return false
+            }
+        }
+        return false
+    }
+
+    private fun captureStartingChild(x: Float, y: Float) {
+        if (startingChild == null) {
+            startingChild = findView(x, y)
+            if (startingChild != null) {
+                if (dragDownCallback.isDragDownEnabledForView(startingChild)) {
+                    expandCallback.setUserLockedChild(startingChild, true)
+                } else {
+                    startingChild = null
+                }
+            }
+        }
+    }
+
+    private fun handleExpansion(heightDelta: Float, child: ExpandableView) {
+        var hDelta = heightDelta
+        if (hDelta < 0) {
+            hDelta = 0f
+        }
+        val expandable = child.isContentExpandable
+        val rubberbandFactor = if (expandable) {
+            RUBBERBAND_FACTOR_EXPANDABLE
+        } else {
+            RUBBERBAND_FACTOR_STATIC
+        }
+        var rubberband = hDelta * rubberbandFactor
+        if (expandable && rubberband + child.collapsedHeight > child.maxContentHeight) {
+            var overshoot = rubberband + child.collapsedHeight - child.maxContentHeight
+            overshoot *= 1 - RUBBERBAND_FACTOR_STATIC
+            rubberband -= overshoot
+        }
+        child.actualHeight = (child.collapsedHeight + rubberband).toInt()
+    }
+
+    private fun cancelChildExpansion(child: ExpandableView) {
+        if (child.actualHeight == child.collapsedHeight) {
+            expandCallback.setUserLockedChild(child, false)
+            return
+        }
+        val anim = ObjectAnimator.ofInt(child, "actualHeight",
+                child.actualHeight, child.collapsedHeight)
+        anim.interpolator = Interpolators.FAST_OUT_SLOW_IN
+        anim.duration = SPRING_BACK_ANIMATION_LENGTH_MS
+        anim.addListener(object : AnimatorListenerAdapter() {
+            override fun onAnimationEnd(animation: Animator) {
+                expandCallback.setUserLockedChild(child, false)
+            }
+        })
+        anim.start()
+    }
+
+    private fun stopDragging() {
+        falsingCollector.onNotificationStopDraggingDown()
+        if (startingChild != null) {
+            cancelChildExpansion(startingChild!!)
+            startingChild = null
+        }
+        isDraggingDown = false
+        dragDownCallback.onDragDownReset()
+    }
+
+    private fun findView(x: Float, y: Float): ExpandableView? {
+        host.getLocationOnScreen(temp2)
+        return expandCallback.getChildAtRawPosition(x + temp2[0], y + temp2[1])
+    }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/PulseExpansionHandler.kt b/packages/SystemUI/src/com/android/systemui/statusbar/PulseExpansionHandler.kt
index 84465a8..9765ace 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/PulseExpansionHandler.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/PulseExpansionHandler.kt
@@ -42,7 +42,6 @@
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
 import com.android.systemui.statusbar.phone.HeadsUpManagerPhone
 import com.android.systemui.statusbar.phone.KeyguardBypassController
-import com.android.systemui.statusbar.phone.ShadeController
 import javax.inject.Inject
 import kotlin.math.max
 
@@ -59,6 +58,7 @@
     private val roundnessManager: NotificationRoundnessManager,
     private val statusBarStateController: StatusBarStateController,
     private val falsingManager: FalsingManager,
+    private val lockscreenShadeTransitionController: LockscreenShadeTransitionController,
     private val falsingCollector: FalsingCollector
 ) : Gefingerpoken {
     companion object {
@@ -66,7 +66,6 @@
         private val SPRING_BACK_ANIMATION_LENGTH_MS = 375
     }
     private val mPowerManager: PowerManager?
-    private lateinit var shadeController: ShadeController
 
     private val mMinDragDistance: Int
     private var mInitialTouchX: Float = 0.0f
@@ -95,7 +94,7 @@
     var leavingLockscreen: Boolean = false
         private set
     private val mTouchSlop: Float
-    private lateinit var expansionCallback: ExpansionCallback
+    private lateinit var overStretchHandler: OverStretchHandler
     private lateinit var stackScrollerController: NotificationStackScrollLayoutController
     private val mTemp2 = IntArray(2)
     private var mDraggedFarEnough: Boolean = false
@@ -103,7 +102,7 @@
     private var mPulsing: Boolean = false
     var isWakingToShadeLocked: Boolean = false
         private set
-    private var mEmptyDragAmount: Float = 0.0f
+    private var overStretchAmount: Float = 0.0f
     private var mWakeUpHeight: Float = 0.0f
     private var mReachedWakeUpHeight: Boolean = false
     private var velocityTracker: VelocityTracker? = null
@@ -215,6 +214,7 @@
 
     private fun finishExpansion() {
         resetClock()
+        val startingChild = mStartingChild
         if (mStartingChild != null) {
             setUserLocked(mStartingChild!!, false)
             mStartingChild = null
@@ -225,7 +225,8 @@
             mPowerManager!!.wakeUp(SystemClock.uptimeMillis(), WAKE_REASON_GESTURE,
                     "com.android.systemui:PULSEDRAG")
         }
-        shadeController.goToLockedShade(mStartingChild)
+        lockscreenShadeTransitionController.goToLockedShade(startingChild,
+                needsQSAnimation = false)
         leavingLockscreen = true
         isExpanding = false
         if (mStartingChild is ExpandableNotificationRow) {
@@ -252,8 +253,8 @@
                     true /* increaseSpeed */)
             expansionHeight = max(mWakeUpHeight, expansionHeight)
         }
-        val emptyDragAmount = wakeUpCoordinator.setPulseHeight(expansionHeight)
-        setEmptyDragAmount(emptyDragAmount * RUBBERBAND_FACTOR_STATIC)
+        val dragDownAmount = wakeUpCoordinator.setPulseHeight(expansionHeight)
+        setOverStretchAmount(dragDownAmount)
     }
 
     private fun captureStartingChild(x: Float, y: Float) {
@@ -265,9 +266,9 @@
         }
     }
 
-    private fun setEmptyDragAmount(amount: Float) {
-        mEmptyDragAmount = amount
-        expansionCallback.setEmptyDragAmount(amount)
+    private fun setOverStretchAmount(amount: Float) {
+        overStretchAmount = amount
+        overStretchHandler.setOverStretchAmount(amount)
     }
 
     private fun reset(child: ExpandableView) {
@@ -294,10 +295,12 @@
     }
 
     private fun resetClock() {
-        val anim = ValueAnimator.ofFloat(mEmptyDragAmount, 0f)
+        val anim = ValueAnimator.ofFloat(overStretchAmount, 0f)
         anim.interpolator = Interpolators.FAST_OUT_SLOW_IN
         anim.duration = SPRING_BACK_ANIMATION_LENGTH_MS.toLong()
-        anim.addUpdateListener { animation -> setEmptyDragAmount(animation.animatedValue as Float) }
+        anim.addUpdateListener {
+            animation -> setOverStretchAmount(animation.animatedValue as Float)
+        }
         anim.start()
     }
 
@@ -329,11 +332,9 @@
 
     fun setUp(
         stackScrollerController: NotificationStackScrollLayoutController,
-        expansionCallback: ExpansionCallback,
-        shadeController: ShadeController
+        overStrechHandler: OverStretchHandler
     ) {
-        this.expansionCallback = expansionCallback
-        this.shadeController = shadeController
+        this.overStretchHandler = overStrechHandler
         this.stackScrollerController = stackScrollerController
     }
 
@@ -345,7 +346,11 @@
         isWakingToShadeLocked = false
     }
 
-    interface ExpansionCallback {
-        fun setEmptyDragAmount(amount: Float)
+    interface OverStretchHandler {
+
+        /**
+         * Set the overstretch amount in pixels This will be rubberbanded later
+         */
+        fun setOverStretchAmount(amount: Float)
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java
index caf4720..66d2347 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java
@@ -95,6 +95,7 @@
 
     /** Height of the notifications panel without top padding when expansion completes. */
     private float mStackEndHeight;
+    private float mTransitionToFullShadeAmount;
 
     /**
      * @return Height of the notifications panel without top padding when expansion completes.
@@ -595,6 +596,21 @@
     }
 
     /**
+     * Set the amount of pixels we have currently dragged down if we're transitioning to the full
+     * shade. 0.0f means we're not transitioning yet.
+     */
+    public void setTransitionToFullShadeAmount(float transitionToFullShadeAmount) {
+        mTransitionToFullShadeAmount = transitionToFullShadeAmount;
+    }
+
+    /**
+     * get
+     */
+    public float getTransitionToFullShadeAmount() {
+        return mTransitionToFullShadeAmount;
+    }
+
+    /**
      * Returns the currently tracked heads up row, if there is one and it is currently above the
      * shelf (still appearing).
      */
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 4dbdb13..3244ff9 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
@@ -46,7 +46,6 @@
 import android.os.UserHandle;
 import android.provider.Settings;
 import android.util.AttributeSet;
-import android.util.DisplayMetrics;
 import android.util.Log;
 import android.util.MathUtils;
 import android.util.Pair;
@@ -71,17 +70,14 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.graphics.ColorUtils;
 import com.android.internal.jank.InteractionJankMonitor;
-import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
 import com.android.keyguard.KeyguardSliceView;
 import com.android.settingslib.Utils;
-import com.android.systemui.Dependency;
 import com.android.systemui.Dumpable;
 import com.android.systemui.ExpandHelper;
 import com.android.systemui.R;
 import com.android.systemui.animation.Interpolators;
 import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper;
 import com.android.systemui.statusbar.CommandQueue;
-import com.android.systemui.statusbar.DragDownHelper.DragDownCallback;
 import com.android.systemui.statusbar.EmptyShadeView;
 import com.android.systemui.statusbar.FeatureFlags;
 import com.android.systemui.statusbar.NotificationRemoteInputManager;
@@ -89,7 +85,6 @@
 import com.android.systemui.statusbar.NotificationShelfController;
 import com.android.systemui.statusbar.RemoteInputController;
 import com.android.systemui.statusbar.StatusBarState;
-import com.android.systemui.statusbar.SysuiStatusBarStateController;
 import com.android.systemui.statusbar.notification.ExpandAnimationParameters;
 import com.android.systemui.statusbar.notification.FakeShadowView;
 import com.android.systemui.statusbar.notification.NotificationActivityStarter;
@@ -108,9 +103,6 @@
 import com.android.systemui.statusbar.notification.row.StackScrollerDecorView;
 import com.android.systemui.statusbar.phone.HeadsUpAppearanceController;
 import com.android.systemui.statusbar.phone.HeadsUpTouchHelper;
-import com.android.systemui.statusbar.phone.LockscreenGestureLogger;
-import com.android.systemui.statusbar.phone.LockscreenGestureLogger.LockscreenUiEvent;
-import com.android.systemui.statusbar.phone.NotificationPanelViewController;
 import com.android.systemui.statusbar.phone.ShadeController;
 import com.android.systemui.statusbar.phone.StatusBar;
 import com.android.systemui.statusbar.policy.HeadsUpUtil;
@@ -151,7 +143,6 @@
      */
     private static final int DISTANCE_BETWEEN_ADJACENT_SECTIONS_PX = 1;
     private KeyguardBypassEnabledProvider mKeyguardBypassEnabledProvider;
-    private final SysuiStatusBarStateController mStatusbarStateController;
 
     private ExpandHelper mExpandHelper;
     private NotificationSwipeHelper mSwipeHelper;
@@ -433,13 +424,9 @@
     private ShadeController mShadeController;
     private Runnable mOnStackYChanged;
 
-    private final DisplayMetrics mDisplayMetrics = Dependency.get(DisplayMetrics.class);
-    private final LockscreenGestureLogger mLockscreenGestureLogger =
-            Dependency.get(LockscreenGestureLogger.class);
     protected boolean mClearAllEnabled;
 
     private Interpolator mHideXInterpolator = Interpolators.FAST_OUT_SLOW_IN;
-    private NotificationPanelViewController mNotificationPanelController;
 
     private final NotificationSectionsManager mSectionsManager;
     private ForegroundServiceDungeonView mFgsSectionView;
@@ -449,6 +436,11 @@
     private boolean mWillExpand;
     private int mGapHeight;
 
+    /**
+     * The extra inset during the full shade transition
+     */
+    private float mExtraTopInsetForFullShadeTransition;
+
     private int mWaterfallTopInset;
     private NotificationStackScrollLayoutController mController;
 
@@ -496,7 +488,6 @@
             NotificationSectionsManager notificationSectionsManager,
             GroupMembershipManager groupMembershipManager,
             GroupExpansionManager groupExpansionManager,
-            SysuiStatusBarStateController statusbarStateController,
             AmbientState ambientState,
             FeatureFlags featureFlags) {
         super(context, attrs, 0, 0);
@@ -535,7 +526,6 @@
         mClearAllEnabled = res.getBoolean(R.bool.config_enableNotificationsClearAll);
         mGroupMembershipManager = groupMembershipManager;
         mGroupExpansionManager = groupExpansionManager;
-        mStatusbarStateController = statusbarStateController;
     }
 
     void initializeForegroundServiceSection(ForegroundServiceDungeonView fgsSectionView) {
@@ -1142,8 +1132,9 @@
      */
     private void updateStackPosition() {
         // Consider interpolating from an mExpansionStartY for use on lockscreen and AOD
+        float endTopPosition = mTopPadding + mExtraTopInsetForFullShadeTransition;
         final float fraction = mAmbientState.getExpansionFraction();
-        final float stackY = MathUtils.lerp(0, mTopPadding, fraction);
+        final float stackY = MathUtils.lerp(0, endTopPosition, fraction);
         mAmbientState.setStackY(stackY);
         if (mOnStackYChanged != null) {
             mOnStackYChanged.run();
@@ -4278,6 +4269,13 @@
                 + mGapHeight;
     }
 
+    /**
+     * @return the padding after the media header on the lockscreen
+     */
+    public int getPaddingAfterMedia() {
+        return mGapHeight + mPaddingBetweenElements;
+    }
+
     @ShadeViewRefactor(RefactorComponent.SHADE_VIEW)
     public int getEmptyShadeViewHeight() {
         return mEmptyShadeView.getHeight();
@@ -4933,12 +4931,6 @@
                 getChildCount() - offsetFromEnd);
     }
 
-    @ShadeViewRefactor(RefactorComponent.SHADE_VIEW)
-    public void setNotificationPanelController(
-            NotificationPanelViewController notificationPanelViewController) {
-        mNotificationPanelController = notificationPanelViewController;
-    }
-
     /**
      * Set how far the wake up is when waking up from pulsing. This is a height and will adjust the
      * notification positions accordingly.
@@ -5155,6 +5147,16 @@
     }
 
     /**
+     * Sets the extra top inset for the full shade transition. This is needed to compensate for
+     * media transitioning to quick settings
+     */
+    public void setExtraTopInsetForFullShadeTransition(float inset) {
+        mExtraTopInsetForFullShadeTransition = inset;
+        updateStackPosition();
+        requestChildrenUpdate();
+    }
+
+    /**
      * A listener that is notified when the empty space below the notifications is clicked on
      */
     @ShadeViewRefactor(RefactorComponent.SHADE_VIEW)
@@ -5526,108 +5528,10 @@
         }
     }
 
-    public void setKeyguardMediaControllorVisible(boolean keyguardMediaControllorVisible) {
-        mKeyguardMediaControllorVisible = keyguardMediaControllorVisible;
-    }
-
     void resetCheckSnoozeLeavebehind() {
         setCheckForLeaveBehind(true);
     }
 
-    // ---------------------- DragDownHelper.OnDragDownListener ------------------------------------
-
-    @ShadeViewRefactor(RefactorComponent.INPUT)
-    private final DragDownCallback mDragDownCallback = new DragDownCallback() {
-
-        @Override
-        public boolean canDragDown() {
-            return mStatusBarState == StatusBarState.KEYGUARD
-                    && (mController.hasActiveNotifications() || mKeyguardMediaControllorVisible)
-                    || mController.isInLockedDownShade();
-        }
-
-        /* Only ever called as a consequence of a lockscreen expansion gesture. */
-        @Override
-        public void onDraggedDown(View startingChild, int dragLengthY) {
-            boolean canDragDown =
-                    mController.hasActiveNotifications() || mKeyguardMediaControllorVisible;
-            if (mStatusBarState == StatusBarState.KEYGUARD && canDragDown) {
-                mLockscreenGestureLogger.write(
-                        MetricsEvent.ACTION_LS_SHADE,
-                        (int) (dragLengthY / mDisplayMetrics.density),
-                        0 /* velocityDp - N/A */);
-                mLockscreenGestureLogger.log(LockscreenUiEvent.LOCKSCREEN_PULL_SHADE_OPEN);
-
-                if (!mAmbientState.isDozing() || startingChild != null) {
-                    // We have notifications, go to locked shade.
-                    mShadeController.goToLockedShade(startingChild);
-                    if (startingChild instanceof ExpandableNotificationRow) {
-                        ExpandableNotificationRow row = (ExpandableNotificationRow) startingChild;
-                        row.onExpandedByGesture(true /* drag down is always an open */);
-                    }
-                }
-            } else if (mController.isInLockedDownShade()) {
-                mStatusbarStateController.setLeaveOpenOnKeyguardHide(true);
-                mStatusBar.dismissKeyguardThenExecute(() -> false /* dismissAction */,
-                        null /* cancelRunnable */, false /* afterKeyguardGone */);
-            }
-        }
-
-        @Override
-        public void onDragDownReset() {
-            setDimmed(true /* dimmed */, true /* animated */);
-            resetScrollPosition();
-            resetCheckSnoozeLeavebehind();
-        }
-
-        @Override
-        public void onCrossedThreshold(boolean above) {
-            setDimmed(!above /* dimmed */, true /* animate */);
-        }
-
-        @Override
-        public void onTouchSlopExceeded() {
-            cancelLongPress();
-            mController.checkSnoozeLeavebehind();
-        }
-
-        @Override
-        public void setEmptyDragAmount(float amount) {
-            mNotificationPanelController.setEmptyDragAmount(amount);
-        }
-
-        @Override
-        public boolean isFalsingCheckNeeded() {
-            return mStatusBarState == StatusBarState.KEYGUARD;
-        }
-
-        @Override
-        public boolean isDragDownEnabledForView(ExpandableView view) {
-            if (isDragDownAnywhereEnabled()) {
-                return true;
-            }
-            if (mController.isInLockedDownShade()) {
-                if (view == null) {
-                    // Dragging down is allowed in general
-                    return true;
-                }
-                if (view instanceof ExpandableNotificationRow) {
-                    // Only drag down on sensitive views, otherwise the ExpandHelper will take this
-                    return ((ExpandableNotificationRow) view).getEntry().isSensitive();
-                }
-            }
-            return false;
-        }
-
-        @Override
-        public boolean isDragDownAnywhereEnabled() {
-            return mStatusbarStateController.getState() == StatusBarState.KEYGUARD
-                    && !mKeyguardBypassEnabledProvider.getBypassEnabled();
-        }
-    };
-
-    public DragDownCallback getDragDownCallback() { return mDragDownCallback; }
-
     @ShadeViewRefactor(RefactorComponent.SHADE_VIEW)
     private final HeadsUpTouchHelper.Callback mHeadsUpCallback = new HeadsUpTouchHelper.Callback() {
         @Override
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 f7eb574..0d42428 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
@@ -31,6 +31,7 @@
 import static com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.canChildBeDismissed;
 import static com.android.systemui.statusbar.phone.NotificationIconAreaController.HIGH_PRIORITY;
 
+import android.content.res.Configuration;
 import android.content.res.Resources;
 import android.graphics.Point;
 import android.graphics.PointF;
@@ -38,6 +39,7 @@
 import android.service.notification.NotificationListenerService;
 import android.service.notification.StatusBarNotification;
 import android.util.Log;
+import android.util.MathUtils;
 import android.util.Pair;
 import android.view.Display;
 import android.view.LayoutInflater;
@@ -59,6 +61,7 @@
 import com.android.systemui.Gefingerpoken;
 import com.android.systemui.R;
 import com.android.systemui.SwipeHelper;
+import com.android.systemui.animation.Interpolators;
 import com.android.systemui.classifier.Classifier;
 import com.android.systemui.classifier.FalsingCollector;
 import com.android.systemui.colorextraction.SysuiColorExtractor;
@@ -70,6 +73,7 @@
 import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.statusbar.FeatureFlags;
+import com.android.systemui.statusbar.LockscreenShadeTransitionController;
 import com.android.systemui.statusbar.NotificationLockscreenUserManager;
 import com.android.systemui.statusbar.NotificationLockscreenUserManager.UserChangedListener;
 import com.android.systemui.statusbar.NotificationRemoteInputManager;
@@ -107,7 +111,6 @@
 import com.android.systemui.statusbar.phone.HeadsUpManagerPhone;
 import com.android.systemui.statusbar.phone.HeadsUpTouchHelper;
 import com.android.systemui.statusbar.phone.KeyguardBypassController;
-import com.android.systemui.statusbar.phone.NotificationPanelViewController;
 import com.android.systemui.statusbar.phone.ScrimController;
 import com.android.systemui.statusbar.phone.ShadeController;
 import com.android.systemui.statusbar.phone.StatusBar;
@@ -168,6 +171,7 @@
     // TODO: StatusBar should be encapsulated behind a Controller
     private final StatusBar mStatusBar;
     private final SectionHeaderController mSilentHeaderController;
+    private final LockscreenShadeTransitionController mLockscreenShadeTransitionController;
 
     private NotificationStackScrollLayout mView;
     private boolean mFadeNotificationsOnDismiss;
@@ -181,6 +185,9 @@
 
     private ColorExtractor.OnColorsChangedListener mOnColorsChangedListener;
 
+    private int mTotalDistanceForFullShadeTransition;
+    private int mTotalExtraMediaInsetFullShadeTransition;
+
     @VisibleForTesting
     final View.OnAttachStateChangeListener mOnAttachStateChangeListener =
             new View.OnAttachStateChangeListener() {
@@ -240,8 +247,20 @@
         public void onThemeChanged() {
             updateFooter();
         }
+
+        @Override
+        public void onConfigChanged(Configuration newConfig) {
+            updateResources();
+        }
     };
 
+    private void updateResources() {
+        mTotalExtraMediaInsetFullShadeTransition = mResources.getDimensionPixelSize(
+                R.dimen.lockscreen_shade_transition_extra_media_inset);
+        mTotalDistanceForFullShadeTransition = mResources.getDimensionPixelSize(
+                R.dimen.lockscreen_shade_qs_transition_distance);
+    }
+
     private final StatusBarStateController.StateListener mStateListener =
             new StatusBarStateController.StateListener() {
                 @Override
@@ -571,6 +590,7 @@
             NotifPipeline notifPipeline,
             NotifCollection notifCollection,
             NotificationEntryManager notificationEntryManager,
+            LockscreenShadeTransitionController lockscreenShadeTransitionController,
             IStatusBarService iStatusBarService,
             UiEventLogger uiEventLogger,
             ForegroundServiceDismissalFeatureController fgFeatureController,
@@ -592,6 +612,7 @@
         mZenModeController = zenModeController;
         mLockscreenUserManager = lockscreenUserManager;
         mMetricsLogger = metricsLogger;
+        mLockscreenShadeTransitionController = lockscreenShadeTransitionController;
         mFalsingCollector = falsingCollector;
         mFalsingManager = falsingManager;
         mResources = resources;
@@ -624,6 +645,7 @@
         mRemoteInputManager = remoteInputManager;
         mVisualStabilityManager = visualStabilityManager;
         mShadeController = shadeController;
+        updateResources();
     }
 
     public void attach(NotificationStackScrollLayout view) {
@@ -676,6 +698,8 @@
 
         mScrimController.setScrimBehindChangeRunnable(mView::updateBackgroundDimming);
 
+        mLockscreenShadeTransitionController.setStackScroller(this);
+
         mLockscreenUserManager.addUserChangedListener(mLockscreenUserChangeListener);
 
         mFadeNotificationsOnDismiss =  // TODO: this should probably be injected directly
@@ -705,7 +729,6 @@
                 Settings.Secure.NOTIFICATION_HISTORY_ENABLED);
 
         mKeyguardMediaController.setVisibilityChangedListener(visible -> {
-            mView.setKeyguardMediaControllorVisible(visible);
             if (visible) {
                 mView.generateAddAnimation(
                         mKeyguardMediaController.getSinglePaneContainer(),
@@ -1203,11 +1226,6 @@
         mView.runAfterAnimationFinished(r);
     }
 
-    public void setNotificationPanelController(
-            NotificationPanelViewController notificationPanelViewController) {
-        mView.setNotificationPanelController(notificationPanelViewController);
-    }
-
     public void setShelfController(NotificationShelfController notificationShelfController) {
         mView.setShelfController(notificationShelfController);
     }
@@ -1275,7 +1293,10 @@
                         NotificationLogger.getNotificationLocation(entry)));
     }
 
-    boolean hasActiveNotifications() {
+    /**
+     * @return if the shade has currently any active notifications.
+     */
+    public boolean hasActiveNotifications() {
         if (mFeatureFlags.isNewNotifPipelineRenderingEnabled()) {
             return !mNotifPipeline.getShadeList().isEmpty();
         } else {
@@ -1354,11 +1375,60 @@
         }
     }
 
+    /**
+     * @return the expand helper callback.
+     */
+    public ExpandHelper.Callback getExpandHelperCallback() {
+        return mView.getExpandHelperCallback();
+    }
+
+    /**
+     * @return If the shade is in the locked down shade.
+     */
     public boolean isInLockedDownShade() {
         return mDynamicPrivacyController.isInLockedDownShade();
     }
 
     /**
+     * Set the dimmed state for all of the notification views.
+     */
+    public void setDimmed(boolean dimmed, boolean animate) {
+        mView.setDimmed(dimmed, animate);
+    }
+
+    /**
+     * @return the inset during the full shade transition, that needs to be added to the position
+     *         of the quick settings edge. This is relevant for media, that is transitioning
+     *         from the keyguard host to the quick settings one.
+     */
+    public int getFullShadeTransitionInset() {
+        MediaHeaderView view = mKeyguardMediaController.getSinglePaneContainer();
+        if (view == null || view.getHeight() == 0
+                || mStatusBarStateController.getState() != KEYGUARD) {
+            return 0;
+        }
+        return view.getHeight() + mView.getPaddingAfterMedia();
+    }
+
+    /**
+     * Set the amount of pixels we have currently dragged down if we're transitioning to the full
+     * 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;
+        }
+        mView.setExtraTopInsetForFullShadeTransition(extraTopInset);
+    }
+
+    /**
      * Enum for UiEvent logged from this class
      */
     enum NotificationPanelEvent implements UiEventLogger.UiEventEnum {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.java
index 426869e..7f919b5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.java
@@ -257,7 +257,6 @@
         super.onFinishInflate();
         mPreviewInflater = new PreviewInflater(mContext, new LockPatternUtils(mContext),
                 new ActivityIntentHelper(mContext));
-        mPreviewContainer = findViewById(R.id.preview_container);
         mOverlayContainer = findViewById(R.id.overlay_container);
         mRightAffordanceView = findViewById(R.id.camera_button);
         mLeftAffordanceView = findViewById(R.id.left_button);
@@ -274,7 +273,6 @@
         mKeyguardStateController.addCallback(this);
         setClipChildren(false);
         setClipToPadding(false);
-        inflateCameraPreview();
         mRightAffordanceView.setOnClickListener(this);
         mLeftAffordanceView.setOnClickListener(this);
         initAccessibility();
@@ -282,13 +280,21 @@
         mFlashlightController = Dependency.get(FlashlightController.class);
         mAccessibilityController = Dependency.get(AccessibilityController.class);
         mActivityIntentHelper = new ActivityIntentHelper(getContext());
-        updateLeftAffordance();
 
         mIndicationPadding = getResources().getDimensionPixelSize(
                 R.dimen.keyguard_indication_area_padding);
         updateWalletVisibility();
     }
 
+    /**
+     * Set the container where the previews are rendered.
+     */
+    public void setPreviewContainer(ViewGroup previewContainer) {
+        mPreviewContainer = previewContainer;
+        inflateCameraPreview();
+        updateLeftAffordance();
+    }
+
     @Override
     protected void onAttachedToWindow() {
         super.onAttachedToWindow();
@@ -689,6 +695,9 @@
     }
 
     private void inflateCameraPreview() {
+        if (mPreviewContainer == null) {
+            return;
+        }
         View previewBefore = mCameraPreview;
         boolean visibleBefore = false;
         if (previewBefore != null) {
@@ -706,6 +715,9 @@
     }
 
     private void updateLeftPreview() {
+        if (mPreviewContainer == null) {
+            return;
+        }
         View previewBefore = mLeftPreview;
         if (previewBefore != null) {
             mPreviewContainer.removeView(previewBefore);
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 069c197..f4710f4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithm.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithm.java
@@ -140,7 +140,7 @@
      */
     private float mQsExpansion;
 
-    private float mEmptyDragAmount;
+    private float mOverStretchAmount;
 
     /**
      * Setting if bypass is enabled. If true the clock should always be positioned like it's dark
@@ -181,7 +181,7 @@
             int notificationStackHeight, float panelExpansion, int parentHeight,
             int keyguardStatusHeight, int userSwitchHeight, int clockPreferredY,
             int userSwitchPreferredY, boolean hasCustomClock, boolean hasVisibleNotifs, float dark,
-            float emptyDragAmount, boolean bypassEnabled, int unlockedStackScrollerPadding,
+            float overStrechAmount, boolean bypassEnabled, int unlockedStackScrollerPadding,
             float qsExpansion, int cutoutTopInset, boolean isSplitShade) {
         mMinTopMargin = keyguardStatusBarHeaderHeight + Math.max(mContainerTopPadding,
                 userSwitchHeight);
@@ -196,7 +196,7 @@
         mHasCustomClock = hasCustomClock;
         mHasVisibleNotifs = hasVisibleNotifs;
         mDarkAmount = dark;
-        mEmptyDragAmount = emptyDragAmount;
+        mOverStretchAmount = overStrechAmount;
         mBypassEnabled = bypassEnabled;
         mUnlockedStackScrollerPadding = unlockedStackScrollerPadding;
         mQsExpansion = qsExpansion;
@@ -301,7 +301,7 @@
             }
             clockYDark = clockY + burnInPreventionOffsetY() + shift;
         }
-        return (int) (MathUtils.lerp(clockY, clockYDark, darkAmount) + mEmptyDragAmount);
+        return (int) (MathUtils.lerp(clockY, clockYDark, darkAmount) + mOverStretchAmount);
     }
 
     private int getUserSwitcherY(float panelExpansion) {
@@ -312,7 +312,7 @@
         float shadeExpansion = Interpolators.FAST_OUT_LINEAR_IN.getInterpolation(panelExpansion);
         float userSwitchY = MathUtils.lerp(userSwitchYBouncer, userSwitchYRegular, shadeExpansion);
 
-        return (int) (userSwitchY + mEmptyDragAmount);
+        return (int) (userSwitchY + mOverStretchAmount);
     }
 
     /**
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 2cb1700..9d8a9bf 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java
@@ -114,6 +114,7 @@
 import com.android.systemui.statusbar.GestureRecorder;
 import com.android.systemui.statusbar.KeyguardAffordanceView;
 import com.android.systemui.statusbar.KeyguardIndicationController;
+import com.android.systemui.statusbar.LockscreenShadeTransitionController;
 import com.android.systemui.statusbar.NotificationLockscreenUserManager;
 import com.android.systemui.statusbar.NotificationShadeDepthController;
 import com.android.systemui.statusbar.NotificationShelfController;
@@ -208,7 +209,6 @@
     private final ConfigurationListener mConfigurationListener = new ConfigurationListener();
     @VisibleForTesting final StatusBarStateListener mStatusBarStateListener =
             new StatusBarStateListener();
-    private final ExpansionCallback mExpansionCallback = new ExpansionCallback();
     private final BiometricUnlockController mBiometricUnlockController;
     private final NotificationPanelView mView;
     private final VibratorHelper mVibratorHelper;
@@ -313,10 +313,12 @@
     // Maximum # notifications to show on Keyguard; extras will be collapsed in an overflow card.
     // If there are exactly 1 + mMaxKeyguardNotifications, then still shows all notifications
     private final int mMaxKeyguardNotifications;
+    private final LockscreenShadeTransitionController mLockscreenShadeTransitionController;
     private boolean mShouldUseSplitNotificationShade;
     // Current max allowed keyguard notifications determined by measuring the panel
     private int mMaxAllowedKeyguardNotifications;
 
+    private ViewGroup mPreviewContainer;
     private KeyguardAffordanceHelper mAffordanceHelper;
     private KeyguardQsUserSwitchController mKeyguardQsUserSwitchController;
     private KeyguardUserSwitcherController mKeyguardUserSwitcherController;
@@ -366,7 +368,7 @@
     private int mStatusBarMinHeight;
     private int mStatusBarHeaderHeightKeyguard;
     private int mNotificationsHeaderCollideDistance;
-    private float mEmptyDragAmount;
+    private float mOverStretchAmount;
     private float mDownX;
     private float mDownY;
     private int mDisplayCutoutTopInset = 0; // in pixels
@@ -482,7 +484,6 @@
     private final CommandQueue mCommandQueue;
     private final NotificationLockscreenUserManager mLockscreenUserManager;
     private final UserManager mUserManager;
-    private final ShadeController mShadeController;
     private final MediaDataManager mMediaDataManager;
     private NotificationShadeDepthController mDepthController;
     private int mDisplayId;
@@ -506,6 +507,39 @@
     private float mSectionPadding;
 
     /**
+     * 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!
+     */
+    private float mTransitioningToFullShadeProgress;
+
+    /**
+     * Position of the qs bottom during the full shade transition. This is needed as the toppadding
+     * can change during state changes, which makes it much harder to do animations
+     */
+    private int mTransitionToFullShadeQSPosition;
+
+    /**
+     * Distance that the full shade transition takes in order for qs to fully transition to the
+     * shade.
+     */
+    private int mDistanceForQSFullShadeTransition;
+
+    /**
+     * The maximum overshoot allowed for the top padding for the full shade transition
+     */
+    private int mMaxOverscrollAmountForDragDown;
+
+    /**
+     * Should we animate the next bounds update
+     */
+    private boolean mAnimateNextNotificationBounds;
+    /**
+     * The delay for the next bounds animation
+     */
+    private long mNotificationBoundsAnimationDelay;
+
+    /**
      * Is this a collapse that started on the panel where we should allow the panel to intercept
      */
     private boolean mIsPanelCollapseOnQQS;
@@ -522,6 +556,16 @@
     private boolean mDelayShowingKeyguardStatusBar;
 
     private boolean mAnimatingQS;
+
+    /**
+     * The end bounds of a clipping animation.
+     */
+    private final Rect mQsClippingAnimationEndBounds = new Rect();
+
+    /**
+     * The animator for the qs clipping bounds.
+     */
+    private ValueAnimator mQsClippingAnimation = null;
     private final Rect mKeyguardStatusAreaClipBounds = new Rect();
     private int mOldLayoutDirection;
     private NotificationShelfController mNotificationShelfController;
@@ -571,7 +615,7 @@
             NotificationWakeUpCoordinator coordinator, PulseExpansionHandler pulseExpansionHandler,
             DynamicPrivacyController dynamicPrivacyController,
             KeyguardBypassController bypassController, FalsingManager falsingManager,
-            FalsingCollector falsingCollector, ShadeController shadeController,
+            FalsingCollector falsingCollector,
             NotificationLockscreenUserManager notificationLockscreenUserManager,
             NotificationEntryManager notificationEntryManager,
             KeyguardStateController keyguardStateController,
@@ -593,6 +637,7 @@
             KeyguardQsUserSwitchComponent.Factory keyguardQsUserSwitchComponentFactory,
             KeyguardUserSwitcherComponent.Factory keyguardUserSwitcherComponentFactory,
             KeyguardStatusBarViewComponent.Factory keyguardStatusBarViewComponentFactory,
+            LockscreenShadeTransitionController lockscreenShadeTransitionController,
             QSDetailDisplayer qsDetailDisplayer,
             NotificationGroupManagerLegacy groupManager,
             NotificationIconAreaController notificationIconAreaController,
@@ -681,6 +726,8 @@
                         }
                     }
                 };
+        mLockscreenShadeTransitionController = lockscreenShadeTransitionController;
+        lockscreenShadeTransitionController.setNotificationPanelController(this);
         mKeyguardStateController.addCallback(keyguardMonitorCallback);
         DynamicPrivacyControlListener
                 dynamicPrivacyControlListener =
@@ -694,7 +741,6 @@
         });
         mBottomAreaShadeAlphaAnimator.setDuration(160);
         mBottomAreaShadeAlphaAnimator.setInterpolator(Interpolators.ALPHA_OUT);
-        mShadeController = shadeController;
         mLockscreenUserManager = notificationLockscreenUserManager;
         mEntryManager = notificationEntryManager;
         mConversationNotificationManager = conversationNotificationManager;
@@ -753,14 +799,22 @@
                 mOnEmptySpaceClickListener);
         addTrackingHeadsUpListener(mNotificationStackScrollLayoutController::setTrackingHeadsUp);
         mKeyguardBottomArea = mView.findViewById(R.id.keyguard_bottom_area);
+        mPreviewContainer = mView.findViewById(R.id.preview_container);
+        mKeyguardBottomArea.setPreviewContainer(mPreviewContainer);
         mLastOrientation = mResources.getConfiguration().orientation;
 
         initBottomArea();
 
         mWakeUpCoordinator.setStackScroller(mNotificationStackScrollLayoutController);
         mQsFrame = mView.findViewById(R.id.qs_frame);
-        mPulseExpansionHandler.setUp(
-                mNotificationStackScrollLayoutController, mExpansionCallback, mShadeController);
+        mPulseExpansionHandler.setUp(mNotificationStackScrollLayoutController,
+                amount -> {
+                    float progress = amount / mView.getHeight();
+                    float overstretch = Interpolators.getOvershootInterpolation(progress,
+                            (float) mMaxOverscrollAmountForDragDown / mView.getHeight(),
+                            0.2f);
+                    setOverStrechAmount(overstretch);
+                });
         mWakeUpCoordinator.addListener(new NotificationWakeUpCoordinator.WakeUpListener() {
             @Override
             public void onFullyHiddenChanged(boolean isFullyHidden) {
@@ -816,6 +870,10 @@
                 com.android.internal.R.dimen.status_bar_height);
         mHeadsUpInset = statusbarHeight + mResources.getDimensionPixelSize(
                 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);
         mScrimCornerRadius = mResources.getDimensionPixelSize(
                 R.dimen.notification_scrim_corner_radius);
         mScreenCornerRadius = mResources.getDimensionPixelSize(
@@ -993,6 +1051,7 @@
         mKeyguardBottomArea = (KeyguardBottomAreaView) mLayoutInflater.inflate(
                 R.layout.keyguard_bottom_area, mView, false);
         mKeyguardBottomArea.initFrom(oldBottomArea);
+        mKeyguardBottomArea.setPreviewContainer(mPreviewContainer);
         mView.addView(mKeyguardBottomArea, index);
         initBottomArea();
         mKeyguardIndicationController.setIndicationArea(mKeyguardBottomArea);
@@ -1114,58 +1173,28 @@
      * showing.
      */
     private void positionClockAndNotifications() {
+        positionClockAndNotifications(false /* forceUpdate */);
+    }
+
+    /**
+     * Positions the clock and notifications dynamically depending on how many notifications are
+     * showing.
+     *
+     * @param forceClockUpdate Should the clock be updated even when not on keyguard
+     */
+    private void positionClockAndNotifications(boolean forceClockUpdate) {
         boolean animate = mNotificationStackScrollLayoutController.isAddOrRemoveAnimationPending();
-        boolean animateClock = animate || mAnimateNextPositionUpdate;
         int stackScrollerPadding;
-        if (mBarState != KEYGUARD) {
+        boolean onKeyguard = isOnKeyguard();
+        if (onKeyguard || forceClockUpdate) {
+            updateClockAppearance();
+        }
+        if (!onKeyguard) {
             stackScrollerPadding = getUnlockedStackScrollerPadding();
         } else {
-            int totalHeight = mView.getHeight();
-            int bottomPadding = Math.max(mIndicationBottomPadding, mAmbientIndicationBottomPadding);
-            int clockPreferredY = mKeyguardStatusViewController.getClockPreferredY(totalHeight);
-            int userSwitcherPreferredY = mStatusBarHeaderHeightKeyguard;
-            boolean bypassEnabled = mKeyguardBypassController.getBypassEnabled();
-            final boolean hasVisibleNotifications = mNotificationStackScrollLayoutController
-                    .getVisibleNotificationCount() != 0 || mMediaDataManager.hasActiveMedia();
-            mKeyguardStatusViewController.setHasVisibleNotifications(hasVisibleNotifications);
-            int userIconHeight = mKeyguardQsUserSwitchController != null
-                    ? mKeyguardQsUserSwitchController.getUserIconHeight() : 0;
-            mClockPositionAlgorithm.setup(mStatusBarHeaderHeightKeyguard,
-                    totalHeight - bottomPadding,
-                    mNotificationStackScrollLayoutController.getIntrinsicContentHeight(),
-                    getExpandedFraction(),
-                    totalHeight,
-                    mLockScreenMode == KeyguardUpdateMonitor.LOCK_SCREEN_MODE_LAYOUT_1
-                            ? mKeyguardStatusViewController.getHeight()
-                            : (int) (mKeyguardStatusViewController.getHeight()
-                                    - mShelfHeight / 2.0f - mDarkIconSize / 2.0f),
-                    userIconHeight,
-                    clockPreferredY, userSwitcherPreferredY, hasCustomClock(),
-                    hasVisibleNotifications, mInterpolatedDarkAmount, mEmptyDragAmount,
-                    bypassEnabled, getUnlockedStackScrollerPadding(),
-                    getQsExpansionFraction(),
-                    mDisplayCutoutTopInset,
-                    shouldUseSplitNotificationShade(mFeatureFlags, mResources));
-            mClockPositionAlgorithm.run(mClockPositionResult);
-            mKeyguardStatusViewController.updatePosition(
-                    mClockPositionResult.clockX, mClockPositionResult.clockY,
-                    mClockPositionResult.clockScale, animateClock);
-            if (mKeyguardQsUserSwitchController != null) {
-                mKeyguardQsUserSwitchController.updatePosition(
-                        mClockPositionResult.clockX,
-                        mClockPositionResult.userSwitchY,
-                        animateClock);
-            }
-            if (mKeyguardUserSwitcherController != null) {
-                mKeyguardUserSwitcherController.updatePosition(
-                        mClockPositionResult.clockX,
-                        mClockPositionResult.userSwitchY,
-                        animateClock);
-            }
-            updateNotificationTranslucency();
-            updateClock();
             stackScrollerPadding = mClockPositionResult.stackScrollerPaddingExpanded;
         }
+
         mNotificationStackScrollLayoutController.setIntrinsicPadding(stackScrollerPadding);
         mKeyguardBottomArea.setAntiBurnInOffsetX(mClockPositionResult.clockX);
 
@@ -1175,6 +1204,60 @@
         mAnimateNextPositionUpdate = false;
     }
 
+    private void updateClockAppearance() {
+        int totalHeight = mView.getHeight();
+        int bottomPadding = Math.max(mIndicationBottomPadding, mAmbientIndicationBottomPadding);
+        int clockPreferredY = mKeyguardStatusViewController.getClockPreferredY(totalHeight);
+        int userSwitcherPreferredY = mStatusBarHeaderHeightKeyguard;
+        boolean bypassEnabled = mKeyguardBypassController.getBypassEnabled();
+        final boolean hasVisibleNotifications = mNotificationStackScrollLayoutController
+                .getVisibleNotificationCount() != 0 || mMediaDataManager.hasActiveMedia();
+        mKeyguardStatusViewController.setHasVisibleNotifications(hasVisibleNotifications);
+        int userIconHeight = mKeyguardQsUserSwitchController != null
+                ? mKeyguardQsUserSwitchController.getUserIconHeight() : 0;
+        float expandedFraction =
+                mKeyguardStatusViewController.isAnimatingScreenOffFromUnlocked() ? 1.0f
+                        : getExpandedFraction();
+        float darkamount = mKeyguardStatusViewController.isAnimatingScreenOffFromUnlocked() ? 1.0f
+                : mInterpolatedDarkAmount;
+        mClockPositionAlgorithm.setup(mStatusBarHeaderHeightKeyguard,
+                totalHeight - bottomPadding,
+                mNotificationStackScrollLayoutController.getIntrinsicContentHeight(),
+                expandedFraction,
+                totalHeight,
+                mLockScreenMode == KeyguardUpdateMonitor.LOCK_SCREEN_MODE_LAYOUT_1
+                        ? mKeyguardStatusViewController.getHeight()
+                        : (int) (mKeyguardStatusViewController.getHeight()
+                                - mShelfHeight / 2.0f - mDarkIconSize / 2.0f),
+                userIconHeight,
+                clockPreferredY, userSwitcherPreferredY, hasCustomClock(),
+                hasVisibleNotifications, darkamount, mOverStretchAmount,
+                bypassEnabled, getUnlockedStackScrollerPadding(),
+                computeQsExpansionFraction(),
+                mDisplayCutoutTopInset,
+                shouldUseSplitNotificationShade(mFeatureFlags, mResources));
+        mClockPositionAlgorithm.run(mClockPositionResult);
+        boolean animate = mNotificationStackScrollLayoutController.isAddOrRemoveAnimationPending();
+        boolean animateClock = animate || mAnimateNextPositionUpdate;
+        mKeyguardStatusViewController.updatePosition(
+                mClockPositionResult.clockX, mClockPositionResult.clockY,
+                mClockPositionResult.clockScale, animateClock);
+        if (mKeyguardQsUserSwitchController != null) {
+            mKeyguardQsUserSwitchController.updatePosition(
+                    mClockPositionResult.clockX,
+                    mClockPositionResult.userSwitchY,
+                    animateClock);
+        }
+        if (mKeyguardUserSwitcherController != null) {
+            mKeyguardUserSwitcherController.updatePosition(
+                    mClockPositionResult.clockX,
+                    mClockPositionResult.userSwitchY,
+                    animateClock);
+        }
+        updateNotificationTranslucency();
+        updateClock();
+    }
+
     /**
      * @return the padding of the stackscroller when unlocked
      */
@@ -1606,7 +1689,7 @@
 
     private boolean flingExpandsQs(float vel) {
         if (Math.abs(vel) < mFlingAnimationUtils.getMinVelocityPxPerSecond()) {
-            return getQsExpansionFraction() > 0.5f;
+            return computeQsExpansionFraction() > 0.5f;
         } else {
             return vel > 0;
         }
@@ -1619,7 +1702,7 @@
         return !mQsTouchAboveFalsingThreshold;
     }
 
-    private float getQsExpansionFraction() {
+    private float computeQsExpansionFraction() {
         return Math.min(
                 1f, (mQsExpansionHeight - mQsMinExpansionHeight) / (mQsMaxExpansionHeight
                         - mQsMinExpansionHeight));
@@ -1844,7 +1927,7 @@
                 mQsTracking = false;
                 mTrackingPointer = -1;
                 trackMovement(event);
-                float fraction = getQsExpansionFraction();
+                float fraction = computeQsExpansionFraction();
                 if (fraction != 0f || y >= mInitialTouchY) {
                     flingQsWithCurrentVelocity(y,
                             event.getActionMasked() == MotionEvent.ACTION_CANCEL);
@@ -2050,18 +2133,19 @@
 
     protected void updateQsExpansion() {
         if (mQs == null) return;
-        float qsExpansionFraction = getQsExpansionFraction();
+        float qsExpansionFraction = computeQsExpansionFraction();
         mQs.setQsExpansion(qsExpansionFraction, getHeaderTranslation());
         mMediaHierarchyManager.setQsExpansion(qsExpansionFraction);
         int qsPanelBottomY = calculateQsBottomPosition(qsExpansionFraction);
         mScrimController.setQsPosition(qsExpansionFraction, qsPanelBottomY);
+        setQSClippingBounds();
         mNotificationStackScrollLayoutController.setQsExpansionFraction(qsExpansionFraction);
         mDepthController.setQsPanelExpansion(qsExpansionFraction);
     }
 
     private Runnable mOnStackYChanged = () -> {
         if (mQs != null) {
-            setNotificationBounds();
+            setQSClippingBounds();
         }
     };
 
@@ -2069,57 +2153,121 @@
      * Updates scrim bounds, QS clipping, and KSV clipping as well based on the bounds of the shade
      * and QS state.
      */
-    private void setNotificationBounds() {
+    private void setQSClippingBounds() {
         int top = 0;
         int bottom = 0;
         int left = 0;
         int right = 0;
 
-        final int qsPanelBottomY = calculateQsBottomPosition(getQsExpansionFraction());
-        final boolean visible = (getQsExpansionFraction() > 0 || qsPanelBottomY > 0)
+        final int qsPanelBottomY = calculateQsBottomPosition(computeQsExpansionFraction());
+        final boolean visible = (computeQsExpansionFraction() > 0 || qsPanelBottomY > 0)
                 && !mShouldUseSplitNotificationShade;
-        final float notificationTop = mAmbientState.getStackY() - mAmbientState.getScrollY();
         setQsExpansionEnabled(mAmbientState.getScrollY() == 0);
 
-        int radius = mScrimCornerRadius;
         if (!mShouldUseSplitNotificationShade) {
-            top = (int) (isOnKeyguard() ? Math.min(qsPanelBottomY, notificationTop)
-                    : notificationTop);
+            if (mTransitioningToFullShadeProgress > 0.0f) {
+                // If we're transitioning, let's use the actual value. The else case
+                // can be wrong during transitions when waiting for the keyguard to unlock
+                top = mTransitionToFullShadeQSPosition;
+            } else {
+                float notificationTop = getQSEdgePosition();
+                top = (int) (isOnKeyguard() ? Math.min(qsPanelBottomY, notificationTop)
+                        : notificationTop);
+            }
             bottom = getView().getBottom();
             left = getView().getLeft();
             right = getView().getRight();
-            radius = (int) MathUtils.lerp(mScreenCornerRadius, mScrimCornerRadius,
-                    Math.min(top / (float) mScrimCornerRadius, 1f));
         } else if (qsPanelBottomY > 0) { // so bounds are empty on lockscreen
             top = Math.min(qsPanelBottomY, mSplitShadeNotificationsTopPadding);
             bottom = mNotificationStackScrollLayoutController.getHeight();
             left = mNotificationStackScrollLayoutController.getLeft();
             right = mNotificationStackScrollLayoutController.getRight();
         }
+        applyQSClippingBounds(left, top, right, bottom, visible);
+    }
 
-        // Fancy clipping for quick settings
-        if (mQs != null) {
-            mQs.setFancyClipping(top, bottom, radius, visible);
+    private void applyQSClippingBounds(int left, int top, int right, int bottom,
+            boolean visible) {
+        if (!mAnimateNextNotificationBounds || mKeyguardStatusAreaClipBounds.isEmpty()) {
+            if (mQsClippingAnimation != null) {
+                // update the end position of the animator
+                mQsClippingAnimationEndBounds.set(left, top, right, bottom);
+            } else {
+                applyQSClippingImmediately(left, top, right, bottom, visible);
+            }
+        } else {
+            mQsClippingAnimationEndBounds.set(left, top, right, bottom);
+            final int startLeft = mKeyguardStatusAreaClipBounds.left;
+            final int startTop = mKeyguardStatusAreaClipBounds.top;
+            final int startRight = mKeyguardStatusAreaClipBounds.right;
+            final int startBottom = mKeyguardStatusAreaClipBounds.bottom;
+            mQsClippingAnimation = ValueAnimator.ofFloat(0.0f, 1.0f);
+            mQsClippingAnimation.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
+            mQsClippingAnimation.setDuration(
+                    StackStateAnimator.ANIMATION_DURATION_GO_TO_FULL_SHADE);
+            mQsClippingAnimation.setStartDelay(mNotificationBoundsAnimationDelay);
+            mQsClippingAnimation.addUpdateListener(animation -> {
+                float fraction = animation.getAnimatedFraction();
+                int animLeft = (int) MathUtils.lerp(startLeft,
+                        mQsClippingAnimationEndBounds.left, fraction);
+                int animTop = (int) MathUtils.lerp(startTop,
+                        mQsClippingAnimationEndBounds.top, fraction);
+                int animRight = (int) MathUtils.lerp(startRight,
+                        mQsClippingAnimationEndBounds.right, fraction);
+                int animBottom = (int) MathUtils.lerp(startBottom,
+                        mQsClippingAnimationEndBounds.bottom, fraction);
+                applyQSClippingImmediately(animLeft, animTop, animRight, animBottom,
+                        visible /* visible */);
+            });
+            mQsClippingAnimation.addListener(new AnimatorListenerAdapter() {
+                @Override
+                public void onAnimationEnd(Animator animation) {
+                    mQsClippingAnimation = null;
+                }
+            });
+            mQsClippingAnimation.start();
         }
+        mAnimateNextNotificationBounds = false;
+        mNotificationBoundsAnimationDelay = 0;
+    }
+
+    private void applyQSClippingImmediately(int left, int top, int right, int bottom,
+            boolean visible) {
+        // Fancy clipping for quick settings
+        int radius = mScrimCornerRadius;
         if (!mShouldUseSplitNotificationShade) {
             // The padding on this area is large enough that we can use a cheaper clipping strategy
             mKeyguardStatusAreaClipBounds.set(left, top, right, bottom);
             mKeyguardStatusViewController.setClipBounds(visible
                     ? mKeyguardStatusAreaClipBounds : null);
+            radius = (int) MathUtils.lerp(mScreenCornerRadius, mScrimCornerRadius,
+                    Math.min(top / (float) mScrimCornerRadius, 1f));
+        }
+        if (mQs != null) {
+            mQs.setFancyClipping(top, bottom, radius, visible);
         }
         mScrimController.setNotificationsBounds(left, top, right, bottom);
         mScrimController.setScrimCornerRadius(radius);
     }
 
+    private float getQSEdgePosition() {
+        // TODO: replace StackY with unified calculation
+        return mAmbientState.getStackY() - mAmbientState.getScrollY();
+    }
+
     private int calculateQsBottomPosition(float qsExpansionFraction) {
-        int qsBottomY = (int) getHeaderTranslation() + mQs.getQsMinExpansionHeight();
-        if (qsExpansionFraction != 0.0) {
-            qsBottomY = (int) MathUtils.lerp(
-                    qsBottomY, mQs.getDesiredHeight(), qsExpansionFraction);
+        if (mTransitioningToFullShadeProgress > 0.0f) {
+            return mTransitionToFullShadeQSPosition;
+        } else {
+            int qsBottomY = (int) getHeaderTranslation() + mQs.getQsMinExpansionHeight();
+            if (qsExpansionFraction != 0.0) {
+                qsBottomY = (int) MathUtils.lerp(
+                        qsBottomY, mQs.getDesiredHeight(), qsExpansionFraction);
+            }
+            // to account for shade overshooting animation, see setSectionPadding method
+            if (mSectionPadding > 0) qsBottomY += mSectionPadding;
+            return qsBottomY;
         }
-        // to account for shade overshooting animation, see setSectionPadding method
-        if (mSectionPadding > 0) qsBottomY += mSectionPadding;
-        return qsBottomY;
     }
 
     private String determineAccessibilityPaneTitle() {
@@ -2163,7 +2311,7 @@
             // from a scrolled quick settings.
             return MathUtils.lerp((float) getKeyguardNotificationStaticPadding(),
                     (float) (mQsMaxExpansionHeight + mQsNotificationTopPadding),
-                    getQsExpansionFraction());
+                    computeQsExpansionFraction());
         } else {
             return mQsExpansionHeight + mQsNotificationTopPadding;
         }
@@ -2200,15 +2348,65 @@
         }
     }
 
-
     private void updateQSPulseExpansion() {
         if (mQs != null) {
-            mQs.setShowCollapsedOnKeyguard(
+            mQs.setPulseExpanding(
                     mKeyguardShowing && mKeyguardBypassController.getBypassEnabled()
                             && mNotificationStackScrollLayoutController.isPulseExpanding());
         }
     }
 
+    /**
+     * Set the amount of pixels we have currently dragged down if we're transitioning to the full
+     * shade. 0.0f means we're not transitioning yet.
+     */
+    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) {
+            if (mNotificationStackScrollLayoutController.getVisibleNotificationCount() == 0
+                    && !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();
+                }
+            } 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();
+            }
+        }
+
+        // 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;
+
+        int position = (int) MathUtils.lerp((float) 0, endPosition,
+                mTransitioningToFullShadeProgress);
+        if (mTransitioningToFullShadeProgress > 0.0f) {
+            // 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();
+    }
+
     private void trackMovement(MotionEvent event) {
         if (mQsVelocityTracker != null) mQsVelocityTracker.addMovement(event);
     }
@@ -2632,7 +2830,7 @@
         if (!mKeyguardShowing) {
             return;
         }
-        float alphaQsExpansion = 1 - Math.min(1, getQsExpansionFraction() * 2);
+        float alphaQsExpansion = 1 - Math.min(1, computeQsExpansionFraction() * 2);
         float newAlpha = Math.min(getKeyguardContentsAlpha(), alphaQsExpansion)
                 * mKeyguardStatusBarAnimateAlpha;
         newAlpha *= 1.0f - mKeyguardHeadsUpShowingAmount;
@@ -2655,7 +2853,7 @@
         float expansionAlpha = MathUtils.map(
                 isUnlockHintRunning() ? 0 : KeyguardBouncer.ALPHA_EXPANSION_THRESHOLD, 1f, 0f, 1f,
                 getExpandedFraction());
-        float alpha = Math.min(expansionAlpha, 1 - getQsExpansionFraction());
+        float alpha = Math.min(expansionAlpha, 1 - computeQsExpansionFraction());
         alpha *= mBottomAreaShadeAlpha;
         mKeyguardBottomArea.setAffordanceAlpha(alpha);
         mKeyguardBottomArea.setImportantForAccessibility(
@@ -2677,7 +2875,7 @@
         float expansionAlpha = MathUtils.map(
                 isUnlockHintRunning() ? 0 : KeyguardBouncer.ALPHA_EXPANSION_THRESHOLD, 1f, 0f, 1f,
                 getExpandedFraction());
-        float alpha = Math.min(expansionAlpha, 1 - getQsExpansionFraction());
+        float alpha = Math.min(expansionAlpha, 1 - computeQsExpansionFraction());
         mBigClockContainer.setAlpha(alpha);
     }
 
@@ -3229,6 +3427,7 @@
                             mHeightListener.onQsHeightChanged();
                         }
                     });
+            mLockscreenShadeTransitionController.setQS(mQs);
             mNotificationStackScrollLayoutController.setQsContainer((ViewGroup) mQs.getView());
             updateQsExpansion();
         }
@@ -3466,9 +3665,9 @@
             StatusBar statusBar,
             NotificationShelfController notificationShelfController) {
         setStatusBar(statusBar);
-        mNotificationStackScrollLayoutController.setNotificationPanelController(this);
         mNotificationStackScrollLayoutController.setShelfController(notificationShelfController);
         mNotificationShelfController = notificationShelfController;
+        mLockscreenShadeTransitionController.bindController(notificationShelfController);
         updateMaxDisplayedNotifications(true);
     }
 
@@ -3519,10 +3718,6 @@
         return new OnLayoutChangeListener();
     }
 
-    public void setEmptyDragAmount(float amount) {
-        mExpansionCallback.setEmptyDragAmount(amount);
-    }
-
     @Override
     protected TouchHandler createTouchHandler() {
         return new TouchHandler() {
@@ -3991,7 +4186,7 @@
                         mClockPositionResult.clockX,
                         mClockPositionResult.clockYFullyDozing,
                         mClockPositionResult.clockScale,
-                        false);
+                        false /* animate */);
             }
 
             mKeyguardStatusViewController.setKeyguardStatusViewVisibility(
@@ -4007,11 +4202,7 @@
             if (oldState == KEYGUARD && (goingToFullShade
                     || statusBarState == StatusBarState.SHADE_LOCKED)) {
                 animateKeyguardStatusBarOut();
-                long
-                        delay =
-                        mBarState == StatusBarState.SHADE_LOCKED ? 0
-                                : mKeyguardStateController.calculateGoingToFullShadeDelay();
-                mQs.animateHeaderSlidingIn(delay);
+                updateQSMinHeight();
             } else if (oldState == StatusBarState.SHADE_LOCKED
                     && statusBarState == KEYGUARD) {
                 animateKeyguardStatusBarIn(StackStateAnimator.ANIMATION_DURATION_STANDARD);
@@ -4058,11 +4249,12 @@
         }
     }
 
-    private class ExpansionCallback implements PulseExpansionHandler.ExpansionCallback {
-        public void setEmptyDragAmount(float amount) {
-            mEmptyDragAmount = amount * 0.2f;
-            positionClockAndNotifications();
-        }
+    /**
+     * Sets the overstretch amount in raw pixels when dragging down.
+     */
+    public void setOverStrechAmount(float amount) {
+        mOverStretchAmount = amount;
+        positionClockAndNotifications(true /* forceUpdate */);
     }
 
     private class OnAttachStateChangeListener implements View.OnAttachStateChangeListener {
@@ -4109,11 +4301,7 @@
             // Calculate quick setting heights.
             int oldMaxHeight = mQsMaxExpansionHeight;
             if (mQs != null) {
-                float previousMin = mQsMinExpansionHeight;
-                mQsMinExpansionHeight = mKeyguardShowing ? 0 : mQs.getQsMinExpansionHeight();
-                if (mQsExpansionHeight == previousMin) {
-                    mQsExpansionHeight = mQsMinExpansionHeight;
-                }
+                updateQSMinHeight();
                 mQsMaxExpansionHeight = mQs.getDesiredHeight();
                 mNotificationStackScrollLayoutController.setMaxTopPadding(
                         mQsMaxExpansionHeight + mQsNotificationTopPadding);
@@ -4155,6 +4343,14 @@
         }
     }
 
+    private void updateQSMinHeight() {
+        float previousMin = mQsMinExpansionHeight;
+        mQsMinExpansionHeight = mKeyguardShowing ? 0 : mQs.getQsMinExpansionHeight();
+        if (mQsExpansionHeight == previousMin) {
+            mQsExpansionHeight = mQsMinExpansionHeight;
+        }
+    }
+
     private class DebugDrawable extends Drawable {
 
         @Override
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationShadeWindowViewController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationShadeWindowViewController.java
index 4d70237..7f4dabd 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationShadeWindowViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationShadeWindowViewController.java
@@ -34,15 +34,14 @@
 import android.view.ViewGroup;
 
 import com.android.internal.annotations.VisibleForTesting;
-import com.android.systemui.ExpandHelper;
 import com.android.systemui.R;
 import com.android.systemui.classifier.FalsingCollector;
 import com.android.systemui.dock.DockManager;
 import com.android.systemui.doze.DozeLog;
-import com.android.systemui.plugins.FalsingManager;
 import com.android.systemui.shared.plugins.PluginManager;
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.DragDownHelper;
+import com.android.systemui.statusbar.LockscreenShadeTransitionController;
 import com.android.systemui.statusbar.NotificationLockscreenUserManager;
 import com.android.systemui.statusbar.NotificationShadeDepthController;
 import com.android.systemui.statusbar.NotificationShadeWindowController;
@@ -73,7 +72,6 @@
     private final DynamicPrivacyController mDynamicPrivacyController;
     private final KeyguardBypassController mBypassController;
     private final PluginManager mPluginManager;
-    private final FalsingManager mFalsingManager;
     private final FalsingCollector mFalsingCollector;
     private final TunerService mTunerService;
     private final NotificationLockscreenUserManager mNotificationLockscreenUserManager;
@@ -87,6 +85,7 @@
     private final ShadeController mShadeController;
     private final NotificationShadeDepthController mDepthController;
     private final NotificationStackScrollLayoutController mNotificationStackScrollLayoutController;
+    private final LockscreenShadeTransitionController mLockscreenShadeTransitionController;
     private final StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
 
     private GestureDetector mGestureDetector;
@@ -119,7 +118,7 @@
             PulseExpansionHandler pulseExpansionHandler,
             DynamicPrivacyController dynamicPrivacyController,
             KeyguardBypassController bypassController,
-            FalsingManager falsingManager,
+            LockscreenShadeTransitionController transitionController,
             FalsingCollector falsingCollector,
             PluginManager pluginManager,
             TunerService tunerService,
@@ -143,7 +142,7 @@
         mPulseExpansionHandler = pulseExpansionHandler;
         mDynamicPrivacyController = dynamicPrivacyController;
         mBypassController = bypassController;
-        mFalsingManager = falsingManager;
+        mLockscreenShadeTransitionController = transitionController;
         mFalsingCollector = falsingCollector;
         mPluginManager = pluginManager;
         mTunerService = tunerService;
@@ -406,12 +405,7 @@
             }
         });
 
-        ExpandHelper.Callback expandHelperCallback = mStackScrollLayout.getExpandHelperCallback();
-        DragDownHelper.DragDownCallback dragDownCallback = mStackScrollLayout.getDragDownCallback();
-        setDragDownHelper(
-                new DragDownHelper(
-                        mView.getContext(), mView, expandHelperCallback,
-                        dragDownCallback, mFalsingManager, mFalsingCollector));
+        setDragDownHelper(mLockscreenShadeTransitionController.getTouchHelper());
 
         mDepthController.setRoot(mView);
         mNotificationPanelViewController.addExpansionListener(mDepthController);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
index c34fa2f..bbde3c3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
@@ -28,6 +28,7 @@
 import android.os.Trace;
 import android.util.Log;
 import android.util.MathUtils;
+import android.util.Pair;
 import android.view.View;
 import android.view.ViewTreeObserver;
 import android.view.animation.DecelerateInterpolator;
@@ -42,10 +43,10 @@
 import com.android.keyguard.KeyguardUpdateMonitor;
 import com.android.keyguard.KeyguardUpdateMonitorCallback;
 import com.android.settingslib.Utils;
-import com.android.systemui.animation.Interpolators;
 import com.android.systemui.DejankUtils;
 import com.android.systemui.Dumpable;
 import com.android.systemui.R;
+import com.android.systemui.animation.Interpolators;
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.dock.DockManager;
@@ -98,6 +99,18 @@
     public static final int OPAQUE = 2;
     private boolean mClipsQsScrim;
 
+    /**
+     * 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.
+     */
+    private float mTransitionToFullShadeProgress;
+
+    /**
+     * If we're currently transitioning to the full shade.
+     */
+    private boolean mTransitioningToFullShade;
+
     @IntDef(prefix = {"VISIBILITY_"}, value = {
             TRANSPARENT,
             SEMI_TRANSPARENT,
@@ -357,7 +370,7 @@
                     + mInFrontAlpha + ", back: " + mBehindAlpha + ", notif: "
                     + mNotificationsAlpha);
         }
-        applyExpansionToAlpha();
+        applyStateToAlpha();
 
         // Scrim might acquire focus when user is navigating with a D-pad or a keyboard.
         // We need to disable focus otherwise AOD would end up with a gray overlay.
@@ -499,11 +512,38 @@
             if (!(relevantState && mExpansionAffectsAlpha)) {
                 return;
             }
-            applyAndDispatchExpansion();
+            applyAndDispatchState();
         }
     }
 
     /**
+     * Set 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.
+     */
+    public void setTransitionToFullShadeProgress(float progress) {
+        if (progress != mTransitionToFullShadeProgress) {
+            mTransitionToFullShadeProgress = progress;
+            setTransitionToFullShade(progress > 0.0f);
+            applyAndDispatchState();
+        }
+    }
+
+    /**
+     * Set if we're currently transitioning to the full shade
+     */
+    private void setTransitionToFullShade(boolean transitioning) {
+        if (transitioning != mTransitioningToFullShade) {
+            mTransitioningToFullShade = transitioning;
+            if (transitioning) {
+                // Let's make sure the shade locked is ready
+                ScrimState.SHADE_LOCKED.prepare(mState);
+            }
+        }
+    }
+
+
+    /**
      * Set bounds for notifications background, all coordinates are absolute
      */
     public void setNotificationsBounds(float left, float top, float right, float bottom) {
@@ -534,7 +574,7 @@
             if (!(relevantState && mExpansionAffectsAlpha)) {
                 return;
             }
-            applyAndDispatchExpansion();
+            applyAndDispatchState();
         }
     }
 
@@ -553,6 +593,11 @@
         if (mScrimBehind != null) {
             mScrimBehind.enableBottomEdgeConcave(mClipsQsScrim);
         }
+        if (mState != ScrimState.UNINITIALIZED) {
+            // the clipScrimState has changed, let's reprepare ourselves
+            mState.prepare(mState);
+            applyAndDispatchState();
+        }
     }
 
     @VisibleForTesting
@@ -583,7 +628,7 @@
         }
     }
 
-    private void applyExpansionToAlpha() {
+    private void applyStateToAlpha() {
         if (!mExpansionAffectsAlpha) {
             return;
         }
@@ -608,47 +653,40 @@
             mInFrontAlpha = 0;
         } else if (mState == ScrimState.KEYGUARD || mState == ScrimState.SHADE_LOCKED
                 || mState == ScrimState.PULSING) {
-            // Either darken of make the scrim transparent when you
-            // pull down the shade
-            float interpolatedFract = getInterpolatedFraction();
-            float stateBehind = mClipsQsScrim ? mState.getNotifAlpha() : mState.getBehindAlpha();
-            float backAlpha;
-            if (mDarkenWhileDragging) {
-                backAlpha = MathUtils.lerp(mDefaultScrimAlpha, stateBehind,
-                        interpolatedFract);
-            } else {
-                backAlpha = MathUtils.lerp(0 /* start */, stateBehind,
-                        interpolatedFract);
+            Pair<Integer, Float> result = calculateBackStateForState(mState);
+            int behindTint = result.first;
+            float behindAlpha = result.second;
+            if (mTransitionToFullShadeProgress > 0.0f) {
+                Pair<Integer, Float> shadeResult = calculateBackStateForState(
+                        ScrimState.SHADE_LOCKED);
+                behindAlpha = MathUtils.lerp(behindAlpha, shadeResult.second,
+                        mTransitionToFullShadeProgress);
+                behindTint = ColorUtils.blendARGB(behindTint, shadeResult.first,
+                        mTransitionToFullShadeProgress);
             }
             mInFrontAlpha = mState.getFrontAlpha();
-            int backTint;
             if (mClipsQsScrim) {
-                backTint = ColorUtils.blendARGB(ScrimState.BOUNCER.getNotifTint(),
-                        mState.getNotifTint(), interpolatedFract);
-            } else {
-                backTint = ColorUtils.blendARGB(ScrimState.BOUNCER.getBehindTint(),
-                        mState.getBehindTint(), interpolatedFract);
-            }
-            if (mQsExpansion > 0) {
-                backAlpha = MathUtils.lerp(backAlpha, mDefaultScrimAlpha, mQsExpansion);
-                int stateTint = mClipsQsScrim ? ScrimState.SHADE_LOCKED.getNotifTint()
-                        : ScrimState.SHADE_LOCKED.getBehindTint();
-                backTint = ColorUtils.blendARGB(backTint, stateTint, mQsExpansion);
-            }
-            if (mClipsQsScrim) {
-                mNotificationsAlpha = backAlpha;
-                mNotificationsTint = backTint;
+                mNotificationsAlpha = behindAlpha;
+                mNotificationsTint = behindTint;
                 mBehindAlpha = 1;
                 mBehindTint = Color.BLACK;
             } else {
-                mBehindAlpha = backAlpha;
+                mBehindAlpha = behindAlpha;
                 if (mState == ScrimState.SHADE_LOCKED) {
                     // going from KEYGUARD to SHADE_LOCKED state
                     mNotificationsAlpha = getInterpolatedFraction();
                 } else {
                     mNotificationsAlpha = Math.max(1.0f - getInterpolatedFraction(), mQsExpansion);
                 }
-                mBehindTint = backTint;
+                if (mState == ScrimState.KEYGUARD && mTransitionToFullShadeProgress > 0.0f) {
+                    // Interpolate the notification alpha when transitioning!
+                    mNotificationsAlpha = MathUtils.lerp(
+                            mNotificationsAlpha,
+                            getInterpolatedFraction(),
+                            mTransitionToFullShadeProgress);
+                }
+                mNotificationsTint = mState.getNotifTint();
+                mBehindTint = behindTint;
             }
         }
         if (isNaN(mBehindAlpha) || isNaN(mInFrontAlpha) || isNaN(mNotificationsAlpha)) {
@@ -658,8 +696,39 @@
         }
     }
 
-    private void applyAndDispatchExpansion() {
-        applyExpansionToAlpha();
+    private Pair<Integer, Float> calculateBackStateForState(ScrimState state) {
+        // Either darken of make the scrim transparent when you
+        // pull down the shade
+        float interpolatedFract = getInterpolatedFraction();
+        float stateBehind = mClipsQsScrim ? state.getNotifAlpha() : state.getBehindAlpha();
+        float behindAlpha;
+        int behindTint;
+        if (mDarkenWhileDragging) {
+            behindAlpha = MathUtils.lerp(mDefaultScrimAlpha, stateBehind,
+                    interpolatedFract);
+        } else {
+            behindAlpha = MathUtils.lerp(0 /* start */, stateBehind,
+                    interpolatedFract);
+        }
+        if (mClipsQsScrim) {
+            behindTint = ColorUtils.blendARGB(ScrimState.BOUNCER.getNotifTint(),
+                    state.getNotifTint(), interpolatedFract);
+        } else {
+            behindTint = ColorUtils.blendARGB(ScrimState.BOUNCER.getBehindTint(),
+                    state.getBehindTint(), interpolatedFract);
+        }
+        if (mQsExpansion > 0) {
+            behindAlpha = MathUtils.lerp(behindAlpha, mDefaultScrimAlpha, mQsExpansion);
+            int stateTint = mClipsQsScrim ? ScrimState.SHADE_LOCKED.getNotifTint()
+                    : ScrimState.SHADE_LOCKED.getBehindTint();
+            behindTint = ColorUtils.blendARGB(behindTint, stateTint, mQsExpansion);
+        }
+        return new Pair<>(behindTint, behindAlpha);
+    }
+
+
+    private void applyAndDispatchState() {
+        applyStateToAlpha();
         if (mUpdatePending) {
             return;
         }
@@ -1195,7 +1264,7 @@
     public void setExpansionAffectsAlpha(boolean expansionAffectsAlpha) {
         mExpansionAffectsAlpha = expansionAffectsAlpha;
         if (expansionAffectsAlpha) {
-            applyAndDispatchExpansion();
+            applyAndDispatchState();
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ShadeController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ShadeController.java
index 2fa6795..2d41e5e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ShadeController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ShadeController.java
@@ -14,8 +14,6 @@
 
 package com.android.systemui.statusbar.phone;
 
-import android.view.View;
-
 import com.android.systemui.statusbar.StatusBarState;
 
 /**
@@ -78,15 +76,6 @@
     void runPostCollapseRunnables();
 
     /**
-     * If secure with redaction: Show bouncer, go to unlocked shade.
-     *
-     * <p>If secure without redaction or no security: Go to {@link StatusBarState#SHADE_LOCKED}.</p>
-     *
-     * @param startingChild The view to expand after going to the shade.
-     */
-    void goToLockedShade(View startingChild);
-
-    /**
      * Close the shade if it was open
      *
      * @return true if the shade was open, else false
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ShadeControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ShadeControllerImpl.java
index a930a89..d4458e2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ShadeControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ShadeControllerImpl.java
@@ -17,7 +17,6 @@
 package com.android.systemui.statusbar.phone;
 
 import android.util.Log;
-import android.view.View;
 import android.view.ViewTreeObserver;
 import android.view.WindowManager;
 
@@ -182,12 +181,6 @@
     }
 
     @Override
-    public void goToLockedShade(View startingChild) {
-        // TODO: Move this code out of StatusBar into ShadeController.
-        getStatusBar().goToLockedShade(startingChild);
-    }
-
-    @Override
     public boolean collapsePanel() {
         if (!getNotificationPanelViewController().isFullyCollapsed()) {
             // close the shade if it was open
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
index ded3be43..ec012bf 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
@@ -194,6 +194,7 @@
 import com.android.systemui.statusbar.KeyguardIndicationController;
 import com.android.systemui.statusbar.LiftReveal;
 import com.android.systemui.statusbar.LightRevealScrim;
+import com.android.systemui.statusbar.LockscreenShadeTransitionController;
 import com.android.systemui.statusbar.NotificationLockscreenUserManager;
 import com.android.systemui.statusbar.NotificationMediaManager;
 import com.android.systemui.statusbar.NotificationPresenter;
@@ -347,6 +348,8 @@
         ONLY_CORE_APPS = onlyCoreApps;
     }
 
+    private LockscreenShadeTransitionController mLockscreenShadeTransitionController;
+
     public interface ExpansionChangedListener {
         void onExpansionChanged(float expansion, boolean expanded);
     }
@@ -438,9 +441,6 @@
 
     KeyguardIndicationController mKeyguardIndicationController;
 
-    // RemoteInputView to be activated after unlock
-    private View mPendingRemoteInputView;
-
     private final RemoteInputQuickSettingsDisabler mRemoteInputQuickSettingsDisabler;
 
     private View mReportRejectedTouch;
@@ -650,12 +650,6 @@
     private final ScreenLifecycle mScreenLifecycle;
     private final WakefulnessLifecycle mWakefulnessLifecycle;
 
-    private final View.OnClickListener mGoToLockedShadeListener = v -> {
-        if (mState == StatusBarState.KEYGUARD) {
-            wakeUpIfDozing(SystemClock.uptimeMillis(), v, "SHADE_CLICK");
-            goToLockedShade(null);
-        }
-    };
     private boolean mNoAnimationOnNextBarModeChange;
     private final SysuiStatusBarStateController mStatusBarStateController;
 
@@ -798,6 +792,7 @@
             OngoingCallController ongoingCallController,
             SystemStatusAnimationScheduler animationScheduler,
             StatusBarLocationPublisher locationPublisher,
+            LockscreenShadeTransitionController lockscreenShadeTransitionController,
             FeatureFlags featureFlags,
             KeyguardUnlockAnimationController keyguardUnlockAnimationController) {
         super(context);
@@ -882,6 +877,8 @@
         mAnimationScheduler = animationScheduler;
         mStatusBarLocationPublisher = locationPublisher;
         mFeatureFlags = featureFlags;
+        mLockscreenShadeTransitionController = lockscreenShadeTransitionController;
+        lockscreenShadeTransitionController.setStatusbar(this);
 
         mExpansionChangedListeners = new ArrayList<>();
 
@@ -1422,7 +1419,8 @@
                 mDozeScrimController, mScrimController, mNotificationShadeWindowController,
                 mDynamicPrivacyController, mKeyguardStateController,
                 mKeyguardIndicationController,
-                this /* statusBar */, mShadeController, mCommandQueue, mInitController,
+                this /* statusBar */, mShadeController,
+                mLockscreenShadeTransitionController, mCommandQueue, mInitController,
                 mNotificationInterruptStateProvider);
 
         mNotificationShelfController.setOnActivatedListener(mPresenter);
@@ -1502,7 +1500,6 @@
     private void inflateShelf() {
         mNotificationShelfController = mSuperStatusBarViewFactory
                 .getNotificationShelfController(mStackScroller);
-        mNotificationShelfController.setOnClickListener(mGoToLockedShadeListener);
     }
 
     @Override
@@ -1675,7 +1672,7 @@
         final boolean expandEnabled = mDeviceProvisionedController.isDeviceProvisioned()
                 && (mUserSetup || mUserSwitcherController == null
                         || !mUserSwitcherController.isSimpleUserSwitcher())
-                && ((mDisabled2 & StatusBarManager.DISABLE2_NOTIFICATION_SHADE) == 0)
+                && !isShadeDisabled()
                 && ((mDisabled2 & StatusBarManager.DISABLE2_QUICK_SETTINGS) == 0)
                 && !mDozing
                 && !ONLY_CORE_APPS;
@@ -1683,6 +1680,10 @@
         Log.d(TAG, "updateQsExpansionEnabled - QS Expand enabled: " + expandEnabled);
     }
 
+    public boolean isShadeDisabled() {
+        return (mDisabled2 & StatusBarManager.DISABLE2_NOTIFICATION_SHADE) != 0;
+    }
+
     public void addQsTile(ComponentName tile) {
         if (mQSPanelController != null && mQSPanelController.getHost() != null) {
             mQSPanelController.getHost().addTile(tile);
@@ -3333,7 +3334,6 @@
     public void showKeyguard() {
         mStatusBarStateController.setKeyguardRequested(true);
         mStatusBarStateController.setLeaveOpenOnKeyguardHide(false);
-        mPendingRemoteInputView = null;
         updateIsKeyguard();
         mAssistManagerLazy.get().onLockscreenShown();
     }
@@ -3392,11 +3392,6 @@
             mStatusBarStateController.setState(StatusBarState.KEYGUARD);
         }
         updatePanelExpansionForKeyguard();
-        if (mDraggedDownEntry != null) {
-            mDraggedDownEntry.setUserLocked(false);
-            mDraggedDownEntry.notifyHeightChanged(false /* needsAnimation */);
-            mDraggedDownEntry = null;
-        }
     }
 
     private void updatePanelExpansionForKeyguard() {
@@ -3524,11 +3519,7 @@
                 mStatusBarStateController.setLeaveOpenOnKeyguardHide(false);
             }
             long delay = mKeyguardStateController.calculateGoingToFullShadeDelay();
-            mNotificationPanelViewController.animateToFullShade(delay);
-            if (mDraggedDownEntry != null) {
-                mDraggedDownEntry.setUserLocked(false);
-                mDraggedDownEntry = null;
-            }
+            mLockscreenShadeTransitionController.onHideKeyguard(delay);
 
             // Disable layout transitions in navbar for this transition because the load is just
             // too heavy for the CPU and GPU on any device.
@@ -3719,6 +3710,22 @@
         }
     }
 
+    /**
+     * Show the bouncer if we're currently on the keyguard or shade locked and aren't hiding.
+     * @param performAction the action to perform when the bouncer is dismissed.
+     * @param cancelAction the action to perform when unlock is aborted.
+     */
+    public void showBouncerWithDimissAndCancelIfKeyguard(OnDismissAction performAction,
+            Runnable cancelAction) {
+        if ((mState == StatusBarState.KEYGUARD || mState == StatusBarState.SHADE_LOCKED)
+                && !mKeyguardViewMediator.isHiding()) {
+            mStatusBarKeyguardViewManager.dismissWithAction(performAction, cancelAction,
+                    false /* afterKeyguardGone */);
+        } else if (cancelAction != null) {
+            cancelAction.run();
+        }
+    }
+
     void instantCollapseNotificationPanel() {
         mNotificationPanelViewController.instantCollapse();
         mShadeController.runPostCollapseRunnables();
@@ -3896,47 +3903,6 @@
     }
 
     /**
-     * If secure with redaction: Show bouncer, go to unlocked shade.
-     *
-     * <p>If secure without redaction or no security: Go to {@link StatusBarState#SHADE_LOCKED}.</p>
-     *
-     * @param expandView The view to expand after going to the shade.
-     */
-    void goToLockedShade(View expandView) {
-        if ((mDisabled2 & StatusBarManager.DISABLE2_NOTIFICATION_SHADE) != 0) {
-            return;
-        }
-
-        int userId = mLockscreenUserManager.getCurrentUserId();
-        ExpandableNotificationRow row = null;
-        NotificationEntry entry = null;
-        if (expandView instanceof ExpandableNotificationRow) {
-            entry = ((ExpandableNotificationRow) expandView).getEntry();
-            entry.setUserExpanded(true /* userExpanded */, true /* allowChildExpansion */);
-            // Indicate that the group expansion is changing at this time -- this way the group
-            // and children backgrounds / divider animations will look correct.
-            entry.setGroupExpansionChanging(true);
-            userId = entry.getSbn().getUserId();
-        }
-        boolean fullShadeNeedsBouncer = !mLockscreenUserManager.
-                userAllowsPrivateNotificationsInPublic(mLockscreenUserManager.getCurrentUserId())
-                || !mLockscreenUserManager.shouldShowLockscreenNotifications()
-                || mFalsingCollector.shouldEnforceBouncer();
-        if (mKeyguardBypassController.getBypassEnabled()) {
-            fullShadeNeedsBouncer = false;
-        }
-        if (mLockscreenUserManager.isLockscreenPublicMode(userId) && fullShadeNeedsBouncer) {
-            mStatusBarStateController.setLeaveOpenOnKeyguardHide(true);
-            showBouncerIfKeyguard();
-            mDraggedDownEntry = entry;
-            mPendingRemoteInputView = null;
-        } else {
-            mNotificationPanelViewController.animateToFullShade(0 /* delay */);
-            mStatusBarStateController.setState(StatusBarState.SHADE_LOCKED);
-        }
-    }
-
-    /**
      * Propagation of the bouncer state, indicating that it's fully visible.
      */
     public void setBouncerShowing(boolean bouncerShowing) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java
index 75c544d..aa58527 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java
@@ -48,6 +48,7 @@
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.KeyguardIndicationController;
+import com.android.systemui.statusbar.LockscreenShadeTransitionController;
 import com.android.systemui.statusbar.NotificationLockscreenUserManager;
 import com.android.systemui.statusbar.NotificationMediaManager;
 import com.android.systemui.statusbar.NotificationPresenter;
@@ -112,6 +113,7 @@
     private final KeyguardIndicationController mKeyguardIndicationController;
     private final StatusBar mStatusBar;
     private final ShadeController mShadeController;
+    private final LockscreenShadeTransitionController mShadeTransitionController;
     private final CommandQueue mCommandQueue;
 
     private final AccessibilityManager mAccessibilityManager;
@@ -138,6 +140,7 @@
             KeyguardIndicationController keyguardIndicationController,
             StatusBar statusBar,
             ShadeController shadeController,
+            LockscreenShadeTransitionController shadeTransitionController,
             CommandQueue commandQueue,
             InitController initController,
             NotificationInterruptStateProvider notificationInterruptStateProvider) {
@@ -149,6 +152,7 @@
         // TODO: use KeyguardStateController#isOccluded to remove this dependency
         mStatusBar = statusBar;
         mShadeController = shadeController;
+        mShadeTransitionController = shadeTransitionController;
         mCommandQueue = commandQueue;
         mAboveShelfObserver = new AboveShelfObserver(stackScrollerController.getView());
         mNotificationShadeWindowController = notificationShadeWindowController;
@@ -394,7 +398,7 @@
         mHeadsUpManager.setExpanded(clickedEntry, nowExpanded);
         if (nowExpanded) {
             if (mStatusBarStateController.getState() == StatusBarState.KEYGUARD) {
-                mShadeController.goToLockedShade(clickedEntry.getRow());
+                mShadeTransitionController.goToLockedShade(clickedEntry.getRow());
             } else if (clickedEntry.isSensitive()
                     && mDynamicPrivacyController.isInLockedDownShade()) {
                 mStatusBarStateController.setLeaveOpenOnKeyguardHide(true);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarPhoneModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarPhoneModule.java
index d0d2cb2..9722d68 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarPhoneModule.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarPhoneModule.java
@@ -51,6 +51,7 @@
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.FeatureFlags;
 import com.android.systemui.statusbar.KeyguardIndicationController;
+import com.android.systemui.statusbar.LockscreenShadeTransitionController;
 import com.android.systemui.statusbar.NotificationLockscreenUserManager;
 import com.android.systemui.statusbar.NotificationMediaManager;
 import com.android.systemui.statusbar.NotificationRemoteInputManager;
@@ -213,6 +214,7 @@
             OngoingCallController ongoingCallController,
             SystemStatusAnimationScheduler animationScheduler,
             StatusBarLocationPublisher locationPublisher,
+            LockscreenShadeTransitionController transitionController,
             FeatureFlags featureFlags,
             KeyguardUnlockAnimationController keyguardUnlockAnimationController) {
         return new StatusBar(
@@ -299,6 +301,7 @@
                 ongoingCallController,
                 animationScheduler,
                 locationPublisher,
+                transitionController,
                 featureFlags,
                 keyguardUnlockAnimationController);
     }
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 a974421..c6aef4a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaHierarchyManagerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaHierarchyManagerTest.kt
@@ -30,6 +30,7 @@
 import com.android.systemui.statusbar.SysuiStatusBarStateController
 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 org.junit.Assert.assertNotNull
@@ -79,6 +80,8 @@
     private lateinit var wakefulnessLifecycle: WakefulnessLifecycle
     @Mock
     private lateinit var statusBarKeyguardViewManager: StatusBarKeyguardViewManager
+    @Mock
+    private lateinit var configurationController: ConfigurationController
     @Captor
     private lateinit var wakefullnessObserver: ArgumentCaptor<(WakefulnessLifecycle.Observer)>
     @Captor
@@ -98,6 +101,7 @@
                 bypassController,
                 mediaCarouselController,
                 notificationLockscreenUserManager,
+                configurationController,
                 wakefulnessLifecycle,
                 statusBarKeyguardViewManager)
         verify(wakefulnessLifecycle).addObserver(wakefullnessObserver.capture())
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerTest.kt
new file mode 100644
index 0000000..18b6c30
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerTest.kt
@@ -0,0 +1,225 @@
+package com.android.systemui.statusbar
+
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import android.testing.TestableLooper.RunWithLooper
+import android.util.DisplayMetrics
+import androidx.test.filters.SmallTest
+import com.android.systemui.ExpandHelper
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.classifier.FalsingCollector
+import com.android.systemui.media.MediaHierarchyManager
+import com.android.systemui.plugins.FalsingManager
+import com.android.systemui.plugins.qs.QS
+import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
+import com.android.systemui.statusbar.notification.row.NotificationTestHelper
+import com.android.systemui.statusbar.notification.stack.AmbientState
+import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout
+import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
+import com.android.systemui.statusbar.phone.KeyguardBypassController
+import com.android.systemui.statusbar.phone.LockscreenGestureLogger
+import com.android.systemui.statusbar.phone.NotificationPanelViewController
+import com.android.systemui.statusbar.phone.ScrimController
+import com.android.systemui.statusbar.phone.StatusBar
+import com.android.systemui.statusbar.policy.ConfigurationController
+import org.junit.After
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyBoolean
+import org.mockito.ArgumentMatchers.anyFloat
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.ArgumentMatchers.anyLong
+import org.mockito.Mock
+import org.mockito.Mockito
+import org.mockito.Mockito.clearInvocations
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.junit.MockitoJUnit
+import org.mockito.Mockito.`when` as whenever
+
+private fun <T> anyObject(): T {
+    return Mockito.anyObject<T>()
+}
+
+@SmallTest
+@RunWithLooper(setAsMainLooper = true)
+@RunWith(AndroidTestingRunner::class)
+class LockscreenShadeTransitionControllerTest : SysuiTestCase() {
+
+    lateinit var transitionController: LockscreenShadeTransitionController
+    lateinit var row: ExpandableNotificationRow
+    @Mock lateinit var statusbarStateController: SysuiStatusBarStateController
+    @Mock lateinit var lockscreenGestureLogger: LockscreenGestureLogger
+    @Mock lateinit var keyguardBypassController: KeyguardBypassController
+    @Mock lateinit var lockScreenUserManager: NotificationLockscreenUserManager
+    @Mock lateinit var falsingCollector: FalsingCollector
+    @Mock lateinit var ambientState: AmbientState
+    @Mock lateinit var displayMetrics: DisplayMetrics
+    @Mock lateinit var mediaHierarchyManager: MediaHierarchyManager
+    @Mock lateinit var scrimController: ScrimController
+    @Mock lateinit var configurationController: ConfigurationController
+    @Mock lateinit var falsingManager: FalsingManager
+    @Mock lateinit var notificationPanelController: NotificationPanelViewController
+    @Mock lateinit var nsslController: NotificationStackScrollLayoutController
+    @Mock lateinit var featureFlags: FeatureFlags
+    @Mock lateinit var stackscroller: NotificationStackScrollLayout
+    @Mock lateinit var expandHelperCallback: ExpandHelper.Callback
+    @Mock lateinit var statusbar: StatusBar
+    @Mock lateinit var qS: QS
+    @JvmField @Rule val mockito = MockitoJUnit.rule()
+
+    @Before
+    fun setup() {
+        val helper = NotificationTestHelper(
+                mContext,
+                mDependency,
+                TestableLooper.get(this))
+        row = helper.createRow()
+        transitionController = LockscreenShadeTransitionController(
+            statusBarStateController = statusbarStateController,
+            lockscreenGestureLogger = lockscreenGestureLogger,
+            keyguardBypassController = keyguardBypassController,
+            lockScreenUserManager = lockScreenUserManager,
+            falsingCollector = falsingCollector,
+            ambientState = ambientState,
+            displayMetrics = displayMetrics,
+            mediaHierarchyManager = mediaHierarchyManager,
+            scrimController = scrimController,
+            featureFlags = featureFlags,
+            context = context,
+            configurationController = configurationController,
+            falsingManager = falsingManager
+        )
+        whenever(nsslController.view).thenReturn(stackscroller)
+        whenever(nsslController.expandHelperCallback).thenReturn(expandHelperCallback)
+        transitionController.notificationPanelController = notificationPanelController
+        transitionController.statusbar = statusbar
+        transitionController.qS = qS
+        transitionController.setStackScroller(nsslController)
+        whenever(statusbarStateController.state).thenReturn(StatusBarState.KEYGUARD)
+        whenever(nsslController.isInLockedDownShade).thenReturn(false)
+        whenever(qS.isFullyCollapsed).thenReturn(true)
+        whenever(lockScreenUserManager.userAllowsPrivateNotificationsInPublic(anyInt())).thenReturn(
+                true)
+        whenever(lockScreenUserManager.shouldShowLockscreenNotifications()).thenReturn(true)
+        whenever(lockScreenUserManager.isLockscreenPublicMode(anyInt())).thenReturn(true)
+        whenever(falsingCollector.shouldEnforceBouncer()).thenReturn(false)
+        whenever(keyguardBypassController.bypassEnabled).thenReturn(false)
+        clearInvocations(statusbar)
+    }
+
+    @After
+    fun tearDown() {
+        transitionController.dragDownAnimator?.cancel()
+    }
+
+    @Test
+    fun testCantDragDownWhenQSExpanded() {
+        assertTrue("Can't drag down on keyguard", transitionController.canDragDown())
+        whenever(qS.isFullyCollapsed).thenReturn(false)
+        assertFalse("Can drag down when QS is expanded", transitionController.canDragDown())
+    }
+
+    @Test
+    fun testCanDragDownInLockedDownShade() {
+        whenever(statusbarStateController.state).thenReturn(StatusBarState.SHADE_LOCKED)
+        assertFalse("Can drag down in shade locked", transitionController.canDragDown())
+        whenever(nsslController.isInLockedDownShade).thenReturn(true)
+        assertTrue("Can't drag down in locked down shade", transitionController.canDragDown())
+    }
+
+    @Test
+    fun testGoingToLockedShade() {
+        transitionController.goToLockedShade(null)
+        verify(statusbarStateController).setState(StatusBarState.SHADE_LOCKED)
+    }
+
+    @Test
+    fun testGoToLockedShadeOnlyOnKeyguard() {
+        whenever(statusbarStateController.state).thenReturn(StatusBarState.SHADE_LOCKED)
+        transitionController.goToLockedShade(null)
+        whenever(statusbarStateController.state).thenReturn(StatusBarState.SHADE)
+        transitionController.goToLockedShade(null)
+        whenever(statusbarStateController.state).thenReturn(StatusBarState.FULLSCREEN_USER_SWITCHER)
+        transitionController.goToLockedShade(null)
+        verify(statusbarStateController, never()).setState(anyInt())
+    }
+
+    @Test
+    fun testDontGoWhenShadeDisabled() {
+        whenever(statusbar.isShadeDisabled).thenReturn(true)
+        transitionController.goToLockedShade(null)
+        verify(statusbarStateController, never()).setState(anyInt())
+    }
+
+    @Test
+    fun testUserExpandsViewOnGoingToFullShade() {
+        assertFalse("Row shouldn't be user expanded yet", row.isUserExpanded)
+        transitionController.goToLockedShade(row)
+        assertTrue("Row wasn't user expanded on drag down", row.isUserExpanded)
+    }
+
+    @Test
+    fun testTriggeringBouncerWhenPrivateNotificationsArentAllowed() {
+        whenever(lockScreenUserManager.userAllowsPrivateNotificationsInPublic(anyInt())).thenReturn(
+                false)
+        transitionController.goToLockedShade(null)
+        verify(statusbarStateController, never()).setState(anyInt())
+        verify(statusbarStateController).setLeaveOpenOnKeyguardHide(true)
+        verify(statusbar).showBouncerWithDimissAndCancelIfKeyguard(anyObject(), anyObject())
+    }
+
+    @Test
+    fun testTriggeringBouncerNoNotificationsOnLockscreen() {
+        whenever(lockScreenUserManager.shouldShowLockscreenNotifications()).thenReturn(false)
+        transitionController.goToLockedShade(null)
+        verify(statusbarStateController, never()).setState(anyInt())
+        verify(statusbarStateController).setLeaveOpenOnKeyguardHide(true)
+        verify(statusbar).showBouncerWithDimissAndCancelIfKeyguard(anyObject(), anyObject())
+    }
+
+    @Test
+    fun testGoToLockedShadeCreatesQSAnimation() {
+        transitionController.goToLockedShade(null)
+        verify(statusbarStateController).setState(StatusBarState.SHADE_LOCKED)
+        verify(notificationPanelController).animateToFullShade(anyLong())
+        assertNotNull(transitionController.dragDownAnimator)
+    }
+
+    @Test
+    fun testGoToLockedShadeDoesntCreateQSAnimation() {
+        transitionController.goToLockedShade(null, needsQSAnimation = false)
+        verify(statusbarStateController).setState(StatusBarState.SHADE_LOCKED)
+        verify(notificationPanelController).animateToFullShade(anyLong())
+        assertNull(transitionController.dragDownAnimator)
+    }
+
+    @Test
+    fun testDragDownAmountDoesntCallOutInLockedDownShade() {
+        whenever(nsslController.isInLockedDownShade).thenReturn(true)
+        transitionController.dragDownAmount = 10f
+        verify(nsslController, never()).setTransitionToFullShadeAmount(anyFloat())
+        verify(mediaHierarchyManager, never()).setTransitionToFullShadeAmount(anyFloat())
+        verify(scrimController, never()).setTransitionToFullShadeProgress(anyFloat())
+        verify(notificationPanelController, never()).setTransitionToFullShadeAmount(anyFloat(),
+                anyBoolean(), anyLong())
+        verify(qS, never()).setTransitionToFullShadeAmount(anyFloat(), anyBoolean())
+    }
+
+    @Test
+    fun testDragDownAmountCallsOut() {
+        transitionController.dragDownAmount = 10f
+        verify(nsslController).setTransitionToFullShadeAmount(anyFloat())
+        verify(mediaHierarchyManager).setTransitionToFullShadeAmount(anyFloat())
+        verify(scrimController).setTransitionToFullShadeProgress(anyFloat())
+        verify(notificationPanelController).setTransitionToFullShadeAmount(anyFloat(),
+                anyBoolean(), anyLong())
+        verify(qS).setTransitionToFullShadeAmount(anyFloat(), anyBoolean())
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
index 84fb368..8758e16 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
@@ -142,7 +142,6 @@
                 mNotificationSectionsManager,
                 mGroupMembershipManger,
                 mGroupExpansionManager,
-                mStatusBarStateController,
                 mAmbientState,
                 mFeatureFlags);
         mStackScrollerInternal.initView(getContext(), mKeyguardBypassEnabledProvider,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollerControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollerControllerTest.java
index 895339f..f376e88 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollerControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollerControllerTest.java
@@ -50,6 +50,7 @@
 import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin.OnMenuEventListener;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.statusbar.FeatureFlags;
+import com.android.systemui.statusbar.LockscreenShadeTransitionController;
 import com.android.systemui.statusbar.NotificationLockscreenUserManager;
 import com.android.systemui.statusbar.NotificationLockscreenUserManager.UserChangedListener;
 import com.android.systemui.statusbar.NotificationRemoteInputManager;
@@ -121,6 +122,7 @@
     @Mock private NotificationEntryManager mEntryManager;
     @Mock private IStatusBarService mIStatusBarService;
     @Mock private UiEventLogger mUiEventLogger;
+    @Mock private LockscreenShadeTransitionController mLockscreenShadeTransitionController;
     @Mock private ForegroundServiceDismissalFeatureController mFgFeatureController;
     @Mock private ForegroundServiceSectionController mFgServicesSectionController;
     @Mock private ForegroundServiceDungeonView mForegroundServiceDungeonView;
@@ -173,6 +175,7 @@
                 mNotifPipeline,
                 mNotifCollection,
                 mEntryManager,
+                mLockscreenShadeTransitionController,
                 mIStatusBarService,
                 mUiEventLogger,
                 mFgFeatureController,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationPanelViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationPanelViewTest.java
index 8996469..6b4797f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationPanelViewTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationPanelViewTest.java
@@ -82,6 +82,7 @@
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.FeatureFlags;
 import com.android.systemui.statusbar.KeyguardAffordanceView;
+import com.android.systemui.statusbar.LockscreenShadeTransitionController;
 import com.android.systemui.statusbar.NotificationLockscreenUserManager;
 import com.android.systemui.statusbar.NotificationShadeDepthController;
 import com.android.systemui.statusbar.NotificationShelfController;
@@ -226,6 +227,8 @@
     @Mock
     private NotificationShadeDepthController mNotificationShadeDepthController;
     @Mock
+    private LockscreenShadeTransitionController mLockscreenShadeTransitionController;
+    @Mock
     private AuthController mAuthController;
     @Mock
     private ScrimController mScrimController;
@@ -314,7 +317,9 @@
                 mKeyguardBypassController, mHeadsUpManager,
                 mock(NotificationRoundnessManager.class),
                 mStatusBarStateController,
-                new FalsingManagerFake(), new FalsingCollectorFake());
+                new FalsingManagerFake(),
+                mLockscreenShadeTransitionController,
+                new FalsingCollectorFake());
         when(mKeyguardStatusViewComponentFactory.build(any()))
                 .thenReturn(mKeyguardStatusViewComponent);
         when(mKeyguardStatusViewComponent.getKeyguardClockSwitchController())
@@ -330,7 +335,7 @@
                 mResources,
                 mLayoutInflater,
                 coordinator, expansionHandler, mDynamicPrivacyController, mKeyguardBypassController,
-                new FalsingManagerFake(), new FalsingCollectorFake(), mShadeController,
+                new FalsingManagerFake(), new FalsingCollectorFake(),
                 mNotificationLockscreenUserManager, mNotificationEntryManager,
                 mKeyguardStateController, mStatusBarStateController, mDozeLog,
                 mDozeParameters, mCommandQueue, mVibratorHelper,
@@ -344,6 +349,7 @@
                 mKeyguardQsUserSwitchComponentFactory,
                 mKeyguardUserSwitcherComponentFactory,
                 mKeyguardStatusBarViewComponentFactory,
+                mLockscreenShadeTransitionController,
                 mQSDetailDisplayer,
                 mGroupManager,
                 mNotificationAreaController,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationShadeWindowViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationShadeWindowViewTest.java
index 0a3eec4d..6c1a3c9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationShadeWindowViewTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationShadeWindowViewTest.java
@@ -31,12 +31,12 @@
 import com.android.systemui.SystemUIFactory;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.classifier.FalsingCollectorFake;
-import com.android.systemui.classifier.FalsingManagerFake;
 import com.android.systemui.dock.DockManager;
 import com.android.systemui.doze.DozeLog;
 import com.android.systemui.shared.plugins.PluginManager;
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.DragDownHelper;
+import com.android.systemui.statusbar.LockscreenShadeTransitionController;
 import com.android.systemui.statusbar.NotificationLockscreenUserManager;
 import com.android.systemui.statusbar.NotificationShadeDepthController;
 import com.android.systemui.statusbar.NotificationShadeWindowController;
@@ -89,6 +89,7 @@
     @Mock private NotificationShadeWindowController mNotificationShadeWindowController;
     @Mock private NotificationStackScrollLayoutController mNotificationStackScrollLayoutController;
     @Mock private StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
+    @Mock private LockscreenShadeTransitionController mLockscreenShadeTransitionController;
 
     @Before
     public void setUp() {
@@ -112,7 +113,7 @@
                 mPulseExpansionHandler,
                 mDynamicPrivacyController,
                 mBypassController,
-                new FalsingManagerFake(),
+                mLockscreenShadeTransitionController,
                 new FalsingCollectorFake(),
                 mPluginManager,
                 mTunerService,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java
index f98f00c..a431a78 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java
@@ -41,6 +41,7 @@
 import android.os.Handler;
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
+import android.util.MathUtils;
 import android.view.View;
 
 import androidx.test.filters.SmallTest;
@@ -1121,6 +1122,32 @@
         assertAlphaAfterExpansion(mNotificationsScrim, /* alpha */ 0.8f, /* expansion */ 0.2f);
     }
 
+    @Test
+    public void testNotificationTransparency_followsTransitionToFullShade() {
+        mScrimController.transitionTo(ScrimState.SHADE_LOCKED);
+        mScrimController.setPanelExpansion(1.0f);
+        finishAnimationsImmediately();
+        float shadeLockedAlpha = mNotificationsScrim.getViewAlpha();
+        mScrimController.transitionTo(ScrimState.KEYGUARD);
+        mScrimController.setPanelExpansion(1.0f);
+        finishAnimationsImmediately();
+        float keyguardAlpha = mNotificationsScrim.getViewAlpha();
+
+        mScrimController.setClipsQsScrim(true);
+        float progress = 0.5f;
+        mScrimController.setTransitionToFullShadeProgress(progress);
+        assertEquals(MathUtils.lerp(keyguardAlpha, shadeLockedAlpha, progress),
+                mNotificationsScrim.getViewAlpha(), 0.2);
+        progress = 0.0f;
+        mScrimController.setTransitionToFullShadeProgress(progress);
+        assertEquals(MathUtils.lerp(keyguardAlpha, shadeLockedAlpha, progress),
+                mNotificationsScrim.getViewAlpha(), 0.2);
+        progress = 1.0f;
+        mScrimController.setTransitionToFullShadeProgress(progress);
+        assertEquals(MathUtils.lerp(keyguardAlpha, shadeLockedAlpha, progress),
+                mNotificationsScrim.getViewAlpha(), 0.2);
+    }
+
     private void assertAlphaAfterExpansion(ScrimView scrim, float expectedAlpha, float expansion) {
         mScrimController.setPanelExpansion(expansion);
         finishAnimationsImmediately();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenterTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenterTest.java
index 8601de5..ce45f26 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenterTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenterTest.java
@@ -40,6 +40,7 @@
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.KeyguardIndicationController;
+import com.android.systemui.statusbar.LockscreenShadeTransitionController;
 import com.android.systemui.statusbar.NotificationLockscreenUserManager;
 import com.android.systemui.statusbar.NotificationMediaManager;
 import com.android.systemui.statusbar.NotificationRemoteInputManager;
@@ -127,7 +128,8 @@
                 mock(NotificationShadeWindowController.class), mock(DynamicPrivacyController.class),
                 mock(KeyguardStateController.class),
                 mock(KeyguardIndicationController.class), mStatusBar,
-                mock(ShadeControllerImpl.class), mCommandQueue, mInitController,
+                mock(ShadeControllerImpl.class), mock(LockscreenShadeTransitionController.class),
+                mCommandQueue, mInitController,
                 mNotificationInterruptStateProvider);
         mInitController.executePostInitTasks();
         ArgumentCaptor<NotificationInterruptSuppressor> suppressorCaptor =
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarTest.java
index b3d52b8..5a3683e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarTest.java
@@ -101,6 +101,7 @@
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.FeatureFlags;
 import com.android.systemui.statusbar.KeyguardIndicationController;
+import com.android.systemui.statusbar.LockscreenShadeTransitionController;
 import com.android.systemui.statusbar.NotificationListener;
 import com.android.systemui.statusbar.NotificationLockscreenUserManager;
 import com.android.systemui.statusbar.NotificationMediaManager;
@@ -267,6 +268,7 @@
     @Mock private OngoingCallController mOngoingCallController;
     @Mock private SystemStatusAnimationScheduler mAnimationScheduler;
     @Mock private StatusBarLocationPublisher mLocationPublisher;
+    @Mock private LockscreenShadeTransitionController mLockscreenTransitionController;
     @Mock private FeatureFlags mFeatureFlags;
     @Mock private IWallpaperManager mWallpaperManager;
     @Mock private KeyguardUnlockAnimationController mKeyguardUnlockAnimationController;
@@ -437,6 +439,7 @@
                 mOngoingCallController,
                 mAnimationScheduler,
                 mLocationPublisher,
+                mLockscreenTransitionController,
                 mFeatureFlags,
                 mKeyguardUnlockAnimationController);
         when(mKeyguardViewMediator.registerStatusBar(any(StatusBar.class), any(ViewGroup.class),