Integrate Heads-up notifications into the shade

Change-Id: I4ca0fb4e76e7c974490538c168da0564fe97e0ae
diff --git a/packages/SystemUI/res/anim/heads_up_enter.xml b/packages/SystemUI/res/anim/heads_up_enter.xml
deleted file mode 100644
index 59eef42..0000000
--- a/packages/SystemUI/res/anim/heads_up_enter.xml
+++ /dev/null
@@ -1,12 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<set xmlns:android="http://schemas.android.com/apk/res/android"
-        >
-    <translate
-        android:interpolator="@android:interpolator/overshoot"
-        android:fromYDelta="-50%" android:toYDelta="0"
-        android:duration="@android:integer/config_shortAnimTime" />
-    <alpha 
-        android:interpolator="@android:interpolator/decelerate_quad"
-        android:fromAlpha="0.0" android:toAlpha="1.0"
-        android:duration="@android:integer/config_shortAnimTime" />
-</set>
diff --git a/packages/SystemUI/res/anim/heads_up_exit.xml b/packages/SystemUI/res/anim/heads_up_exit.xml
deleted file mode 100644
index 2cad8f6..0000000
--- a/packages/SystemUI/res/anim/heads_up_exit.xml
+++ /dev/null
@@ -1,12 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<set xmlns:android="http://schemas.android.com/apk/res/android"
-        >
-    <translate
-            android:interpolator="@android:interpolator/overshoot"
-            android:fromYDelta="0" android:toYDelta="-50%"
-            android:duration="@android:integer/config_shortAnimTime" />
-    <alpha
-        android:interpolator="@android:interpolator/accelerate_quad"
-        android:fromAlpha="1.0" android:toAlpha="0.0"
-        android:duration="@android:integer/config_shortAnimTime" />
-</set>
diff --git a/packages/SystemUI/res/layout/heads_up.xml b/packages/SystemUI/res/layout/heads_up.xml
deleted file mode 100644
index 650ee5d..0000000
--- a/packages/SystemUI/res/layout/heads_up.xml
+++ /dev/null
@@ -1,33 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-     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.
--->
-<!-- extends FrameLayout -->
-<com.android.systemui.statusbar.policy.HeadsUpNotificationView
-        xmlns:android="http://schemas.android.com/apk/res/android"
-        android:layout_height="match_parent"
-        android:layout_width="match_parent"
-        android:background="@drawable/heads_up_scrim">
-
-        <FrameLayout
-                android:layout_width="@dimen/notification_panel_width"
-                android:layout_height="wrap_content"
-                android:layout_gravity="@integer/notification_panel_layout_gravity"
-                android:paddingStart="@dimen/notification_side_padding"
-                android:paddingEnd="@dimen/notification_side_padding"
-                android:elevation="8dp"
-                android:id="@+id/content_holder" />
-
-</com.android.systemui.statusbar.policy.HeadsUpNotificationView>
\ No newline at end of file
diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml
index 2e9e9f7..051d233 100644
--- a/packages/SystemUI/res/values/config.xml
+++ b/packages/SystemUI/res/values/config.xml
@@ -147,7 +147,7 @@
     <integer name="heads_up_default_snooze_length_ms">60000</integer>
 
     <!-- Minimum display time for a heads up notification, in milliseconds. -->
-    <integer name="heads_up_notification_minimum_time">3000</integer>
+    <integer name="heads_up_notification_minimum_time">2000</integer>
 
     <!-- milliseconds before the heads up notification accepts touches. -->
     <integer name="heads_up_sensitivity_delay">700</integer>
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index b6ff1ce..f9504fb 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -330,7 +330,6 @@
          keyguard_clock_height_fraction_* for the difference between min and max.-->
     <dimen name="keyguard_clock_notifications_margin_min">24dp</dimen>
     <dimen name="keyguard_clock_notifications_margin_max">36dp</dimen>
-    <dimen name="heads_up_window_height">250dp</dimen>
 
     <!-- The minimum amount the user needs to swipe to go to the camera / phone. -->
     <dimen name="keyguard_min_swipe_amount">110dp</dimen>
diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml
index 974cc48..87f9ca2 100644
--- a/packages/SystemUI/res/values/styles.xml
+++ b/packages/SystemUI/res/values/styles.xml
@@ -205,11 +205,6 @@
     <style name="Animation.StatusBar">
     </style>
 
-    <style name="Animation.StatusBar.HeadsUp">
-        <item name="android:windowEnterAnimation">@anim/heads_up_enter</item>
-        <item name="android:windowExitAnimation">@anim/heads_up_exit</item>
-    </style>
-
     <style name="systemui_theme" parent="@android:style/Theme.DeviceDefault">
         <item name="android:colorPrimary">@color/system_primary_color</item>
         <item name="android:colorControlActivated">@color/system_accent_color</item>
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java
index ee607a7..b9b3388 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java
@@ -96,7 +96,7 @@
 import com.android.systemui.statusbar.phone.NavigationBarView;
 import com.android.systemui.statusbar.phone.NotificationGroupManager;
 import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager;
-import com.android.systemui.statusbar.policy.HeadsUpNotificationView;
+import com.android.systemui.statusbar.policy.HeadsUpManager;
 import com.android.systemui.statusbar.policy.PreviewInflater;
 import com.android.systemui.statusbar.policy.RemoteInputView;
 import com.android.systemui.statusbar.stack.NotificationStackScrollLayout;
@@ -130,9 +130,6 @@
     protected static final int MSG_CANCEL_PRELOAD_RECENT_APPS = 1023;
     protected static final int MSG_SHOW_NEXT_AFFILIATED_TASK = 1024;
     protected static final int MSG_SHOW_PREV_AFFILIATED_TASK = 1025;
-    protected static final int MSG_SHOW_HEADS_UP = 1028;
-    protected static final int MSG_HIDE_HEADS_UP = 1029;
-    protected static final int MSG_ESCALATE_HEADS_UP = 1030;
 
     protected static final boolean ENABLE_HEADS_UP = true;
     // scores above this threshold should be displayed in heads up mode.
@@ -158,8 +155,7 @@
     protected NotificationGroupManager mGroupManager = new NotificationGroupManager();
 
     // for heads up notifications
-    protected HeadsUpNotificationView mHeadsUpNotificationView;
-    protected int mHeadsUpNotificationDecay;
+    protected HeadsUpManager mHeadsUpManager;
 
     protected int mCurrentUserId = 0;
     final protected SparseArray<UserInfo> mCurrentProfiles = new SparseArray<UserInfo>();
@@ -446,9 +442,8 @@
                     @Override
                     public void run() {
                         processForRemoteInput(sbn.getNotification());
-                        Notification n = sbn.getNotification();
-                        boolean isUpdate = mNotificationData.get(sbn.getKey()) != null
-                                || isHeadsUp(sbn.getKey());
+                        String key = sbn.getKey();
+                        boolean isUpdate = mNotificationData.get(key) != null;
 
                         // In case we don't allow child notifications, we ignore children of
                         // notifications that have a summary, since we're not going to show them
@@ -462,7 +457,7 @@
 
                             // Remove existing notification to avoid stale data.
                             if (isUpdate) {
-                                removeNotification(sbn.getKey(), rankingMap);
+                                removeNotification(key, rankingMap);
                             } else {
                                 mNotificationData.updateRanking(rankingMap);
                             }
@@ -693,13 +688,11 @@
     }
 
     private void setHeadsUpUser(int newUserId) {
-        if (mHeadsUpNotificationView != null) {
-            mHeadsUpNotificationView.setUser(newUserId);
-        }
+        mHeadsUpManager.setUser(newUserId);
     }
 
     public boolean isHeadsUp(String key) {
-      return mHeadsUpNotificationView != null && mHeadsUpNotificationView.isShowing(key);
+      return mHeadsUpManager.isHeadsUp(key);
     }
 
     @Override  // NotificationData.Environment
@@ -758,8 +751,8 @@
 
     protected View updateNotificationVetoButton(View row, StatusBarNotification n) {
         View vetoButton = row.findViewById(R.id.veto);
-        if (n.isClearable() || (mHeadsUpNotificationView.getEntry() != null
-                && mHeadsUpNotificationView.getEntry().row == row)) {
+        // TODO: make headsup non-clearable if it is in the shade
+        if (n.isClearable() || (mHeadsUpManager.isHeadsUp(n.getKey()))) {
             final String _pkg = n.getPackageName();
             final String _tag = n.getTag();
             final int _id = n.getId();
@@ -1002,9 +995,6 @@
         }
     }
 
-    public void onHeadsUpDismissed() {
-    }
-
     @Override
     public void showRecentApps(boolean triggeredFromAltTab) {
         int msg = MSG_SHOW_RECENT_APPS;
@@ -1141,13 +1131,10 @@
         // Do nothing
     }
 
-    public abstract void scheduleHeadsUpDecay(long delay);
-
-    public abstract void scheduleHeadsUpOpen();
-
-    public abstract void scheduleHeadsUpClose();
-
-    public abstract void scheduleHeadsUpEscalation();
+    /**
+     * if the interrupting notification had a fullscreen intent, fire it now.
+     */
+    public abstract void escalateHeadsUp();
 
     /**
      * Save the current "public" (locked and secure) state of the lockscreen.
@@ -1238,15 +1225,8 @@
     protected void workAroundBadLayerDrawableOpacity(View v) {
     }
 
-    protected boolean inflateViews(NotificationData.Entry entry, ViewGroup parent) {
-            return inflateViews(entry, parent, false);
-    }
-
-    protected boolean inflateViewsForHeadsUp(NotificationData.Entry entry, ViewGroup parent) {
-        return inflateViews(entry, parent, true);
-    }
-
-    private boolean inflateViews(NotificationData.Entry entry, ViewGroup parent, boolean isHeadsUp) {
+    protected boolean inflateViews(NotificationData.Entry entry, ViewGroup parent,
+            boolean isHeadsUp) {
         PackageManager pmUser = getPackageManagerForUser(
                 entry.notification.getUser().getIdentifier());
 
@@ -1284,7 +1264,6 @@
             hasUserChangedExpansion = row.hasUserChangedExpansion();
             userExpanded = row.isUserExpanded();
             userLocked = row.isUserLocked();
-            entry.row.setHeadsUp(isHeadsUp);
             entry.reset();
             if (hasUserChangedExpansion) {
                 row.setUserExpanded(userExpanded);
@@ -1597,12 +1576,12 @@
                             mCurrentUserId);
             dismissKeyguardThenExecute(new OnDismissAction() {
                 public boolean onDismiss() {
-                    if (mNotificationKey.equals(mHeadsUpNotificationView.getKey())) {
+                    if (mHeadsUpManager.isHeadsUp(mNotificationKey)) {
                         // Release the HUN notification to the shade.
                         //
                         // In most cases, when FLAG_AUTO_CANCEL is set, the notification will
                         // become canceled shortly by NoMan, but we can't assume that.
-                        mHeadsUpNotificationView.releaseImmediately();
+                        mHeadsUpManager.releaseImmediately(mNotificationKey);
                     }
                     new Thread() {
                         @Override
@@ -1740,7 +1719,8 @@
         return entry.notification;
     }
 
-    protected NotificationData.Entry createNotificationViews(StatusBarNotification sbn) {
+    protected NotificationData.Entry createNotificationViews(StatusBarNotification sbn,
+            boolean isHeadsUp) {
         if (DEBUG) {
             Log.d(TAG, "createNotificationViews(notification=" + sbn);
         }
@@ -1751,7 +1731,7 @@
 
         // Construct the expanded view.
         NotificationData.Entry entry = new NotificationData.Entry(sbn, iconView);
-        if (!inflateViews(entry, mStackScroller)) {
+        if (!inflateViews(entry, mStackScroller, isHeadsUp)) {
             handleNotificationError(sbn, "Couldn't expand RemoteViews for: " + sbn);
             return null;
         }
@@ -1889,26 +1869,117 @@
         if (DEBUG) Log.d(TAG, "updateNotification(" + notification + ")");
 
         final String key = notification.getKey();
-        boolean wasHeadsUp = isHeadsUp(key);
-        Entry oldEntry;
-        if (wasHeadsUp) {
-            oldEntry = mHeadsUpNotificationView.getEntry();
-        } else {
-            oldEntry = mNotificationData.get(key);
-        }
-        if (oldEntry == null) {
+        Entry entry = mNotificationData.get(key);
+        if (entry == null) {
             return;
         }
 
-        final StatusBarNotification oldNotification = oldEntry.notification;
+        Notification n = notification.getNotification();
+        if (DEBUG) {
+            logUpdate(entry, n);
+        }
+        boolean applyInPlace = shouldApplyInPlace(entry, n);
+        final boolean shouldInterrupt = shouldInterrupt(notification);
+        final boolean alertAgain = shouldInterrupt && alertAgain(entry, n);
+        boolean isHeadsUp = shouldInterrupt && alertAgain;
 
+        entry.notification = notification;
+        mGroupManager.onEntryUpdated(entry, entry.notification);
+
+        boolean updateSuccessful = false;
+        if (applyInPlace) {
+            // We can just reapply the notifications in place
+            if (DEBUG) Log.d(TAG, "reusing notification for key: " + key);
+            try {
+                if (entry.icon != null) {
+                    // Update the icon
+                    final StatusBarIcon ic = new StatusBarIcon(notification.getPackageName(),
+                            notification.getUser(),
+                            n.icon,
+                            n.iconLevel,
+                            n.number,
+                            n.tickerText);
+                    entry.icon.setNotification(n);
+                    if (!entry.icon.set(ic)) {
+                        handleNotificationError(notification, "Couldn't update icon: " + ic);
+                        return;
+                    }
+                }
+                updateNotificationViews(entry, notification, isHeadsUp);
+                updateSuccessful = true;
+            }
+            catch (RuntimeException e) {
+                // It failed to add cleanly.  Log, and remove the view from the panel.
+                Log.w(TAG, "Couldn't reapply views for package " + n.contentView.getPackage(), e);
+            }
+        }
+        if (!updateSuccessful) {
+            if (DEBUG) Log.d(TAG, "not reusing notification for key: " + key);
+            final StatusBarIcon ic = new StatusBarIcon(notification.getPackageName(),
+                    notification.getUser(),
+                    n.icon,
+                    n.iconLevel,
+                    n.number,
+                    n.tickerText);
+            entry.icon.setNotification(n);
+            entry.icon.set(ic);
+            inflateViews(entry, mStackScroller, isHeadsUp);
+        }
+        mNotificationData.updateRanking(ranking);
+        updateNotifications();
+        updateHeadsUp(key, entry, shouldInterrupt, alertAgain);
+
+        // Update the veto button accordingly (and as a result, whether this row is
+        // swipe-dismissable)
+        updateNotificationVetoButton(entry.row, notification);
+
+        // Is this for you?
+        boolean isForCurrentUser = isNotificationForCurrentProfiles(notification);
+        if (DEBUG) Log.d(TAG, "notification is " + (isForCurrentUser ? "" : "not ") + "for you");
+
+        // Recalculate the position of the sliding windows and the titles.
+        setAreThereNotifications();
+    }
+
+    private void updateHeadsUp(String key, Entry entry, boolean shouldInterrupt,
+            boolean alertAgain) {
+        final boolean wasHeadsUp = isHeadsUp(key);
+        if (wasHeadsUp) {
+            mHeadsUpManager.updateNotification(entry, alertAgain);
+            if (!shouldInterrupt) {
+                mHeadsUpManager.removeNotification(key);
+            }
+        }
+    }
+
+    private void logUpdate(Entry oldEntry, Notification n) {
+        StatusBarNotification oldNotification = oldEntry.notification;
+        Log.d(TAG, "old notification: when=" + oldNotification.getNotification().when
+                + " ongoing=" + oldNotification.isOngoing()
+                + " expanded=" + oldEntry.expanded
+                + " contentView=" + oldNotification.getNotification().contentView
+                + " bigContentView=" + oldNotification.getNotification().bigContentView
+                + " publicView=" + oldNotification.getNotification().publicVersion
+                + " rowParent=" + oldEntry.row.getParent());
+        Log.d(TAG, "new notification: when=" + n.when
+                + " ongoing=" + oldNotification.isOngoing()
+                + " contentView=" + n.contentView
+                + " bigContentView=" + n.bigContentView
+                + " publicView=" + n.publicVersion);
+    }
+
+    /**
+     * @return whether we can just reapply the RemoteViews in place when it is updated
+     */
+    private boolean shouldApplyInPlace(Entry entry, Notification n) {
+        StatusBarNotification oldNotification = entry.notification;
         // XXX: modify when we do something more intelligent with the two content views
         final RemoteViews oldContentView = oldNotification.getNotification().contentView;
-        Notification n = notification.getNotification();
         final RemoteViews contentView = n.contentView;
         final RemoteViews oldBigContentView = oldNotification.getNotification().bigContentView;
         final RemoteViews bigContentView = n.bigContentView;
-        final RemoteViews oldHeadsUpContentView = oldNotification.getNotification().headsUpContentView;
+        final RemoteViews oldHeadsUpContentView
+                = oldNotification.getNotification().headsUpContentView;
         final RemoteViews headsUpContentView = n.headsUpContentView;
         final Notification oldPublicNotification = oldNotification.getNotification().publicVersion;
         final RemoteViews oldPublicContentView = oldPublicNotification != null
@@ -1916,34 +1987,15 @@
         final Notification publicNotification = n.publicVersion;
         final RemoteViews publicContentView = publicNotification != null
                 ? publicNotification.contentView : null;
-
-        if (DEBUG) {
-            Log.d(TAG, "old notification: when=" + oldNotification.getNotification().when
-                    + " ongoing=" + oldNotification.isOngoing()
-                    + " expanded=" + oldEntry.expanded
-                    + " contentView=" + oldContentView
-                    + " bigContentView=" + oldBigContentView
-                    + " publicView=" + oldPublicContentView
-                    + " rowParent=" + oldEntry.row.getParent());
-            Log.d(TAG, "new notification: when=" + n.when
-                    + " ongoing=" + oldNotification.isOngoing()
-                    + " contentView=" + contentView
-                    + " bigContentView=" + bigContentView
-                    + " publicView=" + publicContentView);
-        }
-
-        // Can we just reapply the RemoteViews in place?
-
-        // 1U is never null
-        boolean contentsUnchanged = oldEntry.expanded != null
+        boolean contentsUnchanged = entry.expanded != null
                 && contentView.getPackage() != null
                 && oldContentView.getPackage() != null
                 && oldContentView.getPackage().equals(contentView.getPackage())
                 && oldContentView.getLayoutId() == contentView.getLayoutId();
         // large view may be null
         boolean bigContentsUnchanged =
-                (oldEntry.getBigContentView() == null && bigContentView == null)
-                || ((oldEntry.getBigContentView() != null && bigContentView != null)
+                (entry.getBigContentView() == null && bigContentView == null)
+                || ((entry.getBigContentView() != null && bigContentView != null)
                     && bigContentView.getPackage() != null
                     && oldBigContentView.getPackage() != null
                     && oldBigContentView.getPackage().equals(bigContentView.getPackage())
@@ -1962,123 +2014,8 @@
                         && oldPublicContentView.getPackage() != null
                         && oldPublicContentView.getPackage().equals(publicContentView.getPackage())
                         && oldPublicContentView.getLayoutId() == publicContentView.getLayoutId());
-
-        final boolean shouldInterrupt = shouldInterrupt(notification);
-        final boolean alertAgain = shouldInterrupt && alertAgain(oldEntry, n);
-        boolean updateSuccessful = false;
-        if (contentsUnchanged && bigContentsUnchanged && headsUpContentsUnchanged
-                && publicUnchanged) {
-            if (DEBUG) Log.d(TAG, "reusing notification for key: " + key);
-            oldEntry.notification = notification;
-            mGroupManager.onEntryUpdated(oldEntry, oldNotification);
-            try {
-                if (oldEntry.icon != null) {
-                    // Update the icon
-                    final StatusBarIcon ic = new StatusBarIcon(notification.getPackageName(),
-                            notification.getUser(),
-                            n.icon,
-                            n.iconLevel,
-                            n.number,
-                            n.tickerText);
-                    oldEntry.icon.setNotification(n);
-                    if (!oldEntry.icon.set(ic)) {
-                        handleNotificationError(notification, "Couldn't update icon: " + ic);
-                        return;
-                    }
-                }
-
-                if (wasHeadsUp) {
-                    // Release may hang on to the views for a bit, so we should always update them.
-                    updateHeadsUpViews(oldEntry, notification);
-                    mHeadsUpNotificationView.updateNotification(oldEntry, alertAgain);
-                    if (!shouldInterrupt) {
-                        // we updated the notification above, so release to build a new shade entry
-                        mHeadsUpNotificationView.release();
-                        return;
-                    }
-                } else {
-                    if (shouldInterrupt && alertAgain) {
-                        mStackScroller.setRemoveAnimationEnabled(false);
-                        removeNotificationViews(key, ranking);
-                        mStackScroller.setRemoveAnimationEnabled(true);
-                        addNotification(notification, ranking, oldEntry);  //this will pop the headsup
-                    } else {
-                        updateNotificationViews(oldEntry, notification);
-                    }
-                }
-                mNotificationData.updateRanking(ranking);
-                updateNotifications();
-                updateSuccessful = true;
-            }
-            catch (RuntimeException e) {
-                // It failed to add cleanly.  Log, and remove the view from the panel.
-                Log.w(TAG, "Couldn't reapply views for package " + contentView.getPackage(), e);
-            }
-        }
-        if (!updateSuccessful) {
-            if (DEBUG) Log.d(TAG, "not reusing notification for key: " + key);
-            if (wasHeadsUp) {
-                if (DEBUG) Log.d(TAG, "rebuilding heads up for key: " + key);
-                ViewGroup holder = mHeadsUpNotificationView.getHolder();
-                if (inflateViewsForHeadsUp(oldEntry, holder)) {
-                    mHeadsUpNotificationView.updateNotification(oldEntry, alertAgain);
-                } else {
-                    Log.w(TAG, "Couldn't create new updated headsup for package "
-                            + contentView.getPackage());
-                }
-                if (!shouldInterrupt) {
-                    if (DEBUG) Log.d(TAG, "releasing heads up for key: " + key);
-                    oldEntry.notification = notification;
-                    mGroupManager.onEntryUpdated(oldEntry, oldNotification);
-                    mHeadsUpNotificationView.release();
-                    return;
-                }
-            } else {
-                if (shouldInterrupt && alertAgain) {
-                    if (DEBUG) Log.d(TAG, "reposting to invoke heads up for key: " + key);
-                    mStackScroller.setRemoveAnimationEnabled(false);
-                    removeNotificationViews(key, ranking);
-                    mStackScroller.setRemoveAnimationEnabled(true);
-                    addNotification(notification, ranking, oldEntry);  //this will pop the headsup
-                } else {
-                    if (DEBUG) Log.d(TAG, "rebuilding update in place for key: " + key);
-                    oldEntry.notification = notification;
-                    mGroupManager.onEntryUpdated(oldEntry, oldNotification);
-                    final StatusBarIcon ic = new StatusBarIcon(notification.getPackageName(),
-                            notification.getUser(),
-                            n.icon,
-                            n.iconLevel,
-                            n.number,
-                            n.tickerText);
-                    oldEntry.icon.setNotification(n);
-                    oldEntry.icon.set(ic);
-                    inflateViews(oldEntry, mStackScroller, wasHeadsUp);
-                    mNotificationData.updateRanking(ranking);
-                    updateNotifications();
-                }
-            }
-        }
-
-        // Update the veto button accordingly (and as a result, whether this row is
-        // swipe-dismissable)
-        updateNotificationVetoButton(oldEntry.row, notification);
-
-        // Is this for you?
-        boolean isForCurrentUser = isNotificationForCurrentProfiles(notification);
-        if (DEBUG) Log.d(TAG, "notification is " + (isForCurrentUser ? "" : "not ") + "for you");
-
-        // Recalculate the position of the sliding windows and the titles.
-        setAreThereNotifications();
-    }
-
-    private void updateNotificationViews(NotificationData.Entry entry,
-            StatusBarNotification notification) {
-        updateNotificationViews(entry, notification, false);
-    }
-
-    private void updateHeadsUpViews(NotificationData.Entry entry,
-            StatusBarNotification notification) {
-        updateNotificationViews(entry, notification, true);
+        return contentsUnchanged && bigContentsUnchanged && headsUpContentsUnchanged
+                && publicUnchanged;
     }
 
     private void updateNotificationViews(NotificationData.Entry entry,
@@ -2115,10 +2052,8 @@
         applyRemoteInput(entry);
     }
 
-    protected void notifyHeadsUpScreenOn(boolean screenOn) {
-        if (!screenOn) {
-            scheduleHeadsUpEscalation();
-        }
+    protected void notifyHeadsUpScreenOff() {
+        escalateHeadsUp();
     }
 
     private boolean alertAgain(Entry oldEntry, Notification newNotification) {
@@ -2134,7 +2069,7 @@
             return false;
         }
 
-        if (mHeadsUpNotificationView.isSnoozed(sbn.getPackageName())) {
+        if (mHeadsUpManager.isSnoozed(sbn.getPackageName())) {
             return false;
         }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableNotificationRow.java
index 06a174e..60093df 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableNotificationRow.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableNotificationRow.java
@@ -108,6 +108,7 @@
                     !mChildrenExpanded);
         }
     };
+    private boolean mInShade;
 
     public void setIconAnimationRunning(boolean running) {
         setIconAnimationRunning(running, mPublicLayout);
@@ -164,6 +165,10 @@
         return mStatusBarNotification;
     }
 
+    public boolean isHeadsUp() {
+        return mIsHeadsUp;
+    }
+
     public void setHeadsUp(boolean isHeadsUp) {
         mIsHeadsUp = isHeadsUp;
     }
@@ -263,6 +268,19 @@
         return realActualHeight;
     }
 
+    public void setInShade(boolean inShade) {
+        mInShade = inShade;
+    }
+
+    public boolean isInShade() {
+        return mInShade;
+    }
+
+    public int getHeadsUpHeight() {
+        // TODO: improve this logic
+        return getIntrinsicHeight();
+    }
+
     public interface ExpansionLogger {
         public void logNotificationExpansion(String key, boolean userAction, boolean expanded);
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpTouchHelper.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpTouchHelper.java
new file mode 100644
index 0000000..386be7e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpTouchHelper.java
@@ -0,0 +1,161 @@
+/*
+ * Copyright (C) 2015 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.phone;
+
+import android.content.Context;
+import android.view.MotionEvent;
+import android.view.ViewConfiguration;
+
+import com.android.systemui.Gefingerpoken;
+import com.android.systemui.statusbar.ExpandableNotificationRow;
+import com.android.systemui.statusbar.ExpandableView;
+import com.android.systemui.statusbar.policy.HeadsUpManager;
+import com.android.systemui.statusbar.stack.NotificationStackScrollLayout;
+
+/**
+ * A Helper class to handle touches on the heads-up views
+ */
+public class HeadsUpTouchHelper implements Gefingerpoken {
+
+    private HeadsUpManager mHeadsUpManager;
+    private NotificationStackScrollLayout mStackScroller;
+    private int mTrackingPointer;
+    private float mTouchSlop;
+    private float mInitialTouchX;
+    private float mInitialTouchY;
+    private boolean mMotionOnHeadsUpView;
+    private boolean mTrackingHeadsUp;
+    private boolean mCollapseSnoozes;
+    private NotificationPanelView mPanel;
+    private ExpandableNotificationRow mPickedChild;
+
+    public boolean isTrackingHeadsUp() {
+        return mTrackingHeadsUp;
+    }
+
+    @Override
+    public boolean onInterceptTouchEvent(MotionEvent event) {
+        if (!mMotionOnHeadsUpView && event.getActionMasked() != MotionEvent.ACTION_DOWN) {
+            return false;
+        }
+        int pointerIndex = event.findPointerIndex(mTrackingPointer);
+        if (pointerIndex < 0) {
+            pointerIndex = 0;
+            mTrackingPointer = event.getPointerId(pointerIndex);
+        }
+        final float x = event.getX(pointerIndex);
+        final float y = event.getY(pointerIndex);
+        switch (event.getActionMasked()) {
+            case MotionEvent.ACTION_DOWN:
+                mInitialTouchY = y;
+                mInitialTouchX = x;
+                setTrackingHeadsUp(false);
+                ExpandableView child = mStackScroller.getChildAtPosition(x, y);
+                mMotionOnHeadsUpView = false;
+                if (child instanceof ExpandableNotificationRow) {
+                    mPickedChild = (ExpandableNotificationRow) child;
+                    mMotionOnHeadsUpView = mPickedChild.isHeadsUp() && !mPickedChild.isInShade();
+                }
+                break;
+            case MotionEvent.ACTION_POINTER_UP:
+                final int upPointer = event.getPointerId(event.getActionIndex());
+                if (mTrackingPointer == upPointer) {
+                    // gesture is ongoing, find a new pointer to track
+                    final int newIndex = event.getPointerId(0) != upPointer ? 0 : 1;
+                    mTrackingPointer = event.getPointerId(newIndex);
+                    mInitialTouchX = event.getX(newIndex);
+                    mInitialTouchY = event.getY(newIndex);
+                }
+                break;
+
+            case MotionEvent.ACTION_MOVE:
+                final float h = y - mInitialTouchY;
+                if (Math.abs(h) > mTouchSlop && Math.abs(h) > Math.abs(x - mInitialTouchX)) {
+                    setTrackingHeadsUp(true);
+                    mCollapseSnoozes = h < 0;
+                    mInitialTouchX = x;
+                    mInitialTouchY = y;
+                    mHeadsUpManager.releaseAllToShade();
+                    int expandedHeight = mPickedChild.getActualHeight();
+                    mPanel.startExpandMotion(x, y, true /* startTracking */, expandedHeight);
+                    return true;
+                }
+                break;
+
+            case MotionEvent.ACTION_CANCEL:
+            case MotionEvent.ACTION_UP:
+                if (mPickedChild != null && mMotionOnHeadsUpView) {
+                    if (mHeadsUpManager.shouldSwallowClick(
+                            mPickedChild.getStatusBarNotification().getKey())) {
+                        endMotion();
+                        return true;
+                    }
+                }
+                endMotion();
+                break;
+        }
+        return false;
+    }
+
+    private void setTrackingHeadsUp(boolean tracking) {
+        mTrackingHeadsUp = tracking;
+        mHeadsUpManager.setTrackingHeadsUp(tracking);
+        mPanel.setTrackingHeadsUp(tracking);
+    }
+
+    public void notifyFling(boolean collapse) {
+        if (collapse && mCollapseSnoozes) {
+            mHeadsUpManager.snooze();
+        }
+        mCollapseSnoozes = false;
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent event) {
+        if (!mTrackingHeadsUp) {
+            return false;
+        }
+        switch (event.getActionMasked()) {
+            case MotionEvent.ACTION_UP:
+            case MotionEvent.ACTION_CANCEL:
+                endMotion();
+                setTrackingHeadsUp(false);
+                break;
+        }
+        return true;
+    }
+
+    private void endMotion() {
+        mTrackingPointer = -1;
+        mPickedChild = null;
+        mMotionOnHeadsUpView = false;
+    }
+
+    public ExpandableView getPickedChild() {
+        return mPickedChild;
+    }
+
+    public void bind(HeadsUpManager headsUpManager, NotificationStackScrollLayout stackScroller,
+            NotificationPanelView notificationPanelView) {
+        mHeadsUpManager = headsUpManager;
+        mStackScroller = stackScroller;
+        mPanel = notificationPanelView;
+        Context context = stackScroller.getContext();
+        final ViewConfiguration configuration = ViewConfiguration.get(context);
+        mTouchSlop = configuration.getScaledTouchSlop();
+    }
+}
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 02b6c19..6fe609e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java
@@ -32,6 +32,7 @@
 import android.view.VelocityTracker;
 import android.view.View;
 import android.view.ViewTreeObserver;
+import android.view.WindowInsets;
 import android.view.accessibility.AccessibilityEvent;
 import android.view.animation.AnimationUtils;
 import android.view.animation.Interpolator;
@@ -39,8 +40,8 @@
 import android.widget.TextView;
 
 import com.android.keyguard.KeyguardStatusView;
-import com.android.systemui.EventLogTags;
 import com.android.systemui.EventLogConstants;
+import com.android.systemui.EventLogTags;
 import com.android.systemui.R;
 import com.android.systemui.qs.QSContainer;
 import com.android.systemui.qs.QSPanel;
@@ -48,7 +49,9 @@
 import com.android.systemui.statusbar.FlingAnimationUtils;
 import com.android.systemui.statusbar.GestureRecorder;
 import com.android.systemui.statusbar.KeyguardAffordanceView;
+import com.android.systemui.statusbar.NotificationData;
 import com.android.systemui.statusbar.StatusBarState;
+import com.android.systemui.statusbar.policy.HeadsUpManager;
 import com.android.systemui.statusbar.policy.KeyguardUserSwitcher;
 import com.android.systemui.statusbar.stack.NotificationStackScrollLayout;
 import com.android.systemui.statusbar.stack.StackStateAnimator;
@@ -56,7 +59,8 @@
 public class NotificationPanelView extends PanelView implements
         ExpandableView.OnHeightChangedListener, ObservableScrollView.Listener,
         View.OnClickListener, NotificationStackScrollLayout.OnOverscrollTopChangedListener,
-        KeyguardAffordanceHelper.Callback, NotificationStackScrollLayout.OnEmptySpaceClickListener {
+        KeyguardAffordanceHelper.Callback, NotificationStackScrollLayout.OnEmptySpaceClickListener,
+        HeadsUpManager.OnHeadsUpChangedListener {
 
     private static final boolean DEBUG = false;
 
@@ -81,7 +85,7 @@
     private TextView mClockView;
     private View mReserveNotificationSpace;
     private View mQsNavbarScrim;
-    private View mNotificationContainerParent;
+    private NotificationsQuickSettingsContainer mNotificationContainerParent;
     private NotificationStackScrollLayout mNotificationStackScroller;
     private int mNotificationTopPadding;
     private boolean mAnimateNextTopPaddingChange;
@@ -177,6 +181,16 @@
 
     private float mKeyguardStatusBarAnimateAlpha = 1f;
     private int mOldLayoutDirection;
+    private HeadsUpTouchHelper mHeadsUpTouchHelper = new HeadsUpTouchHelper();
+    private boolean mPinnedHeadsUpExist;
+    private boolean mExpansionIsFromHeadsUp;
+    private int mBottomBarHeight;
+    private Runnable mHeadsUpExistenceChangedRunnable = new Runnable() {
+        @Override
+        public void run() {
+            notifyBarPanelExpansionChanged();
+        }
+    };
 
     public NotificationPanelView(Context context, AttributeSet attrs) {
         super(context, attrs);
@@ -201,7 +215,8 @@
         mScrollView.setListener(this);
         mScrollView.setFocusable(false);
         mReserveNotificationSpace = findViewById(R.id.reserve_notification_space);
-        mNotificationContainerParent = findViewById(R.id.notification_container_parent);
+        mNotificationContainerParent = (NotificationsQuickSettingsContainer)
+                findViewById(R.id.notification_container_parent);
         mNotificationStackScroller = (NotificationStackScrollLayout)
                 findViewById(R.id.notification_stack_scroller);
         mNotificationStackScroller.setOnHeightChangedListener(this);
@@ -317,6 +332,7 @@
         if (mQsSizeChangeAnimator == null) {
             mQsContainer.setHeightOverride(mQsContainer.getDesiredHeight());
         }
+        updateMaxHeadsUpTranslation();
     }
 
     @Override
@@ -493,22 +509,42 @@
     }
 
     @Override
+    protected void flingToHeight(float vel, boolean expand, float target) {
+        if (!expand && mHeadsUpManager.hasPinnedHeadsUp()) {
+            target = mHeadsUpManager.getHighestPinnedHeadsUp();
+        }
+        mHeadsUpTouchHelper.notifyFling(!expand);
+        super.flingToHeight(vel, expand, target);
+    }
+
+    @Override
     public boolean dispatchPopulateAccessibilityEventInternal(AccessibilityEvent event) {
         if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) {
             event.getText().add(getKeyguardOrLockScreenString());
             mLastAnnouncementWasQuickSettings = false;
             return true;
         }
-
         return super.dispatchPopulateAccessibilityEventInternal(event);
     }
 
     @Override
-    public boolean onInterceptTouchEvent(MotionEvent event) {
+    public boolean
+    onInterceptTouchEvent(MotionEvent event) {
         if (mBlockTouches) {
             return false;
         }
         initDownStates(event);
+        if (mHeadsUpTouchHelper.onInterceptTouchEvent(event)) {
+            mExpansionIsFromHeadsUp = true;
+            return true;
+        }
+        if (onQsIntercept(event)) {
+            return true;
+        }
+        return super.onInterceptTouchEvent(event);
+    }
+
+    private boolean onQsIntercept(MotionEvent event) {
         int pointerIndex = event.findPointerIndex(mTrackingPointer);
         if (pointerIndex < 0) {
             pointerIndex = 0;
@@ -583,7 +619,7 @@
                 mIntercepting = false;
                 break;
         }
-        return super.onInterceptTouchEvent(event);
+        return false;
     }
 
     @Override
@@ -652,6 +688,15 @@
         if (mOnlyAffordanceInThisMotion) {
             return true;
         }
+        mHeadsUpTouchHelper.onTouchEvent(event);
+        if (!mHeadsUpTouchHelper.isTrackingHeadsUp() && handleQSTouch(event)) {
+            return true;
+        }
+        super.onTouchEvent(event);
+        return true;
+    }
+
+    private boolean handleQSTouch(MotionEvent event) {
         if (event.getActionMasked() == MotionEvent.ACTION_DOWN && getExpandedFraction() == 1f
                 && mStatusBar.getBarState() != StatusBarState.KEYGUARD && !mQsExpanded
                 && mQsExpansionEnabled) {
@@ -691,8 +736,7 @@
             // earlier so the state is already up to date when dragging down.
             setListening(true);
         }
-        super.onTouchEvent(event);
-        return true;
+        return false;
     }
 
     private boolean isInQsArea(float x, float y) {
@@ -834,13 +878,13 @@
         setQsExpansion(mQsExpansionHeight);
         flingSettings(!mQsExpansionEnabled && open ? 0f : velocity, open && mQsExpansionEnabled,
                 new Runnable() {
-            @Override
-            public void run() {
-                mStackScrollerOverscrolling = false;
-                mQsExpansionFromOverscroll = false;
-                updateQsState();
-            }
-        });
+                    @Override
+                    public void run() {
+                        mStackScrollerOverscrolling = false;
+                        mQsExpansionFromOverscroll = false;
+                        updateQsState();
+                    }
+                });
     }
 
     private void onQsExpansionStarted() {
@@ -869,6 +913,7 @@
             mNotificationStackScroller.setInterceptDelegateEnabled(expanded);
             mStatusBar.setQsExpanded(expanded);
             mQsPanel.setExpanded(expanded);
+            mNotificationContainerParent.setQsExpanded(expanded);
         }
     }
 
@@ -1056,7 +1101,7 @@
             mKeyguardBottomArea.animate()
                     .alpha(0f)
                     .setStartDelay(mStatusBar.getKeyguardFadingAwayDelay())
-                    .setDuration(mStatusBar.getKeyguardFadingAwayDuration()/2)
+                    .setDuration(mStatusBar.getKeyguardFadingAwayDuration() / 2)
                     .setInterpolator(PhoneStatusBar.ALPHA_OUT)
                     .withEndAction(mAnimateKeyguardBottomAreaInvisibleEndRunnable)
                     .start();
@@ -1402,6 +1447,8 @@
         updateHeader();
         updateUnlockIcon();
         updateNotificationTranslucency();
+        mHeadsUpManager.setIsExpanded(!isShadeCollapsed());
+        mNotificationStackScroller.setShadeExpanded(!isShadeCollapsed());
         if (DEBUG) {
             invalidate();
         }
@@ -1544,7 +1591,14 @@
                 return mExpandedHeight / HEADER_RUBBERBAND_FACTOR - mQsMinExpansionHeight;
             }
         }
-        return Math.min(0, mNotificationStackScroller.getTranslationY()) / HEADER_RUBBERBAND_FACTOR;
+        float paddingOffset = mNotificationStackScroller.getPaddingOffset();
+        float translation = paddingOffset / HEADER_RUBBERBAND_FACTOR;
+        if (mHeadsUpManager.hasPinnedHeadsUp() || mExpansionIsFromHeadsUp) {
+            translation = mNotificationStackScroller.getTopPadding()
+                    + mNotificationStackScroller.getPaddingOffset()
+                    - mNotificationTopPadding - mQsMinExpansionHeight;
+        }
+        return Math.min(0, translation);
     }
 
     /**
@@ -1605,8 +1659,10 @@
     protected void onExpandingFinished() {
         super.onExpandingFinished();
         mNotificationStackScroller.onExpansionStopped();
+        mHeadsUpManager.onExpandingFinished();
         mIsExpanding = false;
         mScrollYOverride = -1;
+        // TODO: look into whether this is still correct
         if (mExpandedHeight == 0f) {
             setListening(false);
         } else {
@@ -1614,6 +1670,8 @@
         }
         mQsExpandImmediate = false;
         mTwoFingerQsExpandPossible = false;
+        mExpansionIsFromHeadsUp = false;
+        mNotificationStackScroller.setTrackingHeadsUp(mHeadsUpTouchHelper.isTrackingHeadsUp());
     }
 
     private void setListening(boolean listening) {
@@ -1709,6 +1767,17 @@
     }
 
     @Override
+    public WindowInsets onApplyWindowInsets(WindowInsets insets) {
+        mBottomBarHeight = insets.getSystemWindowInsetBottom();
+        updateMaxHeadsUpTranslation();
+        return insets;
+    }
+
+    private void updateMaxHeadsUpTranslation() {
+        mNotificationStackScroller.setMaxHeadsUpTranslation(getHeight() - mBottomBarHeight);
+    }
+
+    @Override
     public void onRtlPropertiesChanged(int layoutDirection) {
         if (layoutDirection != mOldLayoutDirection) {
             mAfforanceHelper.onRtlPropertiesChanged();
@@ -2066,4 +2135,44 @@
                     mNotificationStackScroller.getTopPadding(), p);
         }
     }
+
+    @Override
+    public void OnPinnedHeadsUpExistChanged(final boolean exist, boolean changeImmediatly) {
+        if (exist != mPinnedHeadsUpExist) {
+            mPinnedHeadsUpExist = exist;
+            if (!exist) {
+                mNotificationStackScroller.performOnAnimationFinished(
+                        mHeadsUpExistenceChangedRunnable);
+            } else {
+                mHeadsUpExistenceChangedRunnable.run();
+            }
+        }
+    }
+
+    @Override
+    public void OnHeadsUpStateChanged(NotificationData.Entry entry, boolean isHeadsUp) {
+        // TODO: figure out the conditions when not to generate an animation
+        mNotificationStackScroller.generateHeadsUpAnimation(entry.row, isHeadsUp);
+        if (isShadeCollapsed()) {
+            setExpandedHeight(mHeadsUpManager.getHighestPinnedHeadsUp());
+        }
+    }
+
+    private boolean isShadeCollapsed() {
+        // TODO: handle this cleaner
+        return mHeader.getTranslationY() + mHeader.getCollapsedHeight() <= 0;
+    }
+
+    @Override
+    public void setHeadsUpManager(HeadsUpManager headsUpManager) {
+        super.setHeadsUpManager(headsUpManager);
+        mHeadsUpTouchHelper.bind(headsUpManager, mNotificationStackScroller, this);
+    }
+
+    public void setTrackingHeadsUp(boolean tracking) {
+        if (tracking) {
+            // otherwise we update the state when the expansion is finished
+            mNotificationStackScroller.setTrackingHeadsUp(true);
+        }
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationsQuickSettingsContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationsQuickSettingsContainer.java
index a03c297..cbb71c5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationsQuickSettingsContainer.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationsQuickSettingsContainer.java
@@ -37,6 +37,7 @@
     private View mStackScroller;
     private View mKeyguardStatusBar;
     private boolean mInflated;
+    private boolean mQsExpanded;
 
     public NotificationsQuickSettingsContainer(Context context, AttributeSet attrs) {
         super(context, attrs);
@@ -64,26 +65,29 @@
         boolean userSwitcherVisible = mInflated && mUserSwitcher.getVisibility() == View.VISIBLE;
         boolean statusBarVisible = mKeyguardStatusBar.getVisibility() == View.VISIBLE;
 
+        View stackQsTop = mQsExpanded ? mStackScroller : mScrollView;
+        View stackQsBottom = !mQsExpanded ? mStackScroller : mScrollView;
         // Invert the order of the scroll view and user switcher such that the notifications receive
         // touches first but the panel gets drawn above.
         if (child == mScrollView) {
-            return super.drawChild(canvas, mStackScroller, drawingTime);
-        } else if (child == mStackScroller) {
-            return super.drawChild(canvas,
-                    userSwitcherVisible && statusBarVisible ? mUserSwitcher
+            return super.drawChild(canvas, userSwitcherVisible && statusBarVisible ? mUserSwitcher
                     : statusBarVisible ? mKeyguardStatusBar
                     : userSwitcherVisible ? mUserSwitcher
-                    : mScrollView,
+                    : stackQsBottom, drawingTime);
+        } else if (child == mStackScroller) {
+            return super.drawChild(canvas,
+                    userSwitcherVisible && statusBarVisible ? mKeyguardStatusBar
+                    : statusBarVisible || userSwitcherVisible ? stackQsBottom
+                    : stackQsTop,
                     drawingTime);
         } else if (child == mUserSwitcher) {
             return super.drawChild(canvas,
-                    userSwitcherVisible && statusBarVisible ? mKeyguardStatusBar
-                    : mScrollView,
+                    userSwitcherVisible && statusBarVisible ? stackQsBottom
+                    : stackQsTop,
                     drawingTime);
         } else if (child == mKeyguardStatusBar) {
             return super.drawChild(canvas,
-                    userSwitcherVisible && statusBarVisible ? mScrollView
-                    : mScrollView,
+                    stackQsTop,
                     drawingTime);
         }else {
             return super.drawChild(canvas, child, drawingTime);
@@ -97,4 +101,11 @@
             mInflated = true;
         }
     }
+
+    public void setQsExpanded(boolean expanded) {
+        if (mQsExpanded != expanded) {
+            mQsExpanded = expanded;
+            invalidate();
+        }
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelBar.java
index c6e1be9..240438a7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelBar.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelBar.java
@@ -157,8 +157,7 @@
         if (DEBUG) LOG("panelExpansionChanged: start state=%d panel=%s", mState, panel.getName());
         mPanelExpandedFractionSum = 0f;
         for (PanelView pv : mPanels) {
-            boolean visible = pv.getExpandedHeight() > 0;
-            pv.setVisibility(visible ? View.VISIBLE : View.GONE);
+            pv.setVisibility(expanded ? View.VISIBLE : View.INVISIBLE);
             // adjust any other panels that may be partially visible
             if (expanded) {
                 if (mState == STATE_CLOSED) {
@@ -167,7 +166,7 @@
                 }
                 fullyClosed = false;
                 final float thisFrac = pv.getExpandedFraction();
-                mPanelExpandedFractionSum += (visible ? thisFrac : 0);
+                mPanelExpandedFractionSum += thisFrac;
                 if (DEBUG) LOG("panelExpansionChanged:  -> %s: f=%.1f", pv.getName(), thisFrac);
                 if (panel == pv) {
                     if (thisFrac == 1f) fullyOpenedPanel = panel;
@@ -196,7 +195,6 @@
             } else {
                 pv.resetViews();
                 pv.setExpandedFraction(0); // just in case
-                pv.setVisibility(View.GONE);
                 pv.cancelPeek();
             }
         }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelView.java
index 4bbf690..1b89b3f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelView.java
@@ -38,6 +38,7 @@
 import com.android.systemui.doze.DozeLog;
 import com.android.systemui.statusbar.FlingAnimationUtils;
 import com.android.systemui.statusbar.StatusBarState;
+import com.android.systemui.statusbar.policy.HeadsUpManager;
 
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
@@ -45,6 +46,7 @@
 public abstract class PanelView extends FrameLayout {
     public static final boolean DEBUG = PanelBar.DEBUG;
     public static final String TAG = PanelView.class.getSimpleName();
+    protected HeadsUpManager mHeadsUpManager;
 
     private final void logf(String fmt, Object... args) {
         Log.v(TAG, (mViewName != null ? (mViewName + ": ") : "") + String.format(fmt, args));
@@ -238,10 +240,7 @@
 
         switch (event.getActionMasked()) {
             case MotionEvent.ACTION_DOWN:
-                mInitialTouchY = y;
-                mInitialTouchX = x;
-                mInitialOffsetOnTouch = mExpandedHeight;
-                mTouchSlopExceeded = false;
+                startExpandMotion(x, y, false /* startTracking */, mExpandedHeight);
                 mJustPeeked = false;
                 mPanelClosedOnDown = mExpandedHeight == 0.0f;
                 mHasLayoutedSinceDown = false;
@@ -274,9 +273,7 @@
                     final float newY = event.getY(newIndex);
                     final float newX = event.getX(newIndex);
                     mTrackingPointer = event.getPointerId(newIndex);
-                    mInitialOffsetOnTouch = mExpandedHeight;
-                    mInitialTouchY = newY;
-                    mInitialTouchX = newX;
+                    startExpandMotion(newX, newY, true /* startTracking */, mExpandedHeight);
                 }
                 break;
             case MotionEvent.ACTION_POINTER_DOWN:
@@ -297,9 +294,7 @@
                     mTouchSlopExceeded = true;
                     if (waitForTouchSlop && !mTracking) {
                         if (!mJustPeeked && mInitialOffsetOnTouch != 0f) {
-                            mInitialOffsetOnTouch = mExpandedHeight;
-                            mInitialTouchX = x;
-                            mInitialTouchY = y;
+                            startExpandMotion(x, y, false /* startTracking */, mExpandedHeight);
                             h = 0;
                         }
                         cancelHeightAnimator();
@@ -334,6 +329,17 @@
         return !waitForTouchSlop || mTracking;
     }
 
+    protected void startExpandMotion(float newX, float newY, boolean startTracking,
+            float expandedHeight) {
+        mInitialOffsetOnTouch = expandedHeight;
+        mInitialTouchY = newY;
+        mInitialTouchX = newX;
+        if (startTracking) {
+            mTouchSlopExceeded = true;
+            onTrackingStarted();
+        }
+    }
+
     private void endMotionEvent(MotionEvent event, float x, float y, boolean forceCancel) {
         mTrackingPointer = -1;
         if ((mTracking && mTouchSlopExceeded)
@@ -474,12 +480,7 @@
                 if (scrolledToBottom || mTouchStartedInEmptyArea) {
                     if (h < -mTouchSlop && h < -Math.abs(x - mInitialTouchX)) {
                         cancelHeightAnimator();
-                        mInitialOffsetOnTouch = mExpandedHeight;
-                        mInitialTouchY = y;
-                        mInitialTouchX = x;
-                        mTracking = true;
-                        mTouchSlopExceeded = true;
-                        onTrackingStarted();
+                        startExpandMotion(x, y, true /* startTracking */, mExpandedHeight);
                         return true;
                     }
                 }
@@ -564,7 +565,10 @@
     protected void fling(float vel, boolean expand) {
         cancelPeek();
         float target = expand ? getMaxPanelHeight() : 0.0f;
+        flingToHeight(vel, expand, target);
+    }
 
+    protected void flingToHeight(float vel, boolean expand, float target) {
         // Hack to make the expand transition look nice when clear all button is visible - we make
         // the animation only to the last notification, and then jump to the maximum panel height so
         // clear all just fades in and the decelerating motion is towards the last notification.
@@ -644,7 +648,7 @@
         mHasLayoutedSinceDown = true;
         if (mUpdateFlingOnLayout) {
             abortAnimations();
-            fling(mUpdateFlingVelocity, true);
+            fling(mUpdateFlingVelocity, true /* expands */);
             mUpdateFlingOnLayout = false;
         }
     }
@@ -805,7 +809,7 @@
         if (mExpanding) {
             notifyExpandingFinished();
         }
-        setVisibility(VISIBLE);
+        notifyBarPanelExpansionChanged();
 
         // Wait for window manager to pickup the change, so we know the maximum height of the panel
         // then.
@@ -941,9 +945,9 @@
         return animator;
     }
 
-    private void notifyBarPanelExpansionChanged() {
+    protected void notifyBarPanelExpansionChanged() {
         mBar.panelExpansionChanged(this, mExpandedFraction, mExpandedFraction > 0f || mPeekPending
-                || mPeekAnimator != null);
+                || mPeekAnimator != null || mInstantExpanding || mHeadsUpManager.hasPinnedHeadsUp());
     }
 
     /**
@@ -1014,4 +1018,8 @@
      * @return the height of the clear all button, in pixels
      */
     protected abstract int getClearAllHeight();
+
+    public void setHeadsUpManager(HeadsUpManager headsUpManager) {
+        mHeadsUpManager = headsUpManager;
+    }
 }
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 b0d6178..74a26a8 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java
@@ -17,19 +17,6 @@
 package com.android.systemui.statusbar.phone;
 
 
-import static android.app.StatusBarManager.NAVIGATION_HINT_BACK_ALT;
-import static android.app.StatusBarManager.NAVIGATION_HINT_IME_SHOWN;
-import static android.app.StatusBarManager.WINDOW_STATE_HIDDEN;
-import static android.app.StatusBarManager.WINDOW_STATE_SHOWING;
-import static android.app.StatusBarManager.windowStateToString;
-import static com.android.systemui.statusbar.phone.BarTransitions.MODE_LIGHTS_OUT;
-import static com.android.systemui.statusbar.phone.BarTransitions.MODE_LIGHTS_OUT_TRANSPARENT;
-import static com.android.systemui.statusbar.phone.BarTransitions.MODE_OPAQUE;
-import static com.android.systemui.statusbar.phone.BarTransitions.MODE_SEMI_TRANSPARENT;
-import static com.android.systemui.statusbar.phone.BarTransitions.MODE_TRANSLUCENT;
-import static com.android.systemui.statusbar.phone.BarTransitions.MODE_TRANSPARENT;
-import static com.android.systemui.statusbar.phone.BarTransitions.MODE_WARNING;
-
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
 import android.annotation.NonNull;
@@ -92,7 +79,6 @@
 import android.view.MotionEvent;
 import android.view.VelocityTracker;
 import android.view.View;
-import android.view.ViewGroup;
 import android.view.ViewGroup.LayoutParams;
 import android.view.ViewStub;
 import android.view.WindowManager;
@@ -137,7 +123,6 @@
 import com.android.systemui.statusbar.ScrimView;
 import com.android.systemui.statusbar.SignalClusterView;
 import com.android.systemui.statusbar.SpeedBumpView;
-import com.android.systemui.statusbar.StatusBarIconView;
 import com.android.systemui.statusbar.StatusBarState;
 import com.android.systemui.statusbar.phone.UnlockMethodCache.OnUnlockMethodChangedListener;
 import com.android.systemui.statusbar.policy.AccessibilityController;
@@ -147,7 +132,7 @@
 import com.android.systemui.statusbar.policy.BrightnessMirrorController;
 import com.android.systemui.statusbar.policy.CastControllerImpl;
 import com.android.systemui.statusbar.policy.FlashlightController;
-import com.android.systemui.statusbar.policy.HeadsUpNotificationView;
+import com.android.systemui.statusbar.policy.HeadsUpManager;
 import com.android.systemui.statusbar.policy.HotspotControllerImpl;
 import com.android.systemui.statusbar.policy.KeyButtonView;
 import com.android.systemui.statusbar.policy.KeyguardMonitor;
@@ -172,11 +157,26 @@
 import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.TreeMap;
+
+import static android.app.StatusBarManager.NAVIGATION_HINT_BACK_ALT;
+import static android.app.StatusBarManager.NAVIGATION_HINT_IME_SHOWN;
+import static android.app.StatusBarManager.WINDOW_STATE_HIDDEN;
+import static android.app.StatusBarManager.WINDOW_STATE_SHOWING;
+import static android.app.StatusBarManager.windowStateToString;
+import static com.android.systemui.statusbar.phone.BarTransitions.MODE_LIGHTS_OUT;
+import static com.android.systemui.statusbar.phone.BarTransitions.MODE_LIGHTS_OUT_TRANSPARENT;
+import static com.android.systemui.statusbar.phone.BarTransitions.MODE_OPAQUE;
+import static com.android.systemui.statusbar.phone.BarTransitions.MODE_SEMI_TRANSPARENT;
+import static com.android.systemui.statusbar.phone.BarTransitions.MODE_TRANSLUCENT;
+import static com.android.systemui.statusbar.phone.BarTransitions.MODE_TRANSPARENT;
+import static com.android.systemui.statusbar.phone.BarTransitions.MODE_WARNING;
 
 public class PhoneStatusBar extends BaseStatusBar implements DemoMode,
-        DragDownHelper.DragDownCallback, ActivityStarter, OnUnlockMethodChangedListener {
+        DragDownHelper.DragDownCallback, ActivityStarter, OnUnlockMethodChangedListener, HeadsUpManager.OnHeadsUpChangedListener {
     static final String TAG = "PhoneStatusBar";
     public static final boolean DEBUG = BaseStatusBar.DEBUG;
     public static final boolean SPEW = false;
@@ -360,11 +360,7 @@
             if (wasUsing != mUseHeadsUp) {
                 if (!mUseHeadsUp) {
                     Log.d(TAG, "dismissing any existing heads up notification on disable event");
-                    setHeadsUpVisibility(false);
-                    mHeadsUpNotificationView.releaseImmediately();
-                    removeHeadsUpView();
-                } else {
-                    addHeadsUpView();
+                    mHeadsUpManager.releaseAllImmediately();
                 }
             }
         }
@@ -528,6 +524,8 @@
     };
     private HashMap<ExpandableNotificationRow, List<ExpandableNotificationRow>> mTmpChildOrderMap
             = new HashMap<>();
+    private HashSet<Entry> mHeadsUpEntriesToRemoveOnSwitch = new HashSet<>();
+    private RankingMap mLatestRankingMap;
 
     @Override
     public void start() {
@@ -598,7 +596,8 @@
                     }
                 }
                 return mStatusBarWindow.onTouchEvent(event);
-            }});
+            }
+        });
 
         mStatusBarView = (PhoneStatusBarView) mStatusBarWindow.findViewById(R.id.status_bar);
         mStatusBarView.setBar(this);
@@ -615,12 +614,13 @@
             mNotificationPanel.setBackground(new FastColorDrawable(context.getColor(
                     R.color.notification_panel_solid_background)));
         }
-        if (ENABLE_HEADS_UP) {
-            mHeadsUpNotificationView =
-                    (HeadsUpNotificationView) View.inflate(context, R.layout.heads_up, null);
-            mHeadsUpNotificationView.setVisibility(View.GONE);
-            mHeadsUpNotificationView.setBar(this);
-        }
+
+        mHeadsUpManager = new HeadsUpManager(context);
+        mHeadsUpManager.setBar(this);
+        mHeadsUpManager.addListener(this);
+        mHeadsUpManager.addListener(mNotificationPanel);
+        mNotificationPanel.setHeadsUpManager(mHeadsUpManager);
+
         if (MULTIUSER_DEBUG) {
             mNotificationPanelDebugText = (TextView) mNotificationPanel.findViewById(
                     R.id.header_debug_info);
@@ -667,6 +667,7 @@
         mStackScroller.setLongPressListener(getNotificationLongClicker());
         mStackScroller.setPhoneStatusBar(this);
         mStackScroller.setGroupManager(mGroupManager);
+        mStackScroller.setHeadsUpManager(mHeadsUpManager);
         mGroupManager.setOnGroupChangeListener(mStackScroller);
 
         mKeyguardIconOverflowContainer =
@@ -1084,32 +1085,6 @@
         return lp;
     }
 
-    private void addHeadsUpView() {
-        int headsUpHeight = mContext.getResources()
-                .getDimensionPixelSize(R.dimen.heads_up_window_height);
-        WindowManager.LayoutParams lp = new WindowManager.LayoutParams(
-                LayoutParams.MATCH_PARENT, headsUpHeight,
-                WindowManager.LayoutParams.TYPE_STATUS_BAR_PANEL, // above the status bar!
-                WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
-                    | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS
-                    | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
-                    | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
-                    | WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM
-                    | WindowManager.LayoutParams.FLAG_SPLIT_TOUCH,
-                PixelFormat.TRANSLUCENT);
-        lp.flags |= WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED;
-        lp.gravity = Gravity.TOP;
-        lp.setTitle("Heads Up");
-        lp.packageName = mContext.getPackageName();
-        lp.windowAnimations = R.style.Animation_StatusBar_HeadsUp;
-
-        mWindowManager.addView(mHeadsUpNotificationView, lp);
-    }
-
-    private void removeHeadsUpView() {
-        mWindowManager.removeView(mHeadsUpNotificationView);
-    }
-
     public void addIcon(String slot, int index, int viewIndex, StatusBarIcon icon) {
         mIconController.addSystemIcon(slot, index, viewIndex, icon);
     }
@@ -1131,30 +1106,15 @@
     public void addNotification(StatusBarNotification notification, RankingMap ranking,
             Entry oldEntry) {
         if (DEBUG) Log.d(TAG, "addNotification key=" + notification.getKey());
-        if (mUseHeadsUp && shouldInterrupt(notification)) {
-            if (DEBUG) Log.d(TAG, "launching notification in heads up mode");
-            Entry interruptionCandidate = oldEntry;
-            if (interruptionCandidate == null) {
-                final StatusBarIconView iconView = createIcon(notification);
-                if (iconView == null) {
-                    return;
-                }
-                interruptionCandidate = new Entry(notification, iconView);
-            }
-            ViewGroup holder = mHeadsUpNotificationView.getHolder();
-            if (inflateViewsForHeadsUp(interruptionCandidate, holder)) {
-                // 1. Populate mHeadsUpNotificationView
-                mHeadsUpNotificationView.showNotification(interruptionCandidate);
+        boolean isHeadsUp = mUseHeadsUp && shouldInterrupt(notification);
 
-                // do not show the notification in the shade, yet.
-                return;
-            }
-        }
-
-        Entry shadeEntry = createNotificationViews(notification);
+        Entry shadeEntry = createNotificationViews(notification, isHeadsUp);
         if (shadeEntry == null) {
             return;
         }
+        if (isHeadsUp) {
+            mHeadsUpManager.showNotification(shadeEntry);
+        }
 
         if (notification.getNotification().fullScreenIntent != null) {
             // Stop screensaver if the notification has a full-screen intent.
@@ -1175,18 +1135,6 @@
         setAreThereNotifications();
     }
 
-    public void displayNotificationFromHeadsUp(Entry shadeEntry) {
-
-        // The notification comes from the headsup, let's inflate the normal layout again
-        inflateViews(shadeEntry, mStackScroller);
-        shadeEntry.setInterruption();
-        shadeEntry.row.setHeadsUp(false);
-
-        addNotificationViews(shadeEntry, null);
-        // Recalculate the position of the sliding windows and the titles.
-        setAreThereNotifications();
-    }
-
     @Override
     protected void updateNotificationRanking(RankingMap ranking) {
         mNotificationData.updateRanking(ranking);
@@ -1195,10 +1143,15 @@
 
     @Override
     public void removeNotification(String key, RankingMap ranking) {
-        if (ENABLE_HEADS_UP) {
-            mHeadsUpNotificationView.removeNotification(key);
+        boolean defferRemoval = false;
+        if (mHeadsUpManager.isHeadsUp(key)) {
+            defferRemoval = !mHeadsUpManager.removeNotification(key);
         }
-
+        if (defferRemoval) {
+            mLatestRankingMap = ranking;
+            mHeadsUpEntriesToRemoveOnSwitch.add(mHeadsUpManager.getEntry(key));
+            return;
+        }
         StatusBarNotification old = removeNotificationViews(key, ranking);
         if (SPEW) Log.d(TAG, "removeNotification key=" + key + " old=" + old);
 
@@ -1870,6 +1823,38 @@
         logStateToEventlog();
     }
 
+    @Override
+    public void OnPinnedHeadsUpExistChanged(boolean exist, boolean changeImmediatly) {
+        if (exist) {
+            mStatusBarWindowManager.setHeadsUpShowing(true);
+        } else {
+            Runnable endRunnable = new Runnable() {
+                @Override
+                public void run() {
+                    if (!mHeadsUpManager.hasPinnedHeadsUp()) {
+                        mStatusBarWindowManager.setHeadsUpShowing(false);
+                    }
+                }
+            };
+            if (changeImmediatly) {
+                endRunnable.run();
+            } else {
+                mStackScroller.performOnAnimationFinished(endRunnable);
+            }
+        }
+    }
+
+    @Override
+    public void OnHeadsUpStateChanged(Entry entry, boolean isHeadsUp) {
+        if (!isHeadsUp && mHeadsUpEntriesToRemoveOnSwitch.contains(entry)) {
+            removeNotification(entry.key, mLatestRankingMap);
+            mHeadsUpEntriesToRemoveOnSwitch.remove(entry);
+            if (mHeadsUpEntriesToRemoveOnSwitch.isEmpty()) {
+                mLatestRankingMap = null;
+            }
+        }
+    }
+
     /**
      * All changes to the status bar and notifications funnel through here and are batched.
      */
@@ -1886,15 +1871,6 @@
                 case MSG_CLOSE_PANELS:
                     animateCollapsePanels();
                     break;
-                case MSG_SHOW_HEADS_UP:
-                    setHeadsUpVisibility(true);
-                    break;
-                case MSG_ESCALATE_HEADS_UP:
-                    escalateHeadsUp();
-                case MSG_HIDE_HEADS_UP:
-                    mHeadsUpNotificationView.releaseImmediately();
-                    setHeadsUpVisibility(false);
-                    break;
                 case MSG_LAUNCH_TRANSITION_TIMEOUT:
                     onLaunchTransitionTimeout();
                     break;
@@ -1903,44 +1879,16 @@
     }
 
     @Override
-    public void scheduleHeadsUpDecay(long delay) {
-        mHandler.removeMessages(MSG_HIDE_HEADS_UP);
-        if (mHeadsUpNotificationView.isClearable()) {
-            mHandler.sendEmptyMessageDelayed(MSG_HIDE_HEADS_UP, delay);
-        }
-    }
-
-    @Override
-    public void scheduleHeadsUpOpen() {
-        mHandler.removeMessages(MSG_HIDE_HEADS_UP);
-        mHandler.removeMessages(MSG_SHOW_HEADS_UP);
-        mHandler.sendEmptyMessage(MSG_SHOW_HEADS_UP);
-    }
-
-    @Override
-    public void scheduleHeadsUpClose() {
-        mHandler.removeMessages(MSG_HIDE_HEADS_UP);
-        if (mHeadsUpNotificationView.getVisibility() != View.GONE) {
-            mHandler.sendEmptyMessage(MSG_HIDE_HEADS_UP);
-        }
-    }
-
-    @Override
-    public void scheduleHeadsUpEscalation() {
-        mHandler.removeMessages(MSG_HIDE_HEADS_UP);
-        mHandler.removeMessages(MSG_ESCALATE_HEADS_UP);
-        mHandler.sendEmptyMessage(MSG_ESCALATE_HEADS_UP);
-    }
-
-    /**  if the interrupting notification had a fullscreen intent, fire it now.  */
-    private void escalateHeadsUp() {
-        if (mHeadsUpNotificationView.getEntry() != null) {
-            final StatusBarNotification sbn = mHeadsUpNotificationView.getEntry().notification;
-            mHeadsUpNotificationView.releaseImmediately();
+    public void escalateHeadsUp() {
+        TreeMap<String, HeadsUpManager.HeadsUpEntry> entries = mHeadsUpManager.getEntries();
+        for (String key : entries.keySet()) {
+            Entry entry = entries.get(key).entry;
+            final StatusBarNotification sbn = entry.notification;
             final Notification notification = sbn.getNotification();
             if (notification.fullScreenIntent != null) {
-                if (DEBUG)
+                if (DEBUG) {
                     Log.d(TAG, "converting a heads up to fullScreen");
+                }
                 try {
                     EventLog.writeEvent(EventLogTags.SYSUI_HEADS_UP_ESCALATION,
                             sbn.getKey());
@@ -1949,6 +1897,7 @@
                 }
             }
         }
+        mHeadsUpManager.releaseAllImmediately();
     }
 
     boolean panelsEnabled() {
@@ -2081,10 +2030,6 @@
         // Ensure the panel is fully collapsed (just in case; bug 6765842, 7260868)
         mStatusBarView.collapseAllPanels(/*animate=*/ false, false /* delayed*/);
 
-        // reset things to their proper state
-        mStackScroller.setVisibility(View.VISIBLE);
-        mNotificationPanel.setVisibility(View.GONE);
-
         mNotificationPanel.closeQs();
 
         mExpandedVisible = false;
@@ -2478,8 +2423,6 @@
         pw.println(Settings.Global.zenModeToString(mZenMode));
         pw.print("  mUseHeadsUp=");
         pw.println(mUseHeadsUp);
-        pw.print("  interrupting package: ");
-        pw.println(hunStateToString(mHeadsUpNotificationView.getEntry()));
         dumpBarTransitions(pw, "mStatusBarView", mStatusBarView.getBarTransitions());
         if (mNavigationBarView != null) {
             pw.print("  mNavigationBarWindowState=");
@@ -2571,10 +2514,10 @@
         if (mSecurityController != null) {
             mSecurityController.dump(fd, pw, args);
         }
-        if (mHeadsUpNotificationView != null) {
-            mHeadsUpNotificationView.dump(fd, pw, args);
+        if (mHeadsUpManager != null) {
+            mHeadsUpManager.dump(fd, pw, args);
         } else {
-            pw.println("  mHeadsUpNotificationView: null");
+            pw.println("  mHeadsUpManager: null");
         }
 
         pw.println("SharedPreferences:");
@@ -2672,7 +2615,7 @@
             else if (Intent.ACTION_SCREEN_OFF.equals(action)) {
                 mScreenOn = false;
                 notifyNavigationBarScreenOn(false);
-                notifyHeadsUpScreenOn(false);
+                notifyHeadsUpScreenOff();
                 finishBarAnimations();
                 resetUserExpandedStates();
             }
@@ -2764,15 +2707,6 @@
                 mUserSetupObserver, mCurrentUserId);
     }
 
-    private void setHeadsUpVisibility(boolean vis) {
-        if (!ENABLE_HEADS_UP) return;
-        if (DEBUG) Log.v(TAG, (vis ? "showing" : "hiding") + " heads up window");
-        EventLog.writeEvent(EventLogTags.SYSUI_HEADS_UP_STATUS,
-                vis ? mHeadsUpNotificationView.getKey() : "",
-                vis ? 1 : 0);
-        mHeadsUpNotificationView.setVisibility(vis ? View.VISIBLE : View.GONE);
-    }
-
     /**
      * Reload some of our resources when the configuration changes.
      *
@@ -2791,9 +2725,6 @@
         if (mNotificationPanel != null) {
             mNotificationPanel.updateResources();
         }
-        if (mHeadsUpNotificationView != null) {
-            mHeadsUpNotificationView.updateResources();
-        }
         if (mBrightnessMirrorController != null) {
             mBrightnessMirrorController.updateResources();
         }
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 63bbf97..6e0bf8f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarWindowManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarWindowManager.java
@@ -130,7 +130,7 @@
 
     private void applyHeight(State state) {
         boolean expanded = state.isKeyguardShowingAndNotOccluded() || state.statusBarExpanded
-                || state.keyguardFadingAway || state.bouncerShowing;
+                || state.keyguardFadingAway || state.bouncerShowing || state.headsUpShowing;
         if (expanded) {
             mLpChanged.height = ViewGroup.LayoutParams.MATCH_PARENT;
         } else {
@@ -218,6 +218,11 @@
         apply(mCurrentState);
     }
 
+    public void setHeadsUpShowing(boolean showing) {
+        mCurrentState.headsUpShowing = showing;
+        apply(mCurrentState);
+    }
+
     /**
      * @param state The {@link StatusBarState} of the status bar.
      */
@@ -235,6 +240,7 @@
         boolean bouncerShowing;
         boolean keyguardFadingAway;
         boolean qsExpanded;
+        boolean headsUpShowing;
 
         /**
          * The {@link BaseStatusBar} state from the status bar.
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManager.java
new file mode 100644
index 0000000..d43af59
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManager.java
@@ -0,0 +1,474 @@
+/*
+ * Copyright (C) 2011 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.policy;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.database.ContentObserver;
+import android.os.Handler;
+import android.os.SystemClock;
+import android.provider.Settings;
+import android.util.ArrayMap;
+import android.util.Log;
+import android.util.Pools;
+import android.view.View;
+import android.view.ViewTreeObserver;
+import android.view.accessibility.AccessibilityEvent;
+
+import com.android.systemui.R;
+import com.android.systemui.statusbar.NotificationData;
+import com.android.systemui.statusbar.phone.PhoneStatusBar;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.HashSet;
+import java.util.Stack;
+import java.util.TreeMap;
+
+public class HeadsUpManager {
+    private static final String TAG = "HeadsUpManager";
+    private static final boolean DEBUG = false;
+    private static final String SETTING_HEADS_UP_SNOOZE_LENGTH_MS = "heads_up_snooze_length_ms";
+
+    private final int mHeadsUpNotificationDecay;
+    private final int mMinimumDisplayTime;
+
+    private final int mTouchSensitivityDelay;
+    private final ArrayMap<String, Long> mSnoozedPackages;
+    private final HashSet<OnHeadsUpChangedListener> mListeners = new HashSet<>();
+    private final int mDefaultSnoozeLengthMs;
+    private final Handler mHandler = new Handler();
+    private final Pools.Pool<HeadsUpEntry> mEntryPool = new Pools.Pool<HeadsUpEntry>() {
+
+        private Stack<HeadsUpEntry> mPoolObjects = new Stack<>();
+
+        @Override
+        public HeadsUpEntry acquire() {
+            if (!mPoolObjects.isEmpty()) {
+                return mPoolObjects.pop();
+            }
+            return new HeadsUpEntry();
+        }
+
+        @Override
+        public boolean release(HeadsUpEntry instance) {
+            instance.removeAutoCancelCallbacks();
+            mPoolObjects.push(instance);
+            return true;
+        }
+    };
+
+
+    private PhoneStatusBar mBar;
+    private int mSnoozeLengthMs;
+    private ContentObserver mSettingsObserver;
+
+    private TreeMap<String ,HeadsUpEntry> mHeadsUpEntries = new TreeMap<>();
+    private HashSet<String> mSwipedOutKeys = new HashSet<>();
+    private int mUser;
+    private Clock mClock;
+    private boolean mReleaseOnExpandFinish;
+    private boolean mTrackingHeadsUp;
+    private HashSet<NotificationData.Entry> mEntriesToRemoveAfterExpand = new HashSet<>();
+    private boolean mIsExpanded;
+    private boolean mHasPinnedHeadsUp;
+
+    public HeadsUpManager(final Context context) {
+        Resources resources = context.getResources();
+        mTouchSensitivityDelay = resources.getInteger(R.integer.heads_up_sensitivity_delay);
+        if (DEBUG) Log.v(TAG, "create() " + mTouchSensitivityDelay);
+        mSnoozedPackages = new ArrayMap<>();
+        mDefaultSnoozeLengthMs = resources.getInteger(R.integer.heads_up_default_snooze_length_ms);
+        mSnoozeLengthMs = mDefaultSnoozeLengthMs;
+        mMinimumDisplayTime = resources.getInteger(R.integer.heads_up_notification_minimum_time);
+        mHeadsUpNotificationDecay = 2000000;
+        mClock = new Clock();
+        // TODO: shadow mSwipeHelper.setMaxSwipeProgress(mMaxAlpha);
+
+        mSnoozeLengthMs = Settings.Global.getInt(context.getContentResolver(),
+                SETTING_HEADS_UP_SNOOZE_LENGTH_MS, mDefaultSnoozeLengthMs);
+        mSettingsObserver = new ContentObserver(mHandler) {
+            @Override
+            public void onChange(boolean selfChange) {
+                final int packageSnoozeLengthMs = Settings.Global.getInt(
+                        context.getContentResolver(), SETTING_HEADS_UP_SNOOZE_LENGTH_MS, -1);
+                if (packageSnoozeLengthMs > -1 && packageSnoozeLengthMs != mSnoozeLengthMs) {
+                    mSnoozeLengthMs = packageSnoozeLengthMs;
+                    if (DEBUG) Log.v(TAG, "mSnoozeLengthMs = " + mSnoozeLengthMs);
+                }
+            }
+        };
+        context.getContentResolver().registerContentObserver(
+                Settings.Global.getUriFor(SETTING_HEADS_UP_SNOOZE_LENGTH_MS), false,
+                mSettingsObserver);
+        if (DEBUG) Log.v(TAG, "mSnoozeLengthMs = " + mSnoozeLengthMs);
+
+        // TODO: investigate whether this is still needed
+//        if (!mHeadsUpEntries.isEmpty()) {
+//             whoops, we're on already!
+//             showNotification(mHeadsUpEntries);
+//        }
+    }
+
+    public void setBar(PhoneStatusBar bar) {
+        mBar = bar;
+    }
+
+    public void addListener(OnHeadsUpChangedListener listener) {
+        mListeners.add(listener);
+    }
+
+    public PhoneStatusBar getBar() {
+        return mBar;
+    }
+
+    /**
+     * Called when posting a new notification to the heads up.
+     */
+    public void showNotification(NotificationData.Entry headsUp) {
+        if (DEBUG) Log.v(TAG, "showNotification");
+        addHeadsUpEntry(headsUp);
+        updateNotification(headsUp, true);
+        headsUp.setInterruption();
+        updatePinnedHeadsUpState(false);
+    }
+
+    /**
+     * Called when updating or posting a notification to the heads up.
+     */
+    public void updateNotification(NotificationData.Entry headsUp, boolean alert) {
+        if (DEBUG) Log.v(TAG, "updateNotification");
+
+        headsUp.row.setChildrenExpanded(false /* expanded */, false /* animated */);
+        headsUp.row.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
+
+        if (alert) {
+            HeadsUpEntry headsUpEntry = mHeadsUpEntries.get(headsUp.key);
+            headsUpEntry.updateEntry();
+            headsUpEntry.entry.row.setInShade(mIsExpanded);
+        }
+    }
+
+    private void addHeadsUpEntry(NotificationData.Entry entry) {
+        boolean wasEmpty = mHeadsUpEntries.isEmpty();
+        HeadsUpEntry headsUpEntry = mEntryPool.acquire();
+        headsUpEntry.setEntry(entry);
+        mHeadsUpEntries.put(entry.key, headsUpEntry);
+        for (OnHeadsUpChangedListener listener : mListeners) {
+            listener.OnHeadsUpStateChanged(entry, true);
+        }
+        entry.row.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
+        entry.row.setHeadsUp(true);
+    }
+
+    private void removeHeadsUpEntry(NotificationData.Entry entry) {
+        HeadsUpEntry remove = mHeadsUpEntries.remove(entry.key);
+        mEntryPool.release(remove);
+        entry.row.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
+        entry.row.setHeadsUp(false);
+        for (OnHeadsUpChangedListener listener : mListeners) {
+            listener.OnHeadsUpStateChanged(entry, false);
+        }
+        updatePinnedHeadsUpState(false);
+    }
+
+    private void updatePinnedHeadsUpState(boolean forceImmediate) {
+        boolean hasPinnedHeadsUp = hasPinnedHeadsUpInternal();
+        if (hasPinnedHeadsUp == mHasPinnedHeadsUp) {
+            return;
+        }
+        mHasPinnedHeadsUp = hasPinnedHeadsUp;
+        for (OnHeadsUpChangedListener listener :mListeners) {
+            listener.OnPinnedHeadsUpExistChanged(hasPinnedHeadsUp, forceImmediate);
+        }
+    }
+
+    /**
+     * React to the removal of the notification in the heads up.
+     *
+     * @return true if the notification was removed and false if it still needs to be kept around
+     * for a bit since it wasn't shown long enough
+     */
+    public boolean removeNotification(String key) {
+        if (DEBUG) Log.v(TAG, "remove");
+        if (wasShownLongEnough(key)) {
+            releaseImmediately(key);
+            return true;
+        } else {
+            getHeadsUpEntry(key).hideAsSoonAsPossible();
+            return false;
+        }
+    }
+
+    private boolean wasShownLongEnough(String key) {
+        HeadsUpEntry headsUpEntry = getHeadsUpEntry(key);
+        HeadsUpEntry topEntry = getTopEntry();
+        if (mSwipedOutKeys.contains(key)) {
+            // We always instantly dismiss views being manually swiped out.
+            mSwipedOutKeys.remove(key);
+            return true;
+        }
+        if (headsUpEntry != topEntry) {
+            return true;
+        }
+        return headsUpEntry.wasShownLongEnough();
+    }
+
+    public boolean isHeadsUp(String key) {
+        return mHeadsUpEntries.containsKey(key);
+    }
+
+
+    /**
+     * Push any current Heads Up notification down into the shade.
+     */
+    public void releaseAllImmediately() {
+        if (DEBUG) Log.v(TAG, "releaseAllImmediately");
+        for (String key: mHeadsUpEntries.keySet()) {
+            releaseImmediately(key);
+        }
+    }
+
+    public void releaseImmediately(String key) {
+        HeadsUpEntry headsUpEntry = getHeadsUpEntry(key);
+        if (headsUpEntry == null) {
+            return;
+        }
+        NotificationData.Entry shadeEntry = headsUpEntry.entry;
+        removeHeadsUpEntry(shadeEntry);
+    }
+
+    public boolean isSnoozed(String packageName) {
+        final String key = snoozeKey(packageName, mUser);
+        Long snoozedUntil = mSnoozedPackages.get(key);
+        if (snoozedUntil != null) {
+            if (snoozedUntil > SystemClock.elapsedRealtime()) {
+                if (DEBUG) Log.v(TAG, key + " snoozed");
+                return true;
+            }
+            mSnoozedPackages.remove(packageName);
+        }
+        return false;
+    }
+
+    public void snooze() {
+        for (String key: mHeadsUpEntries.keySet()) {
+            HeadsUpEntry entry = mHeadsUpEntries.get(key);
+            String packageName = entry.entry.notification.getPackageName();
+            mSnoozedPackages.put(snoozeKey(packageName, mUser),
+                    SystemClock.elapsedRealtime() + mSnoozeLengthMs);
+        }
+        mReleaseOnExpandFinish = true;
+    }
+
+    private static String snoozeKey(String packageName, int user) {
+        return user + "," + packageName;
+    }
+
+    private HeadsUpEntry getHeadsUpEntry(String key) {
+        return mHeadsUpEntries.get(key);
+    }
+
+    public NotificationData.Entry getEntry(String key) {
+        return mHeadsUpEntries.get(key).entry;
+    }
+
+    public TreeMap<String, HeadsUpEntry> getEntries() {
+        return mHeadsUpEntries;
+    }
+
+    public HeadsUpEntry getTopEntry() {
+        return mHeadsUpEntries.isEmpty() ? null : mHeadsUpEntries.lastEntry().getValue();
+    }
+
+    /**
+     * @param key the key of the touched notification
+     * @return whether the touch is valid and should not be discarded
+     */
+    public boolean shouldSwallowClick(String key) {
+        if (mClock.currentTimeMillis() < mHeadsUpEntries.get(key).postTime) {
+            return true;
+        }
+        return false;
+    }
+
+    public boolean updateSwipeProgress(View animView, boolean dismissable, float swipeProgress) {
+        // TODO: handle the shadow
+        //getBackground().setAlpha((int) (255 * swipeProgress));
+        return false;
+    }
+
+    public void onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo info) {
+        // TODO: Look into touchable region
+//        mContentHolder.getLocationOnScreen(mTmpTwoArray);
+//
+//        info.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION);
+//        info.touchableRegion.set(mTmpTwoArray[0], mTmpTwoArray[1],
+//                mTmpTwoArray[0] + mContentHolder.getWidth(),
+//                mTmpTwoArray[1] + mContentHolder.getHeight());
+    }
+
+    public void setUser(int user) {
+        mUser = user;
+    }
+
+    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+        pw.println("HeadsUpManager state:");
+        pw.print("  mTouchSensitivityDelay="); pw.println(mTouchSensitivityDelay);
+        pw.print("  mSnoozeLengthMs="); pw.println(mSnoozeLengthMs);
+        pw.print("  now="); pw.println(SystemClock.elapsedRealtime());
+        pw.print("  mUser="); pw.println(mUser);
+        for (String key: mHeadsUpEntries.keySet()) {
+            pw.print("  HeadsUpEntry="); pw.println(mHeadsUpEntries.get(key));
+        }
+        int N = mSnoozedPackages.size();
+        pw.println("  snoozed packages: " + N);
+        for (int i = 0; i < N; i++) {
+            pw.print("    "); pw.print(mSnoozedPackages.valueAt(i));
+            pw.print(", "); pw.println(mSnoozedPackages.keyAt(i));
+        }
+    }
+
+    public boolean hasPinnedHeadsUp() {
+        return mHasPinnedHeadsUp;
+    }
+
+    private boolean hasPinnedHeadsUpInternal() {
+        for (String key: mHeadsUpEntries.keySet()) {
+            HeadsUpEntry entry = mHeadsUpEntries.get(key);
+            if (!entry.entry.row.isInShade()) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    public void addSwipedOutKey(String key) {
+        mSwipedOutKeys.add(key);
+    }
+
+    public float getHighestPinnedHeadsUp() {
+        float max = 0;
+        for (String key: mHeadsUpEntries.keySet()) {
+            HeadsUpEntry entry = mHeadsUpEntries.get(key);
+            if (!entry.entry.row.isInShade()) {
+                max = Math.max(max, entry.entry.row.getActualHeight());
+            }
+        }
+        return max;
+    }
+
+    public void releaseAllToShade() {
+        for (String key: mHeadsUpEntries.keySet()) {
+            HeadsUpEntry entry = mHeadsUpEntries.get(key);
+            entry.entry.row.setInShade(true);
+        }
+        updatePinnedHeadsUpState(true);
+    }
+
+    public void onExpandingFinished() {
+        if (mReleaseOnExpandFinish) {
+            releaseAllImmediately();
+            mReleaseOnExpandFinish = false;
+        } else {
+            for (NotificationData.Entry entry : mEntriesToRemoveAfterExpand) {
+                removeHeadsUpEntry(entry);
+            }
+            mEntriesToRemoveAfterExpand.clear();
+        }
+    }
+
+    public void setTrackingHeadsUp(boolean trackingHeadsUp) {
+        mTrackingHeadsUp = trackingHeadsUp;
+    }
+
+    public void setIsExpanded(boolean isExpanded) {
+        mIsExpanded = isExpanded;
+    }
+
+    public int getTopHeadsUpHeight() {
+        HeadsUpEntry topEntry = getTopEntry();
+        return topEntry != null ? topEntry.entry.row.getHeadsUpHeight() : 0;
+    }
+
+    public class HeadsUpEntry implements Comparable<HeadsUpEntry> {
+        public NotificationData.Entry entry;
+        public long postTime;
+        public long earliestRemovaltime;
+        private Runnable mRemoveHeadsUpRunnable;
+
+        public void setEntry(final NotificationData.Entry entry) {
+            this.entry = entry;
+
+            // The actual post time will be just after the heads-up really slided in
+            postTime = mClock.currentTimeMillis() + mTouchSensitivityDelay;
+            mRemoveHeadsUpRunnable = new Runnable() {
+                @Override
+                public void run() {
+                    if (!mTrackingHeadsUp) {
+                        removeHeadsUpEntry(entry);
+                    } else {
+                        mEntriesToRemoveAfterExpand.add(entry);
+                    }
+                }
+            };
+            updateEntry();
+        }
+
+        public void updateEntry() {
+            long currentTime = mClock.currentTimeMillis();
+            postTime = Math.max(postTime, currentTime);
+            long finishTime = postTime + mHeadsUpNotificationDecay;
+            long removeDelay = Math.max(finishTime - currentTime, mMinimumDisplayTime);
+            earliestRemovaltime = currentTime + mMinimumDisplayTime;
+            removeAutoCancelCallbacks();
+            mHandler.postDelayed(mRemoveHeadsUpRunnable, removeDelay);
+        }
+
+        @Override
+        public int compareTo(HeadsUpEntry o) {
+            return postTime < o.postTime ? -1
+                    : postTime == o.postTime ? 0
+                            : 1;
+        }
+
+        public void removeAutoCancelCallbacks() {
+            mHandler.removeCallbacks(mRemoveHeadsUpRunnable);
+        }
+
+        public boolean wasShownLongEnough() {
+            return earliestRemovaltime < mClock.currentTimeMillis();
+        }
+
+        public void hideAsSoonAsPossible() {
+            removeAutoCancelCallbacks();
+            mHandler.postDelayed(mRemoveHeadsUpRunnable,
+                    earliestRemovaltime - mClock.currentTimeMillis());
+        }
+    }
+
+    public static class Clock {
+        public long currentTimeMillis() {
+            return SystemClock.elapsedRealtime();
+        }
+    }
+
+    public interface OnHeadsUpChangedListener {
+        void OnPinnedHeadsUpExistChanged(boolean exist, boolean changeImmediatly);
+        void OnHeadsUpStateChanged(NotificationData.Entry entry, boolean isHeadsUp);
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpNotificationView.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpNotificationView.java
deleted file mode 100644
index 1e40bab..0000000
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpNotificationView.java
+++ /dev/null
@@ -1,622 +0,0 @@
-/*
- * Copyright (C) 2011 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.policy;
-
-import android.content.Context;
-import android.content.res.Configuration;
-import android.content.res.Resources;
-import android.database.ContentObserver;
-import android.graphics.Outline;
-import android.graphics.Rect;
-import android.os.SystemClock;
-import android.provider.Settings;
-import android.util.ArrayMap;
-import android.util.AttributeSet;
-import android.util.Log;
-import android.view.MotionEvent;
-import android.view.View;
-import android.view.ViewConfiguration;
-import android.view.ViewGroup;
-import android.view.ViewOutlineProvider;
-import android.view.ViewTreeObserver;
-import android.view.accessibility.AccessibilityEvent;
-import android.widget.FrameLayout;
-
-import com.android.internal.annotations.VisibleForTesting;
-import com.android.systemui.ExpandHelper;
-import com.android.systemui.Gefingerpoken;
-import com.android.systemui.R;
-import com.android.systemui.SwipeHelper;
-import com.android.systemui.statusbar.ExpandableView;
-import com.android.systemui.statusbar.NotificationData;
-import com.android.systemui.statusbar.phone.PhoneStatusBar;
-
-import java.io.FileDescriptor;
-import java.io.PrintWriter;
-
-public class HeadsUpNotificationView extends FrameLayout implements SwipeHelper.Callback, ExpandHelper.Callback,
-        ViewTreeObserver.OnComputeInternalInsetsListener {
-    private static final String TAG = "HeadsUpNotificationView";
-    private static final boolean DEBUG = false;
-    private static final boolean SPEW = DEBUG;
-    private static final String SETTING_HEADS_UP_SNOOZE_LENGTH_MS = "heads_up_snooze_length_ms";
-
-    Rect mTmpRect = new Rect();
-    int[] mTmpTwoArray = new int[2];
-
-    private final int mHeadsUpNotificationDecay;
-    private final int mMinimumDisplayTime;
-
-    private final int mTouchSensitivityDelay;
-    private final float mMaxAlpha = 1f;
-    private final ArrayMap<String, Long> mSnoozedPackages;
-    private final int mDefaultSnoozeLengthMs;
-
-    private SwipeHelper mSwipeHelper;
-    private EdgeSwipeHelper mEdgeSwipeHelper;
-
-    private PhoneStatusBar mBar;
-
-    private long mLingerUntilMs;
-    private long mStartTouchTime;
-    private ViewGroup mContentHolder;
-    private int mSnoozeLengthMs;
-    private ContentObserver mSettingsObserver;
-
-    private NotificationData.Entry mHeadsUp;
-    private int mUser;
-    private String mMostRecentPackageName;
-    private boolean mTouched;
-    private Clock mClock;
-
-    public static class Clock {
-        public long currentTimeMillis() {
-            return SystemClock.elapsedRealtime();
-        }
-    }
-
-    public HeadsUpNotificationView(Context context, AttributeSet attrs) {
-        this(context, attrs, 0);
-    }
-
-    public HeadsUpNotificationView(Context context, AttributeSet attrs, int defStyle) {
-        super(context, attrs, defStyle);
-        Resources resources = context.getResources();
-        mTouchSensitivityDelay = resources.getInteger(R.integer.heads_up_sensitivity_delay);
-        if (DEBUG) Log.v(TAG, "create() " + mTouchSensitivityDelay);
-        mSnoozedPackages = new ArrayMap<>();
-        mDefaultSnoozeLengthMs = resources.getInteger(R.integer.heads_up_default_snooze_length_ms);
-        mSnoozeLengthMs = mDefaultSnoozeLengthMs;
-        mMinimumDisplayTime = resources.getInteger(R.integer.heads_up_notification_minimum_time);
-        mHeadsUpNotificationDecay = resources.getInteger(R.integer.heads_up_notification_decay);
-        mClock = new Clock();
-    }
-
-    @VisibleForTesting
-    public HeadsUpNotificationView(Context context, Clock clock, SwipeHelper swipeHelper,
-            EdgeSwipeHelper edgeSwipeHelper, int headsUpNotificationDecay, int minimumDisplayTime,
-            int touchSensitivityDelay, int snoozeLength) {
-        super(context, null);
-        mClock = clock;
-        mSwipeHelper = swipeHelper;
-        mEdgeSwipeHelper = edgeSwipeHelper;
-        mMinimumDisplayTime = minimumDisplayTime;
-        mHeadsUpNotificationDecay = headsUpNotificationDecay;
-        mTouchSensitivityDelay = touchSensitivityDelay;
-        mSnoozedPackages = new ArrayMap<>();
-        mDefaultSnoozeLengthMs = snoozeLength;
-    }
-
-    public void updateResources() {
-        if (mContentHolder != null) {
-            final LayoutParams lp = (LayoutParams) mContentHolder.getLayoutParams();
-            lp.width = getResources().getDimensionPixelSize(R.dimen.notification_panel_width);
-            lp.gravity = getResources().getInteger(R.integer.notification_panel_layout_gravity);
-            mContentHolder.setLayoutParams(lp);
-        }
-    }
-
-    public void setBar(PhoneStatusBar bar) {
-        mBar = bar;
-    }
-
-    public PhoneStatusBar getBar() {
-        return mBar;
-    }
-
-    public ViewGroup getHolder() {
-        return mContentHolder;
-    }
-
-    /**
-     * Called when posting a new notification to the heads up.
-     */
-    public void showNotification(NotificationData.Entry headsUp) {
-        if (DEBUG) Log.v(TAG, "showNotification");
-        if (mHeadsUp != null) {
-            // bump any previous heads up back to the shade
-            releaseImmediately();
-        }
-        mTouched = false;
-        updateNotification(headsUp, true);
-        mLingerUntilMs = mClock.currentTimeMillis() + mMinimumDisplayTime;
-    }
-
-    /**
-     * Called when updating or posting a notification to the heads up.
-     */
-    public void updateNotification(NotificationData.Entry headsUp, boolean alert) {
-        if (DEBUG) Log.v(TAG, "updateNotification");
-
-        if (mHeadsUp == headsUp) {
-            resetViewForHeadsup();
-            // This is an in-place update.  Noting more to do.
-            return;
-        }
-
-        mHeadsUp = headsUp;
-
-        if (mContentHolder != null) {
-            mContentHolder.removeAllViews();
-        }
-
-        if (mHeadsUp != null) {
-            mMostRecentPackageName = mHeadsUp.notification.getPackageName();
-            if (mHeadsUp.row != null) {
-                resetViewForHeadsup();
-            }
-
-            mStartTouchTime = SystemClock.elapsedRealtime() + mTouchSensitivityDelay;
-            if (mContentHolder != null) {  // only null in tests and before we are attached to a window
-                mContentHolder.setX(0);
-                mContentHolder.setVisibility(View.VISIBLE);
-                mContentHolder.setAlpha(mMaxAlpha);
-                mContentHolder.addView(mHeadsUp.row);
-                sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
-
-                mSwipeHelper.snapChild(mContentHolder, 1f);
-            }
-
-            mHeadsUp.setInterruption();
-        }
-        if (alert) {
-            // Make sure the heads up window is open.
-            mBar.scheduleHeadsUpOpen();
-            mBar.scheduleHeadsUpDecay(mHeadsUpNotificationDecay);
-        }
-    }
-
-    private void resetViewForHeadsup() {
-        if (mHeadsUp.row.areChildrenExpanded()) {
-            mHeadsUp.row.setChildrenExpanded(false /* expanded */, false /* animated */);
-        }
-        mHeadsUp.row.setSystemExpanded(true);
-        mHeadsUp.row.setSensitive(false);
-        mHeadsUp.row.setHeadsUp(true);
-        mHeadsUp.row.setTranslationY(0);
-        mHeadsUp.row.setTranslationZ(0);
-        mHeadsUp.row.setHideSensitive(
-                false, false /* animated */, 0 /* delay */, 0 /* duration */);
-    }
-
-    /**
-     * Possibly enter the lingering state by delaying the closing of the window.
-     *
-     * @return true if the notification has entered the lingering state.
-     */
-    private boolean startLingering(boolean removed) {
-        final long now = mClock.currentTimeMillis();
-        if (!mTouched && mHeadsUp != null && now < mLingerUntilMs) {
-            if (removed) {
-                mHeadsUp = null;
-            }
-            mBar.scheduleHeadsUpDecay(mLingerUntilMs - now);
-            return true;
-        }
-        return false;
-    }
-
-    /**
-     * React to the removal of the notification in the heads up.
-     */
-    public void removeNotification(String key) {
-        if (DEBUG) Log.v(TAG, "remove");
-        if (mHeadsUp == null || !mHeadsUp.key.equals(key)) {
-            return;
-        }
-        if (!startLingering(/* removed */ true)) {
-            mHeadsUp = null;
-            releaseImmediately();
-        }
-    }
-
-    /**
-     * Ask for any current Heads Up notification to be pushed down into the shade.
-     */
-    public void release() {
-        if (DEBUG) Log.v(TAG, "release");
-        if (!startLingering(/* removed */ false)) {
-            releaseImmediately();
-        }
-    }
-
-    /**
-     * Push any current Heads Up notification down into the shade.
-     */
-    public void releaseImmediately() {
-        if (DEBUG) Log.v(TAG, "releaseImmediately");
-        if (mHeadsUp != null) {
-            mContentHolder.removeView(mHeadsUp.row);
-            mBar.displayNotificationFromHeadsUp(mHeadsUp);
-        }
-        mHeadsUp = null;
-        mBar.scheduleHeadsUpClose();
-    }
-
-    @Override
-    protected void onVisibilityChanged(View changedView, int visibility) {
-        super.onVisibilityChanged(changedView, visibility);
-        if (DEBUG) Log.v(TAG, "onVisibilityChanged: " + visibility);
-        if (changedView.getVisibility() == VISIBLE) {
-            mStartTouchTime = mClock.currentTimeMillis() + mTouchSensitivityDelay;
-            sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
-        }
-    }
-
-    public boolean isSnoozed(String packageName) {
-        final String key = snoozeKey(packageName, mUser);
-        Long snoozedUntil = mSnoozedPackages.get(key);
-        if (snoozedUntil != null) {
-            if (snoozedUntil > SystemClock.elapsedRealtime()) {
-                if (DEBUG) Log.v(TAG, key + " snoozed");
-                return true;
-            }
-            mSnoozedPackages.remove(packageName);
-        }
-        return false;
-    }
-
-    private void snooze() {
-        if (mMostRecentPackageName != null) {
-            mSnoozedPackages.put(snoozeKey(mMostRecentPackageName, mUser),
-                    SystemClock.elapsedRealtime() + mSnoozeLengthMs);
-        }
-        releaseImmediately();
-    }
-
-    private static String snoozeKey(String packageName, int user) {
-        return user + "," + packageName;
-    }
-
-    public boolean isShowing(String key) {
-        return mHeadsUp != null && mHeadsUp.key.equals(key);
-    }
-
-    public NotificationData.Entry getEntry() {
-        return mHeadsUp;
-    }
-
-    public boolean isClearable() {
-        return mHeadsUp == null || mHeadsUp.notification.isClearable();
-    }
-
-    // ViewGroup methods
-
-private static final ViewOutlineProvider CONTENT_HOLDER_OUTLINE_PROVIDER =
-        new ViewOutlineProvider() {
-            @Override
-            public void getOutline(View view, Outline outline) {
-                int outlineLeft = view.getPaddingLeft();
-                int outlineTop = view.getPaddingTop();
-
-                // Apply padding to shadow.
-                outline.setRect(outlineLeft, outlineTop,
-                        view.getWidth() - outlineLeft - view.getPaddingRight(),
-                        view.getHeight() - outlineTop - view.getPaddingBottom());
-            }
-        };
-
-    @Override
-    public void onAttachedToWindow() {
-        final ViewConfiguration viewConfiguration = ViewConfiguration.get(getContext());
-        float touchSlop = viewConfiguration.getScaledTouchSlop();
-        mSwipeHelper = new SwipeHelper(SwipeHelper.X, this, getContext());
-        mSwipeHelper.setMaxSwipeProgress(mMaxAlpha);
-        mEdgeSwipeHelper = new EdgeSwipeHelper(this, touchSlop);
-
-        int minHeight = getResources().getDimensionPixelSize(R.dimen.notification_min_height);
-        int maxHeight = getResources().getDimensionPixelSize(R.dimen.notification_max_height);
-
-        mContentHolder = (ViewGroup) findViewById(R.id.content_holder);
-        mContentHolder.setOutlineProvider(CONTENT_HOLDER_OUTLINE_PROVIDER);
-
-        mSnoozeLengthMs = Settings.Global.getInt(mContext.getContentResolver(),
-                SETTING_HEADS_UP_SNOOZE_LENGTH_MS, mDefaultSnoozeLengthMs);
-        mSettingsObserver = new ContentObserver(getHandler()) {
-            @Override
-            public void onChange(boolean selfChange) {
-                final int packageSnoozeLengthMs = Settings.Global.getInt(
-                        mContext.getContentResolver(), SETTING_HEADS_UP_SNOOZE_LENGTH_MS, -1);
-                if (packageSnoozeLengthMs > -1 && packageSnoozeLengthMs != mSnoozeLengthMs) {
-                    mSnoozeLengthMs = packageSnoozeLengthMs;
-                    if (DEBUG) Log.v(TAG, "mSnoozeLengthMs = " + mSnoozeLengthMs);
-                }
-            }
-        };
-        mContext.getContentResolver().registerContentObserver(
-                Settings.Global.getUriFor(SETTING_HEADS_UP_SNOOZE_LENGTH_MS), false,
-                mSettingsObserver);
-        if (DEBUG) Log.v(TAG, "mSnoozeLengthMs = " + mSnoozeLengthMs);
-
-        if (mHeadsUp != null) {
-            // whoops, we're on already!
-            showNotification(mHeadsUp);
-        }
-
-        getViewTreeObserver().addOnComputeInternalInsetsListener(this);
-    }
-
-
-    @Override
-    protected void onDetachedFromWindow() {
-        mContext.getContentResolver().unregisterContentObserver(mSettingsObserver);
-    }
-
-    @Override
-    public boolean onInterceptTouchEvent(MotionEvent ev) {
-        if (DEBUG) Log.v(TAG, "onInterceptTouchEvent()");
-        if (mClock.currentTimeMillis() < mStartTouchTime) {
-            return true;
-        }
-        mTouched = true;
-        return mEdgeSwipeHelper.onInterceptTouchEvent(ev)
-                || mSwipeHelper.onInterceptTouchEvent(ev)
-                || mHeadsUp == null // lingering
-                || super.onInterceptTouchEvent(ev);
-    }
-
-    // View methods
-
-    @Override
-    public void onDraw(android.graphics.Canvas c) {
-        super.onDraw(c);
-        if (DEBUG) {
-            //Log.d(TAG, "onDraw: canvas height: " + c.getHeight() + "px; measured height: "
-            //        + getMeasuredHeight() + "px");
-            c.save();
-            c.clipRect(6, 6, c.getWidth() - 6, getMeasuredHeight() - 6,
-                    android.graphics.Region.Op.DIFFERENCE);
-            c.drawColor(0xFFcc00cc);
-            c.restore();
-        }
-    }
-
-    @Override
-    public boolean onTouchEvent(MotionEvent ev) {
-        if (mClock.currentTimeMillis() < mStartTouchTime) {
-            return false;
-        }
-
-        final boolean wasRemoved = mHeadsUp == null;
-        if (!wasRemoved) {
-            mBar.scheduleHeadsUpDecay(mHeadsUpNotificationDecay);
-        }
-        return mEdgeSwipeHelper.onTouchEvent(ev)
-                || mSwipeHelper.onTouchEvent(ev)
-                || wasRemoved
-                || super.onTouchEvent(ev);
-    }
-
-    @Override
-    protected void onConfigurationChanged(Configuration newConfig) {
-        super.onConfigurationChanged(newConfig);
-        float densityScale = getResources().getDisplayMetrics().density;
-        mSwipeHelper.setDensityScale(densityScale);
-        float pagingTouchSlop = ViewConfiguration.get(getContext()).getScaledPagingTouchSlop();
-        mSwipeHelper.setPagingTouchSlop(pagingTouchSlop);
-    }
-
-    // ExpandHelper.Callback methods
-
-    @Override
-    public ExpandableView getChildAtRawPosition(float x, float y) {
-        return getChildAtPosition(x, y);
-    }
-
-    @Override
-    public ExpandableView getChildAtPosition(float x, float y) {
-        return mHeadsUp == null ? null : mHeadsUp.row;
-    }
-
-    @Override
-    public boolean canChildBeExpanded(View v) {
-        return mHeadsUp != null && mHeadsUp.row == v && mHeadsUp.row.isExpandable();
-    }
-
-    @Override
-    public void setUserExpandedChild(View v, boolean userExpanded) {
-        if (mHeadsUp != null && mHeadsUp.row == v) {
-            mHeadsUp.row.setUserExpanded(userExpanded);
-        }
-    }
-
-    @Override
-    public void setUserLockedChild(View v, boolean userLocked) {
-        if (mHeadsUp != null && mHeadsUp.row == v) {
-            mHeadsUp.row.setUserLocked(userLocked);
-        }
-    }
-
-    @Override
-    public void expansionStateChanged(boolean isExpanding) {
-
-    }
-
-    // SwipeHelper.Callback methods
-
-    @Override
-    public boolean canChildBeDismissed(View v) {
-        return true;
-    }
-
-    @Override
-    public boolean isAntiFalsingNeeded() {
-        return false;
-    }
-
-    @Override
-    public float getFalsingThresholdFactor() {
-        return 1.0f;
-    }
-
-    @Override
-    public void onChildDismissed(View v) {
-        Log.v(TAG, "User swiped heads up to dismiss");
-        if (mHeadsUp != null && mHeadsUp.notification.isClearable()) {
-            mBar.onNotificationClear(mHeadsUp.notification);
-            mHeadsUp = null;
-        }
-        releaseImmediately();
-    }
-
-    @Override
-    public void onBeginDrag(View v) {
-    }
-
-    @Override
-    public void onDragCancelled(View v) {
-        mContentHolder.setAlpha(mMaxAlpha); // sometimes this isn't quite reset
-    }
-
-    @Override
-    public void onChildSnappedBack(View animView) {
-    }
-
-    @Override
-    public boolean updateSwipeProgress(View animView, boolean dismissable, float swipeProgress) {
-        getBackground().setAlpha((int) (255 * swipeProgress));
-        return false;
-    }
-
-    @Override
-    public View getChildAtPosition(MotionEvent ev) {
-        return mContentHolder;
-    }
-
-    @Override
-    public View getChildContentView(View v) {
-        return mContentHolder;
-    }
-
-    @Override
-    public void onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo info) {
-        mContentHolder.getLocationOnScreen(mTmpTwoArray);
-
-        info.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION);
-        info.touchableRegion.set(mTmpTwoArray[0], mTmpTwoArray[1],
-                mTmpTwoArray[0] + mContentHolder.getWidth(),
-                mTmpTwoArray[1] + mContentHolder.getHeight());
-    }
-
-    public void escalate() {
-        mBar.scheduleHeadsUpEscalation();
-    }
-
-    public String getKey() {
-        return mHeadsUp == null ? null : mHeadsUp.notification.getKey();
-    }
-
-    public void setUser(int user) {
-        mUser = user;
-    }
-
-    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
-        pw.println("HeadsUpNotificationView state:");
-        pw.print("  mTouchSensitivityDelay="); pw.println(mTouchSensitivityDelay);
-        pw.print("  mSnoozeLengthMs="); pw.println(mSnoozeLengthMs);
-        pw.print("  mLingerUntilMs="); pw.println(mLingerUntilMs);
-        pw.print("  mTouched="); pw.println(mTouched);
-        pw.print("  mMostRecentPackageName="); pw.println(mMostRecentPackageName);
-        pw.print("  mStartTouchTime="); pw.println(mStartTouchTime);
-        pw.print("  now="); pw.println(SystemClock.elapsedRealtime());
-        pw.print("  mUser="); pw.println(mUser);
-        if (mHeadsUp == null) {
-            pw.println("  mHeadsUp=null");
-        } else {
-            pw.print("  mHeadsUp="); pw.println(mHeadsUp.notification.getKey());
-        }
-        int N = mSnoozedPackages.size();
-        pw.println("  snoozed packages: " + N);
-        for (int i = 0; i < N; i++) {
-            pw.print("    "); pw.print(mSnoozedPackages.valueAt(i));
-            pw.print(", "); pw.println(mSnoozedPackages.keyAt(i));
-        }
-    }
-
-    public static class EdgeSwipeHelper implements Gefingerpoken {
-        private static final boolean DEBUG_EDGE_SWIPE = false;
-        private final float mTouchSlop;
-        private final HeadsUpNotificationView mHeadsUpView;
-        private boolean mConsuming;
-        private float mFirstY;
-        private float mFirstX;
-
-        public EdgeSwipeHelper(HeadsUpNotificationView headsUpView, float touchSlop) {
-            mHeadsUpView = headsUpView;
-            mTouchSlop = touchSlop;
-        }
-
-        @Override
-        public boolean onInterceptTouchEvent(MotionEvent ev) {
-            switch (ev.getActionMasked()) {
-                case MotionEvent.ACTION_DOWN:
-                    if (DEBUG_EDGE_SWIPE) Log.d(TAG, "action down " + ev.getY());
-                    mFirstX = ev.getX();
-                    mFirstY = ev.getY();
-                    mConsuming = false;
-                    break;
-
-                case MotionEvent.ACTION_MOVE:
-                    if (DEBUG_EDGE_SWIPE) Log.d(TAG, "action move " + ev.getY());
-                    final float dY = ev.getY() - mFirstY;
-                    final float daX = Math.abs(ev.getX() - mFirstX);
-                    final float daY = Math.abs(dY);
-                    if (!mConsuming && daX < daY && daY > mTouchSlop) {
-                        mHeadsUpView.snooze();
-                        if (dY > 0) {
-                            if (DEBUG_EDGE_SWIPE) Log.d(TAG, "found an open");
-                            mHeadsUpView.getBar().animateExpandNotificationsPanel();
-                        }
-                        mConsuming = true;
-                    }
-                    break;
-
-                case MotionEvent.ACTION_UP:
-                case MotionEvent.ACTION_CANCEL:
-                    if (DEBUG_EDGE_SWIPE) Log.d(TAG, "action done");
-                    mConsuming = false;
-                    break;
-            }
-            return mConsuming;
-        }
-
-        @Override
-        public boolean onTouchEvent(MotionEvent ev) {
-            return mConsuming;
-        }
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/stack/AmbientState.java b/packages/SystemUI/src/com/android/systemui/statusbar/stack/AmbientState.java
index 8e677f1..824ba94 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/stack/AmbientState.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/stack/AmbientState.java
@@ -18,8 +18,10 @@
 
 import android.view.View;
 import com.android.systemui.statusbar.ActivatableNotificationView;
+import com.android.systemui.statusbar.policy.HeadsUpManager;
 
 import java.util.ArrayList;
+import java.util.TreeMap;
 
 /**
  * A global state to track all input states for the algorithm.
@@ -34,6 +36,12 @@
     private int mSpeedBumpIndex = -1;
     private boolean mDark;
     private boolean mHideSensitive;
+    private HeadsUpManager mHeadsUpManager;
+    private float mPaddingOffset;
+    private int mLayoutHeight;
+    private int mTopPadding;
+    private boolean mShadeExpanded;
+    private float mMaxHeadsUpTranslation;
 
     public int getScrollY() {
         return mScrollY;
@@ -115,4 +123,56 @@
     public void setSpeedBumpIndex(int speedBumpIndex) {
         mSpeedBumpIndex = speedBumpIndex;
     }
+
+    public void setHeadsUpManager(HeadsUpManager headsUpManager) {
+        mHeadsUpManager = headsUpManager;
+    }
+
+    public TreeMap<String, HeadsUpManager.HeadsUpEntry> getHeadsUpEntries() {
+        return mHeadsUpManager.getEntries();
+    }
+
+    public float getPaddingOffset() {
+        return mPaddingOffset;
+    }
+
+    public void setPaddingOffset(float paddingOffset) {
+        mPaddingOffset = paddingOffset;
+    }
+
+    public int getLayoutHeight() {
+        return mLayoutHeight;
+    }
+
+    public void setLayoutHeight(int layoutHeight) {
+        mLayoutHeight = layoutHeight;
+    }
+
+    public int getTopPadding() {
+        return mTopPadding;
+    }
+
+    public void setTopPadding(int topPadding) {
+        mTopPadding = topPadding;
+    }
+
+    public int getInnerHeight() {
+        return mLayoutHeight - mTopPadding;
+    }
+
+    public boolean isShadeExpanded() {
+        return mShadeExpanded;
+    }
+
+    public void setShadeExpanded(boolean shadeExpanded) {
+        mShadeExpanded = shadeExpanded;
+    }
+
+    public void setMaxHeadsUpTranslation(float maxHeadsUpTranslation) {
+        mMaxHeadsUpTranslation = maxHeadsUpTranslation;
+    }
+
+    public float getMaxHeadsUpTranslation() {
+        return mMaxHeadsUpTranslation;
+    }
 }
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 2eafd57..cac1b8a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java
@@ -24,6 +24,7 @@
 import android.graphics.PointF;
 import android.util.AttributeSet;
 import android.util.Log;
+import android.util.Pair;
 import android.view.MotionEvent;
 import android.view.VelocityTracker;
 import android.view.View;
@@ -47,6 +48,7 @@
 import com.android.systemui.statusbar.StatusBarState;
 import com.android.systemui.statusbar.phone.NotificationGroupManager;
 import com.android.systemui.statusbar.phone.PhoneStatusBar;
+import com.android.systemui.statusbar.policy.HeadsUpManager;
 import com.android.systemui.statusbar.policy.ScrollAdapter;
 
 import java.util.ArrayList;
@@ -179,19 +181,21 @@
     private float mMinTopOverScrollToEscape;
     private int mIntrinsicPadding;
     private int mNotificationTopPadding;
+    private float mPaddingOffset;
     private float mTopPaddingOverflow;
     private boolean mDontReportNextOverScroll;
     private boolean mRequestViewResizeAnimationOnLayout;
     private boolean mNeedViewResizeAnimation;
     private View mExpandedGroupView;
-    private boolean mEverythingNeedsAnimation;
 
+    private boolean mEverythingNeedsAnimation;
     /**
      * The maximum scrollPosition which we are allowed to reach when a notification was expanded.
      * This is needed to avoid scrolling too far after the notification was collapsed in the same
      * motion.
      */
     private int mMaxScrollAfterExpand;
+
     private SwipeHelper.LongPressListener mLongPressListener;
 
     /**
@@ -203,8 +207,8 @@
     private boolean mInterceptDelegateEnabled;
     private boolean mDelegateToScrollView;
     private boolean mDisallowScrollingInThisMotion;
-    private long mGoToFullShadeDelay;
 
+    private long mGoToFullShadeDelay;
     private ViewTreeObserver.OnPreDrawListener mChildrenUpdater
             = new ViewTreeObserver.OnPreDrawListener() {
         @Override
@@ -219,6 +223,11 @@
     private int[] mTempInt2 = new int[2];
     private boolean mGenerateChildOrderChangedEvent;
     private boolean mRemoveAnimationEnabled;
+    private HashSet<Runnable> mAnimationFinishedRunnables = new HashSet<>();
+    private HashSet<Pair<ExpandableNotificationRow, Boolean>> mHeadsUpChangeAnimations
+            = new HashSet<>();
+    private HeadsUpManager mHeadsUpManager;
+    private boolean mTrackingHeadsUp;
 
     public NotificationStackScrollLayout(Context context) {
         this(context, null);
@@ -404,8 +413,8 @@
     }
 
     private void updateAlgorithmHeightAndPadding() {
-        mStackScrollAlgorithm.setLayoutHeight(getLayoutHeight());
-        mStackScrollAlgorithm.setTopPadding(mTopPadding);
+        mAmbientState.setLayoutHeight(getLayoutHeight());
+        mAmbientState.setTopPadding(mTopPadding);
     }
 
     /**
@@ -478,9 +487,13 @@
         int newStackHeight = (int) height;
         int minStackHeight = getMinStackHeight();
         int stackHeight;
-        if (newStackHeight - mTopPadding - mTopPaddingOverflow >= minStackHeight
+        float paddingOffset;
+        boolean trackingHeadsUp = mTrackingHeadsUp;
+        int normalExpandPositionStart = trackingHeadsUp ? mHeadsUpManager.getTopHeadsUpHeight()
+                : minStackHeight;
+        if (newStackHeight - mTopPadding - mTopPaddingOverflow >= normalExpandPositionStart
                 || getNotGoneChildCount() == 0) {
-            setTranslationY(mTopPaddingOverflow);
+            paddingOffset = mTopPaddingOverflow;
             stackHeight = newStackHeight;
         } else {
 
@@ -492,9 +505,13 @@
             float partiallyThere = (newStackHeight - mTopPadding - mTopPaddingOverflow)
                     / minStackHeight;
             partiallyThere = Math.max(0, partiallyThere);
-            translationY += (1 - partiallyThere) * (mBottomStackPeekSize +
-                    mCollapseSecondCardPadding);
-            setTranslationY(translationY - mTopPadding);
+            if (!trackingHeadsUp) {
+                translationY += (1 - partiallyThere) * (mBottomStackPeekSize +
+                        mCollapseSecondCardPadding);
+            } else {
+                translationY = (int) (height - mHeadsUpManager.getTopHeadsUpHeight());
+            }
+            paddingOffset = translationY - mTopPadding;
             stackHeight = (int) (height - (translationY - mTopPadding));
         }
         if (stackHeight != mCurrentStackHeight) {
@@ -502,6 +519,19 @@
             updateAlgorithmHeightAndPadding();
             requestChildrenUpdate();
         }
+        setPaddingOffset(paddingOffset);
+    }
+
+    public float getPaddingOffset() {
+        return mPaddingOffset;
+    }
+
+    private void setPaddingOffset(float paddingOffset) {
+        if (paddingOffset != mPaddingOffset) {
+            mPaddingOffset = paddingOffset;
+            mAmbientState.setPaddingOffset(paddingOffset);
+            requestChildrenUpdate();
+        }
     }
 
     /**
@@ -543,11 +573,6 @@
         if (mDismissAllInProgress) {
             return;
         }
-        if (DEBUG) Log.v(TAG, "onChildDismissed: " + v);
-        final View veto = v.findViewById(R.id.veto);
-        if (veto != null && veto.getVisibility() != View.GONE) {
-            veto.performClick();
-        }
         setSwipingInProgress(false);
         if (mDragAnimPendingChildren.contains(v)) {
             // We start the swipe and finish it in the same frame, we don't want any animation
@@ -556,6 +581,17 @@
         }
         mSwipedOutViews.add(v);
         mAmbientState.onDragFinished(v);
+        if (v instanceof ExpandableNotificationRow) {
+            ExpandableNotificationRow row = (ExpandableNotificationRow) v;
+            if (row.isHeadsUp()) {
+                mHeadsUpManager.addSwipedOutKey(row.getStatusBarNotification().getKey());
+            }
+        }
+        final View veto = v.findViewById(R.id.veto);
+        if (veto != null && veto.getVisibility() != View.GONE) {
+            veto.performClick();
+        }
+        if (DEBUG) Log.v(TAG, "onChildDismissed: " + v);
     }
 
     @Override
@@ -578,11 +614,6 @@
         return false;
     }
 
-    @Override
-    public float getFalsingThresholdFactor() {
-        return mPhoneStatusBar.isScreenOnComingFromTouch() ? 1.5f : 1.0f;
-    }
-
     public void onBeginDrag(View v) {
         setSwipingInProgress(true);
         mAmbientState.onBeginDrag(v);
@@ -597,6 +628,11 @@
         setSwipingInProgress(false);
     }
 
+    @Override
+    public float getFalsingThresholdFactor() {
+        return mPhoneStatusBar.isScreenOnComingFromTouch() ? 1.5f : 1.0f;
+    }
+
     public View getChildAtPosition(MotionEvent ev) {
         return getChildAtPosition(ev.getX(), ev.getY());
     }
@@ -1803,10 +1839,21 @@
         generateGoToFullShadeEvent();
         generateViewResizeEvent();
         generateGroupExpansionEvent();
+        generateHeadsUpAnimationEvents();
         generateAnimateEverythingEvent();
         mNeedsAnimation = false;
     }
 
+    private void generateHeadsUpAnimationEvents() {
+        for (Pair<ExpandableNotificationRow, Boolean> eventPair : mHeadsUpChangeAnimations) {
+            int type = eventPair.second ? AnimationEvent.ANIMATION_TYPE_HEADS_UP_APPEAR
+                    : AnimationEvent.ANIMATION_TYPE_HEADS_UP_DISAPPEAR;
+            mAnimationEvents.add(new AnimationEvent(eventPair.first,
+                    type));
+        }
+        mHeadsUpChangeAnimations.clear();
+    }
+
     private void generateGroupExpansionEvent() {
         // Generate a group expansion/collapsing event if there is such a group at all
         if (mExpandedGroupView != null) {
@@ -2182,6 +2229,10 @@
 
     public void onChildAnimationFinished() {
         requestChildrenUpdate();
+        for (Runnable runnable : mAnimationFinishedRunnables) {
+            runnable.run();
+        }
+        mAnimationFinishedRunnables.clear();
     }
 
     /**
@@ -2579,6 +2630,36 @@
         }
     }
 
+    public void performOnAnimationFinished(Runnable runnable) {
+        mAnimationFinishedRunnables.add(runnable);
+    }
+
+    public void setHeadsUpManager(HeadsUpManager headsUpManager) {
+        mHeadsUpManager = headsUpManager;
+        mAmbientState.setHeadsUpManager(headsUpManager);
+    }
+
+    public void generateHeadsUpAnimation(ExpandableNotificationRow row, boolean isHeadsUp) {
+        if (mAnimationsEnabled) {
+            mHeadsUpChangeAnimations.add(new Pair<>(row, isHeadsUp));
+            mNeedsAnimation = true;
+            requestChildrenUpdate();
+        }
+    }
+
+    public void setShadeExpanded(boolean shadeExpanded) {
+        mAmbientState.setShadeExpanded(shadeExpanded);
+    }
+
+    public void setMaxHeadsUpTranslation(int maxTranslation) {
+        mAmbientState.setMaxHeadsUpTranslation(maxTranslation);
+        requestChildrenUpdate();
+    }
+
+    public void setTrackingHeadsUp(boolean trackingHeadsUp) {
+        mTrackingHeadsUp = trackingHeadsUp;
+    }
+
     /**
      * A listener that is notified when some child locations might have changed.
      */
@@ -2723,6 +2804,22 @@
                         .animateY()
                         .animateZ(),
 
+                // ANIMATION_TYPE_HEADS_UP_APPEAR
+                new AnimationFilter()
+                        .animateAlpha()
+                        .animateHeight()
+                        .animateTopInset()
+                        .animateY()
+                        .animateZ(),
+
+                // ANIMATION_TYPE_HEADS_UP_DISAPPEAR
+                new AnimationFilter()
+                        .animateAlpha()
+                        .animateHeight()
+                        .animateTopInset()
+                        .animateY()
+                        .animateZ(),
+
                 // ANIMATION_TYPE_EVERYTHING
                 new AnimationFilter()
                         .animateAlpha()
@@ -2780,6 +2877,12 @@
                 // ANIMATION_TYPE_GROUP_EXPANSION_CHANGED
                 StackStateAnimator.ANIMATION_DURATION_EXPAND_CLICKED,
 
+                // ANIMATION_TYPE_HEADS_UP_APPEAR
+                StackStateAnimator.ANIMATION_DURATION_HEADS_UP_APPEAR,
+
+                // ANIMATION_TYPE_HEADS_UP_DISAPPEAR
+                StackStateAnimator.ANIMATION_DURATION_HEADS_UP_DISAPPEAR,
+
                 // ANIMATION_TYPE_EVERYTHING
                 StackStateAnimator.ANIMATION_DURATION_STANDARD,
         };
@@ -2798,7 +2901,9 @@
         static final int ANIMATION_TYPE_HIDE_SENSITIVE = 11;
         static final int ANIMATION_TYPE_VIEW_RESIZE = 12;
         static final int ANIMATION_TYPE_GROUP_EXPANSION_CHANGED = 13;
-        static final int ANIMATION_TYPE_EVERYTHING = 14;
+        static final int ANIMATION_TYPE_HEADS_UP_APPEAR = 14;
+        static final int ANIMATION_TYPE_HEADS_UP_DISAPPEAR = 15;
+        static final int ANIMATION_TYPE_EVERYTHING = 16;
 
         static final int DARK_ANIMATION_ORIGIN_INDEX_ABOVE = -1;
         static final int DARK_ANIMATION_ORIGIN_INDEX_BELOW = -2;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackScrollAlgorithm.java b/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackScrollAlgorithm.java
index e7bf47b..b0f287f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackScrollAlgorithm.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackScrollAlgorithm.java
@@ -25,9 +25,11 @@
 import com.android.systemui.R;
 import com.android.systemui.statusbar.ExpandableNotificationRow;
 import com.android.systemui.statusbar.ExpandableView;
+import com.android.systemui.statusbar.policy.HeadsUpManager;
 
 import java.util.ArrayList;
 import java.util.List;
+import java.util.TreeMap;
 
 /**
  * The Algorithm of the {@link com.android.systemui.statusbar.stack
@@ -54,11 +56,6 @@
     private StackIndentationFunctor mTopStackIndentationFunctor;
     private StackIndentationFunctor mBottomStackIndentationFunctor;
 
-    private int mLayoutHeight;
-
-    /** mLayoutHeight - mTopPadding */
-    private int mInnerHeight;
-    private int mTopPadding;
     private StackScrollAlgorithmState mTempAlgorithmState = new StackScrollAlgorithmState();
     private boolean mIsExpansionChanging;
     private int mFirstChildMaxHeight;
@@ -157,13 +154,13 @@
         scrollY = Math.max(0, scrollY);
         algorithmState.scrollY = (int) (scrollY + mCollapsedSize + bottomOverScroll);
 
-        updateVisibleChildren(resultState, algorithmState);
+        updateVisibleChildren(resultState, algorithmState, ambientState);
 
         // Phase 1:
-        findNumberOfItemsInTopStackAndUpdateState(resultState, algorithmState);
+        findNumberOfItemsInTopStackAndUpdateState(resultState, algorithmState, ambientState);
 
         // Phase 2:
-        updatePositionsForState(resultState, algorithmState);
+        updatePositionsForState(resultState, algorithmState, ambientState);
 
         // Phase 3:
         updateZValuesForState(resultState, algorithmState);
@@ -329,23 +326,30 @@
      * Update the visible children on the state.
      */
     private void updateVisibleChildren(StackScrollState resultState,
-            StackScrollAlgorithmState state) {
+            StackScrollAlgorithmState state, AmbientState ambientState) {
         ViewGroup hostView = resultState.getHostView();
         int childCount = hostView.getChildCount();
         state.visibleChildren.clear();
         state.visibleChildren.ensureCapacity(childCount);
         int notGoneIndex = 0;
+        TreeMap<String, HeadsUpManager.HeadsUpEntry> headsUpEntries =
+                ambientState.getHeadsUpEntries();
+        for (String key: headsUpEntries.keySet()) {
+            ExpandableView v = headsUpEntries.get(key).entry.row;
+            notGoneIndex = updateNotGoneIndex(resultState, state, notGoneIndex, v);
+        }
         for (int i = 0; i < childCount; i++) {
             ExpandableView v = (ExpandableView) hostView.getChildAt(i);
             if (v.getVisibility() != View.GONE) {
-                StackViewState viewState = resultState.getViewStateForView(v);
-                viewState.notGoneIndex = notGoneIndex;
-                state.visibleChildren.add(v);
-                notGoneIndex++;
 
-                // handle the notgoneIndex for the children as well
                 if (v instanceof ExpandableNotificationRow) {
                     ExpandableNotificationRow row = (ExpandableNotificationRow) v;
+                    if (row.isHeadsUp()) {
+                        continue;
+                    }
+                    notGoneIndex = updateNotGoneIndex(resultState, state, notGoneIndex, v);
+
+                    // handle the notgoneIndex for the children as well
                     List<ExpandableNotificationRow> children =
                             row.getNotificationChildren();
                     if (row.areChildrenExpanded() && children != null) {
@@ -358,22 +362,35 @@
                             }
                         }
                     }
+                } else {
+                    notGoneIndex = updateNotGoneIndex(resultState, state, notGoneIndex, v);
                 }
             }
         }
     }
 
+    private int updateNotGoneIndex(StackScrollState resultState,
+            StackScrollAlgorithmState state, int notGoneIndex,
+            ExpandableView v) {
+        StackViewState viewState = resultState.getViewStateForView(v);
+        viewState.notGoneIndex = notGoneIndex;
+        state.visibleChildren.add(v);
+        notGoneIndex++;
+        return notGoneIndex;
+    }
+
     /**
      * Determine the positions for the views. This is the main part of the algorithm.
      *
-     * @param resultState The result state to update if a change to the properties of a child occurs
+     *  @param resultState The result state to update if a change to the properties of a child occurs
      * @param algorithmState The state in which the current pass of the algorithm is currently in
+     * @param ambientState The current ambient state
      */
     private void updatePositionsForState(StackScrollState resultState,
-            StackScrollAlgorithmState algorithmState) {
+            StackScrollAlgorithmState algorithmState, AmbientState ambientState) {
 
         // The starting position of the bottom stack peek
-        float bottomPeekStart = mInnerHeight - mBottomStackPeekSize;
+        float bottomPeekStart = ambientState.getInnerHeight() - mBottomStackPeekSize;
 
         // The position where the bottom stack starts.
         float bottomStackStart = bottomPeekStart - mBottomStackSlowDownLength;
@@ -427,7 +444,8 @@
                             bottomPeekStart, childViewState.yTranslation, childViewState,
                             childHeight);
                 }
-                clampPositionToBottomStackStart(childViewState, childViewState.height);
+                clampPositionToBottomStackStart(childViewState, childViewState.height,
+                        ambientState);
             } else if (nextYPosition >= bottomStackStart) {
                 // Case 2:
                 // We are in the bottom stack.
@@ -435,7 +453,7 @@
                     // According to the regular scroll view we are fully translated out of the
                     // bottom of the screen so we are fully in the bottom stack
                     updateStateForChildFullyInBottomStack(algorithmState,
-                            bottomStackStart, childViewState, childHeight);
+                            bottomStackStart, childViewState, childHeight, ambientState);
                 } else {
                     // According to the regular scroll view we are currently translating out of /
                     // into the bottom of the screen
@@ -447,7 +465,7 @@
                 // Case 3:
                 // We are in the regular scroll area.
                 childViewState.location = StackViewState.LOCATION_MAIN_AREA;
-                clampYTranslation(childViewState, childHeight);
+                clampYTranslation(childViewState, childHeight, ambientState);
             }
 
             // The first card is always rendered.
@@ -468,7 +486,44 @@
             currentYPosition = childViewState.yTranslation + childHeight + mPaddingBetweenElements;
             yPositionInScrollView = yPositionInScrollViewAfterElement;
 
-            childViewState.yTranslation += mTopPadding;
+            childViewState.yTranslation += ambientState.getTopPadding()
+                    + ambientState.getPaddingOffset();
+
+            updateHeadsUpStates(resultState, algorithmState, ambientState);
+        }
+    }
+
+    private void updateHeadsUpStates(StackScrollState resultState,
+            StackScrollAlgorithmState algorithmState, AmbientState ambientState) {
+        TreeMap<String, HeadsUpManager.HeadsUpEntry> headsUpEntries =
+                ambientState.getHeadsUpEntries();
+        boolean hasPinnedHeadsUp = false;
+        for (String key: headsUpEntries.keySet()) {
+            ExpandableNotificationRow row = headsUpEntries.get(key).entry.row;
+            StackViewState childState = resultState.getViewStateForView(row);
+            if (!row.isInShade()) {
+                childState.yTranslation = Math.max(childState.yTranslation, 0);
+                hasPinnedHeadsUp = true;
+            }
+            childState.height = Math.max(childState.height, row.getHeadsUpHeight());
+            childState.yTranslation = Math.min(childState.yTranslation,
+                    ambientState.getMaxHeadsUpTranslation() - childState.height);
+        }
+        if (hasPinnedHeadsUp && !ambientState.isShadeExpanded()) {
+            // Let's hide all normal views
+            int childCount = algorithmState.visibleChildren.size();
+            for (int i = 0; i < childCount; i++) {
+                ExpandableView child = algorithmState.visibleChildren.get(i);
+                StackViewState state = resultState.getViewStateForView(child);
+                boolean hideView = true;
+                if (child instanceof ExpandableNotificationRow) {
+                    ExpandableNotificationRow row = (ExpandableNotificationRow) child;
+                    hideView = !row.isHeadsUp();
+                }
+                if (hideView) {
+                    state.alpha = 0.0f;
+                }
+            }
         }
     }
 
@@ -478,8 +533,9 @@
      * @param childViewState the view state of the child
      * @param childHeight the height of this child
      */
-    private void clampYTranslation(StackViewState childViewState, int childHeight) {
-        clampPositionToBottomStackStart(childViewState, childHeight);
+    private void clampYTranslation(StackViewState childViewState, int childHeight,
+            AmbientState ambientState) {
+        clampPositionToBottomStackStart(childViewState, childHeight, ambientState);
         clampPositionToTopStackEnd(childViewState, childHeight);
     }
 
@@ -491,9 +547,10 @@
      * @param childHeight the height of this child
      */
     private void clampPositionToBottomStackStart(StackViewState childViewState,
-            int childHeight) {
+            int childHeight, AmbientState ambientState) {
         childViewState.yTranslation = Math.min(childViewState.yTranslation,
-                mInnerHeight - mBottomStackPeekSize - mCollapseSecondCardPadding - childHeight);
+                ambientState.getInnerHeight() - mBottomStackPeekSize - mCollapseSecondCardPadding
+                        - childHeight);
     }
 
     /**
@@ -548,8 +605,7 @@
 
     private void updateStateForChildFullyInBottomStack(StackScrollAlgorithmState algorithmState,
             float transitioningPositionStart, StackViewState childViewState,
-            int childHeight) {
-
+            int childHeight, AmbientState ambientState) {
         float currentYPosition;
         algorithmState.itemsInBottomStack += 1.0f;
         if (algorithmState.itemsInBottomStack < MAX_ITEMS_IN_BOTTOM_STACK) {
@@ -567,7 +623,7 @@
                 childViewState.alpha = 1.0f - algorithmState.partialInBottom;
             }
             childViewState.location = StackViewState.LOCATION_BOTTOM_STACK_HIDDEN;
-            currentYPosition = mInnerHeight;
+            currentYPosition = ambientState.getInnerHeight();
         }
         childViewState.yTranslation = currentYPosition - childHeight;
         clampPositionToTopStackEnd(childViewState, childHeight);
@@ -629,7 +685,7 @@
      * @param algorithmState The state in which the current pass of the algorithm is currently in
      */
     private void findNumberOfItemsInTopStackAndUpdateState(StackScrollState resultState,
-            StackScrollAlgorithmState algorithmState) {
+            StackScrollAlgorithmState algorithmState, AmbientState ambientState) {
 
         // The y Position if the element would be in a regular scrollView
         float yPositionInScrollView = 0.0f;
@@ -647,7 +703,7 @@
                 if (i == 0 && algorithmState.scrollY <= mCollapsedSize) {
 
                     // The starting position of the bottom stack peek
-                    int bottomPeekStart = mInnerHeight - mBottomStackPeekSize -
+                    int bottomPeekStart = ambientState.getInnerHeight() - mBottomStackPeekSize -
                             mCollapseSecondCardPadding;
                     // Collapse and expand the first child while the shade is being expanded
                     float maxHeight = mIsExpansionChanging && child == mFirstChildWhileExpanding
@@ -744,21 +800,6 @@
         }
     }
 
-    public void setLayoutHeight(int layoutHeight) {
-        this.mLayoutHeight = layoutHeight;
-        updateInnerHeight();
-    }
-
-    public void setTopPadding(int topPadding) {
-        mTopPadding = topPadding;
-        updateInnerHeight();
-    }
-
-    private void updateInnerHeight() {
-        mInnerHeight = mLayoutHeight - mTopPadding;
-    }
-
-
     /**
      * Update whether the device is very small, i.e. Notifications can be in both the top and the
      * bottom stack at the same time
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackStateAnimator.java b/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackStateAnimator.java
index b249fbf..9640b84 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackStateAnimator.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackStateAnimator.java
@@ -45,6 +45,8 @@
     public static final int ANIMATION_DURATION_APPEAR_DISAPPEAR = 464;
     public static final int ANIMATION_DURATION_EXPAND_CLICKED = 360;
     public static final int ANIMATION_DURATION_DIMMED_ACTIVATED = 220;
+    public static final int ANIMATION_DURATION_HEADS_UP_APPEAR = 280;
+    public static final int ANIMATION_DURATION_HEADS_UP_DISAPPEAR = 220;
     public static final int ANIMATION_DELAY_PER_ELEMENT_INTERRUPTING = 80;
     public static final int ANIMATION_DELAY_PER_ELEMENT_EXPAND_CHILDREN = 54;
     public static final int ANIMATION_DELAY_PER_ELEMENT_MANUAL = 32;
@@ -90,6 +92,7 @@
     private ValueAnimator mTopOverScrollAnimator;
     private ValueAnimator mBottomOverScrollAnimator;
     private ExpandableNotificationRow mChildExpandingView;
+    private StackViewState mTmpState = new StackViewState();
 
     public StackStateAnimator(NotificationStackScrollLayout hostLayout) {
         mHostLayout = hostLayout;
@@ -828,6 +831,13 @@
                 ExpandableNotificationRow row = (ExpandableNotificationRow) event.changingView;
                 row.prepareExpansionChanged(finalState);
                 mChildExpandingView = row;
+            } else if (event.animationType == NotificationStackScrollLayout
+                    .AnimationEvent.ANIMATION_TYPE_HEADS_UP_APPEAR) {
+                // This item is added, initialize it's properties.
+                StackViewState viewState = finalState.getViewStateForView(changingView);
+                mTmpState.copyFrom(viewState);
+                mTmpState.yTranslation = -mTmpState.height;
+                finalState.applyState(changingView, mTmpState);
             }
             mNewEvents.add(event);
         }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/tv/TvStatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/tv/TvStatusBar.java
index c272e48..78122d6 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/tv/TvStatusBar.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/tv/TvStatusBar.java
@@ -123,19 +123,7 @@
     }
 
     @Override
-    public void scheduleHeadsUpDecay(long delay) {
-    }
-
-    @Override
-    public void scheduleHeadsUpOpen() {
-    }
-
-    @Override
-    public void scheduleHeadsUpEscalation() {
-    }
-
-    @Override
-    public void scheduleHeadsUpClose() {
+    public void escalateHeadsUp() {
     }
 
     @Override
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/HeadsUpNotificationTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/HeadsUpNotificationTest.java
deleted file mode 100644
index e8a80d9..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/HeadsUpNotificationTest.java
+++ /dev/null
@@ -1,261 +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.policy;
-
-import static org.mockito.Matchers.anyInt;
-import static org.mockito.Mockito.inOrder;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.reset;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.when;
-
-import android.app.Notification;
-import android.os.*;
-import android.service.notification.StatusBarNotification;
-import com.android.systemui.SwipeHelper;
-import com.android.systemui.SysuiTestCase;
-import com.android.systemui.statusbar.ExpandableNotificationRow;
-import com.android.systemui.statusbar.NotificationData;
-import com.android.systemui.statusbar.phone.PhoneStatusBar;
-
-import org.mockito.ArgumentCaptor;
-import org.mockito.InOrder;
-import org.mockito.Mock;
-import org.mockito.Mockito;
-import org.mockito.MockitoAnnotations;
-
-/**
- * Test the Heads Up Notification.
- *
- * Specifically the policy that a notificaiton must remain visibile for a minimum period of time.
- */
-public class HeadsUpNotificationTest extends SysuiTestCase {
-    private static final String TAG = "HeadsUpNotificationTest";
-
-    private static int TOUCH_SENSITIVITY = 100;
-    private static int NOTIFICATION_DECAY = 10000;
-    private static int MINIMUM_DISPLAY_TIME = 3000;
-    private static int SNOOZE_TIME = 60000;
-    private static long TOO_SOON = 1000L;  // less than MINIMUM_DISPLAY_TIME
-    private static long LATER = 5000L;  // more than MINIMUM_DISPLAY_TIME
-    private static long REMAINING_VISIBILITY = MINIMUM_DISPLAY_TIME - TOO_SOON;
-
-    protected HeadsUpNotificationView mHeadsUp;
-
-    @Mock protected PhoneStatusBar mMockStatusBar;
-    @Mock private HeadsUpNotificationView.Clock mClock;
-    @Mock private SwipeHelper mMockSwipeHelper;
-    @Mock private HeadsUpNotificationView.EdgeSwipeHelper mMockEdgeSwipeHelper;
-
-    @Override
-    protected void setUp() throws Exception {
-        super.setUp();
-
-        MockitoAnnotations.initMocks(this);
-
-        mHeadsUp = new HeadsUpNotificationView(mContext,
-                mClock, mMockSwipeHelper, mMockEdgeSwipeHelper,
-                NOTIFICATION_DECAY, MINIMUM_DISPLAY_TIME, TOUCH_SENSITIVITY, SNOOZE_TIME);
-        mHeadsUp.setBar(mMockStatusBar);
-    }
-
-    private NotificationData.Entry makeNotification(String key) {
-        StatusBarNotification sbn = mock(StatusBarNotification.class);
-        when(sbn.getKey()).thenReturn(key);
-        return new NotificationData.Entry(sbn, null);
-    }
-
-    public void testPostAndDecay() {
-        NotificationData.Entry a = makeNotification("a");
-        mHeadsUp.showNotification(a);
-        Mockito.verify(mMockStatusBar, never()).scheduleHeadsUpClose();
-        Mockito.verify(mMockStatusBar, times(1)).scheduleHeadsUpOpen();
-        ArgumentCaptor<Long> decayArg = ArgumentCaptor.forClass(Long.class);
-        Mockito.verify(mMockStatusBar).scheduleHeadsUpDecay(decayArg.capture());
-        // New notification gets a full decay time.
-        assertEquals(NOTIFICATION_DECAY, (long) decayArg.getValue());
-    }
-
-    public void testPostAndDeleteTooSoon() {
-        when(mClock.currentTimeMillis()).thenReturn(0L);
-        NotificationData.Entry a = makeNotification("a");
-        mHeadsUp.showNotification(a);
-        reset(mMockStatusBar);
-
-        when(mClock.currentTimeMillis()).thenReturn(TOO_SOON);
-        mHeadsUp.removeNotification(a.key);
-        ArgumentCaptor<Long> decayArg = ArgumentCaptor.forClass(Long.class);
-        Mockito.verify(mMockStatusBar, never()).scheduleHeadsUpClose();
-        Mockito.verify(mMockStatusBar).scheduleHeadsUpDecay(decayArg.capture());
-        // Leave the window up for the balance of the minumum time.
-        assertEquals(REMAINING_VISIBILITY, (long) decayArg.getValue());
-    }
-
-    public void testPostAndDeleteLater() {
-        when(mClock.currentTimeMillis()).thenReturn(0L);
-        NotificationData.Entry a = makeNotification("a");
-        mHeadsUp.showNotification(a);
-        reset(mMockStatusBar);
-
-        when(mClock.currentTimeMillis()).thenReturn(LATER);
-        mHeadsUp.removeNotification(a.key);
-        // Delete closes immediately if the minimum time window is satisfied.
-        Mockito.verify(mMockStatusBar, times(1)).scheduleHeadsUpClose();
-        Mockito.verify(mMockStatusBar, never()).scheduleHeadsUpDecay(anyInt());
-    }
-
-    // This is a bad test.  It should not care that there is a call to scheduleHeadsUpClose(),
-    // but it happens that there will be one, so it is important that it happen before the
-    // call to scheduleHeadsUpOpen(), so that the final state is open.
-    // Maybe mMockStatusBar should instead be a fake that tracks the open/closed state.
-    public void testPostAndReplaceTooSoon() {
-        InOrder callOrder = inOrder(mMockStatusBar);
-        when(mClock.currentTimeMillis()).thenReturn(0L);
-        NotificationData.Entry a = makeNotification("a");
-        mHeadsUp.showNotification(a);
-        reset(mMockStatusBar);
-
-        when(mClock.currentTimeMillis()).thenReturn(TOO_SOON);
-        NotificationData.Entry b = makeNotification("b");
-        mHeadsUp.showNotification(b);
-        Mockito.verify(mMockStatusBar, times(1)).scheduleHeadsUpClose();
-        ArgumentCaptor<Long> decayArg = ArgumentCaptor.forClass(Long.class);
-        Mockito.verify(mMockStatusBar, times(1)).scheduleHeadsUpDecay(decayArg.capture());
-        // New notification gets a full decay time.
-        assertEquals(NOTIFICATION_DECAY, (long) decayArg.getValue());
-
-        // Make sure close was called before open, so that the heads up stays open.
-        callOrder.verify(mMockStatusBar).scheduleHeadsUpClose();
-        callOrder.verify(mMockStatusBar).scheduleHeadsUpOpen();
-    }
-
-    public void testPostAndUpdateAlertAgain() {
-        when(mClock.currentTimeMillis()).thenReturn(0L);
-        NotificationData.Entry a = makeNotification("a");
-        mHeadsUp.showNotification(a);
-        reset(mMockStatusBar);
-
-        when(mClock.currentTimeMillis()).thenReturn(TOO_SOON);
-        mHeadsUp.updateNotification(a, true);
-        Mockito.verify(mMockStatusBar, never()).scheduleHeadsUpClose();
-        ArgumentCaptor<Long> decayArg = ArgumentCaptor.forClass(Long.class);
-        Mockito.verify(mMockStatusBar, times(1)).scheduleHeadsUpDecay(decayArg.capture());
-        // Alert again gets a full decay time.
-        assertEquals(NOTIFICATION_DECAY, (long) decayArg.getValue());
-    }
-
-    public void testPostAndUpdateAlertAgainFastFail() {
-        when(mClock.currentTimeMillis()).thenReturn(0L);
-        NotificationData.Entry a = makeNotification("a");
-        mHeadsUp.showNotification(a);
-        reset(mMockStatusBar);
-
-        when(mClock.currentTimeMillis()).thenReturn(TOO_SOON);
-        NotificationData.Entry a_prime = makeNotification("a");
-        mHeadsUp.updateNotification(a_prime, true);
-        Mockito.verify(mMockStatusBar, never()).scheduleHeadsUpClose();
-        ArgumentCaptor<Long> decayArg = ArgumentCaptor.forClass(Long.class);
-        Mockito.verify(mMockStatusBar, times(1)).scheduleHeadsUpDecay(decayArg.capture());
-        // Alert again gets a full decay time.
-        assertEquals(NOTIFICATION_DECAY, (long) decayArg.getValue());
-    }
-
-    public void testPostAndUpdateNoAlertAgain() {
-        when(mClock.currentTimeMillis()).thenReturn(0L);
-        NotificationData.Entry a = makeNotification("a");
-        mHeadsUp.showNotification(a);
-        reset(mMockStatusBar);
-
-        when(mClock.currentTimeMillis()).thenReturn(TOO_SOON);
-        mHeadsUp.updateNotification(a, false);
-        Mockito.verify(mMockStatusBar, never()).scheduleHeadsUpClose();
-        Mockito.verify(mMockStatusBar, never()).scheduleHeadsUpDecay(anyInt());
-    }
-
-    public void testPostAndUpdateNoAlertAgainFastFail() {
-        when(mClock.currentTimeMillis()).thenReturn(0L);
-        NotificationData.Entry a = makeNotification("a");
-        mHeadsUp.showNotification(a);
-        reset(mMockStatusBar);
-
-        when(mClock.currentTimeMillis()).thenReturn(TOO_SOON);
-        NotificationData.Entry a_prime = makeNotification("a");
-        mHeadsUp.updateNotification(a_prime, false);
-        Mockito.verify(mMockStatusBar, never()).scheduleHeadsUpClose();
-        Mockito.verify(mMockStatusBar, never()).scheduleHeadsUpDecay(anyInt());
-    }
-
-    public void testPostAndUpdateLowPriorityTooSoon() {
-        when(mClock.currentTimeMillis()).thenReturn(0L);
-        NotificationData.Entry a = makeNotification("a");
-        mHeadsUp.showNotification(a);
-        reset(mMockStatusBar);
-
-        when(mClock.currentTimeMillis()).thenReturn(TOO_SOON);
-        mHeadsUp.release();
-        Mockito.verify(mMockStatusBar, never()).scheduleHeadsUpClose();
-        ArgumentCaptor<Long> decayArg = ArgumentCaptor.forClass(Long.class);
-        Mockito.verify(mMockStatusBar, times(1)).scheduleHeadsUpDecay(decayArg.capture());
-        // Down grade on update leaves the window up for the balance of the minumum time.
-        assertEquals(REMAINING_VISIBILITY, (long) decayArg.getValue());
-    }
-
-    public void testPostAndUpdateLowPriorityTooSoonFastFail() {
-        when(mClock.currentTimeMillis()).thenReturn(0L);
-        NotificationData.Entry a = makeNotification("a");
-        mHeadsUp.showNotification(a);
-        reset(mMockStatusBar);
-
-        when(mClock.currentTimeMillis()).thenReturn(TOO_SOON);
-        NotificationData.Entry a_prime = makeNotification("a");
-        mHeadsUp.updateNotification(a_prime, false);
-        mHeadsUp.release();
-        Mockito.verify(mMockStatusBar, never()).scheduleHeadsUpClose();
-        ArgumentCaptor<Long> decayArg = ArgumentCaptor.forClass(Long.class);
-        Mockito.verify(mMockStatusBar, times(1)).scheduleHeadsUpDecay(decayArg.capture());
-        // Down grade on update leaves the window up for the balance of the minumum time.
-        assertEquals(REMAINING_VISIBILITY, (long) decayArg.getValue());
-    }
-
-    public void testPostAndUpdateLowPriorityLater() {
-        when(mClock.currentTimeMillis()).thenReturn(0L);
-        NotificationData.Entry a = makeNotification("a");
-        mHeadsUp.showNotification(a);
-        reset(mMockStatusBar);
-
-        when(mClock.currentTimeMillis()).thenReturn(LATER);
-        mHeadsUp.release();
-        // Down grade on update closes immediately if the minimum time window is satisfied.
-        Mockito.verify(mMockStatusBar, times(1)).scheduleHeadsUpClose();
-        Mockito.verify(mMockStatusBar, never()).scheduleHeadsUpDecay(anyInt());
-    }
-
-    public void testPostAndUpdateLowPriorityLaterFastFail() {
-        when(mClock.currentTimeMillis()).thenReturn(0L);
-        NotificationData.Entry a = makeNotification("a");
-        mHeadsUp.showNotification(a);
-        reset(mMockStatusBar);
-
-        when(mClock.currentTimeMillis()).thenReturn(LATER);
-        NotificationData.Entry a_prime = makeNotification("a");
-        mHeadsUp.updateNotification(a_prime, false);
-        mHeadsUp.release();
-        // Down grade on update closes immediately if the minimum time window is satisfied.
-        Mockito.verify(mMockStatusBar, times(1)).scheduleHeadsUpClose();
-        Mockito.verify(mMockStatusBar, never()).scheduleHeadsUpDecay(anyInt());
-    }
-}