blob: a3621b6cabf602e5cc705ac3e9f5572889794490 [file] [log] [blame]
/*
* 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.notification.collection;
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_ALL;
import static android.service.notification.NotificationListenerService.REASON_CHANNEL_BANNED;
import static android.service.notification.NotificationListenerService.REASON_CLICK;
import static android.service.notification.NotificationListenerService.REASON_ERROR;
import static android.service.notification.NotificationListenerService.REASON_GROUP_OPTIMIZATION;
import static android.service.notification.NotificationListenerService.REASON_GROUP_SUMMARY_CANCELED;
import static android.service.notification.NotificationListenerService.REASON_LISTENER_CANCEL;
import static android.service.notification.NotificationListenerService.REASON_LISTENER_CANCEL_ALL;
import static android.service.notification.NotificationListenerService.REASON_PACKAGE_BANNED;
import static android.service.notification.NotificationListenerService.REASON_PACKAGE_CHANGED;
import static android.service.notification.NotificationListenerService.REASON_PACKAGE_SUSPENDED;
import static android.service.notification.NotificationListenerService.REASON_PROFILE_TURNED_OFF;
import static android.service.notification.NotificationListenerService.REASON_SNOOZED;
import static android.service.notification.NotificationListenerService.REASON_TIMEOUT;
import static android.service.notification.NotificationListenerService.REASON_UNAUTOBUNDLED;
import static android.service.notification.NotificationListenerService.REASON_USER_STOPPED;
import static com.android.systemui.statusbar.notification.collection.NotificationEntry.DismissState.DISMISSED;
import static com.android.systemui.statusbar.notification.collection.NotificationEntry.DismissState.NOT_DISMISSED;
import static com.android.systemui.statusbar.notification.collection.NotificationEntry.DismissState.PARENT_DISMISSED;
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.SystemClock;
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;
import com.android.internal.statusbar.IStatusBarService;
import com.android.systemui.Dumpable;
import com.android.systemui.dump.DumpManager;
import com.android.systemui.statusbar.FeatureFlags;
import com.android.systemui.statusbar.notification.collection.coalescer.CoalescedEvent;
import com.android.systemui.statusbar.notification.collection.coalescer.GroupCoalescer;
import com.android.systemui.statusbar.notification.collection.coalescer.GroupCoalescer.BatchableNotificationHandler;
import com.android.systemui.statusbar.notification.collection.notifcollection.BindEntryEvent;
import com.android.systemui.statusbar.notification.collection.notifcollection.CleanUpEntryEvent;
import com.android.systemui.statusbar.notification.collection.notifcollection.CollectionReadyForBuildListener;
import com.android.systemui.statusbar.notification.collection.notifcollection.DismissedByUserStats;
import com.android.systemui.statusbar.notification.collection.notifcollection.EntryAddedEvent;
import com.android.systemui.statusbar.notification.collection.notifcollection.EntryRemovedEvent;
import com.android.systemui.statusbar.notification.collection.notifcollection.EntryUpdatedEvent;
import com.android.systemui.statusbar.notification.collection.notifcollection.InitEntryEvent;
import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener;
import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionLogger;
import com.android.systemui.statusbar.notification.collection.notifcollection.NotifDismissInterceptor;
import com.android.systemui.statusbar.notification.collection.notifcollection.NotifEvent;
import com.android.systemui.statusbar.notification.collection.notifcollection.NotifLifetimeExtender;
import com.android.systemui.statusbar.notification.collection.notifcollection.RankingAppliedEvent;
import com.android.systemui.statusbar.notification.collection.notifcollection.RankingUpdatedEvent;
import com.android.systemui.util.Assert;
import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Queue;
import javax.inject.Inject;
import javax.inject.Singleton;
/**
* Keeps a record of all of the "active" notifications, i.e. the notifications that are currently
* posted to the phone. This collection is unsorted, ungrouped, and unfiltered. Just because a
* notification appears in this collection doesn't mean that it's currently present in the shade
* (notifications can be hidden for a variety of reasons). Code that cares about what notifications
* are *visible* right now should register listeners later in the pipeline.
*
* Each notification is represented by a {@link NotificationEntry}, which is itself made up of two
* parts: a {@link StatusBarNotification} and a {@link Ranking}. When notifications are updated,
* their underlying SBNs and Rankings are swapped out, but the enclosing NotificationEntry (and its
* associated key) remain the same. In general, an SBN can only be updated when the notification is
* reposted by the source app; Rankings are updated much more often, usually every time there is an
* update from any kind from NotificationManager.
*
* In general, this collection closely mirrors the list maintained by NotificationManager, but it
* can occasionally diverge due to lifetime extenders (see
* {@link #addNotificationLifetimeExtender(NotifLifetimeExtender)}).
*
* Interested parties can register listeners
* ({@link #addCollectionListener(NotifCollectionListener)}) to be informed when notifications
* events occur.
*/
@MainThread
@Singleton
public class NotifCollection implements Dumpable {
private final IStatusBarService mStatusBarService;
private final FeatureFlags mFeatureFlags;
private final NotifCollectionLogger mLogger;
private final Map<String, NotificationEntry> mNotificationSet = new ArrayMap<>();
private final Collection<NotificationEntry> mReadOnlyNotificationSet =
Collections.unmodifiableCollection(mNotificationSet.values());
@Nullable private CollectionReadyForBuildListener mBuildListener;
private final List<NotifCollectionListener> mNotifCollectionListeners = new ArrayList<>();
private final List<NotifLifetimeExtender> mLifetimeExtenders = new ArrayList<>();
private final List<NotifDismissInterceptor> mDismissInterceptors = new ArrayList<>();
private Queue<NotifEvent> mEventQueue = new ArrayDeque<>();
private boolean mAttached = false;
private boolean mAmDispatchingToOtherCode;
@Inject
public NotifCollection(
IStatusBarService statusBarService,
DumpManager dumpManager,
FeatureFlags featureFlags,
NotifCollectionLogger logger) {
Assert.isMainThread();
mStatusBarService = statusBarService;
mLogger = logger;
dumpManager.registerDumpable(TAG, this);
mFeatureFlags = featureFlags;
}
/** Initializes the NotifCollection and registers it to receive notification events. */
public void attach(GroupCoalescer groupCoalescer) {
Assert.isMainThread();
if (mAttached) {
throw new RuntimeException("attach() called twice");
}
mAttached = true;
groupCoalescer.setNotificationHandler(mNotifHandler);
}
/**
* Sets the class responsible for converting the collection into the list of currently-visible
* notifications.
*/
void setBuildListener(CollectionReadyForBuildListener buildListener) {
Assert.isMainThread();
mBuildListener = buildListener;
}
/** @see NotifPipeline#getAllNotifs() */
Collection<NotificationEntry> getAllNotifs() {
Assert.isMainThread();
return mReadOnlyNotificationSet;
}
/** @see NotifPipeline#addCollectionListener(NotifCollectionListener) */
void addCollectionListener(NotifCollectionListener listener) {
Assert.isMainThread();
mNotifCollectionListeners.add(listener);
}
/** @see NotifPipeline#addNotificationLifetimeExtender(NotifLifetimeExtender) */
void addNotificationLifetimeExtender(NotifLifetimeExtender extender) {
Assert.isMainThread();
checkForReentrantCall();
if (mLifetimeExtenders.contains(extender)) {
throw new IllegalArgumentException("Extender " + extender + " already added.");
}
mLifetimeExtenders.add(extender);
extender.setCallback(this::onEndLifetimeExtension);
}
/** @see NotifPipeline#addNotificationDismissInterceptor(NotifDismissInterceptor) */
void addNotificationDismissInterceptor(NotifDismissInterceptor interceptor) {
Assert.isMainThread();
checkForReentrantCall();
if (mDismissInterceptors.contains(interceptor)) {
throw new IllegalArgumentException("Interceptor " + interceptor + " already added.");
}
mDismissInterceptors.add(interceptor);
interceptor.setCallback(this::onEndDismissInterception);
}
/**
* Dismisses multiple notifications on behalf of the user.
*/
public void dismissNotifications(
List<Pair<NotificationEntry, DismissedByUserStats>> entriesToDismiss) {
Assert.isMainThread();
checkForReentrantCall();
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;
requireNonNull(stats);
if (entry != mNotificationSet.get(entry.getKey())) {
throw new IllegalStateException("Invalid entry: " + entry.getKey());
}
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.
mLogger.logRemoteExceptionOnNotificationClear(entry.getKey(), e);
}
}
}
locallyDismissNotifications(entriesToLocallyDismiss);
dispatchEventsAndRebuildList();
}
/**
* Dismisses a single notification on behalf of the user.
*/
public void dismissNotification(
NotificationEntry entry,
@NonNull DismissedByUserStats stats) {
dismissNotifications(List.of(new Pair<>(entry, stats)));
}
/**
* 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.
mLogger.logRemoteExceptionOnClearAllNotifications(e);
}
final List<NotificationEntry> entries = new ArrayList<>(getAllNotifs());
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);
}
}
locallyDismissNotifications(entries);
dispatchEventsAndRebuildList();
}
/**
* 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);
}
}
}
}
}
}
// Immediately remove any dismissed notifs that have already been canceled by system server
// (probably due to being lifetime-extended up until this point).
for (NotificationEntry canceledEntry : canceledEntries) {
tryRemoveNotification(canceledEntry);
}
}
private void onNotificationPosted(StatusBarNotification sbn, RankingMap rankingMap) {
Assert.isMainThread();
postNotification(sbn, requireRanking(rankingMap, sbn.getKey()));
applyRanking(rankingMap);
dispatchEventsAndRebuildList();
}
private void onNotificationGroupPosted(List<CoalescedEvent> batch) {
Assert.isMainThread();
mLogger.logNotifGroupPosted(batch.get(0).getSbn().getGroupKey(), batch.size());
for (CoalescedEvent event : batch) {
postNotification(event.getSbn(), event.getRanking());
}
dispatchEventsAndRebuildList();
}
private void onNotificationRemoved(
StatusBarNotification sbn,
RankingMap rankingMap,
int reason) {
Assert.isMainThread();
mLogger.logNotifRemoved(sbn.getKey(), reason);
final NotificationEntry entry = mNotificationSet.get(sbn.getKey());
if (entry == null) {
throw new IllegalStateException("No notification to remove with key " + sbn.getKey());
}
entry.mCancellationReason = reason;
tryRemoveNotification(entry);
applyRanking(rankingMap);
dispatchEventsAndRebuildList();
}
private void onNotificationRankingUpdate(RankingMap rankingMap) {
Assert.isMainThread();
mEventQueue.add(new RankingUpdatedEvent(rankingMap));
applyRanking(rankingMap);
dispatchEventsAndRebuildList();
}
private void postNotification(
StatusBarNotification sbn,
Ranking ranking) {
NotificationEntry entry = mNotificationSet.get(sbn.getKey());
if (entry == null) {
// A new notification!
entry = new NotificationEntry(sbn, ranking, SystemClock.uptimeMillis());
mEventQueue.add(new BindEntryEvent(entry, sbn));
mNotificationSet.put(sbn.getKey(), entry);
mLogger.logNotifPosted(sbn.getKey());
mEventQueue.add(new InitEntryEvent(entry));
mEventQueue.add(new EntryAddedEvent(entry));
} else {
// Update to an existing entry
// Notification is updated so it is essentially re-added and thus alive again, so we
// can reset its state.
// TODO: If a coalesced event ever gets here, it's possible to lose track of children,
// since their rankings might have been updated earlier (and thus we may no longer
// think a child is associated with this locally-dismissed entry).
cancelLocalDismissal(entry);
cancelLifetimeExtension(entry);
cancelDismissInterception(entry);
entry.mCancellationReason = REASON_NOT_CANCELED;
entry.setSbn(sbn);
mEventQueue.add(new BindEntryEvent(entry, sbn));
mLogger.logNotifUpdated(sbn.getKey());
mEventQueue.add(new EntryUpdatedEvent(entry));
}
}
/**
* Tries to remove a notification from the notification set. This removal may be blocked by
* lifetime extenders. Does not trigger a rebuild of the list; caller must do that manually.
*
* @return True if the notification was removed, false otherwise.
*/
private boolean tryRemoveNotification(NotificationEntry entry) {
if (mNotificationSet.get(entry.getKey()) != entry) {
throw new IllegalStateException("No notification to remove with key " + entry.getKey());
}
if (!isCanceled(entry)) {
throw new IllegalStateException("Cannot remove notification " + entry.getKey()
+ ": has not been marked for removal");
}
if (isDismissedByUser(entry)) {
// User-dismissed notifications cannot be lifetime-extended
cancelLifetimeExtension(entry);
} else {
updateLifetimeExtension(entry);
}
if (!isLifetimeExtended(entry)) {
mNotificationSet.remove(entry.getKey());
cancelDismissInterception(entry);
mEventQueue.add(new EntryRemovedEvent(entry, entry.mCancellationReason));
mEventQueue.add(new CleanUpEntryEvent(entry));
return true;
} else {
return false;
}
}
private void applyRanking(@NonNull RankingMap rankingMap) {
for (NotificationEntry entry : mNotificationSet.values()) {
if (!isCanceled(entry)) {
// TODO: (b/148791039) We should crash if we are ever handed a ranking with
// incomplete entries. Right now, there's a race condition in NotificationListener
// that means this might occur when SystemUI is starting up.
Ranking ranking = new Ranking();
if (rankingMap.getRanking(entry.getKey(), ranking)) {
entry.setRanking(ranking);
// TODO: (b/145659174) update the sbn's overrideGroupKey in
// NotificationEntry.setRanking instead of here once we fully migrate to the
// NewNotifPipeline
if (mFeatureFlags.isNewNotifPipelineRenderingEnabled()) {
final String newOverrideGroupKey = ranking.getOverrideGroupKey();
if (!Objects.equals(entry.getSbn().getOverrideGroupKey(),
newOverrideGroupKey)) {
entry.getSbn().setOverrideGroupKey(newOverrideGroupKey);
}
}
} else {
mLogger.logRankingMissing(entry.getKey(), rankingMap);
}
}
}
mEventQueue.add(new RankingAppliedEvent());
}
private void dispatchEventsAndRebuildList() {
mAmDispatchingToOtherCode = true;
while (!mEventQueue.isEmpty()) {
mEventQueue.remove().dispatchTo(mNotifCollectionListeners);
}
mAmDispatchingToOtherCode = false;
if (mBuildListener != null) {
mBuildListener.onBuildList(mReadOnlyNotificationSet);
}
}
private void onEndLifetimeExtension(NotifLifetimeExtender extender, NotificationEntry entry) {
Assert.isMainThread();
if (!mAttached) {
return;
}
checkForReentrantCall();
if (!entry.mLifetimeExtenders.remove(extender)) {
throw new IllegalStateException(
String.format(
"Cannot end lifetime extension for extender \"%s\" (%s)",
extender.getName(),
extender));
}
mLogger.logLifetimeExtensionEnded(
entry.getKey(),
extender,
entry.mLifetimeExtenders.size());
if (!isLifetimeExtended(entry)) {
if (tryRemoveNotification(entry)) {
dispatchEventsAndRebuildList();
}
}
}
private void cancelLifetimeExtension(NotificationEntry entry) {
mAmDispatchingToOtherCode = true;
for (NotifLifetimeExtender extender : entry.mLifetimeExtenders) {
extender.cancelLifetimeExtension(entry);
}
mAmDispatchingToOtherCode = false;
entry.mLifetimeExtenders.clear();
}
private boolean isLifetimeExtended(NotificationEntry entry) {
return entry.mLifetimeExtenders.size() > 0;
}
private void updateLifetimeExtension(NotificationEntry entry) {
entry.mLifetimeExtenders.clear();
mAmDispatchingToOtherCode = true;
for (NotifLifetimeExtender extender : mLifetimeExtenders) {
if (extender.shouldExtendLifetime(entry, entry.mCancellationReason)) {
mLogger.logLifetimeExtended(entry.getKey(), extender);
entry.mLifetimeExtenders.add(extender);
}
}
mAmDispatchingToOtherCode = false;
}
private void updateDismissInterceptors(@NonNull NotificationEntry entry) {
entry.mDismissInterceptors.clear();
mAmDispatchingToOtherCode = true;
for (NotifDismissInterceptor interceptor : mDismissInterceptors) {
if (interceptor.shouldInterceptDismissal(entry)) {
entry.mDismissInterceptors.add(interceptor);
}
}
mAmDispatchingToOtherCode = false;
}
private void cancelLocalDismissal(NotificationEntry entry) {
if (isDismissedByUser(entry)) {
entry.setDismissState(NOT_DISMISSED);
if (entry.getSbn().getNotification().isGroupSummary()) {
for (NotificationEntry otherEntry : mNotificationSet.values()) {
if (otherEntry.getSbn().getGroupKey().equals(entry.getSbn().getGroupKey())
&& otherEntry.getDismissState() == PARENT_DISMISSED) {
otherEntry.setDismissState(NOT_DISMISSED);
}
}
}
}
}
private void onEndDismissInterception(
NotifDismissInterceptor interceptor,
NotificationEntry entry,
@NonNull DismissedByUserStats stats) {
Assert.isMainThread();
if (!mAttached) {
return;
}
checkForReentrantCall();
if (!entry.mDismissInterceptors.remove(interceptor)) {
throw new IllegalStateException(
String.format(
"Cannot end dismiss interceptor for interceptor \"%s\" (%s)",
interceptor.getName(),
interceptor));
}
if (!isDismissIntercepted(entry)) {
dismissNotification(entry, stats);
}
}
private void cancelDismissInterception(NotificationEntry entry) {
mAmDispatchingToOtherCode = true;
for (NotifDismissInterceptor interceptor : entry.mDismissInterceptors) {
interceptor.cancelDismissInterception(entry);
}
mAmDispatchingToOtherCode = false;
entry.mDismissInterceptors.clear();
}
private boolean isDismissIntercepted(NotificationEntry entry) {
return entry.mDismissInterceptors.size() > 0;
}
private void checkForReentrantCall() {
if (mAmDispatchingToOtherCode) {
throw new IllegalStateException("Reentrant call detected");
}
}
private static Ranking requireRanking(RankingMap rankingMap, String key) {
// TODO: Modify RankingMap so that we don't have to make a copy here
Ranking ranking = new Ranking();
if (!rankingMap.getRanking(key, ranking)) {
throw new IllegalArgumentException("Ranking map doesn't contain key: " + key);
}
return ranking;
}
/**
* True if the notification has been canceled by system server. Usually, such notifications are
* immediately removed from the collection, but can sometimes stick around due to lifetime
* extenders.
*/
private static boolean isCanceled(NotificationEntry entry) {
return entry.mCancellationReason != REASON_NOT_CANCELED;
}
private static boolean isDismissedByUser(NotificationEntry entry) {
return entry.getDismissState() != NOT_DISMISSED;
}
/**
* When a group summary is dismissed, NotificationManager will also try to dismiss its children.
* Returns true if we think dismissing the group summary with group key
* <code>dismissedGroupKey</code> will cause NotificationManager to also dismiss
* <code>entry</code>.
*
* See NotificationManager.cancelGroupChildrenByListLocked() for corresponding code.
*/
private static boolean shouldAutoDismissChildren(
NotificationEntry entry,
String dismissedGroupKey) {
return entry.getSbn().getGroupKey().equals(dismissedGroupKey)
&& !entry.getSbn().getNotification().isGroupSummary()
&& !hasFlag(entry, Notification.FLAG_FOREGROUND_SERVICE)
&& !hasFlag(entry, Notification.FLAG_BUBBLE)
&& 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) {
return userIdMatches(entry, userId)
&& entry.isClearable()
&& !hasFlag(entry, Notification.FLAG_BUBBLE)
&& 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;
}
@Override
public void dump(@NonNull FileDescriptor fd, PrintWriter pw, @NonNull String[] args) {
final List<NotificationEntry> entries = new ArrayList<>(getAllNotifs());
pw.println("\t" + TAG + " unsorted/unfiltered notifications:");
if (entries.size() == 0) {
pw.println("\t\t None");
}
pw.println(
ListDumper.dumpList(
entries,
true,
"\t\t"));
}
private final BatchableNotificationHandler mNotifHandler = new BatchableNotificationHandler() {
@Override
public void onNotificationPosted(StatusBarNotification sbn, RankingMap rankingMap) {
NotifCollection.this.onNotificationPosted(sbn, rankingMap);
}
@Override
public void onNotificationBatchPosted(List<CoalescedEvent> events) {
NotifCollection.this.onNotificationGroupPosted(events);
}
@Override
public void onNotificationRemoved(StatusBarNotification sbn, RankingMap rankingMap) {
NotifCollection.this.onNotificationRemoved(sbn, rankingMap, REASON_UNKNOWN);
}
@Override
public void onNotificationRemoved(StatusBarNotification sbn, RankingMap rankingMap,
int reason) {
NotifCollection.this.onNotificationRemoved(sbn, rankingMap, reason);
}
@Override
public void onNotificationRankingUpdate(RankingMap rankingMap) {
NotifCollection.this.onNotificationRankingUpdate(rankingMap);
}
};
private static final String TAG = "NotifCollection";
@IntDef(prefix = { "REASON_" }, value = {
REASON_NOT_CANCELED,
REASON_UNKNOWN,
REASON_CLICK,
REASON_CANCEL_ALL,
REASON_ERROR,
REASON_PACKAGE_CHANGED,
REASON_USER_STOPPED,
REASON_PACKAGE_BANNED,
REASON_APP_CANCEL,
REASON_APP_CANCEL_ALL,
REASON_LISTENER_CANCEL,
REASON_LISTENER_CANCEL_ALL,
REASON_GROUP_SUMMARY_CANCELED,
REASON_GROUP_OPTIMIZATION,
REASON_PACKAGE_SUSPENDED,
REASON_PROFILE_TURNED_OFF,
REASON_UNAUTOBUNDLED,
REASON_CHANNEL_BANNED,
REASON_SNOOZED,
REASON_TIMEOUT,
})
@Retention(RetentionPolicy.SOURCE)
public @interface CancellationReason {}
static final int REASON_NOT_CANCELED = -1;
public static final int REASON_UNKNOWN = 0;
}