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));
}
}