From 43018ba23285cbb375520216a0fd4ebfed9edae3 Mon Sep 17 00:00:00 2001 From: Jeff DeCew Date: Wed, 10 Nov 2021 05:00:33 +0000 Subject: [PATCH] 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 --- .../NotificationViewHierarchyManager.java | 61 ++++- .../notification/NotifPipelineFlags.kt | 17 +- .../notification/collection/GroupEntry.java | 20 -- .../collection/NotifInflaterImpl.java | 2 +- .../notification/collection/NotifPipeline.kt | 32 ++- .../coordinator/GroupCountCoordinator.kt | 35 +++ .../coordinator/NotifCoordinators.kt | 6 +- .../coordinator/PreparationCoordinator.java | 17 +- .../coordinator/RemoteInputCoordinator.kt | 4 +- .../coordinator/StackCoordinator.kt | 72 ++++++ .../collection/inflation/NotifInflater.kt | 3 +- .../init/NotifPipelineInitializer.java | 16 +- .../OnAfterRenderEntryListener.java | 37 +++ .../OnAfterRenderGroupListener.java | 36 +++ .../OnAfterRenderListListener.java | 39 +++ .../collection/render/NodeSpecBuilder.kt | 7 +- .../collection/render/NotifGroupController.kt | 23 ++ .../collection/render/NotifRowController.kt | 20 ++ .../collection/render/NotifStackController.kt | 47 ++++ .../collection/render/NotifViewBarn.kt | 34 ++- .../collection/render/NotifViewController.kt | 19 ++ .../collection/render/NotifViewRenderer.kt | 69 ++++++ .../collection/render/RenderExtensions.kt | 25 ++ .../collection/render/RenderStageManager.kt | 142 +++++++++++ .../collection/render/ShadeViewManager.kt | 56 +++-- .../init/NotificationsController.kt | 2 + .../init/NotificationsControllerImpl.kt | 7 +- .../init/NotificationsControllerStub.kt | 2 + .../ExpandableNotificationRowController.java | 14 +- .../stack/NotificationStackScrollLayout.java | 18 +- ...tificationStackScrollLayoutController.java | 72 +++--- .../systemui/statusbar/phone/StatusBar.java | 1 + .../phone/StatusBarNotificationPresenter.java | 1 + .../statusbar/NonPhoneDependencyTest.java | 4 +- .../NotificationViewHierarchyManagerTest.java | 4 +- .../collection/NotifPipelineTest.kt | 4 +- .../coordinator/GroupCountCoordinatorTest.kt | 79 +++++++ .../PreparationCoordinatorTest.java | 45 ++-- .../coordinator/StackCoordinatorTest.kt | 86 +++++++ .../collection/render/NodeSpecBuilderTest.kt | 2 +- .../render/RenderStageManagerTest.kt | 222 ++++++++++++++++++ .../NotificationStackScrollLayoutTest.java | 42 ++-- 42 files changed, 1251 insertions(+), 193 deletions(-) create mode 100644 packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/GroupCountCoordinator.kt create mode 100644 packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinator.kt create mode 100644 packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/OnAfterRenderEntryListener.java create mode 100644 packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/OnAfterRenderGroupListener.java create mode 100644 packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/OnAfterRenderListListener.java create mode 100644 packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NotifGroupController.kt create mode 100644 packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NotifRowController.kt create mode 100644 packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NotifStackController.kt create mode 100644 packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NotifViewController.kt create mode 100644 packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NotifViewRenderer.kt create mode 100644 packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/RenderExtensions.kt create mode 100644 packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/RenderStageManager.kt create mode 100644 packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/GroupCountCoordinatorTest.kt create mode 100644 packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinatorTest.kt create mode 100644 packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/render/RenderStageManagerTest.kt diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationViewHierarchyManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationViewHierarchyManager.java index ecde001e6bf91..83ef41ea0d1db 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationViewHierarchyManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationViewHierarchyManager.java @@ -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"); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotifPipelineFlags.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotifPipelineFlags.kt index 0fb9fc80e5100..9bf21d1e1a260 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotifPipelineFlags.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotifPipelineFlags.kt @@ -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) } \ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/GroupEntry.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/GroupEntry.java index 0ea685793214e..918843cedd2e2 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/GroupEntry.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/GroupEntry.java @@ -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(); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifInflaterImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifInflaterImpl.java index 4f3c287d5f1e2..4daed77c0954e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifInflaterImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifInflaterImpl.java @@ -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()); } } }; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifPipeline.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifPipeline.kt index 9ae9fe5089441..e89903603da4f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifPipeline.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifPipeline.kt @@ -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. diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/GroupCountCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/GroupCountCoordinator.kt new file mode 100644 index 0000000000000..82b1268e47977 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/GroupCountCoordinator.kt @@ -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() + + override fun attach(pipeline: NotifPipeline) { + pipeline.addOnBeforeFinalizeFilterListener(::onBeforeFinalizeFilter) + pipeline.addOnAfterRenderGroupListener(::onAfterRenderGroup) + } + + private fun onBeforeFinalizeFilter(entries: List) { + // save untruncated child counts to our internal map + untruncatedChildCounts.clear() + entries.asSequence().filterIsInstance().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) + } +} \ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.kt index dae76f810bad7..b3458ae7e7384 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.kt @@ -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 } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinator.java index 644f248fca008..a0379f46a2a92 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinator.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinator.java @@ -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(); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RemoteInputCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RemoteInputCoordinator.kt index 3397815f008f6..2608c30e33c59 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RemoteInputCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RemoteInputCoordinator.kt @@ -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, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinator.kt new file mode 100644 index 0000000000000..38f11fc88b724 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinator.kt @@ -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, controller: NotifStackController) { + controller.setNotifStats(calculateNotifStats(entries)) + notificationIconAreaController.updateNotificationIcons(entries) + } + + private fun calculateNotifStats(entries: List): 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 + } +} \ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifInflater.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifInflater.kt index c59f18436b74a..d98e7f76a11bf 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifInflater.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifInflater.kt @@ -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) } /** diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/init/NotifPipelineInitializer.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/init/NotifPipelineInitializer.java index a8f3730b1cdda..f9358eb2df33c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/init/NotifPipelineInitializer.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/init/NotifPipelineInitializer.java @@ -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); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/OnAfterRenderEntryListener.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/OnAfterRenderEntryListener.java new file mode 100644 index 0000000000000..20cd6dd3614cf --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/OnAfterRenderEntryListener.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.notification.collection.listbuilder; + +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); +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/OnAfterRenderGroupListener.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/OnAfterRenderGroupListener.java new file mode 100644 index 0000000000000..b1a4b653c56c5 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/OnAfterRenderGroupListener.java @@ -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); +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/OnAfterRenderListListener.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/OnAfterRenderListListener.java new file mode 100644 index 0000000000000..b5a0f7ae169d1 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/OnAfterRenderListListener.java @@ -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 entries, + @NonNull NotifStackController controller); +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NodeSpecBuilder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NodeSpecBuilder.kt index 8182e73ab86b2..f59e4ab2007b5 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NodeSpecBuilder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NodeSpecBuilder.kt @@ -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") } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NotifGroupController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NotifGroupController.kt new file mode 100644 index 0000000000000..e2edc01f0d7c7 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NotifGroupController.kt @@ -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) +} \ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NotifRowController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NotifRowController.kt new file mode 100644 index 0000000000000..cfd09f4ad8a7c --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NotifRowController.kt @@ -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 \ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NotifStackController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NotifStackController.kt new file mode 100644 index 0000000000000..b6278d1d5f01b --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NotifStackController.kt @@ -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) {} +} \ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NotifViewBarn.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NotifViewBarn.kt index c79f59b5c6250..fd91d5a2c082b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NotifViewBarn.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NotifViewBarn.kt @@ -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() + private val rowMap = mutableMapOf() - 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) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NotifViewController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NotifViewController.kt new file mode 100644 index 0000000000000..11d4e83f1d381 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NotifViewController.kt @@ -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 \ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NotifViewRenderer.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NotifViewRenderer.kt new file mode 100644 index 0000000000000..1ea574b2f3864 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NotifViewRenderer.kt @@ -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) + + /** + * 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() {} +} \ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/RenderExtensions.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/RenderExtensions.kt new file mode 100644 index 0000000000000..6e7f415747438 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/RenderExtensions.kt @@ -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" } \ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/RenderStageManager.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/RenderStageManager.kt new file mode 100644 index 0000000000000..a9c3987261386 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/RenderStageManager.kt @@ -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() + private val onAfterRenderGroupListeners = mutableListOf() + private val onAfterRenderEntryListeners = mutableListOf() + 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) { + 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 + ) { + traceSection("RenderStageManager.dispatchOnAfterRenderList") { + val stackController = viewRenderer.getStackController() + onAfterRenderListListeners.forEach { listener -> + listener.onAfterRenderList(entries, stackController) + } + } + } + + private fun dispatchOnAfterRenderGroups( + viewRenderer: NotifViewRenderer, + entries: List + ) { + traceSection("RenderStageManager.dispatchOnAfterRenderGroups") { + if (onAfterRenderGroupListeners.isEmpty()) { + return + } + entries.asSequence().filterIsInstance().forEach { group -> + val controller = viewRenderer.getGroupController(group) + onAfterRenderGroupListeners.forEach { listener -> + listener.onAfterRenderGroup(group, controller) + } + } + } + } + + private fun dispatchOnAfterRenderEntries( + viewRenderer: NotifViewRenderer, + entries: List + ) { + 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.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") + } + } + } +} \ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewManager.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewManager.kt index b582a24f5a3e0..1a8d720a12c63 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewManager.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewManager.kt @@ -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) { - 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) { - traceSection("ShadeViewManager.updateGroupCounts") { - notifList.asSequence().filterIsInstance().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) { + 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) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsController.kt index 2f496dd55b2b7..a59d4211e68a2 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsController.kt @@ -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, presenter: NotificationPresenter, listContainer: NotificationListContainer, + stackController: NotifStackController, notificationActivityStarter: NotificationActivityStarter, bindRowCallback: NotificationRowBinderImpl.BindRowCallback ) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt index 9411de75ba7f6..41294da03ada0 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt @@ -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, 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()) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerStub.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerStub.kt index eadf5ac735fcb..1c9af11b7816e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerStub.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerStub.kt @@ -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, presenter: NotificationPresenter, listContainer: NotificationListContainer, + stackController: NotifStackController, notificationActivityStarter: NotificationActivityStarter, bindRowCallback: NotificationRowBinderImpl.BindRowCallback ) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java index 0b29ae57510cf..b41b2958a466e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java @@ -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 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"); + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java index a97a54a50da5b..33ce32a56f922 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java @@ -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())) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java index f7a3e3c1f3ba4..dd68f94111c00 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java @@ -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(); + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java index 94b010df22185..c40b680abfd73 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java @@ -1488,6 +1488,7 @@ public class StatusBar extends CoreStartable implements mBubblesOptional, mPresenter, mStackScrollerController.getNotificationListContainer(), + mStackScrollerController.getNotifStackController(), mNotificationActivityStarter, mPresenter); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java index 9682c605e1e6e..c8e1cdc752420 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java @@ -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); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NonPhoneDependencyTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NonPhoneDependencyTest.java index b0f2a89028701..4213b070d0c27 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NonPhoneDependencyTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NonPhoneDependencyTest.java @@ -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)); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationViewHierarchyManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationViewHierarchyManagerTest.java index 3972f140b665c..83f1d87133c6c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationViewHierarchyManagerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationViewHierarchyManagerTest.java @@ -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 { diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotifPipelineTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotifPipelineTest.kt index cf7174eea7cfa..287f50c4202ac 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotifPipelineTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotifPipelineTest.kt @@ -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(), diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/GroupCountCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/GroupCountCoordinatorTest.kt new file mode 100644 index 0000000000000..929c3d4288d45 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/GroupCountCoordinatorTest.kt @@ -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)) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorTest.java index c3e10aa3178ff..287bfa4de7418 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorTest.java @@ -85,7 +85,6 @@ public class PreparationCoordinatorTest extends SysuiTestCase { @Captor private ArgumentCaptor mCollectionListenerCaptor; @Captor private ArgumentCaptor mBeforeFilterListenerCaptor; - @Captor private ArgumentCaptor mCallbackCaptor; @Captor private ArgumentCaptor 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 entries) { diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinatorTest.kt new file mode 100644 index 0000000000000..70266e401f8af --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinatorTest.kt @@ -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)) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/render/NodeSpecBuilderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/render/NodeSpecBuilderTest.kt index 6313d3a4c2fa5..5271745a2b44f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/render/NodeSpecBuilderTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/render/NodeSpecBuilderTest.kt @@ -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)) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/render/RenderStageManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/render/RenderStageManagerTest.kt new file mode 100644 index 0000000000000..70d309b10338d --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/render/RenderStageManagerTest.kt @@ -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) {} + 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() +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java index 185d9cd8733eb..9be2837160584 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java @@ -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);