Change interaction for dragging down to full shade.

Starts expanding the card if it is expandable. If it's not, we
rubberband the expansion a lot. After the threshold has been reached,
we scale all the notifications up and make it fully white and opaque.

Change-Id: I9ecd020ca263b0f84e87fd6ab2332519ac5e9984
diff --git a/packages/SystemUI/src/com/android/systemui/ExpandHelper.java b/packages/SystemUI/src/com/android/systemui/ExpandHelper.java
index 61c268e..4d6d815 100644
--- a/packages/SystemUI/src/com/android/systemui/ExpandHelper.java
+++ b/packages/SystemUI/src/com/android/systemui/ExpandHelper.java
@@ -29,17 +29,16 @@
 import android.view.ScaleGestureDetector;
 import android.view.ScaleGestureDetector.OnScaleGestureListener;
 import android.view.View;
-import android.view.View.OnClickListener;
 import android.view.ViewConfiguration;
 
-import com.android.systemui.statusbar.ExpandableView;
 import com.android.systemui.statusbar.ExpandableNotificationRow;
+import com.android.systemui.statusbar.ExpandableView;
 import com.android.systemui.statusbar.policy.ScrollAdapter;
 
-public class ExpandHelper implements Gefingerpoken, OnClickListener {
+public class ExpandHelper implements Gefingerpoken {
     public interface Callback {
-        View getChildAtRawPosition(float x, float y);
-        View getChildAtPosition(float x, float y);
+        ExpandableView getChildAtRawPosition(float x, float y);
+        ExpandableView getChildAtPosition(float x, float y);
         boolean canChildBeExpanded(View v);
         void setUserExpandedChild(View v, boolean userExpanded);
         void setUserLockedChild(View v, boolean userLocked);
@@ -48,9 +47,7 @@
     private static final String TAG = "ExpandHelper";
     protected static final boolean DEBUG = false;
     protected static final boolean DEBUG_SCALE = false;
-    protected static final boolean DEBUG_GLOW = false;
     private static final long EXPAND_DURATION = 250;
-    private static final long GLOW_DURATION = 150;
 
     // Set to false to disable focus-based gestures (spread-finger vertical pull).
     private static final boolean USE_DRAG = true;
@@ -115,7 +112,7 @@
             float focusX = detector.getFocusX();
             float focusY = detector.getFocusY();
 
-            final View underFocus = findView(focusX, focusY);
+            final ExpandableView underFocus = findView(focusX, focusY);
             startExpanding(underFocus, STRETCH);
             return mExpanding;
         }
@@ -172,24 +169,6 @@
         mPopDuration = mContext.getResources().getInteger(R.integer.blinds_pop_duration_ms);
         mPullGestureMinXSpan = mContext.getResources().getDimension(R.dimen.pull_span_min);
 
-        AnimatorListenerAdapter glowVisibilityController = new AnimatorListenerAdapter() {
-            @Override
-            public void onAnimationStart(Animator animation) {
-                View target = (View) ((ObjectAnimator) animation).getTarget();
-                if (target.getAlpha() <= 0.0f) {
-                    target.setVisibility(View.VISIBLE);
-                }
-            }
-
-            @Override
-            public void onAnimationEnd(Animator animation) {
-                View target = (View) ((ObjectAnimator) animation).getTarget();
-                if (target.getAlpha() <= 0.0f) {
-                    target.setVisibility(View.INVISIBLE);
-                }
-            }
-        };
-
         final ViewConfiguration configuration = ViewConfiguration.get(mContext);
         mTouchSlop = configuration.getScaledTouchSlop();
 
@@ -221,8 +200,8 @@
         return out;
     }
 
-    private View findView(float x, float y) {
-        View v = null;
+    private ExpandableView findView(float x, float y) {
+        ExpandableView v;
         if (mEventSource != null) {
             int[] location = new int[2];
             mEventSource.getLocationOnScreen(location);
@@ -271,15 +250,6 @@
         mScrollAdapter = adapter;
     }
 
-    private float calculateGlow(float target, float actual) {
-        // glow if overscale
-        if (DEBUG_GLOW) Log.d(TAG, "target: " + target + " actual: " + actual);
-        float stretch = Math.abs((target - actual) / mMaximumStretch);
-        float strength = 1f / (1f + (float) Math.pow(Math.E, -1 * ((8f * stretch) - 5f)));
-        if (DEBUG_GLOW) Log.d(TAG, "stretch: " + stretch + " strength: " + strength);
-        return (GLOW_BASE + strength * (1f - GLOW_BASE));
-    }
-
     @Override
     public boolean onInterceptTouchEvent(MotionEvent ev) {
         final int action = ev.getAction();
@@ -313,7 +283,7 @@
                 // detect a vertical pulling gesture with fingers somewhat separated
                 if (DEBUG_SCALE) Log.v(TAG, "got pull gesture (xspan=" + xspan + "px)");
 
-                final View underFocus = findView(x, y);
+                final ExpandableView underFocus = findView(x, y);
                 startExpanding(underFocus, PULL);
                 return true;
             }
@@ -328,7 +298,7 @@
                     if (yDiff > mTouchSlop) {
                         if (DEBUG) Log.v(TAG, "got venetian gesture (dy=" + yDiff + "px)");
                         mLastMotionY = y;
-                        final View underFocus = findView(x, y);
+                        final ExpandableView underFocus = findView(x, y);
                         if (startExpanding(underFocus, BLINDS)) {
                             mInitialTouchY = mLastMotionY;
                             mHasPopped = false;
@@ -394,7 +364,7 @@
 
                     final int x = (int) mSGD.getFocusX();
                     final int y = (int) mSGD.getFocusY();
-                    View underFocus = findView(x, y);
+                    ExpandableView underFocus = findView(x, y);
                     if (isFinished && underFocus != null && underFocus != mCurrView) {
                         finishExpanding(false); // @@@ needed?
                         startExpanding(underFocus, BLINDS);
@@ -432,7 +402,7 @@
     /**
      * @return True if the view is expandable, false otherwise.
      */
-    private boolean startExpanding(View v, int expandType) {
+    private boolean startExpanding(ExpandableView v, int expandType) {
         if (!(v instanceof ExpandableNotificationRow)) {
             return false;
         }
@@ -512,13 +482,6 @@
         mCurrView = v;
     }
 
-    @Override
-    public void onClick(View v) {
-        startExpanding(v, STRETCH);
-        finishExpanding(true);
-        clearView();
-    }
-
     /**
      * Use this to abort any pending expansions in progress.
      */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/ActivatableNotificationView.java b/packages/SystemUI/src/com/android/systemui/statusbar/ActivatableNotificationView.java
index 5a55292..f5f2eb79 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/ActivatableNotificationView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/ActivatableNotificationView.java
@@ -16,12 +16,18 @@
 
 package com.android.systemui.statusbar;
 
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ObjectAnimator;
 import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.drawable.Drawable;
 import android.util.AttributeSet;
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.ViewConfiguration;
-import android.widget.FrameLayout;
+import android.view.animation.AnimationUtils;
+import android.view.animation.Interpolator;
 
 import com.android.internal.R;
 
@@ -34,7 +40,6 @@
     private static final long DOUBLETAP_TIMEOUT_MS = 1000;
 
     private boolean mDimmed;
-    private boolean mLocked;
 
     private int mBgResId = R.drawable.notification_quantum_bg;
     private int mDimmedBgResId = R.drawable.notification_quantum_bg_dim;
@@ -51,13 +56,20 @@
 
     private OnActivatedListener mOnActivatedListener;
 
+    protected Drawable mBackgroundNormal;
+    protected Drawable mBackgroundDimmed;
+    private ObjectAnimator mBackgroundAnimator;
+    private Interpolator mFastOutSlowInInterpolator;
+
     public ActivatableNotificationView(Context context, AttributeSet attrs) {
         super(context, attrs);
         mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
         updateBackgroundResource();
+        setWillNotDraw(false);
+        mFastOutSlowInInterpolator =
+                AnimationUtils.loadInterpolator(context, android.R.interpolator.fast_out_slow_in);
     }
 
-
     private final Runnable mTapTimeoutRunnable = new Runnable() {
         @Override
         public void run() {
@@ -66,20 +78,51 @@
     };
 
     @Override
+    protected void onDraw(Canvas canvas) {
+        draw(canvas, mBackgroundNormal);
+        draw(canvas, mBackgroundDimmed);
+    }
+
+    private void draw(Canvas canvas, Drawable drawable) {
+        if (drawable != null) {
+            drawable.setBounds(0, mClipTopAmount, getWidth(), mActualHeight);
+            drawable.draw(canvas);
+        }
+    }
+
+    @Override
+    protected boolean verifyDrawable(Drawable who) {
+        return super.verifyDrawable(who) || who == mBackgroundNormal
+                || who == mBackgroundDimmed;
+    }
+
+    @Override
+    protected void drawableStateChanged() {
+        drawableStateChanged(mBackgroundNormal);
+        drawableStateChanged(mBackgroundDimmed);
+    }
+
+    private void drawableStateChanged(Drawable d) {
+        if (d != null && d.isStateful()) {
+            d.setState(getDrawableState());
+        }
+    }
+
+    @Override
     public void setOnClickListener(OnClickListener l) {
         super.setOnClickListener(l);
     }
 
     @Override
     public boolean onTouchEvent(MotionEvent event) {
-        if (mLocked) {
-            return handleTouchEventLocked(event);
+        if (mDimmed) {
+            return handleTouchEventDimmed(event);
         } else {
             return super.onTouchEvent(event);
         }
     }
 
-    private boolean handleTouchEventLocked(MotionEvent event) {
+    private boolean handleTouchEventDimmed(MotionEvent event) {
         int action = event.getActionMasked();
         switch (action) {
             case MotionEvent.ACTION_DOWN:
@@ -118,7 +161,7 @@
     }
 
     private void makeActive(float x, float y) {
-        mCustomBackground.setHotspot(0, x, y);
+        mBackgroundDimmed.setHotspot(0, x, y);
         mActivated = true;
         if (mOnActivatedListener != null) {
             mOnActivatedListener.onActivated(this);
@@ -131,8 +174,8 @@
     private void makeInactive() {
         if (mActivated) {
             // Make sure that we clear the hotspot from the center.
-            mCustomBackground.setHotspot(0, getWidth() / 2, getActualHeight() / 2);
-            mCustomBackground.removeHotspot(0);
+            mBackgroundDimmed.setHotspot(0, getWidth() / 2, getActualHeight() / 2);
+            mBackgroundDimmed.removeHotspot(0);
             mActivated = false;
         }
         if (mOnActivatedListener != null) {
@@ -148,23 +191,22 @@
 
     /**
      * Sets the notification as dimmed, meaning that it will appear in a more gray variant.
+     *
+     * @param dimmed Whether the notification should be dimmed.
+     * @param fade Whether an animation should be played to change the state.
      */
-    public void setDimmed(boolean dimmed) {
+    public void setDimmed(boolean dimmed, boolean fade) {
         if (mDimmed != dimmed) {
             mDimmed = dimmed;
-            updateBackgroundResource();
+            if (fade) {
+                fadeBackgroundResource();
+            } else {
+                updateBackgroundResource();
+            }
         }
     }
 
     /**
-     * Sets the notification as locked. In the locked state, the first tap will produce a quantum
-     * ripple to make the notification brighter and only the second tap will cause a click.
-     */
-    public void setLocked(boolean locked) {
-        mLocked = locked;
-    }
-
-    /**
      * Sets the resource id for the background of this notification.
      *
      * @param bgResId The background resource to use in normal state.
@@ -176,20 +218,106 @@
         updateBackgroundResource();
     }
 
+    private void fadeBackgroundResource() {
+        if (mDimmed) {
+            setBackgroundDimmed(mDimmedBgResId);
+        } else {
+            setBackgroundNormal(mBgResId);
+        }
+        int startAlpha = mDimmed ? 255 : 0;
+        int endAlpha = mDimmed ? 0 : 255;
+        int duration = NotificationActivator.ANIMATION_LENGTH_MS;
+        // Check whether there is already a background animation running.
+        if (mBackgroundAnimator != null) {
+            startAlpha = (Integer) mBackgroundAnimator.getAnimatedValue();
+            duration = (int) (NotificationActivator.ANIMATION_LENGTH_MS
+                                - mBackgroundAnimator.getCurrentPlayTime());
+            mBackgroundAnimator.removeAllListeners();
+            mBackgroundAnimator.cancel();
+        }
+        mBackgroundNormal.setAlpha(startAlpha);
+        mBackgroundAnimator =
+                ObjectAnimator.ofInt(mBackgroundNormal, "alpha", startAlpha, endAlpha);
+        mBackgroundAnimator.setInterpolator(mFastOutSlowInInterpolator);
+        mBackgroundAnimator.setDuration(duration);
+        mBackgroundAnimator.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                if (mDimmed) {
+                    setBackgroundNormal(null);
+                } else {
+                    setBackgroundDimmed(null);
+                }
+                mBackgroundAnimator = null;
+            }
+        });
+        mBackgroundAnimator.start();
+    }
+
     private void updateBackgroundResource() {
-        setCustomBackgroundResource(mDimmed ? mDimmedBgResId : mBgResId);
+        if (mDimmed) {
+            setBackgroundDimmed(mDimmedBgResId);
+            setBackgroundNormal(null);
+        } else {
+            setBackgroundDimmed(null);
+            setBackgroundNormal(mBgResId);
+        }
+    }
+
+    /**
+     * Sets a background drawable for the normal state. As we need to change our bounds
+     * independently of layout, we need the notion of a background independently of the regular View
+     * background..
+     */
+    private void setBackgroundNormal(Drawable backgroundNormal) {
+        if (mBackgroundNormal != null) {
+            mBackgroundNormal.setCallback(null);
+            unscheduleDrawable(mBackgroundNormal);
+        }
+        mBackgroundNormal = backgroundNormal;
+        if (mBackgroundNormal != null) {
+            mBackgroundNormal.setCallback(this);
+        }
+        invalidate();
+    }
+
+    private void setBackgroundDimmed(Drawable overlay) {
+        if (mBackgroundDimmed != null) {
+            mBackgroundDimmed.setCallback(null);
+            unscheduleDrawable(mBackgroundDimmed);
+        }
+        mBackgroundDimmed = overlay;
+        if (mBackgroundDimmed != null) {
+            mBackgroundDimmed.setCallback(this);
+        }
+        invalidate();
+    }
+
+    private void setBackgroundNormal(int drawableResId) {
+        setBackgroundNormal(getResources().getDrawable(drawableResId));
+    }
+
+    private void setBackgroundDimmed(int drawableResId) {
+        setBackgroundDimmed(getResources().getDrawable(drawableResId));
     }
 
     @Override
     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
         super.onLayout(changed, left, top, right, bottom);
-        setPivotX(getWidth()/2);
+        setPivotX(getWidth() / 2);
     }
 
     @Override
     public void setActualHeight(int actualHeight) {
         super.setActualHeight(actualHeight);
-        setPivotY(actualHeight/2);
+        invalidate();
+        setPivotY(actualHeight / 2);
+    }
+
+    @Override
+    public void setClipTopAmount(int clipTopAmount) {
+        super.setClipTopAmount(clipTopAmount);
+        invalidate();
     }
 
     public void setOnActivatedListener(OnActivatedListener onActivatedListener) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java
index 829cee4..236e0e9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java
@@ -171,6 +171,7 @@
      * The {@link StatusBarState} of the status bar.
      */
     protected int mState;
+    protected boolean mBouncerShowing;
 
     protected NotificationOverflowContainer mKeyguardIconOverflowContainer;
 
@@ -1073,8 +1074,7 @@
                     entry.row.setSystemExpanded(top);
                 }
             }
-            entry.row.setDimmed(onKeyguard);
-            entry.row.setLocked(onKeyguard);
+            entry.row.setDimmed(onKeyguard, false /* fade */);
             boolean showOnKeyguard = shouldShowOnKeyguard(entry.notification);
             if (onKeyguard && (visibleNotifications >= maxKeyguardNotifications
                     || !showOnKeyguard)) {
@@ -1094,6 +1094,7 @@
 
         if (onKeyguard && mKeyguardIconOverflowContainer.getIconsView().getChildCount() > 0) {
             mKeyguardIconOverflowContainer.setVisibility(View.VISIBLE);
+            mKeyguardIconOverflowContainer.setDimmed(true /* dimmed */, false /* fade */);
         } else {
             mKeyguardIconOverflowContainer.setVisibility(View.GONE);
         }
@@ -1403,6 +1404,17 @@
         // hook for subclasses
     }
 
+    public void setBouncerShowing(boolean bouncerShowing) {
+        mBouncerShowing = bouncerShowing;
+    }
+
+    /**
+     * @return Whether the security bouncer from Keyguard is showing.
+     */
+    public boolean isBouncerShowing() {
+        return mBouncerShowing;
+    }
+
     public void destroy() {
         if (mSearchPanelView != null) {
             mWindowManager.removeViewImmediate(mSearchPanelView);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/DragDownHelper.java b/packages/SystemUI/src/com/android/systemui/statusbar/DragDownHelper.java
index af4c8b8..699c83f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/DragDownHelper.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/DragDownHelper.java
@@ -16,24 +16,31 @@
 
 package com.android.systemui.statusbar;
 
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ObjectAnimator;
 import android.content.Context;
-import android.util.ArraySet;
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.ViewConfiguration;
+import android.view.animation.AnimationUtils;
+import android.view.animation.Interpolator;
 
 import com.android.systemui.ExpandHelper;
 import com.android.systemui.Gefingerpoken;
 import com.android.systemui.R;
 
-import java.util.HashSet;
-
 /**
  * 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 ExpandHelper.Callback mCallback;
     private float mInitialTouchX;
@@ -43,14 +50,16 @@
     private OnDragDownListener mOnDragDownListener;
     private View mHost;
     private final int[] mTemp2 = new int[2];
-    private final ArraySet<View> mHoveredChildren = new ArraySet<View>();
     private boolean mDraggedFarEnough;
-    private View mStartingChild;
+    private ExpandableView mStartingChild;
+    private Interpolator mInterpolator;
 
     public DragDownHelper(Context context, View host, ExpandHelper.Callback callback,
             OnDragDownListener onDragDownListener) {
         mMinDragDistance = context.getResources().getDimensionPixelSize(
                 R.dimen.keyguard_drag_down_min_distance);
+        mInterpolator =
+                AnimationUtils.loadInterpolator(context, android.R.interpolator.fast_out_slow_in);
         mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
         mCallback = callback;
         mOnDragDownListener = onDragDownListener;
@@ -64,7 +73,6 @@
 
         switch (event.getActionMasked()) {
             case MotionEvent.ACTION_DOWN:
-                mHoveredChildren.clear();
                 mDraggedFarEnough = false;
                 mDraggingDown = false;
                 mStartingChild = null;
@@ -76,6 +84,7 @@
                 final float h = y - mInitialTouchY;
                 if (h > mTouchSlop && h > Math.abs(x - mInitialTouchX)) {
                     mDraggingDown = true;
+                    captureStartingChild(mInitialTouchX, mInitialTouchY);
                     mInitialTouchY = y;
                     mInitialTouchX = x;
                     return true;
@@ -96,9 +105,9 @@
         switch (event.getActionMasked()) {
             case MotionEvent.ACTION_MOVE:
                 final float h = y - mInitialTouchY;
-                View child = findView(x, y);
-                if (child != null) {
-                    hoverChild(findView(x, y));
+                captureStartingChild(mInitialTouchX, mInitialTouchY);
+                if (mStartingChild != null) {
+                    handleExpansion(h, mStartingChild);
                 }
                 if (h > mMinDragDistance) {
                     if (!mDraggedFarEnough) {
@@ -114,6 +123,9 @@
                 return true;
             case MotionEvent.ACTION_UP:
                 if (mDraggedFarEnough) {
+                    if (mStartingChild != null) {
+                        mCallback.setUserLockedChild(mStartingChild, false);
+                    }
                     mOnDragDownListener.onDraggedDown(mStartingChild);
                 } else {
                     stopDragging();
@@ -127,22 +139,58 @@
         return false;
     }
 
+    private void captureStartingChild(float x, float y) {
+        if (mStartingChild == null) {
+            mStartingChild = findView(x, y);
+            if (mStartingChild != null) {
+                mCallback.setUserLockedChild(mStartingChild, true);
+            }
+        }
+    }
+
+    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.getMinHeight()) > child.getMaxHeight()) {
+            float overshoot = (rubberband + child.getMinHeight()) - child.getMaxHeight();
+            overshoot *= (1 - RUBBERBAND_FACTOR_STATIC);
+            rubberband -= overshoot;
+        }
+        child.setActualHeight((int) (child.getMinHeight() + rubberband));
+    }
+
+    private void cancelExpansion(final ExpandableView child) {
+        if (child.getActualHeight() == child.getMinHeight()) {
+            return;
+        }
+        ObjectAnimator anim = ObjectAnimator.ofInt(child, "actualHeight",
+                child.getActualHeight(), child.getMinHeight());
+        anim.setInterpolator(mInterpolator);
+        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 stopDragging() {
+        if (mStartingChild != null) {
+            cancelExpansion(mStartingChild);
+        }
         mDraggingDown = false;
         mOnDragDownListener.onReset();
     }
 
-    private void hoverChild(View child) {
-        if (mHoveredChildren.isEmpty()) {
-            mStartingChild = child;
-        }
-        if (!mHoveredChildren.contains(child)) {
-            mOnDragDownListener.onHover(child);
-            mHoveredChildren.add(child);
-        }
-    }
-
-    private View findView(float x, float y) {
+    private ExpandableView findView(float x, float y) {
         mHost.getLocationOnScreen(mTemp2);
         x += mTemp2[0];
         y += mTemp2[1];
@@ -150,7 +198,6 @@
     }
 
     public interface OnDragDownListener {
-        void onHover(View child);
         void onDraggedDown(View startingChild);
         void onReset();
         void onThresholdReached();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableNotificationRow.java
index 5b70cd3..a6dcbeb 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableNotificationRow.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableNotificationRow.java
@@ -23,8 +23,7 @@
 
 import com.android.systemui.R;
 
-public class ExpandableNotificationRow extends ActivatableNotificationView implements
-        NotificationActivatable {
+public class ExpandableNotificationRow extends ActivatableNotificationView {
     private int mRowMinHeight;
     private int mRowMaxHeight;
 
@@ -114,7 +113,6 @@
     public void setUserExpanded(boolean userExpanded) {
         mHasUserChangedExpansion = true;
         mUserExpanded = userExpanded;
-        applyExpansionToLayout();
     }
 
     public boolean isUserLocked() {
@@ -218,16 +216,16 @@
     /**
      * Sets the notification as dimmed, meaning that it will appear in a more gray variant.
      */
-    public void setDimmed(boolean dimmed) {
-        super.setDimmed(dimmed);
-        mActivator.setDimmed(dimmed);
+    @Override
+    public void setDimmed(boolean dimmed, boolean fade) {
+        super.setDimmed(dimmed, fade);
+        mActivator.setDimmed(dimmed, fade);
     }
 
     public int getMaxExpandHeight() {
         return mMaxExpandHeight;
     }
 
-    @Override
     public NotificationActivator getActivator() {
         return mActivator;
     }
@@ -240,6 +238,11 @@
     }
 
     @Override
+    public boolean isContentExpandable() {
+        return mPrivateLayout.isContentExpandable();
+    }
+
+    @Override
     public void setActualHeight(int height) {
         mPrivateLayout.setActualHeight(height);
         invalidate();
@@ -252,6 +255,11 @@
     }
 
     @Override
+    public int getMinHeight() {
+        return mPrivateLayout.getMinHeight();
+    }
+
+    @Override
     public void setClipTopAmount(int clipTopAmount) {
         super.setClipTopAmount(clipTopAmount);
         mPrivateLayout.setClipTopAmount(clipTopAmount);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableView.java b/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableView.java
index 35913fa..871c84b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableView.java
@@ -34,7 +34,6 @@
     private OnHeightChangedListener mOnHeightChangedListener;
     protected int mActualHeight;
     protected int mClipTopAmount;
-    protected Drawable mCustomBackground;
     private boolean mActualHeightInitialized;
 
     public ExpandableView(Context context, AttributeSet attrs) {
@@ -42,27 +41,6 @@
     }
 
     @Override
-    protected void onDraw(Canvas canvas) {
-        if (mCustomBackground != null) {
-            mCustomBackground.setBounds(0, mClipTopAmount, getWidth(), mActualHeight);
-            mCustomBackground.draw(canvas);
-        }
-    }
-
-    @Override
-    protected boolean verifyDrawable(Drawable who) {
-        return super.verifyDrawable(who) || who == mCustomBackground;
-    }
-
-    @Override
-    protected void drawableStateChanged() {
-        final Drawable d = mCustomBackground;
-        if (d != null && d.isStateful()) {
-            d.setState(getDrawableState());
-        }
-    }
-
-    @Override
     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
         super.onLayout(changed, left, top, right, bottom);
         if (!mActualHeightInitialized && mActualHeight == 0) {
@@ -77,7 +55,6 @@
      */
     public void setActualHeight(int actualHeight) {
         mActualHeight = actualHeight;
-        invalidate();
         if (mOnHeightChangedListener != null) {
             mOnHeightChangedListener.onHeightChanged(this);
         }
@@ -95,7 +72,16 @@
     /**
      * @return The maximum height of this notification.
      */
-    public abstract int getMaxHeight();
+    public int getMaxHeight() {
+        return getHeight();
+    }
+
+    /**
+     * @return The minimum height of this notification.
+     */
+    public int getMinHeight() {
+        return getHeight();
+    }
 
     /**
      * Sets the amount this view should be clipped from the top. This is used when an expanded
@@ -105,7 +91,6 @@
      */
     public void setClipTopAmount(int clipTopAmount) {
         mClipTopAmount = clipTopAmount;
-        invalidate();
     }
 
     public void setOnHeightChangedListener(OnHeightChangedListener listener) {
@@ -113,22 +98,10 @@
     }
 
     /**
-     * Sets a custom background drawable. As we need to change our bounds independently of layout,
-     * we need the notition of a custom background.
+     * @return Whether we can expand this views content.
      */
-    public void setCustomBackground(Drawable customBackground) {
-        if (mCustomBackground != null) {
-            mCustomBackground.setCallback(null);
-            unscheduleDrawable(mCustomBackground);
-        }
-        mCustomBackground = customBackground;
-        mCustomBackground.setCallback(this);
-        setWillNotDraw(customBackground == null);
-        invalidate();
-    }
-
-    public void setCustomBackgroundResource(int drawableResId) {
-        setCustomBackground(getResources().getDrawable(drawableResId));
+    public boolean isContentExpandable() {
+        return false;
     }
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationActivatable.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationActivatable.java
deleted file mode 100644
index 410a3aa..0000000
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationActivatable.java
+++ /dev/null
@@ -1,25 +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;
-
-/**
- * An interface which defines a view to be activatable in the meaning of
- * {@link NotificationActivator}.
- */
-public interface NotificationActivatable {
-    NotificationActivator getActivator();
-}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationActivator.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationActivator.java
index 097857c..a03aeec 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationActivator.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationActivator.java
@@ -30,7 +30,7 @@
  */
 public class NotificationActivator {
 
-    private static final int ANIMATION_LENGTH_MS = 220;
+    public static final int ANIMATION_LENGTH_MS = 220;
     private static final float INVERSE_ALPHA = 0.9f;
     private static final float DIMMED_SCALE = 0.95f;
 
@@ -118,20 +118,34 @@
         if (mTargetView.getAlpha() != 1.0f) {
             mTargetView.animate().withLayer().alpha(1);
         }
-        mHotspotView.getBackground().removeHotspot(0);
         mState = STATE_DIMMED;
     }
 
-    public void setDimmed(boolean dimmed) {
+    public void setDimmed(boolean dimmed, boolean fade) {
         if (dimmed) {
             mTargetView.animate().cancel();
-            mTargetView.setScaleX(DIMMED_SCALE);
-            mTargetView.setScaleY(DIMMED_SCALE);
+            if (fade) {
+                mTargetView.animate()
+                        .setInterpolator(mFastOutSlowInInterpolator)
+                        .scaleX(DIMMED_SCALE)
+                        .scaleY(DIMMED_SCALE);
+            } else {
+                mTargetView.setScaleX(DIMMED_SCALE);
+                mTargetView.setScaleY(DIMMED_SCALE);
+            }
             mState = STATE_DIMMED;
         } else {
             mTargetView.animate().cancel();
-            mTargetView.setScaleX(1);
-            mTargetView.setScaleY(1);
+            if (fade) {
+                mTargetView.animate()
+                        .setInterpolator(mFastOutSlowInInterpolator)
+                        .scaleX(1)
+                        .scaleY(1);
+            } else {
+                mTargetView.animate().cancel();
+                mTargetView.setScaleX(1);
+                mTargetView.setScaleY(1);
+            }
             mState = STATE_NORMAL;
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationContentView.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationContentView.java
index fd0cb08..1f15eaf 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationContentView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationContentView.java
@@ -84,6 +84,11 @@
     }
 
     @Override
+    public int getMinHeight() {
+        return mSmallHeight;
+    }
+
+    @Override
     public void setClipTopAmount(int clipTopAmount) {
         super.setClipTopAmount(clipTopAmount);
         updateClipping();
@@ -125,4 +130,9 @@
     public void notifyContentUpdated() {
         selectLayout();
     }
+
+    @Override
+    public boolean isContentExpandable() {
+        return mExpandedChild != null;
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationOverflowContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationOverflowContainer.java
index 6f9cbf8..e6b5600 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationOverflowContainer.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationOverflowContainer.java
@@ -25,8 +25,7 @@
 /**
  * Container view for overflowing notification icons on Keyguard.
  */
-public class NotificationOverflowContainer extends ActivatableNotificationView implements
-        NotificationActivatable {
+public class NotificationOverflowContainer extends ActivatableNotificationView {
 
     private NotificationOverflowIconsView mIconsView;
     private NotificationActivator mActivator;
@@ -46,11 +45,6 @@
     }
 
     @Override
-    public int getMaxHeight() {
-        return getHeight();
-    }
-
-    @Override
     public void setClipTopAmount(int clipTopAmount) {
         // noop
     }
@@ -62,16 +56,19 @@
         mIconsView.setMoreText((TextView) findViewById(R.id.more_text));
 
         mActivator = new NotificationActivator(this, this);
-        mActivator.setDimmed(true);
-        setLocked(true);
-        setDimmed(true);
+        setDimmed(true, false);
+    }
+
+    @Override
+    public void setDimmed(boolean dimmed, boolean fade) {
+        super.setDimmed(dimmed, fade);
+        mActivator.setDimmed(dimmed, fade);
     }
 
     public NotificationOverflowIconsView getIconsView() {
         return mIconsView;
     }
 
-    @Override
     public NotificationActivator getActivator() {
         return mActivator;
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java
index 7d8f3ef..890d1bd 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java
@@ -100,7 +100,6 @@
 import com.android.systemui.statusbar.ExpandableNotificationRow;
 import com.android.systemui.statusbar.GestureRecorder;
 import com.android.systemui.statusbar.InterceptedNotifications;
-import com.android.systemui.statusbar.NotificationActivatable;
 import com.android.systemui.statusbar.NotificationData;
 import com.android.systemui.statusbar.NotificationData.Entry;
 import com.android.systemui.statusbar.NotificationOverflowContainer;
@@ -3134,19 +3133,29 @@
 
     @Override
     public void onReset() {
-        onReset(null);
-    }
-
-    public void onHover(View child) {
-        if (child instanceof NotificationActivatable) {
-            NotificationActivatable activatable = (NotificationActivatable) child;
-            activatable.getActivator().activate();
-            activatable.getActivator().addHotspot();
+        int n = mNotificationData.size();
+        for (int i = 0; i < n; i++) {
+            NotificationData.Entry entry = mNotificationData.get(i);
+            if (entry.row.getVisibility() != View.GONE) {
+                entry.row.setDimmed(true /* dimmed */, true /* fade */);
+            }
+        }
+        if (mKeyguardIconOverflowContainer.getVisibility() != View.GONE) {
+            mKeyguardIconOverflowContainer.setDimmed(true /* dimmed */, true /* fade */);
         }
     }
 
     public void onThresholdReached() {
-        // TODO: Add visual hint that threshold is reached.
+        int n = mNotificationData.size();
+        for (int i = 0; i < n; i++) {
+            NotificationData.Entry entry = mNotificationData.get(i);
+            if (entry.row.getVisibility() != View.GONE) {
+                entry.row.setDimmed(false /* dimmed */, true /* fade */);
+            }
+        }
+        if (mKeyguardIconOverflowContainer.getVisibility() != View.GONE) {
+            mKeyguardIconOverflowContainer.setDimmed(false /* dimmed */, true /* fade */);
+        }
     }
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarView.java
index c9c2867..10c1625 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarView.java
@@ -19,7 +19,6 @@
 import android.app.ActivityManager;
 import android.content.Context;
 import android.content.res.Resources;
-import android.content.res.Resources.NotFoundException;
 import android.util.AttributeSet;
 import android.util.EventLog;
 import android.util.Log;
@@ -188,7 +187,8 @@
         if (panel == mFadingPanel && mScrimColor != 0 && ActivityManager.isHighEndGfx()
                 && mBar.mStatusBarWindow != null) {
             if (mShouldFade) {
-                int scrimColor = mBar.getBarState() == StatusBarState.KEYGUARD
+                int scrimColor = (mBar.getBarState() == StatusBarState.KEYGUARD
+                        || mBar.getBarState() == StatusBarState.SHADE_LOCKED)
                         ? mScrimColorKeyguard
                         : mScrimColor;
                 frac = mPanelExpandedFractionSum; // don't judge me
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
index c2595cf..554892c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
@@ -18,22 +18,14 @@
 
 import android.content.Context;
 import android.os.Bundle;
-import android.os.IBinder;
 import android.os.RemoteException;
-import android.util.Log;
 import android.util.Slog;
-import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
-import android.view.ViewTreeObserver;
 
 import com.android.internal.policy.IKeyguardShowCallback;
 import com.android.internal.widget.LockPatternUtils;
-import com.android.keyguard.KeyguardHostView;
-import com.android.keyguard.KeyguardSimpleHostView;
-import com.android.keyguard.R;
 import com.android.keyguard.ViewMediatorCallback;
-import com.android.systemui.keyguard.KeyguardViewMediator;
 
 /**
  * Manages creating, showing, hiding and resetting the keyguard within the status bar. Calls back
@@ -82,7 +74,7 @@
         mShowing = true;
         mStatusBarWindowManager.setKeyguardShowing(true);
         showBouncerOrKeyguard();
-        updateBackButtonState();
+        updateStates();
     }
 
     /**
@@ -102,9 +94,9 @@
         }
     }
 
-    public void showBouncer() {
+    private void showBouncer() {
         mBouncer.show();
-        updateBackButtonState();
+        updateStates();
     }
 
     /**
@@ -112,7 +104,7 @@
      */
     public void reset() {
         showBouncerOrKeyguard();
-        updateBackButtonState();
+        updateStates();
     }
 
     public void onScreenTurnedOff() {
@@ -155,7 +147,7 @@
     public void setOccluded(boolean occluded) {
         mOccluded = occluded;
         mStatusBarWindowManager.setKeyguardOccluded(occluded);
-        updateBackButtonState();
+        updateStates();
     }
 
     /**
@@ -167,7 +159,7 @@
         mStatusBarWindowManager.setKeyguardShowing(false);
         mBouncer.hide();
         mViewMediatorCallback.keyguardGone();
-        updateBackButtonState();
+        updateStates();
     }
 
     /**
@@ -199,13 +191,13 @@
         if (mBouncer.isShowing()) {
             mBouncer.hide();
             mPhoneStatusBar.showKeyguard();
-            updateBackButtonState();
+            updateStates();
             return true;
         }
         return false;
     }
 
-    private void updateBackButtonState() {
+    private void updateStates() {
         int vis = mContainer.getSystemUiVisibility();
         boolean bouncerDismissable = mBouncer.isShowing() && !mBouncer.needsFullscreenBouncer();
         if (bouncerDismissable || !mShowing) {
@@ -218,6 +210,7 @@
         } else {
             mPhoneStatusBar.getNavigationBarView().setVisibility(View.GONE);
         }
+        mPhoneStatusBar.setBouncerShowing(mBouncer.isShowing());
     }
 
     public boolean onMenuPressed() {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarWindowManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarWindowManager.java
index a04baea..a4c9df5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarWindowManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarWindowManager.java
@@ -147,7 +147,8 @@
     }
 
     private void applyInputFeatures(State state) {
-        if (state.isKeyguardShowingAndNotOccluded()) {
+        if (state.isKeyguardShowingAndNotOccluded()
+                && state.statusBarState == StatusBarState.KEYGUARD) {
             mLp.inputFeatures |= WindowManager.LayoutParams.INPUT_FEATURE_DISABLE_USER_ACTIVITY;
         } else {
             mLp.inputFeatures &= ~WindowManager.LayoutParams.INPUT_FEATURE_DISABLE_USER_ACTIVITY;
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 3e5ba16..acc3a0b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarWindowView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarWindowView.java
@@ -112,7 +112,8 @@
             intercept = mExpandHelper.onInterceptTouchEvent(ev);
         } else if (mNotificationPanel.isFullyExpanded()
                 && mStackScrollLayout.getVisibility() == View.VISIBLE
-                && mService.getBarState() == StatusBarState.KEYGUARD) {
+                && mService.getBarState() == StatusBarState.KEYGUARD
+                && !mService.isBouncerShowing()) {
             intercept = mDragDownHelper.onInterceptTouchEvent(ev);
         }
         if (!intercept) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpNotificationView.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpNotificationView.java
index c94c65f..72e22e9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpNotificationView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpNotificationView.java
@@ -32,6 +32,7 @@
 import com.android.systemui.R;
 import com.android.systemui.SwipeHelper;
 import com.android.systemui.statusbar.BaseStatusBar;
+import com.android.systemui.statusbar.ExpandableView;
 import com.android.systemui.statusbar.NotificationData;
 
 public class HeadsUpNotificationView extends FrameLayout implements SwipeHelper.Callback, ExpandHelper.Callback {
@@ -185,12 +186,12 @@
     // ExpandHelper.Callback methods
 
     @Override
-    public View getChildAtRawPosition(float x, float y) {
+    public ExpandableView getChildAtRawPosition(float x, float y) {
         return getChildAtPosition(x, y);
     }
 
     @Override
-    public View getChildAtPosition(float x, float y) {
+    public ExpandableView getChildAtPosition(float x, float y) {
         return mHeadsUp == null ? null : mHeadsUp.row;
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java
index 0114f3c..8a49ded 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java
@@ -159,6 +159,7 @@
         mScroller = new OverScroller(getContext());
         setFocusable(true);
         setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
+        setClipChildren(false);
         final ViewConfiguration configuration = ViewConfiguration.get(context);
         mTouchSlop = configuration.getScaledTouchSlop();
         mMinimumVelocity = configuration.getScaledMinimumFlingVelocity();
@@ -383,13 +384,13 @@
         return getChildAtPosition(ev.getX(), ev.getY());
     }
 
-    public View getChildAtRawPosition(float touchX, float touchY) {
+    public ExpandableView getChildAtRawPosition(float touchX, float touchY) {
         int[] location = new int[2];
         getLocationOnScreen(location);
         return getChildAtPosition(touchX - location[0], touchY - location[1]);
     }
 
-    public View getChildAtPosition(float touchX, float touchY) {
+    public ExpandableView getChildAtPosition(float touchX, float touchY) {
         // find the view under the pointer, accounting for GONE views
         final int count = getChildCount();
         for (int childIdx = 0; childIdx < count; childIdx++) {