New Pipeline: Extend the NotifPipeline to include a render stage with callbacks

* RenderStageManager implements the pipeline behavior for handoff and listeners.
* ViewRenderer interface (implemented by ShadeViewManager) defines view update API.
* Implement a StackCoordinator (and similar logic in NotificationViewHierarchyManager) which sets notification stats on the NotifStackController rather than letting this be calculated by the view.  This is an example conversion to unidirectional data flow.

Bug: 204674942
Test: atest NotifPipelineTest PreparationCoordinatorTest NodeSpecBuilderTest RenderStageManagerTest StackCoordinatorTest GroupCountCoordinatorTest

Change-Id: I7b2296504202a9cbe87d5d3ead9bd3afffa00aab
This commit is contained in:
Jeff DeCew
2021-11-10 05:00:33 +00:00
parent c273e395e2
commit 43018ba232
42 changed files with 1251 additions and 193 deletions

View File

@@ -16,6 +16,8 @@
package com.android.systemui.statusbar;
import static com.android.systemui.statusbar.notification.stack.NotificationPriorityBucketKt.BUCKET_SILENT;
import android.content.Context;
import android.content.res.Resources;
import android.os.Handler;
@@ -39,6 +41,8 @@ import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.notification.collection.legacy.LowPriorityInflationHelper;
import com.android.systemui.statusbar.notification.collection.legacy.NotificationGroupManagerLegacy;
import com.android.systemui.statusbar.notification.collection.legacy.VisualStabilityManager;
import com.android.systemui.statusbar.notification.collection.render.NotifStackController;
import com.android.systemui.statusbar.notification.collection.render.NotifStats;
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
import com.android.systemui.statusbar.notification.stack.ForegroundServiceSectionController;
import com.android.systemui.statusbar.notification.stack.NotificationListContainer;
@@ -97,6 +101,7 @@ public class NotificationViewHierarchyManager implements DynamicPrivacyControlle
private final Context mContext;
private NotificationPresenter mPresenter;
private NotifStackController mStackController;
private NotificationListContainer mListContainer;
// Used to help track down re-entrant calls to our update methods, which will cause bugs.
@@ -147,8 +152,10 @@ public class NotificationViewHierarchyManager implements DynamicPrivacyControlle
}
public void setUpWithPresenter(NotificationPresenter presenter,
NotifStackController stackController,
NotificationListContainer listContainer) {
mPresenter = presenter;
mStackController = stackController;
mListContainer = listContainer;
if (!mNotifPipelineFlags.isNewPipelineEnabled()) {
mDynamicPrivacyController.addListener(this);
@@ -328,12 +335,62 @@ public class NotificationViewHierarchyManager implements DynamicPrivacyControlle
mTmpChildOrderMap.clear();
updateRowStatesInternal();
updateNotifStats();
mListContainer.onNotificationViewUpdateFinished();
endUpdate();
}
/**
* In the spirit of unidirectional data flow, calculate this information when the notification
* views are updated, and set it once, speeding up lookups later.
* This is analogous to logic in the
* {@link com.android.systemui.statusbar.notification.collection.coordinator.StackCoordinator}
*/
private void updateNotifStats() {
boolean hasNonClearableAlertingNotifs = false;
boolean hasClearableAlertingNotifs = false;
boolean hasNonClearableSilentNotifs = false;
boolean hasClearableSilentNotifs = false;
final int childCount = mListContainer.getContainerChildCount();
int visibleTopLevelEntries = 0;
for (int i = 0; i < childCount; i++) {
View child = mListContainer.getContainerChildAt(i);
if (child == null || child.getVisibility() == View.GONE) {
continue;
}
if (!(child instanceof ExpandableNotificationRow)) {
continue;
}
final ExpandableNotificationRow row = (ExpandableNotificationRow) child;
boolean isSilent = row.getEntry().getBucket() == BUCKET_SILENT;
// NOTE: NotificationEntry.isClearable() will internally check group children to ensure
// the group itself definitively clearable.
boolean isClearable = row.getEntry().isClearable();
if (isSilent) {
if (isClearable) {
hasClearableSilentNotifs = true;
} else { // !isClearable
hasNonClearableSilentNotifs = true;
}
} else { // !isSilent
if (isClearable) {
hasClearableAlertingNotifs = true;
} else { // !isClearable
hasNonClearableAlertingNotifs = true;
}
}
}
mStackController.setNotifStats(new NotifStats(
visibleTopLevelEntries /* numActiveNotifs */,
hasNonClearableAlertingNotifs /* hasNonClearableAlertingNotifs */,
hasClearableAlertingNotifs /* hasClearableAlertingNotifs */,
hasNonClearableSilentNotifs /* hasNonClearableSilentNotifs */,
hasClearableSilentNotifs /* hasClearableSilentNotifs */
));
}
/**
* Should a notification entry from the active list be suppressed and not show?
*/
@@ -528,9 +585,7 @@ public class NotificationViewHierarchyManager implements DynamicPrivacyControlle
@Override
public void onDynamicPrivacyChanged() {
if (mNotifPipelineFlags.isNewPipelineEnabled()) {
throw new IllegalStateException("Old pipeline code running w/ new pipeline enabled");
}
mNotifPipelineFlags.assertLegacyPipelineEnabled();
if (mPerformingUpdate) {
Log.w(TAG, "onDynamicPrivacyChanged made a re-entrant call");
}

View File

@@ -24,11 +24,11 @@ import com.android.systemui.flags.Flags
import javax.inject.Inject
class NotifPipelineFlags @Inject constructor(
val context: Context,
val featureFlags: FeatureFlags
val context: Context,
val featureFlags: FeatureFlags
) {
fun checkLegacyPipelineEnabled(): Boolean {
if (!featureFlags.isEnabled(Flags.NEW_NOTIFICATION_PIPELINE_RENDERING)) {
if (!isNewPipelineEnabled()) {
return true
}
Log.d("NotifPipeline", "Old pipeline code running w/ new pipeline enabled", Exception())
@@ -36,10 +36,13 @@ class NotifPipelineFlags @Inject constructor(
return false
}
fun isNewPipelineEnabled(): Boolean = featureFlags.isEnabled(
Flags.NEW_NOTIFICATION_PIPELINE_RENDERING)
fun assertLegacyPipelineEnabled(): Nothing =
error("Old pipeline code running w/ new pipeline enabled")
fun isNewPipelineEnabled(): Boolean =
featureFlags.isEnabled(Flags.NEW_NOTIFICATION_PIPELINE_RENDERING)
fun isSmartspaceDedupingEnabled(): Boolean =
featureFlags.isEnabled(Flags.SMARTSPACE)
&& featureFlags.isEnabled(Flags.SMARTSPACE_DEDUPING)
featureFlags.isEnabled(Flags.SMARTSPACE) &&
featureFlags.isEnabled(Flags.SMARTSPACE_DEDUPING)
}

View File

@@ -19,8 +19,6 @@ package com.android.systemui.statusbar.notification.collection;
import android.annotation.NonNull;
import android.annotation.Nullable;
import com.android.systemui.statusbar.notification.collection.coordinator.PreparationCoordinator;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
@@ -61,24 +59,6 @@ public class GroupEntry extends ListEntry {
mSummary = summary;
}
/**
* @see #getUntruncatedChildCount()
*/
public void setUntruncatedChildCount(int childCount) {
mUntruncatedChildCount = childCount;
}
/**
* Get the untruncated number of children from the data model, including those that will not
* have views bound. This includes children that {@link PreparationCoordinator} will filter out
* entirely when they are beyond the last visible child.
*
* TODO: This should move to some shared class between the model and view hierarchy
*/
public int getUntruncatedChildCount() {
return mUntruncatedChildCount;
}
void clearChildren() {
mChildren.clear();
}

View File

@@ -93,7 +93,7 @@ public class NotifInflaterImpl implements NotifInflater {
public void onAsyncInflationFinished(NotificationEntry entry) {
mNotifErrorManager.clearInflationError(entry);
if (callback != null) {
callback.onInflationFinished(entry);
callback.onInflationFinished(entry, entry.getRowController());
}
}
};

View File

@@ -16,6 +16,9 @@
package com.android.systemui.statusbar.notification.collection
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.statusbar.notification.collection.listbuilder.OnAfterRenderEntryListener
import com.android.systemui.statusbar.notification.collection.listbuilder.OnAfterRenderGroupListener
import com.android.systemui.statusbar.notification.collection.listbuilder.OnAfterRenderListListener
import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeFinalizeFilterListener
import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeRenderListListener
import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeSortListener
@@ -31,6 +34,7 @@ import com.android.systemui.statusbar.notification.collection.notifcollection.In
import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener
import com.android.systemui.statusbar.notification.collection.notifcollection.NotifDismissInterceptor
import com.android.systemui.statusbar.notification.collection.notifcollection.NotifLifetimeExtender
import com.android.systemui.statusbar.notification.collection.render.RenderStageManager
import javax.inject.Inject
/**
@@ -65,11 +69,15 @@ import javax.inject.Inject
* 9. Finalize filters are fired on each notification ([.addFinalizeFilter])
* 10. OnBeforeRenderListListeners are fired ([.addOnBeforeRenderListListener])
* 11. The list is handed off to the view layer to be rendered
* 12. OnAfterRenderListListeners are fired ([.addOnAfterRenderListListener])
* 13. OnAfterRenderGroupListeners are fired ([.addOnAfterRenderGroupListener])
* 13. OnAfterRenderEntryListeners are fired ([.addOnAfterRenderEntryListener])
*/
@SysUISingleton
class NotifPipeline @Inject constructor(
private val mNotifCollection: NotifCollection,
private val mShadeListBuilder: ShadeListBuilder
private val mShadeListBuilder: ShadeListBuilder,
private val mRenderStageManager: RenderStageManager
) : CommonNotifCollection {
/**
* Returns the list of all known notifications, i.e. the notifications that are currently posted
@@ -205,6 +213,28 @@ class NotifPipeline @Inject constructor(
mShadeListBuilder.addPreRenderInvalidator(invalidator)
}
/**
* Called at the end of the pipeline after the notif list has been handed off to the view layer.
*/
fun addOnAfterRenderListListener(listener: OnAfterRenderListListener) {
mRenderStageManager.addOnAfterRenderListListener(listener)
}
/**
* Called at the end of the pipeline after a group has been handed off to the view layer.
*/
fun addOnAfterRenderGroupListener(listener: OnAfterRenderGroupListener) {
mRenderStageManager.addOnAfterRenderGroupListener(listener)
}
/**
* Called at the end of the pipeline after an entry has been handed off to the view layer.
* This will be called for every top level entry, every group summary, and every group child.
*/
fun addOnAfterRenderEntryListener(listener: OnAfterRenderEntryListener) {
mRenderStageManager.addOnAfterRenderEntryListener(listener)
}
/**
* Get an object which can be used to update a notification (internally to the pipeline)
* in response to a user action.

View File

@@ -0,0 +1,35 @@
package com.android.systemui.statusbar.notification.collection.coordinator
import android.util.ArrayMap
import com.android.systemui.statusbar.notification.collection.GroupEntry
import com.android.systemui.statusbar.notification.collection.ListEntry
import com.android.systemui.statusbar.notification.collection.NotifPipeline
import com.android.systemui.statusbar.notification.collection.coordinator.dagger.CoordinatorScope
import com.android.systemui.statusbar.notification.collection.render.NotifGroupController
import javax.inject.Inject
/** A small coordinator which calculates, stores, and applies the untruncated child count. */
@CoordinatorScope
class GroupCountCoordinator @Inject constructor() : Coordinator {
private val untruncatedChildCounts = ArrayMap<GroupEntry, Int>()
override fun attach(pipeline: NotifPipeline) {
pipeline.addOnBeforeFinalizeFilterListener(::onBeforeFinalizeFilter)
pipeline.addOnAfterRenderGroupListener(::onAfterRenderGroup)
}
private fun onBeforeFinalizeFilter(entries: List<ListEntry>) {
// save untruncated child counts to our internal map
untruncatedChildCounts.clear()
entries.asSequence().filterIsInstance<GroupEntry>().forEach { groupEntry ->
untruncatedChildCounts[groupEntry] = groupEntry.children.size
}
}
private fun onAfterRenderGroup(group: GroupEntry, controller: NotifGroupController) {
// find the untruncated child count for a group and apply it to the controller
val count = untruncatedChildCounts[group]
checkNotNull(count) { "No untruncated child count for group: ${group.key}" }
controller.setUntruncatedChildCount(count)
}
}

View File

@@ -46,8 +46,10 @@ class NotifCoordinatorsImpl @Inject constructor(
gutsCoordinator: GutsCoordinator,
conversationCoordinator: ConversationCoordinator,
preparationCoordinator: PreparationCoordinator,
groupCountCoordinator: GroupCountCoordinator,
mediaCoordinator: MediaCoordinator,
remoteInputCoordinator: RemoteInputCoordinator,
stackCoordinator: StackCoordinator,
shadeEventCoordinator: ShadeEventCoordinator,
smartspaceDedupingCoordinator: SmartspaceDedupingCoordinator,
viewConfigCoordinator: ViewConfigCoordinator,
@@ -72,8 +74,10 @@ class NotifCoordinatorsImpl @Inject constructor(
mCoordinators.add(deviceProvisionedCoordinator)
mCoordinators.add(bubbleCoordinator)
mCoordinators.add(conversationCoordinator)
mCoordinators.add(groupCountCoordinator)
mCoordinators.add(mediaCoordinator)
mCoordinators.add(remoteInputCoordinator)
mCoordinators.add(stackCoordinator)
mCoordinators.add(shadeEventCoordinator)
mCoordinators.add(viewConfigCoordinator)
mCoordinators.add(visualStabilityCoordinator)
@@ -89,7 +93,7 @@ class NotifCoordinatorsImpl @Inject constructor(
}
// Manually add Ordered Sections
// HeadsUp > FGS > People > Alerting > Silent > Unknown/Default
// HeadsUp > FGS > People > Alerting > Silent > Minimized > Unknown/Default
if (notifPipelineFlags.isNewPipelineEnabled()) {
mOrderedSections.add(headsUpCoordinator.sectioner) // HeadsUp
}

View File

@@ -31,19 +31,19 @@ import androidx.annotation.Nullable;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.statusbar.IStatusBarService;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.statusbar.notification.collection.GroupEntry;
import com.android.systemui.statusbar.notification.collection.ListEntry;
import com.android.systemui.statusbar.notification.collection.NotifPipeline;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.notification.collection.ShadeListBuilder;
import com.android.systemui.statusbar.notification.collection.coordinator.dagger.CoordinatorScope;
import com.android.systemui.statusbar.notification.collection.inflation.NotifInflater;
import com.android.systemui.statusbar.notification.collection.inflation.NotifUiAdjustment;
import com.android.systemui.statusbar.notification.collection.inflation.NotifUiAdjustmentProvider;
import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeFinalizeFilterListener;
import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifFilter;
import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener;
import com.android.systemui.statusbar.notification.collection.render.NotifViewBarn;
import com.android.systemui.statusbar.notification.collection.render.NotifViewController;
import com.android.systemui.statusbar.notification.row.NotifInflationErrorManager;
import com.android.systemui.statusbar.notification.row.NotifInflationErrorManager.NotifInflationErrorListener;
@@ -61,8 +61,7 @@ import javax.inject.Inject;
* If a notification was uninflated, this coordinator will filter the notification out from the
* {@link ShadeListBuilder} until it is inflated.
*/
// TODO(b/204468557): Move to @CoordinatorScope
@SysUISingleton
@CoordinatorScope
public class PreparationCoordinator implements Coordinator {
private static final String TAG = "PreparationCoordinator";
@@ -145,7 +144,7 @@ public class PreparationCoordinator implements Coordinator {
pipeline.addCollectionListener(mNotifCollectionListener);
// Inflate after grouping/sorting since that affects what views to inflate.
pipeline.addOnBeforeFinalizeFilterListener(mOnBeforeFinalizeFilterListener);
pipeline.addOnBeforeFinalizeFilterListener(this::inflateAllRequiredViews);
pipeline.addFinalizeFilter(mNotifInflationErrorFilter);
pipeline.addFinalizeFilter(mNotifInflatingFilter);
}
@@ -182,9 +181,6 @@ public class PreparationCoordinator implements Coordinator {
}
};
private final OnBeforeFinalizeFilterListener mOnBeforeFinalizeFilterListener =
entries -> inflateAllRequiredViews(entries);
private final NotifFilter mNotifInflationErrorFilter = new NotifFilter(
TAG + "InflationError") {
/**
@@ -256,7 +252,6 @@ public class PreparationCoordinator implements Coordinator {
ListEntry entry = entries.get(i);
if (entry instanceof GroupEntry) {
GroupEntry groupEntry = (GroupEntry) entry;
groupEntry.setUntruncatedChildCount(groupEntry.getChildren().size());
inflateRequiredGroupViews(groupEntry);
} else {
NotificationEntry notifEntry = (NotificationEntry) entry;
@@ -363,10 +358,10 @@ public class PreparationCoordinator implements Coordinator {
mInflatingNotifs.remove(entry);
}
private void onInflationFinished(NotificationEntry entry) {
private void onInflationFinished(NotificationEntry entry, NotifViewController controller) {
mLogger.logNotifInflated(entry.getKey());
mInflatingNotifs.remove(entry);
mViewBarn.registerViewForEntry(entry, entry.getRowController());
mViewBarn.registerViewForEntry(entry, controller);
mInflationStates.put(entry, STATE_INFLATED);
mNotifInflatingFilter.invalidateList();
}

View File

@@ -22,7 +22,6 @@ import android.service.notification.NotificationListenerService.REASON_CLICK
import android.util.Log
import androidx.annotation.VisibleForTesting
import com.android.systemui.Dumpable
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.dump.DumpManager
import com.android.systemui.statusbar.NotificationRemoteInputManager
@@ -32,6 +31,7 @@ import com.android.systemui.statusbar.RemoteInputNotificationRebuilder
import com.android.systemui.statusbar.SmartReplyController
import com.android.systemui.statusbar.notification.collection.NotifPipeline
import com.android.systemui.statusbar.notification.collection.NotificationEntry
import com.android.systemui.statusbar.notification.collection.coordinator.dagger.CoordinatorScope
import com.android.systemui.statusbar.notification.collection.notifcollection.SelfTrackingLifetimeExtender
import com.android.systemui.statusbar.notification.collection.notifcollection.InternalNotifUpdater
import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener
@@ -61,7 +61,7 @@ private const val REMOTE_INPUT_EXTENDER_RELEASE_DELAY: Long = 200
/** Whether this class should print spammy debug logs */
private val DEBUG: Boolean by lazy { Log.isLoggable(TAG, Log.DEBUG) }
@SysUISingleton
@CoordinatorScope
class RemoteInputCoordinator @Inject constructor(
dumpManager: DumpManager,
private val mRebuilder: RemoteInputNotificationRebuilder,

View File

@@ -0,0 +1,72 @@
/*
* Copyright (C) 2021 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.coordinator
import com.android.systemui.statusbar.notification.collection.ListEntry
import com.android.systemui.statusbar.notification.collection.NotifPipeline
import com.android.systemui.statusbar.notification.collection.coordinator.dagger.CoordinatorScope
import com.android.systemui.statusbar.notification.collection.render.NotifStats
import com.android.systemui.statusbar.notification.collection.render.NotifStackController
import com.android.systemui.statusbar.notification.stack.BUCKET_SILENT
import com.android.systemui.statusbar.phone.NotificationIconAreaController
import javax.inject.Inject
/**
* A small coordinator which updates the notif stack (the view layer which holds notifications)
* with high-level data after the stack is populated with the final entries.
*/
@CoordinatorScope
class StackCoordinator @Inject internal constructor(
private val notificationIconAreaController: NotificationIconAreaController
) : Coordinator {
override fun attach(pipeline: NotifPipeline) {
pipeline.addOnAfterRenderListListener(::onAfterRenderList)
}
fun onAfterRenderList(entries: List<ListEntry>, controller: NotifStackController) {
controller.setNotifStats(calculateNotifStats(entries))
notificationIconAreaController.updateNotificationIcons(entries)
}
private fun calculateNotifStats(entries: List<ListEntry>): NotifStats {
var hasNonClearableAlertingNotifs = false
var hasClearableAlertingNotifs = false
var hasNonClearableSilentNotifs = false
var hasClearableSilentNotifs = false
entries.forEach {
val isSilent = it.section!!.bucket == BUCKET_SILENT
// NOTE: NotificationEntry.isClearable will internally check group children to ensure
// the group itself definitively clearable.
val isClearable = it.representativeEntry!!.isClearable
when {
isSilent && isClearable -> hasClearableSilentNotifs = true
isSilent && !isClearable -> hasNonClearableSilentNotifs = true
!isSilent && isClearable -> hasClearableAlertingNotifs = true
!isSilent && !isClearable -> hasNonClearableAlertingNotifs = true
}
}
val stats = NotifStats(
numActiveNotifs = entries.size,
hasNonClearableAlertingNotifs = hasNonClearableAlertingNotifs,
hasClearableAlertingNotifs = hasClearableAlertingNotifs,
hasNonClearableSilentNotifs = hasNonClearableSilentNotifs,
hasClearableSilentNotifs = hasClearableSilentNotifs
)
return stats
}
}

View File

@@ -17,6 +17,7 @@
package com.android.systemui.statusbar.notification.collection.inflation
import com.android.systemui.statusbar.notification.collection.NotificationEntry
import com.android.systemui.statusbar.notification.collection.render.NotifViewController
/**
* Used by the [PreparationCoordinator]. When notifications are added or updated, the
@@ -49,7 +50,7 @@ interface NotifInflater {
* Callback once all the views are inflated and bound for a given NotificationEntry.
*/
interface InflationCallback {
fun onInflationFinished(entry: NotificationEntry)
fun onInflationFinished(entry: NotificationEntry, controller: NotifViewController)
}
/**

View File

@@ -30,6 +30,8 @@ import com.android.systemui.statusbar.notification.collection.ShadeListBuilder;
import com.android.systemui.statusbar.notification.collection.coalescer.GroupCoalescer;
import com.android.systemui.statusbar.notification.collection.coordinator.NotifCoordinators;
import com.android.systemui.statusbar.notification.collection.inflation.NotificationRowBinderImpl;
import com.android.systemui.statusbar.notification.collection.render.NotifStackController;
import com.android.systemui.statusbar.notification.collection.render.RenderStageManager;
import com.android.systemui.statusbar.notification.collection.render.ShadeViewManagerFactory;
import com.android.systemui.statusbar.notification.stack.NotificationListContainer;
@@ -47,6 +49,7 @@ public class NotifPipelineInitializer implements Dumpable {
private final GroupCoalescer mGroupCoalescer;
private final NotifCollection mNotifCollection;
private final ShadeListBuilder mListBuilder;
private final RenderStageManager mRenderStageManager;
private final NotifCoordinators mNotifPluggableCoordinators;
private final NotifInflaterImpl mNotifInflater;
private final DumpManager mDumpManager;
@@ -60,15 +63,18 @@ public class NotifPipelineInitializer implements Dumpable {
GroupCoalescer groupCoalescer,
NotifCollection notifCollection,
ShadeListBuilder listBuilder,
RenderStageManager renderStageManager,
NotifCoordinators notifCoordinators,
NotifInflaterImpl notifInflater,
DumpManager dumpManager,
ShadeViewManagerFactory shadeViewManagerFactory,
NotifPipelineFlags notifPipelineFlags) {
NotifPipelineFlags notifPipelineFlags
) {
mPipelineWrapper = pipelineWrapper;
mGroupCoalescer = groupCoalescer;
mNotifCollection = notifCollection;
mListBuilder = listBuilder;
mRenderStageManager = renderStageManager;
mNotifPluggableCoordinators = notifCoordinators;
mDumpManager = dumpManager;
mNotifInflater = notifInflater;
@@ -80,7 +86,8 @@ public class NotifPipelineInitializer implements Dumpable {
public void initialize(
NotificationListener notificationService,
NotificationRowBinderImpl rowBinder,
NotificationListContainer listContainer) {
NotificationListContainer listContainer,
NotifStackController stackController) {
mDumpManager.registerDumpable("NotifPipeline", this);
@@ -94,8 +101,11 @@ public class NotifPipelineInitializer implements Dumpable {
// Wire up pipeline
if (mNotifPipelineFlags.isNewPipelineEnabled()) {
mShadeViewManagerFactory.create(listContainer).attach(mListBuilder);
mShadeViewManagerFactory
.create(listContainer, stackController)
.attach(mRenderStageManager);
}
mRenderStageManager.attach(mListBuilder);
mListBuilder.attach(mNotifCollection);
mNotifCollection.attach(mGroupCoalescer);
mGroupCoalescer.attach(notificationService);

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;
import androidx.annotation.NonNull;
import com.android.systemui.statusbar.notification.collection.NotifPipeline;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.notification.collection.render.NotifRowController;
/** See {@link NotifPipeline#addOnAfterRenderEntryListener(OnAfterRenderEntryListener)} */
public interface OnAfterRenderEntryListener {
/**
* Called at the end of the pipeline after an entry has been handed off to the view layer.
* This will be called for every top level entry, every group summary, and every group child.
*
* @param entry the entry to read from.
* @param controller the object to which data can be pushed.
*/
void onAfterRenderEntry(
@NonNull NotificationEntry entry,
@NonNull NotifRowController controller);
}

View File

@@ -0,0 +1,36 @@
/*
* 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 androidx.annotation.NonNull;
import com.android.systemui.statusbar.notification.collection.GroupEntry;
import com.android.systemui.statusbar.notification.collection.NotifPipeline;
import com.android.systemui.statusbar.notification.collection.render.NotifGroupController;
/** See {@link NotifPipeline#addOnAfterRenderGroupListener(OnAfterRenderGroupListener)} */
public interface OnAfterRenderGroupListener {
/**
* Called at the end of the pipeline after a group has been handed off to the view layer.
*
* @param groupEntry the entry for the group itself.
* @param controller the object to which data can be pushed.
*/
void onAfterRenderGroup(
@NonNull GroupEntry groupEntry,
@NonNull NotifGroupController controller);
}

View File

@@ -0,0 +1,39 @@
/*
* 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 androidx.annotation.NonNull;
import com.android.systemui.statusbar.notification.collection.ListEntry;
import com.android.systemui.statusbar.notification.collection.NotifPipeline;
import com.android.systemui.statusbar.notification.collection.render.NotifStackController;
import java.util.List;
/** See {@link NotifPipeline#addOnAfterRenderListListener(OnAfterRenderListListener)} */
public interface OnAfterRenderListListener {
/**
* Called at the end of the pipeline after the notif list 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.
* @param controller An object for setting state on the shade.
*/
void onAfterRenderList(
@NonNull List<ListEntry> entries,
@NonNull NotifStackController controller);
}

View File

@@ -64,12 +64,13 @@ class NodeSpecBuilder(
root.children.add(buildNotifNode(root, entry))
}
return root
return@traceSection root
}
private fun buildNotifNode(parent: NodeSpec, entry: ListEntry): NodeSpec = when (entry) {
is NotificationEntry -> NodeSpecImpl(parent, viewBarn.requireView(entry))
is GroupEntry -> NodeSpecImpl(parent, viewBarn.requireView(checkNotNull(entry.summary)))
is NotificationEntry -> NodeSpecImpl(parent, viewBarn.requireNodeController(entry))
is GroupEntry ->
NodeSpecImpl(parent, viewBarn.requireNodeController(checkNotNull(entry.summary)))
.apply { entry.children.forEach { children.add(buildNotifNode(this, it)) } }
else -> throw RuntimeException("Unexpected entry: $entry")
}

View File

@@ -0,0 +1,23 @@
/*
* Copyright (C) 2021 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.render
/** A view controller for a notification group row */
interface NotifGroupController {
/** Set the number of children that this group would have if not for the 8-child max */
fun setUntruncatedChildCount(untruncatedChildCount: Int)
}

View File

@@ -0,0 +1,20 @@
/*
* Copyright (C) 2021 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.render
/** A view controller for a notification row */
interface NotifRowController

View File

@@ -0,0 +1,47 @@
/*
* Copyright (C) 2021 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.render
/** An interface by which the pipeline can make updates to the notification root view. */
interface NotifStackController {
/** Provides stats about the list of notifications attached to the shade */
fun setNotifStats(stats: NotifStats)
}
/** Data provided to the NotificationRootController whenever the pipeline runs */
data class NotifStats(
val numActiveNotifs: Int,
val hasNonClearableAlertingNotifs: Boolean,
val hasClearableAlertingNotifs: Boolean,
val hasNonClearableSilentNotifs: Boolean,
val hasClearableSilentNotifs: Boolean
) {
companion object {
@JvmStatic
val empty = NotifStats(0, false, false, false, false)
}
}
/**
* An implementation of NotifStackController which provides default, no-op implementations of each
* method. This is used by ArcSystemUI so that that implementation can opt-in to overriding
* methods, rather than forcing us to add no-op implementations in their implementation every time
* a method is added.
*/
open class DefaultNotifStackController : NotifStackController {
override fun setNotifStats(stats: NotifStats) {}
}

View File

@@ -19,6 +19,7 @@ package com.android.systemui.statusbar.notification.collection.render
import android.view.textclassifier.Log
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.statusbar.notification.collection.ListEntry
import com.android.systemui.statusbar.notification.collection.NotificationEntry
import javax.inject.Inject
/**
@@ -26,30 +27,39 @@ import javax.inject.Inject
*/
@SysUISingleton
class NotifViewBarn @Inject constructor() {
private val rowMap = mutableMapOf<String, NodeController>()
private val rowMap = mutableMapOf<String, NotifViewController>()
fun requireView(forEntry: ListEntry): NodeController {
fun requireNodeController(entry: ListEntry): NodeController {
if (DEBUG) {
Log.d(TAG, "requireView: $forEntry.key")
Log.d(TAG, "requireNodeController: ${entry.key}")
}
val li = rowMap[forEntry.key]
if (li == null) {
throw IllegalStateException("No view has been registered for entry: $forEntry")
}
return li
return rowMap[entry.key] ?: error("No view has been registered for entry: ${entry.key}")
}
fun registerViewForEntry(entry: ListEntry, controller: NodeController) {
fun requireGroupController(entry: NotificationEntry): NotifGroupController {
if (DEBUG) {
Log.d(TAG, "registerViewForEntry: $entry.key")
Log.d(TAG, "requireGroupController: ${entry.key}")
}
return rowMap[entry.key] ?: error("No view has been registered for entry: ${entry.key}")
}
fun requireRowController(entry: NotificationEntry): NotifRowController {
if (DEBUG) {
Log.d(TAG, "requireRowController: ${entry.key}")
}
return rowMap[entry.key] ?: error("No view has been registered for entry: ${entry.key}")
}
fun registerViewForEntry(entry: ListEntry, controller: NotifViewController) {
if (DEBUG) {
Log.d(TAG, "registerViewForEntry: ${entry.key}")
}
rowMap[entry.key] = controller
}
fun removeViewForEntry(entry: ListEntry) {
if (DEBUG) {
Log.d(TAG, "removeViewForEntry: $entry.key")
Log.d(TAG, "removeViewForEntry: ${entry.key}")
}
rowMap.remove(entry.key)
}

View File

@@ -0,0 +1,19 @@
/*
* Copyright (C) 2021 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.render
interface NotifViewController : NotifGroupController, NotifRowController, NodeController

View File

@@ -0,0 +1,69 @@
/*
* Copyright (C) 2021 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.render
import com.android.systemui.statusbar.notification.collection.GroupEntry
import com.android.systemui.statusbar.notification.collection.ListEntry
import com.android.systemui.statusbar.notification.collection.NotificationEntry
/**
* This interface and the interfaces it returns define the main API surface that must be
* implemented by the view implementation. The term "render" is used to indicate a handoff
* to the view system, whether that be to attach views to the hierarchy or to update independent
* view models, data stores, or adapters.
*/
interface NotifViewRenderer {
/**
* Hand off the list of notifications to the view implementation. This may attach views to the
* hierarchy or simply update an independent datastore, but once called, the implementer myst
* also ensure that future calls to [getStackController], [getGroupController], and
* [getRowController] will provide valid results.
*/
fun onRenderList(notifList: List<ListEntry>)
/**
* Provides an interface for the pipeline to update the overall shade.
* This will be called at most once for each time [onRenderList] is called.
*/
fun getStackController(): NotifStackController
/**
* Provides an interface for the pipeline to update individual groups.
* This will be called at most once for each group in the most recent call to [onRenderList].
*/
fun getGroupController(group: GroupEntry): NotifGroupController
/**
* Provides an interface for the pipeline to update individual entries.
* This will be called at most once for each entry in the most recent call to [onRenderList].
* This includes top level entries, group summaries, and group children.
*/
fun getRowController(entry: NotificationEntry): NotifRowController
/**
* Invoked after the render stage manager has finished dispatching to all of the listeners.
*
* This is an opportunity for the view system to do any cleanup or trigger any finalization
* logic now that all data from the pipeline is known to have been set for this execution.
*
* When this is called, the view system can expect that no more calls will be made to the
* getters on this interface until after the next call to [onRenderList]. Additionally, there
* should be no further calls made on the objects previously returned by those getters.
*/
fun onDispatchComplete() {}
}

View File

@@ -0,0 +1,25 @@
/*
* Copyright (C) 2021 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.render
import com.android.systemui.statusbar.notification.collection.GroupEntry
/**
* Extension used during the render stage which assumes the summary exists, and throws a more
* helpful error if not.
*/
inline val GroupEntry.requireSummary get() = checkNotNull(summary) { "No Summary: $this" }

View File

@@ -0,0 +1,142 @@
/*
* Copyright (C) 2021 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.render
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.statusbar.notification.collection.GroupEntry
import com.android.systemui.statusbar.notification.collection.ListEntry
import com.android.systemui.statusbar.notification.collection.NotificationEntry
import com.android.systemui.statusbar.notification.collection.ShadeListBuilder
import com.android.systemui.statusbar.notification.collection.listbuilder.OnAfterRenderEntryListener
import com.android.systemui.statusbar.notification.collection.listbuilder.OnAfterRenderGroupListener
import com.android.systemui.statusbar.notification.collection.listbuilder.OnAfterRenderListListener
import com.android.systemui.util.traceSection
import javax.inject.Inject
/**
* The class which is part of the pipeline which guarantees a consistent that coordinators get a
* consistent interface to the view system regardless of the [NotifViewRenderer] implementation
* provided to [setViewRenderer].
*/
@SysUISingleton
class RenderStageManager @Inject constructor() {
private val onAfterRenderListListeners = mutableListOf<OnAfterRenderListListener>()
private val onAfterRenderGroupListeners = mutableListOf<OnAfterRenderGroupListener>()
private val onAfterRenderEntryListeners = mutableListOf<OnAfterRenderEntryListener>()
private var viewRenderer: NotifViewRenderer? = null
/** Attach this stage to the rest of the pipeline */
fun attach(listBuilder: ShadeListBuilder) {
listBuilder.setOnRenderListListener(::onRenderList)
}
private fun onRenderList(notifList: List<ListEntry>) {
traceSection("RenderStageManager.onRenderList") {
val viewRenderer = viewRenderer ?: return
viewRenderer.onRenderList(notifList)
dispatchOnAfterRenderList(viewRenderer, notifList)
dispatchOnAfterRenderGroups(viewRenderer, notifList)
dispatchOnAfterRenderEntries(viewRenderer, notifList)
viewRenderer.onDispatchComplete()
}
}
/** Provides this class with the view rendering implementation. */
fun setViewRenderer(renderer: NotifViewRenderer) {
viewRenderer = renderer
}
/** Adds a listener that will get a single callback after rendering the list. */
fun addOnAfterRenderListListener(listener: OnAfterRenderListListener) {
onAfterRenderListListeners.add(listener)
}
/** Adds a listener that will get a callback for each group rendered. */
fun addOnAfterRenderGroupListener(listener: OnAfterRenderGroupListener) {
onAfterRenderGroupListeners.add(listener)
}
/** Adds a listener that will get a callback for each entry rendered. */
fun addOnAfterRenderEntryListener(listener: OnAfterRenderEntryListener) {
onAfterRenderEntryListeners.add(listener)
}
private fun dispatchOnAfterRenderList(
viewRenderer: NotifViewRenderer,
entries: List<ListEntry>
) {
traceSection("RenderStageManager.dispatchOnAfterRenderList") {
val stackController = viewRenderer.getStackController()
onAfterRenderListListeners.forEach { listener ->
listener.onAfterRenderList(entries, stackController)
}
}
}
private fun dispatchOnAfterRenderGroups(
viewRenderer: NotifViewRenderer,
entries: List<ListEntry>
) {
traceSection("RenderStageManager.dispatchOnAfterRenderGroups") {
if (onAfterRenderGroupListeners.isEmpty()) {
return
}
entries.asSequence().filterIsInstance<GroupEntry>().forEach { group ->
val controller = viewRenderer.getGroupController(group)
onAfterRenderGroupListeners.forEach { listener ->
listener.onAfterRenderGroup(group, controller)
}
}
}
}
private fun dispatchOnAfterRenderEntries(
viewRenderer: NotifViewRenderer,
entries: List<ListEntry>
) {
traceSection("RenderStageManager.dispatchOnAfterRenderEntries") {
if (onAfterRenderEntryListeners.isEmpty()) {
return
}
entries.forEachNotificationEntry { entry ->
val controller = viewRenderer.getRowController(entry)
onAfterRenderEntryListeners.forEach { listener ->
listener.onAfterRenderEntry(entry, controller)
}
}
}
}
/**
* Performs a forward, depth-first traversal of the list where the group's summary
* immediately precedes the group's children.
*/
private inline fun List<ListEntry>.forEachNotificationEntry(
action: (NotificationEntry) -> Unit
) {
forEach { entry ->
when (entry) {
is NotificationEntry -> action(entry)
is GroupEntry -> {
action(entry.requireSummary)
entry.children.forEach(action)
}
else -> error("Unhandled entry: $entry")
}
}
}
}

View File

@@ -20,10 +20,8 @@ import android.content.Context
import android.view.View
import com.android.systemui.statusbar.notification.collection.GroupEntry
import com.android.systemui.statusbar.notification.collection.ListEntry
import com.android.systemui.statusbar.notification.collection.ShadeListBuilder
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
import com.android.systemui.statusbar.notification.collection.NotificationEntry
import com.android.systemui.statusbar.notification.stack.NotificationListContainer
import com.android.systemui.statusbar.phone.NotificationIconAreaController
import com.android.systemui.util.traceSection
import javax.inject.Inject
@@ -34,9 +32,9 @@ import javax.inject.Inject
class ShadeViewManager constructor(
context: Context,
listContainer: NotificationListContainer,
private val stackController: NotifStackController,
logger: ShadeViewDifferLogger,
private val viewBarn: NotifViewBarn,
private val notificationIconAreaController: NotificationIconAreaController
private val viewBarn: NotifViewBarn
) {
// We pass a shim view here because the listContainer may not actually have a view associated
// with it and the differ never actually cares about the root node's view.
@@ -44,39 +42,39 @@ class ShadeViewManager constructor(
private val specBuilder = NodeSpecBuilder(viewBarn)
private val viewDiffer = ShadeViewDiffer(rootController, logger)
fun attach(listBuilder: ShadeListBuilder) =
listBuilder.setOnRenderListListener(::onNewNotifTree)
private fun onNewNotifTree(notifList: List<ListEntry>) {
traceSection("ShadeViewManager.onNewNotifTree") {
viewDiffer.applySpec(specBuilder.buildNodeSpec(rootController, notifList))
updateGroupCounts(notifList)
notificationIconAreaController.updateNotificationIcons(notifList)
}
/** Method for attaching this manager to the pipeline. */
fun attach(renderStageManager: RenderStageManager) {
renderStageManager.setViewRenderer(viewRenderer)
}
private fun updateGroupCounts(notifList: List<ListEntry>) {
traceSection("ShadeViewManager.updateGroupCounts") {
notifList.asSequence().filterIsInstance<GroupEntry>().forEach { groupEntry ->
val controller = viewBarn.requireView(checkNotNull(groupEntry.summary))
val row = controller.view as ExpandableNotificationRow
row.setUntruncatedChildCount(groupEntry.untruncatedChildCount)
private val viewRenderer = object : NotifViewRenderer {
override fun onRenderList(notifList: List<ListEntry>) {
traceSection("ShadeViewManager.onRenderList") {
viewDiffer.applySpec(specBuilder.buildNodeSpec(rootController, notifList))
}
}
override fun getStackController(): NotifStackController = stackController
override fun getGroupController(group: GroupEntry): NotifGroupController =
viewBarn.requireGroupController(group.requireSummary)
override fun getRowController(entry: NotificationEntry): NotifRowController =
viewBarn.requireRowController(entry)
}
}
class ShadeViewManagerFactory @Inject constructor(
private val context: Context,
private val logger: ShadeViewDifferLogger,
private val viewBarn: NotifViewBarn,
private val notificationIconAreaController: NotificationIconAreaController
private val viewBarn: NotifViewBarn
) {
fun create(listContainer: NotificationListContainer) =
ShadeViewManager(
context,
listContainer,
logger,
viewBarn,
notificationIconAreaController)
fun create(listContainer: NotificationListContainer, stackController: NotifStackController) =
ShadeViewManager(
context,
listContainer,
stackController,
logger,
viewBarn)
}

View File

@@ -21,6 +21,7 @@ import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper.Snoo
import com.android.systemui.statusbar.NotificationPresenter
import com.android.systemui.statusbar.notification.NotificationActivityStarter
import com.android.systemui.statusbar.notification.collection.inflation.NotificationRowBinderImpl
import com.android.systemui.statusbar.notification.collection.render.NotifStackController
import com.android.systemui.statusbar.notification.stack.NotificationListContainer
import com.android.systemui.statusbar.phone.StatusBar
import com.android.wm.shell.bubbles.Bubbles
@@ -40,6 +41,7 @@ interface NotificationsController {
bubblesOptional: Optional<Bubbles>,
presenter: NotificationPresenter,
listContainer: NotificationListContainer,
stackController: NotifStackController,
notificationActivityStarter: NotificationActivityStarter,
bindRowCallback: NotificationRowBinderImpl.BindRowCallback
)

View File

@@ -34,6 +34,7 @@ import com.android.systemui.statusbar.notification.collection.TargetSdkResolver
import com.android.systemui.statusbar.notification.collection.inflation.NotificationRowBinderImpl
import com.android.systemui.statusbar.notification.collection.init.NotifPipelineInitializer
import com.android.systemui.statusbar.notification.collection.legacy.NotificationGroupManagerLegacy
import com.android.systemui.statusbar.notification.collection.render.NotifStackController
import com.android.systemui.statusbar.notification.interruption.HeadsUpController
import com.android.systemui.statusbar.notification.interruption.HeadsUpViewBinder
import com.android.systemui.statusbar.notification.row.NotifBindPipelineInitializer
@@ -47,7 +48,7 @@ import com.android.wm.shell.bubbles.Bubbles
import dagger.Lazy
import java.io.FileDescriptor
import java.io.PrintWriter
import java.util.*
import java.util.Optional
import javax.inject.Inject
/**
@@ -85,6 +86,7 @@ class NotificationsControllerImpl @Inject constructor(
bubblesOptional: Optional<Bubbles>,
presenter: NotificationPresenter,
listContainer: NotificationListContainer,
stackController: NotifStackController,
notificationActivityStarter: NotificationActivityStarter,
bindRowCallback: NotificationRowBinderImpl.BindRowCallback
) {
@@ -112,7 +114,8 @@ class NotificationsControllerImpl @Inject constructor(
newNotifPipeline.get().initialize(
notificationListener,
notificationRowBinder,
listContainer)
listContainer,
stackController)
}
if (notifPipelineFlags.isNewPipelineEnabled()) {

View File

@@ -22,6 +22,7 @@ import com.android.systemui.statusbar.NotificationListener
import com.android.systemui.statusbar.NotificationPresenter
import com.android.systemui.statusbar.notification.NotificationActivityStarter
import com.android.systemui.statusbar.notification.collection.inflation.NotificationRowBinderImpl
import com.android.systemui.statusbar.notification.collection.render.NotifStackController
import com.android.systemui.statusbar.notification.stack.NotificationListContainer
import com.android.systemui.statusbar.phone.StatusBar
import com.android.wm.shell.bubbles.Bubbles
@@ -42,6 +43,7 @@ class NotificationsControllerStub @Inject constructor(
bubblesOptional: Optional<Bubbles>,
presenter: NotificationPresenter,
listContainer: NotificationListContainer,
stackController: NotifStackController,
notificationActivityStarter: NotificationActivityStarter,
bindRowCallback: NotificationRowBinderImpl.BindRowCallback
) {

View File

@@ -20,6 +20,7 @@ import static com.android.systemui.Dependency.ALLOW_NOTIFICATION_LONG_PRESS_NAME
import static com.android.systemui.statusbar.NotificationRemoteInputManager.ENABLE_REMOTE_INPUT;
import static com.android.systemui.statusbar.StatusBarState.KEYGUARD;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
@@ -36,6 +37,7 @@ import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.notification.collection.render.GroupExpansionManager;
import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager;
import com.android.systemui.statusbar.notification.collection.render.NodeController;
import com.android.systemui.statusbar.notification.collection.render.NotifViewController;
import com.android.systemui.statusbar.notification.logging.NotificationLogger;
import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier;
import com.android.systemui.statusbar.notification.row.dagger.AppName;
@@ -58,7 +60,8 @@ import javax.inject.Named;
* Controller for {@link ExpandableNotificationRow}.
*/
@NotificationRowScope
public class ExpandableNotificationRowController implements NodeController {
public class ExpandableNotificationRowController implements NotifViewController {
private static final String TAG = "NotifRowController";
private final ExpandableNotificationRow mView;
private final NotificationListContainer mListContainer;
private final RemoteInputViewSubcomponent.Factory mRemoteInputViewSubcomponentFactory;
@@ -267,4 +270,13 @@ public class ExpandableNotificationRowController implements NodeController {
final List<ExpandableNotificationRow> mChildren = mView.getAttachedChildren();
return mChildren != null ? mChildren.size() : 0;
}
@Override
public void setUntruncatedChildCount(int childCount) {
if (mView.isSummaryWithChildren()) {
mView.setUntruncatedChildCount(childCount);
} else {
Log.w(TAG, "Called setUntruncatedChildCount(" + childCount + ") on a leaf row");
}
}
}

View File

@@ -678,7 +678,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable
// TODO: move this logic to controller, which will invoke updateFooterView directly
boolean showDismissView = mClearAllEnabled &&
mController.hasActiveClearableNotifications(ROWS_ALL);
boolean showFooterView = (showDismissView || getVisibleNotificationCount() > 0)
boolean showFooterView = (showDismissView || mController.getVisibleNotificationCount() > 0)
&& mIsCurrentUserSetup // see: b/193149550
&& mStatusBarState != StatusBarState.KEYGUARD
&& !mUnlockedScreenOffAnimationController.isScreenOffAnimationPlaying()
@@ -1173,20 +1173,6 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable
}
}
/**
* Returns best effort count of visible notifications.
*/
public int getVisibleNotificationCount() {
int count = 0;
for (int i = 0; i < getChildCount(); i++) {
final View child = getChildAt(i);
if (child.getVisibility() != View.GONE && child instanceof ExpandableNotificationRow) {
count++;
}
}
return count;
}
@ShadeViewRefactor(RefactorComponent.STATE_RESOLVER)
private boolean isCurrentlyAnimating() {
return mStateAnimator.isRunning();
@@ -1458,7 +1444,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable
@ShadeViewRefactor(RefactorComponent.COORDINATOR)
private float getAppearEndPosition() {
int appearPosition = 0;
int visibleNotifCount = getVisibleNotificationCount();
int visibleNotifCount = mController.getVisibleNotificationCount();
if (mEmptyShadeView.getVisibility() == GONE && visibleNotifCount > 0) {
if (isHeadsUpTransition()
|| (mInHeadsUpPinnedMode && !mAmbientState.isDozing())) {

View File

@@ -48,6 +48,7 @@ import android.view.View;
import android.view.ViewGroup;
import android.view.WindowInsets;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.internal.annotations.VisibleForTesting;
@@ -98,6 +99,8 @@ import com.android.systemui.statusbar.notification.collection.legacy.VisualStabi
import com.android.systemui.statusbar.notification.collection.notifcollection.DismissedByUserStats;
import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener;
import com.android.systemui.statusbar.notification.collection.render.GroupExpansionManager;
import com.android.systemui.statusbar.notification.collection.render.NotifStackController;
import com.android.systemui.statusbar.notification.collection.render.NotifStats;
import com.android.systemui.statusbar.notification.collection.render.NotificationVisibilityProvider;
import com.android.systemui.statusbar.notification.collection.render.SectionHeaderController;
import com.android.systemui.statusbar.notification.dagger.SilentHeader;
@@ -192,6 +195,8 @@ public class NotificationStackScrollLayoutController {
private final NotificationListContainerImpl mNotificationListContainer =
new NotificationListContainerImpl();
private final NotifStackController mNotifStackController =
new NotifStackControllerImpl();
@Nullable
private NotificationActivityStarter mNotificationActivityStarter;
@@ -294,6 +299,8 @@ public class NotificationStackScrollLayoutController {
}
};
private NotifStats mNotifStats = NotifStats.getEmpty();
private void updateResources() {
mNotificationDragDownMovement = mResources.getDimensionPixelSize(
R.dimen.lockscreen_shade_notification_movement);
@@ -988,7 +995,7 @@ public class NotificationStackScrollLayoutController {
}
public int getVisibleNotificationCount() {
return mView.getVisibleNotificationCount();
return mNotifStats.getNumActiveNotifs();
}
public int getIntrinsicContentHeight() {
@@ -1183,7 +1190,7 @@ public class NotificationStackScrollLayoutController {
public void updateShowEmptyShadeView() {
mShowEmptyShadeView = mBarState != KEYGUARD
&& (!mView.isQsExpanded() || mView.isUsingSplitNotificationShade())
&& mView.getVisibleNotificationCount() == 0;
&& getVisibleNotificationCount() == 0;
mView.updateEmptyShadeView(
mShowEmptyShadeView,
@@ -1246,29 +1253,22 @@ public class NotificationStackScrollLayoutController {
}
public boolean hasNotifications(@SelectedRows int selection, boolean isClearable) {
if (mDynamicPrivacyController.isInLockedDownShade()) {
return false;
boolean hasAlertingMatchingClearable = isClearable
? mNotifStats.getHasClearableAlertingNotifs()
: mNotifStats.getHasNonClearableAlertingNotifs();
boolean hasSilentMatchingClearable = isClearable
? mNotifStats.getHasClearableSilentNotifs()
: mNotifStats.getHasNonClearableSilentNotifs();
switch (selection) {
case ROWS_GENTLE:
return hasSilentMatchingClearable;
case ROWS_HIGH_PRIORITY:
return hasAlertingMatchingClearable;
case ROWS_ALL:
return hasSilentMatchingClearable || hasAlertingMatchingClearable;
default:
throw new IllegalStateException("Bad selection: " + selection);
}
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
if (!(child instanceof ExpandableNotificationRow)) {
continue;
}
final ExpandableNotificationRow row = (ExpandableNotificationRow) child;
final boolean matchClearable =
isClearable ? row.canViewBeDismissed() : !row.canViewBeDismissed();
final boolean inSection =
NotificationStackScrollLayout.matchesSelection(row, selection);
if (matchClearable && inSection) {
if (mLegacyGroupManager == null
|| !mLegacyGroupManager.isSummaryOfSuppressedGroup(
row.getEntry().getSbn())) {
return true;
}
}
}
return false;
}
/**
@@ -1383,6 +1383,10 @@ public class NotificationStackScrollLayoutController {
return mNotificationListContainer;
}
public NotifStackController getNotifStackController() {
return mNotifStackController;
}
public void resetCheckSnoozeLeavebehind() {
mView.resetCheckSnoozeLeavebehind();
}
@@ -1394,17 +1398,6 @@ public class NotificationStackScrollLayoutController {
mVisibilityProvider.obtain(entry, true));
}
/**
* @return if the shade has currently any active notifications.
*/
public boolean hasActiveNotifications() {
if (mNotifPipelineFlags.isNewPipelineEnabled()) {
return !mNotifPipeline.getShadeList().isEmpty();
} else {
return mNotificationEntryManager.hasActiveNotifications();
}
}
public void closeControlsIfOutsideTouch(MotionEvent ev) {
NotificationGuts guts = mNotificationGutsManager.getExposedGuts();
NotificationMenuRowPlugin menuRow = mSwipeHelper.getCurrentMenuRow();
@@ -1876,4 +1869,13 @@ public class NotificationStackScrollLayoutController {
}
}
}
private class NotifStackControllerImpl implements NotifStackController {
@Override
public void setNotifStats(@NonNull NotifStats notifStats) {
mNotifStats = notifStats;
updateFooter();
updateShowEmptyShadeView();
}
}
}

View File

@@ -1488,6 +1488,7 @@ public class StatusBar extends CoreStartable implements
mBubblesOptional,
mPresenter,
mStackScrollerController.getNotificationListContainer(),
mStackScrollerController.getNotifStackController(),
mNotificationActivityStarter,
mPresenter);
}

View File

@@ -192,6 +192,7 @@ public class StatusBarNotificationPresenter implements NotificationPresenter,
initController.addPostInitTask(() -> {
mKeyguardIndicationController.init();
mViewHierarchyManager.setUpWithPresenter(this,
stackScrollerController.getNotifStackController(),
stackScrollerController.getNotificationListContainer());
mNotifShadeEventSource.setShadeEmptiedCallback(this::maybeClosePanelForShadeEmptied);
mNotifShadeEventSource.setNotifRemovedByUserCallback(this::maybeEndAmbientPulse);

View File

@@ -29,6 +29,7 @@ import com.android.systemui.Dependency;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.statusbar.notification.NotificationEntryListener;
import com.android.systemui.statusbar.notification.NotificationEntryManager;
import com.android.systemui.statusbar.notification.collection.render.NotifStackController;
import com.android.systemui.statusbar.notification.logging.NotificationLogger;
import com.android.systemui.statusbar.notification.row.NotificationGutsManager;
import com.android.systemui.statusbar.notification.row.NotificationGutsManager.OnSettingsClickListener;
@@ -54,6 +55,7 @@ import org.mockito.MockitoAnnotations;
@TestableLooper.RunWithLooper(setAsMainLooper = true)
public class NonPhoneDependencyTest extends SysuiTestCase {
@Mock private NotificationPresenter mPresenter;
@Mock private NotifStackController mStackController;
@Mock private NotificationListContainer mListContainer;
@Mock
private NotificationEntryListener mEntryListener;
@@ -95,7 +97,7 @@ public class NonPhoneDependencyTest extends SysuiTestCase {
remoteInputManager.setUpWithCallback(mRemoteInputManagerCallback,
mDelegate);
lockscreenUserManager.setUpWithPresenter(mPresenter);
viewHierarchyManager.setUpWithPresenter(mPresenter, mListContainer);
viewHierarchyManager.setUpWithPresenter(mPresenter, mStackController, mListContainer);
TestableLooper.get(this).processAllMessages();
assertFalse(mDependency.hasInstantiatedDependency(NotificationShadeWindowController.class));

View File

@@ -48,6 +48,7 @@ import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.notification.collection.legacy.LowPriorityInflationHelper;
import com.android.systemui.statusbar.notification.collection.legacy.NotificationGroupManagerLegacy;
import com.android.systemui.statusbar.notification.collection.legacy.VisualStabilityManager;
import com.android.systemui.statusbar.notification.collection.render.NotifStackController;
import com.android.systemui.statusbar.notification.logging.NotificationLogger;
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
import com.android.systemui.statusbar.notification.row.ExpandableView;
@@ -74,6 +75,7 @@ import java.util.Optional;
@TestableLooper.RunWithLooper
public class NotificationViewHierarchyManagerTest extends SysuiTestCase {
@Mock private NotificationPresenter mPresenter;
@Mock private NotifStackController mStackController;
@Spy private FakeListContainer mListContainer = new FakeListContainer();
// Dependency mocks:
@@ -122,7 +124,7 @@ public class NotificationViewHierarchyManagerTest extends SysuiTestCase {
mock(LowPriorityInflationHelper.class),
mock(AssistantFeedbackController.class),
mNotifPipelineFlags);
mViewHierarchyManager.setUpWithPresenter(mPresenter, mListContainer);
mViewHierarchyManager.setUpWithPresenter(mPresenter, mStackController, mListContainer);
}
private NotificationEntry createEntry() throws Exception {

View File

@@ -19,6 +19,7 @@ package com.android.systemui.statusbar.notification.collection
import android.testing.AndroidTestingRunner
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.statusbar.notification.collection.render.RenderStageManager
import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Test
@@ -33,12 +34,13 @@ class NotifPipelineTest : SysuiTestCase() {
@Mock private lateinit var notifCollection: NotifCollection
@Mock private lateinit var shadeListBuilder: ShadeListBuilder
@Mock private lateinit var renderStageManager: RenderStageManager
private lateinit var notifPipeline: NotifPipeline
@Before
fun setup() {
MockitoAnnotations.initMocks(this)
notifPipeline = NotifPipeline(notifCollection, shadeListBuilder)
notifPipeline = NotifPipeline(notifCollection, shadeListBuilder, renderStageManager)
whenever(shadeListBuilder.shadeList).thenReturn(listOf(
NotificationEntryBuilder().setPkg("foo").setId(1).build(),
NotificationEntryBuilder().setPkg("foo").setId(2).build(),

View File

@@ -0,0 +1,79 @@
/*
* Copyright (C) 2021 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.coordinator
import android.testing.AndroidTestingRunner
import android.testing.TestableLooper.RunWithLooper
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.statusbar.notification.collection.GroupEntryBuilder
import com.android.systemui.statusbar.notification.collection.NotifPipeline
import com.android.systemui.statusbar.notification.collection.NotificationEntry
import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder
import com.android.systemui.statusbar.notification.collection.listbuilder.OnAfterRenderGroupListener
import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeFinalizeFilterListener
import com.android.systemui.statusbar.notification.collection.render.NotifGroupController
import com.android.systemui.util.mockito.eq
import com.android.systemui.util.mockito.withArgCaptor
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations.initMocks
@SmallTest
@RunWith(AndroidTestingRunner::class)
@RunWithLooper
class GroupCountCoordinatorTest : SysuiTestCase() {
private lateinit var coordinator: GroupCountCoordinator
private lateinit var beforeFinalizeFilterListener: OnBeforeFinalizeFilterListener
private lateinit var afterRenderGroupListener: OnAfterRenderGroupListener
private lateinit var summaryEntry: NotificationEntry
private lateinit var childEntry1: NotificationEntry
private lateinit var childEntry2: NotificationEntry
@Mock private lateinit var pipeline: NotifPipeline
@Mock private lateinit var groupController: NotifGroupController
@Before
fun setUp() {
initMocks(this)
coordinator = GroupCountCoordinator()
coordinator.attach(pipeline)
beforeFinalizeFilterListener = withArgCaptor {
verify(pipeline).addOnBeforeFinalizeFilterListener(capture())
}
afterRenderGroupListener = withArgCaptor {
verify(pipeline).addOnAfterRenderGroupListener(capture())
}
summaryEntry = NotificationEntryBuilder().setId(0).build()
childEntry1 = NotificationEntryBuilder().setId(1).build()
childEntry2 = NotificationEntryBuilder().setId(2).build()
}
@Test
fun testSetUntruncatedChildCount() {
val groupEntry = GroupEntryBuilder()
.setSummary(summaryEntry)
.setChildren(listOf(childEntry1, childEntry2))
.build()
beforeFinalizeFilterListener.onBeforeFinalizeFilter(listOf(groupEntry))
afterRenderGroupListener.onAfterRenderGroup(groupEntry, groupController)
verify(groupController).setUntruncatedChildCount(eq(2))
}
}

View File

@@ -85,7 +85,6 @@ public class PreparationCoordinatorTest extends SysuiTestCase {
@Captor private ArgumentCaptor<NotifCollectionListener> mCollectionListenerCaptor;
@Captor private ArgumentCaptor<OnBeforeFinalizeFilterListener> mBeforeFilterListenerCaptor;
@Captor private ArgumentCaptor<NotifInflater.InflationCallback> mCallbackCaptor;
@Captor private ArgumentCaptor<NotifInflater.Params> mParamsCaptor;
@Mock private NotifSectioner mNotifSectioner;
@@ -180,8 +179,8 @@ public class PreparationCoordinatorTest extends SysuiTestCase {
// GIVEN an inflated notification
mCollectionListener.onEntryAdded(mEntry);
mBeforeFilterListener.onBeforeFinalizeFilter(List.of(mEntry));
verify(mNotifInflater).inflateViews(eq(mEntry), any(), mCallbackCaptor.capture());
mCallbackCaptor.getValue().onInflationFinished(mEntry);
verify(mNotifInflater).inflateViews(eq(mEntry), any(), any());
mNotifInflater.invokeInflateCallbackForEntry(mEntry);
// WHEN notification is updated
mCollectionListener.onEntryUpdated(mEntry);
@@ -199,8 +198,8 @@ public class PreparationCoordinatorTest extends SysuiTestCase {
// GIVEN an inflated notification
mCollectionListener.onEntryAdded(mEntry);
mBeforeFilterListener.onBeforeFinalizeFilter(List.of(mEntry));
verify(mNotifInflater).inflateViews(eq(mEntry), any(), mCallbackCaptor.capture());
mCallbackCaptor.getValue().onInflationFinished(mEntry);
verify(mNotifInflater).inflateViews(eq(mEntry), any(), any());
mNotifInflater.invokeInflateCallbackForEntry(mEntry);
// WHEN notification ranking now has smart replies
mEntry.setRanking(new RankingBuilder(mEntry.getRanking()).setSmartReplies("yes").build());
@@ -218,10 +217,9 @@ public class PreparationCoordinatorTest extends SysuiTestCase {
// GIVEN an inflated notification
mCollectionListener.onEntryAdded(mEntry);
mBeforeFilterListener.onBeforeFinalizeFilter(List.of(mEntry));
verify(mNotifInflater).inflateViews(eq(mEntry),
mParamsCaptor.capture(), mCallbackCaptor.capture());
verify(mNotifInflater).inflateViews(eq(mEntry), mParamsCaptor.capture(), any());
assertFalse(mParamsCaptor.getValue().isLowPriority());
mCallbackCaptor.getValue().onInflationFinished(mEntry);
mNotifInflater.invokeInflateCallbackForEntry(mEntry);
// WHEN notification moves to a min priority section
mAdjustmentProvider.setSectionIsLowPriority(true);
@@ -241,10 +239,9 @@ public class PreparationCoordinatorTest extends SysuiTestCase {
mAdjustmentProvider.setSectionIsLowPriority(true);
mCollectionListener.onEntryAdded(mEntry);
mBeforeFilterListener.onBeforeFinalizeFilter(List.of(mEntry));
verify(mNotifInflater).inflateViews(eq(mEntry),
mParamsCaptor.capture(), mCallbackCaptor.capture());
verify(mNotifInflater).inflateViews(eq(mEntry), mParamsCaptor.capture(), any());
assertTrue(mParamsCaptor.getValue().isLowPriority());
mCallbackCaptor.getValue().onInflationFinished(mEntry);
mNotifInflater.invokeInflateCallbackForEntry(mEntry);
// WHEN notification is moved under a parent
NotificationEntryBuilder.setNewParent(mEntry, mock(GroupEntry.class));
@@ -263,8 +260,8 @@ public class PreparationCoordinatorTest extends SysuiTestCase {
// GIVEN an inflated notification
mCollectionListener.onEntryAdded(mEntry);
mBeforeFilterListener.onBeforeFinalizeFilter(List.of(mEntry));
verify(mNotifInflater).inflateViews(eq(mEntry), any(), mCallbackCaptor.capture());
mCallbackCaptor.getValue().onInflationFinished(mEntry);
verify(mNotifInflater).inflateViews(eq(mEntry), any(), any());
mNotifInflater.invokeInflateCallbackForEntry(mEntry);
// WHEN notification ranking changes rank, which does not affect views
mEntry.setRanking(new RankingBuilder(mEntry.getRanking()).setRank(100).build());
@@ -282,8 +279,8 @@ public class PreparationCoordinatorTest extends SysuiTestCase {
// GIVEN an inflated notification
mCollectionListener.onEntryAdded(mEntry);
mBeforeFilterListener.onBeforeFinalizeFilter(List.of(mEntry));
verify(mNotifInflater).inflateViews(eq(mEntry), any(), mCallbackCaptor.capture());
mCallbackCaptor.getValue().onInflationFinished(mEntry);
verify(mNotifInflater).inflateViews(eq(mEntry), any(), any());
mNotifInflater.invokeInflateCallbackForEntry(mEntry);
// THEN it isn't filtered from shade list
assertFalse(mUninflatedFilter.shouldFilterOut(mEntry, 0));
@@ -347,7 +344,7 @@ public class PreparationCoordinatorTest extends SysuiTestCase {
mBeforeFilterListener.onBeforeFinalizeFilter(List.of(group));
// WHEN one of this children finishes inflating
mNotifInflater.getInflateCallback(child0).onInflationFinished(child0);
mNotifInflater.invokeInflateCallbackForEntry(child0);
// THEN the inflated child is still filtered out
assertTrue(mUninflatedFilter.shouldFilterOut(child0, 401));
@@ -369,8 +366,8 @@ public class PreparationCoordinatorTest extends SysuiTestCase {
mBeforeFilterListener.onBeforeFinalizeFilter(List.of(group));
// WHEN all of the children (but not the summary) finish inflating
mNotifInflater.getInflateCallback(child0).onInflationFinished(child0);
mNotifInflater.getInflateCallback(child1).onInflationFinished(child1);
mNotifInflater.invokeInflateCallbackForEntry(child0);
mNotifInflater.invokeInflateCallbackForEntry(child1);
// THEN the entire group is still filtered out
assertTrue(mUninflatedFilter.shouldFilterOut(summary, 401));
@@ -394,9 +391,9 @@ public class PreparationCoordinatorTest extends SysuiTestCase {
mBeforeFilterListener.onBeforeFinalizeFilter(List.of(group));
// WHEN all of the children (and the summary) finish inflating
mNotifInflater.getInflateCallback(child0).onInflationFinished(child0);
mNotifInflater.getInflateCallback(child1).onInflationFinished(child1);
mNotifInflater.getInflateCallback(summary).onInflationFinished(summary);
mNotifInflater.invokeInflateCallbackForEntry(child0);
mNotifInflater.invokeInflateCallbackForEntry(child1);
mNotifInflater.invokeInflateCallbackForEntry(summary);
// THEN the entire group is still filtered out
assertFalse(mUninflatedFilter.shouldFilterOut(summary, 401));
@@ -418,7 +415,7 @@ public class PreparationCoordinatorTest extends SysuiTestCase {
mBeforeFilterListener.onBeforeFinalizeFilter(List.of(group));
// WHEN one of this children finishes inflating and enough time passes
mNotifInflater.getInflateCallback(child0).onInflationFinished(child0);
mNotifInflater.invokeInflateCallbackForEntry(child0);
// THEN the inflated child is not filtered out even though the rest of the group hasn't
// finished inflating yet
@@ -446,6 +443,10 @@ public class PreparationCoordinatorTest extends SysuiTestCase {
public InflationCallback getInflateCallback(NotificationEntry entry) {
return requireNonNull(mInflateCallbacks.get(entry));
}
public void invokeInflateCallbackForEntry(NotificationEntry entry) {
getInflateCallback(entry).onInflationFinished(entry, entry.getRowController());
}
}
private void fireAddEvents(List<? extends ListEntry> entries) {

View File

@@ -0,0 +1,86 @@
/*
* Copyright (C) 2021 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.coordinator
import android.testing.AndroidTestingRunner
import android.testing.TestableLooper.RunWithLooper
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.statusbar.notification.collection.NotifPipeline
import com.android.systemui.statusbar.notification.collection.NotificationEntry
import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder
import com.android.systemui.statusbar.notification.collection.listbuilder.NotifSection
import com.android.systemui.statusbar.notification.collection.listbuilder.OnAfterRenderListListener
import com.android.systemui.statusbar.notification.collection.render.NotifStackController
import com.android.systemui.statusbar.notification.collection.render.NotifStats
import com.android.systemui.statusbar.notification.stack.BUCKET_ALERTING
import com.android.systemui.statusbar.notification.stack.BUCKET_SILENT
import com.android.systemui.statusbar.phone.NotificationIconAreaController
import com.android.systemui.util.mockito.eq
import com.android.systemui.util.mockito.withArgCaptor
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations.initMocks
import org.mockito.Mockito.`when` as whenever
@SmallTest
@RunWith(AndroidTestingRunner::class)
@RunWithLooper
class StackCoordinatorTest : SysuiTestCase() {
private lateinit var coordinator: StackCoordinator
private lateinit var afterRenderListListener: OnAfterRenderListListener
private lateinit var entry: NotificationEntry
@Mock private lateinit var pipeline: NotifPipeline
@Mock private lateinit var notificationIconAreaController: NotificationIconAreaController
@Mock private lateinit var stackController: NotifStackController
@Mock private lateinit var section: NotifSection
@Before
fun setUp() {
initMocks(this)
coordinator = StackCoordinator(notificationIconAreaController)
coordinator.attach(pipeline)
afterRenderListListener = withArgCaptor {
verify(pipeline).addOnAfterRenderListListener(capture())
}
entry = NotificationEntryBuilder().setSection(section).build()
}
@Test
fun testUpdateNotificationIcons() {
afterRenderListListener.onAfterRenderList(listOf(entry), stackController)
verify(notificationIconAreaController).updateNotificationIcons(eq(listOf(entry)))
}
@Test
fun testSetNotificationStats_clearableAlerting() {
whenever(section.bucket).thenReturn(BUCKET_ALERTING)
afterRenderListListener.onAfterRenderList(listOf(entry), stackController)
verify(stackController).setNotifStats(NotifStats(1, false, true, false, false))
}
@Test
fun testSetNotificationStats_clearableSilent() {
whenever(section.bucket).thenReturn(BUCKET_SILENT)
afterRenderListListener.onAfterRenderList(listOf(entry), stackController)
verify(stackController).setNotifStats(NotifStats(1, false, false, false, true))
}
}

View File

@@ -68,7 +68,7 @@ class NodeSpecBuilderTest : SysuiTestCase() {
fun setUp() {
MockitoAnnotations.initMocks(this)
`when`(viewBarn.requireView(any())).thenAnswer {
`when`(viewBarn.requireNodeController(any())).thenAnswer {
fakeViewBarn.getViewByEntry(it.getArgument(0))
}

View File

@@ -0,0 +1,222 @@
/*
* Copyright (C) 2021 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.render
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.statusbar.notification.collection.GroupEntry
import com.android.systemui.statusbar.notification.collection.GroupEntryBuilder
import com.android.systemui.statusbar.notification.collection.ListEntry
import com.android.systemui.statusbar.notification.collection.NotificationEntry
import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder
import com.android.systemui.statusbar.notification.collection.ShadeListBuilder
import com.android.systemui.statusbar.notification.collection.listbuilder.OnAfterRenderEntryListener
import com.android.systemui.statusbar.notification.collection.listbuilder.OnAfterRenderGroupListener
import com.android.systemui.statusbar.notification.collection.listbuilder.OnAfterRenderListListener
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.withArgCaptor
import org.junit.Before
import org.junit.Test
import org.mockito.Mock
import org.mockito.Mockito.inOrder
import org.mockito.Mockito.never
import org.mockito.Mockito.spy
import org.mockito.Mockito.times
import org.mockito.Mockito.verify
import org.mockito.Mockito.verifyNoMoreInteractions
import org.mockito.MockitoAnnotations
@SmallTest
class RenderStageManagerTest : SysuiTestCase() {
@Mock private lateinit var shadeListBuilder: ShadeListBuilder
@Mock private lateinit var onAfterRenderListListener: OnAfterRenderListListener
@Mock private lateinit var onAfterRenderGroupListener: OnAfterRenderGroupListener
@Mock private lateinit var onAfterRenderEntryListener: OnAfterRenderEntryListener
private lateinit var onRenderListListener: ShadeListBuilder.OnRenderListListener
private lateinit var renderStageManager: RenderStageManager
private val spyViewRenderer = spy(FakeNotifViewRenderer())
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
renderStageManager = RenderStageManager()
renderStageManager.attach(shadeListBuilder)
onRenderListListener = withArgCaptor {
verify(shadeListBuilder).setOnRenderListListener(capture())
}
}
private fun setUpRenderer() {
renderStageManager.setViewRenderer(spyViewRenderer)
}
private fun setUpListeners() {
renderStageManager.addOnAfterRenderListListener(onAfterRenderListListener)
renderStageManager.addOnAfterRenderGroupListener(onAfterRenderGroupListener)
renderStageManager.addOnAfterRenderEntryListener(onAfterRenderEntryListener)
}
@Test
fun testNoCallbacksWithoutRenderer() {
// GIVEN listeners but no renderer
setUpListeners()
// WHEN a shade list is built
onRenderListListener.onRenderList(listWith2Groups8Entries())
// VERIFY that no listeners are called
verifyNoMoreInteractions(
onAfterRenderListListener,
onAfterRenderGroupListener,
onAfterRenderEntryListener
)
}
@Test
fun testDoesNotQueryControllerIfNoListeners() {
// GIVEN a renderer but no listeners
setUpRenderer()
// WHEN a shade list is built
onRenderListListener.onRenderList(listWith2Groups8Entries())
// VERIFY that the renderer is not queried for group or row controllers
inOrder(spyViewRenderer).apply {
verify(spyViewRenderer, times(1)).onRenderList(any())
verify(spyViewRenderer, times(1)).getStackController()
verify(spyViewRenderer, never()).getGroupController(any())
verify(spyViewRenderer, never()).getRowController(any())
verify(spyViewRenderer, times(1)).onDispatchComplete()
verifyNoMoreInteractions(spyViewRenderer)
}
}
@Test
fun testDoesQueryControllerIfListeners() {
// GIVEN a renderer and listeners
setUpRenderer()
setUpListeners()
// WHEN a shade list is built
onRenderListListener.onRenderList(listWith2Groups8Entries())
// VERIFY that the renderer is queried once per group/entry
inOrder(spyViewRenderer).apply {
verify(spyViewRenderer, times(1)).onRenderList(any())
verify(spyViewRenderer, times(1)).getStackController()
verify(spyViewRenderer, times(2)).getGroupController(any())
verify(spyViewRenderer, times(8)).getRowController(any())
verify(spyViewRenderer, times(1)).onDispatchComplete()
verifyNoMoreInteractions(spyViewRenderer)
}
}
@Test
fun testDoesNotQueryControllerTwice() {
// GIVEN a renderer and multiple distinct listeners
setUpRenderer()
setUpListeners()
renderStageManager.addOnAfterRenderListListener(mock())
renderStageManager.addOnAfterRenderGroupListener(mock())
renderStageManager.addOnAfterRenderEntryListener(mock())
// WHEN a shade list is built
onRenderListListener.onRenderList(listWith2Groups8Entries())
// VERIFY that the renderer is queried once per group/entry
inOrder(spyViewRenderer).apply {
verify(spyViewRenderer, times(1)).onRenderList(any())
verify(spyViewRenderer, times(1)).getStackController()
verify(spyViewRenderer, times(2)).getGroupController(any())
verify(spyViewRenderer, times(8)).getRowController(any())
verify(spyViewRenderer, times(1)).onDispatchComplete()
verifyNoMoreInteractions(spyViewRenderer)
}
}
@Test
fun testDoesCallListenerWithEachGroupAndEntry() {
// GIVEN a renderer and multiple distinct listeners
setUpRenderer()
setUpListeners()
// WHEN a shade list is built
onRenderListListener.onRenderList(listWith2Groups8Entries())
// VERIFY that the listeners are invoked once per group and once per entry
verify(onAfterRenderListListener, times(1)).onAfterRenderList(any(), any())
verify(onAfterRenderGroupListener, times(2)).onAfterRenderGroup(any(), any())
verify(onAfterRenderEntryListener, times(8)).onAfterRenderEntry(any(), any())
verifyNoMoreInteractions(
onAfterRenderListListener,
onAfterRenderGroupListener,
onAfterRenderEntryListener
)
}
@Test
fun testDoesNotCallGroupAndEntryListenersIfTheListIsEmpty() {
// GIVEN a renderer and multiple distinct listeners
setUpRenderer()
setUpListeners()
// WHEN a shade list is built empty
onRenderListListener.onRenderList(listOf())
// VERIFY that the stack listener is invoked once but other listeners are not
verify(onAfterRenderListListener, times(1)).onAfterRenderList(any(), any())
verify(onAfterRenderGroupListener, never()).onAfterRenderGroup(any(), any())
verify(onAfterRenderEntryListener, never()).onAfterRenderEntry(any(), any())
verifyNoMoreInteractions(
onAfterRenderListListener,
onAfterRenderGroupListener,
onAfterRenderEntryListener
)
}
private fun listWith2Groups8Entries() = listOf(
group(
notif(1),
notif(2),
notif(3)
),
notif(4),
group(
notif(5),
notif(6),
notif(7)
),
notif(8)
)
private class FakeNotifViewRenderer : NotifViewRenderer {
override fun onRenderList(notifList: List<ListEntry>) {}
override fun getStackController(): NotifStackController = mock()
override fun getGroupController(group: GroupEntry): NotifGroupController = mock()
override fun getRowController(entry: NotificationEntry): NotifRowController = mock()
override fun onDispatchComplete() {}
}
private fun notif(id: Int): NotificationEntry = NotificationEntryBuilder().setId(id).build()
private fun group(summary: NotificationEntry, vararg children: NotificationEntry): GroupEntry =
GroupEntryBuilder().setSummary(summary).setChildren(children.toList()).build()
}

View File

@@ -30,7 +30,6 @@ import static junit.framework.Assert.assertNotNull;
import static org.junit.Assert.assertFalse;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.clearInvocations;
@@ -296,14 +295,10 @@ public class NotificationStackScrollLayoutTest extends SysuiTestCase {
setBarStateForTest(StatusBarState.SHADE);
mStackScroller.setCurrentUserSetup(true);
ExpandableNotificationRow row = mock(ExpandableNotificationRow.class);
when(row.canViewBeDismissed()).thenReturn(true);
when(mStackScroller.getChildCount()).thenReturn(1);
when(mStackScroller.getChildAt(anyInt())).thenReturn(row);
mStackScroller.setIsRemoteInputActive(true);
when(mStackScrollLayoutController.hasActiveClearableNotifications(ROWS_ALL))
when(mStackScrollLayoutController.getVisibleNotificationCount()).thenReturn(1);
when(mStackScrollLayoutController.hasActiveClearableNotifications(eq(ROWS_ALL)))
.thenReturn(true);
when(mStackScrollLayoutController.hasActiveNotifications()).thenReturn(true);
FooterView view = mock(FooterView.class);
mStackScroller.setFooterView(view);
@@ -311,15 +306,29 @@ public class NotificationStackScrollLayoutTest extends SysuiTestCase {
verify(mStackScroller).updateFooterView(false, true, true);
}
@Test
public void testUpdateFooter_withoutNotifications() {
setBarStateForTest(StatusBarState.SHADE);
mStackScroller.setCurrentUserSetup(true);
when(mStackScrollLayoutController.getVisibleNotificationCount()).thenReturn(0);
when(mStackScrollLayoutController.hasActiveClearableNotifications(eq(ROWS_ALL)))
.thenReturn(false);
FooterView view = mock(FooterView.class);
mStackScroller.setFooterView(view);
mStackScroller.updateFooter();
verify(mStackScroller).updateFooterView(false, false, true);
}
@Test
public void testUpdateFooter_oneClearableNotification() {
setBarStateForTest(StatusBarState.SHADE);
mStackScroller.setCurrentUserSetup(true);
when(mEmptyShadeView.getVisibility()).thenReturn(GONE);
when(mStackScrollLayoutController.hasActiveClearableNotifications(ROWS_ALL))
when(mStackScrollLayoutController.getVisibleNotificationCount()).thenReturn(1);
when(mStackScrollLayoutController.hasActiveClearableNotifications(eq(ROWS_ALL)))
.thenReturn(true);
when(mStackScrollLayoutController.hasActiveNotifications()).thenReturn(true);
FooterView view = mock(FooterView.class);
mStackScroller.setFooterView(view);
@@ -332,10 +341,9 @@ public class NotificationStackScrollLayoutTest extends SysuiTestCase {
setBarStateForTest(StatusBarState.SHADE);
mStackScroller.setCurrentUserSetup(false);
when(mEmptyShadeView.getVisibility()).thenReturn(GONE);
when(mStackScrollLayoutController.hasActiveClearableNotifications(ROWS_ALL))
when(mStackScrollLayoutController.getVisibleNotificationCount()).thenReturn(1);
when(mStackScrollLayoutController.hasActiveClearableNotifications(eq(ROWS_ALL)))
.thenReturn(true);
when(mStackScrollLayoutController.hasActiveNotifications()).thenReturn(true);
FooterView view = mock(FooterView.class);
mStackScroller.setFooterView(view);
@@ -348,12 +356,8 @@ public class NotificationStackScrollLayoutTest extends SysuiTestCase {
setBarStateForTest(StatusBarState.SHADE);
mStackScroller.setCurrentUserSetup(true);
ExpandableNotificationRow row = mock(ExpandableNotificationRow.class);
when(row.canViewBeDismissed()).thenReturn(false);
when(mStackScroller.getChildCount()).thenReturn(1);
when(mStackScroller.getChildAt(anyInt())).thenReturn(row);
when(mStackScrollLayoutController.hasActiveNotifications()).thenReturn(true);
when(mStackScrollLayoutController.hasActiveClearableNotifications(ROWS_ALL))
when(mStackScrollLayoutController.getVisibleNotificationCount()).thenReturn(1);
when(mStackScrollLayoutController.hasActiveClearableNotifications(eq(ROWS_ALL)))
.thenReturn(false);
when(mEmptyShadeView.getVisibility()).thenReturn(GONE);