Merge changes from topic "notiflistbuilder"

* changes:
  Add NotifListBuilder to new notif pipeline
  Add Dagger-injectable version of SystemClock
This commit is contained in:
Ned Burns
2019-11-14 22:01:40 +00:00
committed by Android (Google) Code Review
32 changed files with 3106 additions and 30 deletions

View File

@@ -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;

View File

@@ -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;

View File

@@ -28,10 +28,14 @@ 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;
import com.android.systemui.util.sensors.AsyncSensorManager;
import com.android.systemui.util.time.SystemClock;
import com.android.systemui.util.time.SystemClockImpl;
import javax.inject.Singleton;
@@ -84,4 +88,12 @@ public abstract class SystemUIModule {
@BindsOptionalOf
abstract StatusBar optionalStatusBar();
@Singleton
@Binds
abstract SystemClock bindSystemClock(SystemClockImpl systemClock);
@Singleton
@Binds
abstract NotifListBuilder bindNotifListBuilder(NotifListBuilderImpl impl);
}

View File

@@ -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

View File

@@ -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>");
}

View File

@@ -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 = " ";
}

View File

@@ -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;
}
}

View File

@@ -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);

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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 = " ";
}

View File

@@ -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";

View File

@@ -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();
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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 {}
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}
}

View File

@@ -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);
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -0,0 +1,46 @@
/*
* 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.util.time;
/**
* Testable wrapper around {@link android.os.SystemClock}.
*
* Dagger can inject this wrapper into your classes. The implementation just proxies calls to the
* real SystemClock.
*
* In tests, pass an instance of FakeSystemClock, which allows you to control the values returned by
* the various getters below.
*/
public interface SystemClock {
/** @see android.os.SystemClock#uptimeMillis() */
long uptimeMillis();
/** @see android.os.SystemClock#elapsedRealtime() */
long elapsedRealtime();
/** @see android.os.SystemClock#elapsedRealtimeNanos() */
long elapsedRealtimeNanos();
/** @see android.os.SystemClock#currentThreadTimeMillis() */
long currentThreadTimeMillis();
/** @see android.os.SystemClock#currentThreadTimeMicro() */
long currentThreadTimeMicro();
/** @see android.os.SystemClock#currentTimeMicro() */
long currentTimeMicro();
}

View File

@@ -0,0 +1,55 @@
/*
* 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.util.time;
import javax.inject.Inject;
/** Default implementation of {@link SystemClock}. */
public class SystemClockImpl implements SystemClock {
@Inject
public SystemClockImpl() {}
@Override
public long uptimeMillis() {
return android.os.SystemClock.uptimeMillis();
}
@Override
public long elapsedRealtime() {
return android.os.SystemClock.elapsedRealtime();
}
@Override
public long elapsedRealtimeNanos() {
return android.os.SystemClock.elapsedRealtimeNanos();
}
@Override
public long currentThreadTimeMillis() {
return android.os.SystemClock.currentThreadTimeMillis();
}
@Override
public long currentThreadTimeMicro() {
return android.os.SystemClock.currentThreadTimeMicro();
}
@Override
public long currentTimeMicro() {
return android.os.SystemClock.currentTimeMicro();
}
}

View File

@@ -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;

View File

@@ -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() {
if (mBubbleMetadata != null) {
mNotification.setBubbleMetadata(mBubbleMetadata);
Notification notification;
if (mNotificationBuilder != null) {
notification = mNotificationBuilder.build();
} else if (mNotification != null) {
notification = mNotification;
} else {
notification = new Notification();
}
if (mBubbleMetadata != null) {
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;

View File

@@ -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;

View File

@@ -0,0 +1,111 @@
/*
* 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.util.time;
public class FakeSystemClock implements SystemClock {
private boolean mAutoIncrement = true;
private long mUptimeMillis;
private long mElapsedRealtime;
private long mElapsedRealtimeNanos;
private long mCurrentThreadTimeMillis;
private long mCurrentThreadTimeMicro;
private long mCurrentTimeMicro;
@Override
public long uptimeMillis() {
long value = mUptimeMillis;
if (mAutoIncrement) {
mUptimeMillis++;
}
return value;
}
@Override
public long elapsedRealtime() {
long value = mElapsedRealtime;
if (mAutoIncrement) {
mElapsedRealtime++;
}
return value;
}
@Override
public long elapsedRealtimeNanos() {
long value = mElapsedRealtimeNanos;
if (mAutoIncrement) {
mElapsedRealtimeNanos++;
}
return value;
}
@Override
public long currentThreadTimeMillis() {
long value = mCurrentThreadTimeMillis;
if (mAutoIncrement) {
mCurrentThreadTimeMillis++;
}
return value;
}
@Override
public long currentThreadTimeMicro() {
long value = mCurrentThreadTimeMicro;
if (mAutoIncrement) {
mCurrentThreadTimeMicro++;
}
return value;
}
@Override
public long currentTimeMicro() {
long value = mCurrentTimeMicro;
if (mAutoIncrement) {
mCurrentTimeMicro++;
}
return value;
}
public void setUptimeMillis(long uptimeMillis) {
mUptimeMillis = uptimeMillis;
}
public void setElapsedRealtime(long elapsedRealtime) {
mElapsedRealtime = elapsedRealtime;
}
public void setElapsedRealtimeNanos(long elapsedRealtimeNanos) {
mElapsedRealtimeNanos = elapsedRealtimeNanos;
}
public void setCurrentThreadTimeMillis(long currentThreadTimeMillis) {
mCurrentThreadTimeMillis = currentThreadTimeMillis;
}
public void setCurrentThreadTimeMicro(long currentThreadTimeMicro) {
mCurrentThreadTimeMicro = currentThreadTimeMicro;
}
public void setCurrentTimeMicro(long currentTimeMicro) {
mCurrentTimeMicro = currentTimeMicro;
}
/** If true, each call to get____ will be one higher than the previous call to that method. */
public void setAutoIncrement(boolean autoIncrement) {
mAutoIncrement = autoIncrement;
}
}

View File

@@ -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;