package com.android.systemui.bubbles;
+import static android.service.notification.NotificationListenerService.REASON_APP_CANCEL;
+import static android.service.notification.NotificationListenerService.REASON_APP_CANCEL_ALL;
+import static android.service.notification.NotificationListenerService.REASON_CANCEL;
+import static android.service.notification.NotificationListenerService.REASON_CANCEL_ALL;
import static android.view.Display.DEFAULT_DISPLAY;
import static android.view.Display.INVALID_DISPLAY;
import static android.view.View.INVISIBLE;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.statusbar.IStatusBarService;
-import com.android.internal.statusbar.NotificationVisibility;
import com.android.systemui.Dependency;
import com.android.systemui.R;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.shared.system.ActivityManagerWrapper;
import com.android.systemui.shared.system.TaskStackChangeListener;
import com.android.systemui.shared.system.WindowManagerWrapper;
+import com.android.systemui.statusbar.NotificationRemoveInterceptor;
import com.android.systemui.statusbar.notification.NotificationEntryListener;
import com.android.systemui.statusbar.notification.NotificationEntryManager;
import com.android.systemui.statusbar.notification.NotificationInterruptionStateProvider;
mNotificationEntryManager = Dependency.get(NotificationEntryManager.class);
mNotificationEntryManager.addNotificationEntryListener(mEntryListener);
+ mNotificationEntryManager.setNotificationRemoveInterceptor(mRemoveInterceptor);
mStatusBarWindowController = statusBarWindowController;
mStatusBarStateListener = new StatusBarStateListener();
}
@SuppressWarnings("FieldCanBeLocal")
+ private final NotificationRemoveInterceptor mRemoveInterceptor =
+ new NotificationRemoveInterceptor() {
+ @Override
+ public boolean onNotificationRemoveRequested(String key, int reason) {
+ if (!mBubbleData.hasBubbleWithKey(key)) {
+ return false;
+ }
+ NotificationEntry entry = mBubbleData.getBubbleWithKey(key).entry;
+
+ final boolean isClearAll = reason == REASON_CANCEL_ALL;
+ final boolean isUserDimiss = reason == REASON_CANCEL;
+ final boolean isAppCancel = reason == REASON_APP_CANCEL
+ || reason == REASON_APP_CANCEL_ALL;
+
+ // Need to check for !appCancel here because the notification may have
+ // previously been dismissed & entry.isRowDismissed would still be true
+ boolean userRemovedNotif = (entry.isRowDismissed() && !isAppCancel)
+ || isClearAll || isUserDimiss;
+
+ // The bubble notification sticks around in the data as long as the bubble is
+ // not dismissed and the app hasn't cancelled the notification.
+ boolean bubbleExtended = entry.isBubble() && !entry.isBubbleDismissed()
+ && userRemovedNotif;
+ if (bubbleExtended) {
+ entry.setShowInShadeWhenBubble(false);
+ if (mStackView != null) {
+ mStackView.updateDotVisibility(entry.key);
+ }
+ mNotificationEntryManager.updateNotifications();
+ return true;
+ } else if (!userRemovedNotif && !entry.isBubbleDismissed()) {
+ // This wasn't a user removal so we should remove the bubble as well
+ mBubbleData.notificationEntryRemoved(entry, DISMISS_NOTIF_CANCEL);
+ return false;
+ }
+ return false;
+ }
+ };
+
+ @SuppressWarnings("FieldCanBeLocal")
private final NotificationEntryListener mEntryListener = new NotificationEntryListener() {
@Override
public void onPendingEntryAdded(NotificationEntry entry) {
return;
}
if (mNotificationInterruptionStateProvider.shouldBubbleUp(entry)) {
- // TODO: handle group summaries?
updateShowInShadeForSuppressNotification(entry);
}
}
updateBubble(entry);
}
}
-
- @Override
- public void onEntryRemoved(NotificationEntry entry,
- @Nullable NotificationVisibility visibility,
- boolean removedByUser) {
- if (!areBubblesEnabled(mContext)) {
- return;
- }
- entry.setShowInShadeWhenBubble(false);
- if (mStackView != null) {
- mStackView.updateDotVisibility(entry.key);
- }
- if (!removedByUser) {
- // This was a cancel so we should remove the bubble
- removeBubble(entry.key, DISMISS_NOTIF_CANCEL);
- }
- }
};
@SuppressWarnings("FieldCanBeLocal")
}
@Override
- public void onBubbleRemoved(Bubble bubble, int reason) {
+ public void onBubbleRemoved(Bubble bubble, @DismissReason int reason) {
if (mStackView != null) {
mStackView.removeBubble(bubble);
}
- if (!bubble.entry.showInShadeWhenBubble()) {
- // The notification is gone & bubble is gone, time to actually remove it
- mNotificationEntryManager.performRemoveNotification(bubble.entry.notification);
+ if (!mBubbleData.hasBubbleWithKey(bubble.getKey())
+ && !bubble.entry.showInShadeWhenBubble()) {
+ // The bubble is gone & the notification is gone, time to actually remove it
+ mNotificationEntryManager.performRemoveNotification(bubble.entry.notification,
+ 0 /* reason */);
} else {
// The notification is still in the shade but we've removed the bubble so
// lets make sure NoMan knows it's not a bubble anymore
// Remove existing notification to avoid stale data.
if (isUpdate) {
- mEntryManager.removeNotification(key, rankingMap);
+ mEntryManager.removeNotification(key, rankingMap, 0 /* reason */);
} else {
mEntryManager.getNotificationData()
.updateRanking(rankingMap);
}
@Override
- public void onNotificationRemoved(StatusBarNotification sbn,
- final RankingMap rankingMap) {
- if (DEBUG) Log.d(TAG, "onNotificationRemoved: " + sbn);
+ public void onNotificationRemoved(StatusBarNotification sbn, RankingMap rankingMap,
+ int reason) {
+ if (DEBUG) Log.d(TAG, "onNotificationRemoved: " + sbn + " reason: " + reason);
if (sbn != null && !onPluginNotificationRemoved(sbn, rankingMap)) {
final String key = sbn.getKey();
Dependency.get(Dependency.MAIN_HANDLER).post(() -> {
- mEntryManager.removeNotification(key, rankingMap);
+ mEntryManager.removeNotification(key, rankingMap, reason);
});
}
}
@Override
+ public void onNotificationRemoved(StatusBarNotification sbn, RankingMap rankingMap) {
+ onNotificationRemoved(sbn, rankingMap, 0 /* reason */);
+ }
+
+ @Override
public void onNotificationRankingUpdate(final RankingMap rankingMap) {
if (DEBUG) Log.d(TAG, "onRankingUpdate");
if (rankingMap != null) {
--- /dev/null
+/*
+ * Copyright (C) 2019 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.service.notification.NotificationListenerService;
+
+/**
+ * Interface for anything that may need to prevent notifications from being removed. This is
+ * similar to a {@link NotificationLifetimeExtender} in the sense that it extends the life of
+ * a notification by preventing the removal, however, unlike the extender, the remove interceptor
+ * gets first pick at intercepting any type of removal -- the life time extender is unable to
+ * extend the life of a user dismissed or force removed notification.
+ */
+public interface NotificationRemoveInterceptor {
+
+ /**
+ * Called when a notification has been removed.
+ *
+ * @param key the entry key of the notification being removed.
+ * @param removeReason why the notification is being removed, e.g.
+ * {@link NotificationListenerService#REASON_CANCEL} or 0 if unknown.
+ *
+ * @return true if the removal should be ignored, false otherwise.
+ */
+ boolean onNotificationRemoveRequested(String key, int removeReason);
+}
*
* @param key Key identifying the notification to remove
* @param ranking RankingMap to update with
+ * @param reason why the notification is being removed, e.g.
+ * {@link NotificationListenerService#REASON_CANCEL}.
*/
- void removeNotification(String key, NotificationListenerService.RankingMap ranking);
+ void removeNotification(String key, NotificationListenerService.RankingMap ranking, int reason);
/**
* Update a given notification and the current notification ranking map.
*/
package com.android.systemui.statusbar.notification;
+import static android.service.notification.NotificationListenerService.REASON_CANCEL;
+import static android.service.notification.NotificationListenerService.REASON_ERROR;
+
import android.annotation.Nullable;
import android.app.Notification;
import android.content.Context;
import com.android.systemui.statusbar.NotificationLifetimeExtender;
import com.android.systemui.statusbar.NotificationPresenter;
import com.android.systemui.statusbar.NotificationRemoteInputManager;
+import com.android.systemui.statusbar.NotificationRemoveInterceptor;
import com.android.systemui.statusbar.NotificationUiAdjustment;
import com.android.systemui.statusbar.NotificationUpdateHandler;
import com.android.systemui.statusbar.notification.collection.NotificationData;
final ArrayList<NotificationLifetimeExtender> mNotificationLifetimeExtenders
= new ArrayList<>();
private final List<NotificationEntryListener> mNotificationEntryListeners = new ArrayList<>();
+ private NotificationRemoveInterceptor mRemoveInterceptor;
@Override
public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
mNotificationEntryListeners.add(listener);
}
+ /** Sets the {@link NotificationRemoveInterceptor}. */
+ public void setNotificationRemoveInterceptor(NotificationRemoveInterceptor interceptor) {
+ mRemoveInterceptor = interceptor;
+ }
+
/**
* Our dependencies can have cyclic references, so some need to be lazy
*/
/** Adds a {@link NotificationLifetimeExtender}. */
public void addNotificationLifetimeExtender(NotificationLifetimeExtender extender) {
mNotificationLifetimeExtenders.add(extender);
- extender.setCallback(key -> removeNotification(key, mLatestRankingMap));
+ extender.setCallback(key -> removeNotification(key, mLatestRankingMap, 0));
}
public NotificationData getNotificationData() {
updateNotifications();
}
- public void performRemoveNotification(StatusBarNotification n) {
+ /**
+ * Requests a notification to be removed.
+ *
+ * @param n the notification to remove.
+ * @param reason why it is being removed e.g. {@link NotificationListenerService#REASON_CANCEL},
+ * or 0 if unknown.
+ */
+ public void performRemoveNotification(StatusBarNotification n, int reason) {
final NotificationVisibility nv = obtainVisibility(n.getKey());
removeNotificationInternal(
- n.getKey(), null, nv, false /* forceRemove */, true /* removedByUser */);
+ n.getKey(), null, nv, false /* forceRemove */, true /* removedByUser */,
+ reason);
}
private NotificationVisibility obtainVisibility(String key) {
@Override
public void handleInflationException(StatusBarNotification n, Exception e) {
removeNotificationInternal(
- n.getKey(), null, null, true /* forceRemove */, false /* removedByUser */);
+ n.getKey(), null, null, true /* forceRemove */, false /* removedByUser */,
+ REASON_ERROR);
for (NotificationEntryListener listener : mNotificationEntryListeners) {
listener.onInflationError(n, e);
}
}
@Override
- public void removeNotification(String key, NotificationListenerService.RankingMap ranking) {
+ public void removeNotification(String key, NotificationListenerService.RankingMap ranking,
+ int reason) {
removeNotificationInternal(key, ranking, obtainVisibility(key), false /* forceRemove */,
- false /* removedByUser */);
+ false /* removedByUser */, reason);
}
private void removeNotificationInternal(
@Nullable NotificationListenerService.RankingMap ranking,
@Nullable NotificationVisibility visibility,
boolean forceRemove,
- boolean removedByUser) {
+ boolean removedByUser,
+ int reason) {
+
+ if (mRemoveInterceptor != null
+ && mRemoveInterceptor.onNotificationRemoveRequested(key, reason)) {
+ // Remove intercepted; skip
+ return;
+ }
+
final NotificationEntry entry = mNotificationData.get(key);
abortExistingInflation(key);
Dependency.get(LeakDetector.class).trackInstance(entry);
// Construct the expanded view.
- requireBinder().inflateViews(entry, () -> performRemoveNotification(notification));
+ requireBinder().inflateViews(entry, () -> performRemoveNotification(notification,
+ REASON_CANCEL));
abortExistingInflation(key);
listener.onPreEntryUpdated(entry);
}
- requireBinder().inflateViews(entry, () -> performRemoveNotification(notification));
+ requireBinder().inflateViews(entry, () -> performRemoveNotification(notification,
+ REASON_CANCEL));
updateNotifications();
if (DEBUG) {
*/
public boolean showInShadeWhenBubble() {
// We always show it in the shade if non-clearable
- return !isClearable() || mShowInShadeWhenBubble;
+ return !isRowDismissed() && (!isClearable() || mShowInShadeWhenBubble);
}
/**
}
}
+ // TODO: This method has side effects, it is NOT just logging that a notification
+ // was cleared, it also actually removes the notification
private void logNotificationClear(String key, StatusBarNotification notification,
NotificationVisibility nv) {
final String pkg = notification.getPackageName();
import android.os.Bundle;
import android.os.ServiceManager;
import android.provider.Settings;
+import android.service.notification.NotificationListenerService;
import android.service.notification.StatusBarNotification;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
setDismissAllInProgress(false);
for (ExpandableNotificationRow rowToRemove : viewsToRemove) {
if (canChildBeDismissed(rowToRemove)) {
- mEntryManager.removeNotification(rowToRemove.getEntry().key, null);
+ mEntryManager.removeNotification(rowToRemove.getEntry().key, null /* ranking */,
+ NotificationListenerService.REASON_CANCEL_ALL);
} else {
rowToRemove.resetTranslation();
}
package com.android.systemui.statusbar.phone;
+import static android.service.notification.NotificationListenerService.REASON_CLICK;
+
import static com.android.systemui.statusbar.phone.StatusBar.getActivityOptions;
import android.app.ActivityManager;
// We have to post it to the UI thread for synchronization
mMainThreadHandler.post(() -> {
Runnable removeRunnable =
- () -> mEntryManager.performRemoveNotification(notification);
+ () -> mEntryManager.performRemoveNotification(notification, REASON_CLICK);
if (mPresenter.isCollapsing()) {
// To avoid lags we're only performing the remove
// after the shade was collapsed
import static android.app.Notification.FLAG_BUBBLE;
import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
+import static android.service.notification.NotificationListenerService.REASON_APP_CANCEL;
+import static android.service.notification.NotificationListenerService.REASON_CANCEL;
+import static android.service.notification.NotificationListenerService.REASON_CANCEL_ALL;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import com.android.systemui.R;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.statusbar.NotificationPresenter;
+import com.android.systemui.statusbar.NotificationRemoveInterceptor;
import com.android.systemui.statusbar.NotificationTestHelper;
import com.android.systemui.statusbar.notification.NotificationEntryListener;
import com.android.systemui.statusbar.notification.NotificationEntryManager;
private FrameLayout mStatusBarView;
@Captor
private ArgumentCaptor<NotificationEntryListener> mEntryListenerCaptor;
+ @Captor
+ private ArgumentCaptor<NotificationRemoveInterceptor> mRemoveInterceptorCaptor;
private TestableBubbleController mBubbleController;
private StatusBarWindowController mStatusBarWindowController;
private NotificationEntryListener mEntryListener;
+ private NotificationRemoveInterceptor mRemoveInterceptor;
private NotificationTestHelper mNotificationTestHelper;
private ExpandableNotificationRow mRow;
verify(mNotificationEntryManager, atLeastOnce())
.addNotificationEntryListener(mEntryListenerCaptor.capture());
mEntryListener = mEntryListenerCaptor.getValue();
+ // And the remove interceptor
+ verify(mNotificationEntryManager, atLeastOnce())
+ .setNotificationRemoveInterceptor(mRemoveInterceptorCaptor.capture());
+ mRemoveInterceptor = mRemoveInterceptorCaptor.getValue();
}
@Test
}
@Test
+ public void testRemoveBubble_withDismissedNotif() {
+ mEntryListener.onPendingEntryAdded(mRow.getEntry());
+ mBubbleController.updateBubble(mRow.getEntry());
+
+ assertTrue(mBubbleController.hasBubbles());
+ assertTrue(mRow.getEntry().showInShadeWhenBubble());
+
+ // Make it look like dismissed notif
+ mRow.getEntry().setShowInShadeWhenBubble(false);
+
+ // Now remove the bubble
+ mBubbleController.removeBubble(mRow.getEntry().key, BubbleController.DISMISS_USER_GESTURE);
+
+ // Since the notif is dismissed, once the bubble is removed, performRemoveNotification gets
+ // called to really remove the notif
+ verify(mNotificationEntryManager, times(1)).performRemoveNotification(
+ mRow.getEntry().notification, 0);
+ assertFalse(mBubbleController.hasBubbles());
+ }
+
+ @Test
public void testDismissStack() {
mBubbleController.updateBubble(mRow.getEntry());
verify(mNotificationEntryManager, times(1)).updateNotifications();
mBubbleController.updateBubble(mRow.getEntry());
// Simulate notification cancellation.
- mEntryListener.onEntryRemoved(mRow.getEntry(), null /* notificationVisibility (unused) */,
- false /* removedbyUser */);
+ mRemoveInterceptor.onNotificationRemoveRequested(mRow.getEntry().key, REASON_APP_CANCEL);
mBubbleController.expandStackAndSelectBubble(key);
}
verify(mDeleteIntent, never()).send();
}
+ @Test
+ public void testRemoveBubble_succeeds_appCancel() {
+ mEntryListener.onPendingEntryAdded(mRow.getEntry());
+ mBubbleController.updateBubble(mRow.getEntry());
+
+ assertTrue(mBubbleController.hasBubbles());
+
+ boolean intercepted = mRemoveInterceptor.onNotificationRemoveRequested(
+ mRow.getEntry().key, REASON_APP_CANCEL);
+
+ // Cancels always remove so no need to intercept
+ assertFalse(intercepted);
+ assertFalse(mBubbleController.hasBubbles());
+ }
+
+ @Test
+ public void removeBubble_fails_clearAll() {
+ mEntryListener.onPendingEntryAdded(mRow.getEntry());
+ mBubbleController.updateBubble(mRow.getEntry());
+
+ assertTrue(mBubbleController.hasBubbles());
+ assertTrue(mRow.getEntry().showInShadeWhenBubble());
+
+ boolean intercepted = mRemoveInterceptor.onNotificationRemoveRequested(
+ mRow.getEntry().key, REASON_CANCEL_ALL);
+
+ // Intercept!
+ assertTrue(intercepted);
+ // Should update show in shade state
+ assertFalse(mRow.getEntry().showInShadeWhenBubble());
+
+ verify(mNotificationEntryManager, never()).performRemoveNotification(
+ any(), anyInt());
+ assertTrue(mBubbleController.hasBubbles());
+ }
+
+ @Test
+ public void removeBubble_fails_userDismissNotif() {
+ mEntryListener.onPendingEntryAdded(mRow.getEntry());
+ mBubbleController.updateBubble(mRow.getEntry());
+
+ assertTrue(mBubbleController.hasBubbles());
+ assertTrue(mRow.getEntry().showInShadeWhenBubble());
+
+ boolean intercepted = mRemoveInterceptor.onNotificationRemoveRequested(
+ mRow.getEntry().key, REASON_CANCEL);
+
+ // Intercept!
+ assertTrue(intercepted);
+ // Should update show in shade state
+ assertFalse(mRow.getEntry().showInShadeWhenBubble());
+
+ verify(mNotificationEntryManager, never()).performRemoveNotification(
+ any(), anyInt());
+ assertTrue(mBubbleController.hasBubbles());
+ }
+
+ @Test
+ public void removeBubble_succeeds_userDismissBubble_userDimissNotif() {
+ mEntryListener.onPendingEntryAdded(mRow.getEntry());
+ mBubbleController.updateBubble(mRow.getEntry());
+
+ assertTrue(mBubbleController.hasBubbles());
+ assertTrue(mRow.getEntry().showInShadeWhenBubble());
+
+ // Dismiss the bubble
+ mBubbleController.removeBubble(mRow.getEntry().key, BubbleController.DISMISS_USER_GESTURE);
+ assertFalse(mBubbleController.hasBubbles());
+
+ // Dismiss the notification
+ boolean intercepted = mRemoveInterceptor.onNotificationRemoveRequested(
+ mRow.getEntry().key, REASON_CANCEL);
+
+ // It's no longer a bubble so we shouldn't intercept
+ assertFalse(intercepted);
+ }
+
static class TestableBubbleController extends BubbleController {
// Let's assume surfaces can be synchronized immediately.
TestableBubbleController(Context context,
package com.android.systemui.statusbar;
import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public void testNotificationRemovalCallsRemoveNotification() {
mListener.onNotificationRemoved(mSbn, mRanking);
TestableLooper.get(this).processAllMessages();
- verify(mEntryManager).removeNotification(mSbn.getKey(), mRanking);
+ verify(mEntryManager).removeNotification(eq(mSbn.getKey()), eq(mRanking), anyInt());
}
@Test
package com.android.systemui.statusbar.notification;
+import static android.service.notification.NotificationListenerService.REASON_CANCEL;
+
import static junit.framework.Assert.assertNotNull;
import static junit.framework.Assert.assertNull;
import static junit.framework.Assert.assertTrue;
import static org.junit.Assert.assertEquals;
import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
import androidx.test.filters.SmallTest;
import com.android.internal.logging.MetricsLogger;
+import com.android.internal.statusbar.NotificationVisibility;
import com.android.systemui.Dependency;
import com.android.systemui.ForegroundServiceController;
import com.android.systemui.InitController;
import com.android.systemui.statusbar.NotificationLockscreenUserManager;
import com.android.systemui.statusbar.NotificationPresenter;
import com.android.systemui.statusbar.NotificationRemoteInputManager;
+import com.android.systemui.statusbar.NotificationRemoveInterceptor;
import com.android.systemui.statusbar.RemoteInputController;
import com.android.systemui.statusbar.SmartReplyController;
import com.android.systemui.statusbar.StatusBarIconView;
@Mock private ExpandableNotificationRow mRow;
@Mock private NotificationListContainer mListContainer;
@Mock private NotificationEntryListener mEntryListener;
+ @Mock private NotificationRemoveInterceptor mRemoveInterceptor;
@Mock private NotificationRowBinderImpl.BindRowCallback mBindCallback;
@Mock private HeadsUpManager mHeadsUpManager;
@Mock private NotificationListenerService.RankingMap mRankingMap;
Dependency.get(InitController.class).executePostInitTasks();
mEntryManager.setUpWithPresenter(mPresenter, mListContainer, mHeadsUpManager);
mEntryManager.addNotificationEntryListener(mEntryListener);
+ mEntryManager.setNotificationRemoveInterceptor(mRemoveInterceptor);
NotificationRowBinderImpl notificationRowBinder =
new NotificationRowBinderImpl(mContext, true /* allowLongPress */);
mEntry.setRow(mRow);
mEntryManager.getNotificationData().add(mEntry);
- mEntryManager.removeNotification(mSbn.getKey(), mRankingMap);
+ mEntryManager.removeNotification(mSbn.getKey(), mRankingMap, 0 /* reason */);
verify(mEntryListener, never()).onInflationError(any(), any());
public void testRemoveNotification_onEntryRemoveNotFiredIfEntryDoesntExist() {
com.android.systemui.util.Assert.isNotMainThread();
- mEntryManager.removeNotification("not_a_real_key", mRankingMap);
+ mEntryManager.removeNotification("not_a_real_key", mRankingMap, 0 /* reason */);
verify(mEntryListener, never()).onEntryRemoved(
eq(mEntry), any(), eq(false) /* removedByUser */);
mEntryManager.setRowBinder(mMockedRowBinder);
mEntryManager.addNotification(mSbn, mRankingMap);
- mEntryManager.removeNotification(mSbn.getKey(), mRankingMap);
+ mEntryManager.removeNotification(mSbn.getKey(), mRankingMap, 0 /* reason */);
verify(mEntryListener, never()).onEntryRemoved(
eq(mEntry), any(), eq(false /* removedByUser */));
mEntryManager.addNotificationLifetimeExtender(extender);
// WHEN the notification is removed
- mEntryManager.removeNotification(mEntry.key, mRankingMap);
+ mEntryManager.removeNotification(mEntry.key, mRankingMap, 0 /* reason */);
// THEN the extender is asked to manage the lifetime
verify(extender).setShouldManageLifetime(mEntry, true);
mEntryManager.getNotificationData().add(mEntry);
final FakeNotificationLifetimeExtender extender = new FakeNotificationLifetimeExtender();
mEntryManager.addNotificationLifetimeExtender(extender);
- mEntryManager.removeNotification(mEntry.key, mRankingMap);
+ mEntryManager.removeNotification(mEntry.key, mRankingMap, 0 /* reason */);
assertTrue(extender.isManaging(mEntry.key));
// WHEN the extender finishes its extension
NotificationLifetimeExtender extender = mock(NotificationLifetimeExtender.class);
when(extender.shouldExtendLifetime(mEntry)).thenReturn(true);
mEntryManager.addNotificationLifetimeExtender(extender);
- mEntryManager.removeNotification(mEntry.key, mRankingMap);
+ mEntryManager.removeNotification(mEntry.key, mRankingMap, 0 /* reason */);
// WHEN the notification is updated
mEntryManager.updateNotification(mEntry.notification, mRankingMap);
mEntryManager.addNotificationLifetimeExtender(extender2);
// GIVEN a notification was lifetime-extended and extender2 is managing it
- mEntryManager.removeNotification(mEntry.key, mRankingMap);
+ mEntryManager.removeNotification(mEntry.key, mRankingMap, 0 /* reason */);
verify(extender1, never()).setShouldManageLifetime(mEntry, true);
verify(extender2).setShouldManageLifetime(mEntry, true);
// WHEN the extender1 changes its mind and wants to extend the lifetime of the notif
when(extender1.shouldExtendLifetime(mEntry)).thenReturn(true);
- mEntryManager.removeNotification(mEntry.key, mRankingMap);
+ mEntryManager.removeNotification(mEntry.key, mRankingMap, 0 /* reason */);
// THEN extender2 stops managing the notif and extender1 starts managing it
verify(extender1).setShouldManageLifetime(mEntry, true);
@Test
public void testPerformRemoveNotification_removedEntry() {
mEntryManager.getNotificationData().remove(mSbn.getKey(), null /* ranking */);
- mEntryManager.performRemoveNotification(mSbn);
+ mEntryManager.performRemoveNotification(mSbn, REASON_CANCEL);
+ }
+
+ @Test
+ public void testRemoveInterceptor_interceptsDontGetRemoved() {
+ // GIVEN an entry manager with a notification
+ mEntryManager.setRowBinder(mMockedRowBinder);
+ mEntryManager.getNotificationData().add(mEntry);
+
+ // GIVEN interceptor that intercepts that entry
+ when(mRemoveInterceptor.onNotificationRemoveRequested(eq(mEntry.key), anyInt()))
+ .thenReturn(true);
+
+ // WHEN the notification is removed
+ mEntryManager.removeNotification(mEntry.key, mRankingMap, 0 /* reason */);
+
+ // THEN the interceptor intercepts & the entry is not removed & no listeners are called
+ assertNotNull(mEntryManager.getNotificationData().get(mEntry.key));
+ verify(mEntryListener, never()).onEntryRemoved(eq(mEntry),
+ any(NotificationVisibility.class), anyBoolean());
+ }
+
+ @Test
+ public void testRemoveInterceptor_notInterceptedGetsRemoved() {
+ // GIVEN an entry manager with a notification
+ mEntryManager.setRowBinder(mMockedRowBinder);
+ mEntryManager.getNotificationData().add(mEntry);
+
+ // GIVEN interceptor that doesn't intercept
+ when(mRemoveInterceptor.onNotificationRemoveRequested(eq(mEntry.key), anyInt()))
+ .thenReturn(false);
+
+ // WHEN the notification is removed
+ mEntryManager.removeNotification(mEntry.key, mRankingMap, 0 /* reason */);
+
+ // THEN the interceptor intercepts & the entry is not removed & no listeners are called
+ assertNull(mEntryManager.getNotificationData().get(mEntry.key));
+ verify(mEntryListener, atLeastOnce()).onEntryRemoved(eq(mEntry),
+ any(NotificationVisibility.class), anyBoolean());
}
private Notification.Action createAction() {
package com.android.systemui.statusbar.phone;
+import static android.service.notification.NotificationListenerService.REASON_CLICK;
+
import static org.mockito.AdditionalAnswers.answerVoid;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
eq(sbn.getKey()), any(NotificationVisibility.class));
// Notification is removed due to FLAG_AUTO_CANCEL
- verify(mEntryManager).performRemoveNotification(eq(sbn));
+ verify(mEntryManager).performRemoveNotification(eq(sbn), eq(REASON_CLICK));
}
@Test
verifyZeroInteractions(mContentIntent);
// Notification should not be cancelled.
- verify(mEntryManager, never()).performRemoveNotification(eq(sbn));
+ verify(mEntryManager, never()).performRemoveNotification(eq(sbn), anyInt());
}
@Test
verifyZeroInteractions(mContentIntent);
// Notification should not be cancelled.
- verify(mEntryManager, never()).performRemoveNotification(eq(sbn));
+ verify(mEntryManager, never()).performRemoveNotification(eq(sbn), anyInt());
}
@Test
verifyNoMoreInteractions(mContentIntent);
// Notification should not be cancelled.
- verify(mEntryManager, never()).performRemoveNotification(eq(sbn));
+ verify(mEntryManager, never()).performRemoveNotification(eq(sbn), anyInt());
}
}