From 981cca115680f7ba756795367c2f99679e77ef83 Mon Sep 17 00:00:00 2001 From: Steve Elliott Date: Thu, 14 May 2020 11:59:49 -0400 Subject: [PATCH 1/3] Convert NotificationSectionsManager to Kotlin This is a direct conversion, split into a separate CL in order to improve readability of the later CL(s) in this relation chain. There are no functional changes to the code. Bug: 153554168 Test: builds, atest Change-Id: Ic476fbb09d159a17cb811a49002b28b4fa823a57 --- ...NotificationLockscreenUserManagerImpl.java | 4 +- .../NotificationSectionsFeatureManager.kt | 12 +- .../collection/NotificationEntry.java | 8 +- .../collection/NotificationRankingManager.kt | 12 +- .../stack/NotificationSection.java | 8 +- .../stack/NotificationSectionsManager.java | 689 ------------------ .../stack/NotificationSectionsManager.kt | 567 ++++++++++++++ .../stack/NotificationStackScrollLayout.java | 4 +- .../systemui/util/ConvenienceExtensions.kt | 25 + ...NotificationLockscreenUserManagerTest.java | 6 +- .../NotificationRankingManagerTest.kt | 4 +- .../logging/NotificationLoggerTest.java | 2 +- .../NotificationSectionsManagerTest.java | 44 +- 13 files changed, 644 insertions(+), 741 deletions(-) delete mode 100644 packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManager.java create mode 100644 packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManager.kt create mode 100644 packages/SystemUI/src/com/android/systemui/util/ConvenienceExtensions.kt diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java index 2647c04ff586c..2baab61d861b7 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java @@ -19,8 +19,8 @@ import static android.app.Notification.VISIBILITY_SECRET; import static android.app.admin.DevicePolicyManager.ACTION_DEVICE_POLICY_MANAGER_STATE_CHANGED; import static com.android.systemui.DejankUtils.whitelistIpcs; -import static com.android.systemui.statusbar.notification.stack.NotificationSectionsManager.BUCKET_MEDIA_CONTROLS; -import static com.android.systemui.statusbar.notification.stack.NotificationSectionsManager.BUCKET_SILENT; +import static com.android.systemui.statusbar.notification.stack.NotificationSectionsManagerKt.BUCKET_MEDIA_CONTROLS; +import static com.android.systemui.statusbar.notification.stack.NotificationSectionsManagerKt.BUCKET_SILENT; import android.app.ActivityManager; import android.app.KeyguardManager; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManager.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManager.kt index d7b391ff03e49..ce6013f776af2 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManager.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManager.kt @@ -21,12 +21,12 @@ import android.provider.DeviceConfig import com.android.internal.annotations.VisibleForTesting import com.android.internal.config.sysui.SystemUiDeviceConfigFlags.NOTIFICATIONS_USE_PEOPLE_FILTERING -import com.android.systemui.statusbar.notification.stack.NotificationSectionsManager.BUCKET_ALERTING -import com.android.systemui.statusbar.notification.stack.NotificationSectionsManager.BUCKET_FOREGROUND_SERVICE -import com.android.systemui.statusbar.notification.stack.NotificationSectionsManager.BUCKET_HEADS_UP -import com.android.systemui.statusbar.notification.stack.NotificationSectionsManager.BUCKET_MEDIA_CONTROLS -import com.android.systemui.statusbar.notification.stack.NotificationSectionsManager.BUCKET_PEOPLE -import com.android.systemui.statusbar.notification.stack.NotificationSectionsManager.BUCKET_SILENT +import com.android.systemui.statusbar.notification.stack.BUCKET_ALERTING +import com.android.systemui.statusbar.notification.stack.BUCKET_FOREGROUND_SERVICE +import com.android.systemui.statusbar.notification.stack.BUCKET_HEADS_UP +import com.android.systemui.statusbar.notification.stack.BUCKET_MEDIA_CONTROLS +import com.android.systemui.statusbar.notification.stack.BUCKET_PEOPLE +import com.android.systemui.statusbar.notification.stack.BUCKET_SILENT import com.android.systemui.util.DeviceConfigProxy import com.android.systemui.util.Utils diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java index 634872d9d761d..22ac1a2e5cf99 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java @@ -31,7 +31,7 @@ import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_PEEK; import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_STATUS_BAR; import static com.android.systemui.statusbar.notification.collection.NotifCollection.REASON_NOT_CANCELED; -import static com.android.systemui.statusbar.notification.stack.NotificationSectionsManager.BUCKET_ALERTING; +import static com.android.systemui.statusbar.notification.stack.NotificationSectionsManagerKt.BUCKET_ALERTING; import static java.util.Objects.requireNonNull; @@ -68,7 +68,7 @@ import com.android.systemui.statusbar.notification.icon.IconPack; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRowController; import com.android.systemui.statusbar.notification.row.NotificationGuts; -import com.android.systemui.statusbar.notification.stack.NotificationSectionsManager; +import com.android.systemui.statusbar.notification.stack.PriorityBucket; import java.util.ArrayList; import java.util.List; @@ -409,12 +409,12 @@ public final class NotificationEntry extends ListEntry { return wasBubble != isBubble(); } - @NotificationSectionsManager.PriorityBucket + @PriorityBucket public int getBucket() { return mBucket; } - public void setBucket(@NotificationSectionsManager.PriorityBucket int bucket) { + public void setBucket(@PriorityBucket int bucket) { mBucket = bucket; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationRankingManager.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationRankingManager.kt index 9ac42298e539a..5475c2e062d71 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationRankingManager.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationRankingManager.kt @@ -28,12 +28,12 @@ import com.android.systemui.statusbar.notification.NotificationSectionsFeatureMa import com.android.systemui.statusbar.notification.collection.provider.HighPriorityProvider import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier.Companion.TYPE_NON_PERSON -import com.android.systemui.statusbar.notification.stack.NotificationSectionsManager.BUCKET_ALERTING -import com.android.systemui.statusbar.notification.stack.NotificationSectionsManager.BUCKET_FOREGROUND_SERVICE -import com.android.systemui.statusbar.notification.stack.NotificationSectionsManager.BUCKET_HEADS_UP -import com.android.systemui.statusbar.notification.stack.NotificationSectionsManager.BUCKET_PEOPLE -import com.android.systemui.statusbar.notification.stack.NotificationSectionsManager.BUCKET_SILENT -import com.android.systemui.statusbar.notification.stack.NotificationSectionsManager.PriorityBucket +import com.android.systemui.statusbar.notification.stack.BUCKET_ALERTING +import com.android.systemui.statusbar.notification.stack.BUCKET_FOREGROUND_SERVICE +import com.android.systemui.statusbar.notification.stack.BUCKET_HEADS_UP +import com.android.systemui.statusbar.notification.stack.BUCKET_PEOPLE +import com.android.systemui.statusbar.notification.stack.BUCKET_SILENT +import com.android.systemui.statusbar.notification.stack.PriorityBucket import com.android.systemui.statusbar.phone.NotificationGroupManager import com.android.systemui.statusbar.policy.HeadsUpManager import dagger.Lazy diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSection.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSection.java index 9d456ef785a86..bad36bf3de64a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSection.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSection.java @@ -32,8 +32,8 @@ import com.android.systemui.statusbar.notification.row.ActivatableNotificationVi * Represents the bounds of a section of the notification shade and handles animation when the * bounds change. */ -class NotificationSection { - private @NotificationSectionsManager.PriorityBucket int mBucket; +public class NotificationSection { + private @PriorityBucket int mBucket; private View mOwningView; private Rect mBounds = new Rect(); private Rect mCurrentBounds = new Rect(-1, -1, -1, -1); @@ -44,7 +44,7 @@ class NotificationSection { private ActivatableNotificationView mFirstVisibleChild; private ActivatableNotificationView mLastVisibleChild; - NotificationSection(View owningView, @NotificationSectionsManager.PriorityBucket int bucket) { + NotificationSection(View owningView, @PriorityBucket int bucket) { mOwningView = owningView; mBucket = bucket; } @@ -74,7 +74,7 @@ class NotificationSection { return mBottomAnimator != null || mTopAnimator != null; } - @NotificationSectionsManager.PriorityBucket + @PriorityBucket public int getBucket() { return mBucket; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManager.java deleted file mode 100644 index f3ee2ceaf8ee0..0000000000000 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManager.java +++ /dev/null @@ -1,689 +0,0 @@ -/* - * 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.stack; - -import static com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.ROWS_GENTLE; - -import static java.lang.annotation.RetentionPolicy.SOURCE; - -import android.annotation.ColorInt; -import android.annotation.IntDef; -import android.annotation.LayoutRes; -import android.annotation.NonNull; -import android.annotation.Nullable; -import android.content.Intent; -import android.provider.Settings; -import android.view.LayoutInflater; -import android.view.View; - -import com.android.internal.annotations.VisibleForTesting; -import com.android.systemui.R; -import com.android.systemui.media.KeyguardMediaController; -import com.android.systemui.plugins.ActivityStarter; -import com.android.systemui.plugins.statusbar.StatusBarStateController; -import com.android.systemui.statusbar.StatusBarState; -import com.android.systemui.statusbar.notification.NotificationSectionsFeatureManager; -import com.android.systemui.statusbar.notification.people.DataListener; -import com.android.systemui.statusbar.notification.people.PeopleHubViewAdapter; -import com.android.systemui.statusbar.notification.people.PeopleHubViewBoundary; -import com.android.systemui.statusbar.notification.people.PersonViewModel; -import com.android.systemui.statusbar.notification.people.Subscription; -import com.android.systemui.statusbar.notification.row.ActivatableNotificationView; -import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; -import com.android.systemui.statusbar.notification.row.ExpandableView; -import com.android.systemui.statusbar.notification.row.StackScrollerDecorView; -import com.android.systemui.statusbar.policy.ConfigurationController; -import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener; - -import java.lang.annotation.Retention; -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; - -import javax.inject.Inject; - -import kotlin.sequences.Sequence; - -/** - * Manages the boundaries of the two notification sections (high priority and low priority). Also - * shows/hides the headers for those sections where appropriate. - * - * TODO: Move remaining sections logic from NSSL into this class. - */ -public class NotificationSectionsManager implements StackScrollAlgorithm.SectionProvider { - - private static final String TAG = "NotifSectionsManager"; - private static final boolean DEBUG = false; - private static final boolean ENABLE_SNOOZED_CONVERSATION_HUB = false; - - private final ActivityStarter mActivityStarter; - private final StatusBarStateController mStatusBarStateController; - private final ConfigurationController mConfigurationController; - private final PeopleHubViewAdapter mPeopleHubViewAdapter; - private final NotificationSectionsFeatureManager mSectionsFeatureManager; - private final KeyguardMediaController mKeyguardMediaController; - private final int mNumberOfSections; - private final NotificationSectionsLogger mLogger; - private final PeopleHubViewBoundary mPeopleHubViewBoundary = new PeopleHubViewBoundary() { - @Override - public void setVisible(boolean isVisible) { - if (mPeopleHubVisible != isVisible) { - mPeopleHubVisible = isVisible; - if (mInitialized) { - updateSectionBoundaries("PeopleHub visibility changed"); - } - } - } - - @NonNull - @Override - public View getAssociatedViewForClickAnimation() { - return mPeopleHubView; - } - - @NonNull - @Override - public Sequence> getPersonViewAdapters() { - return mPeopleHubView.getPersonViewAdapters(); - } - }; - - private NotificationStackScrollLayout mParent; - private boolean mInitialized = false; - - private SectionHeaderView mGentleHeader; - @Nullable private View.OnClickListener mOnClearGentleNotifsClickListener; - - private SectionHeaderView mAlertingHeader; - private SectionHeaderView mIncomingHeader; - - private PeopleHubView mPeopleHubView; - private boolean mPeopleHubVisible = false; - @Nullable private Subscription mPeopleHubSubscription; - - private MediaHeaderView mMediaControlsView; - - @Inject - NotificationSectionsManager( - ActivityStarter activityStarter, - StatusBarStateController statusBarStateController, - ConfigurationController configurationController, - PeopleHubViewAdapter peopleHubViewAdapter, - KeyguardMediaController keyguardMediaController, - NotificationSectionsFeatureManager sectionsFeatureManager, - NotificationSectionsLogger logger) { - - mActivityStarter = activityStarter; - mStatusBarStateController = statusBarStateController; - mConfigurationController = configurationController; - mPeopleHubViewAdapter = peopleHubViewAdapter; - mSectionsFeatureManager = sectionsFeatureManager; - mNumberOfSections = mSectionsFeatureManager.getNumberOfBuckets(); - mKeyguardMediaController = keyguardMediaController; - mLogger = logger; - } - - NotificationSection[] createSectionsForBuckets() { - int[] buckets = mSectionsFeatureManager.getNotificationBuckets(); - NotificationSection[] sections = new NotificationSection[buckets.length]; - for (int i = 0; i < buckets.length; i++) { - sections[i] = new NotificationSection(mParent, buckets[i] /* bucket */); - } - - return sections; - } - - /** Must be called before use. */ - void initialize( - NotificationStackScrollLayout parent, LayoutInflater layoutInflater) { - if (mInitialized) { - throw new IllegalStateException("NotificationSectionsManager already initialized"); - } - mInitialized = true; - mParent = parent; - reinflateViews(layoutInflater); - mConfigurationController.addCallback(mConfigurationListener); - } - - private T reinflateView( - T view, LayoutInflater layoutInflater, @LayoutRes int layoutResId) { - int oldPos = -1; - if (view != null) { - if (view.getTransientContainer() != null) { - view.getTransientContainer().removeView(mGentleHeader); - } else if (view.getParent() != null) { - oldPos = mParent.indexOfChild(view); - mParent.removeView(view); - } - } - - view = (T) layoutInflater.inflate(layoutResId, mParent, false); - - if (oldPos != -1) { - mParent.addView(view, oldPos); - } - - return view; - } - - /** - * Reinflates the entire notification header, including all decoration views. - */ - void reinflateViews(LayoutInflater layoutInflater) { - mGentleHeader = reinflateView( - mGentleHeader, layoutInflater, R.layout.status_bar_notification_section_header); - mGentleHeader.setHeaderText(R.string.notification_section_header_gentle); - mGentleHeader.setOnHeaderClickListener(this::onGentleHeaderClick); - mGentleHeader.setOnClearAllClickListener(this::onClearGentleNotifsClick); - - mAlertingHeader = reinflateView( - mAlertingHeader, layoutInflater, R.layout.status_bar_notification_section_header); - mAlertingHeader.setHeaderText(R.string.notification_section_header_alerting); - mAlertingHeader.setOnHeaderClickListener(this::onGentleHeaderClick); - - if (mPeopleHubSubscription != null) { - mPeopleHubSubscription.unsubscribe(); - } - mPeopleHubView = reinflateView(mPeopleHubView, layoutInflater, R.layout.people_strip); - if (ENABLE_SNOOZED_CONVERSATION_HUB) { - mPeopleHubSubscription = mPeopleHubViewAdapter.bindView(mPeopleHubViewBoundary); - } - - mIncomingHeader = reinflateView( - mIncomingHeader, layoutInflater, R.layout.status_bar_notification_section_header); - mIncomingHeader.setHeaderText(R.string.notification_section_header_incoming); - mIncomingHeader.setOnHeaderClickListener(this::onGentleHeaderClick); - - mMediaControlsView = reinflateView(mMediaControlsView, layoutInflater, - R.layout.keyguard_media_header); - mKeyguardMediaController.attach(mMediaControlsView); - } - - /** Listener for when the "clear all" button is clicked on the gentle notification header. */ - void setOnClearGentleNotifsClickListener(View.OnClickListener listener) { - mOnClearGentleNotifsClickListener = listener; - } - - @Override - public boolean beginsSection(@NonNull View view, @Nullable View previous) { - return view == mGentleHeader - || view == mMediaControlsView - || view == mPeopleHubView - || view == mAlertingHeader - || view == mIncomingHeader - || !Objects.equals(getBucket(view), getBucket(previous)); - } - - private boolean isUsingMultipleSections() { - return mNumberOfSections > 1; - } - - @Nullable - private Integer getBucket(View view) { - if (view == mGentleHeader) { - return BUCKET_SILENT; - } else if (view == mIncomingHeader) { - return BUCKET_HEADS_UP; - } else if (view == mMediaControlsView) { - return BUCKET_MEDIA_CONTROLS; - } else if (view == mPeopleHubView) { - return BUCKET_PEOPLE; - } else if (view == mAlertingHeader) { - return BUCKET_ALERTING; - } else if (view instanceof ExpandableNotificationRow) { - return ((ExpandableNotificationRow) view).getEntry().getBucket(); - } - return null; - } - - private void logShadeContents() { - final int childCount = mParent.getChildCount(); - for (int i = 0; i < childCount; i++) { - View child = mParent.getChildAt(i); - if (child == mIncomingHeader) { - mLogger.logIncomingHeader(i); - continue; - } - if (child == mMediaControlsView) { - mLogger.logMediaControls(i); - continue; - } - if (child == mPeopleHubView) { - mLogger.logConversationsHeader(i); - continue; - } - if (child == mAlertingHeader) { - mLogger.logAlertingHeader(i); - continue; - } - if (child == mGentleHeader) { - mLogger.logSilentHeader(i); - continue; - } - - if (!(child instanceof ExpandableNotificationRow)) { - mLogger.logOther(i, child.getClass()); - continue; - } - ExpandableNotificationRow row = (ExpandableNotificationRow) child; - // Once we enter a new section, calculate the target position for the header. - switch (row.getEntry().getBucket()) { - case BUCKET_HEADS_UP: - mLogger.logHeadsUp(i); - break; - case BUCKET_PEOPLE: - mLogger.logConversation(i); - break; - case BUCKET_ALERTING: - mLogger.logAlerting(i); - break; - case BUCKET_SILENT: - mLogger.logSilent(i); - break; - } - } - } - - @VisibleForTesting - void updateSectionBoundaries() { - updateSectionBoundaries("test"); - } - - /** - * Should be called whenever notifs are added, removed, or updated. Updates section boundary - * bookkeeping and adds/moves/removes section headers if appropriate. - */ - void updateSectionBoundaries(String reason) { - if (!isUsingMultipleSections()) { - return; - } - - mLogger.logStartSectionUpdate(reason); - - // The overall strategy here is to iterate over the current children of mParent, looking - // for where the sections headers are currently positioned, and where each section begins. - // Then, once we find the start of a new section, we track that position as the "target" for - // the section header, adjusted for the case where existing headers are in front of that - // target, but won't be once they are moved / removed after the pass has completed. - - final boolean showHeaders = mStatusBarStateController.getState() != StatusBarState.KEYGUARD; - final boolean usingPeopleFiltering = mSectionsFeatureManager.isFilteringEnabled(); - final boolean usingMediaControls = mSectionsFeatureManager.isMediaControlsEnabled(); - - boolean peopleNotifsPresent = false; - - int currentMediaControlsIdx = -1; - int mediaControlsTarget = usingMediaControls ? 0 : -1; - int currentIncomingHeaderIdx = -1; - int incomingHeaderTarget = -1; - int currentPeopleHeaderIdx = -1; - int peopleHeaderTarget = -1; - int currentAlertingHeaderIdx = -1; - int alertingHeaderTarget = -1; - int currentGentleHeaderIdx = -1; - int gentleHeaderTarget = -1; - - int lastNotifIndex = 0; - - final int childCount = mParent.getChildCount(); - for (int i = 0; i < childCount; i++) { - View child = mParent.getChildAt(i); - - // Track the existing positions of the headers - if (child == mIncomingHeader) { - mLogger.logIncomingHeader(i); - currentIncomingHeaderIdx = i; - continue; - } - if (child == mMediaControlsView) { - mLogger.logMediaControls(i); - currentMediaControlsIdx = i; - continue; - } - if (child == mPeopleHubView) { - mLogger.logConversationsHeader(i); - currentPeopleHeaderIdx = i; - continue; - } - if (child == mAlertingHeader) { - mLogger.logAlertingHeader(i); - currentAlertingHeaderIdx = i; - continue; - } - if (child == mGentleHeader) { - mLogger.logSilentHeader(i); - currentGentleHeaderIdx = i; - continue; - } - - if (!(child instanceof ExpandableNotificationRow)) { - mLogger.logOther(i, child.getClass()); - continue; - } - lastNotifIndex = i; - ExpandableNotificationRow row = (ExpandableNotificationRow) child; - // Once we enter a new section, calculate the target position for the header. - switch (row.getEntry().getBucket()) { - case BUCKET_HEADS_UP: - mLogger.logHeadsUp(i); - if (showHeaders && incomingHeaderTarget == -1) { - incomingHeaderTarget = i; - // Offset the target if there are other headers before this that will be - // moved. - if (currentIncomingHeaderIdx != -1) { - incomingHeaderTarget--; - } - if (currentMediaControlsIdx != -1) { - incomingHeaderTarget--; - } - if (currentPeopleHeaderIdx != -1) { - incomingHeaderTarget--; - } - if (currentAlertingHeaderIdx != -1) { - incomingHeaderTarget--; - } - if (currentGentleHeaderIdx != -1) { - incomingHeaderTarget--; - } - } - if (mediaControlsTarget != -1) { - mediaControlsTarget++; - } - break; - case BUCKET_FOREGROUND_SERVICE: - mLogger.logForegroundService(i); - if (mediaControlsTarget != -1) { - mediaControlsTarget++; - } - break; - case BUCKET_PEOPLE: - mLogger.logConversation(i); - peopleNotifsPresent = true; - if (showHeaders && peopleHeaderTarget == -1) { - peopleHeaderTarget = i; - // Offset the target if there are other headers before this that will be - // moved. - if (currentPeopleHeaderIdx != -1) { - peopleHeaderTarget--; - } - if (currentAlertingHeaderIdx != -1) { - peopleHeaderTarget--; - } - if (currentGentleHeaderIdx != -1) { - peopleHeaderTarget--; - } - } - break; - case BUCKET_ALERTING: - mLogger.logAlerting(i); - if (showHeaders && usingPeopleFiltering && alertingHeaderTarget == -1) { - alertingHeaderTarget = i; - // Offset the target if there are other headers before this that will be - // moved. - if (currentAlertingHeaderIdx != -1) { - alertingHeaderTarget--; - } - if (currentGentleHeaderIdx != -1) { - alertingHeaderTarget--; - } - } - break; - case BUCKET_SILENT: - mLogger.logSilent(i); - if (showHeaders && gentleHeaderTarget == -1) { - gentleHeaderTarget = i; - // Offset the target if there are other headers before this that will be - // moved. - if (currentGentleHeaderIdx != -1) { - gentleHeaderTarget--; - } - } - break; - default: - throw new IllegalStateException("Cannot find section bucket for view"); - } - } - if (showHeaders && usingPeopleFiltering && mPeopleHubVisible && peopleHeaderTarget == -1) { - // Insert the people header even if there are no people visible, in order to show - // the hub. Put it directly above the next header. - if (alertingHeaderTarget != -1) { - peopleHeaderTarget = alertingHeaderTarget; - } else if (gentleHeaderTarget != -1) { - peopleHeaderTarget = gentleHeaderTarget; - } else { - // Put it at the end of the list. - peopleHeaderTarget = lastNotifIndex; - } - // Offset the target to account for the current position of the people header. - if (currentPeopleHeaderIdx != -1 && currentPeopleHeaderIdx < peopleHeaderTarget) { - peopleHeaderTarget--; - } - } - - mLogger.logStr("New header target positions:"); - mLogger.logIncomingHeader(incomingHeaderTarget); - mLogger.logMediaControls(mediaControlsTarget); - mLogger.logConversationsHeader(peopleHeaderTarget); - mLogger.logAlertingHeader(alertingHeaderTarget); - mLogger.logSilentHeader(gentleHeaderTarget); - - // Add headers in reverse order to preserve indices - adjustHeaderVisibilityAndPosition( - gentleHeaderTarget, mGentleHeader, currentGentleHeaderIdx); - adjustHeaderVisibilityAndPosition( - alertingHeaderTarget, mAlertingHeader, currentAlertingHeaderIdx); - adjustHeaderVisibilityAndPosition( - peopleHeaderTarget, mPeopleHubView, currentPeopleHeaderIdx); - adjustViewPosition(mediaControlsTarget, mMediaControlsView, currentMediaControlsIdx); - adjustHeaderVisibilityAndPosition(incomingHeaderTarget, mIncomingHeader, - currentIncomingHeaderIdx); - - mLogger.logStr("Final order:"); - logShadeContents(); - mLogger.logStr("Section boundary update complete"); - - // Update headers to reflect state of section contents - mGentleHeader.setAreThereDismissableGentleNotifs( - mParent.hasActiveClearableNotifications(ROWS_GENTLE)); - mPeopleHubView.setCanSwipe(showHeaders && mPeopleHubVisible && !peopleNotifsPresent); - if (peopleHeaderTarget != currentPeopleHeaderIdx) { - mPeopleHubView.resetTranslation(); - } - } - - private void adjustHeaderVisibilityAndPosition( - int targetPosition, StackScrollerDecorView header, int currentPosition) { - adjustViewPosition(targetPosition, header, currentPosition); - if (targetPosition != -1 && currentPosition == -1) { - header.setContentVisible(true); - } - } - - private void adjustViewPosition(int targetPosition, ExpandableView view, int currentPosition) { - if (targetPosition == -1) { - if (currentPosition != -1) { - mParent.removeView(view); - } - } else { - if (currentPosition == -1) { - // If the header is animating away, it will still have a parent, so detach it first - // TODO: We should really cancel the active animations here. This will happen - // automatically when the view's intro animation starts, but it's a fragile link. - if (view.getTransientContainer() != null) { - view.getTransientContainer().removeTransientView(view); - view.setTransientContainer(null); - } - mParent.addView(view, targetPosition); - } else { - mParent.changeViewPosition(view, targetPosition); - } - } - } - - /** - * Updates the boundaries (as tracked by their first and last views) of the priority sections. - * - * @return {@code true} If the last view in the top section changed (so we need to animate). - */ - boolean updateFirstAndLastViewsForAllSections( - NotificationSection[] sections, - List children) { - - if (sections.length <= 0 || children.size() <= 0) { - for (NotificationSection s : sections) { - s.setFirstVisibleChild(null); - s.setLastVisibleChild(null); - } - return false; - } - - boolean changed = false; - ArrayList viewsInBucket = new ArrayList<>(); - for (NotificationSection s : sections) { - int filter = s.getBucket(); - viewsInBucket.clear(); - - //TODO: do this in a single pass, and more better - for (ActivatableNotificationView v : children) { - Integer bucket = getBucket(v); - if (bucket == null) { - throw new IllegalArgumentException("Cannot find section bucket for view"); - } - - if (bucket == filter) { - viewsInBucket.add(v); - } - - if (viewsInBucket.size() >= 1) { - changed |= s.setFirstVisibleChild(viewsInBucket.get(0)); - changed |= s.setLastVisibleChild(viewsInBucket.get(viewsInBucket.size() - 1)); - } else { - changed |= s.setFirstVisibleChild(null); - changed |= s.setLastVisibleChild(null); - } - } - } - - if (DEBUG) { - logSections(sections); - } - - return changed; - } - - private void logSections(NotificationSection[] sections) { - for (int i = 0; i < sections.length; i++) { - NotificationSection s = sections[i]; - ActivatableNotificationView first = s.getFirstVisibleChild(); - String fs = first == null ? "(null)" - : (first instanceof ExpandableNotificationRow) - ? ((ExpandableNotificationRow) first).getEntry().getKey() - : Integer.toHexString(System.identityHashCode(first)); - ActivatableNotificationView last = s.getLastVisibleChild(); - String ls = last == null ? "(null)" - : (last instanceof ExpandableNotificationRow) - ? ((ExpandableNotificationRow) last).getEntry().getKey() - : Integer.toHexString(System.identityHashCode(last)); - android.util.Log.d(TAG, "updateSections: f=" + fs + " s=" + i); - android.util.Log.d(TAG, "updateSections: l=" + ls + " s=" + i); - } - } - - @VisibleForTesting - ExpandableView getGentleHeaderView() { - return mGentleHeader; - } - - @VisibleForTesting - ExpandableView getAlertingHeaderView() { - return mAlertingHeader; - } - - @VisibleForTesting - ExpandableView getPeopleHeaderView() { - return mPeopleHubView; - } - - @VisibleForTesting - ExpandableView getMediaControlsView() { - return mMediaControlsView; - } - - @VisibleForTesting - ExpandableView getIncomingHeaderView() { - return mIncomingHeader; - } - - @VisibleForTesting - void setPeopleHubVisible(boolean visible) { - mPeopleHubVisible = visible; - } - - private final ConfigurationListener mConfigurationListener = new ConfigurationListener() { - @Override - public void onLocaleListChanged() { - reinflateViews(LayoutInflater.from(mParent.getContext())); - } - }; - - private void onGentleHeaderClick(View v) { - Intent intent = new Intent(Settings.ACTION_NOTIFICATION_SETTINGS); - mActivityStarter.startActivity( - intent, - true, - true, - Intent.FLAG_ACTIVITY_SINGLE_TOP); - } - - private void onClearGentleNotifsClick(View v) { - if (mOnClearGentleNotifsClickListener != null) { - mOnClearGentleNotifsClickListener.onClick(v); - } - } - - void hidePeopleRow() { - mPeopleHubVisible = false; - updateSectionBoundaries("PeopleHub dismissed"); - } - - void setHeaderForegroundColor(@ColorInt int color) { - mPeopleHubView.setTextColor(color); - mGentleHeader.setForegroundColor(color); - mAlertingHeader.setForegroundColor(color); - } - - /** - * For now, declare the available notification buckets (sections) here so that other - * presentation code can decide what to do based on an entry's buckets - */ - @Retention(SOURCE) - @IntDef(prefix = { "BUCKET_" }, value = { - BUCKET_HEADS_UP, - BUCKET_FOREGROUND_SERVICE, - BUCKET_MEDIA_CONTROLS, - BUCKET_PEOPLE, - BUCKET_ALERTING, - BUCKET_SILENT - }) - public @interface PriorityBucket {} - public static final int BUCKET_HEADS_UP = 0; - public static final int BUCKET_FOREGROUND_SERVICE = 1; - public static final int BUCKET_MEDIA_CONTROLS = 2; - public static final int BUCKET_PEOPLE = 3; - public static final int BUCKET_ALERTING = 4; - public static final int BUCKET_SILENT = 5; -} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManager.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManager.kt new file mode 100644 index 0000000000000..a83c2b321cc16 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManager.kt @@ -0,0 +1,567 @@ +/* + * 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.stack + +import android.annotation.ColorInt +import android.annotation.IntDef +import android.annotation.LayoutRes +import android.content.Intent +import android.provider.Settings +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import com.android.internal.annotations.VisibleForTesting +import com.android.systemui.R +import com.android.systemui.media.KeyguardMediaController +import com.android.systemui.plugins.ActivityStarter +import com.android.systemui.plugins.statusbar.StatusBarStateController +import com.android.systemui.statusbar.StatusBarState +import com.android.systemui.statusbar.notification.NotificationSectionsFeatureManager +import com.android.systemui.statusbar.notification.people.DataListener +import com.android.systemui.statusbar.notification.people.PeopleHubViewAdapter +import com.android.systemui.statusbar.notification.people.PeopleHubViewBoundary +import com.android.systemui.statusbar.notification.people.PersonViewModel +import com.android.systemui.statusbar.notification.people.Subscription +import com.android.systemui.statusbar.notification.row.ActivatableNotificationView +import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow +import com.android.systemui.statusbar.notification.row.ExpandableView +import com.android.systemui.statusbar.notification.row.StackScrollerDecorView +import com.android.systemui.statusbar.notification.stack.StackScrollAlgorithm.SectionProvider +import com.android.systemui.statusbar.policy.ConfigurationController +import com.android.systemui.util.children +import javax.inject.Inject + +/** + * Manages the boundaries of the two notification sections (high priority and low priority). Also + * shows/hides the headers for those sections where appropriate. + * + * TODO: Move remaining sections logic from NSSL into this class. + */ +class NotificationSectionsManager @Inject internal constructor( + private val activityStarter: ActivityStarter, + private val statusBarStateController: StatusBarStateController, + private val configurationController: ConfigurationController, + private val peopleHubViewAdapter: PeopleHubViewAdapter, + private val keyguardMediaController: KeyguardMediaController, + private val sectionsFeatureManager: NotificationSectionsFeatureManager, + private val logger: NotificationSectionsLogger +) : SectionProvider { + + private val configurationListener = object : ConfigurationController.ConfigurationListener { + override fun onLocaleListChanged() { + reinflateViews(LayoutInflater.from(parent.context)) + } + } + + private val peopleHubViewBoundary: PeopleHubViewBoundary = object : PeopleHubViewBoundary { + override fun setVisible(isVisible: Boolean) { + if (peopleHubVisible != isVisible) { + peopleHubVisible = isVisible + if (initialized) { + updateSectionBoundaries("PeopleHub visibility changed") + } + } + } + + override val associatedViewForClickAnimation: View + get() = peopleHeaderView!! + + override val personViewAdapters: Sequence> + get() = peopleHeaderView!!.personViewAdapters + } + + private lateinit var parent: NotificationStackScrollLayout + private var initialized = false + private var onClearSilentNotifsClickListener: View.OnClickListener? = null + + @get:VisibleForTesting + var silentHeaderView: SectionHeaderView? = null + private set + + @get:VisibleForTesting + var alertingHeaderView: SectionHeaderView? = null + private set + + @get:VisibleForTesting + var incomingHeaderView: SectionHeaderView? = null + private set + + @get:VisibleForTesting + var peopleHeaderView: PeopleHubView? = null + private set + + @set:VisibleForTesting + var peopleHubVisible = false + private var peopleHubSubscription: Subscription? = null + + @get:VisibleForTesting + var mediaControlsView: MediaHeaderView? = null + private set + + /** Must be called before use. */ + fun initialize(parent: NotificationStackScrollLayout, layoutInflater: LayoutInflater) { + check(!initialized) { "NotificationSectionsManager already initialized" } + initialized = true + this.parent = parent + reinflateViews(layoutInflater) + configurationController.addCallback(configurationListener) + } + + private fun reinflateView( + view: T?, + layoutInflater: LayoutInflater, + @LayoutRes layoutResId: Int + ): T { + var oldPos = -1 + view?.let { + view.transientContainer?.removeView(view) + if (view.parent === parent) { + oldPos = parent.indexOfChild(view) + parent.removeView(view) + } + } + val inflated = layoutInflater.inflate(layoutResId, parent, false) as T + if (oldPos != -1) { + parent.addView(inflated, oldPos) + } + return inflated + } + + fun createSectionsForBuckets(): Array = + sectionsFeatureManager.getNotificationBuckets() + .map { NotificationSection(parent, it) } + .toTypedArray() + + /** + * Reinflates the entire notification header, including all decoration views. + */ + fun reinflateViews(layoutInflater: LayoutInflater) { + silentHeaderView = reinflateView( + silentHeaderView, layoutInflater, R.layout.status_bar_notification_section_header + ).apply { + setHeaderText(R.string.notification_section_header_gentle) + setOnHeaderClickListener { onGentleHeaderClick() } + setOnClearAllClickListener { onClearGentleNotifsClick(it) } + } + alertingHeaderView = reinflateView( + alertingHeaderView, layoutInflater, R.layout.status_bar_notification_section_header + ).apply { + setHeaderText(R.string.notification_section_header_alerting) + setOnHeaderClickListener { onGentleHeaderClick() } + } + peopleHubSubscription?.unsubscribe() + peopleHubSubscription = null + peopleHeaderView = reinflateView(peopleHeaderView, layoutInflater, R.layout.people_strip) + if (ENABLE_SNOOZED_CONVERSATION_HUB) { + peopleHubSubscription = peopleHubViewAdapter.bindView(peopleHubViewBoundary) + } + incomingHeaderView = reinflateView( + incomingHeaderView, layoutInflater, R.layout.status_bar_notification_section_header + ).apply { + setHeaderText(R.string.notification_section_header_incoming) + setOnHeaderClickListener { onGentleHeaderClick() } + } + mediaControlsView = + reinflateView(mediaControlsView, layoutInflater, R.layout.keyguard_media_header) + .also(keyguardMediaController::attach) + } + + override fun beginsSection(view: View, previous: View?): Boolean = + view === silentHeaderView || + view === mediaControlsView || + view === peopleHeaderView || + view === alertingHeaderView || + view === incomingHeaderView || + getBucket(view) != getBucket(previous) + + private fun getBucket(view: View?): Int? = when { + view === silentHeaderView -> BUCKET_SILENT + view === incomingHeaderView -> BUCKET_HEADS_UP + view === mediaControlsView -> BUCKET_MEDIA_CONTROLS + view === peopleHeaderView -> BUCKET_PEOPLE + view === alertingHeaderView -> BUCKET_ALERTING + view is ExpandableNotificationRow -> view.entry.bucket + else -> null + } + + private fun logShadeContents() = parent.children.forEachIndexed { i, child -> + when { + child === incomingHeaderView -> logger.logIncomingHeader(i) + child === mediaControlsView -> logger.logMediaControls(i) + child === peopleHeaderView -> logger.logConversationsHeader(i) + child === alertingHeaderView -> logger.logAlertingHeader(i) + child === silentHeaderView -> logger.logSilentHeader(i) + child !is ExpandableNotificationRow -> logger.logOther(i, child.javaClass) + else -> when (child.entry.bucket) { + BUCKET_HEADS_UP -> logger.logHeadsUp(i) + BUCKET_PEOPLE -> logger.logConversation(i) + BUCKET_ALERTING -> logger.logAlerting(i) + BUCKET_SILENT -> logger.logSilent(i) + } + } + } + + private val isUsingMultipleSections: Boolean + get() = sectionsFeatureManager.getNumberOfBuckets() > 1 + + @VisibleForTesting + fun updateSectionBoundaries() = updateSectionBoundaries("test") + + /** + * Should be called whenever notifs are added, removed, or updated. Updates section boundary + * bookkeeping and adds/moves/removes section headers if appropriate. + */ + fun updateSectionBoundaries(reason: String) { + if (!isUsingMultipleSections) { + return + } + logger.logStartSectionUpdate(reason) + + // The overall strategy here is to iterate over the current children of mParent, looking + // for where the sections headers are currently positioned, and where each section begins. + // Then, once we find the start of a new section, we track that position as the "target" for + // the section header, adjusted for the case where existing headers are in front of that + // target, but won't be once they are moved / removed after the pass has completed. + val showHeaders = statusBarStateController.state != StatusBarState.KEYGUARD + val usingPeopleFiltering = sectionsFeatureManager.isFilteringEnabled() + val usingMediaControls = sectionsFeatureManager.isMediaControlsEnabled() + + var peopleNotifsPresent = false + var currentMediaControlsIdx = -1 + var mediaControlsTarget = if (usingMediaControls) 0 else -1 + var currentIncomingHeaderIdx = -1 + var incomingHeaderTarget = -1 + var currentPeopleHeaderIdx = -1 + var peopleHeaderTarget = -1 + var currentAlertingHeaderIdx = -1 + var alertingHeaderTarget = -1 + var currentGentleHeaderIdx = -1 + var gentleHeaderTarget = -1 + + var lastNotifIndex = 0 + + parent.children.forEachIndexed { i, child -> + // Track the existing positions of the headers + when { + child === incomingHeaderView -> { + logger.logIncomingHeader(i) + currentIncomingHeaderIdx = i + } + child === mediaControlsView -> { + logger.logMediaControls(i) + currentMediaControlsIdx = i + } + child === peopleHeaderView -> { + logger.logConversationsHeader(i) + currentPeopleHeaderIdx = i + } + child === alertingHeaderView -> { + logger.logAlertingHeader(i) + currentAlertingHeaderIdx = i + } + child === silentHeaderView -> { + logger.logSilentHeader(i) + currentGentleHeaderIdx = i + } + child !is ExpandableNotificationRow -> logger.logOther(i, child.javaClass) + else -> { + lastNotifIndex = i + when (child.entry.bucket) { + BUCKET_HEADS_UP -> { + logger.logHeadsUp(i) + if (showHeaders && incomingHeaderTarget == -1) { + incomingHeaderTarget = i + // Offset the target if there are other headers before this that + // will be moved. + if (currentIncomingHeaderIdx != -1) { + incomingHeaderTarget-- + } + if (currentMediaControlsIdx != -1) { + incomingHeaderTarget-- + } + if (currentPeopleHeaderIdx != -1) { + incomingHeaderTarget-- + } + if (currentAlertingHeaderIdx != -1) { + incomingHeaderTarget-- + } + if (currentGentleHeaderIdx != -1) { + incomingHeaderTarget-- + } + } + if (mediaControlsTarget != -1) { + mediaControlsTarget++ + } + } + BUCKET_FOREGROUND_SERVICE -> { + logger.logForegroundService(i) + if (mediaControlsTarget != -1) { + mediaControlsTarget++ + } + } + BUCKET_PEOPLE -> { + logger.logConversation(i) + peopleNotifsPresent = true + if (showHeaders && peopleHeaderTarget == -1) { + peopleHeaderTarget = i + // Offset the target if there are other headers before this that + // will be moved. + if (currentPeopleHeaderIdx != -1) { + peopleHeaderTarget-- + } + if (currentAlertingHeaderIdx != -1) { + peopleHeaderTarget-- + } + if (currentGentleHeaderIdx != -1) { + peopleHeaderTarget-- + } + } + } + BUCKET_ALERTING -> { + logger.logAlerting(i) + if (showHeaders && usingPeopleFiltering && alertingHeaderTarget == -1) { + alertingHeaderTarget = i + // Offset the target if there are other headers before this that + // will be moved. + if (currentAlertingHeaderIdx != -1) { + alertingHeaderTarget-- + } + if (currentGentleHeaderIdx != -1) { + alertingHeaderTarget-- + } + } + } + BUCKET_SILENT -> { + logger.logSilent(i) + if (showHeaders && gentleHeaderTarget == -1) { + gentleHeaderTarget = i + // Offset the target if there are other headers before this that + // will be moved. + if (currentGentleHeaderIdx != -1) { + gentleHeaderTarget-- + } + } + } + else -> throw IllegalStateException("Cannot find section bucket for view") + } + } + } + } + + if (showHeaders && usingPeopleFiltering && peopleHubVisible && peopleHeaderTarget == -1) { + // Insert the people header even if there are no people visible, in order to show + // the hub. Put it directly above the next header. + peopleHeaderTarget = when { + alertingHeaderTarget != -1 -> alertingHeaderTarget + gentleHeaderTarget != -1 -> gentleHeaderTarget + else -> lastNotifIndex // Put it at the end of the list. + } + // Offset the target to account for the current position of the people header. + if (currentPeopleHeaderIdx != -1 && currentPeopleHeaderIdx < peopleHeaderTarget) { + peopleHeaderTarget-- + } + } + + logger.logStr("New header target positions:") + logger.logIncomingHeader(incomingHeaderTarget) + logger.logMediaControls(mediaControlsTarget) + logger.logConversationsHeader(peopleHeaderTarget) + logger.logAlertingHeader(alertingHeaderTarget) + logger.logSilentHeader(gentleHeaderTarget) + + // Add headers in reverse order to preserve indices + silentHeaderView?.let { + adjustHeaderVisibilityAndPosition(gentleHeaderTarget, it, currentGentleHeaderIdx) + } + alertingHeaderView?.let { + adjustHeaderVisibilityAndPosition(alertingHeaderTarget, it, currentAlertingHeaderIdx) + } + peopleHeaderView?.let { + adjustHeaderVisibilityAndPosition(peopleHeaderTarget, it, currentPeopleHeaderIdx) + } + mediaControlsView?.let { + adjustViewPosition(mediaControlsTarget, it, currentMediaControlsIdx) + } + incomingHeaderView?.let { + adjustHeaderVisibilityAndPosition(incomingHeaderTarget, it, currentIncomingHeaderIdx) + } + + logger.logStr("Final order:") + logShadeContents() + logger.logStr("Section boundary update complete") + + // Update headers to reflect state of section contents + silentHeaderView?.setAreThereDismissableGentleNotifs( + parent.hasActiveClearableNotifications(NotificationStackScrollLayout.ROWS_GENTLE) + ) + peopleHeaderView?.canSwipe = showHeaders && peopleHubVisible && !peopleNotifsPresent + if (peopleHeaderTarget != currentPeopleHeaderIdx) { + peopleHeaderView?.resetTranslation() + } + } + + private fun adjustHeaderVisibilityAndPosition( + targetPosition: Int, + header: StackScrollerDecorView, + currentPosition: Int + ) { + adjustViewPosition(targetPosition, header, currentPosition) + if (targetPosition != -1 && currentPosition == -1) { + header.isContentVisible = true + } + } + + private fun adjustViewPosition( + targetPosition: Int, + view: ExpandableView, + currentPosition: Int + ) { + if (targetPosition == -1) { + if (currentPosition != -1) { + parent.removeView(view) + } + } else { + if (currentPosition == -1) { + // If the header is animating away, it will still have a parent, so detach it first + // TODO: We should really cancel the active animations here. This will happen + // automatically when the view's intro animation starts, but it's a fragile link. + view.transientContainer?.removeTransientView(view) + view.transientContainer = null + parent.addView(view, targetPosition) + } else { + parent.changeViewPosition(view, targetPosition) + } + } + } + + /** + * Updates the boundaries (as tracked by their first and last views) of the priority sections. + * + * @return `true` If the last view in the top section changed (so we need to animate). + */ + fun updateFirstAndLastViewsForAllSections( + sections: Array, + children: List + ): Boolean { + if (sections.isEmpty() || children.isEmpty()) { + for (s in sections) { + s.firstVisibleChild = null + s.lastVisibleChild = null + } + return false + } + var changed = false + val viewsInBucket = mutableListOf() + for (s in sections) { + val filter = s.bucket + viewsInBucket.clear() + + // TODO: do this in a single pass, and more better + for (v in children) { + val bucket = getBucket(v) + ?: throw IllegalArgumentException("Cannot find section bucket for view") + if (bucket == filter) { + viewsInBucket.add(v) + } + if (viewsInBucket.size >= 1) { + changed = changed or s.setFirstVisibleChild(viewsInBucket[0]) + changed = changed or + s.setLastVisibleChild(viewsInBucket[viewsInBucket.size - 1]) + } else { + changed = changed or s.setFirstVisibleChild(null) + changed = changed or s.setLastVisibleChild(null) + } + } + } + if (DEBUG) { + logSections(sections) + } + return changed + } + + private fun logSections(sections: Array) { + for (i in sections.indices) { + val s = sections[i] + val fs = when (val first = s.firstVisibleChild) { + null -> "(null)" + is ExpandableNotificationRow -> first.entry.key + else -> Integer.toHexString(System.identityHashCode(first)) + } + val ls = when (val last = s.lastVisibleChild) { + null -> "(null)" + is ExpandableNotificationRow -> last.entry.key + else -> Integer.toHexString(System.identityHashCode(last)) + } + Log.d(TAG, "updateSections: f=$fs s=$i") + Log.d(TAG, "updateSections: l=$ls s=$i") + } + } + + private fun onGentleHeaderClick() { + val intent = Intent(Settings.ACTION_NOTIFICATION_SETTINGS) + activityStarter.startActivity( + intent, + true, + true, + Intent.FLAG_ACTIVITY_SINGLE_TOP) + } + + private fun onClearGentleNotifsClick(v: View) { + onClearSilentNotifsClickListener?.onClick(v) + } + + /** Listener for when the "clear all" button is clicked on the gentle notification header. */ + fun setOnClearSilentNotifsClickListener(listener: View.OnClickListener) { + onClearSilentNotifsClickListener = listener + } + + fun hidePeopleRow() { + peopleHubVisible = false + updateSectionBoundaries("PeopleHub dismissed") + } + + fun setHeaderForegroundColor(@ColorInt color: Int) { + peopleHeaderView?.setTextColor(color) + silentHeaderView?.setForegroundColor(color) + alertingHeaderView?.setForegroundColor(color) + } + + companion object { + private const val TAG = "NotifSectionsManager" + private const val DEBUG = false + private const val ENABLE_SNOOZED_CONVERSATION_HUB = false + } +} + +/** + * For now, declare the available notification buckets (sections) here so that other + * presentation code can decide what to do based on an entry's buckets + */ +@Retention(AnnotationRetention.SOURCE) +@IntDef( + prefix = ["BUCKET_"], + value = [ + BUCKET_HEADS_UP, BUCKET_FOREGROUND_SERVICE, BUCKET_MEDIA_CONTROLS, BUCKET_PEOPLE, + BUCKET_ALERTING, BUCKET_SILENT + ] +) +annotation class PriorityBucket +const val BUCKET_HEADS_UP = 0 +const val BUCKET_FOREGROUND_SERVICE = 1 +const val BUCKET_MEDIA_CONTROLS = 2 +const val BUCKET_PEOPLE = 3 +const val BUCKET_ALERTING = 4 +const val BUCKET_SILENT = 5 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 1ccc2bde2288a..3db4b6f7ffbb7 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 @@ -21,7 +21,7 @@ import static android.service.notification.NotificationStats.DISMISS_SENTIMENT_N import static com.android.systemui.Dependency.ALLOW_NOTIFICATION_LONG_PRESS_NAME; import static com.android.systemui.statusbar.notification.ActivityLaunchAnimator.ExpandAnimationParameters; -import static com.android.systemui.statusbar.notification.stack.NotificationSectionsManager.BUCKET_SILENT; +import static com.android.systemui.statusbar.notification.stack.NotificationSectionsManagerKt.BUCKET_SILENT; import static com.android.systemui.statusbar.notification.stack.StackScrollAlgorithm.ANCHOR_SCROLLING; import static com.android.systemui.statusbar.notification.stack.StackStateAnimator.ANIMATION_DURATION_SWIPE; import static com.android.systemui.statusbar.phone.NotificationIconAreaController.HIGH_PRIORITY; @@ -577,7 +577,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements ScrollAd mSectionsManager = notificationSectionsManager; mSectionsManager.initialize(this, LayoutInflater.from(context)); - mSectionsManager.setOnClearGentleNotifsClickListener(v -> { + mSectionsManager.setOnClearSilentNotifsClickListener(v -> { // Leave the shade open if there will be other notifs left over to clear final boolean closeShade = !hasActiveClearableNotifications(ROWS_HIGH_PRIORITY); clearNotifications(ROWS_GENTLE, closeShade); diff --git a/packages/SystemUI/src/com/android/systemui/util/ConvenienceExtensions.kt b/packages/SystemUI/src/com/android/systemui/util/ConvenienceExtensions.kt new file mode 100644 index 0000000000000..c91033e4745a8 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/util/ConvenienceExtensions.kt @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.util + +import android.view.ViewGroup + +/** [Sequence] that yields all of the direct children of this [ViewGroup] */ +val ViewGroup.children + get() = sequence { + for (i in 0 until childCount) yield(getChildAt(i)) + } \ No newline at end of file diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java index d124bad438c3f..a24fa842eca7b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java @@ -20,9 +20,9 @@ import static android.app.NotificationManager.IMPORTANCE_LOW; import static android.content.Intent.ACTION_USER_SWITCHED; import static android.provider.Settings.Secure.NOTIFICATION_NEW_INTERRUPTION_MODEL; -import static com.android.systemui.statusbar.notification.stack.NotificationSectionsManager.BUCKET_MEDIA_CONTROLS; -import static com.android.systemui.statusbar.notification.stack.NotificationSectionsManager.BUCKET_PEOPLE; -import static com.android.systemui.statusbar.notification.stack.NotificationSectionsManager.BUCKET_SILENT; +import static com.android.systemui.statusbar.notification.stack.NotificationSectionsManagerKt.BUCKET_MEDIA_CONTROLS; +import static com.android.systemui.statusbar.notification.stack.NotificationSectionsManagerKt.BUCKET_PEOPLE; +import static com.android.systemui.statusbar.notification.stack.NotificationSectionsManagerKt.BUCKET_SILENT; import static junit.framework.Assert.assertFalse; import static junit.framework.Assert.assertTrue; diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotificationRankingManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotificationRankingManagerTest.kt index b4cabfd1855d3..a83de139bbcaf 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotificationRankingManagerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotificationRankingManagerTest.kt @@ -36,8 +36,8 @@ import com.android.systemui.statusbar.notification.people.PeopleNotificationIden import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier.Companion.TYPE_IMPORTANT_PERSON import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier.Companion.TYPE_PERSON import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow -import com.android.systemui.statusbar.notification.stack.NotificationSectionsManager.BUCKET_ALERTING -import com.android.systemui.statusbar.notification.stack.NotificationSectionsManager.BUCKET_SILENT +import com.android.systemui.statusbar.notification.stack.BUCKET_ALERTING +import com.android.systemui.statusbar.notification.stack.BUCKET_SILENT import com.android.systemui.statusbar.phone.NotificationGroupManager import com.android.systemui.statusbar.policy.HeadsUpManager import dagger.Lazy diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationLoggerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationLoggerTest.java index d39b2c202fd9b..a3a46f67ee404 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationLoggerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationLoggerTest.java @@ -16,7 +16,7 @@ package com.android.systemui.statusbar.notification.logging; -import static com.android.systemui.statusbar.notification.stack.NotificationSectionsManager.BUCKET_ALERTING; +import static com.android.systemui.statusbar.notification.stack.NotificationSectionsManagerKt.BUCKET_ALERTING; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManagerTest.java index 546bce81a2600..02b6cbc0da9da 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManagerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManagerTest.java @@ -18,11 +18,11 @@ package com.android.systemui.statusbar.notification.stack; import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; -import static com.android.systemui.statusbar.notification.stack.NotificationSectionsManager.BUCKET_ALERTING; -import static com.android.systemui.statusbar.notification.stack.NotificationSectionsManager.BUCKET_FOREGROUND_SERVICE; -import static com.android.systemui.statusbar.notification.stack.NotificationSectionsManager.BUCKET_HEADS_UP; -import static com.android.systemui.statusbar.notification.stack.NotificationSectionsManager.BUCKET_PEOPLE; -import static com.android.systemui.statusbar.notification.stack.NotificationSectionsManager.BUCKET_SILENT; +import static com.android.systemui.statusbar.notification.stack.NotificationSectionsManagerKt.BUCKET_ALERTING; +import static com.android.systemui.statusbar.notification.stack.NotificationSectionsManagerKt.BUCKET_FOREGROUND_SERVICE; +import static com.android.systemui.statusbar.notification.stack.NotificationSectionsManagerKt.BUCKET_HEADS_UP; +import static com.android.systemui.statusbar.notification.stack.NotificationSectionsManagerKt.BUCKET_PEOPLE; +import static com.android.systemui.statusbar.notification.stack.NotificationSectionsManagerKt.BUCKET_SILENT; import static com.google.common.truth.Truth.assertThat; @@ -141,7 +141,7 @@ public class NotificationSectionsManagerTest extends SysuiTestCase { mSectionsManager.updateSectionBoundaries(); // THEN a LO section header is added - verify(mNssl).addView(mSectionsManager.getGentleHeaderView(), 3); + verify(mNssl).addView(mSectionsManager.getSilentHeaderView(), 3); } @Test @@ -160,7 +160,7 @@ public class NotificationSectionsManagerTest extends SysuiTestCase { mSectionsManager.updateSectionBoundaries(); // THEN the LO section header is removed - verify(mNssl).removeView(mSectionsManager.getGentleHeaderView()); + verify(mNssl).removeView(mSectionsManager.getSilentHeaderView()); } @Test @@ -172,7 +172,7 @@ public class NotificationSectionsManagerTest extends SysuiTestCase { mSectionsManager.updateSectionBoundaries(); // THEN we don't add any section headers - verify(mNssl, never()).addView(eq(mSectionsManager.getGentleHeaderView()), anyInt()); + verify(mNssl, never()).addView(eq(mSectionsManager.getSilentHeaderView()), anyInt()); } @Test @@ -195,7 +195,7 @@ public class NotificationSectionsManagerTest extends SysuiTestCase { mSectionsManager.updateSectionBoundaries(); // THEN the LO section header is also moved forward - verify(mNssl).changeViewPosition(mSectionsManager.getGentleHeaderView(), 2); + verify(mNssl).changeViewPosition(mSectionsManager.getSilentHeaderView(), 2); } @Test @@ -218,7 +218,7 @@ public class NotificationSectionsManagerTest extends SysuiTestCase { mSectionsManager.updateSectionBoundaries(); // THEN the LO section header is also moved backward (with appropriate index shifting) - verify(mNssl).changeViewPosition(mSectionsManager.getGentleHeaderView(), 3); + verify(mNssl).changeViewPosition(mSectionsManager.getSilentHeaderView(), 3); } @Test @@ -237,7 +237,7 @@ public class NotificationSectionsManagerTest extends SysuiTestCase { clearInvocations(mNssl); ViewGroup transientParent = mock(ViewGroup.class); - mSectionsManager.getGentleHeaderView().setTransientContainer(transientParent); + mSectionsManager.getSilentHeaderView().setTransientContainer(transientParent); // WHEN the LO section reappears setStackState( @@ -247,8 +247,8 @@ public class NotificationSectionsManagerTest extends SysuiTestCase { // THEN the header is first removed from the transient parent before being added to the // NSSL. - verify(transientParent).removeTransientView(mSectionsManager.getGentleHeaderView()); - verify(mNssl).addView(mSectionsManager.getGentleHeaderView(), 1); + verify(transientParent).removeTransientView(mSectionsManager.getSilentHeaderView()); + verify(mNssl).addView(mSectionsManager.getSilentHeaderView(), 1); } @Test @@ -261,7 +261,7 @@ public class NotificationSectionsManagerTest extends SysuiTestCase { mSectionsManager.updateSectionBoundaries(); // Then the section header is not added - verify(mNssl, never()).addView(eq(mSectionsManager.getGentleHeaderView()), anyInt()); + verify(mNssl, never()).addView(eq(mSectionsManager.getSilentHeaderView()), anyInt()); } @Test @@ -276,7 +276,7 @@ public class NotificationSectionsManagerTest extends SysuiTestCase { mSectionsManager.updateSectionBoundaries(); // Then the section header is added - verify(mNssl).addView(mSectionsManager.getGentleHeaderView(), 3); + verify(mNssl).addView(mSectionsManager.getSilentHeaderView(), 3); } @Test @@ -289,7 +289,7 @@ public class NotificationSectionsManagerTest extends SysuiTestCase { mSectionsManager.updateSectionBoundaries(); // Then the section header is removed - verify(mNssl).removeView(mSectionsManager.getGentleHeaderView()); + verify(mNssl).removeView(mSectionsManager.getSilentHeaderView()); } @Test @@ -303,7 +303,7 @@ public class NotificationSectionsManagerTest extends SysuiTestCase { ChildType.GENTLE); mSectionsManager.updateSectionBoundaries(); - verify(mNssl).changeViewPosition(mSectionsManager.getGentleHeaderView(), 2); + verify(mNssl).changeViewPosition(mSectionsManager.getSilentHeaderView(), 2); verify(mNssl).addView(mSectionsManager.getAlertingHeaderView(), 1); verify(mNssl).addView(mSectionsManager.getPeopleHeaderView(), 0); } @@ -318,7 +318,7 @@ public class NotificationSectionsManagerTest extends SysuiTestCase { ChildType.GENTLE); mSectionsManager.updateSectionBoundaries(); - verify(mNssl).addView(mSectionsManager.getGentleHeaderView(), 2); + verify(mNssl).addView(mSectionsManager.getSilentHeaderView(), 2); verify(mNssl).addView(mSectionsManager.getAlertingHeaderView(), 1); verify(mNssl).addView(mSectionsManager.getPeopleHeaderView(), 0); } @@ -336,7 +336,7 @@ public class NotificationSectionsManagerTest extends SysuiTestCase { ChildType.GENTLE); mSectionsManager.updateSectionBoundaries(); - verify(mNssl).changeViewPosition(mSectionsManager.getGentleHeaderView(), 4); + verify(mNssl).changeViewPosition(mSectionsManager.getSilentHeaderView(), 4); verify(mNssl).changeViewPosition(mSectionsManager.getAlertingHeaderView(), 2); verify(mNssl).changeViewPosition(mSectionsManager.getPeopleHeaderView(), 0); } @@ -473,7 +473,7 @@ public class NotificationSectionsManagerTest extends SysuiTestCase { child = mSectionsManager.getAlertingHeaderView(); break; case GENTLE_HEADER: - child = mSectionsManager.getGentleHeaderView(); + child = mSectionsManager.getSilentHeaderView(); break; case HEADS_UP: child = mockNotification(BUCKET_HEADS_UP); @@ -533,7 +533,7 @@ public class NotificationSectionsManagerTest extends SysuiTestCase { actual.add(ChildType.ALERTING_HEADER); continue; } - if (child == mSectionsManager.getGentleHeaderView()) { + if (child == mSectionsManager.getSilentHeaderView()) { actual.add(ChildType.GENTLE_HEADER); continue; } @@ -606,7 +606,7 @@ public class NotificationSectionsManagerTest extends SysuiTestCase { child = mSectionsManager.getAlertingHeaderView(); break; case GENTLE_HEADER: - child = mSectionsManager.getGentleHeaderView(); + child = mSectionsManager.getSilentHeaderView(); break; case HEADS_UP: child = mockNotification(BUCKET_HEADS_UP); From 7c1333199152c5a687c5f75930040786eb783d9f Mon Sep 17 00:00:00 2001 From: Steve Elliott Date: Thu, 14 May 2020 16:29:03 -0400 Subject: [PATCH 2/3] Fix TODO in #updateFirstAndLastViewsForAllSections Bug: 153554168 Test: atest, manual Change-Id: Ibd3f73bc837d13975857288648fe14d817e14190 --- .../stack/NotificationSectionsManager.kt | 77 ++++++---- .../android/systemui/util/SparseArrayUtils.kt | 136 ++++++++++++++++++ 2 files changed, 185 insertions(+), 28 deletions(-) create mode 100644 packages/SystemUI/src/com/android/systemui/util/SparseArrayUtils.kt diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManager.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManager.kt index a83c2b321cc16..a2daec15d90b5 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManager.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManager.kt @@ -42,6 +42,7 @@ import com.android.systemui.statusbar.notification.row.StackScrollerDecorView import com.android.systemui.statusbar.notification.stack.StackScrollAlgorithm.SectionProvider import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.util.children +import com.android.systemui.util.foldToSparseArray import javax.inject.Inject /** @@ -447,6 +448,38 @@ class NotificationSectionsManager @Inject internal constructor( } } + private sealed class SectionBounds { + + data class Many( + val first: ActivatableNotificationView, + val last: ActivatableNotificationView + ) : SectionBounds() + + data class One(val lone: ActivatableNotificationView) : SectionBounds() + object None : SectionBounds() + + fun addNotif(notif: ActivatableNotificationView): SectionBounds = when (this) { + is None -> One(notif) + is One -> Many(lone, notif) + is Many -> copy(last = notif) + } + + fun updateSection(section: NotificationSection): Boolean = when (this) { + is None -> section.setFirstAndLastVisibleChildren(null, null) + is One -> section.setFirstAndLastVisibleChildren(lone, lone) + is Many -> section.setFirstAndLastVisibleChildren(first, last) + } + + private fun NotificationSection.setFirstAndLastVisibleChildren( + first: ActivatableNotificationView?, + last: ActivatableNotificationView? + ): Boolean { + val firstChanged = setFirstVisibleChild(first) + val lastChanged = setLastVisibleChild(last) + return firstChanged || lastChanged + } + } + /** * Updates the boundaries (as tracked by their first and last views) of the priority sections. * @@ -456,35 +489,23 @@ class NotificationSectionsManager @Inject internal constructor( sections: Array, children: List ): Boolean { - if (sections.isEmpty() || children.isEmpty()) { - for (s in sections) { - s.firstVisibleChild = null - s.lastVisibleChild = null - } - return false - } - var changed = false - val viewsInBucket = mutableListOf() - for (s in sections) { - val filter = s.bucket - viewsInBucket.clear() - - // TODO: do this in a single pass, and more better - for (v in children) { - val bucket = getBucket(v) - ?: throw IllegalArgumentException("Cannot find section bucket for view") - if (bucket == filter) { - viewsInBucket.add(v) + // Create mapping of bucket to section + val sectionBounds = children.asSequence() + // Group children by bucket + .groupingBy { + getBucket(it) + ?: throw IllegalArgumentException("Cannot find section bucket for view") } - if (viewsInBucket.size >= 1) { - changed = changed or s.setFirstVisibleChild(viewsInBucket[0]) - changed = changed or - s.setLastVisibleChild(viewsInBucket[viewsInBucket.size - 1]) - } else { - changed = changed or s.setFirstVisibleChild(null) - changed = changed or s.setLastVisibleChild(null) - } - } + // Combine each bucket into a SectionBoundary + .foldToSparseArray( + SectionBounds.None, + size = sections.size, + operation = SectionBounds::addNotif + ) + // Update each section with the associated boundary, tracking if there was a change + val changed = sections.fold(false) { changed, section -> + val bounds = sectionBounds[section.bucket] ?: SectionBounds.None + bounds.updateSection(section) || changed } if (DEBUG) { logSections(sections) diff --git a/packages/SystemUI/src/com/android/systemui/util/SparseArrayUtils.kt b/packages/SystemUI/src/com/android/systemui/util/SparseArrayUtils.kt new file mode 100644 index 0000000000000..accb81eae32a0 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/util/SparseArrayUtils.kt @@ -0,0 +1,136 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.util + +import android.util.SparseArray + +/** + * Transforms an [Array] into a [SparseArray], by applying each element to [keySelector] in order to + * generate the index at which it will be placed. If two elements produce the same index, the latter + * replaces the former in the final result. + * + * See [Array.associateBy]. + */ +inline fun Array.associateByToSparseArray( + crossinline keySelector: (T) -> Int +): SparseArray { + val sparseArray = SparseArray(size) + for (value in this) { + sparseArray.put(keySelector(value), value) + } + return sparseArray +} + +/** + * Folds a [Grouping] into a [SparseArray]. See [Grouping.fold]. + */ +inline fun Grouping.foldToSparseArray( + initial: R, + size: Int = -1, + crossinline operation: (R, T) -> R +): SparseArray { + val sparseArray = when { + size < 0 -> SparseArray() + else -> SparseArray(size) + } + sourceIterator().forEach { elem -> + val key = keyOf(elem) + val acc = sparseArray.get(key) ?: initial + sparseArray.put(key, operation(acc, elem)) + } + return sparseArray +} + +/** + * Wraps this [SparseArray] into an immutable [Map], the methods of which forward to this + * [SparseArray]. + */ +fun SparseArray.asMap(): Map = SparseArrayMapWrapper(this) + +private class SparseArrayMapWrapper( + private val sparseArray: SparseArray +) : Map { + + private data class Entry(override val key: Int, override val value: T) : Map.Entry + + private val entrySequence = sequence { + val size = sparseArray.size() + for (i in 0 until size) { + val key = sparseArray.keyAt(i) + val value = sparseArray.get(key) + yield(Entry(key, value)) + } + } + + override val entries: Set> + get() = object : Set> { + override val size: Int + get() = this@SparseArrayMapWrapper.size + + override fun contains(element: Map.Entry): Boolean = + sparseArray[element.key]?.let { it == element.value } == true + + override fun containsAll(elements: Collection>): Boolean = + elements.all { contains(it) } + + override fun isEmpty(): Boolean = size == 0 + + override fun iterator(): Iterator> = entrySequence.iterator() + } + + override val keys: Set = object : Set { + private val keySequence = entrySequence.map { it.key } + + override val size: Int + get() = this@SparseArrayMapWrapper.size + + override fun contains(element: Int): Boolean = containsKey(element) + + override fun containsAll(elements: Collection): Boolean = + elements.all { contains(it) } + + override fun isEmpty(): Boolean = size == 0 + + override fun iterator(): Iterator = keySequence.iterator() + } + override val size: Int + get() = sparseArray.size() + override val values: Collection + get() = object : Collection { + private val valueSequence = entrySequence.map { it.value } + + override val size: Int + get() = this@SparseArrayMapWrapper.size + + override fun contains(element: T): Boolean = containsValue(element) + + override fun containsAll(elements: Collection): Boolean = + elements.all { contains(it) } + + override fun isEmpty(): Boolean = this@SparseArrayMapWrapper.isEmpty() + + override fun iterator(): Iterator = valueSequence.iterator() + } + + override fun containsKey(key: Int): Boolean = sparseArray.contains(key) + + override fun containsValue(value: T): Boolean = sparseArray.indexOfValue(value) >= 0 + + override fun get(key: Int): T? = sparseArray.get(key) + + override fun isEmpty(): Boolean = sparseArray.size() == 0 +} \ No newline at end of file From 33d3b54639af8d8776244934329b3bd2e8045b03 Mon Sep 17 00:00:00 2001 From: Steve Elliott Date: Wed, 13 May 2020 17:24:53 -0400 Subject: [PATCH 3/3] Rework Incoming section logic We cannot assign notifications to the HUNing section during sort and filter in NotifRankMan, because we don't know if those Notifications can be placed there at that time. The VisStabMan can decide that they cannot be moved, and so NotifSecMan can get confused when it sees BUCKET_HEADS_UP in the middle of the shade. In an ideal world, VisStabMan would inform the sort performed by NotifRankMan, as opposed to "shooting it down" after the fact. The main issue here is that NotifRankMan and NotifSecMan are logically related (Notification ordering) but VisStabMan can throw a wrench in that relationship. Also I took the liberty of converting NotifSecMan to Kotlin. :-) Fixes: 153554168 Test: atest, manual Change-Id: I4d1d38eaf4eaac69542c7c115040e76ff95cc1c9 --- .../collection/NotificationRankingManager.kt | 18 +- .../stack/NotificationSectionsLogger.kt | 31 +- .../stack/NotificationSectionsManager.kt | 116 ++++--- .../NotificationSectionsManagerTest.java | 322 ++++++++++++------ 4 files changed, 312 insertions(+), 175 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationRankingManager.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationRankingManager.kt index 5475c2e062d71..cbf680c5b7825 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationRankingManager.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationRankingManager.kt @@ -30,7 +30,6 @@ import com.android.systemui.statusbar.notification.people.PeopleNotificationIden import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier.Companion.TYPE_NON_PERSON import com.android.systemui.statusbar.notification.stack.BUCKET_ALERTING import com.android.systemui.statusbar.notification.stack.BUCKET_FOREGROUND_SERVICE -import com.android.systemui.statusbar.notification.stack.BUCKET_HEADS_UP import com.android.systemui.statusbar.notification.stack.BUCKET_PEOPLE import com.android.systemui.statusbar.notification.stack.BUCKET_SILENT import com.android.systemui.statusbar.notification.stack.PriorityBucket @@ -138,23 +137,8 @@ open class NotificationRankingManager @Inject constructor( .filterNot(notifFilter::shouldFilterOut) .sortedWith(rankingComparator) .toList() - assignBuckets(filtered) - return filtered - } - - private fun assignBuckets(entries: List) { entries.forEach { it.bucket = getBucketForEntry(it) } - if (!usePeopleFiltering) { - // If we don't have a Conversation section, just assign buckets normally based on the - // content. - return - } - // If HUNs are not continuous with the top section, break out into a new Incoming section. - entries.asReversed().asSequence().zipWithNext().forEach { (next, entry) -> - if (entry.isRowHeadsUp && entry.bucket > next.bucket) { - entry.bucket = BUCKET_HEADS_UP - } - } + return filtered } @PriorityBucket diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsLogger.kt index 92fdd6441539a..17b414379f8d5 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsLogger.kt @@ -52,14 +52,35 @@ class NotificationSectionsLogger @Inject constructor( { "$int1: other ($str1)" } ) - fun logHeadsUp(position: Int) = logPosition(position, "Heads Up") - fun logConversation(position: Int) = logPosition(position, "Conversation") - fun logAlerting(position: Int) = logPosition(position, "Alerting") - fun logSilent(position: Int) = logPosition(position, "Silent") - fun logForegroundService(position: Int) = logPosition(position, "Foreground Service") + fun logHeadsUp(position: Int, isHeadsUp: Boolean) = + logPosition(position, "Heads Up", isHeadsUp) + fun logConversation(position: Int, isHeadsUp: Boolean) = + logPosition(position, "Conversation", isHeadsUp) + fun logAlerting(position: Int, isHeadsUp: Boolean) = + logPosition(position, "Alerting", isHeadsUp) + fun logSilent(position: Int, isHeadsUp: Boolean) = + logPosition(position, "Silent", isHeadsUp) + fun logForegroundService(position: Int, isHeadsUp: Boolean) = + logPosition(position, "Foreground Service", isHeadsUp) fun logStr(str: String) = logBuffer.log(TAG, LogLevel.DEBUG, { str1 = str }, { "$str1" }) + private fun logPosition(position: Int, label: String, isHeadsUp: Boolean) { + val headsUpTag = if (isHeadsUp) " (HUN)" else "" + logBuffer.log( + TAG, + LogLevel.DEBUG, + { + int1 = position + str1 = label + str2 = headsUpTag + }, + { + "$int1: $str1$str2" + } + ) + } + private fun logPosition(position: Int, label: String) = logBuffer.log( TAG, LogLevel.DEBUG, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManager.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManager.kt index a2daec15d90b5..65633a2e209f1 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManager.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManager.kt @@ -206,11 +206,14 @@ class NotificationSectionsManager @Inject internal constructor( child === alertingHeaderView -> logger.logAlertingHeader(i) child === silentHeaderView -> logger.logSilentHeader(i) child !is ExpandableNotificationRow -> logger.logOther(i, child.javaClass) - else -> when (child.entry.bucket) { - BUCKET_HEADS_UP -> logger.logHeadsUp(i) - BUCKET_PEOPLE -> logger.logConversation(i) - BUCKET_ALERTING -> logger.logAlerting(i) - BUCKET_SILENT -> logger.logSilent(i) + else -> { + val isHeadsUp = child.isHeadsUp + when (child.entry.bucket) { + BUCKET_HEADS_UP -> logger.logHeadsUp(i, isHeadsUp) + BUCKET_PEOPLE -> logger.logConversation(i, isHeadsUp) + BUCKET_ALERTING -> logger.logAlerting(i, isHeadsUp) + BUCKET_SILENT -> logger.logSilent(i, isHeadsUp) + } } } } @@ -242,7 +245,7 @@ class NotificationSectionsManager @Inject internal constructor( var peopleNotifsPresent = false var currentMediaControlsIdx = -1 - var mediaControlsTarget = if (usingMediaControls) 0 else -1 + val mediaControlsTarget = if (usingMediaControls) 0 else -1 var currentIncomingHeaderIdx = -1 var incomingHeaderTarget = -1 var currentPeopleHeaderIdx = -1 @@ -253,10 +256,12 @@ class NotificationSectionsManager @Inject internal constructor( var gentleHeaderTarget = -1 var lastNotifIndex = 0 + var lastIncomingIndex = -1 + var prev: ExpandableNotificationRow? = null - parent.children.forEachIndexed { i, child -> - // Track the existing positions of the headers + for ((i, child) in parent.children.withIndex()) { when { + // Track the existing positions of the headers child === incomingHeaderView -> { logger.logIncomingHeader(i) currentIncomingHeaderIdx = i @@ -280,41 +285,40 @@ class NotificationSectionsManager @Inject internal constructor( child !is ExpandableNotificationRow -> logger.logOther(i, child.javaClass) else -> { lastNotifIndex = i + // Is there a section discontinuity? This usually occurs due to HUNs + if (prev?.entry?.bucket?.let { it > child.entry.bucket } == true) { + // Remove existing headers, and move the Incoming header if necessary + if (alertingHeaderTarget != -1) { + if (showHeaders && incomingHeaderTarget != -1) { + incomingHeaderTarget = alertingHeaderTarget + } + alertingHeaderTarget = -1 + } + if (peopleHeaderTarget != -1) { + if (showHeaders && incomingHeaderTarget != -1) { + incomingHeaderTarget = peopleHeaderTarget + } + peopleHeaderTarget = -1 + } + if (showHeaders && incomingHeaderTarget == -1) { + incomingHeaderTarget = 0 + } + // Walk backwards changing all previous notifications to the Incoming + // section + for (j in i - 1 downTo lastIncomingIndex + 1) { + val prevChild = parent.getChildAt(j) + if (prevChild is ExpandableNotificationRow) { + prevChild.entry.bucket = BUCKET_HEADS_UP + } + } + // Track the new bottom of the Incoming section + lastIncomingIndex = i - 1 + } + val isHeadsUp = child.isHeadsUp when (child.entry.bucket) { - BUCKET_HEADS_UP -> { - logger.logHeadsUp(i) - if (showHeaders && incomingHeaderTarget == -1) { - incomingHeaderTarget = i - // Offset the target if there are other headers before this that - // will be moved. - if (currentIncomingHeaderIdx != -1) { - incomingHeaderTarget-- - } - if (currentMediaControlsIdx != -1) { - incomingHeaderTarget-- - } - if (currentPeopleHeaderIdx != -1) { - incomingHeaderTarget-- - } - if (currentAlertingHeaderIdx != -1) { - incomingHeaderTarget-- - } - if (currentGentleHeaderIdx != -1) { - incomingHeaderTarget-- - } - } - if (mediaControlsTarget != -1) { - mediaControlsTarget++ - } - } - BUCKET_FOREGROUND_SERVICE -> { - logger.logForegroundService(i) - if (mediaControlsTarget != -1) { - mediaControlsTarget++ - } - } + BUCKET_FOREGROUND_SERVICE -> logger.logForegroundService(i, isHeadsUp) BUCKET_PEOPLE -> { - logger.logConversation(i) + logger.logConversation(i, isHeadsUp) peopleNotifsPresent = true if (showHeaders && peopleHeaderTarget == -1) { peopleHeaderTarget = i @@ -332,7 +336,7 @@ class NotificationSectionsManager @Inject internal constructor( } } BUCKET_ALERTING -> { - logger.logAlerting(i) + logger.logAlerting(i, isHeadsUp) if (showHeaders && usingPeopleFiltering && alertingHeaderTarget == -1) { alertingHeaderTarget = i // Offset the target if there are other headers before this that @@ -346,7 +350,7 @@ class NotificationSectionsManager @Inject internal constructor( } } BUCKET_SILENT -> { - logger.logSilent(i) + logger.logSilent(i, isHeadsUp) if (showHeaders && gentleHeaderTarget == -1) { gentleHeaderTarget = i // Offset the target if there are other headers before this that @@ -358,6 +362,8 @@ class NotificationSectionsManager @Inject internal constructor( } else -> throw IllegalStateException("Cannot find section bucket for view") } + + prev = child } } } @@ -393,12 +399,12 @@ class NotificationSectionsManager @Inject internal constructor( peopleHeaderView?.let { adjustHeaderVisibilityAndPosition(peopleHeaderTarget, it, currentPeopleHeaderIdx) } - mediaControlsView?.let { - adjustViewPosition(mediaControlsTarget, it, currentMediaControlsIdx) - } incomingHeaderView?.let { adjustHeaderVisibilityAndPosition(incomingHeaderTarget, it, currentIncomingHeaderIdx) } + mediaControlsView?.let { + adjustViewPosition(mediaControlsTarget, it, currentMediaControlsIdx) + } logger.logStr("Final order:") logShadeContents() @@ -575,14 +581,16 @@ class NotificationSectionsManager @Inject internal constructor( @IntDef( prefix = ["BUCKET_"], value = [ - BUCKET_HEADS_UP, BUCKET_FOREGROUND_SERVICE, BUCKET_MEDIA_CONTROLS, BUCKET_PEOPLE, - BUCKET_ALERTING, BUCKET_SILENT + BUCKET_UNKNOWN, BUCKET_MEDIA_CONTROLS, BUCKET_HEADS_UP, BUCKET_FOREGROUND_SERVICE, + BUCKET_PEOPLE, BUCKET_ALERTING, BUCKET_SILENT ] ) annotation class PriorityBucket -const val BUCKET_HEADS_UP = 0 -const val BUCKET_FOREGROUND_SERVICE = 1 -const val BUCKET_MEDIA_CONTROLS = 2 -const val BUCKET_PEOPLE = 3 -const val BUCKET_ALERTING = 4 -const val BUCKET_SILENT = 5 + +const val BUCKET_UNKNOWN = 0 +const val BUCKET_MEDIA_CONTROLS = 1 +const val BUCKET_HEADS_UP = 2 +const val BUCKET_FOREGROUND_SERVICE = 3 +const val BUCKET_PEOPLE = 4 +const val BUCKET_ALERTING = 5 +const val BUCKET_SILENT = 6 diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManagerTest.java index 02b6cbc0da9da..3dc941a0bd208 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManagerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManagerTest.java @@ -52,6 +52,7 @@ import com.android.systemui.media.KeyguardMediaController; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.notification.NotificationSectionsFeatureManager; +import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.people.PeopleHubViewAdapter; import com.android.systemui.statusbar.notification.row.ActivatableNotificationViewController; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; @@ -135,7 +136,11 @@ public class NotificationSectionsManagerTest extends SysuiTestCase { @Test public void testInsertHeader() { // GIVEN a stack with HI and LO rows but no section headers - setStackState(ChildType.ALERTING, ChildType.ALERTING, ChildType.ALERTING, ChildType.GENTLE); + setStackState( + ALERTING, + ALERTING, + ALERTING, + GENTLE); // WHEN we update the section headers mSectionsManager.updateSectionBoundaries(); @@ -147,15 +152,18 @@ public class NotificationSectionsManagerTest extends SysuiTestCase { @Test public void testRemoveHeader() { // GIVEN a stack that originally had a header between the HI and LO sections - setStackState(ChildType.ALERTING, ChildType.ALERTING, ChildType.GENTLE); + setStackState( + ALERTING, + ALERTING, + GENTLE); mSectionsManager.updateSectionBoundaries(); // WHEN the last LO row is replaced with a HI row setStackState( - ChildType.ALERTING, - ChildType.ALERTING, - ChildType.GENTLE_HEADER, - ChildType.ALERTING); + ALERTING, + ALERTING, + GENTLE_HEADER, + ALERTING); clearInvocations(mNssl); mSectionsManager.updateSectionBoundaries(); @@ -166,7 +174,10 @@ public class NotificationSectionsManagerTest extends SysuiTestCase { @Test public void testDoNothingIfHeaderAlreadyRemoved() { // GIVEN a stack with only HI rows - setStackState(ChildType.ALERTING, ChildType.ALERTING, ChildType.ALERTING); + setStackState( + ALERTING, + ALERTING, + ALERTING); // WHEN we update the sections headers mSectionsManager.updateSectionBoundaries(); @@ -179,19 +190,19 @@ public class NotificationSectionsManagerTest extends SysuiTestCase { public void testMoveHeaderForward() { // GIVEN a stack that originally had a header between the HI and LO sections setStackState( - ChildType.ALERTING, - ChildType.ALERTING, - ChildType.ALERTING, - ChildType.GENTLE); + ALERTING, + ALERTING, + ALERTING, + GENTLE); mSectionsManager.updateSectionBoundaries(); // WHEN the LO section moves forward setStackState( - ChildType.ALERTING, - ChildType.ALERTING, - ChildType.GENTLE, - ChildType.GENTLE_HEADER, - ChildType.GENTLE); + ALERTING, + ALERTING, + GENTLE, + GENTLE_HEADER, + GENTLE); mSectionsManager.updateSectionBoundaries(); // THEN the LO section header is also moved forward @@ -202,19 +213,19 @@ public class NotificationSectionsManagerTest extends SysuiTestCase { public void testMoveHeaderBackward() { // GIVEN a stack that originally had a header between the HI and LO sections setStackState( - ChildType.ALERTING, - ChildType.GENTLE, - ChildType.GENTLE, - ChildType.GENTLE); + ALERTING, + GENTLE, + GENTLE, + GENTLE); mSectionsManager.updateSectionBoundaries(); // WHEN the LO section moves backward setStackState( - ChildType.ALERTING, - ChildType.GENTLE_HEADER, - ChildType.ALERTING, - ChildType.ALERTING, - ChildType.GENTLE); + ALERTING, + GENTLE_HEADER, + ALERTING, + ALERTING, + GENTLE); mSectionsManager.updateSectionBoundaries(); // THEN the LO section header is also moved backward (with appropriate index shifting) @@ -225,14 +236,8 @@ public class NotificationSectionsManagerTest extends SysuiTestCase { public void testHeaderRemovedFromTransientParent() { // GIVEN a stack where the header is animating away setStackState( - ChildType.ALERTING, - ChildType.GENTLE, - ChildType.GENTLE, - ChildType.GENTLE); - mSectionsManager.updateSectionBoundaries(); - setStackState( - ChildType.ALERTING, - ChildType.GENTLE_HEADER); + ALERTING, + GENTLE_HEADER); mSectionsManager.updateSectionBoundaries(); clearInvocations(mNssl); @@ -241,8 +246,8 @@ public class NotificationSectionsManagerTest extends SysuiTestCase { // WHEN the LO section reappears setStackState( - ChildType.ALERTING, - ChildType.GENTLE); + ALERTING, + GENTLE); mSectionsManager.updateSectionBoundaries(); // THEN the header is first removed from the transient parent before being added to the @@ -255,7 +260,11 @@ public class NotificationSectionsManagerTest extends SysuiTestCase { public void testHeaderNotShownOnLockscreen() { // GIVEN a stack of HI and LO notifs on the lockscreen when(mStatusBarStateController.getState()).thenReturn(StatusBarState.KEYGUARD); - setStackState(ChildType.ALERTING, ChildType.ALERTING, ChildType.ALERTING, ChildType.GENTLE); + setStackState( + ALERTING, + ALERTING, + ALERTING, + GENTLE); // WHEN we update the section headers mSectionsManager.updateSectionBoundaries(); @@ -268,7 +277,11 @@ public class NotificationSectionsManagerTest extends SysuiTestCase { public void testHeaderShownWhenEnterLockscreen() { // GIVEN a stack of HI and LO notifs on the lockscreen when(mStatusBarStateController.getState()).thenReturn(StatusBarState.KEYGUARD); - setStackState(ChildType.ALERTING, ChildType.ALERTING, ChildType.ALERTING, ChildType.GENTLE); + setStackState( + ALERTING, + ALERTING, + ALERTING, + GENTLE); mSectionsManager.updateSectionBoundaries(); // WHEN we unlock @@ -282,7 +295,10 @@ public class NotificationSectionsManagerTest extends SysuiTestCase { @Test public void testHeaderHiddenWhenEnterLockscreen() { // GIVEN a stack of HI and LO notifs on the shade - setStackState(ChildType.ALERTING, ChildType.GENTLE_HEADER, ChildType.GENTLE); + setStackState( + ALERTING, + GENTLE_HEADER, + GENTLE); // WHEN we go back to the keyguard when(mStatusBarStateController.getState()).thenReturn(StatusBarState.KEYGUARD); @@ -297,10 +313,10 @@ public class NotificationSectionsManagerTest extends SysuiTestCase { enablePeopleFiltering(); setStackState( - ChildType.GENTLE_HEADER, - ChildType.PERSON, - ChildType.ALERTING, - ChildType.GENTLE); + GENTLE_HEADER, + PERSON, + ALERTING, + GENTLE); mSectionsManager.updateSectionBoundaries(); verify(mNssl).changeViewPosition(mSectionsManager.getSilentHeaderView(), 2); @@ -313,9 +329,9 @@ public class NotificationSectionsManagerTest extends SysuiTestCase { enablePeopleFiltering(); setStackState( - ChildType.PERSON, - ChildType.ALERTING, - ChildType.GENTLE); + PERSON, + ALERTING, + GENTLE); mSectionsManager.updateSectionBoundaries(); verify(mNssl).addView(mSectionsManager.getSilentHeaderView(), 2); @@ -328,12 +344,12 @@ public class NotificationSectionsManagerTest extends SysuiTestCase { enablePeopleFiltering(); setStackState( - ChildType.PEOPLE_HEADER, - ChildType.ALERTING_HEADER, - ChildType.GENTLE_HEADER, - ChildType.PERSON, - ChildType.ALERTING, - ChildType.GENTLE); + PEOPLE_HEADER, + ALERTING_HEADER, + GENTLE_HEADER, + PERSON, + ALERTING, + GENTLE); mSectionsManager.updateSectionBoundaries(); verify(mNssl).changeViewPosition(mSectionsManager.getSilentHeaderView(), 4); @@ -347,12 +363,11 @@ public class NotificationSectionsManagerTest extends SysuiTestCase { enablePeopleFiltering(); setStackState( - ChildType.PEOPLE_HEADER, - ChildType.ALERTING_HEADER, - ChildType.ALERTING, - ChildType.GENTLE_HEADER, - ChildType.GENTLE - ); + PEOPLE_HEADER, + ALERTING_HEADER, + ALERTING, + GENTLE_HEADER, + GENTLE); mSectionsManager.updateSectionBoundaries(); verify(mNssl, never()).removeView(mSectionsManager.getPeopleHeaderView()); @@ -360,16 +375,16 @@ public class NotificationSectionsManagerTest extends SysuiTestCase { } @Test - public void testPeopleFiltering_HunWhilePeopleVisible() { + public void testPeopleFiltering_AlertingHunWhilePeopleVisible() { enablePeopleFiltering(); setupMockStack( - ChildType.PEOPLE_HEADER, - ChildType.HEADS_UP, - ChildType.PERSON, - ChildType.ALERTING_HEADER, - ChildType.GENTLE_HEADER, - ChildType.GENTLE + PEOPLE_HEADER, + ALERTING.headsUp(), + PERSON, + ALERTING_HEADER, + GENTLE_HEADER, + GENTLE ); mSectionsManager.updateSectionBoundaries(); @@ -383,18 +398,75 @@ public class NotificationSectionsManagerTest extends SysuiTestCase { ); } + @Test + public void testPeopleFiltering_PersonHunWhileAlertingHunVisible() { + enablePeopleFiltering(); + + setupMockStack( + PERSON.headsUp(), + INCOMING_HEADER, + ALERTING.headsUp(), + PEOPLE_HEADER, + PERSON + ); + mSectionsManager.updateSectionBoundaries(); + + verifyMockStack( + ChildType.INCOMING_HEADER, + ChildType.HEADS_UP, + ChildType.HEADS_UP, + ChildType.PEOPLE_HEADER, + ChildType.PERSON + ); + } + + @Test + public void testPeopleFiltering_PersonHun() { + enablePeopleFiltering(); + + setupMockStack( + PERSON.headsUp(), + PEOPLE_HEADER, + PERSON + ); + mSectionsManager.updateSectionBoundaries(); + + verifyMockStack( + ChildType.PEOPLE_HEADER, + ChildType.PERSON, + ChildType.PERSON + ); + } + + @Test + public void testPeopleFiltering_AlertingHunWhilePersonHunning() { + enablePeopleFiltering(); + + setupMockStack( + ALERTING.headsUp(), + PERSON.headsUp() + ); + mSectionsManager.updateSectionBoundaries(); + verifyMockStack( + ChildType.INCOMING_HEADER, + ChildType.HEADS_UP, + ChildType.PEOPLE_HEADER, + ChildType.PERSON + ); + } + @Test public void testPeopleFiltering_Fsn() { enablePeopleFiltering(); setupMockStack( - ChildType.INCOMING_HEADER, - ChildType.HEADS_UP, - ChildType.PEOPLE_HEADER, - ChildType.FSN, - ChildType.PERSON, - ChildType.ALERTING, - ChildType.GENTLE + INCOMING_HEADER, + ALERTING.headsUp(), + PEOPLE_HEADER, + FSN, + PERSON, + ALERTING, + GENTLE ); mSectionsManager.updateSectionBoundaries(); @@ -416,7 +488,7 @@ public class NotificationSectionsManagerTest extends SysuiTestCase { enableMediaControls(); // GIVEN a stack that doesn't include media controls - setStackState(ChildType.ALERTING, ChildType.GENTLE_HEADER, ChildType.GENTLE); + setStackState(ALERTING, GENTLE_HEADER, GENTLE); // WHEN we go back to the keyguard when(mStatusBarStateController.getState()).thenReturn(StatusBarState.KEYGUARD); @@ -431,14 +503,20 @@ public class NotificationSectionsManagerTest extends SysuiTestCase { enableMediaControls(); // GIVEN a stack that doesn't include media controls but includes HEADS_UP - setupMockStack(ChildType.HEADS_UP, ChildType.ALERTING, ChildType.GENTLE_HEADER, - ChildType.GENTLE); + setupMockStack( + ALERTING.headsUp(), + ALERTING, + GENTLE_HEADER, + GENTLE); // WHEN we go back to the keyguard when(mStatusBarStateController.getState()).thenReturn(StatusBarState.KEYGUARD); mSectionsManager.updateSectionBoundaries(); - verifyMockStack(ChildType.HEADS_UP, ChildType.MEDIA_CONTROLS, ChildType.ALERTING, + verifyMockStack( + ChildType.MEDIA_CONTROLS, + ChildType.ALERTING, + ChildType.ALERTING, ChildType.GENTLE); } @@ -455,11 +533,12 @@ public class NotificationSectionsManagerTest extends SysuiTestCase { FSN, PERSON, ALERTING, GENTLE, OTHER } - private void setStackState(ChildType... children) { + private void setStackState(StackEntry... children) { when(mNssl.getChildCount()).thenReturn(children.length); for (int i = 0; i < children.length; i++) { View child; - switch (children[i]) { + StackEntry entry = children[i]; + switch (entry.mChildType) { case INCOMING_HEADER: child = mSectionsManager.getIncomingHeaderView(); break; @@ -475,20 +554,17 @@ public class NotificationSectionsManagerTest extends SysuiTestCase { case GENTLE_HEADER: child = mSectionsManager.getSilentHeaderView(); break; - case HEADS_UP: - child = mockNotification(BUCKET_HEADS_UP); - break; case FSN: - child = mockNotification(BUCKET_FOREGROUND_SERVICE); + child = mockNotification(BUCKET_FOREGROUND_SERVICE, entry.mIsHeadsUp); break; case PERSON: - child = mockNotification(BUCKET_PEOPLE); + child = mockNotification(BUCKET_PEOPLE, entry.mIsHeadsUp); break; case ALERTING: - child = mockNotification(BUCKET_ALERTING); + child = mockNotification(BUCKET_ALERTING, entry.mIsHeadsUp); break; case GENTLE: - child = mockNotification(BUCKET_SILENT); + child = mockNotification(BUCKET_SILENT, entry.mIsHeadsUp); break; case OTHER: child = mock(View.class); @@ -503,12 +579,24 @@ public class NotificationSectionsManagerTest extends SysuiTestCase { } } - private View mockNotification(int bucket) { - ExpandableNotificationRow notifRow = mock(ExpandableNotificationRow.class, - RETURNS_DEEP_STUBS); + private View mockNotification(int bucket, boolean headsUp) { + ExpandableNotificationRow notifRow = + mock(ExpandableNotificationRow.class, RETURNS_DEEP_STUBS); when(notifRow.getVisibility()).thenReturn(View.VISIBLE); - when(notifRow.getEntry().getBucket()).thenReturn(bucket); when(notifRow.getParent()).thenReturn(mNssl); + + NotificationEntry mockEntry = mock(NotificationEntry.class); + when(notifRow.getEntry()).thenReturn(mockEntry); + + int[] bucketRef = new int[] { bucket }; + when(mockEntry.getBucket()).thenAnswer(invocation -> bucketRef[0]); + doAnswer(invocation -> { + bucketRef[0] = invocation.getArgument(0); + return null; + }).when(mockEntry).setBucket(anyInt()); + + when(notifRow.isHeadsUp()).thenReturn(headsUp); + when(mockEntry.isRowHeadsUp()).thenReturn(headsUp); return notifRow; } @@ -565,7 +653,7 @@ public class NotificationSectionsManagerTest extends SysuiTestCase { assertThat(actual).containsExactly((Object[]) expected).inOrder(); } - private void setupMockStack(ChildType... childTypes) { + private void setupMockStack(StackEntry... entries) { final List children = new ArrayList<>(); when(mNssl.getChildCount()).thenAnswer(invocation -> children.size()); when(mNssl.getChildAt(anyInt())) @@ -590,9 +678,9 @@ public class NotificationSectionsManagerTest extends SysuiTestCase { children.add(newIndex, child); return null; }).when(mNssl).changeViewPosition(any(), anyInt()); - for (ChildType childType : childTypes) { + for (StackEntry entry : entries) { View child; - switch (childType) { + switch (entry.mChildType) { case INCOMING_HEADER: child = mSectionsManager.getIncomingHeaderView(); break; @@ -608,20 +696,17 @@ public class NotificationSectionsManagerTest extends SysuiTestCase { case GENTLE_HEADER: child = mSectionsManager.getSilentHeaderView(); break; - case HEADS_UP: - child = mockNotification(BUCKET_HEADS_UP); - break; case FSN: - child = mockNotification(BUCKET_FOREGROUND_SERVICE); + child = mockNotification(BUCKET_FOREGROUND_SERVICE, entry.mIsHeadsUp); break; case PERSON: - child = mockNotification(BUCKET_PEOPLE); + child = mockNotification(BUCKET_PEOPLE, entry.mIsHeadsUp); break; case ALERTING: - child = mockNotification(BUCKET_ALERTING); + child = mockNotification(BUCKET_ALERTING, entry.mIsHeadsUp); break; case GENTLE: - child = mockNotification(BUCKET_SILENT); + child = mockNotification(BUCKET_SILENT, entry.mIsHeadsUp); break; case OTHER: child = mock(View.class); @@ -629,9 +714,48 @@ public class NotificationSectionsManagerTest extends SysuiTestCase { when(child.getParent()).thenReturn(mNssl); break; default: - throw new RuntimeException("Unknown ChildType: " + childType); + throw new RuntimeException("Unknown ChildType: " + entry.mChildType); } children.add(child); } } + + private static final StackEntry INCOMING_HEADER = new StackEntry(ChildType.INCOMING_HEADER); + private static final StackEntry MEDIA_CONTROLS = new StackEntry(ChildType.MEDIA_CONTROLS); + private static final StackEntry PEOPLE_HEADER = new StackEntry(ChildType.PEOPLE_HEADER); + private static final StackEntry ALERTING_HEADER = new StackEntry(ChildType.ALERTING_HEADER); + private static final StackEntry GENTLE_HEADER = new StackEntry(ChildType.GENTLE_HEADER); + private static final StackEntry FSN = new StackEntry(ChildType.FSN); + private static final StackEntry.Hunnable PERSON = new StackEntry.Hunnable(ChildType.PERSON); + private static final StackEntry.Hunnable ALERTING = new StackEntry.Hunnable(ChildType.ALERTING); + private static final StackEntry GENTLE = new StackEntry(ChildType.GENTLE); + + private static class StackEntry { + final ChildType mChildType; + final boolean mIsHeadsUp; + + StackEntry(ChildType childType) { + this(childType, false); + } + + StackEntry(ChildType childType, boolean isHeadsUp) { + mChildType = childType; + mIsHeadsUp = isHeadsUp; + } + + static class Hunnable extends StackEntry { + + Hunnable(ChildType childType) { + super(childType, false); + } + + Hunnable(ChildType childType, boolean isHeadsUp) { + super(childType, isHeadsUp); + } + + public Hunnable headsUp() { + return new Hunnable(mChildType, true); + } + } + } }