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

This commit is contained in:
Jeff DeCew
2021-11-24 02:05:40 +00:00
committed by Android (Google) Code Review
42 changed files with 1250 additions and 192 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
@@ -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);