allow rich notifications in the heads up.

new shouldInterrupt logic: screen on, not locked, not dreaming, and
  priority above HIGH and noisy, or has fullscreen intent
draft of API allowing devs to give hints about head up display

reuse inflateViews()
add an expand helper to the heads up space
move some things into Entry for reuse

don't allow touches in first second
delay decay if touched
make decay time a resource

add a custom viewgroup for notification rows to get view management
out of the NotificationData class.

Change-Id: I36464f110cfa0dabc3f35db7db6c35c27e8ee2ba
diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java
index bffbb51..781b94e 100644
--- a/core/java/android/app/Notification.java
+++ b/core/java/android/app/Notification.java
@@ -457,6 +457,30 @@
     public static final String EXTRA_PEOPLE = "android.people";
 
     /**
+     * Notification extra to specify heads up display preference.
+     * @hide
+     */
+    public static final String EXTRA_AS_HEADS_UP = "headsup";
+
+    /**
+     * Value for {@link #EXTRA_AS_HEADS_UP} indicating that heads up display is not appropriate.
+     * @hide
+     */
+    public static final int HEADS_UP_NEVER = 0;
+
+    /**
+     * Default value for {@link #EXTRA_AS_HEADS_UP} indicating that heads up display is appropriate.
+     * @hide
+     */
+    public static final int HEADS_UP_ALLOWED = 1;
+
+    /**
+     * Value for {@link #EXTRA_AS_HEADS_UP} that advocates for heads up display.
+     * @hide
+     */
+    public static final int HEADS_UP_REQUESTED = 2;
+
+    /**
      * Structure to encapsulate an "action", including title and icon, that can be attached to a Notification.
      */
     public static class Action implements Parcelable {
diff --git a/packages/SystemUI/res/anim/heads_up_enter.xml b/packages/SystemUI/res/anim/heads_up_enter.xml
index 4fd6a7c..59eef42 100644
--- a/packages/SystemUI/res/anim/heads_up_enter.xml
+++ b/packages/SystemUI/res/anim/heads_up_enter.xml
@@ -1,11 +1,9 @@
 <?xml version="1.0" encoding="utf-8"?>
 <set xmlns:android="http://schemas.android.com/apk/res/android"
         >
-    <scale
+    <translate
         android:interpolator="@android:interpolator/overshoot"
-        android:fromXScale="0.7" android:toXScale="1.0"
-        android:fromYScale="0.7" android:toYScale="1.0"
-        android:pivotX="50%" android:pivotY="50%"
+        android:fromYDelta="-50%" android:toYDelta="0"
         android:duration="@android:integer/config_shortAnimTime" />
     <alpha 
         android:interpolator="@android:interpolator/decelerate_quad"
diff --git a/packages/SystemUI/res/drawable/heads_up_window_bg.9.png b/packages/SystemUI/res/drawable/heads_up_window_bg.9.png
index caad169..c68ccdc 100644
--- a/packages/SystemUI/res/drawable/heads_up_window_bg.9.png
+++ b/packages/SystemUI/res/drawable/heads_up_window_bg.9.png
Binary files differ
diff --git a/packages/SystemUI/res/layout/heads_up.xml b/packages/SystemUI/res/layout/heads_up.xml
index b7c1666..95eec0f 100644
--- a/packages/SystemUI/res/layout/heads_up.xml
+++ b/packages/SystemUI/res/layout/heads_up.xml
@@ -24,6 +24,7 @@
     android:layout_height="wrap_content"
     android:layout_width="match_parent"
     android:orientation="vertical"
+    android:paddingTop="@*android:dimen/status_bar_height"
     >
     <FrameLayout
         android:layout_height="wrap_content"
diff --git a/packages/SystemUI/res/layout/status_bar_notification_row.xml b/packages/SystemUI/res/layout/status_bar_notification_row.xml
index 7a5ff3c..f827967 100644
--- a/packages/SystemUI/res/layout/status_bar_notification_row.xml
+++ b/packages/SystemUI/res/layout/status_bar_notification_row.xml
@@ -1,4 +1,5 @@
-<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+<com.android.systemui.statusbar.ExpandableNotificationRow
+    xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
     >
@@ -62,4 +63,4 @@
         android:padding="2dp"
         />
 
-</FrameLayout>
+</com.android.systemui.statusbar.ExpandableNotificationRow>
diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml
index b70dee1..8ce959f 100644
--- a/packages/SystemUI/res/values/config.xml
+++ b/packages/SystemUI/res/values/config.xml
@@ -101,5 +101,11 @@
 
     <!-- Should "4G" be shown instead of "LTE" when the network is NETWORK_TYPE_LTE? -->
     <bool name="config_show4GForLTE">true</bool>
+
+    <!-- milliseconds before the heads up notification auto-dismisses. -->
+    <integer name="heads_up_notification_decay">3700</integer>
+
+    <!-- milliseconds before the heads up notification accepts touches. -->
+    <integer name="heads_up_sensitivity_delay">700</integer>
 </resources>
 
diff --git a/packages/SystemUI/res/values/ids.xml b/packages/SystemUI/res/values/ids.xml
index 2cc3446..d876a95 100644
--- a/packages/SystemUI/res/values/ids.xml
+++ b/packages/SystemUI/res/values/ids.xml
@@ -16,8 +16,5 @@
 -->
 
 <resources>
-    <item type="id" name="expandable_tag" />
-    <item type="id" name="user_expanded_tag" />
-    <item type="id" name="user_lock_tag" />
     <item type="id" name="status_bar_cling_stub" />
 </resources>
diff --git a/packages/SystemUI/src/com/android/systemui/ExpandHelper.java b/packages/SystemUI/src/com/android/systemui/ExpandHelper.java
index 4fd8aee..e1a4bb2 100644
--- a/packages/SystemUI/src/com/android/systemui/ExpandHelper.java
+++ b/packages/SystemUI/src/com/android/systemui/ExpandHelper.java
@@ -38,8 +38,8 @@
         View getChildAtRawPosition(float x, float y);
         View getChildAtPosition(float x, float y);
         boolean canChildBeExpanded(View v);
-        boolean setUserExpandedChild(View v, boolean userExpanded);
-        boolean setUserLockedChild(View v, boolean userLocked);
+        void setUserExpandedChild(View v, boolean userExpanded);
+        void setUserLockedChild(View v, boolean userLocked);
     }
 
     private static final String TAG = "ExpandHelper";
@@ -181,7 +181,6 @@
      * @param callback the container that holds the items to be manipulated
      * @param small the smallest allowable size for the manuipulated items.
      * @param large the largest allowable size for the manuipulated items.
-     * @param scoller if non-null also manipulate the scroll position to obey the gravity.
      */
     public ExpandHelper(Context context, Callback callback, int small, int large) {
         mSmallSize = small;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java
index b1e38d8..7025240 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java
@@ -19,6 +19,7 @@
 import android.app.ActivityManager;
 import android.app.ActivityManagerNative;
 import android.app.KeyguardManager;
+import android.app.Notification;
 import android.app.PendingIntent;
 import android.app.TaskStackBuilder;
 import android.content.BroadcastReceiver;
@@ -91,7 +92,7 @@
 
     protected static final boolean ENABLE_HEADS_UP = true;
     // scores above this threshold should be displayed in heads up mode.
-    private static final int INTERRUPTION_THRESHOLD = 10;
+    private static final int INTERRUPTION_THRESHOLD = 11;
 
     // Should match the value in PhoneWindowManager
     public static final String SYSTEM_DIALOG_REASON_RECENT_APPS = "recentapps";
@@ -108,7 +109,8 @@
     protected NotificationData mNotificationData = new NotificationData();
     protected NotificationRowLayout mPile;
 
-    protected StatusBarNotification mCurrentlyInterruptingNotification;
+    protected NotificationData.Entry mInterruptingNotificationEntry;
+    protected long mInterruptingNotificationTime;
 
     // used to notify status bar for suppressing notification LED
     protected boolean mPanelSlightlyVisible;
@@ -127,6 +129,7 @@
     protected IDreamManager mDreamManager;
     KeyguardManager mKeyguardManager;
     PowerManager mPowerManager;
+    protected int mRowHeight;
 
     // UI-specific methods
 
@@ -432,7 +435,7 @@
         }
     }
 
-    public void dismissHeadsUp() {
+    public void onHeadsUpDismissed() {
         // pass
     }
 
@@ -558,6 +561,8 @@
         }
     }
 
+    public abstract void resetHeadsUpDecayTimer();
+
     protected class H extends Handler {
         public void handleMessage(Message m) {
             Intent intent;
@@ -615,7 +620,7 @@
     protected void workAroundBadLayerDrawableOpacity(View v) {
     }
 
-    protected  boolean inflateViews(NotificationData.Entry entry, ViewGroup parent) {
+    public boolean inflateViews(NotificationData.Entry entry, ViewGroup parent) {
         int minHeight =
                 mContext.getResources().getDimensionPixelSize(R.dimen.notification_min_height);
         int maxHeight =
@@ -630,7 +635,8 @@
         // create the row view
         LayoutInflater inflater = (LayoutInflater)mContext.getSystemService(
                 Context.LAYOUT_INFLATER_SERVICE);
-        View row = inflater.inflate(R.layout.status_bar_notification_row, parent, false);
+        ExpandableNotificationRow row = (ExpandableNotificationRow) inflater.inflate(
+                R.layout.status_bar_notification_row, parent, false);
 
         // for blaming (see SwipeHelper.setLongPressListener)
         row.setTag(sbn.getPackageName());
@@ -697,6 +703,7 @@
             }
         }
         entry.row = row;
+        entry.row.setRowHeight(mRowHeight);
         entry.content = content;
         entry.expanded = contentViewLocal;
         entry.setBigContentView(bigContentViewLocal);
@@ -851,33 +858,18 @@
         return iconView;
     }
 
-    protected boolean expandView(NotificationData.Entry entry, boolean expand) {
-        int rowHeight =
-                mContext.getResources().getDimensionPixelSize(R.dimen.notification_row_min_height);
-        ViewGroup.LayoutParams lp = entry.row.getLayoutParams();
-        if (entry.expandable() && expand) {
-            if (DEBUG) Log.d(TAG, "setting expanded row height to WRAP_CONTENT");
-            lp.height = ViewGroup.LayoutParams.WRAP_CONTENT;
-        } else {
-            if (DEBUG) Log.d(TAG, "setting collapsed row height to " + rowHeight);
-            lp.height = rowHeight;
-        }
-        entry.row.setLayoutParams(lp);
-        return expand;
-    }
-
     protected void updateExpansionStates() {
         int N = mNotificationData.size();
         for (int i = 0; i < N; i++) {
             NotificationData.Entry entry = mNotificationData.get(i);
-            if (!entry.userLocked()) {
+            if (!entry.row.isUserLocked()) {
                 if (i == (N-1)) {
                     if (DEBUG) Log.d(TAG, "expanding top notification at " + i);
-                    expandView(entry, true);
+                    entry.row.setExpanded(true);
                 } else {
-                    if (!entry.userExpanded()) {
+                    if (!entry.row.isUserExpanded()) {
                         if (DEBUG) Log.d(TAG, "collapsing notification at " + i);
-                        expandView(entry, false);
+                        entry.row.setExpanded(false);
                     } else {
                         if (DEBUG) Log.d(TAG, "ignoring user-modified notification at " + i);
                     }
@@ -997,13 +989,13 @@
             if (DEBUG) Log.d(TAG, "contents was " + (contentsUnchanged ? "unchanged" : "changed"));
             if (DEBUG) Log.d(TAG, "order was " + (orderUnchanged ? "unchanged" : "changed"));
             if (DEBUG) Log.d(TAG, "notification is " + (isTopAnyway ? "top" : "not top"));
-            final boolean wasExpanded = oldEntry.userExpanded();
+            final boolean wasExpanded = oldEntry.row.isUserExpanded();
             removeNotificationViews(key);
             addNotificationViews(key, notification);
             if (wasExpanded) {
                 final NotificationData.Entry newEntry = mNotificationData.findByKey(key);
-                expandView(newEntry, true);
-                newEntry.setUserExpanded(true);
+                newEntry.row.setExpanded(true);
+                newEntry.row.setUserExpanded(true);
             }
         }
 
@@ -1026,7 +1018,8 @@
         updateExpandedViewPos(EXPANDED_LEAVE_ALONE);
 
         // See if we need to update the heads up.
-        if (ENABLE_HEADS_UP && oldNotification == mCurrentlyInterruptingNotification) {
+        if (ENABLE_HEADS_UP && mInterruptingNotificationEntry != null
+                && oldNotification == mInterruptingNotificationEntry.notification) {
             if (DEBUG) Log.d(TAG, "updating the current heads up:" + notification);
             // XXX: this is a hack for Alarms. The real implementation will need to *update*
             // the heads up.
@@ -1037,15 +1030,28 @@
         }
     }
 
-    protected boolean shouldInterrupt(StatusBarNotification notification) {
-        boolean interrupt = notification.getNotification().fullScreenIntent == null
-                && notification.getScore() >= INTERRUPTION_THRESHOLD
-                && mPowerManager.isScreenOn() && !mKeyguardManager.isKeyguardLocked();
+    protected boolean shouldInterrupt(StatusBarNotification sbn) {
+        Notification notification = sbn.getNotification();
+        // some predicates to make the boolean logic legible
+        boolean isNoisy = (notification.defaults & Notification.DEFAULT_SOUND) != 0
+                || (notification.defaults & Notification.DEFAULT_VIBRATE) != 0
+                || notification.sound != null
+                || notification.vibrate != null;
+        boolean isHighPriority = sbn.getScore() >= INTERRUPTION_THRESHOLD;
+        boolean isFullscreen = notification.fullScreenIntent != null;
+        boolean isAllowed = notification.extras.getInt(Notification.EXTRA_AS_HEADS_UP,
+                Notification.HEADS_UP_ALLOWED) != Notification.HEADS_UP_NEVER;
+
+        boolean interrupt = (isFullscreen || (isHighPriority && isNoisy))
+                && isAllowed
+                && mPowerManager.isScreenOn()
+                && !mKeyguardManager.isKeyguardLocked();
         try {
             interrupt = interrupt && !mDreamManager.isDreaming();
         } catch (RemoteException e) {
             Log.d(TAG, "failed to query dream manager", e);
         }
+        if (DEBUG) Log.d(TAG, "interrupt: " + interrupt);
         return interrupt;
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableNotificationRow.java
new file mode 100644
index 0000000..cd6495f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableNotificationRow.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+
+public class ExpandableNotificationRow extends FrameLayout {
+    private int mRowHeight;
+
+    /** does this row contain layouts that can adapt to row expansion */
+    private boolean mExpandable;
+    /** has the user manually expanded this row */
+    private boolean mUserExpanded;
+    /** is the user touching this row */
+    private boolean mUserLocked;
+
+    public ExpandableNotificationRow(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public int getRowHeight() {
+        return mRowHeight;
+    }
+
+    public void setRowHeight(int rowHeight) {
+        this.mRowHeight = rowHeight;
+    }
+
+    public boolean isExpandable() {
+        return mExpandable;
+    }
+
+    public void setExpandable(boolean expandable) {
+        mExpandable = expandable;
+    }
+
+    public boolean isUserExpanded() {
+        return mUserExpanded;
+    }
+
+    public void setUserExpanded(boolean userExpanded) {
+        mUserExpanded = userExpanded;
+    }
+
+    public boolean isUserLocked() {
+        return mUserLocked;
+    }
+
+    public void setUserLocked(boolean userLocked) {
+        mUserLocked = userLocked;
+    }
+
+    public void setExpanded(boolean expand) {
+        ViewGroup.LayoutParams lp = getLayoutParams();
+        if (expand && mExpandable) {
+            lp.height = ViewGroup.LayoutParams.WRAP_CONTENT;
+        } else {
+            lp.height = mRowHeight;
+        }
+        setLayoutParams(lp);
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationData.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationData.java
index 8f62ebf..23950fc 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationData.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationData.java
@@ -34,7 +34,7 @@
         public IBinder key;
         public StatusBarNotification notification;
         public StatusBarIconView icon;
-        public View row; // the outer expanded view
+        public ExpandableNotificationRow row; // the outer expanded view
         public View content; // takes the click events and sends the PendingIntent
         public View expanded; // the inflated RemoteViews
         public ImageView largeIcon;
@@ -47,40 +47,16 @@
         }
         public void setBigContentView(View bigContentView) {
             this.expandedBig = bigContentView;
-            writeBooleanTag(row, R.id.expandable_tag, bigContentView != null);
+            row.setExpandable(bigContentView != null);
         }
         public View getBigContentView() {
             return expandedBig;
         }
         /**
-         * Return whether the entry can be expanded.
-         */
-        public boolean expandable() {
-            return NotificationData.getIsExpandable(row);
-        }
-        /**
-         * Return whether the entry has been manually expanded by the user.
-         */
-        public boolean userExpanded() {
-            return NotificationData.getUserExpanded(row);
-        }
-        /**
-         * Set the flag indicating that this was manually expanded by the user.
-         */
-        public boolean setUserExpanded(boolean userExpanded) {
-            return NotificationData.setUserExpanded(row, userExpanded);
-        }
-        /**
-         * Return whether the entry is being touched by the user.
-         */
-        public boolean userLocked() {
-            return NotificationData.getUserLocked(row);
-        }
-        /**
          * Set the flag indicating that this is being touched by the user.
          */
-        public boolean setUserLocked(boolean userLocked) {
-            return NotificationData.setUserLocked(row, userLocked);
+        public void setUserLocked(boolean userLocked) {
+            row.setUserLocked(userLocked);
         }
     }
     private final ArrayList<Entry> mEntries = new ArrayList<Entry>();
@@ -125,8 +101,8 @@
         return i;
     }
 
-    public int add(IBinder key, StatusBarNotification notification, View row, View content,
-            View expanded, StatusBarIconView icon) {
+    public int add(IBinder key, StatusBarNotification notification, ExpandableNotificationRow row,
+            View content, View expanded, StatusBarIconView icon) {
         Entry entry = new Entry();
         entry.key = key;
         entry.notification = notification;
@@ -171,55 +147,4 @@
         }
         return false;
     }
-
-    protected static boolean readBooleanTag(View view, int id)  {
-        if (view != null) {
-            Object value = view.getTag(id);
-            return value != null && value instanceof Boolean && ((Boolean) value).booleanValue();
-        }
-        return false;
-    }
-
-    protected static boolean writeBooleanTag(View view, int id, boolean value)  {
-        if (view != null) {
-            view.setTag(id, Boolean.valueOf(value));
-            return value;
-        }
-        return false;
-    }
-
-    /**
-     * Return whether the entry can be expanded.
-     */
-    public static boolean getIsExpandable(View row) {
-        return readBooleanTag(row, R.id.expandable_tag);
-    }
-
-    /**
-     * Return whether the entry has been manually expanded by the user.
-     */
-    public static boolean getUserExpanded(View row) {
-        return readBooleanTag(row, R.id.user_expanded_tag);
-    }
-
-    /**
-     * Set whether the entry has been manually expanded by the user.
-     */
-    public static boolean setUserExpanded(View row, boolean userExpanded) {
-        return writeBooleanTag(row, R.id.user_expanded_tag, userExpanded);
-    }
-
-    /**
-     * Return whether the entry is being touched by the user.
-     */
-    public static boolean getUserLocked(View row) {
-        return readBooleanTag(row, R.id.user_lock_tag);
-    }
-
-    /**
-     * Set whether the entry is being touched by the user.
-     */
-    public static boolean setUserLocked(View row, boolean userLocked) {
-        return writeBooleanTag(row, R.id.user_lock_tag, userLocked);
-    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java
index 6eadd2b..b9d41c3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java
@@ -122,9 +122,6 @@
     private static final int MSG_OPEN_SETTINGS_PANEL = 1002;
     // 1020-1030 reserved for BaseStatusBar
 
-    // will likely move to a resource or other tunable param at some point
-    private static final int HEADS_UP_DECAY_MS = 0; // disabled, was 10000;
-
     private static final boolean CLOSE_PANEL_WHEN_EMPTIED = true;
 
     private static final int NOTIFICATION_PRIORITY_MULTIPLIER = 10; // see NotificationManagerService
@@ -226,6 +223,7 @@
 
     // for heads up notifications
     private HeadsUpNotificationView mHeadsUpNotificationView;
+    private int mHeadsUpNotificationDecay;
 
     // on-screen navigation buttons
     private NavigationBarView mNavigationBarView = null;
@@ -829,7 +827,7 @@
         WindowManager.LayoutParams lp = new WindowManager.LayoutParams(
                 ViewGroup.LayoutParams.MATCH_PARENT,
                 ViewGroup.LayoutParams.WRAP_CONTENT,
-                WindowManager.LayoutParams.TYPE_STATUS_BAR_PANEL, // above the status bar!
+                WindowManager.LayoutParams.TYPE_STATUS_BAR_SUB_PANEL, // above the status bar!
                 WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
                     | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS
                     | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
@@ -837,8 +835,8 @@
                     | WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM
                     | WindowManager.LayoutParams.FLAG_SPLIT_TOUCH,
                 PixelFormat.TRANSLUCENT);
+        lp.flags |= WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED;
         lp.gravity = Gravity.TOP | Gravity.FILL_HORIZONTAL;
-        //lp.y += height * 1.5; // FIXME
         lp.setTitle("Heads Up");
         lp.packageName = mContext.getPackageName();
         lp.windowAnimations = R.style.Animation_StatusBar_HeadsUp;
@@ -890,26 +888,19 @@
         if (mUseHeadsUp && shouldInterrupt(notification)) {
             if (DEBUG) Log.d(TAG, "launching notification in heads up mode");
             // 1. Populate mHeadsUpNotificationView
+            mInterruptingNotificationTime = System.currentTimeMillis();
+            mInterruptingNotificationEntry = new Entry(key, notification, null);
 
-            // bind the click event to the content area
-            PendingIntent contentIntent = notification.getNotification().contentIntent;
-            final View.OnClickListener listener = (contentIntent != null)
-                    ? new NotificationClicker(contentIntent,
-                    notification.getPackageName(), notification.getTag(), notification.getId())
-                    : null;
-
-            if (mHeadsUpNotificationView.applyContent(notification.getNotification(), listener)) {
-
-                mCurrentlyInterruptingNotification = notification;
-
+            if (inflateViews(mInterruptingNotificationEntry,
+                    mHeadsUpNotificationView.getHolder())) {
+                mHeadsUpNotificationView.setNotification(mInterruptingNotificationEntry);
                 // 2. Animate mHeadsUpNotificationView in
                 mHandler.sendEmptyMessage(MSG_SHOW_HEADS_UP);
 
-                // 3. Set alarm to age the notification off (TODO)
-                mHandler.removeMessages(MSG_HIDE_HEADS_UP);
-                if (HEADS_UP_DECAY_MS > 0) {
-                    mHandler.sendEmptyMessageDelayed(MSG_HIDE_HEADS_UP, HEADS_UP_DECAY_MS);
-                }
+                // 3. Set alarm to age the notification off
+                resetHeadsUpDecayTimer();
+            } else {
+                mInterruptingNotificationEntry = null;
             }
         } else if (notification.getNotification().fullScreenIntent != null) {
             // Stop screensaver if the notification has a full-screen intent.
@@ -926,7 +917,7 @@
             // usual case: status bar visible & not immersive
 
             // show the ticker if there isn't already a heads up
-            if (mCurrentlyInterruptingNotification == null) {
+            if (mInterruptingNotificationEntry == null) {
                 tick(null, notification, true);
             }
         }
@@ -936,6 +927,14 @@
         updateExpandedViewPos(EXPANDED_LEAVE_ALONE);
     }
 
+    @Override
+    public void resetHeadsUpDecayTimer() {
+        mHandler.removeMessages(MSG_HIDE_HEADS_UP);
+        if (mHeadsUpNotificationDecay > 0) {
+            mHandler.sendEmptyMessageDelayed(MSG_HIDE_HEADS_UP, mHeadsUpNotificationDecay);
+        }
+    }
+
     public void removeNotification(IBinder key) {
         StatusBarNotification old = removeNotificationViews(key);
         if (SPEW) Log.d(TAG, "removeNotification key=" + key + " old=" + old);
@@ -947,7 +946,8 @@
             // Recalculate the position of the sliding windows and the titles.
             updateExpandedViewPos(EXPANDED_LEAVE_ALONE);
 
-            if (ENABLE_HEADS_UP && old == mCurrentlyInterruptingNotification) {
+            if (ENABLE_HEADS_UP && mInterruptingNotificationEntry != null
+                    && old == mInterruptingNotificationEntry.notification) {
                 mHandler.sendEmptyMessage(MSG_HIDE_HEADS_UP);
             }
 
@@ -1335,7 +1335,7 @@
                     break;
                 case MSG_HIDE_HEADS_UP:
                     setHeadsUpVisibility(false);
-                    mCurrentlyInterruptingNotification = null;
+                    mInterruptingNotificationEntry = null;
                     break;
             }
         }
@@ -2464,14 +2464,14 @@
         mHeadsUpNotificationView.setVisibility(vis ? View.VISIBLE : View.GONE);
     }
 
-    public void dismissHeadsUp() {
-        if (mCurrentlyInterruptingNotification == null) return;
+    public void onHeadsUpDismissed() {
+        if (mInterruptingNotificationEntry == null) return;
 
         try {
             mBarService.onNotificationClear(
-                    mCurrentlyInterruptingNotification.getPackageName(),
-                    mCurrentlyInterruptingNotification.getTag(),
-                    mCurrentlyInterruptingNotification.getId());
+                    mInterruptingNotificationEntry.notification.getPackageName(),
+                    mInterruptingNotificationEntry.notification.getTag(),
+                    mInterruptingNotificationEntry.notification.getId());
         } catch (android.os.RemoteException ex) {
             // oh well
         }
@@ -2555,6 +2555,9 @@
             mNotificationPanelMinHeightFrac = 0f;
         }
 
+        mHeadsUpNotificationDecay = res.getInteger(R.integer.heads_up_notification_decay);
+        mRowHeight =  res.getDimensionPixelSize(R.dimen.notification_row_min_height);
+
         if (false) Log.v(TAG, "updateResources");
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpNotificationView.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpNotificationView.java
index f6ac4a8..038eba1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpNotificationView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpNotificationView.java
@@ -16,11 +16,9 @@
 
 package com.android.systemui.statusbar.policy;
 
-import android.app.Notification;
 import android.content.Context;
 import android.content.res.Configuration;
 import android.graphics.Rect;
-import android.graphics.drawable.Drawable;
 import android.util.AttributeSet;
 import android.util.Log;
 import android.view.MotionEvent;
@@ -29,23 +27,32 @@
 import android.view.ViewGroup;
 import android.widget.LinearLayout;
 
+import com.android.systemui.ExpandHelper;
 import com.android.systemui.R;
 import com.android.systemui.SwipeHelper;
 import com.android.systemui.statusbar.BaseStatusBar;
+import com.android.systemui.statusbar.NotificationData;
 
-public class HeadsUpNotificationView extends LinearLayout implements SwipeHelper.Callback {
+public class HeadsUpNotificationView extends LinearLayout implements SwipeHelper.Callback, ExpandHelper.Callback {
     private static final String TAG = "HeadsUpNotificationView";
     private static final boolean DEBUG = false;
 
     Rect mTmpRect = new Rect();
 
+    private final int mTouchSensitivityDelay;
     private SwipeHelper mSwipeHelper;
 
-    BaseStatusBar mBar;
+    private BaseStatusBar mBar;
+    private ExpandHelper mExpandHelper;
+    private long mStartTouchTime;
+
+    public ViewGroup getHolder() {
+        return mContentHolder;
+    }
+
     private ViewGroup mContentHolder;
 
-    private Notification mHeadsUp;
-    private OnClickListener mOnClickListener;
+    private NotificationData.Entry mHeadsUp;
 
     public HeadsUpNotificationView(Context context, AttributeSet attrs) {
         this(context, attrs, 0);
@@ -53,8 +60,9 @@
 
     public HeadsUpNotificationView(Context context, AttributeSet attrs, int defStyle) {
         super(context, attrs, defStyle);
-
         setOrientation(LinearLayout.VERTICAL);
+        mTouchSensitivityDelay = getResources().getInteger(R.integer.heads_up_sensitivity_delay);
+        if (DEBUG) Log.v(TAG, "create() " + mTouchSensitivityDelay);
     }
 
     @Override
@@ -63,10 +71,14 @@
         float pagingTouchSlop = ViewConfiguration.get(getContext()).getScaledPagingTouchSlop();
         mSwipeHelper = new SwipeHelper(SwipeHelper.X, this, densityScale, pagingTouchSlop);
 
+        int minHeight = getResources().getDimensionPixelSize(R.dimen.notification_row_min_height);
+        int maxHeight = getResources().getDimensionPixelSize(R.dimen.notification_row_max_height);
+        mExpandHelper = new ExpandHelper(mContext, this, minHeight, maxHeight);
+
         mContentHolder = (ViewGroup) findViewById(R.id.contentHolder);
         if (mHeadsUp != null) {
             // whoops, we're on already!
-            applyContent(mHeadsUp, mOnClickListener);
+            setNotification(mHeadsUp);
         }
     }
 
@@ -77,14 +89,23 @@
     @Override
     public boolean onInterceptTouchEvent(MotionEvent ev) {
         if (DEBUG) Log.v(TAG, "onInterceptTouchEvent()");
-        return mSwipeHelper.onInterceptTouchEvent(ev) ||
-            super.onInterceptTouchEvent(ev);
+        if (System.currentTimeMillis() < mStartTouchTime) {
+            return true;
+        }
+        return mSwipeHelper.onInterceptTouchEvent(ev)
+                || mExpandHelper.onInterceptTouchEvent(ev)
+                || super.onInterceptTouchEvent(ev);
     }
 
     @Override
     public boolean onTouchEvent(MotionEvent ev) {
-        return mSwipeHelper.onTouchEvent(ev) ||
-            super.onTouchEvent(ev);
+        if (System.currentTimeMillis() < mStartTouchTime) {
+            return false;
+        }
+        mBar.resetHeadsUpDecayTimer();
+        return mSwipeHelper.onTouchEvent(ev)
+                || mExpandHelper.onTouchEvent(ev)
+                || super.onTouchEvent(ev);
     }
 
     public boolean canChildBeDismissed(View v) {
@@ -93,7 +114,7 @@
 
     public void onChildDismissed(View v) {
         Log.v(TAG, "User swiped heads up to dismiss");
-        mBar.dismissHeadsUp();
+        mBar.onHeadsUpDismissed();
     }
 
     public void onBeginDrag(View v) {
@@ -134,34 +155,48 @@
         }
     }
 
-    public boolean applyContent(Notification headsUp, OnClickListener listener) {
+    public boolean setNotification(NotificationData.Entry headsUp) {
         mHeadsUp = headsUp;
-        mOnClickListener = listener;
+        mHeadsUp.row.setExpanded(false);
         if (mContentHolder == null) {
             // too soon!
             return false;
         }
-        if (headsUp.contentView == null) {
-            // bad data
-            return false;
-        }
         mContentHolder.setX(0);
         mContentHolder.setVisibility(View.VISIBLE);
         mContentHolder.setAlpha(1f);
         mContentHolder.removeAllViews();
-        final View content = headsUp.contentView.apply(getContext(), mContentHolder);
-        if (listener != null) {
-            content.setOnClickListener(listener);
-
-            Drawable bg = getResources().getDrawable(R.drawable.heads_up_notification_row_bg);
-            if (bg == null) {
-                Log.e(TAG, String.format("Can't find background drawable id=0x%08x",
-                        R.drawable.heads_up_notification_row_bg));
-            } else {
-                content.setBackgroundDrawable(bg);
-            }
-        }
-        mContentHolder.addView(content);
+        mContentHolder.addView(mHeadsUp.row);
+        mStartTouchTime = System.currentTimeMillis() + mTouchSensitivityDelay;
         return true;
     }
+
+    @Override
+    public View getChildAtRawPosition(float x, float y) {
+        return getChildAtPosition(x, y);
+    }
+
+    @Override
+    public View 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);
+        }
+    }
 }
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/NotificationRowLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/NotificationRowLayout.java
index 92c57c8..259422d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/NotificationRowLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/NotificationRowLayout.java
@@ -32,6 +32,7 @@
 import com.android.systemui.ExpandHelper;
 import com.android.systemui.R;
 import com.android.systemui.SwipeHelper;
+import com.android.systemui.statusbar.ExpandableNotificationRow;
 import com.android.systemui.statusbar.NotificationData;
 
 import java.util.HashMap;
@@ -149,15 +150,20 @@
     }
 
     public boolean canChildBeExpanded(View v) {
-        return NotificationData.getIsExpandable(v);
+        return v instanceof ExpandableNotificationRow
+                && ((ExpandableNotificationRow) v).isExpandable();
     }
 
-    public boolean setUserExpandedChild(View v, boolean userExpanded) {
-        return NotificationData.setUserExpanded(v, userExpanded);
+    public void setUserExpandedChild(View v, boolean userExpanded) {
+        if (v instanceof ExpandableNotificationRow) {
+            ((ExpandableNotificationRow) v).setUserExpanded(userExpanded);
+        }
     }
 
-    public boolean setUserLockedChild(View v, boolean userLocked) {
-        return NotificationData.setUserLocked(v, userLocked);
+    public void setUserLockedChild(View v, boolean userLocked) {
+        if (v instanceof ExpandableNotificationRow) {
+            ((ExpandableNotificationRow) v).setUserLocked(userLocked);
+        }
     }
 
     public void onChildDismissed(View v) {
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 cf909f3..64a4a0e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/tv/TvStatusBar.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/tv/TvStatusBar.java
@@ -137,6 +137,10 @@
     }
 
     @Override
+    public void resetHeadsUpDecayTimer() {
+    }
+
+    @Override
     public void animateExpandSettingsPanel() {
     }