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