Single finger notification expansion.

If you liked these changes...

  Change Ie4e79aa5
  Change I8a6f8606
  Change I824937e9
  Change I957b6d50

You'll love this one!

Change-Id: I5256366175fa7ebc965b1c5df02f10ba802ed977
diff --git a/packages/SystemUI/res/layout/status_bar_expanded.xml b/packages/SystemUI/res/layout/status_bar_expanded.xml
index 5841978..17dbcac 100644
--- a/packages/SystemUI/res/layout/status_bar_expanded.xml
+++ b/packages/SystemUI/res/layout/status_bar_expanded.xml
@@ -67,7 +67,7 @@
             android:layout_width="match_parent"
             android:layout_height="match_parent"
             android:fadingEdge="none"
-            android:overScrollMode="always"
+            android:overScrollMode="ifContentScrolls"
             >
             <com.android.systemui.statusbar.policy.NotificationRowLayout
                 android:id="@+id/latestItems"
diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml
index 26dba67..66add1a 100644
--- a/packages/SystemUI/res/values/config.xml
+++ b/packages/SystemUI/res/values/config.xml
@@ -65,5 +65,8 @@
 
     <!-- Vibration duration for MultiWaveView used in SearchPanelView -->
     <integer translatable="false" name="config_search_panel_view_vibration_duration">20</integer>
+
+    <!-- The length of the vibration when the notificaiotn pops open. -->
+    <integer name="one_finger_pop_duration_ms">10</integer>
 </resources>
 
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index 6c40461..6fc79c5 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -150,4 +150,7 @@
 
     <!-- Height of the carrier/wifi name label -->
     <dimen name="carrier_label_height">24dp</dimen>
+
+    <!-- The distance you can pull a notificaiton before it pops open -->
+    <dimen name="one_finger_pop_limit">32dp</dimen>
 </resources>
diff --git a/packages/SystemUI/res/values/ids.xml b/packages/SystemUI/res/values/ids.xml
index 8ebbc52..4a73200 100644
--- a/packages/SystemUI/res/values/ids.xml
+++ b/packages/SystemUI/res/values/ids.xml
@@ -18,4 +18,5 @@
 <resources>
     <item type="id" name="expandable_tag" />
     <item type="id" name="user_expanded_tag" />
+    <item type="id" name="user_lock_tag" />
 </resources>
diff --git a/packages/SystemUI/src/com/android/systemui/ExpandHelper.java b/packages/SystemUI/src/com/android/systemui/ExpandHelper.java
index 5dd15c3..674d9a3 100644
--- a/packages/SystemUI/src/com/android/systemui/ExpandHelper.java
+++ b/packages/SystemUI/src/com/android/systemui/ExpandHelper.java
@@ -22,24 +22,31 @@
 import android.animation.AnimatorSet;
 import android.animation.ObjectAnimator;
 import android.content.Context;
+import android.os.Vibrator;
 import android.util.Slog;
 import android.view.Gravity;
 import android.view.MotionEvent;
 import android.view.ScaleGestureDetector;
 import android.view.View;
+import android.view.ViewConfiguration;
 import android.view.ViewGroup;
 import android.view.View.OnClickListener;
 
+import java.util.Stack;
+
 public class ExpandHelper implements Gefingerpoken, OnClickListener {
     public interface Callback {
         View getChildAtRawPosition(float x, float y);
         View getChildAtPosition(float x, float y);
         boolean canChildBeExpanded(View v);
-        boolean setUserExpandedChild(View v, boolean userxpanded);
+        boolean setUserExpandedChild(View v, boolean userExpanded);
+        boolean setUserLockedChild(View v, boolean userLocked);
     }
 
     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;
 
@@ -63,6 +70,9 @@
     private Context mContext;
 
     private boolean mStretching;
+    private boolean mPullingWithOneFinger;
+    private boolean mWatchingForPull;
+    private boolean mHasPopped;
     private View mEventSource;
     private View mCurrView;
     private View mCurrViewTopGlow;
@@ -70,7 +80,12 @@
     private float mOldHeight;
     private float mNaturalHeight;
     private float mInitialTouchFocusY;
+    private float mInitialTouchY;
     private float mInitialTouchSpan;
+    private int mTouchSlop;
+    private int mLastMotionY;
+    private float mPopLimit;
+    private int mPopDuration;
     private Callback mCallback;
     private ScaleGestureDetector mDetector;
     private ViewScaler mScaler;
@@ -78,6 +93,7 @@
     private AnimatorSet mGlowAnimationSet;
     private ObjectAnimator mGlowTopAnimation;
     private ObjectAnimator mGlowBottomAnimation;
+    private Vibrator mVibrator;
 
     private int mSmallSize;
     private int mLargeSize;
@@ -85,6 +101,8 @@
 
     private int mGravity;
 
+    private View mScrollView;
+
     private class ViewScaler {
         View mView;
 
@@ -93,7 +111,7 @@
             mView = v;
         }
         public void setHeight(float h) {
-            if (DEBUG) Slog.v(TAG, "SetHeight: setting to " + h);
+            if (DEBUG_SCALE) Slog.v(TAG, "SetHeight: setting to " + h);
             ViewGroup.LayoutParams lp = mView.getLayoutParams();
             lp.height = (int)h;
             mView.setLayoutParams(lp);
@@ -108,7 +126,8 @@
         }
         public int getNaturalHeight(int maximum) {
             ViewGroup.LayoutParams lp = mView.getLayoutParams();
-            if (DEBUG) Slog.v(TAG, "Inspecting a child of type: " + mView.getClass().getName());
+            if (DEBUG_SCALE) Slog.v(TAG, "Inspecting a child of type: " +
+                    mView.getClass().getName());
             int oldHeight = lp.height;
             lp.height = ViewGroup.LayoutParams.WRAP_CONTENT;
             mView.setLayoutParams(lp);
@@ -142,6 +161,8 @@
         mGravity = Gravity.TOP;
         mScaleAnimation = ObjectAnimator.ofFloat(mScaler, "height", 0f);
         mScaleAnimation.setDuration(EXPAND_DURATION);
+        mPopLimit = mContext.getResources().getDimension(R.dimen.one_finger_pop_limit);
+        mPopDuration = mContext.getResources().getInteger(R.integer.one_finger_pop_duration_ms);
 
         AnimatorListenerAdapter glowVisibilityController = new AnimatorListenerAdapter() {
             @Override
@@ -169,38 +190,30 @@
         mGlowAnimationSet.play(mGlowTopAnimation).with(mGlowBottomAnimation);
         mGlowAnimationSet.setDuration(GLOW_DURATION);
 
+        final ViewConfiguration configuration = ViewConfiguration.get(mContext);
+        mTouchSlop = configuration.getScaledTouchSlop();
+
         mDetector =
                 new ScaleGestureDetector(context,
                                          new ScaleGestureDetector.SimpleOnScaleGestureListener() {
             @Override
             public boolean onScaleBegin(ScaleGestureDetector detector) {
-                if (DEBUG) Slog.v(TAG, "onscalebegin()");
+                if (DEBUG_SCALE) Slog.v(TAG, "onscalebegin()");
                 float x = detector.getFocusX();
                 float y = detector.getFocusY();
 
-                View v = null;
-                if (mEventSource != null) {
-                    int[] location = new int[2];
-                    mEventSource.getLocationOnScreen(location);
-                    x += (float) location[0];
-                    y += (float) location[1];
-                    v = mCallback.getChildAtRawPosition(x, y);
-                } else {
-                    v = mCallback.getChildAtPosition(x, y);
-                }
-
                 // your fingers have to be somewhat close to the bounds of the view in question
                 mInitialTouchFocusY = detector.getFocusY();
                 mInitialTouchSpan = Math.abs(detector.getCurrentSpan());
-                if (DEBUG) Slog.d(TAG, "got mInitialTouchSpan: (" + mInitialTouchSpan + ")");
+                if (DEBUG_SCALE) Slog.d(TAG, "got mInitialTouchSpan: (" + mInitialTouchSpan + ")");
 
-                mStretching = initScale(v);
+                mStretching = initScale(findView(x, y));
                 return mStretching;
             }
 
             @Override
             public boolean onScale(ScaleGestureDetector detector) {
-                if (DEBUG) Slog.v(TAG, "onscale() on " + mCurrView);
+                if (DEBUG_SCALE) Slog.v(TAG, "onscale() on " + mCurrView);
 
                 // are we scaling or dragging?
                 float span = Math.abs(detector.getCurrentSpan()) - mInitialTouchSpan;
@@ -210,33 +223,71 @@
                 drag *= mGravity == Gravity.BOTTOM ? -1f : 1f;
                 float pull = Math.abs(drag) + Math.abs(span) + 1f;
                 float hand = drag * Math.abs(drag) / pull + span * Math.abs(span) / pull;
-                if (DEBUG) Slog.d(TAG, "current span handle is: " + hand);
-                hand = hand + mOldHeight;
-                float target = hand;
-                if (DEBUG) Slog.d(TAG, "target is: " + target);
-                hand = hand < mSmallSize ? mSmallSize : (hand > mLargeSize ? mLargeSize : hand);
-                hand = hand > mNaturalHeight ? mNaturalHeight : hand;
-                if (DEBUG) Slog.d(TAG, "scale continues: hand =" + hand);
-                mScaler.setHeight(hand);
+                float target = hand + mOldHeight;
+                float newHeight = clamp(target);
+                mScaler.setHeight(newHeight);
 
-                // glow if overscale
-                float stretch = (float) Math.abs((target - hand) / mMaximumStretch);
-                float strength = 1f / (1f + (float) Math.pow(Math.E, -1 * ((8f * stretch) - 5f)));
-                if (DEBUG) Slog.d(TAG, "stretch: " + stretch + " strength: " + strength);
-                setGlow(GLOW_BASE + strength * (1f - GLOW_BASE));
+                setGlow(calculateGlow(target, newHeight));
                 return true;
             }
 
             @Override
             public void onScaleEnd(ScaleGestureDetector detector) {
-                if (DEBUG) Slog.v(TAG, "onscaleend()");
+                if (DEBUG_SCALE) Slog.v(TAG, "onscaleend()");
                 // I guess we're alone now
-                if (DEBUG) Slog.d(TAG, "scale end");
+                if (DEBUG_SCALE) Slog.d(TAG, "scale end");
                 finishScale(false);
+                clearView();
+                mStretching = false;
             }
         });
     }
 
+    private float clamp(float target) {
+        float out = target;
+        out = out < mSmallSize ? mSmallSize : (out > mLargeSize ? mLargeSize : out);
+        out = out > mNaturalHeight ? mNaturalHeight : out;
+        return out;
+    }
+
+    private View findView(float x, float y) {
+        View v = null;
+        if (mEventSource != null) {
+            int[] location = new int[2];
+            mEventSource.getLocationOnScreen(location);
+            x += (float) location[0];
+            y += (float) location[1];
+            v = mCallback.getChildAtRawPosition(x, y);
+        } else {
+            v = mCallback.getChildAtPosition(x, y);
+        }
+        return v;
+    }
+
+    private boolean isInside(View v, float x, float y) {
+        if (DEBUG) Slog.d(TAG, "isinside (" + x + ", " + y + ")");
+
+        if (v == null) {
+            if (DEBUG) Slog.d(TAG, "isinside null subject");
+            return false;
+        }
+        if (mEventSource != null) {
+            int[] location = new int[2];
+            mEventSource.getLocationOnScreen(location);
+            x += (float) location[0];
+            y += (float) location[1];
+            if (DEBUG) Slog.d(TAG, "  to global (" + x + ", " + y + ")");
+        }
+        int[] location = new int[2];
+        v.getLocationOnScreen(location);
+        x -= (float) location[0];
+        y -= (float) location[1];
+        if (DEBUG) Slog.d(TAG, "  to local (" + x + ", " + y + ")");
+        if (DEBUG) Slog.d(TAG, "  inside (" + v.getWidth() + ", " + v.getHeight() + ")");
+        boolean inside = (x > 0f && y > 0f && x < v.getWidth() & y < v.getHeight());
+        return inside;
+    }
+
     public void setEventSource(View eventSource) {
         mEventSource = eventSource;
     }
@@ -245,13 +296,26 @@
         mGravity = gravity;
     }
 
+    public void setScrollView(View scrollView) {
+        mScrollView = scrollView;
+    }
+
+    private float calculateGlow(float target, float actual) {
+        // glow if overscale
+        if (DEBUG_GLOW) Slog.d(TAG, "target: " + target + " actual: " + actual);
+        float stretch = (float) Math.abs((target - actual) / mMaximumStretch);
+        float strength = 1f / (1f + (float) Math.pow(Math.E, -1 * ((8f * stretch) - 5f)));
+        if (DEBUG_GLOW) Slog.d(TAG, "stretch: " + stretch + " strength: " + strength);
+        return (GLOW_BASE + strength * (1f - GLOW_BASE));
+    }
+
     public void setGlow(float glow) {
         if (!mGlowAnimationSet.isRunning() || glow == 0f) {
             if (mGlowAnimationSet.isRunning()) {
-                mGlowAnimationSet.cancel();
+                mGlowAnimationSet.end();
             }
             if (mCurrViewTopGlow != null && mCurrViewBottomGlow != null) {
-                if (glow == 0f || mCurrViewTopGlow.getAlpha() == 0f) { 
+                if (glow == 0f || mCurrViewTopGlow.getAlpha() == 0f) {
                     // animate glow in and out
                     mGlowTopAnimation.setTarget(mCurrViewTopGlow);
                     mGlowBottomAnimation.setTarget(mCurrViewBottomGlow);
@@ -278,23 +342,115 @@
 
     public boolean onInterceptTouchEvent(MotionEvent ev) {
         if (DEBUG) Slog.d(TAG, "interceptTouch: act=" + (ev.getAction()) +
-                         " stretching=" + mStretching);
+                         " stretching=" + mStretching +
+                         " onefinger=" + mPullingWithOneFinger);
+        // check for a two-finger gesture
         mDetector.onTouchEvent(ev);
-        return mStretching;
+        if (mStretching) {
+            return true;
+        } else {
+            final int action = ev.getAction();
+            if ((action == MotionEvent.ACTION_MOVE) && mPullingWithOneFinger) {
+                return true;
+            }
+            if (mScrollView != null && mScrollView.getScrollY() > 0) {
+                return false;
+            }
+            switch (action & MotionEvent.ACTION_MASK) {
+            case MotionEvent.ACTION_MOVE: {
+                if (mWatchingForPull) {
+                    final int x = (int) ev.getX();
+                    final int y = (int) ev.getY();
+                    final int yDiff = y - mLastMotionY;
+                    if (yDiff > mTouchSlop) {
+                        mLastMotionY = y;
+                        mPullingWithOneFinger = initScale(findView(x, y));
+                        if (mPullingWithOneFinger) {
+                            mInitialTouchY = mLastMotionY;
+                            mHasPopped = false;
+                        }
+                    }
+                }
+                break;
+            }
+
+            case MotionEvent.ACTION_DOWN:
+                mWatchingForPull = isInside(mScrollView, ev.getX(), ev.getY());
+                mLastMotionY = (int) ev.getY();
+                break;
+
+            case MotionEvent.ACTION_CANCEL:
+            case MotionEvent.ACTION_UP:
+                if (mPullingWithOneFinger) {
+                    finishScale(false);
+                    clearView();
+                }
+                mPullingWithOneFinger = false;
+                mWatchingForPull = false;
+                break;
+            }
+            return mPullingWithOneFinger;
+        }
     }
 
     public boolean onTouchEvent(MotionEvent ev) {
         final int action = ev.getAction();
-        if (DEBUG) Slog.d(TAG, "touch: act=" + (action) + " stretching=" + mStretching);
+        if (DEBUG_SCALE) Slog.d(TAG, "touch: act=" + (action) +
+                         " stretching=" + mStretching +
+                         " onefinger=" + mPullingWithOneFinger);
         if (mStretching) {
-            if (DEBUG) Slog.d(TAG, "detector ontouch");
             mDetector.onTouchEvent(ev);
         }
         switch (action) {
+            case MotionEvent.ACTION_MOVE: {
+                if (mPullingWithOneFinger) {
+                    final float rawHeight = ev.getY() - mInitialTouchY + mOldHeight;
+                    final float newHeight = clamp(rawHeight);
+                    final boolean wasClosed = (mOldHeight == mSmallSize);
+                    boolean isFinished = false;
+                    if (rawHeight > mNaturalHeight) {
+                        isFinished = true;
+                    }
+                    if (rawHeight < mSmallSize) {
+                        isFinished = true;
+                    }
+
+                    final float pull = Math.abs(ev.getY() - mInitialTouchY);
+                    if (mHasPopped || pull > mPopLimit) {
+                        if (!mHasPopped) {
+                            vibrate(mPopDuration);
+                            mHasPopped = true;
+                        }
+                    }
+
+                    if (mHasPopped) {
+                        mScaler.setHeight(newHeight);
+                        setGlow(GLOW_BASE);
+                    } else {
+                        setGlow(calculateGlow(4f * pull, 0f));
+                    }
+
+                    final int x = (int) ev.getX();
+                    final int y = (int) ev.getY();
+                    View underPointer = findView(x, y);
+                    if (isFinished && underPointer != null && underPointer != mCurrView) {
+                        finishScale(false);
+                        initScale(underPointer);
+                        mInitialTouchY = ev.getY();
+                        mHasPopped = false;
+                    }
+                    return true;
+                }
+                break;
+            }
             case MotionEvent.ACTION_UP:
             case MotionEvent.ACTION_CANCEL:
                 if (DEBUG) Slog.d(TAG, "cancel");
                 mStretching = false;
+                if (mPullingWithOneFinger) {
+                    finishScale(false);
+                    mPullingWithOneFinger = false;
+                }
                 clearView();
                 break;
         }
@@ -303,7 +459,7 @@
     private boolean initScale(View v) {
         if (v != null) {
             if (DEBUG) Slog.d(TAG, "scale begins on view: " + v);
-            mStretching = true;
+            mCallback.setUserLockedChild(v, true);
             setView(v);
             setGlow(GLOW_BASE);
             mScaler.setView(v);
@@ -318,30 +474,34 @@
             if (DEBUG) Slog.d(TAG, "got mOldHeight: " + mOldHeight +
                         " mNaturalHeight: " + mNaturalHeight);
             v.getParent().requestDisallowInterceptTouchEvent(true);
+            return true;
+        } else {
+            return false;
         }
-        return mStretching;
     }
 
     private void finishScale(boolean force) {
+        float currentHeight = mScaler.getHeight();
+        float targetHeight = mSmallSize;
         float h = mScaler.getHeight();
         final boolean wasClosed = (mOldHeight == mSmallSize);
         if (wasClosed) {
-            h = (force || h > mSmallSize) ? mNaturalHeight : mSmallSize;
+            targetHeight = (force || currentHeight > mSmallSize) ? mNaturalHeight : mSmallSize;
         } else {
-            h = (force || h < mNaturalHeight) ? mSmallSize : mNaturalHeight;
+            targetHeight = (force || currentHeight < mNaturalHeight) ? mSmallSize : mNaturalHeight;
         }
-        if (DEBUG && mCurrView != null) mCurrView.setBackgroundColor(0);
         if (mScaleAnimation.isRunning()) {
             mScaleAnimation.cancel();
         }
-        mScaleAnimation.setFloatValues(h);
-        mScaleAnimation.setupStartValues();
-        mScaleAnimation.start();
-        mStretching = false;
         setGlow(0f);
         mCallback.setUserExpandedChild(mCurrView, h == mNaturalHeight);
+        if (targetHeight != currentHeight) {
+            mScaleAnimation.setFloatValues(targetHeight);
+            mScaleAnimation.setupStartValues();
+            mScaleAnimation.start();
+        }
+        mCallback.setUserLockedChild(mCurrView, false);
         if (DEBUG) Slog.d(TAG, "scale was finished on view: " + mCurrView);
-        clearView();
     }
 
     private void clearView() {
@@ -357,7 +517,7 @@
             mCurrViewTopGlow = g.findViewById(R.id.top_glow);
             mCurrViewBottomGlow = g.findViewById(R.id.bottom_glow);
             if (DEBUG) {
-                String debugLog = "Looking for glows: " + 
+                String debugLog = "Looking for glows: " +
                         (mCurrViewTopGlow != null ? "found top " : "didn't find top") +
                         (mCurrViewBottomGlow != null ? "found bottom " : "didn't find bottom");
                 Slog.v(TAG,  debugLog);
@@ -369,6 +529,18 @@
     public void onClick(View v) {
         initScale(v);
         finishScale(true);
+        clearView();
+    }
 
+    /**
+     * Triggers haptic feedback.
+     */
+    private synchronized void vibrate(long duration) {
+        if (mVibrator == null) {
+            mVibrator = (android.os.Vibrator)
+                    mContext.getSystemService(Context.VIBRATOR_SERVICE);
+        }
+        mVibrator.vibrate(duration);
     }
 }
+
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java
index 1204a89..8d7734e0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java
@@ -783,16 +783,20 @@
         int N = mNotificationData.size();
         for (int i = 0; i < N; i++) {
             NotificationData.Entry entry = mNotificationData.get(i);
-            if (i == (N-1)) {
-                if (DEBUG) Slog.d(TAG, "expanding top notification at " + i);
-                expandView(entry, true);
-            } else {
-                if (!entry.userExpanded()) {
-                    if (DEBUG) Slog.d(TAG, "collapsing notification at " + i);
-                    expandView(entry, false);
+            if (!entry.userLocked()) {
+                if (i == (N-1)) {
+                    if (DEBUG) Slog.d(TAG, "expanding top notification at " + i);
+                    expandView(entry, true);
                 } else {
-                    if (DEBUG) Slog.d(TAG, "ignoring user-modified notification at " + i);
+                    if (!entry.userExpanded()) {
+                        if (DEBUG) Slog.d(TAG, "collapsing notification at " + i);
+                        expandView(entry, false);
+                    } else {
+                        if (DEBUG) Slog.d(TAG, "ignoring user-modified notification at " + i);
+                    }
                 }
+            } else {
+                if (DEBUG) Slog.d(TAG, "ignoring notification being held by user at " + i);
             }
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationData.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationData.java
index dfd8cf8..c82f250 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationData.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationData.java
@@ -71,6 +71,18 @@
         public boolean setUserExpanded(boolean userExpanded) {
             return NotificationData.setUserExpanded(row, userExpanded);
         }
+        /**
+         * Return whether the entry is being touched by the user.
+         */
+        public boolean userLocked() {
+            return NotificationData.getUserLocked(row);
+        }
+        /**
+         * Set the flag indicating that this is being touched by the user.
+         */
+        public boolean setUserLocked(boolean userLocked) {
+            return NotificationData.setUserLocked(row, userLocked);
+        }
     }
     private final ArrayList<Entry> mEntries = new ArrayList<Entry>();
     private final Comparator<Entry> mEntryCmp = new Comparator<Entry>() {
@@ -197,4 +209,18 @@
     public static boolean setUserExpanded(View row, boolean userExpanded) {
         return writeBooleanTag(row, R.id.user_expanded_tag, userExpanded);
     }
+
+    /**
+     * Return whether the entry is being touched by the user.
+     */
+    public static boolean getUserLocked(View row) {
+        return readBooleanTag(row, R.id.user_lock_tag);
+    }
+
+    /**
+     * Set whether the entry is being touched by the user.
+     */
+    public static boolean setUserLocked(View row, boolean userLocked) {
+        return writeBooleanTag(row, R.id.user_lock_tag, userLocked);
+    }
 }
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 9317561..2628631 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarWindowView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarWindowView.java
@@ -53,6 +53,7 @@
         int maxHeight = getResources().getDimensionPixelSize(R.dimen.notification_row_max_height);
         mExpandHelper = new ExpandHelper(mContext, latestItems, minHeight, maxHeight);
         mExpandHelper.setEventSource(this);
+        mExpandHelper.setScrollView(scroller);
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/NotificationRowLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/NotificationRowLayout.java
index 61e5ab6..89eed1b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/NotificationRowLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/NotificationRowLayout.java
@@ -78,6 +78,7 @@
         super(context, attrs, defStyle);
 
         mRealLayoutTransition = new LayoutTransition();
+        mRealLayoutTransition.setAnimateParentHierarchy(true);
         setLayoutTransitionsEnabled(true);
         
         setOrientation(LinearLayout.VERTICAL);
@@ -161,7 +162,12 @@
         return NotificationData.setUserExpanded(v, userExpanded);
     }
 
+    public boolean setUserLockedChild(View v, boolean userLocked) {
+        return NotificationData.setUserLocked(v, userLocked);
+    }
+
     public void onChildDismissed(View v) {
+        if (DEBUG) Slog.v(TAG, "onChildDismissed: " + v + " mRemoveViews=" + mRemoveViews);
         final View veto = v.findViewById(R.id.veto);
         if (veto != null && veto.getVisibility() != View.GONE && mRemoveViews) {
             veto.performClick();
@@ -225,6 +231,7 @@
      * get removed properly.
      */
     public void setViewRemoval(boolean removeViews) {
+        if (DEBUG) Slog.v(TAG, "setViewRemoval: " + removeViews);
         mRemoveViews = removeViews;
     }