Introduce SHADE_LOCKED as a special state for the shade.

This state can be either reached by tapping the more card or dragging
down on any card. In this state, the shade is fully interactive, but
the phone is stil locked. This state can only be enterred if
redaction is off but security is on. If redaction is on, we will show
the bouncer instead and go to the normal shade.

Bug: 14161523
Change-Id: I95ca0991745ffc11ed1028581e3da15265c12ae5
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/ActivatableNotificationView.java b/packages/SystemUI/src/com/android/systemui/statusbar/ActivatableNotificationView.java
index 0f32dc0..5a55292 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/ActivatableNotificationView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/ActivatableNotificationView.java
@@ -88,14 +88,6 @@
                 if (mDownY > getActualHeight()) {
                     return false;
                 }
-
-                // Call the listener tentatively directly, even if we don't know whether the user
-                // will stay within the touch slop, as the listener is implemented as a scale
-                // animation, which is cancellable without jarring effects when swiping away
-                // notifications.
-                if (mOnActivatedListener != null) {
-                    mOnActivatedListener.onActivated(this);
-                }
                 break;
             case MotionEvent.ACTION_MOVE:
                 if (!isWithinTouchSlop(event)) {
@@ -109,8 +101,8 @@
                         makeActive(event.getX(), event.getY());
                         postDelayed(mTapTimeoutRunnable, DOUBLETAP_TIMEOUT_MS);
                     } else {
-                        performClick();
                         makeInactive();
+                        performClick();
                     }
                 } else {
                     makeInactive();
@@ -128,6 +120,9 @@
     private void makeActive(float x, float y) {
         mCustomBackground.setHotspot(0, x, y);
         mActivated = true;
+        if (mOnActivatedListener != null) {
+            mOnActivatedListener.onActivated(this);
+        }
     }
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java
index 9149e2d..829cee4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java
@@ -167,7 +167,11 @@
 
     protected int mZenMode;
 
-    protected boolean mOnKeyguard;
+    /**
+     * The {@link StatusBarState} of the status bar.
+     */
+    protected int mState;
+
     protected NotificationOverflowContainer mKeyguardIconOverflowContainer;
 
     public boolean isDeviceProvisioned() {
@@ -1057,9 +1061,10 @@
         mKeyguardIconOverflowContainer.getIconsView().removeAllViews();
         int n = mNotificationData.size();
         int visibleNotifications = 0;
+        boolean onKeyguard = mState == StatusBarState.KEYGUARD;
         for (int i = n-1; i >= 0; i--) {
             NotificationData.Entry entry = mNotificationData.get(i);
-            if (mOnKeyguard) {
+            if (onKeyguard) {
                 entry.row.setExpansionDisabled(true);
             } else {
                 entry.row.setExpansionDisabled(false);
@@ -1068,10 +1073,10 @@
                     entry.row.setSystemExpanded(top);
                 }
             }
-            entry.row.setDimmed(mOnKeyguard);
-            entry.row.setLocked(mOnKeyguard);
+            entry.row.setDimmed(onKeyguard);
+            entry.row.setLocked(onKeyguard);
             boolean showOnKeyguard = shouldShowOnKeyguard(entry.notification);
-            if (mOnKeyguard && (visibleNotifications >= maxKeyguardNotifications
+            if (onKeyguard && (visibleNotifications >= maxKeyguardNotifications
                     || !showOnKeyguard)) {
                 entry.row.setVisibility(View.GONE);
                 if (showOnKeyguard) {
@@ -1087,7 +1092,7 @@
             }
         }
 
-        if (mOnKeyguard && mKeyguardIconOverflowContainer.getIconsView().getChildCount() > 0) {
+        if (onKeyguard && mKeyguardIconOverflowContainer.getIconsView().getChildCount() > 0) {
             mKeyguardIconOverflowContainer.setVisibility(View.VISIBLE);
         } else {
             mKeyguardIconOverflowContainer.setVisibility(View.GONE);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/DragDownHelper.java b/packages/SystemUI/src/com/android/systemui/statusbar/DragDownHelper.java
new file mode 100644
index 0000000..af4c8b8
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/DragDownHelper.java
@@ -0,0 +1,158 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.systemui.statusbar;
+
+import android.content.Context;
+import android.util.ArraySet;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewConfiguration;
+
+import com.android.systemui.ExpandHelper;
+import com.android.systemui.Gefingerpoken;
+import com.android.systemui.R;
+
+import 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 int mMinDragDistance;
+    private ExpandHelper.Callback mCallback;
+    private float mInitialTouchX;
+    private float mInitialTouchY;
+    private boolean mDraggingDown;
+    private float mTouchSlop;
+    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;
+
+    public DragDownHelper(Context context, View host, ExpandHelper.Callback callback,
+            OnDragDownListener onDragDownListener) {
+        mMinDragDistance = context.getResources().getDimensionPixelSize(
+                R.dimen.keyguard_drag_down_min_distance);
+        mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
+        mCallback = callback;
+        mOnDragDownListener = onDragDownListener;
+        mHost = host;
+    }
+
+    @Override
+    public boolean onInterceptTouchEvent(MotionEvent event) {
+        final float x = event.getX();
+        final float y = event.getY();
+
+        switch (event.getActionMasked()) {
+            case MotionEvent.ACTION_DOWN:
+                mHoveredChildren.clear();
+                mDraggedFarEnough = false;
+                mDraggingDown = false;
+                mStartingChild = null;
+                mInitialTouchY = y;
+                mInitialTouchX = x;
+                break;
+
+            case MotionEvent.ACTION_MOVE:
+                final float h = y - mInitialTouchY;
+                if (h > mTouchSlop && h > Math.abs(x - mInitialTouchX)) {
+                    mDraggingDown = true;
+                    mInitialTouchY = y;
+                    mInitialTouchX = x;
+                    return true;
+                }
+                break;
+        }
+        return false;
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent event) {
+        if (!mDraggingDown) {
+            return false;
+        }
+        final float x = event.getX();
+        final float y = event.getY();
+
+        switch (event.getActionMasked()) {
+            case MotionEvent.ACTION_MOVE:
+                final float h = y - mInitialTouchY;
+                View child = findView(x, y);
+                if (child != null) {
+                    hoverChild(findView(x, y));
+                }
+                if (h > mMinDragDistance) {
+                    if (!mDraggedFarEnough) {
+                        mDraggedFarEnough = true;
+                        mOnDragDownListener.onThresholdReached();
+                    }
+                } else {
+                    if (mDraggedFarEnough) {
+                        mDraggedFarEnough = false;
+                        mOnDragDownListener.onReset();
+                    }
+                }
+                return true;
+            case MotionEvent.ACTION_UP:
+                if (mDraggedFarEnough) {
+                    mOnDragDownListener.onDraggedDown(mStartingChild);
+                } else {
+                    stopDragging();
+                    return false;
+                }
+                break;
+            case MotionEvent.ACTION_CANCEL:
+                stopDragging();
+                return false;
+        }
+        return false;
+    }
+
+    private void stopDragging() {
+        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) {
+        mHost.getLocationOnScreen(mTemp2);
+        x += mTemp2[0];
+        y += mTemp2[1];
+        return mCallback.getChildAtRawPosition(x, y);
+    }
+
+    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 61aad6f..5b70cd3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableNotificationRow.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableNotificationRow.java
@@ -23,7 +23,8 @@
 
 import com.android.systemui.R;
 
-public class ExpandableNotificationRow extends ActivatableNotificationView {
+public class ExpandableNotificationRow extends ActivatableNotificationView implements
+        NotificationActivatable {
     private int mRowMinHeight;
     private int mRowMaxHeight;
 
@@ -64,7 +65,7 @@
         mPublicLayout = (NotificationContentView) findViewById(R.id.expandedPublic);
         mPrivateLayout = (NotificationContentView) findViewById(R.id.expanded);
 
-        mActivator = new NotificationActivator(this);
+        mActivator = new NotificationActivator(this, this);
     }
 
     @Override
@@ -113,6 +114,7 @@
     public void setUserExpanded(boolean userExpanded) {
         mHasUserChangedExpansion = true;
         mUserExpanded = userExpanded;
+        applyExpansionToLayout();
     }
 
     public boolean isUserLocked() {
@@ -225,6 +227,7 @@
         return mMaxExpandHeight;
     }
 
+    @Override
     public NotificationActivator getActivator() {
         return mActivator;
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationActivatable.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationActivatable.java
new file mode 100644
index 0000000..410a3aa
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationActivatable.java
@@ -0,0 +1,25 @@
+/*
+ * 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 620e457..097857c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationActivator.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationActivator.java
@@ -34,14 +34,40 @@
     private static final float INVERSE_ALPHA = 0.9f;
     private static final float DIMMED_SCALE = 0.95f;
 
-    private final View mTargetView;
+    /**
+     * Normal state. Notification is fully interactable.
+     */
+    private static final int STATE_NORMAL = 0;
 
+    /**
+     * Dimmed state. Neutral state when on the lockscreen, with slight transparency and scaled down
+     * a bit.
+     */
+    private static final int STATE_DIMMED = 1;
+
+    /**
+     * Activated state. Used after tapping a notification on the lockscreen. Normal transparency and
+     * normal scale.
+     */
+    private static final int STATE_ACTIVATED = 2;
+
+    /**
+     * Inverse activated state. Used for the other notifications on the lockscreen when tapping on
+     * one.
+     */
+    private static final int STATE_ACTIVATED_INVERSE = 3;
+
+    private final View mTargetView;
+    private final View mHotspotView;
     private final Interpolator mFastOutSlowInInterpolator;
     private final Interpolator mLinearOutSlowInInterpolator;
     private final int mTranslationZ;
 
-    public NotificationActivator(View targetView) {
+    private int mState;
+
+    public NotificationActivator(View targetView, View hotspotView) {
         mTargetView = targetView;
+        mHotspotView = hotspotView;
         Context ctx = targetView.getContext();
         mFastOutSlowInInterpolator =
                 AnimationUtils.loadInterpolator(ctx, android.R.interpolator.fast_out_slow_in);
@@ -53,18 +79,37 @@
     }
 
     public void activateInverse() {
+        if (mState == STATE_ACTIVATED_INVERSE) {
+            return;
+        }
+        mTargetView.animate().cancel();
         mTargetView.animate().withLayer().alpha(INVERSE_ALPHA);
+        mState = STATE_ACTIVATED_INVERSE;
+    }
+
+    public void addHotspot() {
+        mHotspotView.getBackground().setHotspot(
+                0, mHotspotView.getWidth()/2, mHotspotView.getHeight()/2);
     }
 
     public void activate() {
+        if (mState == STATE_ACTIVATED) {
+            return;
+        }
+        mTargetView.animate().cancel();
         mTargetView.animate()
                 .setInterpolator(mLinearOutSlowInInterpolator)
                 .scaleX(1)
                 .scaleY(1)
                 .translationZBy(mTranslationZ);
+        mState = STATE_ACTIVATED;
     }
 
     public void reset() {
+        if (mState == STATE_DIMMED) {
+            return;
+        }
+        mTargetView.animate().cancel();
         mTargetView.animate()
                 .setInterpolator(mFastOutSlowInInterpolator)
                 .scaleX(DIMMED_SCALE)
@@ -73,15 +118,21 @@
         if (mTargetView.getAlpha() != 1.0f) {
             mTargetView.animate().withLayer().alpha(1);
         }
+        mHotspotView.getBackground().removeHotspot(0);
+        mState = STATE_DIMMED;
     }
 
     public void setDimmed(boolean dimmed) {
         if (dimmed) {
+            mTargetView.animate().cancel();
             mTargetView.setScaleX(DIMMED_SCALE);
             mTargetView.setScaleY(DIMMED_SCALE);
+            mState = STATE_DIMMED;
         } else {
+            mTargetView.animate().cancel();
             mTargetView.setScaleX(1);
             mTargetView.setScaleY(1);
+            mState = STATE_NORMAL;
         }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationOverflowContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationOverflowContainer.java
index 8ebd50d..6f9cbf8 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationOverflowContainer.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationOverflowContainer.java
@@ -25,7 +25,8 @@
 /**
  * Container view for overflowing notification icons on Keyguard.
  */
-public class NotificationOverflowContainer extends ActivatableNotificationView {
+public class NotificationOverflowContainer extends ActivatableNotificationView implements
+        NotificationActivatable {
 
     private NotificationOverflowIconsView mIconsView;
     private NotificationActivator mActivator;
@@ -60,7 +61,7 @@
         mIconsView = (NotificationOverflowIconsView) findViewById(R.id.overflow_icons_view);
         mIconsView.setMoreText((TextView) findViewById(R.id.more_text));
 
-        mActivator = new NotificationActivator(this);
+        mActivator = new NotificationActivator(this, this);
         mActivator.setDimmed(true);
         setLocked(true);
         setDimmed(true);
@@ -70,6 +71,7 @@
         return mIconsView;
     }
 
+    @Override
     public NotificationActivator getActivator() {
         return mActivator;
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarState.java b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarState.java
new file mode 100644
index 0000000..9d75228
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarState.java
@@ -0,0 +1,40 @@
+/*
+ * 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;
+
+/**
+ * Class to encapsulate all possible status bar states regarding Keyguard.
+ */
+public class StatusBarState {
+
+    /**
+     * The status bar is in the "normal" shade mode.
+     */
+    public static final int SHADE = 0;
+
+    /**
+     * Status bar is currently the Keyguard.
+     */
+    public static final int KEYGUARD = 1;
+
+    /**
+     * Status bar is in the special mode, where it is fully interactive but still locked. So
+     * dismissing the shade will still show the bouncer.
+     */
+    public static final int SHADE_LOCKED = 2;
+
+}
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 712eec8..ec7d80a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java
@@ -25,6 +25,7 @@
 import com.android.systemui.R;
 import com.android.systemui.statusbar.ExpandableView;
 import com.android.systemui.statusbar.GestureRecorder;
+import com.android.systemui.statusbar.StatusBarState;
 import com.android.systemui.statusbar.stack.NotificationStackScrollLayout;
 
 public class NotificationPanelView extends PanelView implements
@@ -76,7 +77,7 @@
         super.onLayout(changed, left, top, right, bottom);
         int keyguardBottomMargin =
                 ((MarginLayoutParams) mKeyguardStatusView.getLayoutParams()).bottomMargin;
-        mNotificationStackScroller.setTopPadding(mStatusBar.isOnKeyguard()
+        mNotificationStackScroller.setTopPadding(mStatusBar.getBarState() == StatusBarState.KEYGUARD
                 ? mKeyguardStatusView.getBottom() + keyguardBottomMargin
                 : mHeader.getBottom() + mNotificationTopPadding);
     }
@@ -107,14 +108,18 @@
     public boolean onInterceptTouchEvent(MotionEvent event) {
         // intercept for quick settings
         if (event.getAction() == MotionEvent.ACTION_DOWN) {
-            final View target = mStatusBar.isOnKeyguard() ?  mKeyguardStatusView : mHeader;
+            final View target = mStatusBar.getBarState() == StatusBarState.KEYGUARD
+                    ? mKeyguardStatusView
+                    : mHeader;
             final boolean inTarget = PhoneStatusBar.inBounds(target, event, true);
             if (inTarget && !isInSettings()) {
                 mTrackingSettings = true;
+                requestDisallowInterceptTouchEvent(true);
                 return true;
             }
             if (!inTarget && isInSettings()) {
                 mTrackingSettings = true;
+                requestDisallowInterceptTouchEvent(true);
                 return true;
             }
         }
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 0c3462c..7d8f3ef 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java
@@ -96,13 +96,17 @@
 import com.android.systemui.keyguard.KeyguardViewMediator;
 import com.android.systemui.statusbar.BaseStatusBar;
 import com.android.systemui.statusbar.CommandQueue;
+import com.android.systemui.statusbar.DragDownHelper;
+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;
 import com.android.systemui.statusbar.SignalClusterView;
 import com.android.systemui.statusbar.StatusBarIconView;
+import com.android.systemui.statusbar.StatusBarState;
 import com.android.systemui.statusbar.policy.BatteryController;
 import com.android.systemui.statusbar.policy.BluetoothController;
 import com.android.systemui.statusbar.policy.DateView;
@@ -120,7 +124,8 @@
 import java.util.Collection;
 import java.util.Collections;
 
-public class PhoneStatusBar extends BaseStatusBar implements DemoMode {
+public class PhoneStatusBar extends BaseStatusBar implements DemoMode,
+        DragDownHelper.OnDragDownListener {
     static final String TAG = "PhoneStatusBar";
     public static final boolean DEBUG = BaseStatusBar.DEBUG;
     public static final boolean SPEW = false;
@@ -233,6 +238,7 @@
     View mNotificationPanelHeader;
     View mKeyguardStatusView;
     View mKeyguardBottomArea;
+    boolean mLeaveOpenOnKeyguardHide;
     KeyguardIndicationTextView mKeyguardIndicationTextView;
 
     // TODO: Fetch phrase from search/hotword provider.
@@ -470,6 +476,13 @@
         }
     };
 
+    private final View.OnClickListener mOverflowClickListener = new View.OnClickListener() {
+        @Override
+        public void onClick(View v) {
+            goToLockedShade(null);
+        }
+    };
+
     @Override
     public void setZenMode(int mode) {
         super.setZenMode(mode);
@@ -619,6 +632,7 @@
                         R.layout.status_bar_notification_keyguard_overflow, mStackScroller, false);
         mKeyguardIconOverflowContainer.setOnActivatedListener(this);
         mKeyguardCarrierLabel = mStatusBarWindow.findViewById(R.id.keyguard_carrier_text);
+        mKeyguardIconOverflowContainer.setOnClickListener(mOverflowClickListener);
         mStackScroller.addView(mKeyguardIconOverflowContainer);
 
         mExpandedContents = mStackScroller;
@@ -784,7 +798,9 @@
         } else if (mSettingsTracker != null && (event.getAction() == MotionEvent.ACTION_UP
                 || event.getAction() == MotionEvent.ACTION_CANCEL)) {
             final float dy = event.getY() - mSettingsDownY;
-            final FlipperButton flipper = mOnKeyguard ? mKeyguardFlipper : mHeaderFlipper;
+            final FlipperButton flipper = mState == StatusBarState.KEYGUARD
+                    ? mKeyguardFlipper
+                    : mHeaderFlipper;
             final boolean inButton = flipper.inHolderBounds(event);
             final boolean qsTap = mSettingsClosing && Math.abs(dy) < slop;
             if (!qsTap && !inButton) {
@@ -1192,7 +1208,7 @@
             }
 
             if (CLOSE_PANEL_WHEN_EMPTIED && mNotificationData.size() == 0
-                    && !mNotificationPanel.isTracking() && !mOnKeyguard) {
+                    && !mNotificationPanel.isTracking() && mState != StatusBarState.KEYGUARD) {
                 animateCollapsePanels();
             }
         }
@@ -1346,7 +1362,7 @@
             && mStackScroller.getHeight() < (mNotificationPanel.getHeight()
                     - mCarrierLabelHeight - mNotificationHeaderHeight)
             && mStackScroller.getVisibility() == View.VISIBLE
-            && !mOnKeyguard;
+            && mState != StatusBarState.KEYGUARD;
 
         if (force || mCarrierLabelVisible != makeVisible) {
             mCarrierLabelVisible = makeVisible;
@@ -2097,7 +2113,7 @@
         if (mDemoMode) return;
         int sbMode = mStatusBarMode;
         if (panelsEnabled() && (mInteractingWindows & StatusBarManager.WINDOW_STATUS_BAR) != 0
-                && !mOnKeyguard) {
+                && mState != StatusBarState.KEYGUARD) {
             // if panels are expandable, force the status bar opaque on any interaction
             sbMode = MODE_OPAQUE;
         }
@@ -2957,28 +2973,37 @@
         }
     }
 
-    public boolean isOnKeyguard() {
-        return mOnKeyguard;
+    /**
+     * @return The {@link StatusBarState} the status bar is in.
+     */
+    public int getBarState() {
+        return mState;
     }
 
     public void showKeyguard() {
-        mOnKeyguard = true;
+        setBarState(StatusBarState.KEYGUARD);
         updateKeyguardState();
         instantExpandNotificationsPanel();
+        mLeaveOpenOnKeyguardHide = false;
     }
 
     public void hideKeyguard() {
-        mOnKeyguard = false;
+        setBarState(StatusBarState.SHADE);
+        if (mLeaveOpenOnKeyguardHide) {
+            mLeaveOpenOnKeyguardHide = false;
+        } else {
+            instantCollapseNotificationPanel();
+        }
         updateKeyguardState();
-        instantCollapseNotificationPanel();
     }
 
     private void updatePublicMode() {
-        setLockscreenPublicMode(mOnKeyguard && mStatusBarKeyguardViewManager.isSecure());
+        setLockscreenPublicMode(mState == StatusBarState.KEYGUARD
+                && mStatusBarKeyguardViewManager.isSecure());
     }
 
     private void updateKeyguardState() {
-        if (mOnKeyguard) {
+        if (mState == StatusBarState.KEYGUARD) {
             if (isFlippedToSettings()) {
                 flipToNotifications(false /*animate*/);
             }
@@ -3010,17 +3035,17 @@
     }
 
     public void userActivity() {
-        if (mOnKeyguard) {
+        if (mState == StatusBarState.KEYGUARD) {
             mKeyguardViewMediatorCallback.userActivity();
         }
     }
 
     public boolean onMenuPressed() {
-        return mOnKeyguard && mStatusBarKeyguardViewManager.onMenuPressed();
+        return mState == StatusBarState.KEYGUARD && mStatusBarKeyguardViewManager.onMenuPressed();
     }
 
     public boolean onBackPressed() {
-        if (mOnKeyguard) {
+        if (mState == StatusBarState.KEYGUARD || mState == StatusBarState.SHADE_LOCKED) {
             return mStatusBarKeyguardViewManager.onBackPressed();
         } else {
             animateCollapsePanels();
@@ -3029,7 +3054,7 @@
     }
 
     private void showBouncer() {
-        if (mOnKeyguard) {
+        if (mState == StatusBarState.KEYGUARD || mState == StatusBarState.SHADE_LOCKED) {
             mStatusBarKeyguardViewManager.dismiss();
         }
     }
@@ -3065,6 +3090,14 @@
         super.onActivated(view);
     }
 
+    /**
+     * @param state The {@link StatusBarState} to set.
+     */
+    public void setBarState(int state) {
+        mState = state;
+        mStatusBarWindowManager.setStatusBarState(state);
+    }
+
     @Override
     public void onReset(View view) {
         super.onReset(view);
@@ -3072,13 +3105,13 @@
     }
 
     public void onTrackingStarted() {
-        if (mOnKeyguard) {
+        if (mState == StatusBarState.KEYGUARD) {
             mKeyguardIndicationTextView.switchIndication(R.string.keyguard_unlock);
         }
     }
 
     public void onTrackingStopped() {
-        if (mOnKeyguard) {
+        if (mState == StatusBarState.KEYGUARD) {
             mKeyguardIndicationTextView.switchIndication(mKeyguardHotwordPhrase);
         }
     }
@@ -3092,6 +3125,56 @@
         return mNavigationBarView;
     }
 
+    // ---------------------- DragDownHelper.OnDragDownListener ------------------------------------
+
+    @Override
+    public void onDraggedDown(View startingChild) {
+        goToLockedShade(startingChild);
+    }
+
+    @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();
+        }
+    }
+
+    public void onThresholdReached() {
+        // TODO: Add visual hint that threshold is reached.
+    }
+
+    /**
+     * If secure with redaction: Show bouncer, go to unlocked shade.
+     *
+     * <p>If secure without redaction: Go to {@link StatusBarState#SHADE_LOCKED}.</p>
+     *
+     * <p>Otherwise go directly to unlocked shade.</p>
+     *
+     * @param expandView The view to expand after going to the shade.
+     */
+    public void goToLockedShade(View expandView) {
+        if (expandView instanceof ExpandableNotificationRow) {
+            ExpandableNotificationRow row = (ExpandableNotificationRow) expandView;
+            row.setUserExpanded(true);
+        }
+        if (isLockscreenPublicMode() && !userAllowsPrivateNotificationsInPublic(mCurrentUserId)) {
+            mLeaveOpenOnKeyguardHide = true;
+            showBouncer();
+        } else if (mStatusBarKeyguardViewManager.isSecure()) {
+            setBarState(StatusBarState.SHADE_LOCKED);
+            updateKeyguardState();
+        } else {
+            mLeaveOpenOnKeyguardHide = true;
+            mStatusBarKeyguardViewManager.dismiss();
+        }
+    }
+
     /**
      * @return a ViewGroup that spans the entire panel which contains the quick settings
      */
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 79c63f7..c9c2867 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarView.java
@@ -29,6 +29,7 @@
 
 import com.android.systemui.EventLogTags;
 import com.android.systemui.R;
+import com.android.systemui.statusbar.StatusBarState;
 
 public class PhoneStatusBarView extends PanelBar {
     private static final String TAG = "PhoneStatusBarView";
@@ -187,7 +188,9 @@
         if (panel == mFadingPanel && mScrimColor != 0 && ActivityManager.isHighEndGfx()
                 && mBar.mStatusBarWindow != null) {
             if (mShouldFade) {
-                int scrimColor = mBar.isOnKeyguard() ? mScrimColorKeyguard : mScrimColor;
+                int scrimColor = mBar.getBarState() == StatusBarState.KEYGUARD
+                        ? mScrimColorKeyguard
+                        : mScrimColor;
                 frac = mPanelExpandedFractionSum; // don't judge me
                 // let's start this 20% of the way down the screen
                 frac = frac * 1.2f - 0.2f;
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 d175d7a..a04baea 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarWindowManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarWindowManager.java
@@ -16,7 +16,6 @@
 
 package com.android.systemui.statusbar.phone;
 
-import android.app.ActionBar;
 import android.content.Context;
 import android.content.pm.ActivityInfo;
 import android.content.res.Resources;
@@ -28,6 +27,8 @@
 import android.view.WindowManager;
 
 import com.android.keyguard.R;
+import com.android.systemui.statusbar.BaseStatusBar;
+import com.android.systemui.statusbar.StatusBarState;
 
 /**
  * Encapsulates all logic for the status bar window state management.
@@ -137,7 +138,8 @@
     }
 
     private void applyUserActivityTimeout(State state) {
-        if (state.isKeyguardShowingAndNotOccluded()) {
+        if (state.isKeyguardShowingAndNotOccluded()
+                && state.statusBarState == StatusBarState.KEYGUARD) {
             mLp.userActivityTimeout = state.keyguardUserActivityTimeout;
         } else {
             mLp.userActivityTimeout = -1;
@@ -194,6 +196,14 @@
         apply(mCurrentState);
     }
 
+    /**
+     * @param state The {@link StatusBarState} of the status bar.
+     */
+    public void setStatusBarState(int state) {
+        mCurrentState.statusBarState = state;
+        apply(mCurrentState);
+    }
+
     private static class State {
         boolean keyguardShowing;
         boolean keyguardOccluded;
@@ -202,6 +212,11 @@
         boolean statusBarFocusable;
         long keyguardUserActivityTimeout;
 
+        /**
+         * The {@link BaseStatusBar} state from the status bar.
+         */
+        int statusBarState;
+
         private boolean isKeyguardShowingAndNotOccluded() {
             return keyguardShowing && !keyguardOccluded;
         }
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 6b5ef5a..3e5ba16 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarWindowView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarWindowView.java
@@ -31,16 +31,17 @@
 import com.android.systemui.ExpandHelper;
 import com.android.systemui.R;
 import com.android.systemui.statusbar.BaseStatusBar;
-import com.android.systemui.statusbar.policy.ScrollAdapter;
+import com.android.systemui.statusbar.DragDownHelper;
+import com.android.systemui.statusbar.StatusBarState;
 import com.android.systemui.statusbar.stack.NotificationStackScrollLayout;
 
 
-public class StatusBarWindowView extends FrameLayout
-{
+public class StatusBarWindowView extends FrameLayout {
     public static final String TAG = "StatusBarWindowView";
     public static final boolean DEBUG = BaseStatusBar.DEBUG;
 
     private ExpandHelper mExpandHelper;
+    private DragDownHelper mDragDownHelper;
     private NotificationStackScrollLayout mStackScrollLayout;
     private NotificationPanelView mNotificationPanel;
 
@@ -75,6 +76,7 @@
                 minHeight, maxHeight);
         mExpandHelper.setEventSource(this);
         mExpandHelper.setScrollAdapter(mStackScrollLayout);
+        mDragDownHelper = new DragDownHelper(getContext(), this, mStackScrollLayout, mService);
 
         // We really need to be able to animate while window animations are going on
         // so that activities may be started asynchronously from panel animations
@@ -106,8 +108,12 @@
         boolean intercept = false;
         if (mNotificationPanel.isFullyExpanded()
                 && mStackScrollLayout.getVisibility() == View.VISIBLE
-                && !mService.isOnKeyguard()) {
+                && mService.getBarState() != StatusBarState.KEYGUARD) {
             intercept = mExpandHelper.onInterceptTouchEvent(ev);
+        } else if (mNotificationPanel.isFullyExpanded()
+                && mStackScrollLayout.getVisibility() == View.VISIBLE
+                && mService.getBarState() == StatusBarState.KEYGUARD) {
+            intercept = mDragDownHelper.onInterceptTouchEvent(ev);
         }
         if (!intercept) {
             super.onInterceptTouchEvent(ev);
@@ -124,8 +130,11 @@
     @Override
     public boolean onTouchEvent(MotionEvent ev) {
         boolean handled = false;
-        if (mNotificationPanel.isFullyExpanded()) {
+        if (mNotificationPanel.isFullyExpanded()
+                && mService.getBarState() != StatusBarState.KEYGUARD) {
             handled = mExpandHelper.onTouchEvent(ev);
+        } else if (mService.getBarState() == StatusBarState.KEYGUARD) {
+            handled = mDragDownHelper.onTouchEvent(ev);
         }
         if (!handled) {
             handled = super.onTouchEvent(ev);