diff options
6 files changed, 403 insertions, 153 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationListener.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationListener.java new file mode 100644 index 000000000000..6bcd174adeae --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationListener.java @@ -0,0 +1,129 @@ +/* + * Copyright (C) 2017 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 com.android.systemui.statusbar.RemoteInputController.processForRemoteInput; +import static com.android.systemui.statusbar.phone.StatusBar.DEBUG; +import static com.android.systemui.statusbar.phone.StatusBar.ENABLE_CHILD_NOTIFICATIONS; + +import android.content.ComponentName; +import android.content.Context; +import android.os.RemoteException; +import android.os.UserHandle; +import android.service.notification.StatusBarNotification; +import android.util.Log; + +import com.android.systemui.statusbar.phone.NotificationListenerWithPlugins; + +/** + * This class handles listening to notification updates and passing them along to + * NotificationPresenter to be displayed to the user. + */ +public class NotificationListener extends NotificationListenerWithPlugins { + private static final String TAG = "NotificationListener"; + + private final NotificationPresenter mPresenter; + private final Context mContext; + + public NotificationListener(NotificationPresenter presenter, Context context) { + mPresenter = presenter; + mContext = context; + } + + @Override + public void onListenerConnected() { + if (DEBUG) Log.d(TAG, "onListenerConnected"); + onPluginConnected(); + final StatusBarNotification[] notifications = getActiveNotifications(); + if (notifications == null) { + Log.w(TAG, "onListenerConnected unable to get active notifications."); + return; + } + final RankingMap currentRanking = getCurrentRanking(); + mPresenter.getHandler().post(() -> { + for (StatusBarNotification sbn : notifications) { + mPresenter.addNotification(sbn, currentRanking); + } + }); + } + + @Override + public void onNotificationPosted(final StatusBarNotification sbn, + final RankingMap rankingMap) { + if (DEBUG) Log.d(TAG, "onNotificationPosted: " + sbn); + if (sbn != null && !onPluginNotificationPosted(sbn, rankingMap)) { + mPresenter.getHandler().post(() -> { + processForRemoteInput(sbn.getNotification(), mContext); + String key = sbn.getKey(); + mPresenter.getKeysKeptForRemoteInput().remove(key); + boolean isUpdate = mPresenter.getNotificationData().get(key) != null; + // In case we don't allow child notifications, we ignore children of + // notifications that have a summary, since` we're not going to show them + // anyway. This is true also when the summary is canceled, + // because children are automatically canceled by NoMan in that case. + if (!ENABLE_CHILD_NOTIFICATIONS + && mPresenter.getGroupManager().isChildInGroupWithSummary(sbn)) { + if (DEBUG) { + Log.d(TAG, "Ignoring group child due to existing summary: " + sbn); + } + + // Remove existing notification to avoid stale data. + if (isUpdate) { + mPresenter.removeNotification(key, rankingMap); + } else { + mPresenter.getNotificationData().updateRanking(rankingMap); + } + return; + } + if (isUpdate) { + mPresenter.updateNotification(sbn, rankingMap); + } else { + mPresenter.addNotification(sbn, rankingMap); + } + }); + } + } + + @Override + public void onNotificationRemoved(StatusBarNotification sbn, + final RankingMap rankingMap) { + if (DEBUG) Log.d(TAG, "onNotificationRemoved: " + sbn); + if (sbn != null && !onPluginNotificationRemoved(sbn, rankingMap)) { + final String key = sbn.getKey(); + mPresenter.getHandler().post(() -> mPresenter.removeNotification(key, rankingMap)); + } + } + + @Override + public void onNotificationRankingUpdate(final RankingMap rankingMap) { + if (DEBUG) Log.d(TAG, "onRankingUpdate"); + if (rankingMap != null) { + RankingMap r = onPluginRankingUpdate(rankingMap); + mPresenter.getHandler().post(() -> mPresenter.updateNotificationRanking(r)); + } + } + + public void register() { + try { + registerAsSystemService(mContext, + new ComponentName(mContext.getPackageName(), getClass().getCanonicalName()), + UserHandle.USER_ALL); + } catch (RemoteException e) { + Log.e(TAG, "Unable to register notification listener", e); + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationPresenter.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationPresenter.java index 867088793f8b..4eca2415d5c3 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationPresenter.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationPresenter.java @@ -19,6 +19,8 @@ import android.content.Intent; import android.os.Handler; import android.service.notification.NotificationListenerService; +import java.util.Set; + /** * An abstraction of something that presents notifications, e.g. StatusBar. Contains methods * for both querying the state of the system (some modularised piece of functionality may @@ -26,7 +28,8 @@ import android.service.notification.NotificationListenerService; * for affecting the state of the system (e.g. starting an intent, given that the presenter may * want to perform some action before doing so). */ -public interface NotificationPresenter { +public interface NotificationPresenter extends NotificationUpdateHandler, + NotificationData.Environment { /** * Returns true if the presenter is not visible. For example, it may not be necessary to do @@ -66,12 +69,6 @@ public interface NotificationPresenter { */ void updateMediaMetaData(boolean metaDataChanged, boolean allowEnterAnimation); - // TODO: Create NotificationUpdateHandler and move this method to there. - /** - * Removes a notification. - */ - void removeNotification(String key, NotificationListenerService.RankingMap ranking); - // TODO: Create NotificationEntryManager and move this method to there. /** * Gets the latest ranking map. @@ -84,6 +81,14 @@ public interface NotificationPresenter { void onWorkChallengeChanged(); /** + * Notifications in this set are kept around when they were canceled in response to a remote + * input interaction. This allows us to show what you replied and allows you to continue typing + * into it. + */ + // TODO: Create NotificationEntryManager and move this method to there. + Set<String> getKeysKeptForRemoteInput(); + + /** * Called when the current user changes. * @param newUserId new user id */ diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationUpdateHandler.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationUpdateHandler.java new file mode 100644 index 000000000000..0044194e886c --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationUpdateHandler.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2017 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; +import android.service.notification.StatusBarNotification; + +/** + * Interface for accepting notification updates from {@link NotificationListener}. + */ +public interface NotificationUpdateHandler { + /** + * Add a new notification and update the current notification ranking map. + * + * @param notification Notification to add + * @param ranking RankingMap to update with + */ + void addNotification(StatusBarNotification notification, + NotificationListenerService.RankingMap ranking); + + /** + * Remove a notification and update the current notification ranking map. + * + * @param key Key identifying the notification to remove + * @param ranking RankingMap to update with + */ + void removeNotification(String key, NotificationListenerService.RankingMap ranking); + + /** + * Update a given notification and the current notification ranking map. + * + * @param notification Updated notification + * @param ranking RankingMap to update with + */ + void updateNotification(StatusBarNotification notification, + NotificationListenerService.RankingMap ranking); + + /** + * Update with a new notification ranking map. + * + * @param ranking RankingMap to update with + */ + void updateNotificationRanking(NotificationListenerService.RankingMap ranking); +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/RemoteInputController.java b/packages/SystemUI/src/com/android/systemui/statusbar/RemoteInputController.java index ff6c775ce7ea..97e3d22c6a46 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/RemoteInputController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/RemoteInputController.java @@ -21,17 +21,24 @@ import com.android.systemui.Dependency; import com.android.systemui.statusbar.phone.StatusBarWindowManager; import com.android.systemui.statusbar.policy.RemoteInputView; +import android.app.Notification; +import android.app.RemoteInput; +import android.content.Context; +import android.os.SystemProperties; import android.util.ArrayMap; import android.util.ArraySet; import android.util.Pair; import java.lang.ref.WeakReference; import java.util.ArrayList; +import java.util.List; /** * Keeps track of the currently active {@link RemoteInputView}s. */ public class RemoteInputController { + private static final boolean ENABLE_REMOTE_INPUT = + SystemProperties.getBoolean("debug.enable_remote_input", true); private final ArrayList<Pair<WeakReference<NotificationData.Entry>, Object>> mOpen = new ArrayList<>(); @@ -45,6 +52,53 @@ public class RemoteInputController { } /** + * Adds RemoteInput actions from the WearableExtender; to be removed once more apps support this + * via first-class API. + * + * TODO: Remove once enough apps specify remote inputs on their own. + */ + public static void processForRemoteInput(Notification n, Context context) { + if (!ENABLE_REMOTE_INPUT) { + return; + } + + if (n.extras != null && n.extras.containsKey("android.wearable.EXTENSIONS") && + (n.actions == null || n.actions.length == 0)) { + Notification.Action viableAction = null; + Notification.WearableExtender we = new Notification.WearableExtender(n); + + List<Notification.Action> actions = we.getActions(); + final int numActions = actions.size(); + + for (int i = 0; i < numActions; i++) { + Notification.Action action = actions.get(i); + if (action == null) { + continue; + } + RemoteInput[] remoteInputs = action.getRemoteInputs(); + if (remoteInputs == null) { + continue; + } + for (RemoteInput ri : remoteInputs) { + if (ri.getAllowFreeFormInput()) { + viableAction = action; + break; + } + } + if (viableAction != null) { + break; + } + } + + if (viableAction != null) { + Notification.Builder rebuilder = Notification.Builder.recoverBuilder(context, n); + rebuilder.setActions(viableAction); + rebuilder.build(); // will rewrite n + } + } + } + + /** * Adds a currently active remote input. * * @param entry the entry for which a remote input is now active. 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 c96742368715..80bab7293a8b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java @@ -200,6 +200,7 @@ import com.android.systemui.statusbar.NotificationData; import com.android.systemui.statusbar.NotificationData.Entry; import com.android.systemui.statusbar.NotificationGutsManager; import com.android.systemui.statusbar.NotificationInfo; +import com.android.systemui.statusbar.NotificationListener; import com.android.systemui.statusbar.NotificationLockscreenUserManager; import com.android.systemui.statusbar.NotificationMediaManager; import com.android.systemui.statusbar.NotificationPresenter; @@ -249,6 +250,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.Stack; public class StatusBar extends SystemUI implements DemoMode, @@ -816,14 +818,8 @@ public class StatusBar extends SystemUI implements DemoMode, } // Set up the initial notification state. - try { - mNotificationListener.registerAsSystemService(mContext, - new ComponentName(mContext.getPackageName(), getClass().getCanonicalName()), - UserHandle.USER_ALL); - } catch (RemoteException e) { - Log.e(TAG, "Unable to register notification listener", e); - } - + mNotificationListener = new NotificationListener(this, mContext); + mNotificationListener.register(); if (DEBUG) { Log.d(TAG, String.format( @@ -1516,13 +1512,19 @@ public class StatusBar extends SystemUI implements DemoMode, SystemServicesProxy.getInstance(mContext).awakenDreamsAsync(); } - public void addNotification(StatusBarNotification notification, RankingMap ranking) - throws InflationException { + @Override + public void addNotification(StatusBarNotification notification, RankingMap ranking) { String key = notification.getKey(); if (DEBUG) Log.d(TAG, "addNotification key=" + key); mNotificationData.updateRanking(ranking); - Entry shadeEntry = createNotificationViews(notification); + Entry shadeEntry = null; + try { + shadeEntry = createNotificationViews(notification); + } catch (InflationException e) { + handleInflationException(notification, e); + return; + } boolean isHeadsUped = shouldPeek(shadeEntry); if (!isHeadsUped && notification.getNotification().fullScreenIntent != null) { if (shouldSuppressFullScreenIntent(key)) { @@ -1536,11 +1538,11 @@ public class StatusBar extends SystemUI implements DemoMode, + key); } } else { - // Stop screensaver if the notification has a full-screen intent. + // Stop screensaver if the notification has a fullscreen intent. // (like an incoming phone call) awakenDreams(); - // not immersive & a full-screen alert should be shown + // not immersive & a fullscreen alert should be shown if (DEBUG) Log.d(TAG, "Notification has fullScreenIntent; sending fullScreenIntent"); try { @@ -1617,7 +1619,8 @@ public class StatusBar extends SystemUI implements DemoMode, } } - protected void updateNotificationRanking(RankingMap ranking) { + @Override + public void updateNotificationRanking(RankingMap ranking) { mNotificationData.updateRanking(ranking); updateNotifications(); } @@ -1670,7 +1673,7 @@ public class StatusBar extends SystemUI implements DemoMode, newNotification, sbn.getUser(), sbn.getOverrideGroupKey(), sbn.getPostTime()); boolean updated = false; try { - updateNotification(newSbn, null); + updateNotificationInternal(newSbn, null); updated = true; } catch (InflationException e) { deferRemoval = false; @@ -5705,90 +5708,7 @@ public class StatusBar extends SystemUI implements DemoMode, } }; - private final NotificationListenerWithPlugins mNotificationListener = - new NotificationListenerWithPlugins() { - @Override - public void onListenerConnected() { - if (DEBUG) Log.d(TAG, "onListenerConnected"); - onPluginConnected(); - final StatusBarNotification[] notifications = getActiveNotifications(); - if (notifications == null) { - Log.w(TAG, "onListenerConnected unable to get active notifications."); - return; - } - final RankingMap currentRanking = getCurrentRanking(); - mHandler.post(() -> { - for (StatusBarNotification sbn : notifications) { - try { - addNotification(sbn, currentRanking); - } catch (InflationException e) { - handleInflationException(sbn, e); - } - } - }); - } - - @Override - public void onNotificationPosted(final StatusBarNotification sbn, - final RankingMap rankingMap) { - if (DEBUG) Log.d(TAG, "onNotificationPosted: " + sbn); - if (sbn != null && !onPluginNotificationPosted(sbn, rankingMap)) { - mHandler.post(() -> { - processForRemoteInput(sbn.getNotification()); - String key = sbn.getKey(); - mKeysKeptForRemoteInput.remove(key); - boolean isUpdate = mNotificationData.get(key) != null; - // In case we don't allow child notifications, we ignore children of - // notifications that have a summary, since we're not going to show them - // anyway. This is true also when the summary is canceled, - // because children are automatically canceled by NoMan in that case. - if (!ENABLE_CHILD_NOTIFICATIONS - && mGroupManager.isChildInGroupWithSummary(sbn)) { - if (DEBUG) { - Log.d(TAG, "Ignoring group child due to existing summary: " + sbn); - } - - // Remove existing notification to avoid stale data. - if (isUpdate) { - removeNotification(key, rankingMap); - } else { - mNotificationData.updateRanking(rankingMap); - } - return; - } - try { - if (isUpdate) { - updateNotification(sbn, rankingMap); - } else { - addNotification(sbn, rankingMap); - } - } catch (InflationException e) { - handleInflationException(sbn, e); - } - }); - } - } - - @Override - public void onNotificationRemoved(StatusBarNotification sbn, - final RankingMap rankingMap) { - if (DEBUG) Log.d(TAG, "onNotificationRemoved: " + sbn); - if (sbn != null && !onPluginNotificationRemoved(sbn, rankingMap)) { - final String key = sbn.getKey(); - mHandler.post(() -> removeNotification(key, rankingMap)); - } - } - - @Override - public void onNotificationRankingUpdate(final RankingMap rankingMap) { - if (DEBUG) Log.d(TAG, "onRankingUpdate"); - if (rankingMap != null) { - RankingMap r = onPluginRankingUpdate(rankingMap); - mHandler.post(() -> updateNotificationRanking(r)); - } - } - - }; + protected NotificationListener mNotificationListener; protected void notifyUserAboutHiddenNotifications() { if (0 != Settings.Secure.getInt(mContext.getContentResolver(), @@ -6075,51 +5995,6 @@ public class StatusBar extends SystemUI implements DemoMode, row.updateNotification(entry); } - /** - * Adds RemoteInput actions from the WearableExtender; to be removed once more apps support this - * via first-class API. - * - * TODO: Remove once enough apps specify remote inputs on their own. - */ - private void processForRemoteInput(Notification n) { - if (!ENABLE_REMOTE_INPUT) return; - - if (n.extras != null && n.extras.containsKey("android.wearable.EXTENSIONS") && - (n.actions == null || n.actions.length == 0)) { - Notification.Action viableAction = null; - Notification.WearableExtender we = new Notification.WearableExtender(n); - - List<Notification.Action> actions = we.getActions(); - final int numActions = actions.size(); - - for (int i = 0; i < numActions; i++) { - Notification.Action action = actions.get(i); - if (action == null) { - continue; - } - RemoteInput[] remoteInputs = action.getRemoteInputs(); - if (remoteInputs == null) { - continue; - } - for (RemoteInput ri : remoteInputs) { - if (ri.getAllowFreeFormInput()) { - viableAction = action; - break; - } - } - if (viableAction != null) { - break; - } - } - - if (viableAction != null) { - Notification.Builder rebuilder = Notification.Builder.recoverBuilder(mContext, n); - rebuilder.setActions(viableAction); - rebuilder.build(); // will rewrite n - } - } - } - public void startPendingIntentDismissingKeyguard(final PendingIntent intent) { if (!isDeviceProvisioned()) return; @@ -6508,8 +6383,9 @@ public class StatusBar extends SystemUI implements DemoMode, mScrimController.setNotificationCount(mStackScroller.getNotGoneChildCount()); } - public void updateNotification(StatusBarNotification notification, RankingMap ranking) - throws InflationException { + // TODO: Move this to NotificationEntryManager once it is created. + private void updateNotificationInternal(StatusBarNotification notification, + RankingMap ranking) throws InflationException { if (DEBUG) Log.d(TAG, "updateNotification(" + notification + ")"); final String key = notification.getKey(); @@ -6559,6 +6435,15 @@ public class StatusBar extends SystemUI implements DemoMode, setAreThereNotifications(); } + @Override + public void updateNotification(StatusBarNotification notification, RankingMap ranking) { + try { + updateNotificationInternal(notification, ranking); + } catch (InflationException e) { + handleInflationException(notification, e); + } + } + protected void notifyHeadsUpGoingToSleep() { maybeEscalateHeadsUp(); } @@ -6755,6 +6640,11 @@ public class StatusBar extends SystemUI implements DemoMode, return mLatestRankingMap; } + @Override + public Set<String> getKeysKeptForRemoteInput() { + return mKeysKeptForRemoteInput; + } + private final NotificationInfo.CheckSaveListener mCheckSaveListener = (Runnable saveImportance, StatusBarNotification sbn) -> { // If the user has security enabled, show challenge if the setting is changed. diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationListenerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationListenerTest.java new file mode 100644 index 000000000000..6ecfe3e6be47 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationListenerTest.java @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2017 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.assertTrue; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.Notification; +import android.os.Handler; +import android.os.Looper; +import android.os.UserHandle; +import android.service.notification.NotificationListenerService; +import android.service.notification.StatusBarNotification; +import android.support.test.filters.SmallTest; +import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper; + +import com.android.systemui.SysuiTestCase; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.HashSet; +import java.util.Set; + +@SmallTest +@RunWith(AndroidTestingRunner.class) +@TestableLooper.RunWithLooper +public class NotificationListenerTest extends SysuiTestCase { + private static final String TEST_PACKAGE_NAME = "test"; + private static final int TEST_UID = 0; + + private NotificationPresenter mPresenter; + private Handler mHandler; + private NotificationListener mListener; + private StatusBarNotification mSbn; + private NotificationListenerService.RankingMap mRanking; + private Set<String> mKeysKeptForRemoteInput; + private NotificationData mNotificationData; + + @Before + public void setUp() { + mHandler = new Handler(Looper.getMainLooper()); + mPresenter = mock(NotificationPresenter.class); + mNotificationData = mock(NotificationData.class); + mRanking = mock(NotificationListenerService.RankingMap.class); + mKeysKeptForRemoteInput = new HashSet<>(); + + when(mPresenter.getHandler()).thenReturn(mHandler); + when(mPresenter.getNotificationData()).thenReturn(mNotificationData); + when(mPresenter.getKeysKeptForRemoteInput()).thenReturn(mKeysKeptForRemoteInput); + + mListener = new NotificationListener(mPresenter, mContext); + mSbn = new StatusBarNotification(TEST_PACKAGE_NAME, TEST_PACKAGE_NAME, 0, null, TEST_UID, 0, + new Notification(), UserHandle.CURRENT, null, 0); + } + + @Test + public void testNotificationAddCallsAddNotification() { + mListener.onNotificationPosted(mSbn, mRanking); + waitForIdleSync(mHandler); + verify(mPresenter).addNotification(mSbn, mRanking); + } + + @Test + public void testPostNotificationRemovesKeyKeptForRemoteInput() { + mKeysKeptForRemoteInput.add(mSbn.getKey()); + mListener.onNotificationPosted(mSbn, mRanking); + waitForIdleSync(mHandler); + assertTrue(mKeysKeptForRemoteInput.isEmpty()); + } + + @Test + public void testNotificationUpdateCallsUpdateNotification() { + when(mNotificationData.get(mSbn.getKey())).thenReturn(new NotificationData.Entry(mSbn)); + mListener.onNotificationPosted(mSbn, mRanking); + waitForIdleSync(mHandler); + verify(mPresenter).updateNotification(mSbn, mRanking); + } + + @Test + public void testNotificationRemovalCallsRemoveNotification() { + mListener.onNotificationRemoved(mSbn, mRanking); + waitForIdleSync(mHandler); + verify(mPresenter).removeNotification(mSbn.getKey(), mRanking); + } + + @Test + public void testRankingUpdateCallsNotificationRankingUpdate() { + mListener.onNotificationRankingUpdate(mRanking); + waitForIdleSync(mHandler); + // RankingMap may be modified by plugins. + verify(mPresenter).updateNotificationRanking(any()); + } +} |