diff options
10 files changed, 661 insertions, 159 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java index ec3285f2b241..6d4b13ccf494 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java @@ -44,12 +44,16 @@ import static java.util.Objects.requireNonNull; import android.annotation.IntDef; import android.annotation.MainThread; import android.annotation.Nullable; +import android.annotation.UserIdInt; import android.app.Notification; import android.os.RemoteException; +import android.os.UserHandle; +import android.service.notification.NotificationListenerService; import android.service.notification.NotificationListenerService.Ranking; import android.service.notification.NotificationListenerService.RankingMap; import android.service.notification.StatusBarNotification; import android.util.ArrayMap; +import android.util.Pair; import androidx.annotation.NonNull; @@ -191,59 +195,121 @@ public class NotifCollection implements Dumpable { } /** - * Dismiss a notification on behalf of the user. + * Dismisses multiple notifications on behalf of the user. */ - public void dismissNotification(NotificationEntry entry, @NonNull DismissedByUserStats stats) { + public void dismissNotifications( + List<Pair<NotificationEntry, DismissedByUserStats>> entriesToDismiss) { Assert.isMainThread(); - requireNonNull(stats); checkForReentrantCall(); - if (entry != mNotificationSet.get(entry.getKey())) { - throw new IllegalStateException("Invalid entry: " + entry.getKey()); - } + final List<NotificationEntry> entriesToLocallyDismiss = new ArrayList<>(); + for (int i = 0; i < entriesToDismiss.size(); i++) { + NotificationEntry entry = entriesToDismiss.get(i).first; + DismissedByUserStats stats = entriesToDismiss.get(i).second; - if (entry.getDismissState() == DISMISSED) { - return; - } + requireNonNull(stats); + if (entry != mNotificationSet.get(entry.getKey())) { + throw new IllegalStateException("Invalid entry: " + entry.getKey()); + } - updateDismissInterceptors(entry); - if (isDismissIntercepted(entry)) { - mLogger.logNotifDismissedIntercepted(entry.getKey()); - return; + if (entry.getDismissState() == DISMISSED) { + continue; + } + + updateDismissInterceptors(entry); + if (isDismissIntercepted(entry)) { + mLogger.logNotifDismissedIntercepted(entry.getKey()); + continue; + } + + entriesToLocallyDismiss.add(entry); + if (!isCanceled(entry)) { + // send message to system server if this notification hasn't already been cancelled + try { + mStatusBarService.onNotificationClear( + entry.getSbn().getPackageName(), + entry.getSbn().getTag(), + entry.getSbn().getId(), + entry.getSbn().getUser().getIdentifier(), + entry.getSbn().getKey(), + stats.dismissalSurface, + stats.dismissalSentiment, + stats.notificationVisibility); + } catch (RemoteException e) { + // system process is dead if we're here. + } + } } - // Optimistically mark the notification as dismissed -- we'll wait for the signal from - // system server before removing it from our notification set. - entry.setDismissState(DISMISSED); - mLogger.logNotifDismissed(entry.getKey()); + locallyDismissNotifications(entriesToLocallyDismiss); + rebuildList(); + } - List<NotificationEntry> canceledEntries = new ArrayList<>(); + /** + * Dismisses a single notification on behalf of the user. + */ + public void dismissNotification( + NotificationEntry entry, + @NonNull DismissedByUserStats stats) { + dismissNotifications(List.of( + new Pair<NotificationEntry, DismissedByUserStats>(entry, stats))); + } - if (isCanceled(entry)) { - canceledEntries.add(entry); - } else { - // Ask system server to remove it for us - try { - mStatusBarService.onNotificationClear( - entry.getSbn().getPackageName(), - entry.getSbn().getTag(), - entry.getSbn().getId(), - entry.getSbn().getUser().getIdentifier(), - entry.getSbn().getKey(), - stats.dismissalSurface, - stats.dismissalSentiment, - stats.notificationVisibility); - } catch (RemoteException e) { - // system process is dead if we're here. + /** + * Dismisses all clearable notifications for a given userid on behalf of the user. + */ + public void dismissAllNotifications(@UserIdInt int userId) { + Assert.isMainThread(); + checkForReentrantCall(); + + try { + mStatusBarService.onClearAllNotifications(userId); + } catch (RemoteException e) { + // system process is dead if we're here. + } + + final List<NotificationEntry> entries = new ArrayList(getActiveNotifs()); + for (int i = entries.size() - 1; i >= 0; i--) { + NotificationEntry entry = entries.get(i); + if (!shouldDismissOnClearAll(entry, userId)) { + // system server won't be removing these notifications, but we still give dismiss + // interceptors the chance to filter the notification + updateDismissInterceptors(entry); + if (isDismissIntercepted(entry)) { + mLogger.logNotifClearAllDismissalIntercepted(entry.getKey()); + } + entries.remove(i); } + } - // Also mark any children as dismissed as system server will auto-dismiss them as well - if (entry.getSbn().getNotification().isGroupSummary()) { - for (NotificationEntry otherEntry : mNotificationSet.values()) { - if (shouldAutoDismiss(otherEntry, entry.getSbn().getGroupKey())) { - otherEntry.setDismissState(PARENT_DISMISSED); - if (isCanceled(otherEntry)) { - canceledEntries.add(otherEntry); + locallyDismissNotifications(entries); + rebuildList(); + } + + /** + * Optimistically marks the given notifications as dismissed -- we'll wait for the signal + * from system server before removing it from our notification set. + */ + private void locallyDismissNotifications(List<NotificationEntry> entries) { + final List<NotificationEntry> canceledEntries = new ArrayList<>(); + + for (int i = 0; i < entries.size(); i++) { + NotificationEntry entry = entries.get(i); + + entry.setDismissState(DISMISSED); + mLogger.logNotifDismissed(entry.getKey()); + + if (isCanceled(entry)) { + canceledEntries.add(entry); + } else { + // Mark any children as dismissed as system server will auto-dismiss them as well + if (entry.getSbn().getNotification().isGroupSummary()) { + for (NotificationEntry otherEntry : mNotificationSet.values()) { + if (shouldAutoDismissChildren(otherEntry, entry.getSbn().getGroupKey())) { + otherEntry.setDismissState(PARENT_DISMISSED); + if (isCanceled(otherEntry)) { + canceledEntries.add(otherEntry); + } } } } @@ -255,7 +321,6 @@ public class NotifCollection implements Dumpable { for (NotificationEntry canceledEntry : canceledEntries) { tryRemoveNotification(canceledEntry); } - rebuildList(); } private void onNotificationPosted(StatusBarNotification sbn, RankingMap rankingMap) { @@ -552,7 +617,7 @@ public class NotifCollection implements Dumpable { * * See NotificationManager.cancelGroupChildrenByListLocked() for corresponding code. */ - private static boolean shouldAutoDismiss( + private static boolean shouldAutoDismissChildren( NotificationEntry entry, String dismissedGroupKey) { return entry.getSbn().getGroupKey().equals(dismissedGroupKey) @@ -562,10 +627,39 @@ public class NotifCollection implements Dumpable { && entry.getDismissState() != DISMISSED; } + /** + * When the user 'clears all notifications' through SystemUI, NotificationManager will not + * dismiss unclearable notifications. + * @return true if we think NotificationManager will dismiss the entry when asked to + * cancel this notification with {@link NotificationListenerService#REASON_CANCEL_ALL} + * + * See NotificationManager.cancelAllLocked for corresponding code. + */ + private static boolean shouldDismissOnClearAll( + NotificationEntry entry, + @UserIdInt int userId) { + // TODO: (b/149396544) add FLAG_BUBBLE check here + in NoManService + return userIdMatches(entry, userId) + && entry.isClearable() + && entry.getDismissState() != DISMISSED; + } + private static boolean hasFlag(NotificationEntry entry, int flag) { return (entry.getSbn().getNotification().flags & flag) != 0; } + /** + * Determine whether the userId applies to the notification in question, either because + * they match exactly, or one of them is USER_ALL (which is treated as a wildcard). + * + * See NotificationManager#notificationMatchesUserId + */ + private static boolean userIdMatches(NotificationEntry entry, int userId) { + return userId == UserHandle.USER_ALL + || entry.getSbn().getUser().getIdentifier() == UserHandle.USER_ALL + || entry.getSbn().getUser().getIdentifier() == userId; + } + private void dispatchOnEntryInit(NotificationEntry entry) { mAmDispatchingToOtherCode = true; for (NotifCollectionListener listener : mNotifCollectionListeners) { @@ -613,6 +707,7 @@ public class NotifCollection implements Dumpable { } mAmDispatchingToOtherCode = false; } + @Override public void dump(@NonNull FileDescriptor fd, PrintWriter pw, @NonNull String[] args) { final List<NotificationEntry> entries = new ArrayList<>(getActiveNotifs()); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifInflaterImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifInflaterImpl.java index 83f56cc1e83d..1f6413b525cb 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifInflaterImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifInflaterImpl.java @@ -44,6 +44,7 @@ public class NotifInflaterImpl implements NotifInflater { private final IStatusBarService mStatusBarService; private final NotifCollection mNotifCollection; private final NotifInflationErrorManager mNotifErrorManager; + private final NotifPipeline mNotifPipeline; private NotificationRowBinderImpl mNotificationRowBinder; private InflationCallback mExternalInflationCallback; @@ -52,10 +53,12 @@ public class NotifInflaterImpl implements NotifInflater { public NotifInflaterImpl( IStatusBarService statusBarService, NotifCollection notifCollection, - NotifInflationErrorManager errorManager) { + NotifInflationErrorManager errorManager, + NotifPipeline notifPipeline) { mStatusBarService = statusBarService; mNotifCollection = notifCollection; mNotifErrorManager = errorManager; + mNotifPipeline = notifPipeline; } /** @@ -110,7 +113,7 @@ public class NotifInflaterImpl implements NotifInflater { DISMISS_SENTIMENT_NEUTRAL, NotificationVisibility.obtain(entry.getKey(), entry.getRanking().getRank(), - mNotifCollection.getActiveNotifs().size(), + mNotifPipeline.getShadeListCount(), true, NotificationLogger.getNotificationLocation(entry)) )); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifPipeline.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifPipeline.java index d4d2369ba822..44cec966ba55 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifPipeline.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifPipeline.java @@ -195,4 +195,27 @@ public class NotifPipeline implements CommonNotifCollection { public List<ListEntry> getShadeList() { return mShadeListBuilder.getShadeList(); } + + /** + * Returns the number of notifications currently shown in the shade. This includes all + * children and summary notifications. If this method is called during pipeline execution it + * will return the number of notifications in its current state, which will likely be only + * partially-generated. + */ + public int getShadeListCount() { + final List<ListEntry> entries = getShadeList(); + int numNotifs = 0; + for (int i = 0; i < entries.size(); i++) { + final ListEntry entry = entries.get(i); + if (entry instanceof GroupEntry) { + final GroupEntry parentEntry = (GroupEntry) entry; + numNotifs++; // include the summary in the count + numNotifs += parentEntry.getChildren().size(); + } else { + numNotifs++; + } + } + + return numNotifs; + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/BubbleCoordinator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/BubbleCoordinator.java index 116c70c4f1cf..8b2a07d00378 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/BubbleCoordinator.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/BubbleCoordinator.java @@ -17,7 +17,7 @@ package com.android.systemui.statusbar.notification.collection.coordinator; import static android.service.notification.NotificationStats.DISMISSAL_OTHER; -import static android.service.notification.NotificationStats.DISMISS_SENTIMENT_UNKNOWN; +import static android.service.notification.NotificationStats.DISMISS_SENTIMENT_NEUTRAL; import com.android.internal.statusbar.NotificationVisibility; import com.android.systemui.bubbles.BubbleController; @@ -153,10 +153,10 @@ public class BubbleCoordinator implements Coordinator { private DismissedByUserStats createDismissedByUserStats(NotificationEntry entry) { return new DismissedByUserStats( DISMISSAL_OTHER, - DISMISS_SENTIMENT_UNKNOWN, + DISMISS_SENTIMENT_NEUTRAL, NotificationVisibility.obtain(entry.getKey(), entry.getRanking().getRank(), - mNotifPipeline.getActiveNotifs().size(), + mNotifPipeline.getShadeListCount(), true, // was visible as a bubble NotificationLogger.getNotificationLocation(entry)) ); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionLogger.kt index dc7a50d621a1..8675cca3cffe 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionLogger.kt @@ -77,6 +77,14 @@ class NotifCollectionLogger @Inject constructor( }) } + fun logNotifClearAllDismissalIntercepted(key: String) { + buffer.log(TAG, INFO, { + str1 = key + }, { + "CLEAR ALL DISMISSAL INTERCEPTED $str1" + }) + } + fun logRankingMissing(key: String, rankingMap: RankingMap) { buffer.log(TAG, WARNING, { str1 = key }, { "Ranking update is missing ranking for $str1" }) buffer.log(TAG, DEBUG, {}, { "Ranking map contents:" }) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java index 0cc337187f02..b2b46d58713f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java @@ -16,6 +16,9 @@ package com.android.systemui.statusbar.notification.stack; +import static android.service.notification.NotificationStats.DISMISSAL_SHADE; +import static android.service.notification.NotificationStats.DISMISS_SENTIMENT_NEUTRAL; + import static com.android.systemui.Dependency.ALLOW_NOTIFICATION_LONG_PRESS_NAME; import static com.android.systemui.statusbar.notification.ActivityLaunchAnimator.ExpandAnimationParameters; import static com.android.systemui.statusbar.notification.stack.NotificationSectionsManager.BUCKET_SILENT; @@ -79,6 +82,7 @@ import com.android.internal.graphics.ColorUtils; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.internal.statusbar.IStatusBarService; +import com.android.internal.statusbar.NotificationVisibility; import com.android.keyguard.KeyguardSliceView; import com.android.settingslib.Utils; import com.android.systemui.Dependency; @@ -98,6 +102,7 @@ import com.android.systemui.plugins.statusbar.StatusBarStateController.StateList import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.DragDownHelper.DragDownCallback; import com.android.systemui.statusbar.EmptyShadeView; +import com.android.systemui.statusbar.FeatureFlags; import com.android.systemui.statusbar.NotificationLockscreenUserManager; import com.android.systemui.statusbar.NotificationLockscreenUserManager.UserChangedListener; import com.android.systemui.statusbar.NotificationRemoteInputManager; @@ -114,7 +119,11 @@ import com.android.systemui.statusbar.notification.NotificationUtils; import com.android.systemui.statusbar.notification.ShadeViewRefactor; import com.android.systemui.statusbar.notification.ShadeViewRefactor.RefactorComponent; import com.android.systemui.statusbar.notification.VisualStabilityManager; +import com.android.systemui.statusbar.notification.collection.NotifCollection; +import com.android.systemui.statusbar.notification.collection.NotifPipeline; import com.android.systemui.statusbar.notification.collection.NotificationEntry; +import com.android.systemui.statusbar.notification.collection.notifcollection.DismissedByUserStats; +import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener; import com.android.systemui.statusbar.notification.logging.NotificationLogger; import com.android.systemui.statusbar.notification.row.ActivatableNotificationView; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; @@ -484,8 +493,10 @@ public class NotificationStackScrollLayout extends ViewGroup implements ScrollAd private NotificationIconAreaController mIconAreaController; private final NotificationLockscreenUserManager mLockscreenUserManager; private final Rect mTmpRect = new Rect(); - private final NotificationEntryManager mEntryManager = - Dependency.get(NotificationEntryManager.class); + private final FeatureFlags mFeatureFlags; + private final NotifPipeline mNotifPipeline; + private final NotifCollection mNotifCollection; + private final NotificationEntryManager mEntryManager; private final IStatusBarService mBarService = IStatusBarService.Stub.asInterface( ServiceManager.getService(Context.STATUS_BAR_SERVICE)); @VisibleForTesting @@ -529,7 +540,11 @@ public class NotificationStackScrollLayout extends ViewGroup implements ScrollAd ZenModeController zenController, NotificationSectionsManager notificationSectionsManager, ForegroundServiceSectionController fgsSectionController, - ForegroundServiceDismissalFeatureController fgsFeatureController + ForegroundServiceDismissalFeatureController fgsFeatureController, + FeatureFlags featureFlags, + NotifPipeline notifPipeline, + NotificationEntryManager entryManager, + NotifCollection notifCollection ) { super(context, attrs, 0, 0); Resources res = getResources(); @@ -607,16 +622,26 @@ public class NotificationStackScrollLayout extends ViewGroup implements ScrollAd } }, HIGH_PRIORITY, Settings.Secure.NOTIFICATION_DISMISS_RTL); - mEntryManager.addNotificationEntryListener(new NotificationEntryListener() { - @Override - public void onPreEntryUpdated(NotificationEntry entry) { - if (entry.rowExists() && !entry.getSbn().isClearable()) { - // If the row already exists, the user may have performed a dismiss action on - // the notification. Since it's not clearable we should snap it back. - snapViewIfNeeded(entry); + mFeatureFlags = featureFlags; + mNotifPipeline = notifPipeline; + mEntryManager = entryManager; + mNotifCollection = notifCollection; + if (mFeatureFlags.isNewNotifPipelineRenderingEnabled()) { + mNotifPipeline.addCollectionListener(new NotifCollectionListener() { + @Override + public void onEntryUpdated(NotificationEntry entry) { + NotificationStackScrollLayout.this.onEntryUpdated(entry); } - } - }); + }); + } else { + mEntryManager.addNotificationEntryListener(new NotificationEntryListener() { + @Override + public void onPreEntryUpdated(NotificationEntry entry) { + NotificationStackScrollLayout.this.onEntryUpdated(entry); + } + }); + } + dynamicPrivacyController.addListener(this); mDynamicPrivacyController = dynamicPrivacyController; mStatusbarStateController = statusBarStateController; @@ -708,7 +733,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements ScrollAd @ShadeViewRefactor(RefactorComponent.SHADE_VIEW) public void updateFooter() { boolean showDismissView = mClearAllEnabled && hasActiveClearableNotifications(ROWS_ALL); - boolean showFooterView = (showDismissView || mEntryManager.hasActiveNotifications()) + boolean showFooterView = (showDismissView || hasActiveNotifications()) && mStatusBarState != StatusBarState.KEYGUARD && !mRemoteInputManager.getController().isRemoteInputActive(); @@ -5537,32 +5562,10 @@ public class NotificationStackScrollLayout extends ViewGroup implements ScrollAd return; } - performDismissAllAnimations(viewsToHide, closeShade, () -> { - for (ExpandableNotificationRow rowToRemove : viewsToRemove) { - if (canChildBeDismissed(rowToRemove)) { - if (selection == ROWS_ALL) { - // TODO: This is a listener method; we shouldn't be calling it. Can we just - // call performRemoveNotification as below? - mEntryManager.removeNotification( - rowToRemove.getEntry().getKey(), - null /* ranking */, - NotificationListenerService.REASON_CANCEL_ALL); - } else { - mEntryManager.performRemoveNotification( - rowToRemove.getEntry().getSbn(), - NotificationListenerService.REASON_CANCEL_ALL); - } - } else { - rowToRemove.resetTranslation(); - } - } - if (selection == ROWS_ALL) { - try { - mBarService.onClearAllNotifications(mLockscreenUserManager.getCurrentUserId()); - } catch (Exception ex) { - } - } - }); + performDismissAllAnimations( + viewsToHide, + closeShade, + () -> onDismissAllAnimationsEnd(viewsToRemove, selection)); } private boolean includeChildInDismissAll( @@ -6407,6 +6410,83 @@ public class NotificationStackScrollLayout extends ViewGroup implements ScrollAd return false; } + // --------------------- NotificationEntryManager/NotifPipeline methods ------------------------ + + private void onEntryUpdated(NotificationEntry entry) { + // If the row already exists, the user may have performed a dismiss action on the + // notification. Since it's not clearable we should snap it back. + if (entry.rowExists() && !entry.getSbn().isClearable()) { + snapViewIfNeeded(entry); + } + } + + private boolean hasActiveNotifications() { + if (mFeatureFlags.isNewNotifPipelineRenderingEnabled()) { + return mNotifPipeline.getShadeList().isEmpty(); + } else { + return mEntryManager.hasActiveNotifications(); + } + } + + /** + * Called after the animations for a "clear all notifications" action has ended. + */ + private void onDismissAllAnimationsEnd( + List<ExpandableNotificationRow> viewsToRemove, + @SelectedRows int selectedRows) { + if (mFeatureFlags.isNewNotifPipelineRenderingEnabled()) { + if (selectedRows == ROWS_ALL) { + mNotifCollection.dismissAllNotifications(mLockscreenUserManager.getCurrentUserId()); + } else { + final List<Pair<NotificationEntry, DismissedByUserStats>> + entriesWithRowsDismissedFromShade = new ArrayList<>(); + final List<DismissedByUserStats> dismissalUserStats = new ArrayList<>(); + final int numVisibleEntries = mNotifPipeline.getShadeListCount(); + for (int i = 0; i < viewsToRemove.size(); i++) { + final NotificationEntry entry = viewsToRemove.get(i).getEntry(); + final DismissedByUserStats stats = + new DismissedByUserStats( + DISMISSAL_SHADE, + DISMISS_SENTIMENT_NEUTRAL, + NotificationVisibility.obtain( + entry.getKey(), + entry.getRanking().getRank(), + numVisibleEntries, + true, + NotificationLogger.getNotificationLocation(entry))); + entriesWithRowsDismissedFromShade.add( + new Pair<NotificationEntry, DismissedByUserStats>(entry, stats)); + } + mNotifCollection.dismissNotifications(entriesWithRowsDismissedFromShade); + } + } else { + for (ExpandableNotificationRow rowToRemove : viewsToRemove) { + if (canChildBeDismissed(rowToRemove)) { + if (selectedRows == ROWS_ALL) { + // TODO: This is a listener method; we shouldn't be calling it. Can we just + // call performRemoveNotification as below? + mEntryManager.removeNotification( + rowToRemove.getEntry().getKey(), + null /* ranking */, + NotificationListenerService.REASON_CANCEL_ALL); + } else { + mEntryManager.performRemoveNotification( + rowToRemove.getEntry().getSbn(), + NotificationListenerService.REASON_CANCEL_ALL); + } + } else { + rowToRemove.resetTranslation(); + } + } + if (selectedRows == ROWS_ALL) { + try { + mBarService.onClearAllNotifications(mLockscreenUserManager.getCurrentUserId()); + } catch (Exception ex) { + } + } + } + } + // ---------------------- DragDownHelper.OnDragDownListener ------------------------------------ @ShadeViewRefactor(RefactorComponent.INPUT) @@ -6415,8 +6495,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements ScrollAd /* Only ever called as a consequence of a lockscreen expansion gesture. */ @Override public boolean onDraggedDown(View startingChild, int dragLengthY) { - if (mStatusBarState == StatusBarState.KEYGUARD - && mEntryManager.hasActiveNotifications()) { + if (mStatusBarState == StatusBarState.KEYGUARD && hasActiveNotifications()) { mLockscreenGestureLogger.write( MetricsEvent.ACTION_LS_SHADE, (int) (dragLengthY / mDisplayMetrics.density), diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java index 0f3b5db2d281..70b43bfc0367 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java @@ -17,6 +17,7 @@ package com.android.systemui.statusbar.phone; import static android.service.notification.NotificationListenerService.REASON_CLICK; +import static android.service.notification.NotificationStats.DISMISS_SENTIMENT_NEUTRAL; import static com.android.systemui.statusbar.phone.StatusBar.getActivityOptions; @@ -35,6 +36,7 @@ import android.os.Looper; import android.os.RemoteException; import android.os.UserHandle; import android.service.dreams.IDreamManager; +import android.service.notification.NotificationStats; import android.service.notification.StatusBarNotification; import android.text.TextUtils; import android.util.EventLog; @@ -56,6 +58,7 @@ import com.android.systemui.dagger.qualifiers.UiBackground; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.statusbar.CommandQueue; +import com.android.systemui.statusbar.FeatureFlags; import com.android.systemui.statusbar.NotificationLockscreenUserManager; import com.android.systemui.statusbar.NotificationPresenter; import com.android.systemui.statusbar.NotificationRemoteInputManager; @@ -66,7 +69,11 @@ import com.android.systemui.statusbar.notification.NotificationActivityStarter; import com.android.systemui.statusbar.notification.NotificationEntryListener; import com.android.systemui.statusbar.notification.NotificationEntryManager; import com.android.systemui.statusbar.notification.NotificationInterruptionStateProvider; +import com.android.systemui.statusbar.notification.collection.NotifCollection; +import com.android.systemui.statusbar.notification.collection.NotifPipeline; import com.android.systemui.statusbar.notification.collection.NotificationEntry; +import com.android.systemui.statusbar.notification.collection.notifcollection.DismissedByUserStats; +import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener; import com.android.systemui.statusbar.notification.logging.NotificationLogger; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; import com.android.systemui.statusbar.policy.HeadsUpUtil; @@ -97,6 +104,9 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit private final KeyguardStateController mKeyguardStateController; private final ActivityStarter mActivityStarter; private final NotificationEntryManager mEntryManager; + private final NotifPipeline mNotifPipeline; + private final NotifCollection mNotifCollection; + private final FeatureFlags mFeatureFlags; private final StatusBarStateController mStatusBarStateController; private final NotificationInterruptionStateProvider mNotificationInterruptionStateProvider; private final MetricsLogger mMetricsLogger; @@ -135,7 +145,9 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit NotificationInterruptionStateProvider notificationInterruptionStateProvider, MetricsLogger metricsLogger, LockPatternUtils lockPatternUtils, Handler mainThreadHandler, Handler backgroundHandler, Executor uiBgExecutor, - ActivityIntentHelper activityIntentHelper, BubbleController bubbleController) { + ActivityIntentHelper activityIntentHelper, BubbleController bubbleController, + FeatureFlags featureFlags, NotifPipeline notifPipeline, + NotifCollection notifCollection) { mContext = context; mNotificationPanel = panel; mPresenter = presenter; @@ -162,12 +174,25 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit mLockPatternUtils = lockPatternUtils; mBackgroundHandler = backgroundHandler; mUiBgExecutor = uiBgExecutor; - mEntryManager.addNotificationEntryListener(new NotificationEntryListener() { - @Override - public void onPendingEntryAdded(NotificationEntry entry) { - handleFullScreenIntent(entry); - } - }); + mFeatureFlags = featureFlags; + mNotifPipeline = notifPipeline; + mNotifCollection = notifCollection; + if (!mFeatureFlags.isNewNotifPipelineRenderingEnabled()) { + mEntryManager.addNotificationEntryListener(new NotificationEntryListener() { + @Override + public void onPendingEntryAdded(NotificationEntry entry) { + handleFullScreenIntent(entry); + } + }); + } else { + mNotifPipeline.addCollectionListener(new NotifCollectionListener() { + @Override + public void onEntryAdded(NotificationEntry entry) { + handleFullScreenIntent(entry); + } + }); + } + mStatusBarRemoteInputCallback = remoteInputCallback; mMainThreadHandler = mainThreadHandler; mActivityIntentHelper = activityIntentHelper; @@ -246,15 +271,14 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit mHeadsUpManager.removeNotification(sbn.getKey(), true /* releaseImmediately */); } - StatusBarNotification parentToCancel = null; + NotificationEntry parentToCancel = null; if (shouldAutoCancel(sbn) && mGroupManager.isOnlyChildInGroup(sbn)) { - StatusBarNotification summarySbn = - mGroupManager.getLogicalGroupSummary(sbn).getSbn(); - if (shouldAutoCancel(summarySbn)) { + NotificationEntry summarySbn = mGroupManager.getLogicalGroupSummary(sbn); + if (shouldAutoCancel(summarySbn.getSbn())) { parentToCancel = summarySbn; } } - final StatusBarNotification parentToCancelFinal = parentToCancel; + final NotificationEntry parentToCancelFinal = parentToCancel; final Runnable runnable = () -> handleNotificationClickAfterPanelCollapsed( sbn, row, controller, intent, isActivityIntent, wasOccluded, parentToCancelFinal); @@ -279,7 +303,7 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit PendingIntent intent, boolean isActivityIntent, boolean wasOccluded, - StatusBarNotification parentToCancelFinal) { + NotificationEntry parentToCancelFinal) { String notificationKey = sbn.getKey(); try { // The intent we are sending is for the application, which @@ -330,7 +354,7 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit collapseOnMainThread(); } - final int count = mEntryManager.getActiveNotificationsCount(); + final int count = getVisibleNotificationsCount(); final int rank = entry.getRanking().getRank(); NotificationVisibility.NotificationLocation location = NotificationLogger.getNotificationLocation(entry); @@ -349,7 +373,7 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit || mRemoteInputManager.isNotificationKeptForRemoteInputHistory( notificationKey)) { // Automatically remove all notifications that we may have kept around longer - removeNotification(sbn); + removeNotification(row.getEntry()); } } mIsCollapsingToShowActivityOverLockscreen = false; @@ -482,11 +506,10 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit return entry.shouldSuppressFullScreenIntent(); } - private void removeNotification(StatusBarNotification notification) { + private void removeNotification(NotificationEntry entry) { // We have to post it to the UI thread for synchronization mMainThreadHandler.post(() -> { - Runnable removeRunnable = - () -> mEntryManager.performRemoveNotification(notification, REASON_CLICK); + Runnable removeRunnable = createRemoveRunnable(entry); if (mPresenter.isCollapsing()) { // To avoid lags we're only performing the remove // after the shade was collapsed @@ -497,6 +520,53 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit }); } + // --------------------- NotificationEntryManager/NotifPipeline methods ------------------------ + + private int getVisibleNotificationsCount() { + if (mFeatureFlags.isNewNotifPipelineRenderingEnabled()) { + return mNotifPipeline.getShadeListCount(); + } else { + return mEntryManager.getActiveNotificationsCount(); + } + } + + private Runnable createRemoveRunnable(NotificationEntry entry) { + if (mFeatureFlags.isNewNotifPipelineRenderingEnabled()) { + return new Runnable() { + @Override + public void run() { + // see NotificationLogger#logNotificationClear + int dismissalSurface = NotificationStats.DISMISSAL_SHADE; + if (mHeadsUpManager.isAlerting(entry.getKey())) { + dismissalSurface = NotificationStats.DISMISSAL_PEEK; + } else if (mNotificationPanel.hasPulsingNotifications()) { + dismissalSurface = NotificationStats.DISMISSAL_AOD; + } + + mNotifCollection.dismissNotification( + entry, + new DismissedByUserStats( + dismissalSurface, + DISMISS_SENTIMENT_NEUTRAL, + NotificationVisibility.obtain( + entry.getKey(), + entry.getRanking().getRank(), + mNotifPipeline.getShadeListCount(), + true, + NotificationLogger.getNotificationLocation(entry)) + )); + } + }; + } else { + return new Runnable() { + @Override + public void run() { + mEntryManager.performRemoveNotification(entry.getSbn(), REASON_CLICK); + } + }; + } + } + /** * Public builder for {@link StatusBarNotificationActivityStarter}. */ @@ -506,6 +576,9 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit private final CommandQueue mCommandQueue; private final Lazy<AssistManager> mAssistManagerLazy; private final NotificationEntryManager mEntryManager; + private final FeatureFlags mFeatureFlags; + private final NotifPipeline mNotifPipeline; + private final NotifCollection mNotifCollection; private final HeadsUpManagerPhone mHeadsUpManager; private final ActivityStarter mActivityStarter; private final IStatusBarService mStatusBarService; @@ -557,7 +630,10 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit @UiBackground Executor uiBgExecutor, ActivityIntentHelper activityIntentHelper, BubbleController bubbleController, - ShadeController shadeController) { + ShadeController shadeController, + FeatureFlags featureFlags, + NotifPipeline notifPipeline, + NotifCollection notifCollection) { mContext = context; mCommandQueue = commandQueue; mAssistManagerLazy = assistManagerLazy; @@ -583,6 +659,9 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit mActivityIntentHelper = activityIntentHelper; mBubbleController = bubbleController; mShadeController = shadeController; + mFeatureFlags = featureFlags; + mNotifPipeline = notifPipeline; + mNotifCollection = notifCollection; } /** Sets the status bar to use as {@link StatusBar}. */ @@ -608,8 +687,6 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit return this; } - - public StatusBarNotificationActivityStarter build() { return new StatusBarNotificationActivityStarter(mContext, mCommandQueue, mAssistManagerLazy, @@ -638,7 +715,10 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit mBackgroundHandler, mUiBgExecutor, mActivityIntentHelper, - mBubbleController); + mBubbleController, + mFeatureFlags, + mNotifPipeline, + mNotifCollection); } } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotifCollectionTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotifCollectionTest.java index 96db16adb7dc..12e9d31fdd0c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotifCollectionTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotifCollectionTest.java @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.notification.collection; +import static android.app.Notification.FLAG_NO_CLEAR; import static android.service.notification.NotificationListenerService.REASON_APP_CANCEL; import static android.service.notification.NotificationListenerService.REASON_CANCEL; import static android.service.notification.NotificationListenerService.REASON_CLICK; @@ -33,8 +34,8 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.ArgumentMatchers.anyObject; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.mock; @@ -50,12 +51,12 @@ import android.app.Notification; import android.os.RemoteException; import android.service.notification.NotificationListenerService.Ranking; import android.service.notification.NotificationListenerService.RankingMap; -import android.service.notification.NotificationStats; import android.service.notification.StatusBarNotification; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; import android.util.ArrayMap; import android.util.ArraySet; +import android.util.Pair; import androidx.test.filters.SmallTest; @@ -104,7 +105,6 @@ public class NotifCollectionTest extends SysuiTestCase { @Spy private RecordingCollectionListener mCollectionListener; @Mock private CollectionReadyForBuildListener mBuildListener; @Mock private FeatureFlags mFeatureFlags; - @Mock private DismissedByUserStats mDismissedByUserStats; @Spy private RecordingLifetimeExtender mExtender1 = new RecordingLifetimeExtender("Extender1"); @Spy private RecordingLifetimeExtender mExtender2 = new RecordingLifetimeExtender("Extender2"); @@ -424,13 +424,16 @@ public class NotifCollectionTest extends SysuiTestCase { public void testDismissingLifetimeExtendedSummaryDoesNotDismissChildren() { // GIVEN A notif group with one summary and two children mCollection.addNotificationLifetimeExtender(mExtender1); - NotifEvent notif1 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 1, "myTag") - .setGroup(mContext, GROUP_1) - .setGroupSummary(mContext, true)); - NotifEvent notif2 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 2, "myTag") - .setGroup(mContext, GROUP_1)); - NotifEvent notif3 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 3, "myTag") - .setGroup(mContext, GROUP_1)); + CollectionEvent notif1 = postNotif( + buildNotif(TEST_PACKAGE, 1, "myTag") + .setGroup(mContext, GROUP_1) + .setGroupSummary(mContext, true)); + CollectionEvent notif2 = postNotif( + buildNotif(TEST_PACKAGE, 2, "myTag") + .setGroup(mContext, GROUP_1)); + CollectionEvent notif3 = postNotif( + buildNotif(TEST_PACKAGE, 3, "myTag") + .setGroup(mContext, GROUP_1)); NotificationEntry entry1 = mCollectionListener.getEntry(notif1.key); NotificationEntry entry2 = mCollectionListener.getEntry(notif2.key); @@ -456,7 +459,7 @@ public class NotifCollectionTest extends SysuiTestCase { } @Test - public void testDismissInterceptorsAreCalled() throws RemoteException { + public void testDismissNotificationCallsDismissInterceptors() throws RemoteException { // GIVEN a collection with notifications with multiple dismiss interceptors mInterceptor1.shouldInterceptDismissal = true; mInterceptor2.shouldInterceptDismissal = true; @@ -469,10 +472,7 @@ public class NotifCollectionTest extends SysuiTestCase { NotificationEntry entry = mCollectionListener.getEntry(notif.key); // WHEN a notification is manually dismissed - DismissedByUserStats stats = new DismissedByUserStats( - NotificationStats.DISMISSAL_SHADE, - NotificationStats.DISMISS_SENTIMENT_NEUTRAL, - NotificationVisibility.obtain(entry.getKey(), 7, 2, true)); + DismissedByUserStats stats = defaultStats(entry); mCollection.dismissNotification(entry, stats); // THEN all interceptors get checked @@ -506,10 +506,7 @@ public class NotifCollectionTest extends SysuiTestCase { NotificationEntry entry = mCollectionListener.getEntry(notif.key); // WHEN a notification is manually dismissed and intercepted - DismissedByUserStats stats = new DismissedByUserStats( - NotificationStats.DISMISSAL_SHADE, - NotificationStats.DISMISS_SENTIMENT_NEUTRAL, - NotificationVisibility.obtain(entry.getKey(), 7, 2, true)); + DismissedByUserStats stats = defaultStats(entry); mCollection.dismissNotification(entry, stats); assertEquals(List.of(mInterceptor1, mInterceptor2), entry.mDismissInterceptors); clearInvocations(mInterceptor1, mInterceptor2); @@ -531,7 +528,7 @@ public class NotifCollectionTest extends SysuiTestCase { eq(notif.sbn.getKey()), anyInt(), anyInt(), - anyObject()); + eq(stats.notificationVisibility)); } @Test @@ -544,19 +541,16 @@ public class NotifCollectionTest extends SysuiTestCase { NotificationEntry entry = mCollectionListener.getEntry(notif.key); // GIVEN a notification is manually dismissed - DismissedByUserStats stats = new DismissedByUserStats( - NotificationStats.DISMISSAL_SHADE, - NotificationStats.DISMISS_SENTIMENT_NEUTRAL, - NotificationVisibility.obtain(entry.getKey(), 7, 2, true)); + DismissedByUserStats stats = defaultStats(entry); mCollection.dismissNotification(entry, stats); // WHEN all interceptors end their interception dismissal mInterceptor1.shouldInterceptDismissal = false; mInterceptor1.onEndInterceptionCallback.onEndDismissInterception(mInterceptor1, entry, - mDismissedByUserStats); + stats); // THEN we send the dismissal to system server - verify(mStatusBarService, times(1)).onNotificationClear( + verify(mStatusBarService).onNotificationClear( eq(notif.sbn.getPackageName()), eq(notif.sbn.getTag()), eq(47), @@ -564,7 +558,7 @@ public class NotifCollectionTest extends SysuiTestCase { eq(notif.sbn.getKey()), anyInt(), anyInt(), - anyObject()); + eq(stats.notificationVisibility)); } @Test @@ -581,16 +575,12 @@ public class NotifCollectionTest extends SysuiTestCase { NotificationEntry entry = mCollectionListener.getEntry(notif.key); // GIVEN a notification is manually dismissed - DismissedByUserStats stats = new DismissedByUserStats( - NotificationStats.DISMISSAL_SHADE, - NotificationStats.DISMISS_SENTIMENT_NEUTRAL, - NotificationVisibility.obtain(entry.getKey(), 7, 2, true)); - mCollection.dismissNotification(entry, stats); + mCollection.dismissNotification(entry, defaultStats(entry)); // WHEN an interceptor ends its interception mInterceptor1.shouldInterceptDismissal = false; mInterceptor1.onEndInterceptionCallback.onEndDismissInterception(mInterceptor1, entry, - mDismissedByUserStats); + defaultStats(entry)); // THEN all interceptors get checked verify(mInterceptor1).shouldInterceptDismissal(entry); @@ -613,7 +603,7 @@ public class NotifCollectionTest extends SysuiTestCase { // WHEN we try to end the dismissal of an interceptor that didn't intercept the notif mInterceptor1.onEndInterceptionCallback.onEndDismissInterception(mInterceptor1, entry, - mDismissedByUserStats); + defaultStats(entry)); // THEN an exception is thrown } @@ -636,11 +626,11 @@ public class NotifCollectionTest extends SysuiTestCase { @Test public void testGroupChildrenAreDismissedLocallyWhenSummaryIsDismissed() { // GIVEN a collection with two grouped notifs in it - NotifEvent notif0 = mNoMan.postNotif( + CollectionEvent notif0 = postNotif( buildNotif(TEST_PACKAGE, 0) .setGroup(mContext, GROUP_1) .setGroupSummary(mContext, true)); - NotifEvent notif1 = mNoMan.postNotif( + CollectionEvent notif1 = postNotif( buildNotif(TEST_PACKAGE, 1) .setGroup(mContext, GROUP_1)); NotificationEntry entry0 = mCollectionListener.getEntry(notif0.key); @@ -657,11 +647,11 @@ public class NotifCollectionTest extends SysuiTestCase { @Test public void testUpdatingDismissedSummaryBringsChildrenBack() { // GIVEN a collection with two grouped notifs in it - NotifEvent notif0 = mNoMan.postNotif( + CollectionEvent notif0 = postNotif( buildNotif(TEST_PACKAGE, 0) .setGroup(mContext, GROUP_1) .setGroupSummary(mContext, true)); - NotifEvent notif1 = mNoMan.postNotif( + CollectionEvent notif1 = postNotif( buildNotif(TEST_PACKAGE, 1) .setGroup(mContext, GROUP_1)); NotificationEntry entry0 = mCollectionListener.getEntry(notif0.key); @@ -680,14 +670,14 @@ public class NotifCollectionTest extends SysuiTestCase { @Test public void testDismissedChildrenAreNotResetByParentUpdate() { // GIVEN a collection with three grouped notifs in it - NotifEvent notif0 = mNoMan.postNotif( + CollectionEvent notif0 = postNotif( buildNotif(TEST_PACKAGE, 0) .setGroup(mContext, GROUP_1) .setGroupSummary(mContext, true)); - NotifEvent notif1 = mNoMan.postNotif( + CollectionEvent notif1 = postNotif( buildNotif(TEST_PACKAGE, 1) .setGroup(mContext, GROUP_1)); - NotifEvent notif2 = mNoMan.postNotif( + CollectionEvent notif2 = postNotif( buildNotif(TEST_PACKAGE, 2) .setGroup(mContext, GROUP_1)); NotificationEntry entry0 = mCollectionListener.getEntry(notif0.key); @@ -709,11 +699,11 @@ public class NotifCollectionTest extends SysuiTestCase { @Test public void testUpdatingGroupKeyOfDismissedSummaryBringsChildrenBack() { // GIVEN a collection with two grouped notifs in it - NotifEvent notif0 = mNoMan.postNotif( + CollectionEvent notif0 = postNotif( buildNotif(TEST_PACKAGE, 0) .setOverrideGroupKey(GROUP_1) .setGroupSummary(mContext, true)); - NotifEvent notif1 = mNoMan.postNotif( + CollectionEvent notif1 = postNotif( buildNotif(TEST_PACKAGE, 1) .setOverrideGroupKey(GROUP_1)); NotificationEntry entry0 = mCollectionListener.getEntry(notif0.key); @@ -1055,6 +1045,213 @@ public class NotifCollectionTest extends SysuiTestCase { assertEquals(REASON_NOT_CANCELED, entry0.mCancellationReason); } + @Test + public void testDismissNotificationsRebuildsOnce() { + // GIVEN a collection with a couple notifications + NotifEvent notif1 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47, "myTag")); + NotifEvent notif2 = mNoMan.postNotif(buildNotif(TEST_PACKAGE2, 88, "barTag")); + NotificationEntry entry1 = mCollectionListener.getEntry(notif1.key); + NotificationEntry entry2 = mCollectionListener.getEntry(notif2.key); + clearInvocations(mBuildListener); + + // WHEN both notifications are manually dismissed together + mCollection.dismissNotifications( + List.of(new Pair(entry1, defaultStats(entry1)), + new Pair(entry2, defaultStats(entry2)))); + + // THEN build list is only called one time + verify(mBuildListener).onBuildList(any(Collection.class)); + } + + @Test + public void testDismissNotificationsSentToSystemServer() throws RemoteException { + // GIVEN a collection with a couple notifications + NotifEvent notif1 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47, "myTag")); + NotifEvent notif2 = mNoMan.postNotif(buildNotif(TEST_PACKAGE2, 88, "barTag")); + NotificationEntry entry1 = mCollectionListener.getEntry(notif1.key); + NotificationEntry entry2 = mCollectionListener.getEntry(notif2.key); + + // WHEN both notifications are manually dismissed together + DismissedByUserStats stats1 = defaultStats(entry1); + DismissedByUserStats stats2 = defaultStats(entry2); + mCollection.dismissNotifications( + List.of(new Pair(entry1, defaultStats(entry1)), + new Pair(entry2, defaultStats(entry2)))); + + // THEN we send the dismissals to system server + verify(mStatusBarService).onNotificationClear( + notif1.sbn.getPackageName(), + notif1.sbn.getTag(), + 47, + notif1.sbn.getUser().getIdentifier(), + notif1.sbn.getKey(), + stats1.dismissalSurface, + stats1.dismissalSentiment, + stats1.notificationVisibility); + + verify(mStatusBarService).onNotificationClear( + notif2.sbn.getPackageName(), + notif2.sbn.getTag(), + 88, + notif2.sbn.getUser().getIdentifier(), + notif2.sbn.getKey(), + stats2.dismissalSurface, + stats2.dismissalSentiment, + stats2.notificationVisibility); + } + + @Test + public void testDismissNotificationsMarkedAsDismissed() { + // GIVEN a collection with a couple notifications + NotifEvent notif1 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47, "myTag")); + NotifEvent notif2 = mNoMan.postNotif(buildNotif(TEST_PACKAGE2, 88, "barTag")); + NotificationEntry entry1 = mCollectionListener.getEntry(notif1.key); + NotificationEntry entry2 = mCollectionListener.getEntry(notif2.key); + + // WHEN both notifications are manually dismissed together + mCollection.dismissNotifications( + List.of(new Pair(entry1, defaultStats(entry1)), + new Pair(entry2, defaultStats(entry2)))); + + // THEN the entries are marked as dismissed + assertEquals(DISMISSED, entry1.getDismissState()); + assertEquals(DISMISSED, entry2.getDismissState()); + } + + @Test + public void testDismissNotificationssCallsDismissInterceptors() { + // GIVEN a collection with notifications with multiple dismiss interceptors + mInterceptor1.shouldInterceptDismissal = true; + mInterceptor2.shouldInterceptDismissal = true; + mInterceptor3.shouldInterceptDismissal = false; + mCollection.addNotificationDismissInterceptor(mInterceptor1); + mCollection.addNotificationDismissInterceptor(mInterceptor2); + mCollection.addNotificationDismissInterceptor(mInterceptor3); + + NotifEvent notif1 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47, "myTag")); + NotifEvent notif2 = mNoMan.postNotif(buildNotif(TEST_PACKAGE2, 88, "barTag")); + NotificationEntry entry1 = mCollectionListener.getEntry(notif1.key); + NotificationEntry entry2 = mCollectionListener.getEntry(notif2.key); + + // WHEN both notifications are manually dismissed together + mCollection.dismissNotifications( + List.of(new Pair(entry1, defaultStats(entry1)), + new Pair(entry2, defaultStats(entry2)))); + + // THEN all interceptors get checked + verify(mInterceptor1).shouldInterceptDismissal(entry1); + verify(mInterceptor2).shouldInterceptDismissal(entry1); + verify(mInterceptor3).shouldInterceptDismissal(entry1); + verify(mInterceptor1).shouldInterceptDismissal(entry2); + verify(mInterceptor2).shouldInterceptDismissal(entry2); + verify(mInterceptor3).shouldInterceptDismissal(entry2); + + assertEquals(List.of(mInterceptor1, mInterceptor2), entry1.mDismissInterceptors); + assertEquals(List.of(mInterceptor1, mInterceptor2), entry2.mDismissInterceptors); + } + + @Test + public void testDismissAllNotificationsCallsRebuildOnce() { + // GIVEN a collection with a couple notifications + NotifEvent notif1 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47, "myTag")); + NotifEvent notif2 = mNoMan.postNotif(buildNotif(TEST_PACKAGE2, 88, "barTag")); + NotificationEntry entry1 = mCollectionListener.getEntry(notif1.key); + NotificationEntry entry2 = mCollectionListener.getEntry(notif2.key); + clearInvocations(mBuildListener); + + // WHEN all notifications are dismissed for the user who posted both notifs + mCollection.dismissAllNotifications(entry1.getSbn().getUser().getIdentifier()); + + // THEN build list is only called one time + verify(mBuildListener).onBuildList(any(Collection.class)); + } + + @Test + public void testDismissAllNotificationsSentToSystemServer() throws RemoteException { + // GIVEN a collection with a couple notifications + NotifEvent notif1 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47, "myTag")); + NotifEvent notif2 = mNoMan.postNotif(buildNotif(TEST_PACKAGE2, 88, "barTag")); + NotificationEntry entry1 = mCollectionListener.getEntry(notif1.key); + NotificationEntry entry2 = mCollectionListener.getEntry(notif2.key); + + // WHEN all notifications are dismissed for the user who posted both notifs + mCollection.dismissAllNotifications(entry1.getSbn().getUser().getIdentifier()); + + // THEN we send the dismissal to system server + verify(mStatusBarService).onClearAllNotifications( + entry1.getSbn().getUser().getIdentifier()); + } + + @Test + public void testDismissAllNotificationsMarkedAsDismissed() { + // GIVEN a collection with a couple notifications + NotifEvent notif1 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47, "myTag")); + NotifEvent notif2 = mNoMan.postNotif(buildNotif(TEST_PACKAGE2, 88, "barTag")); + NotificationEntry entry1 = mCollectionListener.getEntry(notif1.key); + NotificationEntry entry2 = mCollectionListener.getEntry(notif2.key); + + // WHEN all notifications are dismissed for the user who posted both notifs + mCollection.dismissAllNotifications(entry1.getSbn().getUser().getIdentifier()); + + // THEN the entries are marked as dismissed + assertEquals(DISMISSED, entry1.getDismissState()); + assertEquals(DISMISSED, entry2.getDismissState()); + } + + @Test + public void testDismissAllNotificationsDoesNotMarkDismissedUnclearableNotifs() { + // GIVEN a collection with one unclearable notification and one clearable notification + NotificationEntryBuilder notifEntryBuilder = buildNotif(TEST_PACKAGE, 47, "myTag"); + notifEntryBuilder.modifyNotification(mContext) + .setFlag(FLAG_NO_CLEAR, true); + NotifEvent unclearabeNotif = mNoMan.postNotif(notifEntryBuilder); + NotifEvent notif2 = mNoMan.postNotif(buildNotif(TEST_PACKAGE2, 88, "barTag")); + NotificationEntry unclearableEntry = mCollectionListener.getEntry(unclearabeNotif.key); + NotificationEntry entry2 = mCollectionListener.getEntry(notif2.key); + + // WHEN all notifications are dismissed for the user who posted both notifs + mCollection.dismissAllNotifications(unclearableEntry.getSbn().getUser().getIdentifier()); + + // THEN only the clearable entry is marked as dismissed + assertEquals(NOT_DISMISSED, unclearableEntry.getDismissState()); + assertEquals(DISMISSED, entry2.getDismissState()); + } + + @Test + public void testDismissAllNotificationsCallsDismissInterceptorsOnlyOnUnclearableNotifs() { + // GIVEN a collection with multiple dismiss interceptors + mInterceptor1.shouldInterceptDismissal = true; + mInterceptor2.shouldInterceptDismissal = true; + mInterceptor3.shouldInterceptDismissal = false; + mCollection.addNotificationDismissInterceptor(mInterceptor1); + mCollection.addNotificationDismissInterceptor(mInterceptor2); + mCollection.addNotificationDismissInterceptor(mInterceptor3); + + // GIVEN a collection with one unclearable and one clearable notification + NotifEvent unclearableNotif = mNoMan.postNotif( + buildNotif(TEST_PACKAGE, 47, "myTag") + .setFlag(mContext, FLAG_NO_CLEAR, true)); + NotificationEntry unclearable = mCollectionListener.getEntry(unclearableNotif.key); + NotifEvent clearableNotif = mNoMan.postNotif( + buildNotif(TEST_PACKAGE, 88, "myTag") + .setFlag(mContext, FLAG_NO_CLEAR, false)); + NotificationEntry clearable = mCollectionListener.getEntry(clearableNotif.key); + + // WHEN all notifications are dismissed for the user who posted the notif + mCollection.dismissAllNotifications(clearable.getSbn().getUser().getIdentifier()); + + // THEN all interceptors get checked for the unclearable notification + verify(mInterceptor1).shouldInterceptDismissal(unclearable); + verify(mInterceptor2).shouldInterceptDismissal(unclearable); + verify(mInterceptor3).shouldInterceptDismissal(unclearable); + assertEquals(List.of(mInterceptor1, mInterceptor2), unclearable.mDismissInterceptors); + + // THEN no interceptors get checked for the clearable notification + verify(mInterceptor1, never()).shouldInterceptDismissal(clearable); + verify(mInterceptor2, never()).shouldInterceptDismissal(clearable); + verify(mInterceptor3, never()).shouldInterceptDismissal(clearable); + } + private static NotificationEntryBuilder buildNotif(String pkg, int id, String tag) { return new NotificationEntryBuilder() .setPkg(pkg) diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java index b16e52ce7bd4..9ccee75a3d09 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java @@ -70,6 +70,8 @@ import com.android.systemui.statusbar.notification.NotificationFilter; import com.android.systemui.statusbar.notification.NotificationSectionsFeatureManager; import com.android.systemui.statusbar.notification.TestableNotificationEntryManager; import com.android.systemui.statusbar.notification.VisualStabilityManager; +import com.android.systemui.statusbar.notification.collection.NotifCollection; +import com.android.systemui.statusbar.notification.collection.NotifPipeline; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder; import com.android.systemui.statusbar.notification.collection.NotificationRankingManager; @@ -133,6 +135,7 @@ public class NotificationStackScrollLayoutTest extends SysuiTestCase { @Mock private NotificationSectionsManager mNotificationSectionsManager; @Mock private NotificationSection mNotificationSection; @Mock private NotificationLockscreenUserManager mLockscreenUserManager; + @Mock private FeatureFlags mFeatureFlags; private UserChangedListener mUserChangedListener; private TestableNotificationEntryManager mEntryManager; private int mOriginalInterruptionModelSetting; @@ -182,9 +185,8 @@ public class NotificationStackScrollLayoutTest extends SysuiTestCase { mock(LeakDetector.class), mock(ForegroundServiceDismissalFeatureController.class) ); - mDependency.injectTestDependency(NotificationEntryManager.class, mEntryManager); mEntryManager.setUpForTest(mock(NotificationPresenter.class), null, mHeadsUpManager); - + when(mFeatureFlags.isNewNotifPipelineRenderingEnabled()).thenReturn(false); NotificationShelf notificationShelf = mock(NotificationShelf.class); when(mNotificationSectionsManager.createSectionsForBuckets()).thenReturn( @@ -208,7 +210,11 @@ public class NotificationStackScrollLayoutTest extends SysuiTestCase { mZenModeController, mNotificationSectionsManager, mock(ForegroundServiceSectionController.class), - mock(ForegroundServiceDismissalFeatureController.class) + mock(ForegroundServiceDismissalFeatureController.class), + mFeatureFlags, + mock(NotifPipeline.class), + mEntryManager, + mock(NotifCollection.class) ); verify(mLockscreenUserManager).addUserChangedListener(userChangedCaptor.capture()); mUserChangedListener = userChangedCaptor.getValue(); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java index 50276106f8d4..1e4df272b02b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java @@ -58,6 +58,7 @@ import com.android.systemui.bubbles.BubbleController; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.statusbar.CommandQueue; +import com.android.systemui.statusbar.FeatureFlags; import com.android.systemui.statusbar.NotificationLockscreenUserManager; import com.android.systemui.statusbar.NotificationPresenter; import com.android.systemui.statusbar.NotificationRemoteInputManager; @@ -67,6 +68,8 @@ import com.android.systemui.statusbar.notification.ActivityLaunchAnimator; import com.android.systemui.statusbar.notification.NotificationActivityStarter; import com.android.systemui.statusbar.notification.NotificationEntryManager; import com.android.systemui.statusbar.notification.NotificationInterruptionStateProvider; +import com.android.systemui.statusbar.notification.collection.NotifCollection; +import com.android.systemui.statusbar.notification.collection.NotifPipeline; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; import com.android.systemui.statusbar.notification.row.NotificationTestHelper; @@ -114,6 +117,12 @@ public class StatusBarNotificationActivityStarterTest extends SysuiTestCase { private BubbleController mBubbleController; @Mock private ShadeControllerImpl mShadeController; + @Mock + private FeatureFlags mFeatureFlags; + @Mock + private NotifPipeline mNotifPipeline; + @Mock + private NotifCollection mNotifCollection; @Mock private ActivityIntentHelper mActivityIntentHelper; @@ -162,6 +171,7 @@ public class StatusBarNotificationActivityStarterTest extends SysuiTestCase { mActiveNotifications.add(mBubbleNotificationRow.getEntry()); when(mEntryManager.getVisibleNotifications()).thenReturn(mActiveNotifications); when(mStatusBarStateController.getState()).thenReturn(StatusBarState.SHADE); + when(mFeatureFlags.isNewNotifPipelineRenderingEnabled()).thenReturn(false); mNotificationActivityStarter = (new StatusBarNotificationActivityStarter.Builder( getContext(), mock(CommandQueue.class), () -> mAssistManager, @@ -175,11 +185,12 @@ public class StatusBarNotificationActivityStarterTest extends SysuiTestCase { mKeyguardStateController, mock(NotificationInterruptionStateProvider.class), mock(MetricsLogger.class), mock(LockPatternUtils.class), mHandler, mHandler, mUiBgExecutor, - mActivityIntentHelper, mBubbleController, mShadeController)) + mActivityIntentHelper, mBubbleController, mShadeController, mFeatureFlags, + mNotifPipeline, mNotifCollection) .setStatusBar(mStatusBar) .setNotificationPanelViewController(mock(NotificationPanelViewController.class)) .setNotificationPresenter(mock(NotificationPresenter.class)) - .setActivityLaunchAnimator(mock(ActivityLaunchAnimator.class)) + .setActivityLaunchAnimator(mock(ActivityLaunchAnimator.class))) .build(); // set up dismissKeyguardThenExecute to synchronously invoke the OnDismissAction arg |