Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 7342e3c3 authored by Ned Burns's avatar Ned Burns
Browse files

Change NotifCollection to use an event queue

Whenever some change needs to be applied to the NotifCollection (adding
a notification, a lifetime expiring, etc), we'd ideally like to first
make any appropriate changes to our internal state and only after all
changes have been made emit events to any listeners. This ensure that we
never emit an event while our internal state is in some inconsistent or
half-transitioned form.

Previously, we sidestepped this problem by carefully arranging
the order of our internal method calls so that things tended to work
out, but with the introduction of a new collection event,
onRankingApplied(), that is no longer a viable approach.

Now, whenever we need to emit a change event to listeners, we first push
that event to an event queue (mEventQueue). Only once the entire
mutation has been applied to our state do we then emit any events that
have built up in the queue (dispatchEventsAndRebuildList()).

In this CL:
- Move to event queue system for emitting events
- Add new event, onRankingApplied()
- Rearrange how and in what order applyRanking() is called. It is now
always called after the main mutation event. This means, for example,
that the order of events for adding a new notification is (initEntry,
addEntry, applyRanking).
- Clean up some minor test nits

Bug: 112656837
Test: atest SystemUITests
Change-Id: If1b69c19b3303718443dfe91fcde3e03113eea8c
parent 4723328c
Loading
Loading
Loading
Loading
+46 −80
Original line number Diff line number Diff line
@@ -64,24 +64,34 @@ 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.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;
@@ -124,6 +134,8 @@ public class NotifCollection implements Dumpable {
    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;

@@ -242,7 +254,7 @@ public class NotifCollection implements Dumpable {
        }

        locallyDismissNotifications(entriesToLocallyDismiss);
        rebuildList();
        dispatchEventsAndRebuildList();
    }

    /**
@@ -251,8 +263,7 @@ public class NotifCollection implements Dumpable {
    public void dismissNotification(
            NotificationEntry entry,
            @NonNull DismissedByUserStats stats) {
        dismissNotifications(List.of(
                new Pair<NotificationEntry, DismissedByUserStats>(entry, stats)));
        dismissNotifications(List.of(new Pair<>(entry, stats)));
    }

    /**
@@ -268,7 +279,7 @@ public class NotifCollection implements Dumpable {
            // system process is dead if we're here.
        }

        final List<NotificationEntry> entries = new ArrayList(getActiveNotifs());
        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)) {
@@ -283,7 +294,7 @@ public class NotifCollection implements Dumpable {
        }

        locallyDismissNotifications(entries);
        rebuildList();
        dispatchEventsAndRebuildList();
    }

    /**
@@ -326,8 +337,9 @@ public class NotifCollection implements Dumpable {
    private void onNotificationPosted(StatusBarNotification sbn, RankingMap rankingMap) {
        Assert.isMainThread();

        postNotification(sbn, requireRanking(rankingMap, sbn.getKey()), rankingMap);
        rebuildList();
        postNotification(sbn, requireRanking(rankingMap, sbn.getKey()));
        applyRanking(rankingMap);
        dispatchEventsAndRebuildList();
    }

    private void onNotificationGroupPosted(List<CoalescedEvent> batch) {
@@ -336,9 +348,9 @@ public class NotifCollection implements Dumpable {
        mLogger.logNotifGroupPosted(batch.get(0).getSbn().getGroupKey(), batch.size());

        for (CoalescedEvent event : batch) {
            postNotification(event.getSbn(), event.getRanking(), null);
            postNotification(event.getSbn(), event.getRanking());
        }
        rebuildList();
        dispatchEventsAndRebuildList();
    }

    private void onNotificationRemoved(
@@ -354,55 +366,49 @@ public class NotifCollection implements Dumpable {
            throw new IllegalStateException("No notification to remove with key " + sbn.getKey());
        }
        entry.mCancellationReason = reason;
        applyRanking(rankingMap);
        tryRemoveNotification(entry);
        rebuildList();
        applyRanking(rankingMap);
        dispatchEventsAndRebuildList();
    }

    private void onNotificationRankingUpdate(RankingMap rankingMap) {
        Assert.isMainThread();
        mEventQueue.add(new RankingUpdatedEvent(rankingMap));
        applyRanking(rankingMap);
        dispatchNotificationRankingUpdate(rankingMap);
        rebuildList();
        dispatchEventsAndRebuildList();
    }

    private void postNotification(
            StatusBarNotification sbn,
            Ranking ranking,
            @Nullable RankingMap rankingMap) {
            Ranking ranking) {
        NotificationEntry entry = mNotificationSet.get(sbn.getKey());

        if (entry == null) {
            // A new notification!
            mLogger.logNotifPosted(sbn.getKey());

            entry = new NotificationEntry(sbn, ranking);
            mNotificationSet.put(sbn.getKey(), entry);
            dispatchOnEntryInit(entry);

            if (rankingMap != null) {
                applyRanking(rankingMap);
            }

            dispatchOnEntryAdded(entry);
            mLogger.logNotifPosted(sbn.getKey());
            mEventQueue.add(new InitEntryEvent(entry));
            mEventQueue.add(new EntryAddedEvent(entry));

        } else {
            // Update to an existing entry
            mLogger.logNotifUpdated(sbn.getKey());

            // 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);
            if (rankingMap != null) {
                applyRanking(rankingMap);
            }

            dispatchOnEntryUpdated(entry);
            mLogger.logNotifUpdated(sbn.getKey());
            mEventQueue.add(new EntryUpdatedEvent(entry));
        }
    }

@@ -432,8 +438,8 @@ public class NotifCollection implements Dumpable {
        if (!isLifetimeExtended(entry)) {
            mNotificationSet.remove(entry.getKey());
            cancelDismissInterception(entry);
            dispatchOnEntryRemoved(entry, entry.mCancellationReason);
            dispatchOnEntryCleanUp(entry);
            mEventQueue.add(new EntryRemovedEvent(entry, entry.mCancellationReason));
            mEventQueue.add(new CleanUpEntryEvent(entry));
            return true;
        } else {
            return false;
@@ -466,9 +472,16 @@ public class NotifCollection implements Dumpable {
                }
            }
        }
        mEventQueue.add(new RankingAppliedEvent());
    }

    private void dispatchEventsAndRebuildList() {
        mAmDispatchingToOtherCode = true;
        while (!mEventQueue.isEmpty()) {
            mEventQueue.remove().dispatchTo(mNotifCollectionListeners);
        }
        mAmDispatchingToOtherCode = false;

    private void rebuildList() {
        if (mBuildListener != null) {
            mBuildListener.onBuildList(mReadOnlyNotificationSet);
        }
@@ -491,7 +504,7 @@ public class NotifCollection implements Dumpable {

        if (!isLifetimeExtended(entry)) {
            if (tryRemoveNotification(entry)) {
                rebuildList();
                dispatchEventsAndRebuildList();
            }
        }
    }
@@ -660,54 +673,6 @@ public class NotifCollection implements Dumpable {
                || entry.getSbn().getUser().getIdentifier() == userId;
    }

    private void dispatchOnEntryInit(NotificationEntry entry) {
        mAmDispatchingToOtherCode = true;
        for (NotifCollectionListener listener : mNotifCollectionListeners) {
            listener.onEntryInit(entry);
        }
        mAmDispatchingToOtherCode = false;
    }

    private void dispatchOnEntryAdded(NotificationEntry entry) {
        mAmDispatchingToOtherCode = true;
        for (NotifCollectionListener listener : mNotifCollectionListeners) {
            listener.onEntryAdded(entry);
        }
        mAmDispatchingToOtherCode = false;
    }

    private void dispatchOnEntryUpdated(NotificationEntry entry) {
        mAmDispatchingToOtherCode = true;
        for (NotifCollectionListener listener : mNotifCollectionListeners) {
            listener.onEntryUpdated(entry);
        }
        mAmDispatchingToOtherCode = false;
    }

    private void dispatchNotificationRankingUpdate(RankingMap map) {
        mAmDispatchingToOtherCode = true;
        for (NotifCollectionListener listener : mNotifCollectionListeners) {
            listener.onRankingUpdate(map);
        }
        mAmDispatchingToOtherCode = false;
    }

    private void dispatchOnEntryRemoved(NotificationEntry entry, @CancellationReason int reason) {
        mAmDispatchingToOtherCode = true;
        for (NotifCollectionListener listener : mNotifCollectionListeners) {
            listener.onEntryRemoved(entry, reason);
        }
        mAmDispatchingToOtherCode = false;
    }

    private void dispatchOnEntryCleanUp(NotificationEntry entry) {
        mAmDispatchingToOtherCode = true;
        for (NotifCollectionListener listener : mNotifCollectionListeners) {
            listener.onEntryCleanUp(entry);
        }
        mAmDispatchingToOtherCode = false;
    }

    @Override
    public void dump(@NonNull FileDescriptor fd, PrintWriter pw, @NonNull String[] args) {
        final List<NotificationEntry> entries = new ArrayList<>(getActiveNotifs());
@@ -754,6 +719,7 @@ public class NotifCollection implements Dumpable {
    private static final String TAG = "NotifCollection";

    @IntDef(prefix = { "REASON_" }, value = {
            REASON_NOT_CANCELED,
            REASON_UNKNOWN,
            REASON_CLICK,
            REASON_CANCEL_ALL,
+18 −2
Original line number Diff line number Diff line
@@ -70,8 +70,24 @@ public interface NotifCollectionListener {
    }

    /**
     * Called whenever the RankingMap is updated by system server. By the time this listener is
     * called, the Rankings of all entries will have been updated.
     * Called whenever a ranking update is applied. During a ranking update, all active,
     * non-lifetime-extended notification entries will have their ranking object updated.
     *
     * Ranking updates occur whenever a notification is added, updated, or removed, or when a
     * standalone ranking is sent from the server.
     */
    default void onRankingApplied() {
    }

    /**
     * Called whenever system server sends a standalone ranking update (i.e. one that isn't
     * associated with a notification being added or removed).
     *
     * In general it is unsafe to depend on this method as rankings can change for other reasons.
     * Instead, listen for {@link #onRankingApplied()}, which is called whenever ANY ranking update
     * is applied, regardless of source.
     *
     * @deprecated Use {@link #onRankingApplied()} instead.
     */
    default void onRankingUpdate(NotificationListenerService.RankingMap rankingMap) {
    }
+93 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2020 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.notifcollection

import android.service.notification.NotificationListenerService.RankingMap
import com.android.systemui.statusbar.notification.collection.NotifCollection
import com.android.systemui.statusbar.notification.collection.NotificationEntry

/**
 * Set of classes that represent the various events that [NotifCollection] can dispatch to
 * [NotifCollectionListener]s.
 *
 * These events build up in a queue and are periodically emitted in chunks by the collection.
 */

sealed class NotifEvent {
    fun dispatchTo(listeners: List<NotifCollectionListener>) {
        for (i in listeners.indices) {
            dispatchToListener(listeners[i])
        }
    }

    abstract fun dispatchToListener(listener: NotifCollectionListener)
}

data class InitEntryEvent(
    val entry: NotificationEntry
) : NotifEvent() {
    override fun dispatchToListener(listener: NotifCollectionListener) {
        listener.onEntryInit(entry)
    }
}

data class EntryAddedEvent(
    val entry: NotificationEntry
) : NotifEvent() {
    override fun dispatchToListener(listener: NotifCollectionListener) {
        listener.onEntryAdded(entry)
    }
}

data class EntryUpdatedEvent(
    val entry: NotificationEntry
) : NotifEvent() {
    override fun dispatchToListener(listener: NotifCollectionListener) {
        listener.onEntryUpdated(entry)
    }
}

data class EntryRemovedEvent(
    val entry: NotificationEntry,
    val reason: Int
) : NotifEvent() {
    override fun dispatchToListener(listener: NotifCollectionListener) {
        listener.onEntryRemoved(entry, reason)
    }
}

data class CleanUpEntryEvent(
    val entry: NotificationEntry
) : NotifEvent() {
    override fun dispatchToListener(listener: NotifCollectionListener) {
        listener.onEntryCleanUp(entry)
    }
}

data class RankingUpdatedEvent(
    val rankingMap: RankingMap
) : NotifEvent() {
    override fun dispatchToListener(listener: NotifCollectionListener) {
        listener.onRankingUpdate(rankingMap)
    }
}

class RankingAppliedEvent() : NotifEvent() {
    override fun dispatchToListener(listener: NotifCollectionListener) {
        listener.onRankingApplied()
    }
}
+53 −30
Original line number Diff line number Diff line
@@ -34,16 +34,17 @@ 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.eq;
import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import static java.util.Collections.singletonList;
import static java.util.Objects.requireNonNull;

import android.annotation.Nullable;
@@ -83,13 +84,13 @@ import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.InOrder;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.Spy;

import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;

@@ -123,6 +124,8 @@ public class NotifCollectionTest extends SysuiTestCase {
    private NotifCollection mCollection;
    private BatchableNotificationHandler mNotifHandler;

    private InOrder mListenerInOrder;

    private NoManSimulator mNoMan;

    @Before
@@ -133,6 +136,8 @@ public class NotifCollectionTest extends SysuiTestCase {
        when(mFeatureFlags.isNewNotifPipelineRenderingEnabled()).thenReturn(true);
        when(mFeatureFlags.isNewNotifPipelineEnabled()).thenReturn(true);

        mListenerInOrder = inOrder(mCollectionListener);

        mCollection = new NotifCollection(
                mStatusBarService,
                mock(DumpManager.class),
@@ -159,10 +164,12 @@ public class NotifCollectionTest extends SysuiTestCase {
                        .setRank(4747));

        // THEN the listener is notified
        verify(mCollectionListener).onEntryInit(mEntryCaptor.capture());
        NotificationEntry entry = mEntryCaptor.getValue();
        final NotificationEntry entry = mCollectionListener.getEntry(notif1.key);

        mListenerInOrder.verify(mCollectionListener).onEntryInit(entry);
        mListenerInOrder.verify(mCollectionListener).onEntryAdded(entry);
        mListenerInOrder.verify(mCollectionListener).onRankingApplied();

        verify(mCollectionListener).onEntryAdded(entry);
        assertEquals(notif1.key, entry.getKey());
        assertEquals(notif1.sbn, entry.getSbn());
        assertEquals(notif1.ranking, entry.getRanking());
@@ -215,12 +222,11 @@ public class NotifCollectionTest extends SysuiTestCase {
        assertEquals(entry2.getRanking(), capturedUpdate.getRanking());

        // THEN onBuildList is called only once
        verify(mBuildListener).onBuildList(mBuildListCaptor.capture());
        assertEquals(new ArraySet<>(Arrays.asList(
        verifyBuiltList(
                List.of(
                        capturedAdds.get(0),
                        capturedAdds.get(1),
                capturedUpdate
        )), new ArraySet<>(mBuildListCaptor.getValue()));
                        capturedUpdate));
    }

    @Test
@@ -234,9 +240,11 @@ public class NotifCollectionTest extends SysuiTestCase {
                .setRank(89));

        // THEN the listener is notified
        verify(mCollectionListener).onEntryUpdated(mEntryCaptor.capture());
        final NotificationEntry entry = mCollectionListener.getEntry(notif2.key);

        mListenerInOrder.verify(mCollectionListener).onEntryUpdated(entry);
        mListenerInOrder.verify(mCollectionListener).onRankingApplied();

        NotificationEntry entry = mEntryCaptor.getValue();
        assertEquals(notif2.key, entry.getKey());
        assertEquals(notif2.sbn, entry.getSbn());
        assertEquals(notif2.ranking, entry.getRanking());
@@ -256,8 +264,10 @@ public class NotifCollectionTest extends SysuiTestCase {
        mNoMan.retractNotif(notif.sbn, REASON_APP_CANCEL);

        // THEN the listener is notified
        verify(mCollectionListener).onEntryRemoved(entry, REASON_APP_CANCEL);
        verify(mCollectionListener).onEntryCleanUp(entry);
        mListenerInOrder.verify(mCollectionListener).onEntryRemoved(entry, REASON_APP_CANCEL);
        mListenerInOrder.verify(mCollectionListener).onEntryCleanUp(entry);
        mListenerInOrder.verify(mCollectionListener).onRankingApplied();

        assertEquals(notif.sbn, entry.getSbn());
        assertEquals(notif.ranking, entry.getRanking());
    }
@@ -415,7 +425,7 @@ public class NotifCollectionTest extends SysuiTestCase {

        // THEN the dismissed entry still appears in the notification set
        assertEquals(
                new ArraySet<>(Collections.singletonList(entry1)),
                new ArraySet<>(singletonList(entry1)),
                new ArraySet<>(mCollection.getActiveNotifs()));
    }

@@ -561,7 +571,7 @@ public class NotifCollectionTest extends SysuiTestCase {
    }

    @Test
    public void testEndDismissInterceptionUpdatesDismissInterceptors() throws RemoteException {
    public void testEndDismissInterceptionUpdatesDismissInterceptors() {
        // GIVEN a collection with notifications with multiple dismiss interceptors
        mInterceptor1.shouldInterceptDismissal = true;
        mInterceptor2.shouldInterceptDismissal = true;
@@ -592,7 +602,7 @@ public class NotifCollectionTest extends SysuiTestCase {


    @Test(expected = IllegalStateException.class)
    public void testEndingDismissalOfNonInterceptedThrows() throws RemoteException {
    public void testEndingDismissalOfNonInterceptedThrows() {
        // GIVEN a collection with notifications with a dismiss interceptor that hasn't been called
        mInterceptor1.shouldInterceptDismissal = false;
        mCollection.addNotificationDismissInterceptor(mInterceptor1);
@@ -894,7 +904,7 @@ public class NotifCollectionTest extends SysuiTestCase {
        verify(mExtender3, never()).shouldExtendLifetime(entry2, REASON_APP_CANCEL);

        // THEN the entry properly records all extenders that returned true
        assertEquals(Arrays.asList(mExtender1), entry2.mLifetimeExtenders);
        assertEquals(singletonList(mExtender1), entry2.mLifetimeExtenders);
    }

    @Test
@@ -1055,11 +1065,11 @@ public class NotifCollectionTest extends SysuiTestCase {

        // WHEN both notifications are manually dismissed together
        mCollection.dismissNotifications(
                List.of(new Pair(entry1, defaultStats(entry1)),
                        new Pair(entry2, defaultStats(entry2))));
                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));
        verifyBuiltList(List.of(entry1, entry2));
    }

    @Test
@@ -1074,8 +1084,8 @@ public class NotifCollectionTest extends SysuiTestCase {
        DismissedByUserStats stats1 = defaultStats(entry1);
        DismissedByUserStats stats2 = defaultStats(entry2);
        mCollection.dismissNotifications(
                List.of(new Pair(entry1, defaultStats(entry1)),
                        new Pair(entry2, defaultStats(entry2))));
                List.of(new Pair<>(entry1, defaultStats(entry1)),
                        new Pair<>(entry2, defaultStats(entry2))));

        // THEN we send the dismissals to system server
        verify(mStatusBarService).onNotificationClear(
@@ -1109,8 +1119,8 @@ public class NotifCollectionTest extends SysuiTestCase {

        // WHEN both notifications are manually dismissed together
        mCollection.dismissNotifications(
                List.of(new Pair(entry1, defaultStats(entry1)),
                        new Pair(entry2, defaultStats(entry2))));
                List.of(new Pair<>(entry1, defaultStats(entry1)),
                        new Pair<>(entry2, defaultStats(entry2))));

        // THEN the entries are marked as dismissed
        assertEquals(DISMISSED, entry1.getDismissState());
@@ -1134,8 +1144,8 @@ public class NotifCollectionTest extends SysuiTestCase {

        // WHEN both notifications are manually dismissed together
        mCollection.dismissNotifications(
                List.of(new Pair(entry1, defaultStats(entry1)),
                        new Pair(entry2, defaultStats(entry2))));
                List.of(new Pair<>(entry1, defaultStats(entry1)),
                        new Pair<>(entry2, defaultStats(entry2))));

        // THEN all interceptors get checked
        verify(mInterceptor1).shouldInterceptDismissal(entry1);
@@ -1162,7 +1172,7 @@ public class NotifCollectionTest extends SysuiTestCase {
        mCollection.dismissAllNotifications(entry1.getSbn().getUser().getIdentifier());

        // THEN build list is only called one time
        verify(mBuildListener).onBuildList(any(Collection.class));
        verifyBuiltList(List.of(entry1, entry2));
    }

    @Test
@@ -1271,13 +1281,18 @@ public class NotifCollectionTest extends SysuiTestCase {
                NotificationVisibility.obtain(entry.getKey(), 7, 2, true));
    }

    public CollectionEvent postNotif(NotificationEntryBuilder builder) {
    private CollectionEvent postNotif(NotificationEntryBuilder builder) {
        clearInvocations(mCollectionListener);
        NotifEvent rawEvent = mNoMan.postNotif(builder);
        verify(mCollectionListener).onEntryAdded(mEntryCaptor.capture());
        return new CollectionEvent(rawEvent, requireNonNull(mEntryCaptor.getValue()));
    }

    private void verifyBuiltList(Collection<NotificationEntry> list) {
        verify(mBuildListener).onBuildList(mBuildListCaptor.capture());
        assertEquals(new ArraySet<>(list), new ArraySet<>(mBuildListCaptor.getValue()));
    }

    private static class RecordingCollectionListener implements NotifCollectionListener {
        private final Map<String, NotificationEntry> mLastSeenEntries = new ArrayMap<>();

@@ -1303,6 +1318,14 @@ public class NotifCollectionTest extends SysuiTestCase {
        public void onEntryCleanUp(NotificationEntry entry) {
        }

        @Override
        public void onRankingApplied() {
        }

        @Override
        public void onRankingUpdate(RankingMap rankingMap) {
        }

        public NotificationEntry getEntry(String key) {
            if (!mLastSeenEntries.containsKey(key)) {
                throw new RuntimeException("Key not found: " + key);