summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/CarSystemUI/src/com/android/systemui/statusbar/car/CarStatusBar.java2
-rw-r--r--packages/CarSystemUI/src/com/android/systemui/statusbar/car/CarStatusBarModule.java2
-rw-r--r--packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java6
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/CollectionReadyForBuildListener.java (renamed from packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifListBuilder.java)2
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/GroupEntry.java76
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ListDumper.java60
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ListEntry.java72
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java22
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifListBuilderImpl.java737
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java19
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/init/FakePipelineConsumer.java85
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/init/NewNotifPipeline.java (renamed from packages/SystemUI/src/com/android/systemui/statusbar/notification/NewNotifPipeline.java)29
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/NotifListBuilder.java117
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/OnBeforeRenderListListener.java33
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/OnBeforeSortListener.java33
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/OnBeforeTransformGroupsListener.java40
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/PipelineState.java97
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/pluggable/NotifComparator.java43
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/pluggable/NotifFilter.java45
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/pluggable/NotifPromoter.java42
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/pluggable/Pluggable.java71
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/pluggable/SectionsProvider.java37
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java2
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarModule.java2
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationEntryBuilder.java5
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/SbnBuilder.java30
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotifListBuilderImplTest.java1199
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarTest.java2
-rw-r--r--tests/StatusBar/src/com/android/statusbartest/StatusBarTest.java6
29 files changed, 2887 insertions, 29 deletions
diff --git a/packages/CarSystemUI/src/com/android/systemui/statusbar/car/CarStatusBar.java b/packages/CarSystemUI/src/com/android/systemui/statusbar/car/CarStatusBar.java
index 10527b231169..4e5a3a633c3e 100644
--- a/packages/CarSystemUI/src/com/android/systemui/statusbar/car/CarStatusBar.java
+++ b/packages/CarSystemUI/src/com/android/systemui/statusbar/car/CarStatusBar.java
@@ -96,12 +96,12 @@ import com.android.systemui.statusbar.SysuiStatusBarStateController;
import com.android.systemui.statusbar.VibratorHelper;
import com.android.systemui.statusbar.notification.BypassHeadsUpNotifier;
import com.android.systemui.statusbar.notification.DynamicPrivacyController;
-import com.android.systemui.statusbar.notification.NewNotifPipeline;
import com.android.systemui.statusbar.notification.NotificationAlertingManager;
import com.android.systemui.statusbar.notification.NotificationEntryManager;
import com.android.systemui.statusbar.notification.NotificationInterruptionStateProvider;
import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator;
import com.android.systemui.statusbar.notification.VisualStabilityManager;
+import com.android.systemui.statusbar.notification.collection.init.NewNotifPipeline;
import com.android.systemui.statusbar.notification.logging.NotificationLogger;
import com.android.systemui.statusbar.notification.row.NotificationGutsManager;
import com.android.systemui.statusbar.phone.AutoHideController;
diff --git a/packages/CarSystemUI/src/com/android/systemui/statusbar/car/CarStatusBarModule.java b/packages/CarSystemUI/src/com/android/systemui/statusbar/car/CarStatusBarModule.java
index 5418ebe5b8de..4813d6dfeb7e 100644
--- a/packages/CarSystemUI/src/com/android/systemui/statusbar/car/CarStatusBarModule.java
+++ b/packages/CarSystemUI/src/com/android/systemui/statusbar/car/CarStatusBarModule.java
@@ -56,12 +56,12 @@ import com.android.systemui.statusbar.SysuiStatusBarStateController;
import com.android.systemui.statusbar.VibratorHelper;
import com.android.systemui.statusbar.notification.BypassHeadsUpNotifier;
import com.android.systemui.statusbar.notification.DynamicPrivacyController;
-import com.android.systemui.statusbar.notification.NewNotifPipeline;
import com.android.systemui.statusbar.notification.NotificationAlertingManager;
import com.android.systemui.statusbar.notification.NotificationEntryManager;
import com.android.systemui.statusbar.notification.NotificationInterruptionStateProvider;
import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator;
import com.android.systemui.statusbar.notification.VisualStabilityManager;
+import com.android.systemui.statusbar.notification.collection.init.NewNotifPipeline;
import com.android.systemui.statusbar.notification.logging.NotificationLogger;
import com.android.systemui.statusbar.notification.row.NotificationGutsManager;
import com.android.systemui.statusbar.phone.AutoHideController;
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
index bcaad2225724..41ef8fcfdc53 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
@@ -28,6 +28,8 @@ import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.recents.Recents;
import com.android.systemui.stackdivider.Divider;
import com.android.systemui.statusbar.CommandQueue;
+import com.android.systemui.statusbar.notification.collection.NotifListBuilderImpl;
+import com.android.systemui.statusbar.notification.collection.listbuilder.NotifListBuilder;
import com.android.systemui.statusbar.notification.people.PeopleHubModule;
import com.android.systemui.statusbar.phone.KeyguardLiftController;
import com.android.systemui.statusbar.phone.StatusBar;
@@ -90,4 +92,8 @@ public abstract class SystemUIModule {
@Singleton
@Binds
abstract SystemClock bindSystemClock(SystemClockImpl systemClock);
+
+ @Singleton
+ @Binds
+ abstract NotifListBuilder bindNotifListBuilder(NotifListBuilderImpl impl);
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifListBuilder.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/CollectionReadyForBuildListener.java
index 17fef6850f97..cefb506b5233 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifListBuilder.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/CollectionReadyForBuildListener.java
@@ -22,7 +22,7 @@ import java.util.Collection;
* Interface for the class responsible for converting a NotifCollection into the final sorted,
* filtered, and grouped list of currently visible notifications.
*/
-public interface NotifListBuilder {
+public interface CollectionReadyForBuildListener {
/**
* Called after the NotifCollection has received an update from NotificationManager but before
* it dispatches any change events to its listeners. This is to inform the list builder that
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/GroupEntry.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/GroupEntry.java
new file mode 100644
index 000000000000..f9f3266f1afa
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/GroupEntry.java
@@ -0,0 +1,76 @@
+/*
+ * 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.annotation.Nullable;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+/**
+ * Represents a set of grouped notifications. The final notification list is usually a mix of
+ * GroupEntries and NotificationEntries.
+ */
+public class GroupEntry extends ListEntry {
+ @Nullable private NotificationEntry mSummary;
+ private final List<NotificationEntry> mChildren = new ArrayList<>();
+
+ private final List<NotificationEntry> mUnmodifiableChildren =
+ Collections.unmodifiableList(mChildren);
+
+ GroupEntry(String key) {
+ super(key);
+ }
+
+ @Override
+ public NotificationEntry getRepresentativeEntry() {
+ return mSummary;
+ }
+
+ @Nullable
+ public NotificationEntry getSummary() {
+ return mSummary;
+ }
+
+ public List<NotificationEntry> getChildren() {
+ return mUnmodifiableChildren;
+ }
+
+ void setSummary(@Nullable NotificationEntry summary) {
+ mSummary = summary;
+ }
+
+ void clearChildren() {
+ mChildren.clear();
+ }
+
+ void addChild(NotificationEntry child) {
+ mChildren.add(child);
+ }
+
+ void sortChildren(Comparator<? super NotificationEntry> c) {
+ mChildren.sort(c);
+ }
+
+ List<NotificationEntry> getRawChildren() {
+ return mChildren;
+ }
+
+ public static final GroupEntry ROOT_ENTRY = new GroupEntry("<root>");
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ListDumper.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ListDumper.java
new file mode 100644
index 000000000000..e1268f6d60ef
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ListDumper.java
@@ -0,0 +1,60 @@
+/*
+ * 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 com.android.systemui.statusbar.notification.collection.listbuilder.NotifListBuilder;
+
+import java.util.List;
+
+
+/**
+ * Utility class for dumping the results of a {@link NotifListBuilder} to a debug string.
+ */
+public class ListDumper {
+
+ /** See class description */
+ public static String dumpList(List<ListEntry> entries) {
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < entries.size(); i++) {
+ ListEntry entry = entries.get(i);
+ dumpEntry(entry, Integer.toString(i), "", sb);
+ if (entry instanceof GroupEntry) {
+ GroupEntry ge = (GroupEntry) entry;
+ for (int j = 0; j < ge.getChildren().size(); j++) {
+ dumpEntry(
+ ge.getChildren().get(j),
+ Integer.toString(j),
+ INDENT,
+ sb);
+ }
+ }
+ }
+ return sb.toString();
+ }
+
+ private static void dumpEntry(
+ ListEntry entry, String index, String indent, StringBuilder sb) {
+ sb.append(indent)
+ .append("[").append(index).append("] ")
+ .append(entry.getKey())
+ .append(" (parent=")
+ .append(entry.getParent() != null ? entry.getParent().getKey() : null)
+ .append(")\n");
+ }
+
+ private static final String INDENT = " ";
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ListEntry.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ListEntry.java
new file mode 100644
index 000000000000..dc68c4bdba78
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ListEntry.java
@@ -0,0 +1,72 @@
+/*
+ * 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.annotation.Nullable;
+
+/**
+ * Abstract superclass for top-level entries, i.e. things that can appear in the final notification
+ * list shown to users. In practice, this means either GroupEntries or NotificationEntries.
+ */
+public abstract class ListEntry {
+ private final String mKey;
+
+ @Nullable private GroupEntry mParent;
+ @Nullable private GroupEntry mPreviousParent;
+ private int mSection;
+ int mFirstAddedIteration = -1;
+
+ ListEntry(String key) {
+ mKey = key;
+ }
+
+ public String getKey() {
+ return mKey;
+ }
+
+ /**
+ * Should return the "representative entry" for this ListEntry. For NotificationEntries, its
+ * the entry itself. For groups, it should be the summary. This method exists to interface with
+ * legacy code that expects groups to also be NotificationEntries.
+ */
+ public abstract NotificationEntry getRepresentativeEntry();
+
+ @Nullable public GroupEntry getParent() {
+ return mParent;
+ }
+
+ void setParent(@Nullable GroupEntry parent) {
+ mParent = parent;
+ }
+
+ @Nullable public GroupEntry getPreviousParent() {
+ return mPreviousParent;
+ }
+
+ void setPreviousParent(@Nullable GroupEntry previousParent) {
+ mPreviousParent = previousParent;
+ }
+
+ /** The section this notification was assigned to (0 to N-1, where N is number of sections). */
+ public int getSection() {
+ return mSection;
+ }
+
+ void setSection(int section) {
+ mSection = section;
+ }
+}
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 b5513529d7ba..6f085c0ce7c0 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
@@ -95,7 +95,7 @@ public class NotifCollection {
private final Collection<NotificationEntry> mReadOnlyNotificationSet =
Collections.unmodifiableCollection(mNotificationSet.values());
- @Nullable private NotifListBuilder mListBuilder;
+ @Nullable private CollectionReadyForBuildListener mBuildListener;
private final List<NotifCollectionListener> mNotifCollectionListeners = new ArrayList<>();
private final List<NotifLifetimeExtender> mLifetimeExtenders = new ArrayList<>();
@@ -123,9 +123,9 @@ public class NotifCollection {
* Sets the class responsible for converting the collection into the list of currently-visible
* notifications.
*/
- public void setListBuilder(NotifListBuilder listBuilder) {
+ public void setBuildListener(CollectionReadyForBuildListener buildListener) {
Assert.isMainThread();
- mListBuilder = listBuilder;
+ mBuildListener = buildListener;
}
/**
@@ -282,8 +282,8 @@ public class NotifCollection {
}
private void rebuildList() {
- if (mListBuilder != null) {
- mListBuilder.onBuildList(mReadOnlyNotificationSet);
+ if (mBuildListener != null) {
+ mBuildListener.onBuildList(mReadOnlyNotificationSet);
}
}
@@ -339,8 +339,8 @@ public class NotifCollection {
private void dispatchOnEntryAdded(NotificationEntry entry) {
mAmDispatchingToOtherCode = true;
- if (mListBuilder != null) {
- mListBuilder.onBeginDispatchToListeners();
+ if (mBuildListener != null) {
+ mBuildListener.onBeginDispatchToListeners();
}
for (NotifCollectionListener listener : mNotifCollectionListeners) {
listener.onEntryAdded(entry);
@@ -350,8 +350,8 @@ public class NotifCollection {
private void dispatchOnEntryUpdated(NotificationEntry entry) {
mAmDispatchingToOtherCode = true;
- if (mListBuilder != null) {
- mListBuilder.onBeginDispatchToListeners();
+ if (mBuildListener != null) {
+ mBuildListener.onBeginDispatchToListeners();
}
for (NotifCollectionListener listener : mNotifCollectionListeners) {
listener.onEntryUpdated(entry);
@@ -364,8 +364,8 @@ public class NotifCollection {
@CancellationReason int reason,
boolean removedByUser) {
mAmDispatchingToOtherCode = true;
- if (mListBuilder != null) {
- mListBuilder.onBeginDispatchToListeners();
+ if (mBuildListener != null) {
+ mBuildListener.onBeginDispatchToListeners();
}
for (NotifCollectionListener listener : mNotifCollectionListeners) {
listener.onEntryRemoved(entry, reason, removedByUser);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifListBuilderImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifListBuilderImpl.java
new file mode 100644
index 000000000000..21a4b4f895e5
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifListBuilderImpl.java
@@ -0,0 +1,737 @@
+/*
+ * 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 com.android.systemui.statusbar.notification.collection.GroupEntry.ROOT_ENTRY;
+import static com.android.systemui.statusbar.notification.collection.ListDumper.dumpList;
+import static com.android.systemui.statusbar.notification.collection.listbuilder.PipelineState.STATE_BUILD_PENDING;
+import static com.android.systemui.statusbar.notification.collection.listbuilder.PipelineState.STATE_BUILD_STARTED;
+import static com.android.systemui.statusbar.notification.collection.listbuilder.PipelineState.STATE_FILTERING;
+import static com.android.systemui.statusbar.notification.collection.listbuilder.PipelineState.STATE_FINALIZING;
+import static com.android.systemui.statusbar.notification.collection.listbuilder.PipelineState.STATE_IDLE;
+import static com.android.systemui.statusbar.notification.collection.listbuilder.PipelineState.STATE_SORTING;
+import static com.android.systemui.statusbar.notification.collection.listbuilder.PipelineState.STATE_TRANSFORMING;
+
+import android.annotation.MainThread;
+import android.annotation.Nullable;
+import android.util.ArrayMap;
+import android.util.Log;
+
+import com.android.systemui.statusbar.notification.collection.listbuilder.NotifListBuilder;
+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.PipelineState;
+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.SectionsProvider;
+import com.android.systemui.util.Assert;
+import com.android.systemui.util.time.SystemClock;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+
+/**
+ * The implementation of {@link NotifListBuilder}.
+ */
+@MainThread
+@Singleton
+public class NotifListBuilderImpl implements NotifListBuilder {
+
+ private final SystemClock mSystemClock;
+
+ private final List<ListEntry> mNotifList = new ArrayList<>();
+
+ private final PipelineState mPipelineState = new PipelineState();
+ private final Map<String, GroupEntry> mGroups = new ArrayMap<>();
+ private Collection<NotificationEntry> mAllEntries = Collections.emptyList();
+ private final List<ListEntry> mNewEntries = new ArrayList<>();
+ private int mIterationCount = 0;
+
+ private final List<NotifFilter> mNotifFilters = new ArrayList<>();
+ private final List<NotifPromoter> mNotifPromoters = new ArrayList<>();
+ private final List<NotifComparator> mNotifComparators = new ArrayList<>();
+ private SectionsProvider mSectionsProvider = new DefaultSectionsProvider();
+
+ private final List<OnBeforeTransformGroupsListener> mOnBeforeTransformGroupsListeners =
+ new ArrayList<>();
+ private final List<OnBeforeSortListener> mOnBeforeSortListeners =
+ new ArrayList<>();
+ private final List<OnBeforeRenderListListener> mOnBeforeRenderListListeners =
+ new ArrayList<>();
+ @Nullable private OnRenderListListener mOnRenderListListener;
+
+ private final List<ListEntry> mReadOnlyNotifList = Collections.unmodifiableList(mNotifList);
+
+ @Inject
+ public NotifListBuilderImpl(SystemClock systemClock) {
+ Assert.isMainThread();
+ mSystemClock = systemClock;
+ }
+
+ /**
+ * Attach the list builder to the NotifCollection. After this is called, it will start building
+ * the notif list in response to changes to the colletion.
+ */
+ public void attach(NotifCollection collection) {
+ Assert.isMainThread();
+ collection.setBuildListener(mReadyForBuildListener);
+ }
+
+ /**
+ * Registers the listener that's responsible for rendering the notif list to the screen. Called
+ * At the very end of pipeline execution, after all other listeners and pluggables have fired.
+ */
+ public void setOnRenderListListener(OnRenderListListener onRenderListListener) {
+ Assert.isMainThread();
+
+ mPipelineState.requireState(STATE_IDLE);
+ mOnRenderListListener = onRenderListListener;
+ }
+
+ @Override
+ public void addOnBeforeTransformGroupsListener(OnBeforeTransformGroupsListener listener) {
+ Assert.isMainThread();
+
+ mPipelineState.requireState(STATE_IDLE);
+ mOnBeforeTransformGroupsListeners.add(listener);
+ }
+
+ @Override
+ public void addOnBeforeSortListener(OnBeforeSortListener listener) {
+ Assert.isMainThread();
+
+ mPipelineState.requireState(STATE_IDLE);
+ mOnBeforeSortListeners.add(listener);
+ }
+
+ @Override
+ public void addOnBeforeRenderListListener(OnBeforeRenderListListener listener) {
+ Assert.isMainThread();
+
+ mPipelineState.requireState(STATE_IDLE);
+ mOnBeforeRenderListListeners.add(listener);
+ }
+
+ @Override
+ public void addFilter(NotifFilter filter) {
+ Assert.isMainThread();
+ mPipelineState.requireState(STATE_IDLE);
+
+ mNotifFilters.add(filter);
+ filter.setInvalidationListener(this::onFilterInvalidated);
+ }
+
+ @Override
+ public void addPromoter(NotifPromoter promoter) {
+ Assert.isMainThread();
+ mPipelineState.requireState(STATE_IDLE);
+
+ mNotifPromoters.add(promoter);
+ promoter.setInvalidationListener(this::onPromoterInvalidated);
+ }
+
+ @Override
+ public void setSectionsProvider(SectionsProvider provider) {
+ Assert.isMainThread();
+ mPipelineState.requireState(STATE_IDLE);
+
+ mSectionsProvider = provider;
+ provider.setInvalidationListener(this::onSectionsProviderInvalidated);
+ }
+
+ @Override
+ public void setComparators(List<NotifComparator> comparators) {
+ Assert.isMainThread();
+ mPipelineState.requireState(STATE_IDLE);
+
+ mNotifComparators.clear();
+ for (NotifComparator comparator : comparators) {
+ mNotifComparators.add(comparator);
+ comparator.setInvalidationListener(this::onNotifComparatorInvalidated);
+ }
+ }
+
+ @Override
+ public List<ListEntry> getActiveNotifs() {
+ Assert.isMainThread();
+ return mReadOnlyNotifList;
+ }
+
+ private final CollectionReadyForBuildListener mReadyForBuildListener =
+ new CollectionReadyForBuildListener() {
+ @Override
+ public void onBeginDispatchToListeners() {
+ Assert.isMainThread();
+ mPipelineState.incrementTo(STATE_BUILD_PENDING);
+ }
+
+ @Override
+ public void onBuildList(Collection<NotificationEntry> entries) {
+ Assert.isMainThread();
+ mPipelineState.requireIsBefore(STATE_BUILD_STARTED);
+
+ Log.i(TAG, "Build request received from NotifCollection");
+ mAllEntries = entries;
+ buildList();
+ }
+ };
+
+ private void onFilterInvalidated(NotifFilter filter) {
+ Assert.isMainThread();
+
+ // TODO: Convert these log statements (here and elsewhere) into timeline logging
+ Log.i(TAG, String.format(
+ "Filter \"%s\" invalidated; pipeline state is %d",
+ filter.getName(),
+ mPipelineState.getState()));
+
+ rebuildListIfBefore(STATE_FILTERING);
+ }
+
+ private void onPromoterInvalidated(NotifPromoter filter) {
+ Assert.isMainThread();
+
+ Log.i(TAG, String.format(
+ "NotifPromoter \"%s\" invalidated; pipeline state is %d",
+ filter.getName(),
+ mPipelineState.getState()));
+
+ rebuildListIfBefore(STATE_TRANSFORMING);
+ }
+
+ private void onSectionsProviderInvalidated(SectionsProvider provider) {
+ Assert.isMainThread();
+
+ Log.i(TAG, String.format(
+ "Sections provider \"%s\" invalidated; pipeline state is %d",
+ provider.getName(),
+ mPipelineState.getState()));
+
+ rebuildListIfBefore(STATE_SORTING);
+ }
+
+ private void onNotifComparatorInvalidated(NotifComparator comparator) {
+ Assert.isMainThread();
+
+ Log.i(TAG, String.format(
+ "Comparator \"%s\" invalidated; pipeline state is %d",
+ comparator.getName(),
+ mPipelineState.getState()));
+
+ rebuildListIfBefore(STATE_SORTING);
+ }
+
+ /**
+ * The core algorithm of the pipeline. See the top comment in {@link NotifListBuilder} for
+ * details on our contracts with other code.
+ *
+ * Once the build starts we are very careful to protect against reentrant code. Anything that
+ * tries to invalidate itself after the pipeline has passed it by will return in an exception.
+ * In general, we should be extremely sensitive to client code doing things in the wrong order;
+ * if we detect that behavior, we should crash instantly.
+ */
+ private void buildList() {
+ Log.i(TAG, "Starting notif list build #" + mIterationCount + "...");
+
+ mPipelineState.requireIsBefore(STATE_BUILD_STARTED);
+ mPipelineState.setState(STATE_BUILD_STARTED);
+
+ // Step 1: Filtering and initial grouping
+ // Filter out any notifs that shouldn't be shown right now and cluster any that are part of
+ // a group
+ mPipelineState.incrementTo(STATE_FILTERING);
+ mNotifList.clear();
+ mNewEntries.clear();
+ filterAndGroup(mAllEntries, mNotifList, mNewEntries);
+ pruneIncompleteGroups(mNotifList, mNewEntries);
+
+ // Step 2: Group transforming
+ // Move some notifs out of their groups and up to top-level (mostly used for heads-upping)
+ dispatchOnBeforeTransformGroups(mReadOnlyNotifList, mNewEntries);
+ mPipelineState.incrementTo(STATE_TRANSFORMING);
+ promoteNotifs(mNotifList);
+ pruneIncompleteGroups(mNotifList, mNewEntries);
+
+ // Step 3: Sort
+ // Assign each top-level entry a section, then sort the list by section and then within
+ // section by our list of custom comparators
+ dispatchOnBeforeSort(mReadOnlyNotifList);
+ mPipelineState.incrementTo(STATE_SORTING);
+ sortList();
+
+ // Step 4: Lock in our group structure and log anything that's changed since the last run
+ mPipelineState.incrementTo(STATE_FINALIZING);
+ logParentingChanges();
+ freeEmptyGroups();
+
+ // Step 5: Dispatch the new list, first to any listeners and then to the view layer
+ Log.i(TAG, "List finalized, is:\n" + dumpList(mNotifList));
+ Log.i(TAG, "Dispatching final list to listeners...");
+ dispatchOnBeforeRenderList(mReadOnlyNotifList);
+ if (mOnRenderListListener != null) {
+ mOnRenderListListener.onRenderList(mReadOnlyNotifList);
+ }
+
+ // Step 6: We're done!
+ Log.i(TAG, "Notif list build #" + mIterationCount + " completed");
+ mPipelineState.setState(STATE_IDLE);
+ mIterationCount++;
+ }
+
+ private void filterAndGroup(
+ Collection<NotificationEntry> entries,
+ List<ListEntry> out,
+ List<ListEntry> newlyVisibleEntries) {
+
+ long now = mSystemClock.uptimeMillis();
+
+ for (GroupEntry group : mGroups.values()) {
+ group.setPreviousParent(group.getParent());
+ group.setParent(null);
+ group.clearChildren();
+ group.setSummary(null);
+ }
+
+ for (NotificationEntry entry : entries) {
+ entry.setPreviousParent(entry.getParent());
+ entry.setParent(null);
+
+ // See if we should filter out this notification
+ boolean shouldFilterOut = applyFilters(entry, now);
+ if (shouldFilterOut) {
+ continue;
+ }
+
+ if (entry.mFirstAddedIteration == -1) {
+ entry.mFirstAddedIteration = mIterationCount;
+ newlyVisibleEntries.add(entry);
+ }
+
+ // Otherwise, group it
+ if (entry.getSbn().isGroup()) {
+ final String topLevelKey = entry.getSbn().getGroupKey();
+
+ GroupEntry group = mGroups.get(topLevelKey);
+ if (group == null) {
+ group = new GroupEntry(topLevelKey);
+ group.mFirstAddedIteration = mIterationCount;
+ newlyVisibleEntries.add(group);
+ mGroups.put(topLevelKey, group);
+ }
+ if (group.getParent() == null) {
+ group.setParent(ROOT_ENTRY);
+ out.add(group);
+ }
+
+ entry.setParent(group);
+
+ if (entry.getSbn().getNotification().isGroupSummary()) {
+ final NotificationEntry existingSummary = group.getSummary();
+
+ if (existingSummary == null) {
+ group.setSummary(entry);
+ } else {
+ Log.w(TAG, String.format(
+ "Duplicate summary for group '%s': '%s' vs. '%s'",
+ group.getKey(),
+ existingSummary.getKey(),
+ entry.getKey()));
+
+ // Use whichever one was posted most recently
+ if (entry.getSbn().getPostTime()
+ > existingSummary.getSbn().getPostTime()) {
+ group.setSummary(entry);
+ annulAddition(existingSummary, out, newlyVisibleEntries);
+ } else {
+ annulAddition(entry, out, newlyVisibleEntries);
+ }
+ }
+ } else {
+ group.addChild(entry);
+ }
+
+ } else {
+
+ final String topLevelKey = entry.getKey();
+ if (mGroups.containsKey(topLevelKey)) {
+ Log.wtf(TAG, "Duplicate non-group top-level key: " + topLevelKey);
+ } else {
+ entry.setParent(ROOT_ENTRY);
+ out.add(entry);
+ }
+ }
+ }
+ }
+
+ private void promoteNotifs(List<ListEntry> list) {
+ for (int i = 0; i < list.size(); i++) {
+ final ListEntry tle = list.get(i);
+
+ if (tle instanceof GroupEntry) {
+ final GroupEntry group = (GroupEntry) tle;
+
+ group.getRawChildren().removeIf(child -> {
+ final boolean shouldPromote = applyTopLevelPromoters(child);
+
+ if (shouldPromote) {
+ child.setParent(ROOT_ENTRY);
+ list.add(child);
+ }
+
+ return shouldPromote;
+ });
+ }
+ }
+ }
+
+ private void pruneIncompleteGroups(
+ List<ListEntry> shadeList,
+ List<ListEntry> newlyVisibleEntries) {
+
+ for (int i = 0; i < shadeList.size(); i++) {
+ final ListEntry tle = shadeList.get(i);
+
+ if (tle instanceof GroupEntry) {
+ final GroupEntry group = (GroupEntry) tle;
+ final List<NotificationEntry> children = group.getRawChildren();
+
+ if (group.getSummary() != null && children.size() == 0) {
+ shadeList.remove(i);
+ i--;
+
+ NotificationEntry summary = group.getSummary();
+ summary.setParent(ROOT_ENTRY);
+ shadeList.add(summary);
+
+ group.setSummary(null);
+ annulAddition(group, shadeList, newlyVisibleEntries);
+
+ } else if (group.getSummary() == null
+ || children.size() < MIN_CHILDREN_FOR_GROUP) {
+ // If the group doesn't provide a summary or is too small, ignore it and add
+ // its children (if any) directly to top-level.
+
+ shadeList.remove(i);
+ i--;
+
+ if (group.getSummary() != null) {
+ final NotificationEntry summary = group.getSummary();
+ group.setSummary(null);
+ annulAddition(summary, shadeList, newlyVisibleEntries);
+ }
+
+ for (int j = 0; j < children.size(); j++) {
+ final NotificationEntry child = children.get(j);
+ child.setParent(ROOT_ENTRY);
+ shadeList.add(child);
+ }
+ children.clear();
+
+ annulAddition(group, shadeList, newlyVisibleEntries);
+ }
+ }
+ }
+ }
+
+ /**
+ * If a ListEntry was added to the shade list and then later removed (e.g. because it was a
+ * group that was broken up), this method will erase any bookkeeping traces of that addition
+ * and/or check that they were already erased.
+ *
+ * Before calling this method, the entry must already have been removed from its parent. If
+ * it's a group, its summary must be null and its children must be empty.
+ */
+ private void annulAddition(
+ ListEntry entry,
+ List<ListEntry> shadeList,
+ List<ListEntry> newlyVisibleEntries) {
+
+ // This function does very little, but if any of its assumptions are violated (and it has a
+ // lot of them), it will put the system into an inconsistent state. So we check all of them
+ // here.
+
+ if (entry.getParent() == null || entry.mFirstAddedIteration == -1) {
+ throw new IllegalStateException(
+ "Cannot nullify addition of " + entry.getKey() + ": no such addition. ("
+ + entry.getParent() + " " + entry.mFirstAddedIteration + ")");
+ }
+
+ if (entry.getParent() == ROOT_ENTRY) {
+ if (shadeList.contains(entry)) {
+ throw new IllegalStateException("Cannot nullify addition of " + entry.getKey()
+ + ": it's still in the shade list.");
+ }
+ }
+
+ if (entry instanceof GroupEntry) {
+ GroupEntry ge = (GroupEntry) entry;
+ if (ge.getSummary() != null) {
+ throw new IllegalStateException(
+ "Cannot nullify group " + ge.getKey() + ": summary is not null");
+ }
+ if (!ge.getChildren().isEmpty()) {
+ throw new IllegalStateException(
+ "Cannot nullify group " + ge.getKey() + ": still has children");
+ }
+ } else if (entry instanceof NotificationEntry) {
+ if (entry == entry.getParent().getSummary()
+ || entry.getParent().getChildren().contains(entry)) {
+ throw new IllegalStateException("Cannot nullify addition of child "
+ + entry.getKey() + ": it's still attached to its parent.");
+ }
+ }
+
+ entry.setParent(null);
+ if (entry.mFirstAddedIteration == mIterationCount) {
+ if (!newlyVisibleEntries.remove(entry)) {
+ throw new IllegalStateException("Cannot late-filter entry " + entry.getKey() + " "
+ + entry + " from " + newlyVisibleEntries + " "
+ + entry.mFirstAddedIteration);
+ }
+ entry.mFirstAddedIteration = -1;
+ }
+ }
+
+ private void sortList() {
+ // Assign sections to top-level elements and sort their children
+ for (ListEntry entry : mNotifList) {
+ entry.setSection(mSectionsProvider.getSection(entry));
+ if (entry instanceof GroupEntry) {
+ GroupEntry parent = (GroupEntry) entry;
+ for (NotificationEntry child : parent.getChildren()) {
+ child.setSection(0);
+ }
+ parent.sortChildren(sChildComparator);
+ }
+ }
+
+ // Finally, sort all top-level elements
+ mNotifList.sort(mTopLevelComparator);
+ }
+
+ private void freeEmptyGroups() {
+ mGroups.values().removeIf(ge -> ge.getSummary() == null && ge.getChildren().isEmpty());
+ }
+
+ private void logParentingChanges() {
+ for (NotificationEntry entry : mAllEntries) {
+ if (entry.getParent() != entry.getPreviousParent()) {
+ Log.i(TAG, String.format(
+ "%s: parent changed from %s to %s",
+ entry.getKey(),
+ entry.getPreviousParent() == null
+ ? "null" : entry.getPreviousParent().getKey(),
+ entry.getParent() == null
+ ? "null" : entry.getParent().getKey()));
+ }
+ }
+ for (GroupEntry group : mGroups.values()) {
+ if (group.getParent() != group.getPreviousParent()) {
+ Log.i(TAG, String.format(
+ "%s: parent changed from %s to %s",
+ group.getKey(),
+ group.getPreviousParent() == null
+ ? "null" : group.getPreviousParent().getKey(),
+ group.getParent() == null
+ ? "null" : group.getParent().getKey()));
+ }
+ }
+ }
+
+ private final Comparator<ListEntry> mTopLevelComparator = (o1, o2) -> {
+
+ int cmp = Integer.compare(o1.getSection(), o2.getSection());
+
+ if (cmp == 0) {
+ for (int i = 0; i < mNotifComparators.size(); i++) {
+ cmp = mNotifComparators.get(i).compare(o1, o2);
+ if (cmp != 0) {
+ break;
+ }
+ }
+ }
+
+ final NotificationEntry rep1 = o1.getRepresentativeEntry();
+ final NotificationEntry rep2 = o2.getRepresentativeEntry();
+
+ if (cmp == 0) {
+ cmp = rep1.getRanking().getRank() - rep2.getRanking().getRank();
+ }
+
+ if (cmp == 0) {
+ cmp = Long.compare(
+ rep2.getSbn().getNotification().when,
+ rep1.getSbn().getNotification().when);
+ }
+
+ return cmp;
+ };
+
+ private static final Comparator<NotificationEntry> sChildComparator = (o1, o2) -> {
+ int cmp = o1.getRanking().getRank() - o2.getRanking().getRank();
+
+ if (cmp == 0) {
+ cmp = Long.compare(
+ o2.getSbn().getNotification().when,
+ o1.getSbn().getNotification().when);
+ }
+
+ return cmp;
+ };
+
+ private boolean applyFilters(NotificationEntry entry, long now) {
+ NotifFilter filter = findRejectingFilter(entry, now);
+
+ if (filter != entry.mExcludingFilter) {
+ if (entry.mExcludingFilter == null) {
+ Log.i(TAG, String.format(
+ "%s: filtered out by '%s'",
+ entry.getKey(),
+ filter.getName()));
+ } else if (filter == null) {
+ Log.i(TAG, String.format(
+ "%s: no longer filtered out (previous filter was '%s')",
+ entry.getKey(),
+ entry.mExcludingFilter.getName()));
+ } else {
+ Log.i(TAG, String.format(
+ "%s: filter changed: '%s' -> '%s'",
+ entry.getKey(),
+ entry.mExcludingFilter,
+ filter));
+ }
+
+ // Note that groups and summaries can also be filtered out later if they're part of a
+ // malformed group. We currently don't have a great way to track that beyond parenting
+ // change logs. Consider adding something similar to mExcludingFilter for them.
+ entry.mExcludingFilter = filter;
+ }
+
+ return filter != null;
+ }
+
+ @Nullable private NotifFilter findRejectingFilter(NotificationEntry entry, long now) {
+ for (int i = 0; i < mNotifFilters.size(); i++) {
+ NotifFilter filter = mNotifFilters.get(i);
+ if (filter.shouldFilterOut(entry, now)) {
+ return filter;
+ }
+ }
+ return null;
+ }
+
+ private boolean applyTopLevelPromoters(NotificationEntry entry) {
+ NotifPromoter promoter = findPromoter(entry);
+
+ if (promoter != entry.mNotifPromoter) {
+ if (entry.mNotifPromoter == null) {
+ Log.i(TAG, String.format(
+ "%s: Entry promoted to top level by '%s'",
+ entry.getKey(),
+ promoter.getName()));
+ } else if (promoter == null) {
+ Log.i(TAG, String.format(
+ "%s: Entry is no longer promoted to top level (previous promoter was '%s')",
+ entry.getKey(),
+ entry.mNotifPromoter.getName()));
+ } else {
+ Log.i(TAG, String.format(
+ "%s: Top-level promoter changed: '%s' -> '%s'",
+ entry.getKey(),
+ entry.mNotifPromoter,
+ promoter));
+ }
+
+ entry.mNotifPromoter = promoter;
+ }
+
+ return promoter != null;
+ }
+
+ @Nullable private NotifPromoter findPromoter(NotificationEntry entry) {
+ for (int i = 0; i < mNotifPromoters.size(); i++) {
+ NotifPromoter promoter = mNotifPromoters.get(i);
+ if (promoter.shouldPromoteToTopLevel(entry)) {
+ return promoter;
+ }
+ }
+ return null;
+ }
+
+ private void rebuildListIfBefore(@PipelineState.StateName int state) {
+ mPipelineState.requireIsBefore(state);
+ if (mPipelineState.is(STATE_IDLE)) {
+ buildList();
+ }
+ }
+
+ private void dispatchOnBeforeTransformGroups(
+ List<ListEntry> entries,
+ List<ListEntry> newlyVisibleEntries) {
+ for (int i = 0; i < mOnBeforeTransformGroupsListeners.size(); i++) {
+ mOnBeforeTransformGroupsListeners.get(i)
+ .onBeforeTransformGroups(entries, newlyVisibleEntries);
+ }
+ }
+
+ private void dispatchOnBeforeSort(List<ListEntry> entries) {
+ for (int i = 0; i < mOnBeforeSortListeners.size(); i++) {
+ mOnBeforeSortListeners.get(i).onBeforeSort(entries);
+ }
+ }
+
+ private void dispatchOnBeforeRenderList(List<ListEntry> entries) {
+ for (int i = 0; i < mOnBeforeRenderListListeners.size(); i++) {
+ mOnBeforeRenderListListeners.get(i).onBeforeRenderList(entries);
+ }
+ }
+
+ /** See {@link #setOnRenderListListener(OnRenderListListener)} */
+ public interface OnRenderListListener {
+ /**
+ * Called with the final filtered, grouped, and sorted list.
+ *
+ * @param entries A read-only view into the current notif list. Note that this list is
+ * backed by the live list and will change in response to new pipeline runs.
+ */
+ void onRenderList(List<ListEntry> entries);
+ }
+
+ private static class DefaultSectionsProvider extends SectionsProvider {
+ DefaultSectionsProvider() {
+ super("DefaultSectionsProvider");
+ }
+
+ @Override
+ public int getSection(ListEntry entry) {
+ return 0;
+ }
+ }
+
+ private static final String TAG = "NotifListBuilderImpl";
+
+ private static final int MIN_CHILDREN_FOR_GROUP = 2;
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java
index 28ccaf54e15e..3eb55ef6bc93 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java
@@ -60,6 +60,8 @@ import com.android.internal.util.ContrastColorUtil;
import com.android.systemui.statusbar.InflationTask;
import com.android.systemui.statusbar.StatusBarIconView;
import com.android.systemui.statusbar.notification.InflationException;
+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.row.ExpandableNotificationRow;
import com.android.systemui.statusbar.notification.row.NotificationContentInflater.InflationFlag;
import com.android.systemui.statusbar.notification.row.NotificationGuts;
@@ -84,7 +86,7 @@ import java.util.Objects;
* At the moment, there are many things here that shouldn't be and vice-versa. Hopefully we can
* clean this up in the future.
*/
-public final class NotificationEntry {
+public final class NotificationEntry extends ListEntry {
private final String mKey;
private StatusBarNotification mSbn;
@@ -98,6 +100,12 @@ public final class NotificationEntry {
/** List of lifetime extenders that are extending the lifetime of this notification. */
final List<NotifLifetimeExtender> mLifetimeExtenders = new ArrayList<>();
+ /** If this notification was filtered out, then the filter that did the filtering. */
+ @Nullable NotifFilter mExcludingFilter;
+
+ /** If this was a group child that was promoted to the top level, then who did the promoting. */
+ @Nullable NotifPromoter mNotifPromoter;
+
/*
* Old members
@@ -164,8 +172,8 @@ public final class NotificationEntry {
public NotificationEntry(
@NonNull StatusBarNotification sbn,
@NonNull Ranking ranking) {
- checkNotNull(sbn);
- checkNotNull(sbn.getKey());
+ super(checkNotNull(checkNotNull(sbn).getKey()));
+
checkNotNull(ranking);
mKey = sbn.getKey();
@@ -173,6 +181,11 @@ public final class NotificationEntry {
setRanking(ranking);
}
+ @Override
+ public NotificationEntry getRepresentativeEntry() {
+ return this;
+ }
+
/** The key for this notification. Guaranteed to be immutable and unique */
public String getKey() {
return mKey;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/init/FakePipelineConsumer.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/init/FakePipelineConsumer.java
new file mode 100644
index 000000000000..986ee17cc906
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/init/FakePipelineConsumer.java
@@ -0,0 +1,85 @@
+/*
+ * 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.init;
+
+import com.android.systemui.Dumpable;
+import com.android.systemui.statusbar.notification.collection.GroupEntry;
+import com.android.systemui.statusbar.notification.collection.ListEntry;
+import com.android.systemui.statusbar.notification.collection.NotifListBuilderImpl;
+import com.android.systemui.statusbar.notification.collection.NotificationEntry;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Temporary class that tracks the result of the list builder and dumps it to text when requested.
+ *
+ * Eventually, this will be something that hands off the result of the pipeline to the View layer.
+ */
+public class FakePipelineConsumer implements Dumpable {
+ private List<ListEntry> mEntries = Collections.emptyList();
+
+ /** Attach the consumer to the pipeline. */
+ public void attach(NotifListBuilderImpl listBuilder) {
+ listBuilder.setOnRenderListListener(this::onBuildComplete);
+ }
+
+ private void onBuildComplete(List<ListEntry> entries) {
+ mEntries = entries;
+ }
+
+ @Override
+ public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+ pw.println();
+ pw.println("Active notif tree:");
+ for (int i = 0; i < mEntries.size(); i++) {
+ ListEntry entry = mEntries.get(i);
+ if (entry instanceof GroupEntry) {
+ GroupEntry ge = (GroupEntry) entry;
+ pw.println(dumpGroup(ge, "", i));
+
+ pw.println(dumpEntry(ge.getSummary(), INDENT, -1));
+ for (int j = 0; j < ge.getChildren().size(); j++) {
+ pw.println(dumpEntry(ge.getChildren().get(j), INDENT, j));
+ }
+ } else {
+ pw.println(dumpEntry(entry.getRepresentativeEntry(), "", i));
+ }
+ }
+ }
+
+ private String dumpGroup(GroupEntry entry, String indent, int index) {
+ return String.format(
+ "%s[%d] %s (group)",
+ indent,
+ index,
+ entry.getKey());
+ }
+
+ private String dumpEntry(NotificationEntry entry, String indent, int index) {
+ return String.format(
+ "%s[%s] %s (channel=%s)",
+ indent,
+ index == -1 ? "*" : Integer.toString(index),
+ entry.getKey(),
+ entry.getChannel() != null ? entry.getChannel().getId() : "");
+ }
+
+ private static final String INDENT = " ";
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NewNotifPipeline.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/init/NewNotifPipeline.java
index 31921a436747..3b3e7e2a2933 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NewNotifPipeline.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/init/NewNotifPipeline.java
@@ -14,12 +14,18 @@
* limitations under the License.
*/
-package com.android.systemui.statusbar.notification;
+package com.android.systemui.statusbar.notification.collection.init;
import android.util.Log;
+import com.android.systemui.DumpController;
+import com.android.systemui.Dumpable;
import com.android.systemui.statusbar.NotificationListener;
import com.android.systemui.statusbar.notification.collection.NotifCollection;
+import com.android.systemui.statusbar.notification.collection.NotifListBuilderImpl;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
import javax.inject.Inject;
import javax.inject.Singleton;
@@ -28,21 +34,38 @@ import javax.inject.Singleton;
* Initialization code for the new notification pipeline.
*/
@Singleton
-public class NewNotifPipeline {
+public class NewNotifPipeline implements Dumpable {
private final NotifCollection mNotifCollection;
+ private final NotifListBuilderImpl mNotifPipeline;
+ private final DumpController mDumpController;
+
+ private final FakePipelineConsumer mFakePipelineConsumer = new FakePipelineConsumer();
@Inject
public NewNotifPipeline(
- NotifCollection notifCollection) {
+ NotifCollection notifCollection,
+ NotifListBuilderImpl notifPipeline,
+ DumpController dumpController) {
mNotifCollection = notifCollection;
+ mNotifPipeline = notifPipeline;
+ mDumpController = dumpController;
}
/** Hooks the new pipeline up to NotificationManager */
public void initialize(
NotificationListener notificationService) {
+ mFakePipelineConsumer.attach(mNotifPipeline);
+ mNotifPipeline.attach(mNotifCollection);
mNotifCollection.attach(notificationService);
Log.d(TAG, "Notif pipeline initialized");
+
+ mDumpController.registerDumpable("NotifPipeline", this);
+ }
+
+ @Override
+ public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+ mFakePipelineConsumer.dump(fd, pw, args);
}
private static final String TAG = "NewNotifPipeline";
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/NotifListBuilder.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/NotifListBuilder.java
new file mode 100644
index 000000000000..15d3b9222cd2
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/NotifListBuilder.java
@@ -0,0 +1,117 @@
+/*
+ * 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.listbuilder;
+
+import com.android.systemui.statusbar.notification.collection.ListEntry;
+import com.android.systemui.statusbar.notification.collection.NotifCollection;
+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.SectionsProvider;
+
+import java.util.List;
+
+/**
+ * The system that constructs the current "notification list", the list of notifications that are
+ * currently being displayed to the user.
+ *
+ * The pipeline proceeds through a series of stages in order to produce the final list (see below).
+ * Each stage exposes hooks and listeners for other code to participate.
+ *
+ * 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. To participate, see {@link #addFilter(NotifFilter)}.
+ * - 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(NotifPromoter)}.
+ * - Sorted: All top-level notifications are sorted. To participate, see
+ * {@link #setSectionsProvider(SectionsProvider)} and {@link #setComparators(List)}
+ *
+ * The exact order of all hooks is as follows:
+ * 0. Collection listeners are fired (see {@link NotifCollection}).
+ * 1. NotifFilters are called on each notification currently in NotifCollection.
+ * 2. Initial grouping is performed (NotificationEntries will have their parents set
+ * appropriately).
+ * 3. OnBeforeTransformGroupListeners are fired
+ * 4. NotifPromoters are called on each notification with a parent
+ * 5. OnBeforeSortListeners are fired
+ * 6. SectionsProvider is called on each top-level entry in the list
+ * 7. The top-level entries are sorted using the provided NotifComparators (plus some additional
+ * built-in logic).
+ * 8. OnBeforeRenderListListeners are fired
+ * 9. The list is handed off to the view layer to be rendered.
+ */
+public interface NotifListBuilder {
+
+ /**
+ * Registers a filter with the pipeline. Filters are called on each notification in the order
+ * that they were registered. If any filter returns true, the notification is removed from the
+ * pipeline (and no other filters are called on that notif).
+ */
+ void addFilter(NotifFilter filter);
+
+ /**
+ * Registers a promoter with the pipeline. Promoters are able to promote child notifications to
+ * top-level, i.e. move a notification that would be a child of a group and make it appear
+ * ungrouped. Promoters are called on each child notification in the order that they are
+ * registered. If any promoter returns true, the notification is removed from the group (and no
+ * other promoters are called on it).
+ */
+ void addPromoter(NotifPromoter promoter);
+
+ /**
+ * Assigns sections to each top-level entry, where a section is simply an integer. Sections are
+ * the primary metric by which top-level entries are sorted; NotifComparators are only consulted
+ * when two entries are in the same section. The pipeline doesn't assign any particular meaning
+ * to section IDs -- from it's perspective they're just numbers and it sorts them by a simple
+ * numerical comparison.
+ */
+ void setSectionsProvider(SectionsProvider provider);
+
+ /**
+ * Comparators that are used to sort top-level entries that share the same section. The
+ * 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).
+ */
+ void setComparators(List<NotifComparator> comparators);
+
+ /**
+ * 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.
+ */
+ void addOnBeforeTransformGroupsListener(OnBeforeTransformGroupsListener listener);
+
+ /**
+ * Called after notifs have been filtered and groups have been determined but before sections
+ * have been determined or the notifs have been sorted.
+ */
+ void addOnBeforeSortListener(OnBeforeSortListener listener);
+
+ /**
+ * 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.
+ */
+ void addOnBeforeRenderListListener(OnBeforeRenderListListener listener);
+
+ /**
+ * Returns a read-only view in to the current notification list. If this method is called
+ * during pipeline execution it will return the current state of the list, which will likely
+ * be only partially-generated.
+ */
+ List<ListEntry> getActiveNotifs();
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/OnBeforeRenderListListener.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/OnBeforeRenderListListener.java
new file mode 100644
index 000000000000..f6ca12d83fdd
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/OnBeforeRenderListListener.java
@@ -0,0 +1,33 @@
+/*
+ * 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.listbuilder;
+
+import com.android.systemui.statusbar.notification.collection.ListEntry;
+
+import java.util.List;
+
+/** See {@link NotifListBuilder#addOnBeforeRenderListListener(OnBeforeRenderListListener)} */
+public interface OnBeforeRenderListListener {
+ /**
+ * 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.
+ *
+ * @param entries The current list of top-level entries. Note that this is a live view into the
+ * current list and will change whenever the pipeline is rerun.
+ */
+ void onBeforeRenderList(List<ListEntry> entries);
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/OnBeforeSortListener.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/OnBeforeSortListener.java
new file mode 100644
index 000000000000..7be7ac03e1f1
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/OnBeforeSortListener.java
@@ -0,0 +1,33 @@
+/*
+ * 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.listbuilder;
+
+import com.android.systemui.statusbar.notification.collection.ListEntry;
+
+import java.util.List;
+
+/** See {@link NotifListBuilder#addOnBeforeSortListener(OnBeforeSortListener)} */
+public interface OnBeforeSortListener {
+ /**
+ * Called after the notif list has been filtered and grouped but before sections have been
+ * determined or sorting has taken place.
+ *
+ * @param entries The current list of top-level entries. Note that this is a live view into the
+ * current list and will change whenever the pipeline is rerun.
+ */
+ void onBeforeSort(List<ListEntry> entries);
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/OnBeforeTransformGroupsListener.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/OnBeforeTransformGroupsListener.java
new file mode 100644
index 000000000000..170ff48ad7f1
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/OnBeforeTransformGroupsListener.java
@@ -0,0 +1,40 @@
+/*
+ * 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.listbuilder;
+
+import com.android.systemui.statusbar.notification.collection.ListEntry;
+import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifPromoter;
+
+import java.util.List;
+
+/**
+ * See
+ * {@link NotifListBuilder#addOnBeforeTransformGroupsListener(OnBeforeTransformGroupsListener)}
+ */
+public interface OnBeforeTransformGroupsListener {
+ /**
+ * Called after notifs have been filtered and grouped but before {@link NotifPromoter}s have
+ * been called.
+ *
+ * @param list The current filtered and grouped list of (top-level) entries. Note that this is
+ * a live view into the current notif list and will change as the list moves through
+ * the pipeline.
+ * @param newlyVisibleEntries The list of all entries (both top-level and children) who have
+ * been added to the list for the first time.
+ */
+ void onBeforeTransformGroups(List<ListEntry> list, List<ListEntry> newlyVisibleEntries);
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/PipelineState.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/PipelineState.java
new file mode 100644
index 000000000000..ad4bbd915787
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/PipelineState.java
@@ -0,0 +1,97 @@
+/*
+ * 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.listbuilder;
+
+import android.annotation.IntDef;
+
+import com.android.systemui.statusbar.notification.collection.NotifListBuilderImpl;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Used by {@link NotifListBuilderImpl} to track its internal state machine.
+ */
+public class PipelineState {
+
+ private @StateName int mState = STATE_IDLE;
+
+ /** Returns true if the current state matches <code>state</code> */
+ public boolean is(@StateName int state) {
+ return state == mState;
+ }
+
+ public @StateName int getState() {
+ return mState;
+ }
+
+ public void setState(@StateName int state) {
+ mState = state;
+ }
+
+ /**
+ * Increments the state from <code>(to - 1)</code> to <code>to</code>. If the current state
+ * isn't <code>(to - 1)</code>, throws an exception.
+ */
+ public void incrementTo(@StateName int to) {
+ if (mState != to - 1) {
+ throw new IllegalStateException(
+ "Cannot increment from state " + mState + " to state " + to);
+ }
+ mState = to;
+ }
+
+ /**
+ * Throws an exception if the current state is not <code>state</code>.
+ */
+ public void requireState(@StateName int state) {
+ if (state != mState) {
+ throw new IllegalStateException(
+ "Required state is <" + state + " but actual state is " + mState);
+ }
+ }
+
+ /**
+ * Throws an exception if the current state is >= <code>state</code>.
+ */
+ public void requireIsBefore(@StateName int state) {
+ if (mState >= state) {
+ throw new IllegalStateException(
+ "Required state is <" + state + " but actual state is " + mState);
+ }
+ }
+
+ public static final int STATE_IDLE = 0;
+ public static final int STATE_BUILD_PENDING = 1;
+ public static final int STATE_BUILD_STARTED = 2;
+ public static final int STATE_FILTERING = 3;
+ public static final int STATE_TRANSFORMING = 4;
+ public static final int STATE_SORTING = 5;
+ public static final int STATE_FINALIZING = 6;
+
+ @IntDef(prefix = { "STATE_" }, value = {
+ STATE_IDLE,
+ STATE_BUILD_PENDING,
+ STATE_BUILD_STARTED,
+ STATE_FILTERING,
+ STATE_TRANSFORMING,
+ STATE_SORTING,
+ STATE_FINALIZING,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface StateName {}
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/pluggable/NotifComparator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/pluggable/NotifComparator.java
new file mode 100644
index 000000000000..a191c830537d
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/pluggable/NotifComparator.java
@@ -0,0 +1,43 @@
+/*
+ * 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.listbuilder.pluggable;
+
+import com.android.systemui.statusbar.notification.collection.ListEntry;
+import com.android.systemui.statusbar.notification.collection.listbuilder.NotifListBuilder;
+
+import java.util.Comparator;
+import java.util.List;
+
+/**
+ * Pluggable for participating in notif sorting. See {@link NotifListBuilder#setComparators(List)}.
+ */
+public abstract class NotifComparator
+ extends Pluggable<NotifComparator>
+ implements Comparator<ListEntry> {
+
+ protected NotifComparator(String name) {
+ super(name);
+ }
+
+ /**
+ * Compare two ListEntries. Note that these might be either NotificationEntries or GroupEntries.
+ *
+ * @return a negative integer, zero, or a positive integer as the first argument is less than
+ * equal to, or greater than the second (same as standard Comparator<> interface).
+ */
+ public abstract int compare(ListEntry o1, ListEntry o2);
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/pluggable/NotifFilter.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/pluggable/NotifFilter.java
new file mode 100644
index 000000000000..685eac88de34
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/pluggable/NotifFilter.java
@@ -0,0 +1,45 @@
+/*
+ * 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.listbuilder.pluggable;
+
+import com.android.systemui.statusbar.notification.collection.NotificationEntry;
+import com.android.systemui.statusbar.notification.collection.listbuilder.NotifListBuilder;
+
+/**
+ * Pluggable for participating in notif filtering. See
+ * {@link NotifListBuilder#addFilter(NotifFilter)}.
+ */
+public abstract class NotifFilter extends Pluggable<NotifFilter> {
+ protected NotifFilter(String name) {
+ super(name);
+ }
+
+ /**
+ * If returns true, this notification will not be included in the final list displayed to the
+ * user. Filtering is performed on each active notification every time the pipeline is run.
+ * This doesn't necessarily mean that your filter will get called on every notification,
+ * however. If another filter returns true before yours, we'll skip straight to the next notif.
+ *
+ * @param entry The entry in question
+ * @param now A timestamp in SystemClock.uptimeMillis that represents "now" for the purposes of
+ * pipeline execution. This value will be the same for all pluggable calls made
+ * during this pipeline run, giving pluggables a stable concept of "now" to compare
+ * various entries against.
+ * @return True if the notif should be removed from the list
+ */
+ public abstract boolean shouldFilterOut(NotificationEntry entry, long now);
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/pluggable/NotifPromoter.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/pluggable/NotifPromoter.java
new file mode 100644
index 000000000000..84e16f432740
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/pluggable/NotifPromoter.java
@@ -0,0 +1,42 @@
+/*
+ * 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.listbuilder.pluggable;
+
+import com.android.systemui.statusbar.notification.collection.NotificationEntry;
+import com.android.systemui.statusbar.notification.collection.listbuilder.NotifListBuilder;
+
+/**
+ * Pluggable for participating in notif promotion. Notif promoters can upgrade notifications
+ * from being children of a group to top-level notifications. See
+ * {@link NotifListBuilder#addPromoter(NotifPromoter)}.
+ */
+public abstract class NotifPromoter extends Pluggable<NotifPromoter> {
+ protected NotifPromoter(String name) {
+ super(name);
+ }
+
+ /**
+ * If true, the child will be removed from its parent and placed at the top level of the notif
+ * list. By the time this method is called, child.getParent() has been set, so you can
+ * examine it (or any other entries in the notif list) for extra information.
+ *
+ * This method is only called on notifs that are currently children of groups. This doesn't
+ * necessarily mean that your promoter will get called on every child notification, however. If
+ * another promoter returns true before yours, we'll skip straight to the next notif.
+ */
+ public abstract boolean shouldPromoteToTopLevel(NotificationEntry child);
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/pluggable/Pluggable.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/pluggable/Pluggable.java
new file mode 100644
index 000000000000..f9ce197c6547
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/pluggable/Pluggable.java
@@ -0,0 +1,71 @@
+/*
+ * 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.listbuilder.pluggable;
+
+import android.annotation.Nullable;
+
+import com.android.systemui.statusbar.notification.collection.listbuilder.NotifListBuilder;
+
+/**
+ * Generic superclass for chunks of code that can plug into the {@link NotifListBuilder}.
+ *
+ * A pluggable is fundamentally three things:
+ * 1. A name (for debugging purposes)
+ * 2. The functionality that the pluggable provides to the pipeline (this is determined by the
+ * subclass).
+ * 3. A way for the pluggable to inform the pipeline that its state has changed and the pipeline
+ * should be rerun (in this case, the invalidate() method).
+ *
+ * @param <This> The type of the subclass. Subclasses should bind their own type here.
+ */
+public abstract class Pluggable<This> {
+ private final String mName;
+ @Nullable private PluggableListener<This> mListener;
+
+ Pluggable(String name) {
+ mName = name;
+ }
+
+ public final String getName() {
+ return mName;
+ }
+
+ /**
+ * Call this method when something has caused this pluggable's behavior to change. The pipeline
+ * will be re-run.
+ */
+ public final void invalidateList() {
+ if (mListener != null) {
+ mListener.onPluggableInvalidated((This) this);
+ }
+ }
+
+ /** Set a listener to be notified when a pluggable is invalidated. */
+ public void setInvalidationListener(PluggableListener<This> listener) {
+ mListener = listener;
+ }
+
+ /**
+ * Listener interface for when pluggables are invalidated.
+ *
+ * @param <T> The type of pluggable that is being listened to.
+ */
+ public interface PluggableListener<T> {
+ /** Called whenever {@link #invalidateList()} is called on this pluggable. */
+ void onPluggableInvalidated(T pluggable);
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/pluggable/SectionsProvider.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/pluggable/SectionsProvider.java
new file mode 100644
index 000000000000..11ea85062781
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/pluggable/SectionsProvider.java
@@ -0,0 +1,37 @@
+/*
+ * 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.listbuilder.pluggable;
+
+import com.android.systemui.statusbar.notification.collection.ListEntry;
+
+/**
+ * Interface for sorting notifications into "sections", such as a heads-upping section, people
+ * section, alerting section, silent section, etc.
+ */
+public abstract class SectionsProvider extends Pluggable<SectionsProvider> {
+
+ protected SectionsProvider(String name) {
+ super(name);
+ }
+
+ /**
+ * Returns the section that this entry belongs to. A section can be any non-negative integer.
+ * When entries are sorted, they are first sorted by section and then by any remainining
+ * comparators.
+ */
+ public abstract int getSection(ListEntry entry);
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
index adea8c6aa014..6e0a46117861 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
@@ -198,7 +198,6 @@ import com.android.systemui.statusbar.VibratorHelper;
import com.android.systemui.statusbar.notification.ActivityLaunchAnimator;
import com.android.systemui.statusbar.notification.BypassHeadsUpNotifier;
import com.android.systemui.statusbar.notification.DynamicPrivacyController;
-import com.android.systemui.statusbar.notification.NewNotifPipeline;
import com.android.systemui.statusbar.notification.NotificationActivityStarter;
import com.android.systemui.statusbar.notification.NotificationAlertingManager;
import com.android.systemui.statusbar.notification.NotificationClicker;
@@ -210,6 +209,7 @@ import com.android.systemui.statusbar.notification.ViewGroupFadeHelper;
import com.android.systemui.statusbar.notification.VisualStabilityManager;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.notification.collection.NotificationRowBinderImpl;
+import com.android.systemui.statusbar.notification.collection.init.NewNotifPipeline;
import com.android.systemui.statusbar.notification.logging.NotificationLogger;
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
import com.android.systemui.statusbar.notification.row.NotificationGutsManager;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarModule.java
index 60e93857e9da..88f1c63f627a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarModule.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarModule.java
@@ -56,12 +56,12 @@ import com.android.systemui.statusbar.SysuiStatusBarStateController;
import com.android.systemui.statusbar.VibratorHelper;
import com.android.systemui.statusbar.notification.BypassHeadsUpNotifier;
import com.android.systemui.statusbar.notification.DynamicPrivacyController;
-import com.android.systemui.statusbar.notification.NewNotifPipeline;
import com.android.systemui.statusbar.notification.NotificationAlertingManager;
import com.android.systemui.statusbar.notification.NotificationEntryManager;
import com.android.systemui.statusbar.notification.NotificationInterruptionStateProvider;
import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator;
import com.android.systemui.statusbar.notification.VisualStabilityManager;
+import com.android.systemui.statusbar.notification.collection.init.NewNotifPipeline;
import com.android.systemui.statusbar.notification.logging.NotificationLogger;
import com.android.systemui.statusbar.notification.row.NotificationGutsManager;
import com.android.systemui.statusbar.policy.BatteryController;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationEntryBuilder.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationEntryBuilder.java
index fcfdd11a1906..d18b16be33d6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationEntryBuilder.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationEntryBuilder.java
@@ -20,6 +20,7 @@ import android.annotation.Nullable;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager.Importance;
+import android.content.Context;
import android.os.UserHandle;
import android.service.notification.SnoozeCriterion;
import android.service.notification.StatusBarNotification;
@@ -92,6 +93,10 @@ public class NotificationEntryBuilder {
return this;
}
+ public Notification.Builder modifyNotification(Context context) {
+ return mSbnBuilder.modifyNotification(context);
+ }
+
public NotificationEntryBuilder setUser(UserHandle user) {
mSbnBuilder.setUser(user);
return this;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/SbnBuilder.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/SbnBuilder.java
index fe117fe443a6..94b3ac49ab09 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/SbnBuilder.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/SbnBuilder.java
@@ -16,7 +16,9 @@
package com.android.systemui.statusbar;
+import android.annotation.Nullable;
import android.app.Notification;
+import android.content.Context;
import android.os.UserHandle;
import android.service.notification.StatusBarNotification;
@@ -32,7 +34,8 @@ public class SbnBuilder {
private String mTag;
private int mUid;
private int mInitialPid;
- private Notification mNotification = new Notification();
+ @Nullable private Notification mNotification;
+ @Nullable private Notification.Builder mNotificationBuilder;
private Notification.BubbleMetadata mBubbleMetadata;
private UserHandle mUser = UserHandle.of(0);
private String mOverrideGroupKey;
@@ -55,9 +58,19 @@ public class SbnBuilder {
}
public StatusBarNotification build() {
+ Notification notification;
+ if (mNotificationBuilder != null) {
+ notification = mNotificationBuilder.build();
+ } else if (mNotification != null) {
+ notification = mNotification;
+ } else {
+ notification = new Notification();
+ }
+
if (mBubbleMetadata != null) {
- mNotification.setBubbleMetadata(mBubbleMetadata);
+ notification.setBubbleMetadata(mBubbleMetadata);
}
+
return new StatusBarNotification(
mPkg,
mOpPkg,
@@ -65,7 +78,7 @@ public class SbnBuilder {
mTag,
mUid,
mInitialPid,
- mNotification,
+ notification,
mUser,
mOverrideGroupKey,
mPostTime);
@@ -106,6 +119,17 @@ public class SbnBuilder {
return this;
}
+ public Notification.Builder modifyNotification(Context context) {
+ if (mNotification != null) {
+ mNotificationBuilder = new Notification.Builder(context, mNotification);
+ mNotification = null;
+ } else if (mNotificationBuilder == null) {
+ mNotificationBuilder = new Notification.Builder(context);
+ }
+
+ return mNotificationBuilder;
+ }
+
public SbnBuilder setUser(UserHandle user) {
mUser = user;
return this;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotifListBuilderImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotifListBuilderImplTest.java
new file mode 100644
index 000000000000..7326cd4002bb
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotifListBuilderImplTest.java
@@ -0,0 +1,1199 @@
+/*
+ * 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 com.android.internal.util.Preconditions.checkNotNull;
+import static com.android.systemui.statusbar.notification.collection.ListDumper.dumpList;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyList;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+import android.util.ArrayMap;
+import android.util.Log;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.statusbar.NotificationEntryBuilder;
+import com.android.systemui.statusbar.notification.collection.NotifListBuilderImpl.OnRenderListListener;
+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.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.SectionsProvider;
+import com.android.systemui.util.Assert;
+import com.android.systemui.util.time.FakeSystemClock;
+
+import org.junit.Before;
+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.Mockito;
+import org.mockito.MockitoAnnotations;
+import org.mockito.Spy;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+@TestableLooper.RunWithLooper
+public class NotifListBuilderImplTest extends SysuiTestCase {
+
+ private NotifListBuilderImpl mListBuilder;
+ private FakeSystemClock mSystemClock = new FakeSystemClock();
+
+ @Mock private NotifCollection mNotifCollection;
+ @Spy private OnBeforeTransformGroupsListener mOnBeforeTransformGroupsListener;
+ @Spy private OnBeforeSortListener mOnBeforeSortListener;
+ @Spy private OnBeforeRenderListListener mOnBeforeRenderListListener;
+ @Spy private OnRenderListListener mOnRenderListListener = list -> mBuiltList = list;
+
+ @Captor private ArgumentCaptor<CollectionReadyForBuildListener> mBuildListenerCaptor;
+
+ private CollectionReadyForBuildListener mReadyForBuildListener;
+ private List<NotificationEntryBuilder> mPendingSet = new ArrayList<>();
+ private List<NotificationEntry> mEntrySet = new ArrayList<>();
+ private List<ListEntry> mBuiltList;
+
+ private Map<String, Integer> mNextIdMap = new ArrayMap<>();
+ private int mNextRank = 0;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ Assert.sMainLooper = TestableLooper.get(this).getLooper();
+
+ mListBuilder = new NotifListBuilderImpl(mSystemClock);
+ mListBuilder.setOnRenderListListener(mOnRenderListListener);
+
+ mListBuilder.attach(mNotifCollection);
+
+ Mockito.verify(mNotifCollection).setBuildListener(mBuildListenerCaptor.capture());
+ mReadyForBuildListener = checkNotNull(mBuildListenerCaptor.getValue());
+ }
+
+ @Test
+ public void testNotifsAreSortedByRankAndWhen() {
+ // GIVEN a simple pipeline
+
+ // WHEN a series of notifs with jumbled ranks are added
+ addNotif(0, PACKAGE_1).setRank(2);
+ addNotif(1, PACKAGE_2).setRank(4).modifyNotification(mContext).setWhen(22);
+ addNotif(2, PACKAGE_3).setRank(4).modifyNotification(mContext).setWhen(33);
+ addNotif(3, PACKAGE_3).setRank(3);
+ addNotif(4, PACKAGE_5).setRank(4).modifyNotification(mContext).setWhen(11);
+ addNotif(5, PACKAGE_3).setRank(1);
+ addNotif(6, PACKAGE_1).setRank(0);
+ dispatchBuild();
+
+ // The final output is sorted based on rank
+ verifyBuiltList(
+ notif(6),
+ notif(5),
+ notif(0),
+ notif(3),
+ notif(2),
+ notif(1),
+ notif(4)
+ );
+ }
+
+ @Test
+ public void testNotifsAreGrouped() {
+ // GIVEN a simple pipeline
+
+ // WHEN a group is added
+ addGroupChild(0, PACKAGE_1, GROUP_1);
+ addGroupChild(1, PACKAGE_1, GROUP_1);
+ addGroupChild(2, PACKAGE_1, GROUP_1);
+ addGroupSummary(3, PACKAGE_1, GROUP_1);
+ dispatchBuild();
+
+ // THEN the notifs are grouped together
+ verifyBuiltList(
+ group(
+ summary(3),
+ child(0),
+ child(1),
+ child(2)
+ )
+ );
+ }
+
+ @Test
+ public void testNotifsWithDifferentGroupKeysAreGrouped() {
+ // GIVEN a simple pipeline
+
+ // WHEN a package posts two different groups
+ addGroupChild(0, PACKAGE_1, GROUP_1);
+ addGroupChild(1, PACKAGE_1, GROUP_2);
+ addGroupSummary(2, PACKAGE_1, GROUP_2);
+ addGroupChild(3, PACKAGE_1, GROUP_2);
+ addGroupChild(4, PACKAGE_1, GROUP_1);
+ addGroupChild(5, PACKAGE_1, GROUP_2);
+ addGroupChild(6, PACKAGE_1, GROUP_1);
+ addGroupSummary(7, PACKAGE_1, GROUP_1);
+ dispatchBuild();
+
+ // THEN the groups are separated separately
+ verifyBuiltList(
+ group(
+ summary(2),
+ child(1),
+ child(3),
+ child(5)
+ ),
+ group(
+ summary(7),
+ child(0),
+ child(4),
+ child(6)
+ )
+ );
+ }
+
+ @Test
+ public void testNotifsNotifChildrenAreSorted() {
+ // GIVEN a simple pipeline
+
+ // WHEN a group is added
+ addGroupChild(0, PACKAGE_1, GROUP_1).setRank(4);
+ addGroupChild(1, PACKAGE_1, GROUP_1).setRank(2)
+ .modifyNotification(mContext).setWhen(11);
+ addGroupChild(2, PACKAGE_1, GROUP_1).setRank(1);
+ addGroupChild(3, PACKAGE_1, GROUP_1).setRank(2)
+ .modifyNotification(mContext).setWhen(33);
+ addGroupChild(4, PACKAGE_1, GROUP_1).setRank(2)
+ .modifyNotification(mContext).setWhen(22);
+ addGroupChild(5, PACKAGE_1, GROUP_1).setRank(0);
+ addGroupSummary(6, PACKAGE_1, GROUP_1).setRank(3);
+ dispatchBuild();
+
+ // THEN the notifs are grouped together
+ verifyBuiltList(
+ group(
+ summary(6),
+ child(5),
+ child(2),
+ child(3),
+ child(4),
+ child(1),
+ child(0)
+ )
+ );
+ }
+
+ @Test
+ public void testDuplicateGroupSummariesAreDiscarded() {
+ // GIVEN a simple pipeline
+
+ // WHEN a group with multiple summaries is added
+ addNotif(0, PACKAGE_3);
+ addGroupChild(1, PACKAGE_1, GROUP_1);
+ addGroupChild(2, PACKAGE_1, GROUP_1);
+ addGroupSummary(3, PACKAGE_1, GROUP_1).setPostTime(22);
+ addGroupSummary(4, PACKAGE_1, GROUP_1).setPostTime(33);
+ addNotif(5, PACKAGE_2);
+ addGroupSummary(6, PACKAGE_1, GROUP_1).setPostTime(11);
+ addGroupChild(7, PACKAGE_1, GROUP_1);
+ dispatchBuild();
+
+ // THEN only most recent summary is used
+ verifyBuiltList(
+ notif(0),
+ group(
+ summary(4),
+ child(1),
+ child(2),
+ child(7)
+ ),
+ notif(5)
+ );
+
+ // THEN the extra summaries have their parents set to null
+ assertNull(mEntrySet.get(3).getParent());
+ assertNull(mEntrySet.get(6).getParent());
+ }
+
+ @Test
+ public void testGroupsWithNoSummaryAreUngrouped() {
+ // GIVEN a group with no summary
+ addNotif(0, PACKAGE_2);
+ addGroupChild(1, PACKAGE_4, GROUP_2);
+ addGroupChild(2, PACKAGE_4, GROUP_2);
+ addGroupChild(3, PACKAGE_4, GROUP_2);
+ addGroupChild(4, PACKAGE_4, GROUP_2);
+
+ // WHEN we build the list
+ dispatchBuild();
+
+ // THEN the children aren't grouped
+ verifyBuiltList(
+ notif(0),
+ notif(1),
+ notif(2),
+ notif(3),
+ notif(4)
+ );
+ }
+
+ @Test
+ public void testGroupsWithNoChildrenAreUngrouped() {
+ // GIVEN a group with a summary but no children
+ addGroupSummary(0, PACKAGE_5, GROUP_1);
+ addNotif(1, PACKAGE_5);
+ addNotif(2, PACKAGE_1);
+
+ // WHEN we build the list
+ dispatchBuild();
+
+ // THEN the summary isn't grouped but is still added to the final list
+ verifyBuiltList(
+ notif(0),
+ notif(1),
+ notif(2)
+ );
+ }
+
+ @Test
+ public void testGroupsWithTooFewChildrenAreSplitUp() {
+ // GIVEN a group with one child
+ addGroupChild(0, PACKAGE_2, GROUP_1);
+ addGroupSummary(1, PACKAGE_2, GROUP_1);
+
+ // WHEN we build the list
+ dispatchBuild();
+
+ // THEN the child is added at top level and the summary is discarded
+ verifyBuiltList(
+ notif(0)
+ );
+
+ assertNull(mEntrySet.get(1).getParent());
+ }
+
+ @Test
+ public void testGroupsWhoLoseChildrenMidPipelineAreSplitUp() {
+ // GIVEN a group with two children
+ addGroupChild(0, PACKAGE_2, GROUP_1);
+ addGroupSummary(1, PACKAGE_2, GROUP_1);
+ addGroupChild(2, PACKAGE_2, GROUP_1);
+
+ // GIVEN a promoter that will promote one of children to top level
+ mListBuilder.addPromoter(new IdPromoter(0));
+
+ // WHEN we build the list
+ dispatchBuild();
+
+ // THEN both children end up at top level (because group is now too small)
+ verifyBuiltList(
+ notif(0),
+ notif(2)
+ );
+
+ // THEN the summary is discarded
+ assertNull(mEntrySet.get(1).getParent());
+ }
+
+ @Test
+ public void testPreviousParentsAreSetProperly() {
+ // GIVEN a notification that is initially added to the list
+ PackageFilter filter = new PackageFilter(PACKAGE_2);
+ filter.setEnabled(false);
+ mListBuilder.addFilter(filter);
+
+ addNotif(0, PACKAGE_1);
+ addNotif(1, PACKAGE_2);
+ addNotif(2, PACKAGE_3);
+ dispatchBuild();
+
+ // WHEN it is suddenly filtered out
+ filter.setEnabled(true);
+ dispatchBuild();
+
+ // THEN its previous parent indicates that it used to be added
+ assertNull(mEntrySet.get(1).getParent());
+ assertEquals(GroupEntry.ROOT_ENTRY, mEntrySet.get(1).getPreviousParent());
+ }
+
+ @Test
+ public void testThatAnnulledGroupsAndSummariesAreProperlyRolledBack() {
+ // GIVEN a registered transform groups listener
+ RecordingOnBeforeTransformGroupsListener listener =
+ new RecordingOnBeforeTransformGroupsListener();
+ mListBuilder.addOnBeforeTransformGroupsListener(listener);
+
+ // GIVEN a malformed group that will be dismantled
+ addGroupChild(0, PACKAGE_2, GROUP_1);
+ addGroupSummary(1, PACKAGE_2, GROUP_1);
+ addNotif(2, PACKAGE_1);
+
+ // WHEN we build the list
+ dispatchBuild();
+
+ // THEN only the child appears in the final list
+ verifyBuiltList(
+ notif(0),
+ notif(2)
+ );
+
+ // THEN the list of newly visible entries doesn't contain the summary or the group
+ assertEquals(
+ Arrays.asList(
+ mEntrySet.get(0),
+ mEntrySet.get(2)),
+ listener.newlyVisibleEntries
+ );
+
+ // THEN the summary has a null parent and an unset firstAddedIteration
+ assertNull(mEntrySet.get(1).getParent());
+ assertEquals(-1, mEntrySet.get(1).mFirstAddedIteration);
+ }
+
+ @Test
+ public void testNotifsAreFiltered() {
+ // GIVEN a NotifFilter that filters out a specific package
+ NotifFilter filter1 = spy(new PackageFilter(PACKAGE_2));
+ mListBuilder.addFilter(filter1);
+
+ // WHEN the pipeline is kicked off on a list of notifs
+ addNotif(0, PACKAGE_1);
+ addNotif(1, PACKAGE_2);
+ addNotif(2, PACKAGE_3);
+ addNotif(3, PACKAGE_2);
+ dispatchBuild();
+
+ // THEN the filter is called on each notif in the original set
+ verify(filter1).shouldFilterOut(eq(mEntrySet.get(0)), anyLong());
+ verify(filter1).shouldFilterOut(eq(mEntrySet.get(1)), anyLong());
+ verify(filter1).shouldFilterOut(eq(mEntrySet.get(2)), anyLong());
+ verify(filter1).shouldFilterOut(eq(mEntrySet.get(3)), anyLong());
+
+ // THEN the final list doesn't contain any filtered-out notifs
+ verifyBuiltList(
+ notif(0),
+ notif(2)
+ );
+
+ // THEN each filtered notif records the filter that did it
+ assertEquals(filter1, mEntrySet.get(1).mExcludingFilter);
+ assertEquals(filter1, mEntrySet.get(3).mExcludingFilter);
+ }
+
+ @Test
+ public void testNotifFiltersCanBePreempted() {
+ // GIVEN two notif filters
+ NotifFilter filter1 = spy(new PackageFilter(PACKAGE_2));
+ NotifFilter filter2 = spy(new PackageFilter(PACKAGE_5));
+ mListBuilder.addFilter(filter1);
+ mListBuilder.addFilter(filter2);
+
+ // WHEN the pipeline is kicked off on a list of notifs
+ addNotif(0, PACKAGE_1);
+ addNotif(1, PACKAGE_2);
+ addNotif(2, PACKAGE_5);
+ dispatchBuild();
+
+ // THEN both filters are called on the first notif but the second filter is never called
+ // on the already-filtered second notif
+ verify(filter1).shouldFilterOut(eq(mEntrySet.get(0)), anyLong());
+ verify(filter1).shouldFilterOut(eq(mEntrySet.get(1)), anyLong());
+ verify(filter1).shouldFilterOut(eq(mEntrySet.get(2)), anyLong());
+ verify(filter2).shouldFilterOut(eq(mEntrySet.get(0)), anyLong());
+ verify(filter2).shouldFilterOut(eq(mEntrySet.get(2)), anyLong());
+
+ // THEN the final list doesn't contain any filtered-out notifs
+ verifyBuiltList(
+ notif(0)
+ );
+
+ // THEN each filtered notif records the filter that did it
+ assertEquals(filter1, mEntrySet.get(1).mExcludingFilter);
+ assertEquals(filter2, mEntrySet.get(2).mExcludingFilter);
+ }
+
+ @Test
+ public void testNotifsArePromoted() {
+ // GIVEN a NotifPromoter that promotes certain notif IDs
+ NotifPromoter promoter = spy(new IdPromoter(1, 2));
+ mListBuilder.addPromoter(promoter);
+
+ // WHEN the pipeline is kicked off
+ addNotif(0, PACKAGE_1);
+ addGroupChild(1, PACKAGE_2, GROUP_1);
+ addGroupChild(2, PACKAGE_2, GROUP_1);
+ addGroupChild(3, PACKAGE_2, GROUP_1);
+ addGroupChild(4, PACKAGE_2, GROUP_1);
+ addGroupSummary(5, PACKAGE_2, GROUP_1);
+ addNotif(6, PACKAGE_3);
+ dispatchBuild();
+
+ // THEN the filter is called on each group child
+ verify(promoter).shouldPromoteToTopLevel(mEntrySet.get(1));
+ verify(promoter).shouldPromoteToTopLevel(mEntrySet.get(2));
+ verify(promoter).shouldPromoteToTopLevel(mEntrySet.get(3));
+ verify(promoter).shouldPromoteToTopLevel(mEntrySet.get(4));
+
+ // THEN the final list contains the promoted entries at top level
+ verifyBuiltList(
+ notif(0),
+ notif(2),
+ notif(3),
+ group(
+ summary(5),
+ child(1),
+ child(4)),
+ notif(6)
+ );
+
+ // THEN each promoted notif records the promoter that did it
+ assertEquals(promoter, mEntrySet.get(2).mNotifPromoter);
+ assertEquals(promoter, mEntrySet.get(3).mNotifPromoter);
+ }
+
+ @Test
+ public void testNotifPromotersCanBePreempted() {
+ // GIVEN two notif promoters
+ NotifPromoter promoter1 = spy(new IdPromoter(1));
+ NotifPromoter promoter2 = spy(new IdPromoter(2));
+ mListBuilder.addPromoter(promoter1);
+ mListBuilder.addPromoter(promoter2);
+
+ // WHEN the pipeline is kicked off on some notifs and a group
+ addNotif(0, PACKAGE_1);
+ addGroupChild(1, PACKAGE_2, GROUP_1);
+ addGroupChild(2, PACKAGE_2, GROUP_1);
+ addGroupChild(3, PACKAGE_2, GROUP_1);
+ addGroupSummary(4, PACKAGE_2, GROUP_1);
+ addNotif(5, PACKAGE_3);
+ dispatchBuild();
+
+ for (NotificationEntry entry : mEntrySet) {
+ Log.d("pizza", "entry: " + entry.getKey() + " " + entry);
+ }
+
+ // THEN both promoters are called on each child, except for children that a previous
+ // promoter has already promoted
+ verify(promoter1).shouldPromoteToTopLevel(mEntrySet.get(1));
+ verify(promoter1).shouldPromoteToTopLevel(mEntrySet.get(2));
+ verify(promoter1).shouldPromoteToTopLevel(mEntrySet.get(3));
+
+ verify(promoter2).shouldPromoteToTopLevel(mEntrySet.get(1));
+ verify(promoter2).shouldPromoteToTopLevel(mEntrySet.get(3));
+
+ // THEN each promoter is recorded on each notif it promoted
+ assertEquals(promoter1, mEntrySet.get(2).mNotifPromoter);
+ assertEquals(promoter2, mEntrySet.get(3).mNotifPromoter);
+ }
+
+ @Test
+ public void testNotifsAreSectioned() {
+ // GIVEN a filter that removes all PACKAGE_4 notifs and a SectionsProvider that divides
+ // notifs based on package name
+ mListBuilder.addFilter(new PackageFilter(PACKAGE_4));
+ final SectionsProvider sectionsProvider = spy(new PackageSectioner());
+ mListBuilder.setSectionsProvider(sectionsProvider);
+
+ // WHEN we build a list with different packages
+ addNotif(0, PACKAGE_4);
+ addNotif(1, PACKAGE_2);
+ addNotif(2, PACKAGE_1);
+ addNotif(3, PACKAGE_3);
+ addGroupSummary(4, PACKAGE_2, GROUP_1);
+ addGroupChild(5, PACKAGE_2, GROUP_1);
+ addGroupChild(6, PACKAGE_2, GROUP_1);
+ addNotif(7, PACKAGE_1);
+ addNotif(8, PACKAGE_2);
+ addNotif(9, PACKAGE_5);
+ addNotif(10, PACKAGE_4);
+ dispatchBuild();
+
+ // THEN the list is sorted according to section
+ verifyBuiltList(
+ notif(2),
+ notif(7),
+ notif(1),
+ group(
+ summary(4),
+ child(5),
+ child(6)
+ ),
+ notif(8),
+ notif(3),
+ notif(9)
+ );
+
+ // THEN the sections provider is called on all top level elements (but no children and no
+ // entries that were filtered out)
+ verify(sectionsProvider).getSection(mEntrySet.get(1));
+ verify(sectionsProvider).getSection(mEntrySet.get(2));
+ verify(sectionsProvider).getSection(mEntrySet.get(3));
+ verify(sectionsProvider).getSection(mEntrySet.get(7));
+ verify(sectionsProvider).getSection(mEntrySet.get(8));
+ verify(sectionsProvider).getSection(mEntrySet.get(9));
+ verify(sectionsProvider).getSection(mBuiltList.get(3));
+ }
+
+ @Test
+ public void testThatNotifComparatorsAreCalled() {
+ // GIVEN a set of comparators that care about specific packages
+ mListBuilder.setComparators(Arrays.asList(
+ new HypeComparator(PACKAGE_4),
+ new HypeComparator(PACKAGE_1, PACKAGE_3),
+ new HypeComparator(PACKAGE_2)
+ ));
+
+ // WHEN the pipeline is kicked off on a bunch of notifications
+ addNotif(0, PACKAGE_1);
+ addNotif(1, PACKAGE_5);
+ addNotif(2, PACKAGE_3);
+ addNotif(3, PACKAGE_4);
+ addNotif(4, PACKAGE_2);
+ dispatchBuild();
+
+ // THEN the notifs are sorted according to the hierarchy of comparators
+ verifyBuiltList(
+ notif(3),
+ notif(0),
+ notif(2),
+ notif(4),
+ notif(1)
+ );
+ }
+
+ @Test
+ public void testListenersAndPluggablesAreFiredInOrder() {
+ // GIVEN a bunch of registered listeners and pluggables
+ NotifFilter filter = spy(new PackageFilter(PACKAGE_1));
+ NotifPromoter promoter = spy(new IdPromoter(3));
+ PackageSectioner sectioner = spy(new PackageSectioner());
+ NotifComparator comparator = spy(new HypeComparator(PACKAGE_4));
+ mListBuilder.addFilter(filter);
+ mListBuilder.addOnBeforeTransformGroupsListener(mOnBeforeTransformGroupsListener);
+ mListBuilder.addPromoter(promoter);
+ mListBuilder.addOnBeforeSortListener(mOnBeforeSortListener);
+ mListBuilder.setComparators(Collections.singletonList(comparator));
+ mListBuilder.setSectionsProvider(sectioner);
+ mListBuilder.addOnBeforeRenderListListener(mOnBeforeRenderListListener);
+
+ // WHEN a few new notifs are added
+ addNotif(0, PACKAGE_1);
+ addGroupSummary(1, PACKAGE_2, GROUP_1);
+ addGroupChild(2, PACKAGE_2, GROUP_1);
+ addGroupChild(3, PACKAGE_2, GROUP_1);
+ addNotif(4, PACKAGE_5);
+ addNotif(5, PACKAGE_5);
+ addNotif(6, PACKAGE_4);
+ dispatchBuild();
+
+ // THEN the pluggables and listeners are called in order
+ InOrder inOrder = inOrder(
+ filter,
+ mOnBeforeTransformGroupsListener,
+ promoter,
+ mOnBeforeSortListener,
+ sectioner,
+ comparator,
+ mOnBeforeRenderListListener,
+ mOnRenderListListener);
+
+ inOrder.verify(filter, atLeastOnce())
+ .shouldFilterOut(any(NotificationEntry.class), anyLong());
+ inOrder.verify(mOnBeforeTransformGroupsListener)
+ .onBeforeTransformGroups(anyList(), anyList());
+ inOrder.verify(promoter, atLeastOnce())
+ .shouldPromoteToTopLevel(any(NotificationEntry.class));
+ inOrder.verify(mOnBeforeSortListener).onBeforeSort(anyList());
+ inOrder.verify(sectioner, atLeastOnce()).getSection(any(ListEntry.class));
+ inOrder.verify(comparator, atLeastOnce())
+ .compare(any(ListEntry.class), any(ListEntry.class));
+ inOrder.verify(mOnBeforeRenderListListener).onBeforeRenderList(anyList());
+ inOrder.verify(mOnRenderListListener).onRenderList(anyList());
+ }
+
+ @Test
+ public void testThatPluggableInvalidationsTriggersRerun() {
+ // GIVEN a variety of pluggables
+ NotifFilter packageFilter = new PackageFilter(PACKAGE_1);
+ NotifPromoter idPromoter = new IdPromoter(4);
+ SectionsProvider sectionsProvider = new PackageSectioner();
+ NotifComparator hypeComparator = new HypeComparator(PACKAGE_2);
+
+ mListBuilder.addFilter(packageFilter);
+ mListBuilder.addPromoter(idPromoter);
+ mListBuilder.setSectionsProvider(sectionsProvider);
+ mListBuilder.setComparators(Collections.singletonList(hypeComparator));
+
+ // GIVEN a set of random notifs
+ addNotif(0, PACKAGE_1);
+ addNotif(1, PACKAGE_2);
+ addNotif(2, PACKAGE_3);
+ dispatchBuild();
+
+ // WHEN each pluggable is invalidated THEN the list is re-rendered
+
+ clearInvocations(mOnRenderListListener);
+ packageFilter.invalidateList();
+ verify(mOnRenderListListener).onRenderList(anyList());
+
+ clearInvocations(mOnRenderListListener);
+ idPromoter.invalidateList();
+ verify(mOnRenderListListener).onRenderList(anyList());
+
+ clearInvocations(mOnRenderListListener);
+ sectionsProvider.invalidateList();
+ verify(mOnRenderListListener).onRenderList(anyList());
+
+ clearInvocations(mOnRenderListListener);
+ hypeComparator.invalidateList();
+ verify(mOnRenderListListener).onRenderList(anyList());
+ }
+
+ @Test
+ public void testNotifFiltersAreAllSentTheSameNow() {
+ // GIVEN three notif filters
+ NotifFilter filter1 = spy(new PackageFilter(PACKAGE_5));
+ NotifFilter filter2 = spy(new PackageFilter(PACKAGE_5));
+ NotifFilter filter3 = spy(new PackageFilter(PACKAGE_5));
+ mListBuilder.addFilter(filter1);
+ mListBuilder.addFilter(filter2);
+ mListBuilder.addFilter(filter3);
+
+ // GIVEN the SystemClock is set to a particular time:
+ mSystemClock.setAutoIncrement(true);
+ mSystemClock.setUptimeMillis(47);
+
+ // WHEN the pipeline is kicked off on a list of notifs
+ addNotif(0, PACKAGE_1);
+ addNotif(1, PACKAGE_2);
+ dispatchBuild();
+
+ // THEN the value of `now` is the same for all calls to shouldFilterOut
+ verify(filter1).shouldFilterOut(mEntrySet.get(0), 47);
+ verify(filter2).shouldFilterOut(mEntrySet.get(0), 47);
+ verify(filter3).shouldFilterOut(mEntrySet.get(0), 47);
+ verify(filter1).shouldFilterOut(mEntrySet.get(1), 47);
+ verify(filter2).shouldFilterOut(mEntrySet.get(1), 47);
+ verify(filter3).shouldFilterOut(mEntrySet.get(1), 47);
+ }
+
+ @Test
+ public void testNewlyAddedEntries() {
+ // GIVEN a registered OnBeforeTransformGroupsListener
+ RecordingOnBeforeTransformGroupsListener listener =
+ spy(new RecordingOnBeforeTransformGroupsListener());
+ mListBuilder.addOnBeforeTransformGroupsListener(listener);
+
+ // Given some new notifs
+ addNotif(0, PACKAGE_1);
+ addGroupChild(1, PACKAGE_2, GROUP_1);
+ addGroupSummary(2, PACKAGE_2, GROUP_1);
+ addGroupChild(3, PACKAGE_2, GROUP_1);
+ addNotif(4, PACKAGE_3);
+ addGroupChild(5, PACKAGE_2, GROUP_1);
+
+ // WHEN we run the pipeline
+ dispatchBuild();
+
+ verifyBuiltList(
+ notif(0),
+ group(
+ summary(2),
+ child(1),
+ child(3),
+ child(5)
+ ),
+ notif(4)
+ );
+
+ // THEN all the new notifs, including the new GroupEntry, are passed to the listener
+ verify(listener).onBeforeTransformGroups(
+ Arrays.asList(
+ mEntrySet.get(0),
+ mBuiltList.get(1),
+ mEntrySet.get(4)
+ ),
+ Arrays.asList(
+ mEntrySet.get(0),
+ mEntrySet.get(1),
+ mBuiltList.get(1),
+ mEntrySet.get(2),
+ mEntrySet.get(3),
+ mEntrySet.get(4),
+ mEntrySet.get(5)
+ )
+ );
+ }
+
+ @Test
+ public void testNewlyAddedEntriesOnSecondRun() {
+ // GIVEN a registered OnBeforeTransformGroupsListener
+ RecordingOnBeforeTransformGroupsListener listener =
+ spy(new RecordingOnBeforeTransformGroupsListener());
+ mListBuilder.addOnBeforeTransformGroupsListener(listener);
+
+ // Given some notifs that have already been added (two of which are in malformed groups)
+ addNotif(0, PACKAGE_1);
+ addGroupChild(1, PACKAGE_2, GROUP_1);
+ addGroupChild(2, PACKAGE_3, GROUP_2);
+
+ dispatchBuild();
+ clearInvocations(listener);
+
+ // WHEN we run the pipeline
+ addGroupSummary(3, PACKAGE_2, GROUP_1);
+ addGroupChild(4, PACKAGE_3, GROUP_2);
+ addGroupSummary(5, PACKAGE_3, GROUP_2);
+ addGroupChild(6, PACKAGE_3, GROUP_2);
+ addNotif(7, PACKAGE_2);
+
+ dispatchBuild();
+
+ verifyBuiltList(
+ notif(0),
+ notif(1),
+ group(
+ summary(5),
+ child(2),
+ child(4),
+ child(6)
+ ),
+ notif(7)
+ );
+
+ // THEN all the new notifs, including the new GroupEntry, are passed to the listener
+ verify(listener).onBeforeTransformGroups(
+ Arrays.asList(
+ mEntrySet.get(0),
+ mEntrySet.get(1),
+ mBuiltList.get(2),
+ mEntrySet.get(7)
+ ),
+ Arrays.asList(
+ mBuiltList.get(2),
+ mEntrySet.get(4),
+ mEntrySet.get(5),
+ mEntrySet.get(6),
+ mEntrySet.get(7)
+ )
+ );
+ }
+
+ @Test
+ public void testAnnulledGroupsHaveParentSetProperly() {
+ // GIVEN a list containing a small group that's already been built once
+ addGroupChild(0, PACKAGE_2, GROUP_2);
+ addGroupSummary(1, PACKAGE_2, GROUP_2);
+ addGroupChild(2, PACKAGE_2, GROUP_2);
+ dispatchBuild();
+
+ verifyBuiltList(
+ group(
+ summary(1),
+ child(0),
+ child(2)
+ )
+ );
+ GroupEntry group = (GroupEntry) mBuiltList.get(0);
+
+ // WHEN a child is removed such that the group is no longer big enough
+ mEntrySet.remove(2);
+ dispatchBuild();
+
+ // THEN the group is annulled and its parent is set back to null
+ verifyBuiltList(
+ notif(0)
+ );
+ assertNull(group.getParent());
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void testOutOfOrderFilterInvalidationThrows() {
+ // GIVEN a NotifFilter that gets invalidated during the grouping stage
+ NotifFilter filter = new PackageFilter(PACKAGE_5);
+ OnBeforeTransformGroupsListener listener =
+ (list, newlyVisibleEntries) -> filter.invalidateList();
+ mListBuilder.addFilter(filter);
+ mListBuilder.addOnBeforeTransformGroupsListener(listener);
+
+ // WHEN we try to run the pipeline and the filter is invalidated
+ addNotif(0, PACKAGE_1);
+ dispatchBuild();
+
+ // Then an exception is thrown
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void testOutOfOrderPrompterInvalidationThrows() {
+ // GIVEN a NotifFilter that gets invalidated during the grouping stage
+ NotifPromoter promoter = new IdPromoter(47);
+ OnBeforeSortListener listener =
+ (list) -> promoter.invalidateList();
+ mListBuilder.addPromoter(promoter);
+ mListBuilder.addOnBeforeSortListener(listener);
+
+ // WHEN we try to run the pipeline and the filter is invalidated
+ addNotif(0, PACKAGE_1);
+ dispatchBuild();
+
+ // Then an exception is thrown
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void testOutOfOrderComparatorInvalidationThrows() {
+ // GIVEN a NotifFilter that gets invalidated during the grouping stage
+ NotifComparator comparator = new HypeComparator(PACKAGE_5);
+ OnBeforeRenderListListener listener =
+ (list) -> comparator.invalidateList();
+ mListBuilder.setComparators(Collections.singletonList(comparator));
+ mListBuilder.addOnBeforeRenderListListener(listener);
+
+ // WHEN we try to run the pipeline and the filter is invalidated
+ addNotif(0, PACKAGE_1);
+ dispatchBuild();
+
+ // Then an exception is thrown
+ }
+
+ /**
+ * Adds a notif to the collection that will be passed to the list builder when
+ * {@link #dispatchBuild()}s is called.
+ *
+ * @param index Index of this notification in the set. This must be the current size of the set.
+ * it exists to improve readability of the resulting code, since later tests will
+ * have to refer to notifs by index.
+ * @param packageId Package that the notif should be posted under
+ * @return A NotificationEntryBuilder that can be used to further modify the notif. Do not call
+ * build() on the builder; that will be done on the next dispatchBuild().
+ */
+ private NotificationEntryBuilder addNotif(int index, String packageId) {
+ final NotificationEntryBuilder builder = new NotificationEntryBuilder()
+ .setPkg(packageId)
+ .setId(nextId(packageId))
+ .setRank(nextRank());
+
+ builder.modifyNotification(mContext)
+ .setContentTitle("Top level singleton")
+ .setChannelId("test_channel");
+
+ assertEquals(mEntrySet.size() + mPendingSet.size(), index);
+ mPendingSet.add(builder);
+ return builder;
+ }
+
+ /** Same behavior as {@link #addNotif(int, String)}. */
+ private NotificationEntryBuilder addGroupSummary(int index, String packageId, String groupId) {
+ final NotificationEntryBuilder builder = new NotificationEntryBuilder()
+ .setPkg(packageId)
+ .setId(nextId(packageId))
+ .setRank(nextRank());
+
+ builder.modifyNotification(mContext)
+ .setChannelId("test_channel")
+ .setContentTitle("Group summary")
+ .setGroup(groupId)
+ .setGroupSummary(true);
+
+ assertEquals(mEntrySet.size() + mPendingSet.size(), index);
+ mPendingSet.add(builder);
+ return builder;
+ }
+
+ /** Same behavior as {@link #addNotif(int, String)}. */
+ private NotificationEntryBuilder addGroupChild(int index, String packageId, String groupId) {
+ final NotificationEntryBuilder builder = new NotificationEntryBuilder()
+ .setPkg(packageId)
+ .setId(nextId(packageId))
+ .setRank(nextRank());
+
+ builder.modifyNotification(mContext)
+ .setChannelId("test_channel")
+ .setContentTitle("Group child")
+ .setGroup(groupId);
+
+ assertEquals(mEntrySet.size() + mPendingSet.size(), index);
+ mPendingSet.add(builder);
+ return builder;
+ }
+
+ private int nextId(String packageName) {
+ Integer nextId = mNextIdMap.get(packageName);
+ if (nextId == null) {
+ nextId = 0;
+ }
+ mNextIdMap.put(packageName, nextId + 1);
+ return nextId;
+ }
+
+ private int nextRank() {
+ int nextRank = mNextRank;
+ mNextRank++;
+ return nextRank;
+ }
+
+ private void dispatchBuild() {
+ if (mPendingSet.size() > 0) {
+ for (NotificationEntryBuilder builder : mPendingSet) {
+ mEntrySet.add(builder.build());
+ }
+ mPendingSet.clear();
+ }
+
+ mReadyForBuildListener.onBeginDispatchToListeners();
+ mReadyForBuildListener.onBuildList(mEntrySet);
+ }
+
+ private void verifyBuiltList(ExpectedEntry ...expectedEntries) {
+ try {
+ assertEquals(
+ "List is the wrong length",
+ expectedEntries.length,
+ mBuiltList.size());
+
+ for (int i = 0; i < expectedEntries.length; i++) {
+ ListEntry outEntry = mBuiltList.get(i);
+ ExpectedEntry expectedEntry = expectedEntries[i];
+
+ if (expectedEntry instanceof ExpectedNotif) {
+ assertEquals(
+ "Entry " + i + " isn't a NotifEntry",
+ NotificationEntry.class,
+ outEntry.getClass());
+ assertEquals(
+ "Entry " + i + " doesn't match expected value.",
+ ((ExpectedNotif) expectedEntry).entry, outEntry);
+ } else {
+ ExpectedGroup cmpGroup = (ExpectedGroup) expectedEntry;
+
+ assertEquals(
+ "Entry " + i + " isn't a GroupEntry",
+ GroupEntry.class,
+ outEntry.getClass());
+
+ GroupEntry outGroup = (GroupEntry) outEntry;
+
+ assertEquals(
+ "Summary notif for entry " + i
+ + " doesn't match expected value",
+ cmpGroup.summary,
+ outGroup.getSummary());
+ assertEquals(
+ "Summary notif for entry " + i
+ + " doesn't have proper parent",
+ outGroup,
+ outGroup.getSummary().getParent());
+
+ assertEquals("Children for entry " + i,
+ cmpGroup.children,
+ outGroup.getChildren());
+
+ for (int j = 0; j < outGroup.getChildren().size(); j++) {
+ NotificationEntry child = outGroup.getChildren().get(j);
+ assertEquals(
+ "Child " + j + " for entry " + i
+ + " doesn't have proper parent",
+ outGroup,
+ child.getParent());
+ }
+ }
+ }
+ } catch (AssertionError err) {
+ throw new AssertionError(
+ "List under test failed verification:\n" + dumpList(mBuiltList), err);
+ }
+ }
+
+ private ExpectedNotif notif(int index) {
+ return new ExpectedNotif(mEntrySet.get(index));
+ }
+
+ private ExpectedGroup group(ExpectedSummary summary, ExpectedChild...children) {
+ return new ExpectedGroup(
+ summary.entry,
+ Arrays.stream(children)
+ .map(child -> child.entry)
+ .collect(Collectors.toList()));
+ }
+
+ private ExpectedSummary summary(int index) {
+ return new ExpectedSummary(mEntrySet.get(index));
+ }
+
+ private ExpectedChild child(int index) {
+ return new ExpectedChild(mEntrySet.get(index));
+ }
+
+ private abstract static class ExpectedEntry {
+ }
+
+ private static class ExpectedNotif extends ExpectedEntry {
+ public final NotificationEntry entry;
+
+ private ExpectedNotif(NotificationEntry entry) {
+ this.entry = entry;
+ }
+ }
+
+ private static class ExpectedGroup extends ExpectedEntry {
+ public final NotificationEntry summary;
+ public final List<NotificationEntry> children;
+
+ private ExpectedGroup(
+ NotificationEntry summary,
+ List<NotificationEntry> children) {
+ this.summary = summary;
+ this.children = children;
+ }
+ }
+
+ private static class ExpectedSummary {
+ public final NotificationEntry entry;
+
+ private ExpectedSummary(NotificationEntry entry) {
+ this.entry = entry;
+ }
+ }
+
+ private static class ExpectedChild {
+ public final NotificationEntry entry;
+
+ private ExpectedChild(NotificationEntry entry) {
+ this.entry = entry;
+ }
+ }
+
+ /** Filters out notifs from a particular package */
+ private static class PackageFilter extends NotifFilter {
+ private final String mPackageName;
+
+ private boolean mEnabled = true;
+
+ PackageFilter(String packageName) {
+ super("PackageFilter");
+
+ mPackageName = packageName;
+ }
+
+ @Override
+ public boolean shouldFilterOut(NotificationEntry entry, long now) {
+ return mEnabled && entry.getSbn().getPackageName().equals(mPackageName);
+ }
+
+ public void setEnabled(boolean enabled) {
+ mEnabled = enabled;
+ }
+ }
+
+ /** Promotes notifs with particular IDs */
+ private static class IdPromoter extends NotifPromoter {
+ private final List<Integer> mIds;
+
+ IdPromoter(Integer... ids) {
+ super("IdPromoter");
+ mIds = Arrays.asList(ids);
+ }
+
+ @Override
+ public boolean shouldPromoteToTopLevel(NotificationEntry child) {
+ return mIds.contains(child.getSbn().getId());
+ }
+ }
+
+ /** Sorts specific notifs above all others. */
+ private static class HypeComparator extends NotifComparator {
+
+ private final List<String> mPreferredPackages;
+
+ HypeComparator(String ...preferredPackages) {
+ super("HypeComparator");
+ mPreferredPackages = Arrays.asList(preferredPackages);
+ }
+
+ @Override
+ public int compare(ListEntry o1, ListEntry o2) {
+ boolean contains1 = mPreferredPackages.contains(
+ o1.getRepresentativeEntry().getSbn().getPackageName());
+ boolean contains2 = mPreferredPackages.contains(
+ o2.getRepresentativeEntry().getSbn().getPackageName());
+
+ return Boolean.compare(contains2, contains1);
+ }
+ }
+
+ /** Sorts notifs into sections based on their package name */
+ private static class PackageSectioner extends SectionsProvider {
+
+ PackageSectioner() {
+ super("PackageSectioner");
+ }
+
+ @Override
+ public int getSection(ListEntry entry) {
+ switch (entry.getRepresentativeEntry().getSbn().getPackageName()) {
+ case PACKAGE_1:
+ return 1;
+ case PACKAGE_2:
+ return 2;
+ case PACKAGE_3:
+ return 3;
+ default:
+ return 4;
+ }
+ }
+ }
+
+ private static class RecordingOnBeforeTransformGroupsListener
+ implements OnBeforeTransformGroupsListener {
+ public List<ListEntry> newlyVisibleEntries;
+
+ @Override
+ public void onBeforeTransformGroups(List<ListEntry> list,
+ List<ListEntry> newlyVisibleEntries) {
+ this.newlyVisibleEntries = newlyVisibleEntries;
+ }
+ }
+
+ private static final String PACKAGE_1 = "com.test1";
+ private static final String PACKAGE_2 = "com.test2";
+ private static final String PACKAGE_3 = "org.test3";
+ private static final String PACKAGE_4 = "com.test4";
+ private static final String PACKAGE_5 = "com.test5";
+
+ private static final String GROUP_1 = "group_1";
+ private static final String GROUP_2 = "group_2";
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarTest.java
index 95929c3adcb8..aecdd83e24b7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarTest.java
@@ -111,7 +111,6 @@ import com.android.systemui.statusbar.SuperStatusBarViewFactory;
import com.android.systemui.statusbar.VibratorHelper;
import com.android.systemui.statusbar.notification.BypassHeadsUpNotifier;
import com.android.systemui.statusbar.notification.DynamicPrivacyController;
-import com.android.systemui.statusbar.notification.NewNotifPipeline;
import com.android.systemui.statusbar.notification.NotificationAlertingManager;
import com.android.systemui.statusbar.notification.NotificationEntryListener;
import com.android.systemui.statusbar.notification.NotificationEntryManager;
@@ -120,6 +119,7 @@ import com.android.systemui.statusbar.notification.NotificationInterruptionState
import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator;
import com.android.systemui.statusbar.notification.VisualStabilityManager;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
+import com.android.systemui.statusbar.notification.collection.init.NewNotifPipeline;
import com.android.systemui.statusbar.notification.logging.NotificationLogger;
import com.android.systemui.statusbar.notification.row.NotificationGutsManager;
import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout;
diff --git a/tests/StatusBar/src/com/android/statusbartest/StatusBarTest.java b/tests/StatusBar/src/com/android/statusbartest/StatusBarTest.java
index cd04c2e197f9..3d72ee67a227 100644
--- a/tests/StatusBar/src/com/android/statusbartest/StatusBarTest.java
+++ b/tests/StatusBar/src/com/android/statusbartest/StatusBarTest.java
@@ -18,13 +18,13 @@ package com.android.statusbartest;
import android.app.Notification;
import android.app.NotificationManager;
-import android.view.View;
-import android.content.Intent;
import android.app.PendingIntent;
import android.app.StatusBarManager;
+import android.content.Intent;
import android.os.Handler;
-import android.util.Log;
import android.os.SystemClock;
+import android.util.Log;
+import android.view.View;
import android.view.Window;
import android.view.WindowManager;