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

Commit 5385b5c7 authored by Jeff DeCew's avatar Jeff DeCew
Browse files

New Pipeline: Update NotificationLogger to use NotifPipeline

Fixes: 204764064
Test: atest NotifPipelineTest NotificationLoggerTest NotificationLoggerLegacyTest
Change-Id: I4a4422a898c908439ba914703561e89b3ad81b21
parent 1c8ed43c
Loading
Loading
Loading
Loading
+91 −110
Original line number Diff line number Diff line
@@ -13,32 +13,25 @@
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.systemui.statusbar.notification.collection;

import androidx.annotation.Nullable;

import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeFinalizeFilterListener;
import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeRenderListListener;
import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeSortListener;
import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeTransformGroupsListener;
import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.Invalidator;
import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifComparator;
import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifFilter;
import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifPromoter;
import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSectioner;
import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifStabilityManager;
import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection;
import com.android.systemui.statusbar.notification.collection.notifcollection.InternalNotifUpdater;
import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener;
import com.android.systemui.statusbar.notification.collection.notifcollection.NotifDismissInterceptor;
import com.android.systemui.statusbar.notification.collection.notifcollection.NotifLifetimeExtender;

import java.util.Collection;
import java.util.List;

import javax.inject.Inject;
package com.android.systemui.statusbar.notification.collection

import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeFinalizeFilterListener
import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeRenderListListener
import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeSortListener
import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeTransformGroupsListener
import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.Invalidator
import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifComparator
import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifFilter
import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifPromoter
import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSectioner
import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifStabilityManager
import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection
import com.android.systemui.statusbar.notification.collection.notifcollection.InternalNotifUpdater
import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener
import com.android.systemui.statusbar.notification.collection.notifcollection.NotifDismissInterceptor
import com.android.systemui.statusbar.notification.collection.notifcollection.NotifLifetimeExtender
import javax.inject.Inject

/**
 * The system that constructs the "shade list", the filtered, grouped, and sorted list of
@@ -50,42 +43,33 @@ import javax.inject.Inject;
 * This list differs from the canonical one we receive from system server in a few ways:
 * - Filtered: Some notifications are filtered out. For example, we filter out notifications whose
 *   views haven't been inflated yet. We also filter out some notifications if we're on the lock
 *   screen and notifications for other users. So participate, see
 *   {@link #addPreGroupFilter} and similar methods.
 *   screen and notifications for other users. To participate, see
 *   [.addPreGroupFilter] and similar methods.
 * - Grouped: Notifications that are part of the same group are clustered together into a single
 *   GroupEntry. These groups are then transformed in order to remove children or completely split
 *   them apart. To participate, see {@link #addPromoter}.
 *   them apart. To participate, see [.addPromoter].
 * - Sorted: All top-level notifications are sorted. To participate, see
 *   {@link #setSections} and {@link #setComparators}
 *   [.setSections] and [.setComparators]
 *
 * The exact order of all hooks is as follows:
 *  0. Collection listeners are fired ({@link #addCollectionListener}).
 *  1. Pre-group filters are fired on each notification ({@link #addPreGroupFilter}).
 *  0. Collection listeners are fired ([.addCollectionListener]).
 *  1. Pre-group filters are fired on each notification ([.addPreGroupFilter]).
 *  2. Initial grouping is performed (NotificationEntries will have their parents set
 *     appropriately).
 *  3. OnBeforeTransformGroupListeners are fired ({@link #addOnBeforeTransformGroupsListener})
 *  4. NotifPromoters are called on each notification with a parent ({@link #addPromoter})
 *  5. OnBeforeSortListeners are fired ({@link #addOnBeforeSortListener})
 *  6. Top-level entries are assigned sections by NotifSections ({@link #setSections})
 *  7. Top-level entries within the same section are sorted by NotifComparators
 *     ({@link #setComparators})
 *  8. Finalize filters are fired on each notification ({@link #addFinalizeFilter})
 *  9. OnBeforeRenderListListeners are fired ({@link #addOnBeforeRenderListListener})
 *  9. The list is handed off to the view layer to be rendered
 *  3. OnBeforeTransformGroupListeners are fired ([.addOnBeforeTransformGroupsListener])
 *  4. NotifPromoters are called on each notification with a parent ([.addPromoter])
 *  5. Finalize filters are fired on each notification ([.addFinalizeFilter])
 *  6. OnBeforeSortListeners are fired ([.addOnBeforeSortListener])
 *  7. Top-level entries are assigned sections by NotifSections ([.setSections])
 *  8. Top-level entries within the same section are sorted by NotifComparators ([.setComparators])
 *  9. OnBeforeRenderListListeners are fired ([.addOnBeforeRenderListListener])
 *  10. The list is handed off to the view layer to be rendered
 */
@SysUISingleton
public class NotifPipeline implements CommonNotifCollection {
    private final NotifCollection mNotifCollection;
    private final ShadeListBuilder mShadeListBuilder;

    @Inject
    public NotifPipeline(
            NotifCollection notifCollection,
            ShadeListBuilder shadeListBuilder) {
        mNotifCollection = notifCollection;
        mShadeListBuilder = shadeListBuilder;
    }

class NotifPipeline @Inject constructor(
    private val mNotifCollection: NotifCollection,
    private val mShadeListBuilder: ShadeListBuilder
) : CommonNotifCollection {
    /**
     * Returns the list of all known notifications, i.e. the notifications that are currently posted
     * to the phone. In general, this tracks closely to the list maintained by NotificationManager,
@@ -93,39 +77,35 @@ public class NotifPipeline implements CommonNotifCollection {
     *
     * The returned collection is read-only, unsorted, unfiltered, and ungrouped.
     */
    @Override
    public Collection<NotificationEntry> getAllNotifs() {
        return mNotifCollection.getAllNotifs();
    override fun getAllNotifs(): Collection<NotificationEntry> {
        return mNotifCollection.allNotifs
    }

    @Override
    public void addCollectionListener(NotifCollectionListener listener) {
        mNotifCollection.addCollectionListener(listener);
    override fun addCollectionListener(listener: NotifCollectionListener) {
        mNotifCollection.addCollectionListener(listener)
    }

    /**
     * Returns the NotificationEntry associated with [key].
     */
    @Override
    @Nullable
    public NotificationEntry getEntry(String key) {
        return mNotifCollection.getEntry(key);
    override fun getEntry(key: String): NotificationEntry? {
        return mNotifCollection.getEntry(key)
    }

    /**
     * Registers a lifetime extender. Lifetime extenders can cause notifications that have been
     * dismissed or retracted by system server to be temporarily retained in the collection.
     */
    public void addNotificationLifetimeExtender(NotifLifetimeExtender extender) {
        mNotifCollection.addNotificationLifetimeExtender(extender);
    fun addNotificationLifetimeExtender(extender: NotifLifetimeExtender) {
        mNotifCollection.addNotificationLifetimeExtender(extender)
    }

    /**
     * Registers a dismiss interceptor. Dismiss interceptors can cause notifications that have been
     * dismissed by the user to be retained (won't send a dismissal to system server).
     */
    public void addNotificationDismissInterceptor(NotifDismissInterceptor interceptor) {
        mNotifCollection.addNotificationDismissInterceptor(interceptor);
    fun addNotificationDismissInterceptor(interceptor: NotifDismissInterceptor) {
        mNotifCollection.addNotificationDismissInterceptor(interceptor)
    }

    /**
@@ -134,16 +114,16 @@ public class NotifPipeline implements CommonNotifCollection {
     * returns true, the notification is removed from the pipeline (and no other filters are
     * called on that notif).
     */
    public void addPreGroupFilter(NotifFilter filter) {
        mShadeListBuilder.addPreGroupFilter(filter);
    fun addPreGroupFilter(filter: NotifFilter) {
        mShadeListBuilder.addPreGroupFilter(filter)
    }

    /**
     * Called after notifications have been filtered and after the initial grouping has been
     * performed but before NotifPromoters have had a chance to promote children out of groups.
     */
    public void addOnBeforeTransformGroupsListener(OnBeforeTransformGroupsListener listener) {
        mShadeListBuilder.addOnBeforeTransformGroupsListener(listener);
    fun addOnBeforeTransformGroupsListener(listener: OnBeforeTransformGroupsListener) {
        mShadeListBuilder.addOnBeforeTransformGroupsListener(listener)
    }

    /**
@@ -153,34 +133,34 @@ public class NotifPipeline implements CommonNotifCollection {
     * registered. If any promoter returns true, the notification is removed from the group (and no
     * other promoters are called on it).
     */
    public void addPromoter(NotifPromoter promoter) {
        mShadeListBuilder.addPromoter(promoter);
    fun addPromoter(promoter: NotifPromoter) {
        mShadeListBuilder.addPromoter(promoter)
    }

    /**
     * Called after notifs have been filtered and groups have been determined but before sections
     * have been determined or the notifs have been sorted.
     */
    public void addOnBeforeSortListener(OnBeforeSortListener listener) {
        mShadeListBuilder.addOnBeforeSortListener(listener);
    fun addOnBeforeSortListener(listener: OnBeforeSortListener) {
        mShadeListBuilder.addOnBeforeSortListener(listener)
    }

    /**
     * Sections that are used to sort top-level entries.  If two entries have the same section,
     * NotifComparators are consulted. Sections from this list are called in order for each
     * notification passed through the pipeline. The first NotifSection to return true for
     * {@link NotifSectioner#isInSection(ListEntry)} sets the entry as part of its Section.
     * [NotifSectioner.isInSection] sets the entry as part of its Section.
     */
    public void setSections(List<NotifSectioner> sections) {
        mShadeListBuilder.setSectioners(sections);
    fun setSections(sections: List<NotifSectioner>) {
        mShadeListBuilder.setSectioners(sections)
    }

    /**
     * StabilityManager that is used to determine whether to suppress group and section changes.
     * This should only be set once.
     */
    public void setVisualStabilityManager(NotifStabilityManager notifStabilityManager) {
        mShadeListBuilder.setNotifStabilityManager(notifStabilityManager);
    fun setVisualStabilityManager(notifStabilityManager: NotifStabilityManager) {
        mShadeListBuilder.setNotifStabilityManager(notifStabilityManager)
    }

    /**
@@ -188,16 +168,16 @@ public class NotifPipeline implements CommonNotifCollection {
     * comparators are executed in order until one of them returns a non-zero result. If all return
     * zero, the pipeline falls back to sorting by rank (and, failing that, Notification.when).
     */
    public void setComparators(List<NotifComparator> comparators) {
        mShadeListBuilder.setComparators(comparators);
    fun setComparators(comparators: List<NotifComparator>) {
        mShadeListBuilder.setComparators(comparators)
    }

    /**
     * Called after notifs have been filtered once, grouped, and sorted but before the final
     * filtering.
     */
    public void addOnBeforeFinalizeFilterListener(OnBeforeFinalizeFilterListener listener) {
        mShadeListBuilder.addOnBeforeFinalizeFilterListener(listener);
    fun addOnBeforeFinalizeFilterListener(listener: OnBeforeFinalizeFilterListener) {
        mShadeListBuilder.addOnBeforeFinalizeFilterListener(listener)
    }

    /**
@@ -207,21 +187,21 @@ public class NotifPipeline implements CommonNotifCollection {
     * true, the notification is removed from the pipeline (and no other filters are called on that
     * notif).
     */
    public void addFinalizeFilter(NotifFilter filter) {
        mShadeListBuilder.addFinalizeFilter(filter);
    fun addFinalizeFilter(filter: NotifFilter) {
        mShadeListBuilder.addFinalizeFilter(filter)
    }

    /**
     * Called at the end of the pipeline after the notif list has been finalized but before it has
     * been handed off to the view layer.
     */
    public void addOnBeforeRenderListListener(OnBeforeRenderListListener listener) {
        mShadeListBuilder.addOnBeforeRenderListListener(listener);
    fun addOnBeforeRenderListListener(listener: OnBeforeRenderListListener) {
        mShadeListBuilder.addOnBeforeRenderListListener(listener)
    }

    /** Registers an invalidator that can be used to invalidate the entire notif list. */
    public void addPreRenderInvalidator(Invalidator invalidator) {
        mShadeListBuilder.addPreRenderInvalidator(invalidator);
    fun addPreRenderInvalidator(invalidator: Invalidator) {
        mShadeListBuilder.addPreRenderInvalidator(invalidator)
    }

    /**
@@ -231,8 +211,8 @@ public class NotifPipeline implements CommonNotifCollection {
     * @param name the name of the component that will update notifiations
     * @return an updater
     */
    public InternalNotifUpdater getInternalNotifUpdater(String name) {
        return mNotifCollection.getInternalNotifUpdater(name);
    fun getInternalNotifUpdater(name: String?): InternalNotifUpdater {
        return mNotifCollection.getInternalNotifUpdater(name)
    }

    /**
@@ -240,8 +220,20 @@ public class NotifPipeline implements CommonNotifCollection {
     * are currently present in the shade. If this method is called during pipeline execution it
     * will return the current state of the list, which will likely be only partially-generated.
     */
    public List<ListEntry> getShadeList() {
        return mShadeListBuilder.getShadeList();
    val shadeList: List<ListEntry>
        get() = mShadeListBuilder.shadeList

    /**
     * Constructs a flattened representation of the notification tree, where each group will have
     * the summary (if present) followed by the children.
     */
    fun getFlatShadeList(): List<NotificationEntry> = shadeList.flatMap { entry ->
        when (entry) {
            is NotificationEntry -> sequenceOf(entry)
            is GroupEntry -> (entry.summary?.let { sequenceOf(it) }.orEmpty() +
                    entry.children)
            else -> throw RuntimeException("Unexpected entry $entry")
        }
    }

    /**
@@ -250,20 +242,9 @@ public class NotifPipeline implements CommonNotifCollection {
     * 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;
    fun getShadeListCount(): Int = shadeList.sumOf { entry ->
        // include the summary in the count
        if (entry is GroupEntry) 1 + entry.children.size
        else 1
    }
}
 No newline at end of file
+2 −0
Original line number Diff line number Diff line
@@ -214,6 +214,7 @@ public interface NotificationsModule {
            FeatureFlags featureFlags,
            NotificationVisibilityProvider visibilityProvider,
            NotificationEntryManager entryManager,
            NotifPipeline notifPipeline,
            StatusBarStateController statusBarStateController,
            NotificationLogger.ExpansionStateLogger expansionStateLogger,
            NotificationPanelLogger notificationPanelLogger) {
@@ -223,6 +224,7 @@ public interface NotificationsModule {
                featureFlags,
                visibilityProvider,
                entryManager,
                notifPipeline,
                statusBarStateController,
                expansionStateLogger,
                notificationPanelLogger);
+39 −8
Original line number Diff line number Diff line
@@ -26,6 +26,7 @@ import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.Log;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import com.android.internal.annotations.GuardedBy;
@@ -40,7 +41,9 @@ import com.android.systemui.statusbar.NotificationListener;
import com.android.systemui.statusbar.StatusBarState;
import com.android.systemui.statusbar.notification.NotificationEntryListener;
import com.android.systemui.statusbar.notification.NotificationEntryManager;
import com.android.systemui.statusbar.notification.collection.NotifPipeline;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener;
import com.android.systemui.statusbar.notification.collection.render.NotificationVisibilityProvider;
import com.android.systemui.statusbar.notification.dagger.NotificationsModule;
import com.android.systemui.statusbar.notification.stack.ExpandableViewState;
@@ -75,6 +78,7 @@ public class NotificationLogger implements StateListener {
    private final FeatureFlags mFeatureFlags;
    private final NotificationVisibilityProvider mVisibilityProvider;
    private final NotificationEntryManager mEntryManager;
    private final NotifPipeline mNotifPipeline;
    private final NotificationPanelLogger mNotificationPanelLogger;
    private final ExpansionStateLogger mExpansionStateLogger;

@@ -131,9 +135,7 @@ public class NotificationLogger implements StateListener {
            //    notifications.
            // 3. Report newly visible and no-longer visible notifications.
            // 4. Keep currently visible notifications for next report.
            // TODO(b/204764064): support new pipeline
            mFeatureFlags.checkLegacyPipelineEnabled();
            List<NotificationEntry> activeNotifications = mEntryManager.getVisibleNotifications();
            List<NotificationEntry> activeNotifications = getVisibleNotifications();
            int N = activeNotifications.size();
            for (int i = 0; i < N; i++) {
                NotificationEntry entry = activeNotifications.get(i);
@@ -172,6 +174,14 @@ public class NotificationLogger implements StateListener {
        }
    };

    private List<NotificationEntry> getVisibleNotifications() {
        if (mFeatureFlags.isNewNotifPipelineRenderingEnabled()) {
            return mNotifPipeline.getFlatShadeList();
        } else {
            return mEntryManager.getVisibleNotifications();
        }
    }

    /**
     * Returns the location of the notification referenced by the given {@link NotificationEntry}.
     */
@@ -211,6 +221,7 @@ public class NotificationLogger implements StateListener {
            FeatureFlags featureFlags,
            NotificationVisibilityProvider visibilityProvider,
            NotificationEntryManager entryManager,
            NotifPipeline notifPipeline,
            StatusBarStateController statusBarStateController,
            ExpansionStateLogger expansionStateLogger,
            NotificationPanelLogger notificationPanelLogger) {
@@ -219,6 +230,7 @@ public class NotificationLogger implements StateListener {
        mFeatureFlags = featureFlags;
        mVisibilityProvider = visibilityProvider;
        mEntryManager = entryManager;
        mNotifPipeline = notifPipeline;
        mBarService = IStatusBarService.Stub.asInterface(
                ServiceManager.getService(Context.STATUS_BAR_SERVICE));
        mExpansionStateLogger = expansionStateLogger;
@@ -226,7 +238,15 @@ public class NotificationLogger implements StateListener {
        // Not expected to be destroyed, don't need to unsubscribe
        statusBarStateController.addCallback(this);

        entryManager.addNotificationEntryListener(new NotificationEntryListener() {
        if (mFeatureFlags.isNewNotifPipelineRenderingEnabled()) {
            registerNewPipelineListener();
        } else {
            registerLegacyListener();
        }
    }

    private void registerLegacyListener() {
        mEntryManager.addNotificationEntryListener(new NotificationEntryListener() {
            @Override
            public void onEntryRemoved(
                    NotificationEntry entry,
@@ -250,6 +270,20 @@ public class NotificationLogger implements StateListener {
        });
    }

    private void registerNewPipelineListener() {
        mNotifPipeline.addCollectionListener(new NotifCollectionListener() {
            @Override
            public void onEntryUpdated(@NonNull NotificationEntry entry, boolean fromSystem) {
                mExpansionStateLogger.onEntryUpdated(entry.getKey());
            }

            @Override
            public void onEntryRemoved(@NonNull NotificationEntry entry, int reason) {
                mExpansionStateLogger.onEntryRemoved(entry.getKey());
            }
        });
    }

    public void setUpWithContainer(NotificationListContainer listContainer) {
        mListContainer = listContainer;
    }
@@ -417,10 +451,7 @@ public class NotificationLogger implements StateListener {
        // Once we know panelExpanded and Dozing, turn logging on & off when appropriate
        boolean lockscreen = mLockscreen == null ? false : mLockscreen;
        if (mPanelExpanded && !mDozing) {
            // TODO(b/204764064): support new pipeline
            mFeatureFlags.checkLegacyPipelineEnabled();
            mNotificationPanelLogger.logPanelShown(lockscreen,
                    mEntryManager.getVisibleNotifications());
            mNotificationPanelLogger.logPanelShown(lockscreen, getVisibleNotifications());
            if (DEBUG) {
                Log.i(TAG, "Notification panel shown, lockscreen=" + lockscreen);
            }
+81 −0
Original line number Diff line number Diff line
/*
 * 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 android.testing.AndroidTestingRunner
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.MockitoAnnotations
import org.mockito.Mockito.`when` as whenever

@SmallTest
@RunWith(AndroidTestingRunner::class)
class NotifPipelineTest : SysuiTestCase() {

    @Mock private lateinit var notifCollection: NotifCollection
    @Mock private lateinit var shadeListBuilder: ShadeListBuilder
    private lateinit var notifPipeline: NotifPipeline

    @Before
    fun setup() {
        MockitoAnnotations.initMocks(this)
        notifPipeline = NotifPipeline(notifCollection, shadeListBuilder)
        whenever(shadeListBuilder.shadeList).thenReturn(listOf(
                NotificationEntryBuilder().setPkg("foo").setId(1).build(),
                NotificationEntryBuilder().setPkg("foo").setId(2).build(),
                group(
                        NotificationEntryBuilder().setPkg("bar").setId(1).build(),
                        NotificationEntryBuilder().setPkg("bar").setId(2).build(),
                        NotificationEntryBuilder().setPkg("bar").setId(3).build(),
                        NotificationEntryBuilder().setPkg("bar").setId(4).build()
                ),
                NotificationEntryBuilder().setPkg("baz").setId(1).build()
        ))
    }

    private fun group(summary: NotificationEntry, vararg children: NotificationEntry): GroupEntry {
        return GroupEntry(summary.key, summary.creationTime).also { group ->
            group.summary = summary
            for (it in children) {
                group.addChild(it)
            }
        }
    }

    @Test
    fun testGetShadeListCount() {
        assertThat(notifPipeline.getShadeListCount()).isEqualTo(7)
    }

    @Test
    fun testGetFlatShadeList() {
        assertThat(notifPipeline.getFlatShadeList().map { it.key }).containsExactly(
                "0|foo|1|null|0",
                "0|foo|2|null|0",
                "0|bar|1|null|0",
                "0|bar|2|null|0",
                "0|bar|3|null|0",
                "0|bar|4|null|0",
                "0|baz|1|null|0"
        ).inOrder()
    }
}
+4 −4

File changed.

Preview size limit exceeded, changes collapsed.

Loading