Introducing new PulseExpansionHandler for dragging down while pulsing

This also uses the same wake-up animation now when we're pulsing.

Test: atest SystemUITests
Bug: 125942236
Change-Id: I34a0eff6ecc2280aa983740e4273b344d073ca6e
diff --git a/packages/SystemUI/src/com/android/systemui/analytics/DataCollector.java b/packages/SystemUI/src/com/android/systemui/analytics/DataCollector.java
index 46e004c..0db5bef 100644
--- a/packages/SystemUI/src/com/android/systemui/analytics/DataCollector.java
+++ b/packages/SystemUI/src/com/android/systemui/analytics/DataCollector.java
@@ -380,6 +380,20 @@
         addEvent(PhoneEvent.ON_NOTIFICATION_START_DRAGGING_DOWN);
     }
 
+    public void onStartExpandingFromPulse() {
+        if (DEBUG) {
+            Log.d(TAG, "onStartExpandingFromPulse");
+        }
+        // TODO: maybe add event
+    }
+
+    public void onExpansionFromPulseStopped() {
+        if (DEBUG) {
+            Log.d(TAG, "onExpansionFromPulseStopped");
+        }
+        // TODO: maybe add event
+    }
+
     public void onNotificatonStopDraggingDown() {
         if (DEBUG) {
             Log.d(TAG, "onNotificationStopDraggingDown");
diff --git a/packages/SystemUI/src/com/android/systemui/classifier/Classifier.java b/packages/SystemUI/src/com/android/systemui/classifier/Classifier.java
index 909896e..f8856ce 100644
--- a/packages/SystemUI/src/com/android/systemui/classifier/Classifier.java
+++ b/packages/SystemUI/src/com/android/systemui/classifier/Classifier.java
@@ -32,6 +32,7 @@
     public static final int RIGHT_AFFORDANCE = 6;
     public static final int GENERIC = 7;
     public static final int BOUNCER_UNLOCK = 8;
+    public static final int PULSE_EXPAND = 9;
 
     /**
      * Contains all the information about touch events from which the classifier can query
diff --git a/packages/SystemUI/src/com/android/systemui/classifier/DirectionEvaluator.java b/packages/SystemUI/src/com/android/systemui/classifier/DirectionEvaluator.java
index 5f04222..78b4168 100644
--- a/packages/SystemUI/src/com/android/systemui/classifier/DirectionEvaluator.java
+++ b/packages/SystemUI/src/com/android/systemui/classifier/DirectionEvaluator.java
@@ -22,6 +22,7 @@
         boolean vertical = Math.abs(yDiff) >= Math.abs(xDiff);
         switch (type) {
             case Classifier.QUICK_SETTINGS:
+            case Classifier.PULSE_EXPAND:
             case Classifier.NOTIFICATION_DRAG_DOWN:
                 if (!vertical || yDiff <= 0.0) {
                     return falsingEvaluation;
diff --git a/packages/SystemUI/src/com/android/systemui/classifier/FalsingManager.java b/packages/SystemUI/src/com/android/systemui/classifier/FalsingManager.java
index 8f215ff..a63fdbd 100644
--- a/packages/SystemUI/src/com/android/systemui/classifier/FalsingManager.java
+++ b/packages/SystemUI/src/com/android/systemui/classifier/FalsingManager.java
@@ -408,10 +408,22 @@
         mDataCollector.onNotificatonStartDraggingDown();
     }
 
+    public void onStartExpandingFromPulse() {
+        if (FalsingLog.ENABLED) {
+            FalsingLog.i("onStartExpandingFromPulse", "");
+        }
+        mHumanInteractionClassifier.setType(Classifier.PULSE_EXPAND);
+        mDataCollector.onStartExpandingFromPulse();
+    }
+
     public void onNotificatonStopDraggingDown() {
         mDataCollector.onNotificatonStopDraggingDown();
     }
 
+    public void onExpansionFromPulseStopped() {
+        mDataCollector.onExpansionFromPulseStopped();
+    }
+
     public void onNotificationDismissed() {
         mDataCollector.onNotificationDismissed();
     }
diff --git a/packages/SystemUI/src/com/android/systemui/classifier/HumanInteractionClassifier.java b/packages/SystemUI/src/com/android/systemui/classifier/HumanInteractionClassifier.java
index 577d57a..0cc50cd 100644
--- a/packages/SystemUI/src/com/android/systemui/classifier/HumanInteractionClassifier.java
+++ b/packages/SystemUI/src/com/android/systemui/classifier/HumanInteractionClassifier.java
@@ -128,7 +128,8 @@
         // sent to the classifiers until the finger moves far enough. When the finger if lifted
         // up, the last MotionEvent which was far enough from the finger is set as the final
         // MotionEvent and sent to the Classifiers.
-        if (mCurrentType == Classifier.NOTIFICATION_DRAG_DOWN) {
+        if (mCurrentType == Classifier.NOTIFICATION_DRAG_DOWN
+                || mCurrentType == Classifier.PULSE_EXPAND) {
             mBufferedEvents.add(MotionEvent.obtain(event));
             Point pointEnd = new Point(event.getX() / mDpi, event.getY() / mDpi);
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/PulseExpansionHandler.kt b/packages/SystemUI/src/com/android/systemui/statusbar/PulseExpansionHandler.kt
new file mode 100644
index 0000000..f318c54
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/PulseExpansionHandler.kt
@@ -0,0 +1,288 @@
+/*
+ * Copyright (C) 2019 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 android.animation.Animator
+import android.animation.AnimatorListenerAdapter
+import android.animation.ObjectAnimator
+import android.animation.ValueAnimator
+import android.content.Context
+import android.os.PowerManager
+import android.os.PowerManager.WAKE_REASON_GESTURE
+import android.os.SystemClock
+import android.view.MotionEvent
+import android.view.ViewConfiguration
+
+import com.android.systemui.Gefingerpoken
+import com.android.systemui.Interpolators
+import com.android.systemui.R
+import com.android.systemui.classifier.FalsingManager
+import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator
+import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
+import com.android.systemui.statusbar.notification.row.ExpandableView
+import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout
+import com.android.systemui.statusbar.phone.ShadeController
+
+import javax.inject.Inject
+import javax.inject.Singleton
+import kotlin.math.max
+
+/**
+ * A utility class to enable the downward swipe on when pulsing.
+ */
+@Singleton
+class PulseExpansionHandler @Inject
+constructor(context: Context,
+            private val mWakeUpCoordinator: NotificationWakeUpCoordinator,
+            private val mAmbientPulseManager: AmbientPulseManager) : Gefingerpoken {
+    private val mPowerManager: PowerManager?
+    private var mShadeController: ShadeController? = null
+
+    private val mMinDragDistance: Int
+    private var mInitialTouchX: Float = 0.0f
+    private var mInitialTouchY: Float = 0.0f
+    var isExpanding: Boolean = false
+        private set
+    private val mTouchSlop: Float
+    private var mExpansionCallback: ExpansionCallback? = null
+    private lateinit var mStackScroller: NotificationStackScrollLayout
+    private val mTemp2 = IntArray(2)
+    private var mDraggedFarEnough: Boolean = false
+    private var mStartingChild: ExpandableView? = null
+    private val mFalsingManager: FalsingManager
+    private var mPulsing: Boolean = false
+    var isWakingToShadeLocked: Boolean = false
+        private set
+    private var mEmptyDragAmount: Float = 0.0f
+    private var mWakeUpHeight: Float = 0.0f
+    private var mReachedWakeUpHeight: Boolean = false
+
+    private val isFalseTouch: Boolean
+        get() = mFalsingManager.isFalseTouch
+
+    init {
+        mMinDragDistance = context.resources.getDimensionPixelSize(
+                R.dimen.keyguard_drag_down_min_distance)
+        mTouchSlop = ViewConfiguration.get(context).scaledTouchSlop.toFloat()
+        mFalsingManager = FalsingManager.getInstance(context)
+        mPowerManager = context.getSystemService(PowerManager::class.java)
+    }
+
+    override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
+        return maybeStartExpansion(event)
+    }
+
+    private fun maybeStartExpansion(event: MotionEvent): Boolean {
+        if (!mPulsing) {
+            return false
+        }
+        val x = event.x
+        val y = event.y
+
+        when (event.actionMasked) {
+            MotionEvent.ACTION_DOWN -> {
+                mDraggedFarEnough = false
+                isExpanding = false
+                mStartingChild = null
+                mInitialTouchY = y
+                mInitialTouchX = x
+            }
+
+            MotionEvent.ACTION_MOVE -> {
+                val h = y - mInitialTouchY
+                if (h > mTouchSlop && h > Math.abs(x - mInitialTouchX)) {
+                    mFalsingManager.onStartExpandingFromPulse()
+                    isExpanding = true
+                    captureStartingChild(mInitialTouchX, mInitialTouchY)
+                    mInitialTouchY = y
+                    mInitialTouchX = x
+                    mWakeUpHeight = mWakeUpCoordinator.getWakeUpHeight()
+                    mReachedWakeUpHeight = false
+                    return true
+                }
+            }
+        }
+        return false
+    }
+
+    override fun onTouchEvent(event: MotionEvent): Boolean {
+        if (!isExpanding) {
+            return maybeStartExpansion(event)
+        }
+        val y = event.y
+
+        when (event.actionMasked) {
+            MotionEvent.ACTION_MOVE -> updateExpansionHeight(y - mInitialTouchY)
+            MotionEvent.ACTION_UP -> if (!mFalsingManager.isUnlockingDisabled && !isFalseTouch) {
+                finishExpansion()
+            } else {
+                cancelExpansion()
+            }
+            MotionEvent.ACTION_CANCEL -> cancelExpansion()
+        }
+        return isExpanding
+    }
+
+    private fun finishExpansion() {
+        resetClock()
+        if (mStartingChild != null) {
+            setUserLocked(mStartingChild!!, false)
+            mStartingChild = null
+        }
+        isExpanding = false
+        isWakingToShadeLocked = true
+        mPowerManager!!.wakeUp(SystemClock.uptimeMillis(), WAKE_REASON_GESTURE,
+                "com.android.systemui:PULSEDRAG")
+        mShadeController!!.goToLockedShade(mStartingChild)
+        if (mStartingChild is ExpandableNotificationRow) {
+            val row = mStartingChild as ExpandableNotificationRow?
+            row!!.onExpandedByGesture(true /* userExpanded */)
+        }
+    }
+
+    private fun updateExpansionHeight(height: Float) {
+        var expansionHeight = max(height, 0.0f)
+        if (!mReachedWakeUpHeight && height > mWakeUpHeight) {
+            mReachedWakeUpHeight = true;
+        }
+        if (mStartingChild != null) {
+            val child = mStartingChild!!
+            val newHeight = Math.min((child.collapsedHeight + expansionHeight).toInt(),
+                    child.maxContentHeight)
+            child.actualHeight = newHeight
+            expansionHeight = max(newHeight.toFloat(), expansionHeight)
+        } else {
+            if (!mAmbientPulseManager.hasNotifications()) {
+                // No pulsing notifications, we need to make notifications visible
+                val target = if (mReachedWakeUpHeight) (mWakeUpHeight / 2.0f) else 0.0f
+
+                mWakeUpCoordinator.setNotificationsVisible(height > target, true /* animate */,
+                        true /* increaseSpeed */)
+            }
+            expansionHeight = max(mWakeUpHeight, expansionHeight)
+        }
+        val emptyDragAmount = mWakeUpCoordinator.setPulseWakeUpHeight(expansionHeight)
+        setEmptyDragAmount(emptyDragAmount * RUBBERBAND_FACTOR_STATIC)
+    }
+
+    private fun captureStartingChild(x: Float, y: Float) {
+        if (mStartingChild == null) {
+            mStartingChild = findView(x, y)
+            if (mStartingChild != null) {
+                setUserLocked(mStartingChild!!, true)
+            }
+        }
+    }
+
+    private fun setEmptyDragAmount(amount: Float) {
+        mEmptyDragAmount = amount
+        mExpansionCallback!!.setEmptyDragAmount(amount)
+    }
+
+    private fun reset(child: ExpandableView) {
+        if (child.actualHeight == child.collapsedHeight) {
+            setUserLocked(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.toLong()
+        anim.addListener(object : AnimatorListenerAdapter() {
+            override fun onAnimationEnd(animation: Animator) {
+                setUserLocked(child, false)
+            }
+        })
+        anim.start()
+    }
+
+    private fun setUserLocked(child: ExpandableView, userLocked: Boolean) {
+        if (child is ExpandableNotificationRow) {
+            child.isUserLocked = userLocked
+        }
+    }
+
+    private fun resetClock() {
+        val anim = ValueAnimator.ofFloat(mEmptyDragAmount, 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.start()
+    }
+
+    private fun cancelExpansion() {
+        mFalsingManager.onExpansionFromPulseStopped()
+        if (mStartingChild != null) {
+            reset(mStartingChild!!)
+            mStartingChild = null
+        } else {
+            resetClock()
+        }
+        if (!mAmbientPulseManager.hasNotifications()) {
+            mWakeUpCoordinator.setNotificationsVisible(false /* visible */, true /* animate */,
+                    false /* increaseSpeed */)
+        }
+        isExpanding = false
+    }
+
+    private fun findView(x: Float, y: Float): ExpandableView? {
+        var totalX = x
+        var totalY = y
+        mStackScroller.getLocationOnScreen(mTemp2)
+        totalX += mTemp2[0].toFloat()
+        totalY += mTemp2[1].toFloat()
+        val childAtRawPosition = mStackScroller.getChildAtRawPosition(totalX, totalY)
+        return if (childAtRawPosition != null && childAtRawPosition.isContentExpandable) {
+            childAtRawPosition
+        } else null
+    }
+
+    fun setUp(notificationStackScroller: NotificationStackScrollLayout,
+             expansionCallback: ExpansionCallback,
+             shadeController: ShadeController) {
+        mExpansionCallback = expansionCallback
+        mShadeController = shadeController
+        mStackScroller = notificationStackScroller
+    }
+
+    fun setPulsing(pulsing: Boolean) {
+        mPulsing = pulsing
+        val hasAmbientNotifications = mAmbientPulseManager.hasNotifications();
+        if (pulsing && hasAmbientNotifications) {
+            mWakeUpCoordinator.setNotificationsVisible(true /* visible */, true /* animate */,
+                    false /* increaseSpeed */)
+        } else if (!pulsing && !hasAmbientNotifications) {
+            // TODO: figure out the optimal UX for this, should we extend the pulse instead?
+            mWakeUpCoordinator.setNotificationsVisible(false /* visible */, true /* animate */,
+                    false /* increaseSpeed */)
+        }
+    }
+
+    fun onStartedWakingUp() {
+        isWakingToShadeLocked = false
+    }
+
+    interface ExpansionCallback {
+        fun setEmptyDragAmount(amount: Float)
+    }
+
+    companion object {
+        private val RUBBERBAND_FACTOR_STATIC = 0.25f
+
+        private val SPRING_BACK_ANIMATION_LENGTH_MS = 375
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinator.kt
index f3c8e6f..1ea3bb1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinator.kt
@@ -55,37 +55,47 @@
         mStackScroller = stackScroller
     }
 
-    fun setNotificationsVisible(visible: Boolean, animate: Boolean) {
+    /**
+     * @param visible should notifications be visible
+     * @param animate should this change be animated
+     * @param increaseSpeed should the speed be increased of the animation
+     */
+    fun setNotificationsVisible(visible: Boolean, animate: Boolean, increaseSpeed: Boolean) {
         if (mNotificationsVisible == visible) {
             return
         }
         mNotificationsVisible = visible
         mDarkAnimator?.cancel();
         if (animate) {
-            startVisibilityAnimation()
             notifyAnimationStart(visible)
+            startVisibilityAnimation(increaseSpeed)
         } else {
             setVisibilityAmount(if (visible) 1.0f else 0.0f)
         }
     }
 
     fun setDozeAmount(linearAmount: Float, interpolatedAmount: Float) {
-        mLinearDozeAmount = linearAmount;
-        mDozeAmount = interpolatedAmount;
+        mLinearDozeAmount = linearAmount
+        mDozeAmount = interpolatedAmount
+        mStackScroller.setDozeAmount(mDozeAmount)
         updateDarkAmount()
     }
 
-    private fun startVisibilityAnimation() {
+    private fun startVisibilityAnimation(increaseSpeed: Boolean) {
         if (mNotificationVisibleAmount == 0f || mNotificationVisibleAmount == 1f) {
             mVisibilityInterpolator = if (mNotificationsVisible)
-                Interpolators.TOUCH_RESPONSE_REVERSE
+                Interpolators.TOUCH_RESPONSE
             else
-                Interpolators.FAST_OUT_SLOW_IN
+                Interpolators.FAST_OUT_SLOW_IN_REVERSE
         }
         val target = if (mNotificationsVisible) 1.0f else 0.0f
         val darkAnimator = ObjectAnimator.ofFloat(this, mNotificationVisibility, target)
         darkAnimator.setInterpolator(Interpolators.LINEAR)
-        darkAnimator.setDuration(StackStateAnimator.ANIMATION_DURATION_WAKEUP.toLong())
+        var duration = StackStateAnimator.ANIMATION_DURATION_WAKEUP.toLong()
+        if (increaseSpeed) {
+            duration = (duration.toFloat() / 1.5F).toLong();
+        }
+        darkAnimator.setDuration(duration)
         darkAnimator.start()
         mDarkAnimator = darkAnimator
     }
@@ -97,13 +107,30 @@
         updateDarkAmount()
     }
 
+    fun getWakeUpHeight() : Float {
+        return mStackScroller.wakeUpHeight
+    }
+
     private fun updateDarkAmount() {
         val linearAmount = Math.min(1.0f - mLinearVisibilityAmount, mLinearDozeAmount)
         val amount = Math.min(1.0f - mVisibilityAmount, mDozeAmount)
         mStackScroller.setDarkAmount(linearAmount, amount)
     }
 
-    fun notifyAnimationStart(awake: Boolean) {
+    private fun notifyAnimationStart(awake: Boolean) {
         mStackScroller.notifyDarkAnimationStart(!awake)
     }
+
+    fun setDozing(dozing: Boolean, animate: Boolean) {
+        if (dozing) {
+            setNotificationsVisible(false /* animate */, false, true /* visible */ )
+        }
+        if (animate) {
+            notifyAnimationStart(!dozing)
+        }
+    }
+
+    fun setPulseWakeUpHeight(height: Float): Float {
+        return mStackScroller.setPulseWakeUpHeight(height)
+    }
 }
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java
index 646617c..3cc2e83 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java
@@ -793,11 +793,11 @@
     }
 
     public int getMaxHeight() {
-        if (mContainingNotification.isOnAmbient() && getShowingAmbientView() != null) {
-            return getShowingAmbientView().getHeight();
-        } else if (mExpandedChild != null) {
+        if (mExpandedChild != null) {
             return getViewHeight(VISIBLE_TYPE_EXPANDED)
                     + getExtraRemoteInputHeight(mExpandedRemoteInput);
+        } else if (mContainingNotification.isOnAmbient() && getShowingAmbientView() != null) {
+            return getShowingAmbientView().getHeight();
         } else if (mIsHeadsUp && mHeadsUpChild != null && !mContainingNotification.isOnKeyguard()) {
             return getViewHeight(VISIBLE_TYPE_HEADSUP)
                     + getExtraRemoteInputHeight(mHeadsUpRemoteInput);
@@ -1113,15 +1113,6 @@
      * @return one of the static enum types in this view, calculated form the current state
      */
     public int calculateVisibleType() {
-        if (mContainingNotification.isOnAmbient()) {
-            if (mIsChildInGroup && mAmbientSingleLineChild != null) {
-                return VISIBLE_TYPE_AMBIENT_SINGLELINE;
-            } else if (mAmbientChild != null) {
-                return VISIBLE_TYPE_AMBIENT;
-            } else {
-                return VISIBLE_TYPE_CONTRACTED;
-            }
-        }
         if (mUserExpanding) {
             int height = !mIsChildInGroup || isGroupExpanded()
                     || mContainingNotification.isExpanded(true /* allowOnKeyguard */)
@@ -1152,8 +1143,10 @@
         if (!noExpandedChild && viewHeight == getViewHeight(VISIBLE_TYPE_EXPANDED)) {
             return VISIBLE_TYPE_EXPANDED;
         }
+        boolean onAmbient = mContainingNotification.isOnAmbient();
         if (!mUserExpanding && mIsChildInGroup && !isGroupExpanded()) {
-            return VISIBLE_TYPE_SINGLELINE;
+            return onAmbient ? VISIBLE_TYPE_AMBIENT_SINGLELINE
+                    : VISIBLE_TYPE_SINGLELINE;
         }
 
         if ((mIsHeadsUp || mHeadsUpAnimatingAway) && mHeadsUpChild != null
@@ -1164,11 +1157,13 @@
                 return VISIBLE_TYPE_EXPANDED;
             }
         } else {
+            int collapsedType = onAmbient && mAmbientChild != null ? VISIBLE_TYPE_AMBIENT :
+                    VISIBLE_TYPE_CONTRACTED;
             if (noExpandedChild || (mContractedChild != null
-                    && viewHeight <= getViewHeight(VISIBLE_TYPE_CONTRACTED)
+                    && viewHeight <= getViewHeight(collapsedType)
                     && (!mIsChildInGroup || isGroupExpanded()
                             || !mContainingNotification.isExpanded(true /* allowOnKeyguard */)))) {
-                return VISIBLE_TYPE_CONTRACTED;
+                return collapsedType;
             } else {
                 return VISIBLE_TYPE_EXPANDED;
             }
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 c246af5..cb066f1 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
@@ -18,6 +18,7 @@
 
 import android.annotation.Nullable;
 import android.content.Context;
+import android.util.MathUtils;
 import android.view.View;
 
 import com.android.systemui.Dependency;
@@ -79,6 +80,8 @@
     private ExpandableNotificationRow mExpandingNotification;
     private float mDarkAmount;
     private boolean mAppearing;
+    private float mPulseWakeUpHeight = Float.MAX_VALUE;
+    private float mDozeAmount = 0.0f;
 
     public AmbientState(Context context) {
         mSectionBoundaryIndices.add(NO_SECTION_BOUNDARY);
@@ -279,7 +282,21 @@
     }
 
     public int getInnerHeight() {
-        return Math.max(Math.min(mLayoutHeight, mMaxLayoutHeight) - mTopPadding, mLayoutMinHeight);
+        return getInnerHeight(false /* ignorePulseHeight */);
+    }
+
+    /**
+     * @param ignorePulseHeight ignore the pulse height for this request
+     * @return the inner height of the algorithm.
+     */
+    public int getInnerHeight(boolean ignorePulseHeight) {
+        int height = Math.max(mLayoutMinHeight,
+                Math.min(mLayoutHeight, mMaxLayoutHeight) - mTopPadding);
+        if (ignorePulseHeight) {
+            return height;
+        }
+        float pulseHeight = Math.min(mPulseWakeUpHeight, (float) height);
+        return (int) MathUtils.lerp(height, pulseHeight, mDozeAmount);
     }
 
     public boolean isShadeExpanded() {
@@ -488,4 +505,17 @@
     public boolean isAppearing() {
         return mAppearing;
     }
+
+    public void setPulseWakeUpHeight(float height) {
+        mPulseWakeUpHeight = height;
+    }
+
+    public void setDozeAmount(float dozeAmount) {
+        if (dozeAmount != mDozeAmount) {
+            mDozeAmount = dozeAmount;
+            if (dozeAmount == 0.0f || dozeAmount == 1.0f) {
+                mPulseWakeUpHeight = Float.MAX_VALUE;
+            }
+        }
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AnimationFilter.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AnimationFilter.java
index 507cf07..a471d87 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AnimationFilter.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AnimationFilter.java
@@ -132,10 +132,6 @@
                 // to look nice
                 customDelay = StackStateAnimator.ANIMATION_DELAY_HEADS_UP_CLICKED
                         + StackStateAnimator.ANIMATION_DELAY_HEADS_UP;
-            } else if (ev.animationType == NotificationStackScrollLayout.AnimationEvent
-                    .ANIMATION_TYPE_PULSE_APPEAR || ev.animationType ==
-                    NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_PULSE_DISAPPEAR) {
-                customDelay = StackStateAnimator.ANIMATION_DURATION_PULSE_APPEAR / 2;
             }
         }
     }
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 4d69742..4bb8aa6 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
@@ -88,6 +88,7 @@
 import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.plugins.statusbar.StatusBarStateController.StateListener;
+import com.android.systemui.statusbar.AmbientPulseManager;
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.DragDownHelper.DragDownCallback;
 import com.android.systemui.statusbar.EmptyShadeView;
@@ -167,6 +168,7 @@
      * gap is drawn between them). In this case we don't want to round their corners.
      */
     private static final int DISTANCE_BETWEEN_ADJACENT_SECTIONS_PX = 1;
+    private final AmbientPulseManager mAmbientPulseManager;
 
     private ExpandHelper mExpandHelper;
     private final NotificationSwipeHelper mSwipeHelper;
@@ -392,7 +394,6 @@
     private boolean mGroupExpandedForMeasure;
     private boolean mScrollable;
     private View mForcedScroll;
-    private ExpandableView mNeedingPulseAnimation;
 
     /**
      * @see #setDarkAmount(float, float)
@@ -481,7 +482,8 @@
             @Named(VIEW_CONTEXT) Context context,
             AttributeSet attrs,
             @Named(ALLOW_NOTIFICATION_LONG_PRESS_NAME) boolean allowLongPress,
-            NotificationRoundnessManager notificationRoundnessManager) {
+            NotificationRoundnessManager notificationRoundnessManager,
+            AmbientPulseManager ambientPulseManager) {
         super(context, attrs, 0, 0);
         Resources res = getResources();
 
@@ -491,6 +493,7 @@
             mSections[i] = new NotificationSection(this);
         }
 
+        mAmbientPulseManager = ambientPulseManager;
         mAmbientState = new AmbientState(context);
         mRoundnessManager = notificationRoundnessManager;
         mBgColor = context.getColor(R.color.notification_shade_background_color);
@@ -575,6 +578,14 @@
         }
     }
 
+    public float getWakeUpHeight() {
+        ActivatableNotificationView firstChild = getFirstChildWithBackground();
+        if (firstChild != null) {
+            return firstChild.getCollapsedHeight();
+        }
+        return 0;
+    }
+
     @Override
     @ShadeViewRefactor(RefactorComponent.SHADE_VIEW)
     public void onDensityOrFontScaleChanged() {
@@ -1277,7 +1288,8 @@
             mIsClipped = clipped;
         }
 
-        if (mPulsing || mAmbientState.isFullyDark() && mShowDarkShelf) {
+        if (!mAmbientPulseManager.hasNotifications()
+                && mAmbientState.isFullyDark() && mShowDarkShelf) {
             setClipBounds(null);
         } else if (mAmbientState.isDarkAtAll()) {
             setClipBounds(mBackgroundAnimationRect);
@@ -1514,7 +1526,7 @@
         return null;
     }
 
-    private ExpandableView getChildAtRawPosition(float touchX, float touchY) {
+    public ExpandableView getChildAtRawPosition(float touchX, float touchY) {
         getLocationOnScreen(mTempInt2);
         return getChildAtPosition(touchX - mTempInt2[0], touchY - mTempInt2[1]);
     }
@@ -3354,7 +3366,6 @@
         generateViewResizeEvent();
         generateGroupExpansionEvent();
         generateAnimateEverythingEvent();
-        generatePulsingAnimationEvent();
     }
 
     @ShadeViewRefactor(RefactorComponent.STATE_RESOLVER)
@@ -4354,10 +4365,12 @@
     }
 
     @ShadeViewRefactor(RefactorComponent.COORDINATOR)
-    public void setIsExpanded(boolean isExpanded) {
+    private void setIsExpanded(boolean isExpanded) {
         boolean changed = isExpanded != mIsExpanded;
         mIsExpanded = isExpanded;
         mStackScrollAlgorithm.setIsExpanded(isExpanded);
+        mAmbientState.setShadeExpanded(isExpanded);
+        mStateAnimator.setShadeExpanded(isExpanded);
         if (changed) {
             if (!mIsExpanded) {
                 mGroupManager.collapseAllGroups();
@@ -5099,12 +5112,6 @@
         }
     }
 
-    @ShadeViewRefactor(RefactorComponent.SHADE_VIEW)
-    public void setShadeExpanded(boolean shadeExpanded) {
-        mAmbientState.setShadeExpanded(shadeExpanded);
-        mStateAnimator.setShadeExpanded(shadeExpanded);
-    }
-
     /**
      * Set the boundary for the bottom heads up position. The heads up will always be above this
      * position.
@@ -5166,24 +5173,12 @@
             return;
         }
         mPulsing = pulsing;
-        mNeedingPulseAnimation = animated ? getFirstChildNotGone() : null;
         mAmbientState.setPulsing(pulsing);
         updateNotificationAnimationStates();
         updateAlgorithmHeightAndPadding();
         updateContentHeight();
         requestChildrenUpdate();
         notifyHeightChangeListener(null, animated);
-        mNeedsAnimation |= animated;
-    }
-
-    @ShadeViewRefactor(RefactorComponent.SHADE_VIEW)
-    private void generatePulsingAnimationEvent() {
-        if (mNeedingPulseAnimation != null) {
-            int type = mPulsing ? AnimationEvent.ANIMATION_TYPE_PULSE_APPEAR
-                    : AnimationEvent.ANIMATION_TYPE_PULSE_DISAPPEAR;
-            mAnimationEvents.add(new AnimationEvent(mNeedingPulseAnimation, type));
-            mNeedingPulseAnimation = null;
-        }
     }
 
     @ShadeViewRefactor(RefactorComponent.SHADE_VIEW)
@@ -5626,6 +5621,27 @@
     }
 
     /**
+     * Set how far the wake up is when waking up from pulsing. This is a height and will adjust the
+     * notification positions accordingly.
+     * @param height the new wake up height
+     * @return the overflow how much the height is further than he lowest notification
+     */
+    public float setPulseWakeUpHeight(float height) {
+        mAmbientState.setPulseWakeUpHeight(height);
+        requestChildrenUpdate();
+        return Math.max(0, height - mAmbientState.getInnerHeight(true /* ignorePulseHeight */));
+    }
+
+    /**
+     * Set the amount how much we're dozing. This is different from how dark the shade is, when
+     * the notification is pulsing.
+     */
+    public void setDozeAmount(float dozeAmount) {
+        mAmbientState.setDozeAmount(dozeAmount);
+        requestChildrenUpdate();
+    }
+
+    /**
      * A listener that is notified when the empty space below the notifications is clicked on
      */
     @ShadeViewRefactor(RefactorComponent.SHADE_VIEW)
@@ -5861,18 +5877,6 @@
                         .animateTopInset()
                         .animateY()
                         .animateZ(),
-
-                // ANIMATION_TYPE_PULSE_APPEAR
-                new AnimationFilter()
-                        .animateAlpha()
-                        .hasDelays()
-                        .animateY(),
-
-                // ANIMATION_TYPE_PULSE_DISAPPEAR
-                new AnimationFilter()
-                        .animateAlpha()
-                        .hasDelays()
-                        .animateY(),
         };
 
         static int[] LENGTHS = new int[]{
@@ -5927,12 +5931,6 @@
 
                 // ANIMATION_TYPE_EVERYTHING
                 StackStateAnimator.ANIMATION_DURATION_STANDARD,
-
-                // ANIMATION_TYPE_PULSE_APPEAR
-                StackStateAnimator.ANIMATION_DURATION_PULSE_APPEAR,
-
-                // ANIMATION_TYPE_PULSE_DISAPPEAR
-                StackStateAnimator.ANIMATION_DURATION_PULSE_APPEAR / 2,
         };
 
         static final int ANIMATION_TYPE_ADD = 0;
@@ -5952,8 +5950,6 @@
         static final int ANIMATION_TYPE_HEADS_UP_DISAPPEAR_CLICK = 14;
         static final int ANIMATION_TYPE_HEADS_UP_OTHER = 15;
         static final int ANIMATION_TYPE_EVERYTHING = 16;
-        static final int ANIMATION_TYPE_PULSE_APPEAR = 17;
-        static final int ANIMATION_TYPE_PULSE_DISAPPEAR = 18;
 
         static final int DARK_ANIMATION_ORIGIN_INDEX_ABOVE = -1;
         static final int DARK_ANIMATION_ORIGIN_INDEX_BELOW = -2;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateAnimator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateAnimator.java
index b4c205a..7332b03 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateAnimator.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateAnimator.java
@@ -419,32 +419,6 @@
                 ExpandableNotificationRow row = (ExpandableNotificationRow) event.mChangingView;
                 row.prepareExpansionChanged();
             } else if (event.animationType == NotificationStackScrollLayout
-                    .AnimationEvent.ANIMATION_TYPE_PULSE_APPEAR) {
-                ExpandableViewState viewState = changingView.getViewState();
-                if (viewState != null) {
-                    mTmpState.copyFrom(viewState);
-                    mTmpState.yTranslation += mPulsingAppearingTranslation;
-                    mTmpState.alpha = 0;
-                    mTmpState.applyToView(changingView);
-
-                    mTmpState.copyFrom(mShelf.getViewState());
-                    mTmpState.applyToView(mShelf);
-                }
-            } else if (event.animationType == NotificationStackScrollLayout
-                    .AnimationEvent.ANIMATION_TYPE_PULSE_DISAPPEAR) {
-                ExpandableViewState viewState = changingView.getViewState();
-                if (viewState != null) {
-                    viewState.alpha = 0;
-                    // We want to animate the alpha away before the view starts translating,
-                    // otherwise everything will overlap and look xtra ugly.
-                    float originalYTranslation = viewState.yTranslation;
-                    viewState.yTranslation = changingView.getTranslationY();
-                    mAnimationFilter.animateAlpha = true;
-                    mAnimationProperties.duration = ANIMATION_DURATION_PULSE_APPEAR / 2;
-                    viewState.animateTo(changingView, mAnimationProperties);
-                    viewState.yTranslation = originalYTranslation;
-                }
-            } else if (event.animationType == NotificationStackScrollLayout
                     .AnimationEvent.ANIMATION_TYPE_HEADS_UP_APPEAR) {
                 // This item is added, initialize it's properties.
                 ExpandableViewState viewState = changingView.getViewState();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java
index ce4d95f..4398ae0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java
@@ -18,6 +18,7 @@
 
 import static com.android.systemui.SysUiServiceProvider.getComponent;
 import static com.android.systemui.statusbar.notification.ActivityLaunchAnimator.ExpandAnimationParameters;
+import static com.android.systemui.util.InjectionInflationController.VIEW_CONTEXT;
 
 
 import static com.android.systemui.util.InjectionInflationController.VIEW_CONTEXT;
@@ -75,6 +76,7 @@
 import com.android.systemui.statusbar.KeyguardIndicationController;
 import com.android.systemui.statusbar.NotificationLockscreenUserManager;
 import com.android.systemui.statusbar.NotificationShelf;
+import com.android.systemui.statusbar.PulseExpansionHandler;
 import com.android.systemui.statusbar.RemoteInputController;
 import com.android.systemui.statusbar.StatusBarState;
 import com.android.systemui.statusbar.notification.ActivityLaunchAnimator;
@@ -108,7 +110,8 @@
         View.OnClickListener, NotificationStackScrollLayout.OnOverscrollTopChangedListener,
         KeyguardAffordanceHelper.Callback, NotificationStackScrollLayout.OnEmptySpaceClickListener,
         OnHeadsUpChangedListener, QS.HeightListener, ZenModeController.Callback,
-        ConfigurationController.ConfigurationListener, StateListener {
+        ConfigurationController.ConfigurationListener, StateListener,
+        PulseExpansionHandler.ExpansionCallback {
 
     private static final boolean DEBUG = false;
 
@@ -144,6 +147,7 @@
     private final PowerManager mPowerManager;
     private final AccessibilityManager mAccessibilityManager;
     private final NotificationWakeUpCoordinator mWakeUpCoordinator;
+    private final PulseExpansionHandler mPulseExpansionHandler;
 
     private KeyguardAffordanceHelper mAffordanceHelper;
     private KeyguardUserSwitcher mKeyguardUserSwitcher;
@@ -327,7 +331,8 @@
 
     @Inject
     public NotificationPanelView(@Named(VIEW_CONTEXT) Context context, AttributeSet attrs,
-            NotificationWakeUpCoordinator coordinator) {
+            NotificationWakeUpCoordinator coordinator,
+            PulseExpansionHandler pulseExpansionHandler) {
         super(context, attrs);
         setWillNotDraw(!DEBUG);
         mFalsingManager = FalsingManager.getInstance(context);
@@ -339,6 +344,7 @@
         setPanelAlpha(255, false /* animate */);
         mCommandQueue = getComponent(context, CommandQueue.class);
         mDisplayId = context.getDisplayId();
+        mPulseExpansionHandler = pulseExpansionHandler;
     }
 
     /**
@@ -377,6 +383,7 @@
 
         mWakeUpCoordinator.setStackScroller(mNotificationStackScroller);
         mQsFrame = findViewById(R.id.qs_frame);
+        mPulseExpansionHandler.setUp(mNotificationStackScroller, this, mShadeController);
     }
 
     @Override
@@ -798,6 +805,9 @@
             MetricsLogger.count(mContext, COUNTER_PANEL_OPEN_PEEK, 1);
             return true;
         }
+        if (mPulseExpansionHandler.onInterceptTouchEvent(event)) {
+            return true;
+        }
 
         if (!isFullyCollapsed() && onQsIntercept(event)) {
             return true;
@@ -957,6 +967,10 @@
             return false;
         }
         initDownStates(event);
+        if (!mIsExpanding && mPulseExpansionHandler.onTouchEvent(event)) {
+            // We're expanding all the other ones shouldn't get this anymore
+            return true;
+        }
         if (mListenForHeadsUp && !mHeadsUpTouchHelper.isTrackingHeadsUp()
                 && mHeadsUpTouchHelper.onInterceptTouchEvent(event)) {
             mIsExpansionFromHeadsUp = true;
@@ -1500,16 +1514,16 @@
             int max = mBarState == StatusBarState.KEYGUARD
                     ? Math.max(maxNotificationPadding, maxQsPadding)
                     : maxQsPadding;
-            return (int) interpolate(getExpandedFraction(),
-                    mQsMinExpansionHeight, max);
+            return (int) MathUtils.lerp((float) mQsMinExpansionHeight, (float) max,
+                    getExpandedFraction());
         } else if (mQsSizeChangeAnimator != null) {
             return (int) mQsSizeChangeAnimator.getAnimatedValue();
         } else if (mKeyguardShowing) {
             // We can only do the smoother transition on Keyguard when we also are not collapsing
             // from a scrolled quick settings.
-            return interpolate(getQsExpansionFraction(),
-                    mNotificationStackScroller.getIntrinsicPadding(),
-                    mQsMaxExpansionHeight + mQsNotificationTopPadding);
+            return MathUtils.lerp((float) mNotificationStackScroller.getIntrinsicPadding(),
+                    (float) (mQsMaxExpansionHeight + mQsNotificationTopPadding),
+                    getQsExpansionFraction());
         } else {
             return mQsExpansionHeight + mQsNotificationTopPadding;
         }
@@ -1710,7 +1724,6 @@
         updateUnlockIcon();
         updateNotificationTranslucency();
         updatePanelExpanded();
-        mNotificationStackScroller.setShadeExpanded(!isFullyCollapsed());
         if (DEBUG) {
             invalidate();
         }
@@ -2824,9 +2837,7 @@
 
         final float darkAmount = dozing ? 1 : 0;
         mStatusBarStateController.setDozeAmount(darkAmount, animate);
-        if (animate) {
-            mWakeUpCoordinator.notifyAnimationStart(!mDozing);
-        }
+        mWakeUpCoordinator.setDozing(mDozing, animate);
     }
 
     @Override
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 41580f6..a1560bd 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
@@ -184,6 +184,7 @@
 import com.android.systemui.statusbar.NotificationRemoteInputManager;
 import com.android.systemui.statusbar.NotificationShelf;
 import com.android.systemui.statusbar.NotificationViewHierarchyManager;
+import com.android.systemui.statusbar.PulseExpansionHandler;
 import com.android.systemui.statusbar.ScrimView;
 import com.android.systemui.statusbar.StatusBarState;
 import com.android.systemui.statusbar.SysuiStatusBarStateController;
@@ -363,6 +364,8 @@
     protected StatusBarIconController mIconController;
     @Inject
     InjectionInflationController mInjectionInflater;
+    @Inject
+    PulseExpansionHandler mPulseExpansionHandler;
 
     // expanded notifications
     protected NotificationPanelView mNotificationPanel; // the sliding/resizing panel within the notification window
@@ -1079,7 +1082,7 @@
 
     @Override
     public boolean isDozing() {
-        return mDozing && mNotificationPanel.isFullyDark();
+        return mDozing;
     }
 
     @Override
@@ -2965,7 +2968,7 @@
         mHandler.removeMessages(MSG_LAUNCH_TRANSITION_TIMEOUT);
         if (mUserSwitcherController != null && mUserSwitcherController.useFullscreenUserSwitcher()) {
             mStatusBarStateController.setState(StatusBarState.FULLSCREEN_USER_SWITCHER);
-        } else {
+        } else if (!mPulseExpansionHandler.isWakingToShadeLocked()){
             mStatusBarStateController.setState(StatusBarState.KEYGUARD);
         }
         updatePanelExpansionForKeyguard();
@@ -3594,6 +3597,7 @@
             updateVisibleToUser();
             updateIsKeyguard();
             mDozeServiceHost.stopDozing();
+            mPulseExpansionHandler.onStartedWakingUp();
         }
     };
 
@@ -3896,6 +3900,7 @@
                         mKeyguardUpdateMonitor.onAuthInterruptDetected(pulsing /* active */);
                     }
                     updateScrimController();
+                    mPulseExpansionHandler.setPulsing(pulsing);
                 }
             }, reason);
             // DozeScrimController is in pulse state, now let's ask ScrimController to start
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarWindowView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarWindowView.java
index 50c4fac..a8ae5f6 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarWindowView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarWindowView.java
@@ -388,7 +388,8 @@
         if (mNotificationPanel.isFullyExpanded()
                 && stackScrollLayout.getVisibility() == View.VISIBLE
                 && mStatusBarStateController.getState() == StatusBarState.KEYGUARD
-                && !mService.isBouncerShowing()) {
+                && !mService.isBouncerShowing()
+                && !mService.isDozing()) {
             intercept = mDragDownHelper.onInterceptTouchEvent(ev);
         }
         if (!intercept) {