diff options
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; |