Merge changes I4d1d38ea,Ibd3f73bc,Ic476fbb0 into rvc-dev
* changes: Rework Incoming section logic Fix TODO in #updateFirstAndLastViewsForAllSections Convert NotificationSectionsManager to Kotlin
This commit is contained in:
committed by
Android (Google) Code Review
commit
2a952d33f6
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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);
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,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;
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user