Merge "Split auto dismissing functionality from heads up"
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/AlertingNotificationManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/AlertingNotificationManager.java
new file mode 100644
index 0000000..c017104
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/AlertingNotificationManager.java
@@ -0,0 +1,317 @@
+/*
+ * Copyright (C) 2018 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.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.SystemClock;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+import android.util.Log;
+import android.view.accessibility.AccessibilityEvent;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.systemui.statusbar.notification.NotificationData;
+
+import java.util.stream.Stream;
+
+/**
+ * A manager which contains notification alerting functionality, providing methods to add and
+ * remove notifications that appear on screen for a period of time and dismiss themselves at the
+ * appropriate time.  These include heads up notifications and ambient pulses.
+ */
+public abstract class AlertingNotificationManager {
+    private static final String TAG = "AlertNotifManager";
+    protected final Clock mClock = new Clock();
+    protected final ArrayMap<String, AlertEntry> mAlertEntries = new ArrayMap<>();
+    protected int mMinimumDisplayTime;
+    protected int mAutoDismissNotificationDecay;
+    @VisibleForTesting
+    public Handler mHandler = new Handler(Looper.getMainLooper());
+
+    /**
+     * Called when posting a new notification that should alert the user and appear on screen.
+     * Adds the notification to be managed.
+     * @param entry entry to show
+     */
+    public void showNotification(@NonNull NotificationData.Entry entry) {
+        if (Log.isLoggable(TAG, Log.VERBOSE)) {
+            Log.v(TAG, "showNotification");
+        }
+        addAlertEntry(entry);
+        updateNotification(entry.key, true /* alert */);
+        entry.setInterruption();
+    }
+
+    /**
+     * Try to remove the notification.  May not succeed if the notification has not been shown long
+     * enough and needs to be kept around.
+     * @param key the key of the notification to remove
+     * @param releaseImmediately force a remove regardless of earliest removal time
+     * @return true if notification is removed, false otherwise
+     */
+    public boolean removeNotification(@NonNull String key, boolean releaseImmediately) {
+        if (Log.isLoggable(TAG, Log.VERBOSE)) {
+            Log.v(TAG, "removeNotification");
+        }
+        AlertEntry alertEntry = mAlertEntries.get(key);
+        if (alertEntry == null) {
+            return true;
+        }
+        if (releaseImmediately || alertEntry.wasShownLongEnough()) {
+            removeAlertEntry(key);
+        } else {
+            alertEntry.removeAsSoonAsPossible();
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * Called when the notification state has been updated.
+     * @param key the key of the entry that was updated
+     * @param alert whether the notification should alert again and force reevaluation of
+     *              removal time
+     */
+    public void updateNotification(@NonNull String key, boolean alert) {
+        if (Log.isLoggable(TAG, Log.VERBOSE)) {
+            Log.v(TAG, "updateNotification");
+        }
+
+        AlertEntry alertEntry = mAlertEntries.get(key);
+        if (alertEntry == null) {
+            // the entry was released before this update (i.e by a listener) This can happen
+            // with the groupmanager
+            return;
+        }
+
+        alertEntry.mEntry.row.sendAccessibilityEvent(
+                AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
+        if (alert) {
+            alertEntry.updateEntry(true /* updatePostTime */);
+        }
+    }
+
+    /**
+     * Clears all managed notifications.
+     */
+    public void releaseAllImmediately() {
+        if (Log.isLoggable(TAG, Log.VERBOSE)) {
+            Log.v(TAG, "releaseAllImmediately");
+        }
+        // A copy is necessary here as we are changing the underlying map.  This would cause
+        // undefined behavior if we iterated over the key set directly.
+        ArraySet<String> keysToRemove = new ArraySet<>(mAlertEntries.keySet());
+        for (String key : keysToRemove) {
+            removeAlertEntry(key);
+        }
+    }
+
+    /**
+     * Returns the entry if it is managed by this manager.
+     * @param key key of notification
+     * @return the entry
+     */
+    @Nullable
+    public NotificationData.Entry getEntry(@NonNull String key) {
+        AlertEntry entry = mAlertEntries.get(key);
+        return entry != null ? entry.mEntry : null;
+    }
+
+    /**
+     * Returns the stream of all current notifications managed by this manager.
+     * @return all entries
+     */
+    @NonNull
+    public Stream<NotificationData.Entry> getAllEntries() {
+        return mAlertEntries.values().stream().map(headsUpEntry -> headsUpEntry.mEntry);
+    }
+
+    /**
+     * Whether or not there are any active alerting notifications.
+     * @return true if there is an alert, false otherwise
+     */
+    public boolean hasNotifications() {
+        return !mAlertEntries.isEmpty();
+    }
+
+    /**
+     * Whether or not the given notification is alerting and managed by this manager.
+     * @return true if the notification is alerting
+     */
+    public boolean contains(@NonNull String key) {
+        return mAlertEntries.containsKey(key);
+    }
+
+    /**
+     * Add a new entry and begin managing it.
+     * @param entry the entry to add
+     */
+    protected final void addAlertEntry(@NonNull NotificationData.Entry entry) {
+        AlertEntry alertEntry = createAlertEntry();
+        alertEntry.setEntry(entry);
+        mAlertEntries.put(entry.key, alertEntry);
+        onAlertEntryAdded(alertEntry);
+        entry.row.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
+    }
+
+    /**
+     * Manager-specific logic that should occur when an entry is added.
+     * @param alertEntry alert entry added
+     */
+    protected abstract void onAlertEntryAdded(@NonNull AlertEntry alertEntry);
+
+    /**
+     * Remove a notification and reset the alert entry.
+     * @param key key of notification to remove
+     */
+    protected final void removeAlertEntry(@NonNull String key) {
+        AlertEntry alertEntry = mAlertEntries.get(key);
+        if (alertEntry == null) {
+            return;
+        }
+        NotificationData.Entry entry = alertEntry.mEntry;
+        mAlertEntries.remove(key);
+        onAlertEntryRemoved(alertEntry);
+        entry.row.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
+        alertEntry.reset();
+    }
+
+    /**
+     * Manager-specific logic that should occur when an alert entry is removed.
+     * @param alertEntry alert entry removed
+     */
+    protected abstract void onAlertEntryRemoved(@NonNull AlertEntry alertEntry);
+
+    /**
+     * Returns a new alert entry instance.
+     * @return a new AlertEntry
+     */
+    protected AlertEntry createAlertEntry() {
+        return new AlertEntry();
+    }
+
+    protected class AlertEntry implements Comparable<AlertEntry> {
+        @Nullable public NotificationData.Entry mEntry;
+        public long mPostTime;
+        public long mEarliestRemovaltime;
+
+        @Nullable protected Runnable mRemoveAlertRunnable;
+
+        public void setEntry(@Nullable final NotificationData.Entry entry) {
+            setEntry(entry, () -> removeAlertEntry(entry.key));
+        }
+
+        public void setEntry(@Nullable final NotificationData.Entry entry,
+                @Nullable Runnable removeAlertRunnable) {
+            mEntry = entry;
+            mRemoveAlertRunnable = removeAlertRunnable;
+
+            mPostTime = calculatePostTime();
+            updateEntry(true /* updatePostTime */);
+        }
+
+        /**
+         * Updates an entry's removal time.
+         * @param updatePostTime whether or not to refresh the post time
+         */
+        public void updateEntry(boolean updatePostTime) {
+            if (Log.isLoggable(TAG, Log.VERBOSE)) {
+                Log.v(TAG, "updateEntry");
+            }
+
+            long currentTime = mClock.currentTimeMillis();
+            mEarliestRemovaltime = currentTime + mMinimumDisplayTime;
+            if (updatePostTime) {
+                mPostTime = Math.max(mPostTime, currentTime);
+            }
+            removeAutoRemovalCallbacks();
+
+            if (!isSticky()) {
+                long finishTime = mPostTime + mAutoDismissNotificationDecay;
+                long removeDelay = Math.max(finishTime - currentTime, mMinimumDisplayTime);
+                mHandler.postDelayed(mRemoveAlertRunnable, removeDelay);
+            }
+        }
+
+        /**
+         * Whether or not the notification is "sticky" i.e. should stay on screen regardless
+         * of the timer and should be removed externally.
+         * @return true if the notification is sticky
+         */
+        protected boolean isSticky() {
+            return false;
+        }
+
+        /**
+         * Whether the notification has been on screen long enough and can be removed.
+         * @return true if the notification has been on screen long enough
+         */
+        public boolean wasShownLongEnough() {
+            return mEarliestRemovaltime < mClock.currentTimeMillis();
+        }
+
+        @Override
+        public int compareTo(@NonNull AlertEntry alertEntry) {
+            return (mPostTime < alertEntry.mPostTime)
+                    ? 1 : ((mPostTime == alertEntry.mPostTime)
+                            ? mEntry.key.compareTo(alertEntry.mEntry.key) : -1);
+        }
+
+        public void reset() {
+            mEntry = null;
+            removeAutoRemovalCallbacks();
+            mRemoveAlertRunnable = null;
+        }
+
+        /**
+         * Clear any pending removal runnables.
+         */
+        public void removeAutoRemovalCallbacks() {
+            if (mRemoveAlertRunnable != null) {
+                mHandler.removeCallbacks(mRemoveAlertRunnable);
+            }
+        }
+
+        /**
+         * Remove the alert at the earliest allowed removal time.
+         */
+        public void removeAsSoonAsPossible() {
+            if (mRemoveAlertRunnable != null) {
+                removeAutoRemovalCallbacks();
+                mHandler.postDelayed(mRemoveAlertRunnable,
+                        mEarliestRemovaltime - mClock.currentTimeMillis());
+            }
+        }
+
+        /**
+         * Calculate what the post time of a notification is at some current time.
+         * @return the post time
+         */
+        protected long calculatePostTime() {
+            return mClock.currentTimeMillis();
+        }
+    }
+
+    protected final static class Clock {
+        public long currentTimeMillis() {
+            return SystemClock.elapsedRealtime();
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationEntryManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationEntryManager.java
index dc58cab..0ab71382 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationEntryManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationEntryManager.java
@@ -487,7 +487,7 @@
     public void removeNotification(String key, NotificationListenerService.RankingMap ranking) {
         boolean deferRemoval = false;
         abortExistingInflation(key);
-        if (mHeadsUpManager.isHeadsUp(key)) {
+        if (mHeadsUpManager.contains(key)) {
             // A cancel() in response to a remote input shouldn't be delayed, as it makes the
             // sending look longer than it takes.
             // Also we should not defer the removal if reordering isn't allowed since otherwise
@@ -1060,7 +1060,7 @@
                 // We don't want this to be interrupting anymore, lets remove it
                 mHeadsUpManager.removeNotification(key, false /* ignoreEarliestRemovalTime */);
             } else {
-                mHeadsUpManager.updateNotification(entry, alertAgain);
+                mHeadsUpManager.updateNotification(entry.key, alertAgain);
             }
         } else if (shouldPeek && alertAgain) {
             // This notification was updated to be a heads-up, show it!
@@ -1069,7 +1069,7 @@
     }
 
     protected boolean isHeadsUp(String key) {
-        return mHeadsUpManager.isHeadsUp(key);
+        return mHeadsUpManager.contains(key);
     }
 
     public boolean isNotificationKeptForRemoteInput(String key) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java
index 89107bb..81066f3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java
@@ -31,7 +31,6 @@
 import android.view.View;
 import android.view.ViewTreeObserver;
 
-import com.android.internal.annotations.VisibleForTesting;
 import com.android.systemui.Dumpable;
 import com.android.systemui.R;
 import com.android.systemui.ScreenDecorations;
@@ -55,7 +54,6 @@
        ViewTreeObserver.OnComputeInternalInsetsListener, VisualStabilityManager.Callback,
        OnHeadsUpChangedListener, ConfigurationController.ConfigurationListener {
     private static final String TAG = "HeadsUpManagerPhone";
-    private static final boolean DEBUG = false;
 
     private final View mStatusBarWindowView;
     private final NotificationGroupManager mGroupManager;
@@ -114,7 +112,9 @@
         addListener(new OnHeadsUpChangedListener() {
             @Override
             public void onHeadsUpPinnedModeChanged(boolean hasPinnedNotification) {
-                if (DEBUG) Log.w(TAG, "onHeadsUpPinnedModeChanged");
+                if (Log.isLoggable(TAG, Log.WARN)) {
+                    Log.w(TAG, "onHeadsUpPinnedModeChanged");
+                }
                 updateTouchableRegionListener();
             }
         });
@@ -153,7 +153,7 @@
      */
     public boolean shouldSwallowClick(@NonNull String key) {
         HeadsUpManager.HeadsUpEntry entry = getHeadsUpEntry(key);
-        return entry != null && mClock.currentTimeMillis() < entry.postTime;
+        return entry != null && mClock.currentTimeMillis() < entry.mPostTime;
     }
 
     public void onExpandingFinished() {
@@ -162,9 +162,9 @@
             mReleaseOnExpandFinish = false;
         } else {
             for (NotificationData.Entry entry : mEntriesToRemoveAfterExpand) {
-                if (isHeadsUp(entry.key)) {
+                if (contains(entry.key)) {
                     // Maybe the heads-up was removed already
-                    removeHeadsUpEntry(entry);
+                    removeAlertEntry(entry.key);
                 }
             }
         }
@@ -235,13 +235,6 @@
         }
     }
 
-    @VisibleForTesting
-    public void removeMinimumDisplayTimeForTesting() {
-        mMinimumDisplayTime = 0;
-        mHeadsUpNotificationDecay = 0;
-        mTouchAcceptanceDelay = 0;
-    }
-
     ///////////////////////////////////////////////////////////////////////////////////////////////
     //  HeadsUpManager public methods overrides:
 
@@ -250,12 +243,6 @@
         return mTrackingHeadsUp;
     }
 
-    @Override
-    public void snooze() {
-        super.snooze();
-        mReleaseOnExpandFinish = true;
-    }
-
     /**
      * React to the removal of the notification in the heads up.
      *
@@ -263,14 +250,15 @@
      * for a bit since it wasn't shown long enough
      */
     @Override
-    public boolean removeNotification(@NonNull String key, boolean ignoreEarliestRemovalTime) {
-        if (wasShownLongEnough(key) || ignoreEarliestRemovalTime) {
-            return super.removeNotification(key, ignoreEarliestRemovalTime);
-        } else {
-            HeadsUpEntryPhone entry = getHeadsUpEntryPhone(key);
-            entry.removeAsSoonAsPossible();
-            return false;
-        }
+    public boolean removeNotification(@NonNull String key, boolean releaseImmediately) {
+        return super.removeNotification(key, canRemoveImmediately(key)
+                || releaseImmediately);
+    }
+
+    @Override
+    public void snooze() {
+        super.snooze();
+        mReleaseOnExpandFinish = true;
     }
 
     public void addSwipedOutNotification(@NonNull String key) {
@@ -354,9 +342,9 @@
     public void onReorderingAllowed() {
         mBar.getNotificationScrollLayout().setHeadsUpGoingAwayAnimationsAllowed(false);
         for (NotificationData.Entry entry : mEntriesToRemoveWhenReorderingAllowed) {
-            if (isHeadsUp(entry.key)) {
+            if (contains(entry.key)) {
                 // Maybe the heads-up was removed already
-                removeHeadsUpEntry(entry);
+                removeAlertEntry(entry.key);
             }
         }
         mEntriesToRemoveWhenReorderingAllowed.clear();
@@ -367,14 +355,14 @@
     //  HeadsUpManager utility (protected) methods overrides:
 
     @Override
-    protected HeadsUpEntry createHeadsUpEntry() {
+    protected HeadsUpEntry createAlertEntry() {
         return mEntryPool.acquire();
     }
 
     @Override
-    protected void releaseHeadsUpEntry(HeadsUpEntry entry) {
-        entry.reset();
-        mEntryPool.release((HeadsUpEntryPhone) entry);
+    protected void onAlertEntryRemoved(AlertEntry alertEntry) {
+        super.onAlertEntryRemoved(alertEntry);
+        mEntryPool.release((HeadsUpEntryPhone) alertEntry);
     }
 
     @Override
@@ -394,7 +382,7 @@
 
     @Nullable
     private HeadsUpEntryPhone getHeadsUpEntryPhone(@NonNull String key) {
-        return (HeadsUpEntryPhone) getHeadsUpEntry(key);
+        return (HeadsUpEntryPhone) mAlertEntries.get(key);
     }
 
     @Nullable
@@ -402,7 +390,7 @@
         return (HeadsUpEntryPhone) getTopHeadsUpEntry();
     }
 
-    private boolean wasShownLongEnough(@NonNull String key) {
+    private boolean canRemoveImmediately(@NonNull String key) {
         if (mSwipedOutKeys.contains(key)) {
             // We always instantly dismiss views being manually swiped out.
             mSwipedOutKeys.remove(key);
@@ -461,33 +449,29 @@
                     mVisualStabilityManager.addReorderingAllowedCallback(
                             HeadsUpManagerPhone.this);
                 } else if (!mTrackingHeadsUp) {
-                    removeHeadsUpEntry(entry);
+                    removeAlertEntry(entry.key);
                 } else {
                     mEntriesToRemoveAfterExpand.add(entry);
                 }
             };
 
-            super.setEntry(entry, removeHeadsUpRunnable);
-        }
-
-        public boolean wasShownLongEnough() {
-            return earliestRemovaltime < mClock.currentTimeMillis();
+            setEntry(entry, removeHeadsUpRunnable);
         }
 
         @Override
         public void updateEntry(boolean updatePostTime) {
             super.updateEntry(updatePostTime);
 
-            if (mEntriesToRemoveAfterExpand.contains(entry)) {
-                mEntriesToRemoveAfterExpand.remove(entry);
+            if (mEntriesToRemoveAfterExpand.contains(mEntry)) {
+                mEntriesToRemoveAfterExpand.remove(mEntry);
             }
-            if (mEntriesToRemoveWhenReorderingAllowed.contains(entry)) {
-                mEntriesToRemoveWhenReorderingAllowed.remove(entry);
+            if (mEntriesToRemoveWhenReorderingAllowed.contains(mEntry)) {
+                mEntriesToRemoveWhenReorderingAllowed.remove(mEntry);
             }
         }
 
         @Override
-        public void expanded(boolean expanded) {
+        public void setExpanded(boolean expanded) {
             if (this.expanded == expanded) {
                 return;
             }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationGroupManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationGroupManager.java
index 37c2fdf..c27ccea 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationGroupManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationGroupManager.java
@@ -171,7 +171,7 @@
      */
     private void cleanUpHeadsUpStatesOnAdd(NotificationGroup group, boolean addIsPending) {
         if (!addIsPending && group.hunSummaryOnNextAddition) {
-            if (!mHeadsUpManager.isHeadsUp(group.summary.key)) {
+            if (!mHeadsUpManager.contains(group.summary.key)) {
                 mHeadsUpManager.showNotification(group.summary);
             }
             group.hunSummaryOnNextAddition = false;
@@ -208,15 +208,17 @@
                 NotificationData.Entry entry = children.get(i);
                 if (onlySummaryAlerts(entry) && entry.row.isHeadsUp()) {
                     releasedChild = true;
-                    mHeadsUpManager.releaseImmediately(entry.key);
+                    mHeadsUpManager.removeNotification(
+                            entry.key, true /* releaseImmediately */);
                 }
             }
             if (isolatedChild != null && onlySummaryAlerts(isolatedChild)
                     && isolatedChild.row.isHeadsUp()) {
                 releasedChild = true;
-                mHeadsUpManager.releaseImmediately(isolatedChild.key);
+                mHeadsUpManager.removeNotification(
+                        isolatedChild.key, true /* releaseImmediately */);
             }
-            if (releasedChild && !mHeadsUpManager.isHeadsUp(group.summary.key)) {
+            if (releasedChild && !mHeadsUpManager.contains(group.summary.key)) {
                 boolean notifyImmediately = (numChildren - numPendingChildren) > 1;
                 if (notifyImmediately) {
                     mHeadsUpManager.showNotification(group.summary);
@@ -546,8 +548,8 @@
                     // the notification is actually already removed, no need to do heads-up on it.
                     return;
                 }
-                if (mHeadsUpManager.isHeadsUp(child.key)) {
-                    mHeadsUpManager.updateNotification(child, true);
+                if (mHeadsUpManager.contains(child.key)) {
+                    mHeadsUpManager.updateNotification(child.key, true /* alert */);
                 } else {
                     if (onlySummaryAlerts(entry)) {
                         notificationGroup.lastHeadsUpTransfer = SystemClock.elapsedRealtime();
@@ -556,7 +558,7 @@
                 }
             }
         }
-        mHeadsUpManager.releaseImmediately(entry.key);
+        mHeadsUpManager.removeNotification(entry.key, true /* releaseImmediately */);
     }
 
     private boolean onlySummaryAlerts(NotificationData.Entry entry) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
index 3c351ab..cd0255b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
@@ -56,7 +56,6 @@
 import android.app.StatusBarManager;
 import android.app.TaskStackBuilder;
 import android.app.UiModeManager;
-import android.app.WallpaperColors;
 import android.app.WallpaperInfo;
 import android.app.WallpaperManager;
 import android.app.admin.DevicePolicyManager;
@@ -67,8 +66,6 @@
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.content.IntentSender;
-import android.content.om.IOverlayManager;
-import android.content.om.OverlayInfo;
 import android.content.pm.IPackageManager;
 import android.content.pm.PackageManager;
 import android.content.pm.PackageManager.NameNotFoundException;
@@ -1414,7 +1411,7 @@
     @Override
     public void onPerformRemoveNotification(StatusBarNotification n) {
         if (mStackScroller.hasPulsingNotifications() &&
-                    !mHeadsUpManager.hasHeadsUpNotifications()) {
+                    !mHeadsUpManager.hasNotifications()) {
             // We were showing a pulse for a notification, but no notifications are pulsing anymore.
             // Finish the pulse.
             mDozeScrimController.pulseOutNow();
@@ -4835,7 +4832,7 @@
                 @Override
                 public void onPulseStarted() {
                     callback.onPulseStarted();
-                    if (mHeadsUpManager.hasHeadsUpNotifications()) {
+                    if (mHeadsUpManager.hasNotifications()) {
                         // Only pulse the stack scroller if there's actually something to show.
                         // Otherwise just show the always-on screen.
                         setPulsing(true);
@@ -5108,7 +5105,7 @@
         final boolean wasOccluded = mIsOccluded;
         dismissKeyguardThenExecute(() -> {
             // TODO: Some of this code may be able to move to NotificationEntryManager.
-            if (mHeadsUpManager != null && mHeadsUpManager.isHeadsUp(notificationKey)) {
+            if (mHeadsUpManager != null && mHeadsUpManager.contains(notificationKey)) {
                 // Release the HUN notification to the shade.
 
                 if (isPresenterFullyCollapsed()) {
@@ -5117,7 +5114,8 @@
                 //
                 // In most cases, when FLAG_AUTO_CANCEL is set, the notification will
                 // become canceled shortly by NoMan, but we can't assume that.
-                mHeadsUpManager.releaseImmediately(notificationKey);
+                mHeadsUpManager.removeNotification(sbn.getKey(),
+                        true /* releaseImmediately */);
             }
             StatusBarNotification parentToCancel = null;
             if (shouldAutoCancel(sbn) && mGroupManager.isOnlyChildInGroup(sbn)) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManager.java
index 677dd73..d477587 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManager.java
@@ -21,56 +21,44 @@
 import android.content.Context;
 import android.content.res.Resources;
 import android.database.ContentObserver;
-import android.os.SystemClock;
-import android.os.Handler;
-import android.os.Looper;
 import android.util.ArrayMap;
 import android.provider.Settings;
 import android.util.Log;
-import android.view.accessibility.AccessibilityEvent;
 
 import com.android.internal.logging.MetricsLogger;
 import com.android.systemui.R;
+import com.android.systemui.statusbar.AlertingNotificationManager;
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
 import com.android.systemui.statusbar.notification.NotificationData;
 
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
-import java.util.Iterator;
-import java.util.stream.Stream;
-import java.util.HashMap;
 import java.util.HashSet;
 
 /**
  * A manager which handles heads up notifications which is a special mode where
  * they simply peek from the top of the screen.
  */
-public class HeadsUpManager {
+public abstract class HeadsUpManager extends AlertingNotificationManager {
     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";
 
-    protected final Clock mClock = new Clock();
     protected final HashSet<OnHeadsUpChangedListener> mListeners = new HashSet<>();
-    protected final Handler mHandler = new Handler(Looper.getMainLooper());
 
     protected final Context mContext;
 
-    protected int mHeadsUpNotificationDecay;
-    protected int mMinimumDisplayTime;
     protected int mTouchAcceptanceDelay;
     protected int mSnoozeLengthMs;
     protected boolean mHasPinnedNotification;
     protected int mUser;
 
-    private final HashMap<String, HeadsUpEntry> mHeadsUpEntries = new HashMap<>();
     private final ArrayMap<String, Long> mSnoozedPackages;
 
     public HeadsUpManager(@NonNull final Context context) {
         mContext = context;
         Resources resources = context.getResources();
         mMinimumDisplayTime = resources.getInteger(R.integer.heads_up_notification_minimum_time);
-        mHeadsUpNotificationDecay = resources.getInteger(R.integer.heads_up_notification_decay);
+        mAutoDismissNotificationDecay = resources.getInteger(R.integer.heads_up_notification_decay);
         mTouchAcceptanceDelay = resources.getInteger(R.integer.touch_acceptance_delay);
         mSnoozedPackages = new ArrayMap<>();
         int defaultSnoozeLengthMs =
@@ -85,7 +73,9 @@
                         context.getContentResolver(), SETTING_HEADS_UP_SNOOZE_LENGTH_MS, -1);
                 if (packageSnoozeLengthMs > -1 && packageSnoozeLengthMs != mSnoozeLengthMs) {
                     mSnoozeLengthMs = packageSnoozeLengthMs;
-                    if (DEBUG) Log.v(TAG, "mSnoozeLengthMs = " + mSnoozeLengthMs);
+                    if (Log.isLoggable(TAG, Log.VERBOSE)) {
+                        Log.v(TAG, "mSnoozeLengthMs = " + mSnoozeLengthMs);
+                    }
                 }
             }
         };
@@ -108,49 +98,14 @@
         mListeners.remove(listener);
     }
 
-    /**
-     * Called when posting a new notification to the heads up.
-     */
-    public void showNotification(@NonNull NotificationData.Entry headsUp) {
-        if (DEBUG) Log.v(TAG, "showNotification");
-        addHeadsUpEntry(headsUp);
-        updateNotification(headsUp, true);
-        headsUp.setInterruption();
-    }
-
-    /**
-     * Called when updating or posting a notification to the heads up.
-     */
-    public void updateNotification(@NonNull NotificationData.Entry headsUp, boolean alert) {
-        if (DEBUG) Log.v(TAG, "updateNotification");
-
-        headsUp.row.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
-
-        if (alert) {
-            HeadsUpEntry headsUpEntry = mHeadsUpEntries.get(headsUp.key);
-            if (headsUpEntry == null) {
-                // the entry was released before this update (i.e by a listener) This can happen
-                // with the groupmanager
-                return;
-            }
-            headsUpEntry.updateEntry(true /* updatePostTime */);
-            setEntryPinned(headsUpEntry, shouldHeadsUpBecomePinned(headsUp));
+    public void updateNotification(@NonNull String key, boolean alert) {
+        super.updateNotification(key, alert);
+        AlertEntry alertEntry = getHeadsUpEntry(key);
+        if (alert && alertEntry != null) {
+            setEntryPinned((HeadsUpEntry) alertEntry, shouldHeadsUpBecomePinned(alertEntry.mEntry));
         }
     }
 
-    private void addHeadsUpEntry(@NonNull NotificationData.Entry entry) {
-        HeadsUpEntry headsUpEntry = createHeadsUpEntry();
-        // This will also add the entry to the sortedList
-        headsUpEntry.setEntry(entry);
-        mHeadsUpEntries.put(entry.key, headsUpEntry);
-        entry.row.setHeadsUp(true);
-        setEntryPinned(headsUpEntry, shouldHeadsUpBecomePinned(entry));
-        for (OnHeadsUpChangedListener listener : mListeners) {
-            listener.onHeadsUpStateChanged(entry, true);
-        }
-        entry.row.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
-    }
-
     protected boolean shouldHeadsUpBecomePinned(@NonNull NotificationData.Entry entry) {
         return hasFullScreenIntent(entry);
     }
@@ -161,8 +116,10 @@
 
     protected void setEntryPinned(
             @NonNull HeadsUpManager.HeadsUpEntry headsUpEntry, boolean isPinned) {
-        if (DEBUG) Log.v(TAG, "setEntryPinned: " + isPinned);
-        ExpandableNotificationRow row = headsUpEntry.entry.row;
+        if (Log.isLoggable(TAG, Log.VERBOSE)) {
+            Log.v(TAG, "setEntryPinned: " + isPinned);
+        }
+        ExpandableNotificationRow row = headsUpEntry.mEntry.row;
         if (row.isPinned() != isPinned) {
             row.setPinned(isPinned);
             updatePinnedMode();
@@ -176,20 +133,24 @@
         }
     }
 
-    protected void removeHeadsUpEntry(@NonNull NotificationData.Entry entry) {
-        HeadsUpEntry remove = mHeadsUpEntries.remove(entry.key);
-        onHeadsUpEntryRemoved(remove);
+    @Override
+    protected void onAlertEntryAdded(AlertEntry alertEntry) {
+        NotificationData.Entry entry = alertEntry.mEntry;
+        entry.row.setHeadsUp(true);
+        setEntryPinned((HeadsUpEntry) alertEntry, shouldHeadsUpBecomePinned(entry));
+        for (OnHeadsUpChangedListener listener : mListeners) {
+            listener.onHeadsUpStateChanged(entry, true);
+        }
     }
 
-    protected void onHeadsUpEntryRemoved(@NonNull HeadsUpEntry remove) {
-        NotificationData.Entry entry = remove.entry;
-        entry.row.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
+    @Override
+    protected void onAlertEntryRemoved(AlertEntry alertEntry) {
+        NotificationData.Entry entry = alertEntry.mEntry;
         entry.row.setHeadsUp(false);
-        setEntryPinned(remove, false /* isPinned */);
+        setEntryPinned((HeadsUpEntry) alertEntry, false /* isPinned */);
         for (OnHeadsUpChangedListener listener : mListeners) {
             listener.onHeadsUpStateChanged(entry, false);
         }
-        releaseHeadsUpEntry(remove);
     }
 
     protected void updatePinnedMode() {
@@ -197,7 +158,7 @@
         if (hasPinnedNotification == mHasPinnedNotification) {
             return;
         }
-        if (DEBUG) {
+        if (Log.isLoggable(TAG, Log.VERBOSE)) {
             Log.v(TAG, "Pinned mode changed: " + mHasPinnedNotification + " -> " +
                        hasPinnedNotification);
         }
@@ -211,50 +172,6 @@
     }
 
     /**
-     * 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(@NonNull String key, boolean ignoreEarliestRemovalTime) {
-        if (DEBUG) Log.v(TAG, "removeNotification");
-        releaseImmediately(key);
-        return true;
-    }
-
-    /**
-     * Returns if the given notification is in the Heads Up Notification list or not.
-     */
-    public boolean isHeadsUp(@NonNull String key) {
-        return mHeadsUpEntries.containsKey(key);
-    }
-
-    /**
-     * Pushes any current Heads Up notification down into the shade.
-     */
-    public void releaseAllImmediately() {
-        if (DEBUG) Log.v(TAG, "releaseAllImmediately");
-        Iterator<HeadsUpEntry> iterator = mHeadsUpEntries.values().iterator();
-        while (iterator.hasNext()) {
-            HeadsUpEntry entry = iterator.next();
-            iterator.remove();
-            onHeadsUpEntryRemoved(entry);
-        }
-    }
-
-    /**
-     * Pushes the given Heads Up notification down into the shade.
-     */
-    public void releaseImmediately(@NonNull String key) {
-        HeadsUpEntry headsUpEntry = getHeadsUpEntry(key);
-        if (headsUpEntry == null) {
-            return;
-        }
-        NotificationData.Entry shadeEntry = headsUpEntry.entry;
-        removeHeadsUpEntry(shadeEntry);
-    }
-
-    /**
      * Returns if the given notification is snoozed or not.
      */
     public boolean isSnoozed(@NonNull String packageName) {
@@ -262,7 +179,9 @@
         Long snoozedUntil = mSnoozedPackages.get(key);
         if (snoozedUntil != null) {
             if (snoozedUntil > mClock.currentTimeMillis()) {
-                if (DEBUG) Log.v(TAG, key + " snoozed");
+                if (Log.isLoggable(TAG, Log.VERBOSE)) {
+                    Log.v(TAG, key + " snoozed");
+                }
                 return true;
             }
             mSnoozedPackages.remove(packageName);
@@ -274,9 +193,9 @@
      * Snoozes all current Heads Up Notifications.
      */
     public void snooze() {
-        for (String key : mHeadsUpEntries.keySet()) {
-            HeadsUpEntry entry = mHeadsUpEntries.get(key);
-            String packageName = entry.entry.notification.getPackageName();
+        for (String key : mAlertEntries.keySet()) {
+            AlertEntry entry = getHeadsUpEntry(key);
+            String packageName = entry.mEntry.notification.getPackageName();
             mSnoozedPackages.put(snoozeKey(packageName, mUser),
                     mClock.currentTimeMillis() + mSnoozeLengthMs);
         }
@@ -289,53 +208,27 @@
 
     @Nullable
     protected HeadsUpEntry getHeadsUpEntry(@NonNull String key) {
-        return mHeadsUpEntries.get(key);
+        return (HeadsUpEntry) mAlertEntries.get(key);
     }
 
     /**
-     * Returns the entry of given Heads Up Notification.
-     *
-     * @param key Key of heads up notification
-     */
-    @Nullable
-    public NotificationData.Entry getEntry(@NonNull String key) {
-        HeadsUpEntry entry = mHeadsUpEntries.get(key);
-        return entry != null ? entry.entry : null;
-    }
-
-    /**
-     * Returns the stream of all current Heads Up Notifications.
-     */
-    @NonNull
-    public Stream<NotificationData.Entry> getAllEntries() {
-        return mHeadsUpEntries.values().stream().map(headsUpEntry -> headsUpEntry.entry);
-    }
-
-    /**
-     * Returns the top Heads Up Notification, which appeares to show at first.
+     * Returns the top Heads Up Notification, which appears to show at first.
      */
     @Nullable
     public NotificationData.Entry getTopEntry() {
         HeadsUpEntry topEntry = getTopHeadsUpEntry();
-        return (topEntry != null) ? topEntry.entry : null;
-    }
-
-    /**
-     * Returns if any heads up notification is available or not.
-     */
-    public boolean hasHeadsUpNotifications() {
-        return !mHeadsUpEntries.isEmpty();
+        return (topEntry != null) ? topEntry.mEntry : null;
     }
 
     @Nullable
     protected HeadsUpEntry getTopHeadsUpEntry() {
-        if (mHeadsUpEntries.isEmpty()) {
+        if (mAlertEntries.isEmpty()) {
             return null;
         }
         HeadsUpEntry topEntry = null;
-        for (HeadsUpEntry entry: mHeadsUpEntries.values()) {
+        for (AlertEntry entry: mAlertEntries.values()) {
             if (topEntry == null || entry.compareTo(topEntry) < 0) {
-                topEntry = entry;
+                topEntry = (HeadsUpEntry) entry;
             }
         }
         return topEntry;
@@ -359,8 +252,8 @@
         pw.print("  mSnoozeLengthMs="); pw.println(mSnoozeLengthMs);
         pw.print("  now="); pw.println(mClock.currentTimeMillis());
         pw.print("  mUser="); pw.println(mUser);
-        for (HeadsUpEntry entry: mHeadsUpEntries.values()) {
-            pw.print("  HeadsUpEntry="); pw.println(entry.entry);
+        for (AlertEntry entry: mAlertEntries.values()) {
+            pw.print("  HeadsUpEntry="); pw.println(entry.mEntry);
         }
         int N = mSnoozedPackages.size();
         pw.println("  snoozed packages: " + N);
@@ -378,9 +271,9 @@
     }
 
     private boolean hasPinnedNotificationInternal() {
-        for (String key : mHeadsUpEntries.keySet()) {
-            HeadsUpEntry entry = mHeadsUpEntries.get(key);
-            if (entry.entry.row.isPinned()) {
+        for (String key : mAlertEntries.keySet()) {
+            AlertEntry entry = getHeadsUpEntry(key);
+            if (entry.mEntry.row.isPinned()) {
                 return true;
             }
         }
@@ -392,16 +285,16 @@
      * @param userUnPinned The unpinned action is trigger by user real operation.
      */
     public void unpinAll(boolean userUnPinned) {
-        for (String key : mHeadsUpEntries.keySet()) {
-            HeadsUpEntry entry = mHeadsUpEntries.get(key);
+        for (String key : mAlertEntries.keySet()) {
+            HeadsUpEntry entry = getHeadsUpEntry(key);
             setEntryPinned(entry, false /* isPinned */);
             // maybe it got un sticky
             entry.updateEntry(false /* updatePostTime */);
 
             // when the user unpinned all of HUNs by moving one HUN, all of HUNs should not stay
             // on the screen.
-            if (userUnPinned && entry.entry != null && entry.entry.row != null) {
-                ExpandableNotificationRow row = entry.entry.row;
+            if (userUnPinned && entry.mEntry != null && entry.mEntry.row != null) {
+                ExpandableNotificationRow row = entry.mEntry.row;
                 if (row.mustStayOnScreen()) {
                     row.setHeadsUpIsVisible();
                 }
@@ -425,8 +318,8 @@
      * one should be ranked higher and 0 if they are equal.
      */
     public int compare(@NonNull NotificationData.Entry a, @NonNull NotificationData.Entry b) {
-        HeadsUpEntry aEntry = getHeadsUpEntry(a.key);
-        HeadsUpEntry bEntry = getHeadsUpEntry(b.key);
+        AlertEntry aEntry = getHeadsUpEntry(a.key);
+        AlertEntry bEntry = getHeadsUpEntry(b.key);
         if (aEntry == null || bEntry == null) {
             return aEntry == null ? 1 : -1;
         }
@@ -438,21 +331,18 @@
      * until it's collapsed again.
      */
     public void setExpanded(@NonNull NotificationData.Entry entry, boolean expanded) {
-        HeadsUpManager.HeadsUpEntry headsUpEntry = mHeadsUpEntries.get(entry.key);
+        HeadsUpEntry headsUpEntry = getHeadsUpEntry(entry.key);
         if (headsUpEntry != null && entry.row.isPinned()) {
-            headsUpEntry.expanded(expanded);
+            headsUpEntry.setExpanded(expanded);
         }
     }
 
     @NonNull
-    protected HeadsUpEntry createHeadsUpEntry() {
+    @Override
+    protected HeadsUpEntry createAlertEntry() {
         return new HeadsUpEntry();
     }
 
-    protected void releaseHeadsUpEntry(@NonNull HeadsUpEntry entry) {
-        entry.reset();
-    }
-
     public void onDensityOrFontScaleChanged() {
     }
 
@@ -460,108 +350,58 @@
      * This represents a notification and how long it is in a heads up mode. It also manages its
      * lifecycle automatically when created.
      */
-    protected class HeadsUpEntry implements Comparable<HeadsUpEntry> {
-        @Nullable public NotificationData.Entry entry;
-        public long postTime;
+    protected class HeadsUpEntry extends AlertEntry {
         public boolean remoteInputActive;
-        public long earliestRemovaltime;
-        public boolean expanded;
+        protected boolean expanded;
 
-        @Nullable private Runnable mRemoveHeadsUpRunnable;
-
-        public void setEntry(@Nullable final NotificationData.Entry entry) {
-            setEntry(entry, null);
-        }
-
-        public void setEntry(@Nullable final NotificationData.Entry entry,
-                @Nullable Runnable removeHeadsUpRunnable) {
-            this.entry = entry;
-            this.mRemoveHeadsUpRunnable = removeHeadsUpRunnable;
-
-            // The actual post time will be just after the heads-up really slided in
-            postTime = mClock.currentTimeMillis() + mTouchAcceptanceDelay;
-            updateEntry(true /* updatePostTime */);
-        }
-
-        public void updateEntry(boolean updatePostTime) {
-            if (DEBUG) Log.v(TAG, "updateEntry");
-
-            long currentTime = mClock.currentTimeMillis();
-            earliestRemovaltime = currentTime + mMinimumDisplayTime;
-            if (updatePostTime) {
-                postTime = Math.max(postTime, currentTime);
-            }
-            removeAutoRemovalCallbacks();
-
-            if (!isSticky()) {
-                long finishTime = postTime + mHeadsUpNotificationDecay;
-                long removeDelay = Math.max(finishTime - currentTime, mMinimumDisplayTime);
-                mHandler.postDelayed(mRemoveHeadsUpRunnable, removeDelay);
-            }
-        }
-
-        private boolean isSticky() {
-            return (entry.row.isPinned() && expanded)
-                    || remoteInputActive || hasFullScreenIntent(entry);
+        @Override
+        protected boolean isSticky() {
+            return (mEntry.row.isPinned() && expanded)
+                    || remoteInputActive || hasFullScreenIntent(mEntry);
         }
 
         @Override
-        public int compareTo(@NonNull HeadsUpEntry o) {
-            boolean isPinned = entry.row.isPinned();
-            boolean otherPinned = o.entry.row.isPinned();
+        public int compareTo(@NonNull AlertEntry alertEntry) {
+            HeadsUpEntry headsUpEntry = (HeadsUpEntry) alertEntry;
+            boolean isPinned = mEntry.row.isPinned();
+            boolean otherPinned = headsUpEntry.mEntry.row.isPinned();
             if (isPinned && !otherPinned) {
                 return -1;
             } else if (!isPinned && otherPinned) {
                 return 1;
             }
-            boolean selfFullscreen = hasFullScreenIntent(entry);
-            boolean otherFullscreen = hasFullScreenIntent(o.entry);
+            boolean selfFullscreen = hasFullScreenIntent(mEntry);
+            boolean otherFullscreen = hasFullScreenIntent(headsUpEntry.mEntry);
             if (selfFullscreen && !otherFullscreen) {
                 return -1;
             } else if (!selfFullscreen && otherFullscreen) {
                 return 1;
             }
 
-            if (remoteInputActive && !o.remoteInputActive) {
+            if (remoteInputActive && !headsUpEntry.remoteInputActive) {
                 return -1;
-            } else if (!remoteInputActive && o.remoteInputActive) {
+            } else if (!remoteInputActive && headsUpEntry.remoteInputActive) {
                 return 1;
             }
 
-            return postTime < o.postTime ? 1
-                    : postTime == o.postTime ? entry.key.compareTo(o.entry.key)
-                            : -1;
+            return super.compareTo(headsUpEntry);
         }
 
-        public void expanded(boolean expanded) {
+        public void setExpanded(boolean expanded) {
             this.expanded = expanded;
         }
 
+        @Override
         public void reset() {
-            entry = null;
+            super.reset();
             expanded = false;
             remoteInputActive = false;
-            removeAutoRemovalCallbacks();
-            mRemoveHeadsUpRunnable = null;
         }
 
-        public void removeAutoRemovalCallbacks() {
-            if (mRemoveHeadsUpRunnable != null)
-                mHandler.removeCallbacks(mRemoveHeadsUpRunnable);
-        }
-
-        public void removeAsSoonAsPossible() {
-            if (mRemoveHeadsUpRunnable != null) {
-                removeAutoRemovalCallbacks();
-                mHandler.postDelayed(mRemoveHeadsUpRunnable,
-                        earliestRemovaltime - mClock.currentTimeMillis());
-            }
-        }
-    }
-
-    public static class Clock {
-        public long currentTimeMillis() {
-            return SystemClock.elapsedRealtime();
+        @Override
+        protected long calculatePostTime() {
+            // The actual post time will be just after the heads-up really slided in
+            return super.calculatePostTime() + mTouchAcceptanceDelay;
         }
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/AlertingNotificationManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/AlertingNotificationManagerTest.java
new file mode 100644
index 0000000..f04a115
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/AlertingNotificationManagerTest.java
@@ -0,0 +1,176 @@
+/*
+ * Copyright (C) 2018 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 static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertTrue;
+
+import static org.junit.Assert.assertEquals;
+
+import android.app.ActivityManager;
+import android.app.Notification;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.UserHandle;
+import android.service.notification.StatusBarNotification;
+import android.support.test.filters.SmallTest;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+
+import com.android.systemui.R;
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.statusbar.AlertingNotificationManager;
+import com.android.systemui.statusbar.notification.NotificationData;
+import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
+
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+@TestableLooper.RunWithLooper
+public class AlertingNotificationManagerTest extends SysuiTestCase {
+    @Rule
+    public MockitoRule rule = MockitoJUnit.rule();
+
+    private static final String TEST_PACKAGE_NAME = "test";
+    private static final int TEST_UID = 0;
+
+    private static final int TEST_MINIMUM_DISPLAY_TIME = 200;
+    private static final int TEST_AUTO_DISMISS_TIME = 500;
+    // Number of notifications to use in tests requiring multiple notifications
+    private static final int TEST_NUM_NOTIFICATIONS = 4;
+    private static final int TEST_TIMEOUT_TIME = 10000;
+    private final Runnable TEST_TIMEOUT_RUNNABLE = () -> mTimedOut = true;
+
+    private AlertingNotificationManager mAlertingNotificationManager;
+
+    protected NotificationData.Entry mEntry;
+    protected Handler mTestHandler;
+    private StatusBarNotification mSbn;
+    private boolean mTimedOut = false;
+
+    @Mock protected ExpandableNotificationRow mRow;
+
+    private final class TestableAlertingNotificationManager extends AlertingNotificationManager {
+        private TestableAlertingNotificationManager() {
+            mMinimumDisplayTime = TEST_MINIMUM_DISPLAY_TIME;
+            mAutoDismissNotificationDecay = TEST_AUTO_DISMISS_TIME;
+            mHandler = mTestHandler;
+        }
+
+        @Override
+        protected void onAlertEntryAdded(AlertEntry alertEntry) {}
+
+        @Override
+        protected void onAlertEntryRemoved(AlertEntry alertEntry) {}
+    }
+
+    protected AlertingNotificationManager createAlertingNotificationManager() {
+        return new TestableAlertingNotificationManager();
+    }
+
+    private StatusBarNotification createNewNotification(int id) {
+        Notification.Builder n = new Notification.Builder(mContext, "")
+                .setSmallIcon(R.drawable.ic_person)
+                .setContentTitle("Title")
+                .setContentText("Text");
+        return new StatusBarNotification(
+                TEST_PACKAGE_NAME /* pkg */,
+                TEST_PACKAGE_NAME,
+                id,
+                null /* tag */,
+                TEST_UID,
+                0 /* initialPid */,
+                n.build(),
+                new UserHandle(ActivityManager.getCurrentUser()),
+                null /* overrideGroupKey */,
+                0 /* postTime */);
+    }
+
+    @Before
+    public void setUp() {
+        mTestHandler = Handler.createAsync(Looper.myLooper());
+        mSbn = createNewNotification(0 /* id */);
+        mEntry = new NotificationData.Entry(mSbn);
+        mEntry.row = mRow;
+
+        mAlertingNotificationManager = createAlertingNotificationManager();
+    }
+
+    @Test
+    public void testShowNotification_addsEntry() {
+        mAlertingNotificationManager.showNotification(mEntry);
+
+        assertTrue(mAlertingNotificationManager.contains(mEntry.key));
+        assertTrue(mAlertingNotificationManager.hasNotifications());
+        assertEquals(mEntry, mAlertingNotificationManager.getEntry(mEntry.key));
+    }
+
+    @Test
+    public void testShowNotification_autoDismisses() {
+        mAlertingNotificationManager.showNotification(mEntry);
+        mTestHandler.postDelayed(TEST_TIMEOUT_RUNNABLE, TEST_TIMEOUT_TIME);
+
+        // Wait for remove runnable and then process it immediately
+        TestableLooper.get(this).processMessages(1);
+
+        assertFalse("Test timed out", mTimedOut);
+        assertFalse(mAlertingNotificationManager.contains(mEntry.key));
+    }
+
+    @Test
+    public void testRemoveNotification_removeDeferred() {
+        mAlertingNotificationManager.showNotification(mEntry);
+
+        // Try to remove but defer, since the notification has not been shown long enough.
+        mAlertingNotificationManager.removeNotification(mEntry.key, false /* releaseImmediately */);
+
+        assertTrue(mAlertingNotificationManager.contains(mEntry.key));
+    }
+
+    @Test
+    public void testRemoveNotification_forceRemove() {
+        mAlertingNotificationManager.showNotification(mEntry);
+
+        //Remove forcibly with releaseImmediately = true.
+        mAlertingNotificationManager.removeNotification(mEntry.key, true /* releaseImmediately */);
+
+        assertFalse(mAlertingNotificationManager.contains(mEntry.key));
+    }
+
+    @Test
+    public void testReleaseAllImmediately() {
+        for (int i = 0; i < TEST_NUM_NOTIFICATIONS; i++) {
+            StatusBarNotification sbn = createNewNotification(i);
+            NotificationData.Entry entry = new NotificationData.Entry(sbn);
+            entry.row = mRow;
+            mAlertingNotificationManager.showNotification(entry);
+        }
+
+        mAlertingNotificationManager.releaseAllImmediately();
+
+        assertEquals(0, mAlertingNotificationManager.getAllEntries().count());
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhoneTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhoneTest.java
index 1837909..bdf7cd3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhoneTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhoneTest.java
@@ -16,22 +16,13 @@
 
 package com.android.systemui.statusbar.phone;
 
-import android.app.ActivityManager;
-import android.app.Instrumentation;
-import android.app.Notification;
-import android.os.UserHandle;
 import android.view.View;
-import android.service.notification.StatusBarNotification;
-import android.support.test.InstrumentationRegistry;
 import android.support.test.filters.SmallTest;
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
 
-import com.android.systemui.R;
-import com.android.systemui.SysuiTestCase;
-import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
-import com.android.systemui.statusbar.notification.NotificationData;
-import com.android.systemui.statusbar.StatusBarIconView;
+import com.android.systemui.statusbar.AlertingNotificationManager;
+import com.android.systemui.statusbar.AlertingNotificationManagerTest;
 import com.android.systemui.statusbar.notification.VisualStabilityManager;
 
 import org.junit.Before;
@@ -42,175 +33,54 @@
 import org.mockito.junit.MockitoJUnit;
 import org.mockito.junit.MockitoRule;
 
-import static junit.framework.Assert.assertNotNull;
-import static junit.framework.Assert.assertNull;
 import static junit.framework.Assert.assertTrue;
 import static junit.framework.Assert.assertFalse;
 
-import static org.junit.Assert.assertEquals;
-import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
 @SmallTest
 @RunWith(AndroidTestingRunner.class)
 @TestableLooper.RunWithLooper
-public class HeadsUpManagerPhoneTest extends SysuiTestCase {
+public class HeadsUpManagerPhoneTest extends AlertingNotificationManagerTest {
     @Rule public MockitoRule rule = MockitoJUnit.rule();
 
-    private static final String TEST_PACKAGE_NAME = "test";
-    private static final int TEST_UID = 0;
-
     private HeadsUpManagerPhone mHeadsUpManager;
 
-    private NotificationData.Entry mEntry;
-    private StatusBarNotification mSbn;
-
     @Mock private NotificationGroupManager mGroupManager;
     @Mock private View mStatusBarWindowView;
-    @Mock private StatusBar mBar;
-    @Mock private ExpandableNotificationRow mRow;
     @Mock private VisualStabilityManager mVSManager;
+    @Mock private StatusBar mBar;
+
+    protected AlertingNotificationManager createAlertingNotificationManager() {
+        return mHeadsUpManager;
+    }
 
     @Before
     public void setUp() {
         when(mVSManager.isReorderingAllowed()).thenReturn(true);
-
-        mHeadsUpManager = new HeadsUpManagerPhone(
-                mContext, mStatusBarWindowView, mGroupManager, mBar, mVSManager);
-
-        Notification.Builder n = new Notification.Builder(mContext, "")
-                .setSmallIcon(R.drawable.ic_person)
-                .setContentTitle("Title")
-                .setContentText("Text");
-        mSbn = new StatusBarNotification(TEST_PACKAGE_NAME, TEST_PACKAGE_NAME, 0, null, TEST_UID,
-             0, n.build(), new UserHandle(ActivityManager.getCurrentUser()), null, 0);
-
-        mEntry = new NotificationData.Entry(mSbn);
-        mEntry.row = mRow;
-        mEntry.expandedIcon = mock(StatusBarIconView.class);
+        mHeadsUpManager = new HeadsUpManagerPhone(mContext, mStatusBarWindowView, mGroupManager,
+                mBar, mVSManager);
+        super.setUp();
+        mHeadsUpManager.mHandler = mTestHandler;
     }
 
     @Test
-    public void testBasicOperations() {
-        // Check the initial state.
-        assertNull(mHeadsUpManager.getEntry(mEntry.key));
-        assertNull(mHeadsUpManager.getTopEntry());
-        assertEquals(0, mHeadsUpManager.getAllEntries().count());
-        assertFalse(mHeadsUpManager.hasHeadsUpNotifications());
-
-        // Add a notification.
+    public void testSnooze() {
         mHeadsUpManager.showNotification(mEntry);
 
-        assertEquals(mEntry, mHeadsUpManager.getEntry(mEntry.key));
-        assertEquals(mEntry, mHeadsUpManager.getTopEntry());
-        assertEquals(1, mHeadsUpManager.getAllEntries().count());
-        assertTrue(mHeadsUpManager.hasHeadsUpNotifications());
+        mHeadsUpManager.snooze();
 
-        // Update the notification.
-        mHeadsUpManager.updateNotification(mEntry, false);
-
-        assertEquals(mEntry, mHeadsUpManager.getEntry(mEntry.key));
-        assertEquals(mEntry, mHeadsUpManager.getTopEntry());
-        assertEquals(1, mHeadsUpManager.getAllEntries().count());
-        assertTrue(mHeadsUpManager.hasHeadsUpNotifications());
-
-        // Try to remove but defer, since the notification is currenlt visible on display.
-        mHeadsUpManager.removeNotification(mEntry.key, false /* ignoreEarliestRemovalTime */);
-
-        assertEquals(mEntry, mHeadsUpManager.getEntry(mEntry.key));
-        assertEquals(mEntry, mHeadsUpManager.getTopEntry());
-        assertEquals(1, mHeadsUpManager.getAllEntries().count());
-        assertTrue(mHeadsUpManager.hasHeadsUpNotifications());
-
-        // Remove forcibly with ignoreEarliestRemovalTime = true.
-        mHeadsUpManager.removeNotification(mEntry.key, true /* ignoreEarliestRemovalTime */);
-
-        // Check the initial state.
-        assertNull(mHeadsUpManager.getEntry(mEntry.key));
-        assertNull(mHeadsUpManager.getTopEntry());
-        assertEquals(0, mHeadsUpManager.getAllEntries().count());
-        assertFalse(mHeadsUpManager.hasHeadsUpNotifications());
+        assertTrue(mHeadsUpManager.isSnoozed(mEntry.notification.getPackageName()));
     }
 
     @Test
-    public void testsTimeoutRemoval() {
-        mHeadsUpManager.removeMinimumDisplayTimeForTesting();
-
-        // Check the initial state.
-        assertNull(mHeadsUpManager.getEntry(mEntry.key));
-        assertNull(mHeadsUpManager.getTopEntry());
-        assertEquals(0, mHeadsUpManager.getAllEntries().count());
-        assertFalse(mHeadsUpManager.hasHeadsUpNotifications());
-
-        Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
-
-        // Run the code on the main thready, not to run an async operations.
-        instrumentation.runOnMainSync(() -> {
-            // Add a notification.
-            mHeadsUpManager.showNotification(mEntry);
-
-            // Ensure the head up is visible before timeout.
-            assertNotNull(mHeadsUpManager.getEntry(mEntry.key));
-            assertNotNull(mHeadsUpManager.getTopEntry());
-            assertEquals(1, mHeadsUpManager.getAllEntries().count());
-            assertTrue(mHeadsUpManager.hasHeadsUpNotifications());
-        });
-        // Wait for the async operations, which removes the heads up notification.
-        waitForIdleSync();
-
-        assertNull(mHeadsUpManager.getEntry(mEntry.key));
-        assertNull(mHeadsUpManager.getTopEntry());
-        assertEquals(0, mHeadsUpManager.getAllEntries().count());
-        assertFalse(mHeadsUpManager.hasHeadsUpNotifications());
-    }
-
-    @Test
-    public void releaseImmediately() {
-        // Check the initial state.
-        assertNull(mHeadsUpManager.getEntry(mEntry.key));
-        assertNull(mHeadsUpManager.getTopEntry());
-        assertEquals(0, mHeadsUpManager.getAllEntries().count());
-        assertFalse(mHeadsUpManager.hasHeadsUpNotifications());
-
-        // Add a notification.
+    public void testSwipedOutNotification() {
         mHeadsUpManager.showNotification(mEntry);
+        mHeadsUpManager.addSwipedOutNotification(mEntry.key);
 
-        assertEquals(mEntry, mHeadsUpManager.getEntry(mEntry.key));
-        assertEquals(mEntry, mHeadsUpManager.getTopEntry());
-        assertEquals(1, mHeadsUpManager.getAllEntries().count());
-        assertTrue(mHeadsUpManager.hasHeadsUpNotifications());
+        // Remove should succeed because the notification is swiped out
+        mHeadsUpManager.removeNotification(mEntry.key, false /* releaseImmediately */);
 
-        // Remove but defer, since the notification is visible on display.
-        mHeadsUpManager.releaseImmediately(mEntry.key);
-
-        assertNull(mHeadsUpManager.getEntry(mEntry.key));
-        assertNull(mHeadsUpManager.getTopEntry());
-        assertEquals(0, mHeadsUpManager.getAllEntries().count());
-        assertFalse(mHeadsUpManager.hasHeadsUpNotifications());
-    }
-
-    @Test
-    public void releaseAllImmediately() {
-        // Check the initial state.
-        assertNull(mHeadsUpManager.getEntry(mEntry.key));
-        assertNull(mHeadsUpManager.getTopEntry());
-        assertEquals(0, mHeadsUpManager.getAllEntries().count());
-        assertFalse(mHeadsUpManager.hasHeadsUpNotifications());
-
-        // Add a notification.
-        mHeadsUpManager.showNotification(mEntry);
-
-        assertEquals(mEntry, mHeadsUpManager.getEntry(mEntry.key));
-        assertEquals(mEntry, mHeadsUpManager.getTopEntry());
-        assertEquals(1, mHeadsUpManager.getAllEntries().count());
-        assertTrue(mHeadsUpManager.hasHeadsUpNotifications());
-
-        // Remove but defer, since the notification is visible on display.
-        mHeadsUpManager.releaseAllImmediately();
-
-        assertNull(mHeadsUpManager.getEntry(mEntry.key));
-        assertNull(mHeadsUpManager.getTopEntry());
-        assertEquals(0, mHeadsUpManager.getAllEntries().count());
-        assertFalse(mHeadsUpManager.hasHeadsUpNotifications());
+        assertFalse(mHeadsUpManager.contains(mEntry.key));
     }
 }