diff options
15 files changed, 1054 insertions, 857 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java index 2647c04ff586..2baab61d861b 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 d7b391ff03e4..ce6013f776af 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 634872d9d761..22ac1a2e5cf9 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 9ac42298e539..cbf680c5b782 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,11 @@ 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_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 @@ -138,23 +137,8 @@ open class NotificationRankingManager @Inject constructor( .filterNot(notifFilter::shouldFilterOut) .sortedWith(rankingComparator) .toList() - assignBuckets(filtered) - return filtered - } - - private fun assignBuckets(entries: List<NotificationEntry>) { 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/NotificationSection.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSection.java index 9d456ef785a8..bad36bf3de64 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/NotificationSectionsLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsLogger.kt index 92fdd6441539..17b414379f8d 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.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManager.java deleted file mode 100644 index f3ee2ceaf8ee..000000000000 --- 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<DataListener<PersonViewModel>> 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 extends ExpandableView> 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<ActivatableNotificationView> children) { - - if (sections.length <= 0 || children.size() <= 0) { - for (NotificationSection s : sections) { - s.setFirstVisibleChild(null); - s.setLastVisibleChild(null); - } - return false; - } - - boolean changed = false; - ArrayList<ActivatableNotificationView> 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 000000000000..65633a2e209f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManager.kt @@ -0,0 +1,596 @@ +/* + * 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 com.android.systemui.util.foldToSparseArray +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<DataListener<PersonViewModel?>> + 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 <T : ExpandableView> 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<NotificationSection> = + 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 -> { + 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) + } + } + } + } + + 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 + val 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 + var lastIncomingIndex = -1 + var prev: ExpandableNotificationRow? = null + + for ((i, child) in parent.children.withIndex()) { + when { + // Track the existing positions of the headers + 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 + // 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_FOREGROUND_SERVICE -> logger.logForegroundService(i, isHeadsUp) + BUCKET_PEOPLE -> { + logger.logConversation(i, isHeadsUp) + 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, isHeadsUp) + 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, isHeadsUp) + 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") + } + + prev = child + } + } + } + + 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) + } + incomingHeaderView?.let { + adjustHeaderVisibilityAndPosition(incomingHeaderTarget, it, currentIncomingHeaderIdx) + } + mediaControlsView?.let { + adjustViewPosition(mediaControlsTarget, it, currentMediaControlsIdx) + } + + 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) + } + } + } + + 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. + * + * @return `true` If the last view in the top section changed (so we need to animate). + */ + fun updateFirstAndLastViewsForAllSections( + sections: Array<NotificationSection>, + children: List<ActivatableNotificationView> + ): Boolean { + // 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") + } + // 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) + } + return changed + } + + private fun logSections(sections: Array<NotificationSection>) { + 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_UNKNOWN, BUCKET_MEDIA_CONTROLS, BUCKET_HEADS_UP, BUCKET_FOREGROUND_SERVICE, + BUCKET_PEOPLE, BUCKET_ALERTING, BUCKET_SILENT + ] +) +annotation class PriorityBucket + +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/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java index 1ccc2bde2288..3db4b6f7ffbb 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 000000000000..c91033e4745a --- /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/src/com/android/systemui/util/SparseArrayUtils.kt b/packages/SystemUI/src/com/android/systemui/util/SparseArrayUtils.kt new file mode 100644 index 000000000000..accb81eae32a --- /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 <T> Array<T>.associateByToSparseArray( + crossinline keySelector: (T) -> Int +): SparseArray<T> { + val sparseArray = SparseArray<T>(size) + for (value in this) { + sparseArray.put(keySelector(value), value) + } + return sparseArray +} + +/** + * Folds a [Grouping] into a [SparseArray]. See [Grouping.fold]. + */ +inline fun <T, R> Grouping<T, Int>.foldToSparseArray( + initial: R, + size: Int = -1, + crossinline operation: (R, T) -> R +): SparseArray<R> { + val sparseArray = when { + size < 0 -> SparseArray<R>() + else -> SparseArray<R>(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 <T> SparseArray<T>.asMap(): Map<Int, T> = SparseArrayMapWrapper(this) + +private class SparseArrayMapWrapper<T>( + private val sparseArray: SparseArray<T> +) : Map<Int, T> { + + private data class Entry<T>(override val key: Int, override val value: T) : Map.Entry<Int, T> + + 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<Map.Entry<Int, T>> + get() = object : Set<Map.Entry<Int, T>> { + override val size: Int + get() = this@SparseArrayMapWrapper.size + + override fun contains(element: Map.Entry<Int, T>): Boolean = + sparseArray[element.key]?.let { it == element.value } == true + + override fun containsAll(elements: Collection<Map.Entry<Int, T>>): Boolean = + elements.all { contains(it) } + + override fun isEmpty(): Boolean = size == 0 + + override fun iterator(): Iterator<Map.Entry<Int, T>> = entrySequence.iterator() + } + + override val keys: Set<Int> = object : Set<Int> { + 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<Int>): Boolean = + elements.all { contains(it) } + + override fun isEmpty(): Boolean = size == 0 + + override fun iterator(): Iterator<Int> = keySequence.iterator() + } + override val size: Int + get() = sparseArray.size() + override val values: Collection<T> + get() = object : Collection<T> { + 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<T>): Boolean = + elements.all { contains(it) } + + override fun isEmpty(): Boolean = this@SparseArrayMapWrapper.isEmpty() + + override fun iterator(): Iterator<T> = 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 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 d124bad438c3..a24fa842eca7 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 b4cabfd1855d..a83de139bbca 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 d39b2c202fd9..a3a46f67ee40 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 546bce81a260..3dc941a0bd20 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; @@ -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,140 +136,152 @@ 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(); // THEN a LO section header is added - verify(mNssl).addView(mSectionsManager.getGentleHeaderView(), 3); + verify(mNssl).addView(mSectionsManager.getSilentHeaderView(), 3); } @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(); // THEN the LO section header is removed - verify(mNssl).removeView(mSectionsManager.getGentleHeaderView()); + verify(mNssl).removeView(mSectionsManager.getSilentHeaderView()); } @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(); // 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 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 - verify(mNssl).changeViewPosition(mSectionsManager.getGentleHeaderView(), 2); + verify(mNssl).changeViewPosition(mSectionsManager.getSilentHeaderView(), 2); } @Test 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) - verify(mNssl).changeViewPosition(mSectionsManager.getGentleHeaderView(), 3); + verify(mNssl).changeViewPosition(mSectionsManager.getSilentHeaderView(), 3); } @Test 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); ViewGroup transientParent = mock(ViewGroup.class); - mSectionsManager.getGentleHeaderView().setTransientContainer(transientParent); + mSectionsManager.getSilentHeaderView().setTransientContainer(transientParent); // 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 // NSSL. - verify(transientParent).removeTransientView(mSectionsManager.getGentleHeaderView()); - verify(mNssl).addView(mSectionsManager.getGentleHeaderView(), 1); + verify(transientParent).removeTransientView(mSectionsManager.getSilentHeaderView()); + verify(mNssl).addView(mSectionsManager.getSilentHeaderView(), 1); } @Test 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(); // Then the section header is not added - verify(mNssl, never()).addView(eq(mSectionsManager.getGentleHeaderView()), anyInt()); + verify(mNssl, never()).addView(eq(mSectionsManager.getSilentHeaderView()), anyInt()); } @Test 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 @@ -276,20 +289,23 @@ 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 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); mSectionsManager.updateSectionBoundaries(); // Then the section header is removed - verify(mNssl).removeView(mSectionsManager.getGentleHeaderView()); + verify(mNssl).removeView(mSectionsManager.getSilentHeaderView()); } @Test @@ -297,13 +313,13 @@ 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.getGentleHeaderView(), 2); + verify(mNssl).changeViewPosition(mSectionsManager.getSilentHeaderView(), 2); verify(mNssl).addView(mSectionsManager.getAlertingHeaderView(), 1); verify(mNssl).addView(mSectionsManager.getPeopleHeaderView(), 0); } @@ -313,12 +329,12 @@ public class NotificationSectionsManagerTest extends SysuiTestCase { enablePeopleFiltering(); setStackState( - ChildType.PERSON, - ChildType.ALERTING, - ChildType.GENTLE); + PERSON, + ALERTING, + 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); } @@ -328,15 +344,15 @@ 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.getGentleHeaderView(), 4); + verify(mNssl).changeViewPosition(mSectionsManager.getSilentHeaderView(), 4); verify(mNssl).changeViewPosition(mSectionsManager.getAlertingHeaderView(), 2); verify(mNssl).changeViewPosition(mSectionsManager.getPeopleHeaderView(), 0); } @@ -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,41 +375,98 @@ public class NotificationSectionsManagerTest extends SysuiTestCase { } @Test - public void testPeopleFiltering_HunWhilePeopleVisible() { + public void testPeopleFiltering_AlertingHunWhilePeopleVisible() { enablePeopleFiltering(); setupMockStack( - ChildType.PEOPLE_HEADER, + PEOPLE_HEADER, + ALERTING.headsUp(), + PERSON, + ALERTING_HEADER, + GENTLE_HEADER, + GENTLE + ); + mSectionsManager.updateSectionBoundaries(); + + verifyMockStack( + ChildType.INCOMING_HEADER, ChildType.HEADS_UP, + ChildType.PEOPLE_HEADER, ChildType.PERSON, - ChildType.ALERTING_HEADER, ChildType.GENTLE_HEADER, ChildType.GENTLE ); + } + + @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.GENTLE_HEADER, - ChildType.GENTLE + ChildType.PERSON ); } @Test - public void testPeopleFiltering_Fsn() { + public void testPeopleFiltering_AlertingHunWhilePersonHunning() { enablePeopleFiltering(); setupMockStack( + ALERTING.headsUp(), + PERSON.headsUp() + ); + mSectionsManager.updateSectionBoundaries(); + verifyMockStack( ChildType.INCOMING_HEADER, ChildType.HEADS_UP, ChildType.PEOPLE_HEADER, - ChildType.FSN, - ChildType.PERSON, - ChildType.ALERTING, - ChildType.GENTLE + ChildType.PERSON + ); + } + + @Test + public void testPeopleFiltering_Fsn() { + enablePeopleFiltering(); + + setupMockStack( + 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; @@ -473,22 +552,19 @@ public class NotificationSectionsManagerTest extends SysuiTestCase { child = mSectionsManager.getAlertingHeaderView(); break; case GENTLE_HEADER: - child = mSectionsManager.getGentleHeaderView(); - break; - case HEADS_UP: - child = mockNotification(BUCKET_HEADS_UP); + child = mSectionsManager.getSilentHeaderView(); 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; } @@ -533,7 +621,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; } @@ -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<View> 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; @@ -606,22 +694,19 @@ public class NotificationSectionsManagerTest extends SysuiTestCase { child = mSectionsManager.getAlertingHeaderView(); break; case GENTLE_HEADER: - child = mSectionsManager.getGentleHeaderView(); - break; - case HEADS_UP: - child = mockNotification(BUCKET_HEADS_UP); + child = mSectionsManager.getSilentHeaderView(); 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); + } + } + } } |