blob: 9f8f42ee116ce196f6a79e74ae6fc11734519459 [file] [log] [blame]
Ned Burns77050aa2019-10-17 21:55:24 -04001/*
2 * Copyright (C) 2019 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.systemui.statusbar.notification.collection;
18
19import static com.android.systemui.statusbar.notification.collection.GroupEntry.ROOT_ENTRY;
Ned Burns77050aa2019-10-17 21:55:24 -040020import static com.android.systemui.statusbar.notification.collection.listbuilder.PipelineState.STATE_BUILD_STARTED;
Ned Burns77050aa2019-10-17 21:55:24 -040021import static com.android.systemui.statusbar.notification.collection.listbuilder.PipelineState.STATE_FINALIZING;
Beverly142c5732019-12-11 14:26:49 -050022import static com.android.systemui.statusbar.notification.collection.listbuilder.PipelineState.STATE_GROUPING;
Ned Burns77050aa2019-10-17 21:55:24 -040023import static com.android.systemui.statusbar.notification.collection.listbuilder.PipelineState.STATE_IDLE;
Beverly142c5732019-12-11 14:26:49 -050024import static com.android.systemui.statusbar.notification.collection.listbuilder.PipelineState.STATE_PRE_GROUP_FILTERING;
25import static com.android.systemui.statusbar.notification.collection.listbuilder.PipelineState.STATE_PRE_RENDER_FILTERING;
26import static com.android.systemui.statusbar.notification.collection.listbuilder.PipelineState.STATE_RESETTING;
Ned Burns77050aa2019-10-17 21:55:24 -040027import static com.android.systemui.statusbar.notification.collection.listbuilder.PipelineState.STATE_SORTING;
28import static com.android.systemui.statusbar.notification.collection.listbuilder.PipelineState.STATE_TRANSFORMING;
29
30import android.annotation.MainThread;
31import android.annotation.Nullable;
32import android.util.ArrayMap;
Beverly48220972020-01-13 16:55:38 -050033import android.util.Pair;
Ned Burns77050aa2019-10-17 21:55:24 -040034
Ned Burns7d2e9c12020-01-21 17:47:16 -050035import androidx.annotation.NonNull;
36
Beverlyb6f4dc22020-01-10 14:58:20 -050037import com.android.systemui.DumpController;
38import com.android.systemui.Dumpable;
Ned Burns77050aa2019-10-17 21:55:24 -040039import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeRenderListListener;
40import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeSortListener;
41import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeTransformGroupsListener;
42import com.android.systemui.statusbar.notification.collection.listbuilder.PipelineState;
Ned Burns7d2e9c12020-01-21 17:47:16 -050043import com.android.systemui.statusbar.notification.collection.listbuilder.ShadeListBuilderLogger;
Ned Burns77050aa2019-10-17 21:55:24 -040044import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifComparator;
45import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifFilter;
46import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifPromoter;
Beverly48220972020-01-13 16:55:38 -050047import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSection;
Ned Burns012048d2020-01-08 19:57:30 -050048import com.android.systemui.statusbar.notification.collection.notifcollection.CollectionReadyForBuildListener;
Ned Burns77050aa2019-10-17 21:55:24 -040049import com.android.systemui.util.Assert;
50import com.android.systemui.util.time.SystemClock;
51
Beverlyb6f4dc22020-01-10 14:58:20 -050052import java.io.FileDescriptor;
53import java.io.PrintWriter;
Ned Burns77050aa2019-10-17 21:55:24 -040054import java.util.ArrayList;
55import java.util.Collection;
56import java.util.Collections;
57import java.util.Comparator;
58import java.util.List;
59import java.util.Map;
60
61import javax.inject.Inject;
62import javax.inject.Singleton;
63
64/**
Ned Burns8172d3a2020-01-10 00:24:17 -050065 * The second half of {@link NotifPipeline}. Sits downstream of the NotifCollection and transforms
66 * its "notification set" into the "shade list", the filtered, grouped, and sorted list of
67 * notifications that are currently present in the notification shade.
Ned Burns77050aa2019-10-17 21:55:24 -040068 */
69@MainThread
70@Singleton
Beverlyb6f4dc22020-01-10 14:58:20 -050071public class ShadeListBuilder implements Dumpable {
Ned Burns77050aa2019-10-17 21:55:24 -040072 private final SystemClock mSystemClock;
Ned Burns7d2e9c12020-01-21 17:47:16 -050073 private final ShadeListBuilderLogger mLogger;
Ned Burns77050aa2019-10-17 21:55:24 -040074
Beverly142c5732019-12-11 14:26:49 -050075 private List<ListEntry> mNotifList = new ArrayList<>();
76 private List<ListEntry> mNewNotifList = new ArrayList<>();
Ned Burns77050aa2019-10-17 21:55:24 -040077
78 private final PipelineState mPipelineState = new PipelineState();
79 private final Map<String, GroupEntry> mGroups = new ArrayMap<>();
80 private Collection<NotificationEntry> mAllEntries = Collections.emptyList();
Ned Burns77050aa2019-10-17 21:55:24 -040081 private int mIterationCount = 0;
82
Beverly142c5732019-12-11 14:26:49 -050083 private final List<NotifFilter> mNotifPreGroupFilters = new ArrayList<>();
Ned Burns77050aa2019-10-17 21:55:24 -040084 private final List<NotifPromoter> mNotifPromoters = new ArrayList<>();
Beverly142c5732019-12-11 14:26:49 -050085 private final List<NotifFilter> mNotifPreRenderFilters = new ArrayList<>();
Ned Burns77050aa2019-10-17 21:55:24 -040086 private final List<NotifComparator> mNotifComparators = new ArrayList<>();
Beverly48220972020-01-13 16:55:38 -050087 private final List<NotifSection> mNotifSections = new ArrayList<>();
Ned Burns77050aa2019-10-17 21:55:24 -040088
89 private final List<OnBeforeTransformGroupsListener> mOnBeforeTransformGroupsListeners =
90 new ArrayList<>();
91 private final List<OnBeforeSortListener> mOnBeforeSortListeners =
92 new ArrayList<>();
93 private final List<OnBeforeRenderListListener> mOnBeforeRenderListListeners =
94 new ArrayList<>();
95 @Nullable private OnRenderListListener mOnRenderListListener;
96
97 private final List<ListEntry> mReadOnlyNotifList = Collections.unmodifiableList(mNotifList);
98
99 @Inject
Beverlyb6f4dc22020-01-10 14:58:20 -0500100 public ShadeListBuilder(
101 SystemClock systemClock,
Ned Burns7d2e9c12020-01-21 17:47:16 -0500102 ShadeListBuilderLogger logger,
Beverlyb6f4dc22020-01-10 14:58:20 -0500103 DumpController dumpController) {
Ned Burns77050aa2019-10-17 21:55:24 -0400104 Assert.isMainThread();
105 mSystemClock = systemClock;
Ned Burns7d2e9c12020-01-21 17:47:16 -0500106 mLogger = logger;
Beverlyb6f4dc22020-01-10 14:58:20 -0500107 dumpController.registerDumpable(TAG, this);
Ned Burns77050aa2019-10-17 21:55:24 -0400108 }
109
110 /**
111 * Attach the list builder to the NotifCollection. After this is called, it will start building
112 * the notif list in response to changes to the colletion.
113 */
114 public void attach(NotifCollection collection) {
115 Assert.isMainThread();
116 collection.setBuildListener(mReadyForBuildListener);
117 }
118
119 /**
120 * Registers the listener that's responsible for rendering the notif list to the screen. Called
121 * At the very end of pipeline execution, after all other listeners and pluggables have fired.
122 */
123 public void setOnRenderListListener(OnRenderListListener onRenderListListener) {
124 Assert.isMainThread();
125
126 mPipelineState.requireState(STATE_IDLE);
127 mOnRenderListListener = onRenderListListener;
128 }
129
Ned Burns8172d3a2020-01-10 00:24:17 -0500130 void addOnBeforeTransformGroupsListener(OnBeforeTransformGroupsListener listener) {
Ned Burns77050aa2019-10-17 21:55:24 -0400131 Assert.isMainThread();
132
133 mPipelineState.requireState(STATE_IDLE);
134 mOnBeforeTransformGroupsListeners.add(listener);
135 }
136
Ned Burns8172d3a2020-01-10 00:24:17 -0500137 void addOnBeforeSortListener(OnBeforeSortListener listener) {
Ned Burns77050aa2019-10-17 21:55:24 -0400138 Assert.isMainThread();
139
140 mPipelineState.requireState(STATE_IDLE);
141 mOnBeforeSortListeners.add(listener);
142 }
143
Ned Burns8172d3a2020-01-10 00:24:17 -0500144 void addOnBeforeRenderListListener(OnBeforeRenderListListener listener) {
Ned Burns77050aa2019-10-17 21:55:24 -0400145 Assert.isMainThread();
146
147 mPipelineState.requireState(STATE_IDLE);
148 mOnBeforeRenderListListeners.add(listener);
149 }
150
Ned Burns8172d3a2020-01-10 00:24:17 -0500151 void addPreGroupFilter(NotifFilter filter) {
Ned Burns77050aa2019-10-17 21:55:24 -0400152 Assert.isMainThread();
153 mPipelineState.requireState(STATE_IDLE);
154
Beverly142c5732019-12-11 14:26:49 -0500155 mNotifPreGroupFilters.add(filter);
156 filter.setInvalidationListener(this::onPreGroupFilterInvalidated);
157 }
158
Ned Burns8172d3a2020-01-10 00:24:17 -0500159 void addPreRenderFilter(NotifFilter filter) {
Beverly142c5732019-12-11 14:26:49 -0500160 Assert.isMainThread();
161 mPipelineState.requireState(STATE_IDLE);
162
163 mNotifPreRenderFilters.add(filter);
164 filter.setInvalidationListener(this::onPreRenderFilterInvalidated);
Ned Burns77050aa2019-10-17 21:55:24 -0400165 }
166
Ned Burns8172d3a2020-01-10 00:24:17 -0500167 void addPromoter(NotifPromoter promoter) {
Ned Burns77050aa2019-10-17 21:55:24 -0400168 Assert.isMainThread();
169 mPipelineState.requireState(STATE_IDLE);
170
171 mNotifPromoters.add(promoter);
172 promoter.setInvalidationListener(this::onPromoterInvalidated);
173 }
174
Beverly48220972020-01-13 16:55:38 -0500175 void setSections(List<NotifSection> sections) {
Ned Burns77050aa2019-10-17 21:55:24 -0400176 Assert.isMainThread();
177 mPipelineState.requireState(STATE_IDLE);
178
Beverly48220972020-01-13 16:55:38 -0500179 mNotifSections.clear();
180 for (NotifSection section : sections) {
181 mNotifSections.add(section);
182 section.setInvalidationListener(this::onNotifSectionInvalidated);
183 }
Ned Burns77050aa2019-10-17 21:55:24 -0400184 }
185
Ned Burns8172d3a2020-01-10 00:24:17 -0500186 void setComparators(List<NotifComparator> comparators) {
Ned Burns77050aa2019-10-17 21:55:24 -0400187 Assert.isMainThread();
188 mPipelineState.requireState(STATE_IDLE);
189
190 mNotifComparators.clear();
191 for (NotifComparator comparator : comparators) {
192 mNotifComparators.add(comparator);
193 comparator.setInvalidationListener(this::onNotifComparatorInvalidated);
194 }
195 }
196
Ned Burns8172d3a2020-01-10 00:24:17 -0500197 List<ListEntry> getShadeList() {
Ned Burns77050aa2019-10-17 21:55:24 -0400198 Assert.isMainThread();
199 return mReadOnlyNotifList;
200 }
201
202 private final CollectionReadyForBuildListener mReadyForBuildListener =
203 new CollectionReadyForBuildListener() {
204 @Override
Ned Burns77050aa2019-10-17 21:55:24 -0400205 public void onBuildList(Collection<NotificationEntry> entries) {
206 Assert.isMainThread();
207 mPipelineState.requireIsBefore(STATE_BUILD_STARTED);
208
Ned Burns7d2e9c12020-01-21 17:47:16 -0500209 mLogger.logOnBuildList();
Ned Burns77050aa2019-10-17 21:55:24 -0400210 mAllEntries = entries;
211 buildList();
212 }
213 };
214
Beverly142c5732019-12-11 14:26:49 -0500215 private void onPreGroupFilterInvalidated(NotifFilter filter) {
Ned Burns77050aa2019-10-17 21:55:24 -0400216 Assert.isMainThread();
217
Ned Burns7d2e9c12020-01-21 17:47:16 -0500218 mLogger.logPreGroupFilterInvalidated(filter.getName(), mPipelineState.getState());
Ned Burns77050aa2019-10-17 21:55:24 -0400219
Beverly142c5732019-12-11 14:26:49 -0500220 rebuildListIfBefore(STATE_PRE_GROUP_FILTERING);
Ned Burns77050aa2019-10-17 21:55:24 -0400221 }
222
Ned Burns7d2e9c12020-01-21 17:47:16 -0500223 private void onPromoterInvalidated(NotifPromoter promoter) {
Ned Burns77050aa2019-10-17 21:55:24 -0400224 Assert.isMainThread();
225
Ned Burns7d2e9c12020-01-21 17:47:16 -0500226 mLogger.logPromoterInvalidated(promoter.getName(), mPipelineState.getState());
Ned Burns77050aa2019-10-17 21:55:24 -0400227
228 rebuildListIfBefore(STATE_TRANSFORMING);
229 }
230
Beverly48220972020-01-13 16:55:38 -0500231 private void onNotifSectionInvalidated(NotifSection section) {
Ned Burns77050aa2019-10-17 21:55:24 -0400232 Assert.isMainThread();
233
Ned Burns7d2e9c12020-01-21 17:47:16 -0500234 mLogger.logNotifSectionInvalidated(section.getName(), mPipelineState.getState());
Ned Burns77050aa2019-10-17 21:55:24 -0400235
236 rebuildListIfBefore(STATE_SORTING);
237 }
238
Beverly142c5732019-12-11 14:26:49 -0500239 private void onPreRenderFilterInvalidated(NotifFilter filter) {
240 Assert.isMainThread();
241
Ned Burns7d2e9c12020-01-21 17:47:16 -0500242 mLogger.logPreRenderFilterInvalidated(filter.getName(), mPipelineState.getState());
Beverly142c5732019-12-11 14:26:49 -0500243
244 rebuildListIfBefore(STATE_PRE_RENDER_FILTERING);
245 }
246
Ned Burns77050aa2019-10-17 21:55:24 -0400247 private void onNotifComparatorInvalidated(NotifComparator comparator) {
248 Assert.isMainThread();
249
Ned Burns7d2e9c12020-01-21 17:47:16 -0500250 mLogger.logNotifComparatorInvalidated(comparator.getName(), mPipelineState.getState());
Ned Burns77050aa2019-10-17 21:55:24 -0400251
252 rebuildListIfBefore(STATE_SORTING);
253 }
254
255 /**
Beverly142c5732019-12-11 14:26:49 -0500256 * Points mNotifList to the list stored in mNewNotifList.
257 * Reuses the (emptied) mNotifList as mNewNotifList.
258 */
259 private void applyNewNotifList() {
260 mNotifList.clear();
261 List<ListEntry> emptyList = mNotifList;
262 mNotifList = mNewNotifList;
263 mNewNotifList = emptyList;
264 }
265
266 /**
Ned Burns8172d3a2020-01-10 00:24:17 -0500267 * The core algorithm of the pipeline. See the top comment in {@link NotifPipeline} for
Ned Burns77050aa2019-10-17 21:55:24 -0400268 * details on our contracts with other code.
269 *
270 * Once the build starts we are very careful to protect against reentrant code. Anything that
271 * tries to invalidate itself after the pipeline has passed it by will return in an exception.
272 * In general, we should be extremely sensitive to client code doing things in the wrong order;
273 * if we detect that behavior, we should crash instantly.
274 */
275 private void buildList() {
Ned Burns7d2e9c12020-01-21 17:47:16 -0500276 mLogger.logStartBuildList(mIterationCount);
Ned Burns77050aa2019-10-17 21:55:24 -0400277
278 mPipelineState.requireIsBefore(STATE_BUILD_STARTED);
279 mPipelineState.setState(STATE_BUILD_STARTED);
280
Beverly142c5732019-12-11 14:26:49 -0500281 // Step 1: Reset notification states
282 mPipelineState.incrementTo(STATE_RESETTING);
283 resetNotifs();
Ned Burns77050aa2019-10-17 21:55:24 -0400284
Beverly142c5732019-12-11 14:26:49 -0500285 // Step 2: Filter out any notifications that shouldn't be shown right now
286 mPipelineState.incrementTo(STATE_PRE_GROUP_FILTERING);
287 filterNotifs(mAllEntries, mNotifList, mNotifPreGroupFilters);
288
289 // Step 3: Group notifications with the same group key and set summaries
290 mPipelineState.incrementTo(STATE_GROUPING);
291 groupNotifs(mNotifList, mNewNotifList);
292 applyNewNotifList();
293 pruneIncompleteGroups(mNotifList);
294
295 // Step 4: Group transforming
Ned Burns77050aa2019-10-17 21:55:24 -0400296 // Move some notifs out of their groups and up to top-level (mostly used for heads-upping)
Beverly142c5732019-12-11 14:26:49 -0500297 dispatchOnBeforeTransformGroups(mReadOnlyNotifList);
Ned Burns77050aa2019-10-17 21:55:24 -0400298 mPipelineState.incrementTo(STATE_TRANSFORMING);
299 promoteNotifs(mNotifList);
Beverly142c5732019-12-11 14:26:49 -0500300 pruneIncompleteGroups(mNotifList);
Ned Burns77050aa2019-10-17 21:55:24 -0400301
Beverly142c5732019-12-11 14:26:49 -0500302 // Step 5: Sort
Ned Burns77050aa2019-10-17 21:55:24 -0400303 // Assign each top-level entry a section, then sort the list by section and then within
304 // section by our list of custom comparators
305 dispatchOnBeforeSort(mReadOnlyNotifList);
306 mPipelineState.incrementTo(STATE_SORTING);
307 sortList();
308
Beverly142c5732019-12-11 14:26:49 -0500309 // Step 6: Filter out entries after pre-group filtering, grouping, promoting and sorting
Beverly48220972020-01-13 16:55:38 -0500310 // Now filters can see grouping information to determine whether to filter or not.
Beverly142c5732019-12-11 14:26:49 -0500311 mPipelineState.incrementTo(STATE_PRE_RENDER_FILTERING);
312 filterNotifs(mNotifList, mNewNotifList, mNotifPreRenderFilters);
313 applyNewNotifList();
314 pruneIncompleteGroups(mNotifList);
315
316 // Step 7: Lock in our group structure and log anything that's changed since the last run
Ned Burns77050aa2019-10-17 21:55:24 -0400317 mPipelineState.incrementTo(STATE_FINALIZING);
318 logParentingChanges();
319 freeEmptyGroups();
320
Beverly142c5732019-12-11 14:26:49 -0500321 // Step 6: Dispatch the new list, first to any listeners and then to the view layer
Ned Burns7d2e9c12020-01-21 17:47:16 -0500322 if (mIterationCount % 10 == 0) {
323 mLogger.logFinalList(mNotifList);
324 }
Ned Burns77050aa2019-10-17 21:55:24 -0400325 dispatchOnBeforeRenderList(mReadOnlyNotifList);
326 if (mOnRenderListListener != null) {
327 mOnRenderListListener.onRenderList(mReadOnlyNotifList);
328 }
329
Beverly142c5732019-12-11 14:26:49 -0500330 // Step 7: We're done!
Ned Burns7d2e9c12020-01-21 17:47:16 -0500331 mLogger.logEndBuildList(mIterationCount);
Ned Burns77050aa2019-10-17 21:55:24 -0400332 mPipelineState.setState(STATE_IDLE);
333 mIterationCount++;
334 }
335
Beverly142c5732019-12-11 14:26:49 -0500336 private void resetNotifs() {
Ned Burns77050aa2019-10-17 21:55:24 -0400337 for (GroupEntry group : mGroups.values()) {
338 group.setPreviousParent(group.getParent());
339 group.setParent(null);
340 group.clearChildren();
341 group.setSummary(null);
342 }
343
Beverly142c5732019-12-11 14:26:49 -0500344 for (NotificationEntry entry : mAllEntries) {
Ned Burns77050aa2019-10-17 21:55:24 -0400345 entry.setPreviousParent(entry.getParent());
346 entry.setParent(null);
347
Ned Burns77050aa2019-10-17 21:55:24 -0400348 if (entry.mFirstAddedIteration == -1) {
349 entry.mFirstAddedIteration = mIterationCount;
Ned Burns77050aa2019-10-17 21:55:24 -0400350 }
Beverly142c5732019-12-11 14:26:49 -0500351 }
Ned Burns77050aa2019-10-17 21:55:24 -0400352
Beverly142c5732019-12-11 14:26:49 -0500353 mNotifList.clear();
354 }
355
356 private void filterNotifs(Collection<? extends ListEntry> entries,
357 List<ListEntry> out, List<NotifFilter> filters) {
358 final long now = mSystemClock.uptimeMillis();
359 for (ListEntry entry : entries) {
360 if (entry instanceof GroupEntry) {
361 final GroupEntry groupEntry = (GroupEntry) entry;
362
363 // apply filter on its summary
364 final NotificationEntry summary = groupEntry.getRepresentativeEntry();
365 if (applyFilters(summary, now, filters)) {
366 groupEntry.setSummary(null);
367 annulAddition(summary);
368 }
369
370 // apply filter on its children
371 final List<NotificationEntry> children = groupEntry.getRawChildren();
372 for (int j = children.size() - 1; j >= 0; j--) {
373 final NotificationEntry child = children.get(j);
374 if (applyFilters(child, now, filters)) {
375 children.remove(child);
376 annulAddition(child);
377 }
378 }
379
380 out.add(groupEntry);
381 } else {
382 if (applyFilters((NotificationEntry) entry, now, filters)) {
383 annulAddition(entry);
384 } else {
385 out.add(entry);
386 }
387 }
388 }
389 }
390
391 private void groupNotifs(List<ListEntry> entries, List<ListEntry> out) {
392 for (ListEntry listEntry : entries) {
393 // since grouping hasn't happened yet, all notifs are NotificationEntries
394 NotificationEntry entry = (NotificationEntry) listEntry;
Ned Burns77050aa2019-10-17 21:55:24 -0400395 if (entry.getSbn().isGroup()) {
396 final String topLevelKey = entry.getSbn().getGroupKey();
397
398 GroupEntry group = mGroups.get(topLevelKey);
399 if (group == null) {
400 group = new GroupEntry(topLevelKey);
401 group.mFirstAddedIteration = mIterationCount;
Ned Burns77050aa2019-10-17 21:55:24 -0400402 mGroups.put(topLevelKey, group);
403 }
404 if (group.getParent() == null) {
405 group.setParent(ROOT_ENTRY);
406 out.add(group);
407 }
408
409 entry.setParent(group);
410
411 if (entry.getSbn().getNotification().isGroupSummary()) {
412 final NotificationEntry existingSummary = group.getSummary();
413
414 if (existingSummary == null) {
415 group.setSummary(entry);
416 } else {
Ned Burns7d2e9c12020-01-21 17:47:16 -0500417 mLogger.logDuplicateSummary(
Ned Burns77050aa2019-10-17 21:55:24 -0400418 group.getKey(),
419 existingSummary.getKey(),
Ned Burns7d2e9c12020-01-21 17:47:16 -0500420 entry.getKey());
Ned Burns77050aa2019-10-17 21:55:24 -0400421
422 // Use whichever one was posted most recently
423 if (entry.getSbn().getPostTime()
424 > existingSummary.getSbn().getPostTime()) {
425 group.setSummary(entry);
Beverly142c5732019-12-11 14:26:49 -0500426 annulAddition(existingSummary, out);
Ned Burns77050aa2019-10-17 21:55:24 -0400427 } else {
Beverly142c5732019-12-11 14:26:49 -0500428 annulAddition(entry, out);
Ned Burns77050aa2019-10-17 21:55:24 -0400429 }
430 }
431 } else {
432 group.addChild(entry);
433 }
434
435 } else {
436
437 final String topLevelKey = entry.getKey();
438 if (mGroups.containsKey(topLevelKey)) {
Ned Burns7d2e9c12020-01-21 17:47:16 -0500439 mLogger.logDuplicateTopLevelKey(topLevelKey);
Ned Burns77050aa2019-10-17 21:55:24 -0400440 } else {
441 entry.setParent(ROOT_ENTRY);
442 out.add(entry);
443 }
444 }
445 }
446 }
447
448 private void promoteNotifs(List<ListEntry> list) {
449 for (int i = 0; i < list.size(); i++) {
450 final ListEntry tle = list.get(i);
451
452 if (tle instanceof GroupEntry) {
453 final GroupEntry group = (GroupEntry) tle;
454
455 group.getRawChildren().removeIf(child -> {
456 final boolean shouldPromote = applyTopLevelPromoters(child);
457
458 if (shouldPromote) {
459 child.setParent(ROOT_ENTRY);
460 list.add(child);
461 }
462
463 return shouldPromote;
464 });
465 }
466 }
467 }
468
Beverly142c5732019-12-11 14:26:49 -0500469 private void pruneIncompleteGroups(List<ListEntry> shadeList) {
Ned Burns77050aa2019-10-17 21:55:24 -0400470 for (int i = 0; i < shadeList.size(); i++) {
471 final ListEntry tle = shadeList.get(i);
472
473 if (tle instanceof GroupEntry) {
474 final GroupEntry group = (GroupEntry) tle;
475 final List<NotificationEntry> children = group.getRawChildren();
476
477 if (group.getSummary() != null && children.size() == 0) {
478 shadeList.remove(i);
479 i--;
480
481 NotificationEntry summary = group.getSummary();
482 summary.setParent(ROOT_ENTRY);
483 shadeList.add(summary);
484
485 group.setSummary(null);
Beverly142c5732019-12-11 14:26:49 -0500486 annulAddition(group, shadeList);
Ned Burns77050aa2019-10-17 21:55:24 -0400487
488 } else if (group.getSummary() == null
489 || children.size() < MIN_CHILDREN_FOR_GROUP) {
490 // If the group doesn't provide a summary or is too small, ignore it and add
491 // its children (if any) directly to top-level.
492
493 shadeList.remove(i);
494 i--;
495
496 if (group.getSummary() != null) {
497 final NotificationEntry summary = group.getSummary();
498 group.setSummary(null);
Beverly142c5732019-12-11 14:26:49 -0500499 annulAddition(summary, shadeList);
Ned Burns77050aa2019-10-17 21:55:24 -0400500 }
501
502 for (int j = 0; j < children.size(); j++) {
503 final NotificationEntry child = children.get(j);
504 child.setParent(ROOT_ENTRY);
505 shadeList.add(child);
506 }
507 children.clear();
508
Beverly142c5732019-12-11 14:26:49 -0500509 annulAddition(group, shadeList);
Ned Burns77050aa2019-10-17 21:55:24 -0400510 }
511 }
512 }
513 }
514
515 /**
516 * If a ListEntry was added to the shade list and then later removed (e.g. because it was a
517 * group that was broken up), this method will erase any bookkeeping traces of that addition
518 * and/or check that they were already erased.
519 *
520 * Before calling this method, the entry must already have been removed from its parent. If
521 * it's a group, its summary must be null and its children must be empty.
522 */
Beverly142c5732019-12-11 14:26:49 -0500523 private void annulAddition(ListEntry entry, List<ListEntry> shadeList) {
Ned Burns77050aa2019-10-17 21:55:24 -0400524
525 // This function does very little, but if any of its assumptions are violated (and it has a
526 // lot of them), it will put the system into an inconsistent state. So we check all of them
527 // here.
528
529 if (entry.getParent() == null || entry.mFirstAddedIteration == -1) {
530 throw new IllegalStateException(
531 "Cannot nullify addition of " + entry.getKey() + ": no such addition. ("
532 + entry.getParent() + " " + entry.mFirstAddedIteration + ")");
533 }
534
535 if (entry.getParent() == ROOT_ENTRY) {
536 if (shadeList.contains(entry)) {
537 throw new IllegalStateException("Cannot nullify addition of " + entry.getKey()
538 + ": it's still in the shade list.");
539 }
540 }
541
542 if (entry instanceof GroupEntry) {
543 GroupEntry ge = (GroupEntry) entry;
544 if (ge.getSummary() != null) {
545 throw new IllegalStateException(
546 "Cannot nullify group " + ge.getKey() + ": summary is not null");
547 }
548 if (!ge.getChildren().isEmpty()) {
549 throw new IllegalStateException(
550 "Cannot nullify group " + ge.getKey() + ": still has children");
551 }
552 } else if (entry instanceof NotificationEntry) {
553 if (entry == entry.getParent().getSummary()
554 || entry.getParent().getChildren().contains(entry)) {
555 throw new IllegalStateException("Cannot nullify addition of child "
556 + entry.getKey() + ": it's still attached to its parent.");
557 }
558 }
559
Beverly142c5732019-12-11 14:26:49 -0500560 annulAddition(entry);
561
562 }
563
564 /**
565 * Erases bookkeeping traces stored on an entry when it is removed from the notif list.
566 * This can happen if the entry is removed from a group that was broken up or if the entry was
567 * filtered out during any of the filtering steps.
568 */
569 private void annulAddition(ListEntry entry) {
Beverly48220972020-01-13 16:55:38 -0500570 entry.setSection(-1);
571 entry.mNotifSection = null;
Ned Burns77050aa2019-10-17 21:55:24 -0400572 entry.setParent(null);
573 if (entry.mFirstAddedIteration == mIterationCount) {
Ned Burns77050aa2019-10-17 21:55:24 -0400574 entry.mFirstAddedIteration = -1;
575 }
576 }
577
578 private void sortList() {
579 // Assign sections to top-level elements and sort their children
580 for (ListEntry entry : mNotifList) {
Beverly48220972020-01-13 16:55:38 -0500581 Pair<NotifSection, Integer> sectionWithIndex = applySections(entry);
Ned Burns77050aa2019-10-17 21:55:24 -0400582 if (entry instanceof GroupEntry) {
583 GroupEntry parent = (GroupEntry) entry;
584 for (NotificationEntry child : parent.getChildren()) {
Beverly48220972020-01-13 16:55:38 -0500585 child.mNotifSection = sectionWithIndex.first;
586 child.setSection(sectionWithIndex.second);
Ned Burns77050aa2019-10-17 21:55:24 -0400587 }
588 parent.sortChildren(sChildComparator);
589 }
590 }
591
592 // Finally, sort all top-level elements
593 mNotifList.sort(mTopLevelComparator);
594 }
595
596 private void freeEmptyGroups() {
597 mGroups.values().removeIf(ge -> ge.getSummary() == null && ge.getChildren().isEmpty());
598 }
599
600 private void logParentingChanges() {
601 for (NotificationEntry entry : mAllEntries) {
602 if (entry.getParent() != entry.getPreviousParent()) {
Ned Burns7d2e9c12020-01-21 17:47:16 -0500603 mLogger.logParentChanged(
Ned Burns77050aa2019-10-17 21:55:24 -0400604 entry.getKey(),
605 entry.getPreviousParent() == null
Ned Burns7d2e9c12020-01-21 17:47:16 -0500606 ? null : entry.getPreviousParent().getKey(),
Ned Burns77050aa2019-10-17 21:55:24 -0400607 entry.getParent() == null
Ned Burns7d2e9c12020-01-21 17:47:16 -0500608 ? null : entry.getParent().getKey());
Ned Burns77050aa2019-10-17 21:55:24 -0400609 }
610 }
611 for (GroupEntry group : mGroups.values()) {
612 if (group.getParent() != group.getPreviousParent()) {
Ned Burns7d2e9c12020-01-21 17:47:16 -0500613 mLogger.logParentChanged(
Ned Burns77050aa2019-10-17 21:55:24 -0400614 group.getKey(),
615 group.getPreviousParent() == null
Ned Burns7d2e9c12020-01-21 17:47:16 -0500616 ? null : group.getPreviousParent().getKey(),
Ned Burns77050aa2019-10-17 21:55:24 -0400617 group.getParent() == null
Ned Burns7d2e9c12020-01-21 17:47:16 -0500618 ? null : group.getParent().getKey());
Ned Burns77050aa2019-10-17 21:55:24 -0400619 }
620 }
621 }
622
623 private final Comparator<ListEntry> mTopLevelComparator = (o1, o2) -> {
624
625 int cmp = Integer.compare(o1.getSection(), o2.getSection());
626
627 if (cmp == 0) {
628 for (int i = 0; i < mNotifComparators.size(); i++) {
629 cmp = mNotifComparators.get(i).compare(o1, o2);
630 if (cmp != 0) {
631 break;
632 }
633 }
634 }
635
636 final NotificationEntry rep1 = o1.getRepresentativeEntry();
637 final NotificationEntry rep2 = o2.getRepresentativeEntry();
638
639 if (cmp == 0) {
640 cmp = rep1.getRanking().getRank() - rep2.getRanking().getRank();
641 }
642
643 if (cmp == 0) {
644 cmp = Long.compare(
645 rep2.getSbn().getNotification().when,
646 rep1.getSbn().getNotification().when);
647 }
648
649 return cmp;
650 };
651
652 private static final Comparator<NotificationEntry> sChildComparator = (o1, o2) -> {
653 int cmp = o1.getRanking().getRank() - o2.getRanking().getRank();
654
655 if (cmp == 0) {
656 cmp = Long.compare(
657 o2.getSbn().getNotification().when,
658 o1.getSbn().getNotification().when);
659 }
660
661 return cmp;
662 };
663
Beverly142c5732019-12-11 14:26:49 -0500664 private boolean applyFilters(NotificationEntry entry, long now, List<NotifFilter> filters) {
665 NotifFilter filter = findRejectingFilter(entry, now, filters);
Ned Burns77050aa2019-10-17 21:55:24 -0400666
667 if (filter != entry.mExcludingFilter) {
Ned Burns7d2e9c12020-01-21 17:47:16 -0500668 mLogger.logFilterChanged(
669 entry.getKey(),
670 entry.mExcludingFilter != null ? entry.mExcludingFilter.getName() : null,
671 filter != null ? filter.getName() : null);
Ned Burns77050aa2019-10-17 21:55:24 -0400672
673 // Note that groups and summaries can also be filtered out later if they're part of a
674 // malformed group. We currently don't have a great way to track that beyond parenting
675 // change logs. Consider adding something similar to mExcludingFilter for them.
676 entry.mExcludingFilter = filter;
677 }
678
679 return filter != null;
680 }
681
Beverly142c5732019-12-11 14:26:49 -0500682 @Nullable private static NotifFilter findRejectingFilter(NotificationEntry entry, long now,
683 List<NotifFilter> filters) {
684 final int size = filters.size();
685
686 for (int i = 0; i < size; i++) {
687 NotifFilter filter = filters.get(i);
Ned Burns77050aa2019-10-17 21:55:24 -0400688 if (filter.shouldFilterOut(entry, now)) {
689 return filter;
690 }
691 }
692 return null;
693 }
694
695 private boolean applyTopLevelPromoters(NotificationEntry entry) {
696 NotifPromoter promoter = findPromoter(entry);
697
698 if (promoter != entry.mNotifPromoter) {
Ned Burns7d2e9c12020-01-21 17:47:16 -0500699 mLogger.logPromoterChanged(
700 entry.getKey(),
701 entry.mNotifPromoter != null ? entry.mNotifPromoter.getName() : null,
702 promoter != null ? promoter.getName() : null);
Ned Burns77050aa2019-10-17 21:55:24 -0400703 entry.mNotifPromoter = promoter;
704 }
705
706 return promoter != null;
707 }
708
709 @Nullable private NotifPromoter findPromoter(NotificationEntry entry) {
710 for (int i = 0; i < mNotifPromoters.size(); i++) {
711 NotifPromoter promoter = mNotifPromoters.get(i);
712 if (promoter.shouldPromoteToTopLevel(entry)) {
713 return promoter;
714 }
715 }
716 return null;
717 }
718
Beverly48220972020-01-13 16:55:38 -0500719 private Pair<NotifSection, Integer> applySections(ListEntry entry) {
720 final Pair<NotifSection, Integer> sectionWithIndex = findSection(entry);
721 final NotifSection section = sectionWithIndex.first;
722 final Integer sectionIndex = sectionWithIndex.second;
723
724 if (section != entry.mNotifSection) {
Ned Burns7d2e9c12020-01-21 17:47:16 -0500725 mLogger.logSectionChanged(
726 entry.getKey(),
727 entry.mNotifSection != null ? entry.mNotifSection.getName() : null,
728 entry.getSection(),
729 section.getName(),
730 sectionIndex);
Beverly48220972020-01-13 16:55:38 -0500731
732 entry.mNotifSection = section;
733 entry.setSection(sectionIndex);
734 }
735
736 return sectionWithIndex;
737 }
738
739 private Pair<NotifSection, Integer> findSection(ListEntry entry) {
740 for (int i = 0; i < mNotifSections.size(); i++) {
741 NotifSection sectioner = mNotifSections.get(i);
742 if (sectioner.isInSection(entry)) {
743 return new Pair<>(sectioner, i);
744 }
745 }
746 return new Pair<>(sDefaultSection, mNotifSections.size());
747 }
748
Ned Burns77050aa2019-10-17 21:55:24 -0400749 private void rebuildListIfBefore(@PipelineState.StateName int state) {
750 mPipelineState.requireIsBefore(state);
751 if (mPipelineState.is(STATE_IDLE)) {
752 buildList();
753 }
754 }
755
Beverly142c5732019-12-11 14:26:49 -0500756 private void dispatchOnBeforeTransformGroups(List<ListEntry> entries) {
Ned Burns77050aa2019-10-17 21:55:24 -0400757 for (int i = 0; i < mOnBeforeTransformGroupsListeners.size(); i++) {
Beverly142c5732019-12-11 14:26:49 -0500758 mOnBeforeTransformGroupsListeners.get(i).onBeforeTransformGroups(entries);
Ned Burns77050aa2019-10-17 21:55:24 -0400759 }
760 }
761
762 private void dispatchOnBeforeSort(List<ListEntry> entries) {
763 for (int i = 0; i < mOnBeforeSortListeners.size(); i++) {
764 mOnBeforeSortListeners.get(i).onBeforeSort(entries);
765 }
766 }
767
768 private void dispatchOnBeforeRenderList(List<ListEntry> entries) {
769 for (int i = 0; i < mOnBeforeRenderListListeners.size(); i++) {
770 mOnBeforeRenderListListeners.get(i).onBeforeRenderList(entries);
771 }
772 }
773
Beverlyb6f4dc22020-01-10 14:58:20 -0500774 @Override
Ned Burns7d2e9c12020-01-21 17:47:16 -0500775 public void dump(@NonNull FileDescriptor fd, PrintWriter pw, @NonNull String[] args) {
Beverlyb6f4dc22020-01-10 14:58:20 -0500776 pw.println("\t" + TAG + " shade notifications:");
777 if (getShadeList().size() == 0) {
778 pw.println("\t\t None");
779 }
780
781 pw.println(ListDumper.dumpTree(
782 getShadeList(),
783 true,
784 "\t\t"));
785 }
786
Ned Burns77050aa2019-10-17 21:55:24 -0400787 /** See {@link #setOnRenderListListener(OnRenderListListener)} */
788 public interface OnRenderListListener {
789 /**
790 * Called with the final filtered, grouped, and sorted list.
791 *
792 * @param entries A read-only view into the current notif list. Note that this list is
793 * backed by the live list and will change in response to new pipeline runs.
794 */
795 void onRenderList(List<ListEntry> entries);
796 }
797
Beverly48220972020-01-13 16:55:38 -0500798 private static final NotifSection sDefaultSection =
799 new NotifSection("DefaultSection") {
800 @Override
801 public boolean isInSection(ListEntry entry) {
802 return true;
803 }
804 };
Ned Burns77050aa2019-10-17 21:55:24 -0400805
806 private static final String TAG = "NotifListBuilderImpl";
807
808 private static final int MIN_CHILDREN_FOR_GROUP = 2;
809}