Merge changes from topic "gentle-header" into qt-dev

* changes:
  Add "clear all" button to gentle notif section header
  Add "Gentle Notifications" header to gentle section
This commit is contained in:
Ned Burns
2019-05-07 21:13:31 +00:00
committed by Android (Google) Code Review
15 changed files with 867 additions and 236 deletions

View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ 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
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="40dp"
android:height="40dp"
android:viewportWidth="40"
android:viewportHeight="40">
<path
android:fillColor="#9AA0A6"
android:pathData="M24.6667 16.2733L23.7267 15.3333L20 19.06L16.2734 15.3333L15.3334 16.2733L19.06 20L15.3334 23.7266L16.2734 24.6666L20 20.94L23.7267 24.6666L24.6667 23.7266L20.94 20L24.6667 16.2733Z"/>
</vector>

View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
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.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="40dp"
android:height="40dp"
android:viewportWidth="40"
android:viewportHeight="40">
<path
android:fillColor="#5F6368"
android:pathData="M24.6667 16.2733L23.7267 15.3333L20 19.06L16.2734 15.3333L15.3334 16.2733L19.06 20L15.3334 23.7267L16.2734 24.6667L20 20.94L23.7267 24.6667L24.6667 23.7267L20.94 20L24.6667 16.2733Z"/>
</vector>

View File

@@ -0,0 +1,66 @@
<!--
~ 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
-->
<!-- Extends FrameLayout -->
<com.android.systemui.statusbar.notification.stack.SectionHeaderView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="@dimen/notification_section_header_height"
android:focusable="true"
android:clickable="true"
>
<com.android.systemui.statusbar.notification.row.NotificationBackgroundView
android:id="@+id/backgroundNormal"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<com.android.systemui.statusbar.notification.row.NotificationBackgroundView
android:id="@+id/backgroundDimmed"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<LinearLayout
android:id="@+id/content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_vertical"
android:orientation="horizontal"
>
<TextView
android:id="@+id/header_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginLeft="@dimen/notification_section_header_padding_left"
android:text="@string/notification_section_header_gentle"
android:textSize="12sp"
android:textColor="@color/notification_section_header_label_color"
/>
<ImageView
android:id="@+id/btn_clear_all"
android:layout_width="@dimen/notification_section_header_height"
android:layout_height="@dimen/notification_section_header_height"
android:layout_marginRight="4dp"
android:src="@drawable/status_bar_notification_section_header_clear_btn"
android:contentDescription="@string/accessibility_notification_section_header_gentle_clear_all"
/>
</LinearLayout>
<com.android.systemui.statusbar.notification.FakeShadowView
android:id="@+id/fake_shadow"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</com.android.systemui.statusbar.notification.stack.SectionHeaderView>

View File

@@ -49,6 +49,8 @@
<color name="notification_guts_header_text_color">@color/GM2_grey_200</color>
<color name="notification_guts_button_color">@color/GM2_blue_200</color>
<color name="notification_section_header_label_color">@color/GM2_grey_200</color>
<!-- The color of the background in the top part of QSCustomizer -->
<color name="qs_customize_background">@color/GM2_grey_900</color>

View File

@@ -97,6 +97,8 @@
<color name="notification_alert_color">#FFF87B2B</color>
<color name="notification_guts_button_color">@color/GM2_blue_700</color>
<color name="notification_section_header_label_color">@color/GM2_grey_900</color>
<color name="assist_orb_color">#ffffff</color>
<color name="keyguard_user_switcher_background_gradient_color">#77000000</color>

View File

@@ -695,6 +695,9 @@
<!-- The top padding of the clear all button -->
<dimen name="clear_all_padding_top">12dp</dimen>
<dimen name="notification_section_header_height">40dp</dimen>
<dimen name="notification_section_header_padding_left">16dp</dimen>
<!-- Largest size an avatar might need to be drawn in the user picker, status bar, or
quick settings header -->
<dimen name="max_avatar_size">48dp</dimen>

View File

@@ -1102,6 +1102,12 @@
<!-- The text for the manage notifications link. [CHAR LIMIT=40] -->
<string name="manage_notifications_text">Manage</string>
<!-- Section title for notifications that do not vibrate or make noise. [CHAR LIMIT=40] -->
<string name="notification_section_header_gentle">Gentle notifications</string>
<!-- Content description for accessibility: Tapping this button will dismiss all gentle notifications [CHAR LIMIT=NONE] -->
<string name="accessibility_notification_section_header_gentle_clear_all">Clear all gentle notifications</string>
<!-- The text to show in the notifications shade when dnd is suppressing notifications. [CHAR LIMIT=100] -->
<string name="dnd_suppressing_shade_text">Notifications paused by Do Not Disturb</string>

View File

@@ -16,6 +16,7 @@
package com.android.systemui.statusbar.notification.stack;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
import android.util.MathUtils;
@@ -30,18 +31,18 @@ import com.android.systemui.statusbar.notification.collection.NotificationEntry;
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.stack.StackScrollAlgorithm.SectionProvider;
import java.util.ArrayList;
import java.util.List;
/**
* A global state to track all input states for the algorithm.
*/
public class AmbientState {
private static final int NO_SECTION_BOUNDARY = -1;
private static final float MAX_PULSE_HEIGHT = 100000f;
private final SectionProvider mSectionProvider;
private ArrayList<ExpandableView> mDraggedViews = new ArrayList<>();
private int mScrollY;
private int mAnchorViewIndex;
@@ -51,7 +52,6 @@ public class AmbientState {
private float mOverScrollTopAmount;
private float mOverScrollBottomAmount;
private int mSpeedBumpIndex = -1;
private final List<Integer> mSectionBoundaryIndices = new ArrayList<>();
private boolean mDark;
private boolean mHideSensitive;
private AmbientPulseManager mAmbientPulseManager = Dependency.get(AmbientPulseManager.class);
@@ -84,8 +84,10 @@ public class AmbientState {
private float mPulseHeight = MAX_PULSE_HEIGHT;
private float mDozeAmount = 0.0f;
public AmbientState(Context context) {
mSectionBoundaryIndices.add(NO_SECTION_BOUNDARY);
public AmbientState(
Context context,
@NonNull SectionProvider sectionProvider) {
mSectionProvider = sectionProvider;
reload(context);
}
@@ -245,25 +247,8 @@ public class AmbientState {
mSpeedBumpIndex = shelfIndex;
}
/**
* Returns the index of the boundary between two sections, where the first section is at index
* {@code boundaryNum}.
*/
public int getSectionBoundaryIndex(int boundaryNum) {
return mSectionBoundaryIndices.get(boundaryNum);
}
/** Returns true if the item at {@code index} is directly below a section boundary. */
public boolean beginsNewSection(int index) {
return mSectionBoundaryIndices.contains(index);
}
/**
* Sets the index of the boundary between the section at {@code boundaryNum} and the following
* section to {@code boundaryIndex}.
*/
public void setSectionBoundaryIndex(int boundaryNum, int boundaryIndex) {
mSectionBoundaryIndices.set(boundaryNum, boundaryIndex);
public SectionProvider getSectionProvider() {
return mSectionProvider;
}
public float getStackTranslation() {

View File

@@ -0,0 +1,246 @@
/*
* 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 android.annotation.Nullable;
import android.content.Context;
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.plugins.ActivityStarter;
import com.android.systemui.statusbar.notification.row.ActivatableNotificationView;
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
/**
* 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 implements StackScrollAlgorithm.SectionProvider {
private final NotificationStackScrollLayout mParent;
private final ActivityStarter mActivityStarter;
private final boolean mUseMultipleSections;
private SectionHeaderView mGentleHeader;
private boolean mGentleHeaderVisible = false;
@Nullable private View.OnClickListener mOnClearGentleNotifsClickListener;
NotificationSectionsManager(
NotificationStackScrollLayout parent,
ActivityStarter activityStarter,
boolean useMultipleSections) {
mParent = parent;
mActivityStarter = activityStarter;
mUseMultipleSections = useMultipleSections;
}
/**
* Must be called before use. Should be called again whenever inflation-related things change,
* such as density or theme changes.
*/
void inflateViews(Context context) {
int oldPos = -1;
if (mGentleHeader != null) {
if (mGentleHeader.getTransientContainer() != null) {
mGentleHeader.getTransientContainer().removeView(mGentleHeader);
} else if (mGentleHeader.getParent() != null) {
oldPos = mParent.indexOfChild(mGentleHeader);
mParent.removeView(mGentleHeader);
}
}
mGentleHeader = (SectionHeaderView) LayoutInflater.from(context).inflate(
R.layout.status_bar_notification_section_header, mParent, false);
mGentleHeader.setOnHeaderClickListener(this::onGentleHeaderClick);
mGentleHeader.setOnClearAllClickListener(this::onClearGentleNotifsClick);
if (oldPos != -1) {
mParent.addView(mGentleHeader, oldPos);
}
}
/** Listener for when the "clear all" buttton is clciked on the gentle notification header. */
void setOnClearGentleNotifsClickListener(View.OnClickListener listener) {
mOnClearGentleNotifsClickListener = listener;
}
/** Must be called whenever the UI mode changes (i.e. when we enter night mode). */
void onUiModeChanged() {
mGentleHeader.onUiModeChanged();
}
@Override
public boolean beginsSection(View view) {
return view == mGentleHeader;
}
/**
* Should be called whenever notifs are added, removed, or updated. Updates section boundary
* bookkeeping and adds/moves/removes section headers if appropriate.
*/
void updateSectionBoundaries() {
if (!mUseMultipleSections) {
return;
}
int firstGentleNotifIndex = -1;
final int n = mParent.getChildCount();
for (int i = 0; i < n; i++) {
View child = mParent.getChildAt(i);
if (child instanceof ExpandableNotificationRow
&& child.getVisibility() != View.GONE) {
ExpandableNotificationRow row = (ExpandableNotificationRow) child;
if (!row.getEntry().isHighPriority()) {
firstGentleNotifIndex = i;
break;
}
}
}
adjustGentleHeaderVisibilityAndPosition(firstGentleNotifIndex);
mGentleHeader.setAreThereDismissableGentleNotifs(
mParent.hasActiveClearableNotifications(ROWS_GENTLE));
}
private void adjustGentleHeaderVisibilityAndPosition(int firstGentleNotifIndex) {
final int currentHeaderIndex = mParent.indexOfChild(mGentleHeader);
if (firstGentleNotifIndex == -1) {
if (mGentleHeaderVisible) {
mGentleHeaderVisible = false;
mParent.removeView(mGentleHeader);
}
} else {
if (!mGentleHeaderVisible) {
mGentleHeaderVisible = true;
// 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 (mGentleHeader.getTransientContainer() != null) {
mGentleHeader.getTransientContainer().removeTransientView(mGentleHeader);
mGentleHeader.setTransientContainer(null);
}
mParent.addView(mGentleHeader, firstGentleNotifIndex);
} else if (currentHeaderIndex != firstGentleNotifIndex - 1) {
// Relocate the header to be immediately before the first child in the section
int targetIndex = firstGentleNotifIndex;
if (currentHeaderIndex < firstGentleNotifIndex) {
// Adjust the target index to account for the header itself being temporarily
// removed during the position change.
targetIndex--;
}
mParent.changeViewPosition(mGentleHeader, targetIndex);
}
}
}
/**
* Updates the boundaries (as tracked by their first and last views) of the high and low
* priority sections.
*
* @return {@code true} If the last view in the top section changed (so we need to animate).
*/
boolean updateFirstAndLastViewsInSections(
final NotificationSection highPrioritySection,
final NotificationSection lowPrioritySection,
ActivatableNotificationView firstChild,
ActivatableNotificationView lastChild) {
if (mUseMultipleSections) {
ActivatableNotificationView previousLastHighPriorityChild =
highPrioritySection.getLastVisibleChild();
ActivatableNotificationView previousFirstLowPriorityChild =
lowPrioritySection.getFirstVisibleChild();
ActivatableNotificationView lastHighPriorityChild = getLastHighPriorityChild();
ActivatableNotificationView firstLowPriorityChild = getFirstLowPriorityChild();
if (lastHighPriorityChild != null && firstLowPriorityChild != null) {
highPrioritySection.setFirstVisibleChild(firstChild);
highPrioritySection.setLastVisibleChild(lastHighPriorityChild);
lowPrioritySection.setFirstVisibleChild(firstLowPriorityChild);
lowPrioritySection.setLastVisibleChild(lastChild);
} else if (lastHighPriorityChild != null) {
highPrioritySection.setFirstVisibleChild(firstChild);
highPrioritySection.setLastVisibleChild(lastChild);
lowPrioritySection.setFirstVisibleChild(null);
lowPrioritySection.setLastVisibleChild(null);
} else {
highPrioritySection.setFirstVisibleChild(null);
highPrioritySection.setLastVisibleChild(null);
lowPrioritySection.setFirstVisibleChild(firstChild);
lowPrioritySection.setLastVisibleChild(lastChild);
}
return lastHighPriorityChild != previousLastHighPriorityChild
|| firstLowPriorityChild != previousFirstLowPriorityChild;
} else {
highPrioritySection.setFirstVisibleChild(firstChild);
highPrioritySection.setLastVisibleChild(lastChild);
return false;
}
}
@VisibleForTesting
SectionHeaderView getGentleHeaderView() {
return mGentleHeader;
}
@Nullable
private ActivatableNotificationView getFirstLowPriorityChild() {
return mGentleHeaderVisible ? mGentleHeader : null;
}
@Nullable
private ActivatableNotificationView getLastHighPriorityChild() {
ActivatableNotificationView lastChildBeforeGap = null;
int childCount = mParent.getChildCount();
for (int i = 0; i < childCount; i++) {
View child = mParent.getChildAt(i);
if (child.getVisibility() != View.GONE && child instanceof ExpandableNotificationRow) {
ExpandableNotificationRow row = (ExpandableNotificationRow) child;
if (!row.getEntry().isHighPriority()) {
break;
} else {
lastChildBeforeGap = row;
}
}
}
return lastChildBeforeGap;
}
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);
}
}
}

View File

@@ -23,10 +23,13 @@ import static com.android.systemui.statusbar.notification.stack.StackStateAnimat
import static com.android.systemui.statusbar.phone.NotificationIconAreaController.LOW_PRIORITY;
import static com.android.systemui.util.InjectionInflationController.VIEW_CONTEXT;
import static java.lang.annotation.RetentionPolicy.SOURCE;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.TimeAnimator;
import android.animation.ValueAnimator;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.WallpaperManager;
@@ -87,6 +90,7 @@ import com.android.systemui.SwipeHelper;
import com.android.systemui.classifier.FalsingManagerFactory;
import com.android.systemui.classifier.FalsingManagerFactory.FalsingManager;
import com.android.systemui.colorextraction.SysuiColorExtractor;
import com.android.systemui.plugins.ActivityStarter;
import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin;
import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin.MenuItem;
import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin.OnMenuEventListener;
@@ -142,6 +146,7 @@ import com.android.systemui.tuner.TunerService;
import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.lang.annotation.Retention;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
@@ -498,6 +503,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements ScrollAd
private final NotificationGutsManager
mNotificationGutsManager = Dependency.get(NotificationGutsManager.class);
private final NotificationSectionsManager mSectionsManager;
/**
* If the {@link NotificationShelf} should be visible when dark.
*/
@@ -511,7 +517,8 @@ public class NotificationStackScrollLayout extends ViewGroup implements ScrollAd
@Named(ALLOW_NOTIFICATION_LONG_PRESS_NAME) boolean allowLongPress,
NotificationRoundnessManager notificationRoundnessManager,
AmbientPulseManager ambientPulseManager,
DynamicPrivacyController dynamicPrivacyController) {
DynamicPrivacyController dynamicPrivacyController,
ActivityStarter activityStarter) {
super(context, attrs, 0, 0);
Resources res = getResources();
@@ -522,7 +529,20 @@ public class NotificationStackScrollLayout extends ViewGroup implements ScrollAd
}
mAmbientPulseManager = ambientPulseManager;
mAmbientState = new AmbientState(context);
mSectionsManager =
new NotificationSectionsManager(
this,
activityStarter,
NotificationUtils.useNewInterruptionModel(context));
mSectionsManager.inflateViews(context);
mSectionsManager.setOnClearGentleNotifsClickListener(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);
});
mAmbientState = new AmbientState(context, mSectionsManager);
mRoundnessManager = notificationRoundnessManager;
mBgColor = context.getColor(R.color.notification_shade_background_color);
int minHeight = res.getDimensionPixelSize(R.dimen.notification_min_height);
@@ -629,6 +649,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements ScrollAd
inflateFooterView();
inflateEmptyShadeView();
updateFooter();
mSectionsManager.inflateViews(mContext);
}
@Override
@@ -662,7 +683,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements ScrollAd
@VisibleForTesting
@ShadeViewRefactor(RefactorComponent.SHADE_VIEW)
public void updateFooter() {
boolean showDismissView = mClearAllEnabled && hasActiveClearableNotifications();
boolean showDismissView = mClearAllEnabled && hasActiveClearableNotifications(ROWS_ALL);
boolean showFooterView = (showDismissView ||
mEntryManager.getNotificationData().getActiveNotifications().size() != 0)
&& mStatusBarState != StatusBarState.KEYGUARD
@@ -675,14 +696,15 @@ public class NotificationStackScrollLayout extends ViewGroup implements ScrollAd
* Return whether there are any clearable notifications
*/
@ShadeViewRefactor(RefactorComponent.SHADE_VIEW)
public boolean hasActiveClearableNotifications() {
public boolean hasActiveClearableNotifications(@SelectedRows int selection) {
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
if (!(child instanceof ExpandableNotificationRow)) {
continue;
}
if (((ExpandableNotificationRow) child).canViewBeDismissed()) {
final ExpandableNotificationRow row = (ExpandableNotificationRow) child;
if (row.canViewBeDismissed() && matchesSelection(row, selection)) {
return true;
}
}
@@ -739,6 +761,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements ScrollAd
mBgColor = mContext.getColor(R.color.notification_shade_background_color);
updateBackgroundDimming();
mShelf.onUiModeChanged();
mSectionsManager.onUiModeChanged();
}
@ShadeViewRefactor(RefactorComponent.DECORATOR)
@@ -1684,11 +1707,6 @@ public class NotificationStackScrollLayout extends ViewGroup implements ScrollAd
return mScrollingEnabled;
}
@ShadeViewRefactor(RefactorComponent.ADAPTER)
private boolean canChildBeDismissed(View v) {
return StackScrollAlgorithm.canChildBeDismissed(v);
}
@ShadeViewRefactor(RefactorComponent.SHADE_VIEW)
private boolean onKeyguard() {
return mStatusBarState == StatusBarState.KEYGUARD;
@@ -2580,41 +2598,6 @@ public class NotificationStackScrollLayout extends ViewGroup implements ScrollAd
return null;
}
@ShadeViewRefactor(RefactorComponent.COORDINATOR)
@Nullable
private ActivatableNotificationView getLastHighPriorityChild() {
ActivatableNotificationView lastChildBeforeGap = null;
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
if (child.getVisibility() != View.GONE && child instanceof ExpandableNotificationRow) {
ExpandableNotificationRow row = (ExpandableNotificationRow) child;
if (!row.getEntry().isHighPriority()) {
break;
} else {
lastChildBeforeGap = row;
}
}
}
return lastChildBeforeGap;
}
@ShadeViewRefactor(RefactorComponent.COORDINATOR)
@Nullable
private ActivatableNotificationView getFirstLowPriorityChild() {
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
if (child.getVisibility() != View.GONE && child instanceof ExpandableNotificationRow) {
ExpandableNotificationRow row = (ExpandableNotificationRow) child;
if (!row.getEntry().isHighPriority()) {
return row;
}
}
}
return null;
}
/**
* Fling the scroll view
*
@@ -3180,7 +3163,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements ScrollAd
ActivatableNotificationView firstChild = getFirstChildWithBackground();
ActivatableNotificationView lastChild = getLastChildWithBackground();
boolean sectionViewsChanged = updateFirstAndLastViewsInSectionsByPriority(
boolean sectionViewsChanged = mSectionsManager.updateFirstAndLastViewsInSections(
mSections[0], mSections[1], firstChild, lastChild);
if (mAnimationsEnabled && mIsExpanded) {
@@ -3198,44 +3181,6 @@ public class NotificationStackScrollLayout extends ViewGroup implements ScrollAd
invalidate();
}
/** @return {@code true} if the last view in the top section changed (so we need to animate). */
private boolean updateFirstAndLastViewsInSectionsByPriority(
final NotificationSection highPrioritySection,
final NotificationSection lowPrioritySection,
ActivatableNotificationView firstChild,
ActivatableNotificationView lastChild) {
if (NotificationUtils.useNewInterruptionModel(mContext)) {
ActivatableNotificationView previousLastHighPriorityChild =
highPrioritySection.getLastVisibleChild();
ActivatableNotificationView previousFirstLowPriorityChild =
lowPrioritySection.getFirstVisibleChild();
ActivatableNotificationView lastHighPriorityChild = getLastHighPriorityChild();
ActivatableNotificationView firstLowPriorityChild = getFirstLowPriorityChild();
if (lastHighPriorityChild != null && firstLowPriorityChild != null) {
highPrioritySection.setFirstVisibleChild(firstChild);
highPrioritySection.setLastVisibleChild(lastHighPriorityChild);
lowPrioritySection.setFirstVisibleChild(firstLowPriorityChild);
lowPrioritySection.setLastVisibleChild(lastChild);
} else if (lastHighPriorityChild != null) {
highPrioritySection.setFirstVisibleChild(firstChild);
highPrioritySection.setLastVisibleChild(lastChild);
lowPrioritySection.setFirstVisibleChild(null);
lowPrioritySection.setLastVisibleChild(null);
} else {
highPrioritySection.setFirstVisibleChild(null);
highPrioritySection.setLastVisibleChild(null);
lowPrioritySection.setFirstVisibleChild(firstChild);
lowPrioritySection.setLastVisibleChild(lastChild);
}
return lastHighPriorityChild != previousLastHighPriorityChild
|| firstLowPriorityChild != previousFirstLowPriorityChild;
} else {
highPrioritySection.setFirstVisibleChild(firstChild);
highPrioritySection.setLastVisibleChild(lastChild);
return false;
}
}
@ShadeViewRefactor(RefactorComponent.COORDINATOR)
private void onViewAddedInternal(ExpandableView child) {
updateHideSensitiveForChild(child);
@@ -4595,11 +4540,6 @@ public class NotificationStackScrollLayout extends ViewGroup implements ScrollAd
return mAmbientState.isDimmed();
}
@VisibleForTesting
int getSectionBoundaryIndex(int boundaryNum) {
return mAmbientState.getSectionBoundaryIndex(boundaryNum);
}
@ShadeViewRefactor(RefactorComponent.SHADE_VIEW)
private void setDimAmount(float dimAmount) {
mDimAmount = dimAmount;
@@ -4984,7 +4924,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements ScrollAd
} else {
child.setMinClipTopAmount(0);
}
previousChildWillBeDismissed = canChildBeDismissed(child);
previousChildWillBeDismissed = StackScrollAlgorithm.canChildBeDismissed(child);
}
}
@@ -5540,7 +5480,9 @@ public class NotificationStackScrollLayout extends ViewGroup implements ScrollAd
}
@ShadeViewRefactor(RefactorComponent.SHADE_VIEW)
public void clearAllNotifications() {
private void clearNotifications(
@SelectedRows int selection,
boolean closeShade) {
// animate-swipe all dismissable notifications, then animate the shade closed
int numChildren = getChildCount();
@@ -5552,7 +5494,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements ScrollAd
ExpandableNotificationRow row = (ExpandableNotificationRow) child;
boolean parentVisible = false;
boolean hasClipBounds = child.getClipBounds(mTmpRect);
if (canChildBeDismissed(child)) {
if (includeChildInDismissAll(row, selection)) {
viewsToRemove.add(row);
if (child.getVisibility() == View.VISIBLE
&& (!hasClipBounds || mTmpRect.height() > 0)) {
@@ -5566,51 +5508,94 @@ public class NotificationStackScrollLayout extends ViewGroup implements ScrollAd
List<ExpandableNotificationRow> children = row.getNotificationChildren();
if (children != null) {
for (ExpandableNotificationRow childRow : children) {
viewsToRemove.add(childRow);
if (parentVisible && row.areChildrenExpanded()
&& canChildBeDismissed(childRow)) {
hasClipBounds = childRow.getClipBounds(mTmpRect);
if (childRow.getVisibility() == View.VISIBLE
&& (!hasClipBounds || mTmpRect.height() > 0)) {
viewsToHide.add(childRow);
if (includeChildInDismissAll(row, selection)) {
viewsToRemove.add(childRow);
if (parentVisible && row.areChildrenExpanded()) {
hasClipBounds = childRow.getClipBounds(mTmpRect);
if (childRow.getVisibility() == View.VISIBLE
&& (!hasClipBounds || mTmpRect.height() > 0)) {
viewsToHide.add(childRow);
}
}
}
}
}
}
}
if (viewsToRemove.isEmpty()) {
mStatusBar.animateCollapsePanels(CommandQueue.FLAG_EXCLUDE_NONE);
if (closeShade) {
mStatusBar.animateCollapsePanels(CommandQueue.FLAG_EXCLUDE_NONE);
}
return;
}
mShadeController.addPostCollapseAction(() -> {
setDismissAllInProgress(false);
performDismissAllAnimations(viewsToHide, closeShade, () -> {
for (ExpandableNotificationRow rowToRemove : viewsToRemove) {
if (canChildBeDismissed(rowToRemove)) {
mEntryManager.removeNotification(rowToRemove.getEntry().key, null /* ranking */,
NotificationListenerService.REASON_CANCEL_ALL);
if (StackScrollAlgorithm.canChildBeDismissed(rowToRemove)) {
if (selection == ROWS_ALL) {
// TODO: This is a listener method; we shouldn't be calling it. Can we just
// call performRemoveNotification as below?
mEntryManager.removeNotification(
rowToRemove.getEntry().key,
null /* ranking */,
NotificationListenerService.REASON_CANCEL_ALL);
} else {
mEntryManager.performRemoveNotification(
rowToRemove.getEntry().notification,
NotificationListenerService.REASON_CANCEL_ALL);
}
} else {
rowToRemove.resetTranslation();
}
}
try {
mBarService.onClearAllNotifications(mLockscreenUserManager.getCurrentUserId());
} catch (Exception ex) {
if (selection == ROWS_ALL) {
try {
mBarService.onClearAllNotifications(mLockscreenUserManager.getCurrentUserId());
} catch (Exception ex) {
}
}
});
performDismissAllAnimations(viewsToHide);
}
private boolean includeChildInDismissAll(
ExpandableNotificationRow row,
@SelectedRows int selection) {
return StackScrollAlgorithm.canChildBeDismissed(row) && matchesSelection(row, selection);
}
/**
* Given a list of rows, animates them away in a staggered fashion as if they were dismissed.
* Doesn't actually dismiss them, though -- that must be done in the onAnimationComplete
* handler.
*
* @param hideAnimatedList List of rows to animated away. Should only be views that are
* currently visible, or else the stagger will look funky.
* @param closeShade Whether to close the shade after the stagger animation completes.
* @param onAnimationComplete Called after the entire animation completes (including the shade
* closing if appropriate). The rows must be dismissed for real here.
*/
@ShadeViewRefactor(RefactorComponent.SHADE_VIEW)
public void performDismissAllAnimations(ArrayList<View> hideAnimatedList) {
Runnable animationFinishAction = () -> {
mStatusBar.animateCollapsePanels(CommandQueue.FLAG_EXCLUDE_NONE);
private void performDismissAllAnimations(
final ArrayList<View> hideAnimatedList,
final boolean closeShade,
final Runnable onAnimationComplete) {
final Runnable onSlideAwayAnimationComplete = () -> {
if (closeShade) {
mShadeController.addPostCollapseAction(() -> {
setDismissAllInProgress(false);
onAnimationComplete.run();
});
mStatusBar.animateCollapsePanels(CommandQueue.FLAG_EXCLUDE_NONE);
} else {
setDismissAllInProgress(false);
onAnimationComplete.run();
}
};
if (hideAnimatedList.isEmpty()) {
animationFinishAction.run();
onSlideAwayAnimationComplete.run();
return;
}
@@ -5627,7 +5612,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements ScrollAd
View view = hideAnimatedList.get(i);
Runnable endRunnable = null;
if (i == 0) {
endRunnable = animationFinishAction;
endRunnable = onSlideAwayAnimationComplete;
}
dismissViewAnimated(view, endRunnable, totalDelay, ANIMATION_DURATION_SWIPE);
currentDelay = Math.max(50, currentDelay - rowDelayDecrement);
@@ -5642,7 +5627,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements ScrollAd
R.layout.status_bar_notification_footer, this, false);
footerView.setDismissButtonClickListener(v -> {
mMetricsLogger.action(MetricsEvent.ACTION_DISMISS_ALL_NOTES);
clearAllNotifications();
clearNotifications(ROWS_ALL, true /* closeShade */);
});
footerView.setManageButtonClickListener(this::manageNotifications);
setFooterView(footerView);
@@ -5813,27 +5798,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements ScrollAd
/** Updates the indices of the boundaries between sections. */
@ShadeViewRefactor(RefactorComponent.INPUT)
public void updateSectionBoundaries() {
int gapIndex = -1;
if (NotificationUtils.useNewInterruptionModel(mContext)) {
int currentIndex = 0;
final int n = getChildCount();
for (int i = 0; i < n; i++) {
View view = getChildAt(i);
if (view.getVisibility() == View.GONE
|| !(view instanceof ExpandableNotificationRow)) {
continue;
}
ExpandableNotificationRow row = (ExpandableNotificationRow) view;
if (!row.getEntry().isHighPriority()) {
if (currentIndex > 0) {
gapIndex = currentIndex;
}
break;
}
currentIndex++;
}
}
mAmbientState.setSectionBoundaryIndex(0, gapIndex);
mSectionsManager.updateSectionBoundaries();
}
private void updateContinuousBackgroundDrawing() {
@@ -5869,6 +5834,21 @@ public class NotificationStackScrollLayout extends ViewGroup implements ScrollAd
mSwipeHelper.resetExposedMenuView(animate, force);
}
private static boolean matchesSelection(
ExpandableNotificationRow row,
@SelectedRows int selection) {
switch (selection) {
case ROWS_ALL:
return true;
case ROWS_HIGH_PRIORITY:
return row.getEntry().isHighPriority();
case ROWS_GENTLE:
return !row.getEntry().isHighPriority();
default:
throw new IllegalArgumentException("Unknown selection: " + selection);
}
}
@ShadeViewRefactor(RefactorComponent.STATE_RESOLVER)
static class AnimationEvent {
@@ -6353,7 +6333,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements ScrollAd
@Override
public boolean canChildBeDismissed(View v) {
return NotificationStackScrollLayout.this.canChildBeDismissed(v);
return StackScrollAlgorithm.canChildBeDismissed(v);
}
@Override
@@ -6560,4 +6540,15 @@ public class NotificationStackScrollLayout extends ViewGroup implements ScrollAd
public ExpandHelper.Callback getExpandHelperCallback() {
return mExpandHelperCallback;
}
/** Enum for selecting some or all notification rows (does not included non-notif views). */
@Retention(SOURCE)
@IntDef({ROWS_ALL, ROWS_HIGH_PRIORITY, ROWS_GENTLE})
public @interface SelectedRows {}
/** All rows representing notifs. */
public static final int ROWS_ALL = 0;
/** Only rows where entry.isHighPriority() is true. */
public static final int ROWS_HIGH_PRIORITY = 1;
/** Only rows where entry.isHighPriority() is false. */
public static final int ROWS_GENTLE = 2;
}

View File

@@ -0,0 +1,93 @@
/*
* 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.content.Context;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
import com.android.systemui.R;
import com.android.systemui.statusbar.notification.row.ActivatableNotificationView;
/**
* Similar in size and appearance to the NotificationShelf, appears at the beginning of some
* notification sections. Currently only used for gentle notifications.
*/
public class SectionHeaderView extends ActivatableNotificationView {
private View mContents;
private TextView mLabelView;
private ImageView mClearAllButton;
private final RectF mTmpRect = new RectF();
public SectionHeaderView(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mContents = findViewById(R.id.content);
mLabelView = findViewById(R.id.header_label);
mClearAllButton = findViewById(R.id.btn_clear_all);
}
@Override
protected View getContentView() {
return mContents;
}
/** Must be called whenever the UI mode changes (i.e. when we enter night mode). */
void onUiModeChanged() {
updateBackgroundColors();
mLabelView.setTextColor(
getContext().getColor(R.color.notification_section_header_label_color));
mClearAllButton.setImageResource(
R.drawable.status_bar_notification_section_header_clear_btn);
}
void setAreThereDismissableGentleNotifs(boolean areThereDismissableGentleNotifs) {
mClearAllButton.setVisibility(areThereDismissableGentleNotifs ? View.VISIBLE : View.GONE);
}
@Override
protected boolean disallowSingleClick(MotionEvent event) {
// Disallow single click on lockscreen if user is tapping on clear all button
mTmpRect.set(
mClearAllButton.getLeft(),
mClearAllButton.getTop(),
mClearAllButton.getLeft() + mClearAllButton.getWidth(),
mClearAllButton.getTop() + mClearAllButton.getHeight());
return mTmpRect.contains(event.getX(), event.getY());
}
/**
* Fired whenever the user clicks on the body of the header (e.g. no sub-buttons or anything).
*/
void setOnHeaderClickListener(View.OnClickListener listener) {
mContents.setOnClickListener(listener);
}
/** Fired when the user clicks on the "X" button on the far right of the header. */
void setOnClearAllClickListener(View.OnClickListener listener) {
mClearAllButton.setOnClickListener(listener);
}
}

View File

@@ -58,7 +58,9 @@ public class StackScrollAlgorithm {
private float mHeadsUpInset;
private int mPinnedZTranslationExtra;
public StackScrollAlgorithm(Context context, ViewGroup hostView) {
public StackScrollAlgorithm(
Context context,
ViewGroup hostView) {
mHostView = hostView;
initView(context);
}
@@ -364,22 +366,15 @@ public class StackScrollAlgorithm {
*/
private void updatePositionsForState(StackScrollAlgorithmState algorithmState,
AmbientState ambientState) {
if (ANCHOR_SCROLLING) {
float currentYPosition = algorithmState.anchorViewY;
int childCount = algorithmState.visibleChildren.size();
for (int i = algorithmState.anchorViewIndex; i < childCount; i++) {
if (i > algorithmState.anchorViewIndex && ambientState.beginsNewSection(i)) {
currentYPosition += mGapHeight;
}
currentYPosition = updateChild(i, algorithmState, ambientState, currentYPosition,
false /* reverse */);
}
currentYPosition = algorithmState.anchorViewY;
for (int i = algorithmState.anchorViewIndex - 1; i >= 0; i--) {
if (ambientState.beginsNewSection(i + 1)) {
currentYPosition -= mGapHeight;
}
currentYPosition = updateChild(i, algorithmState, ambientState, currentYPosition,
true /* reverse */);
}
@@ -388,9 +383,6 @@ public class StackScrollAlgorithm {
float currentYPosition = -algorithmState.scrollY;
int childCount = algorithmState.visibleChildren.size();
for (int i = 0; i < childCount; i++) {
if (ambientState.beginsNewSection(i)) {
currentYPosition += mGapHeight;
}
currentYPosition = updateChild(i, algorithmState, ambientState, currentYPosition,
false /* reverse */);
}
@@ -421,8 +413,15 @@ public class StackScrollAlgorithm {
float currentYPosition,
boolean reverse) {
ExpandableView child = algorithmState.visibleChildren.get(i);
final boolean applyGapHeight =
childNeedsGapHeight(ambientState.getSectionProvider(), algorithmState, i, child);
ExpandableViewState childViewState = child.getViewState();
childViewState.location = ExpandableViewState.LOCATION_UNKNOWN;
if (applyGapHeight && !reverse) {
currentYPosition += mGapHeight;
}
int paddingAfterChild = getPaddingAfterChild(algorithmState, child);
int childHeight = getMaxAllowedChildHeight(child);
if (reverse) {
@@ -459,6 +458,9 @@ public class StackScrollAlgorithm {
if (reverse) {
currentYPosition = childViewState.yTranslation;
if (applyGapHeight) {
currentYPosition -= mGapHeight;
}
} else {
currentYPosition = childViewState.yTranslation + childHeight + paddingAfterChild;
if (currentYPosition <= 0) {
@@ -473,6 +475,18 @@ public class StackScrollAlgorithm {
return currentYPosition;
}
private boolean childNeedsGapHeight(
SectionProvider sectionProvider,
StackScrollAlgorithmState algorithmState,
int visibleIndex,
View child) {
boolean needsGapHeight = sectionProvider.beginsSection(child) && visibleIndex > 0;
if (ANCHOR_SCROLLING) {
needsGapHeight &= visibleIndex != algorithmState.anchorViewIndex;
}
return needsGapHeight;
}
protected int getPaddingAfterChild(StackScrollAlgorithmState algorithmState,
ExpandableView child) {
return algorithmState.getPaddingAfterChild(child);
@@ -727,4 +741,15 @@ public class StackScrollAlgorithm {
}
}
/**
* Interface for telling the SSA when a new notification section begins (so it can add in
* appropriate margins).
*/
public interface SectionProvider {
/**
* True if this view starts a new "section" of notifications, such as the gentle
* notifications section. False if sections are not enabled.
*/
boolean beginsSection(View view);
}
}

View File

@@ -18,6 +18,7 @@ package com.android.systemui.statusbar.phone;
import static com.android.systemui.SysUiServiceProvider.getComponent;
import static com.android.systemui.statusbar.notification.ActivityLaunchAnimator.ExpandAnimationParameters;
import static com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.ROWS_ALL;
import static com.android.systemui.util.InjectionInflationController.VIEW_CONTEXT;
import android.animation.Animator;
@@ -2992,7 +2993,7 @@ public class NotificationPanelView extends PanelView implements
}
public boolean hasActiveClearableNotifications() {
return mNotificationStackScroller.hasActiveClearableNotifications();
return mNotificationStackScroller.hasActiveClearableNotifications(ROWS_ALL);
}
@Override

View File

@@ -0,0 +1,215 @@
/*
* 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 android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import androidx.test.filters.SmallTest;
import com.android.systemui.ActivityStarterDelegate;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
@SmallTest
@RunWith(AndroidTestingRunner.class)
@TestableLooper.RunWithLooper
public class NotificationSectionsManagerTest extends SysuiTestCase {
@Rule public final MockitoRule mockitoRule = MockitoJUnit.rule();
@Mock private NotificationStackScrollLayout mNssl;
@Mock private ActivityStarterDelegate mActivityStarterDelegate;
private NotificationSectionsManager mSectionsManager;
@Before
public void setUp() {
mSectionsManager = new NotificationSectionsManager(mNssl, mActivityStarterDelegate, true);
// Required in order for the header inflation to work properly
when(mNssl.generateLayoutParams(any(AttributeSet.class)))
.thenReturn(new ViewGroup.LayoutParams(WRAP_CONTENT, WRAP_CONTENT));
mSectionsManager.inflateViews(mContext);
when(mNssl.indexOfChild(any(View.class))).thenReturn(-1);
}
@Test
public void testInsertHeader() {
// GIVEN a stack with HI and LO rows but no section headers
setStackState(ChildType.HIPRI, ChildType.HIPRI, ChildType.HIPRI, ChildType.LOPRI);
// WHEN we update the section headers
mSectionsManager.updateSectionBoundaries();
// THEN a LO section header is added
verify(mNssl).addView(mSectionsManager.getGentleHeaderView(), 3);
}
@Test
public void testRemoveHeader() {
// GIVEN a stack that originally had a header between the HI and LO sections
setStackState(ChildType.HIPRI, ChildType.HIPRI, ChildType.LOPRI);
mSectionsManager.updateSectionBoundaries();
// WHEN the last LO row is replaced with a HI row
setStackState(ChildType.HIPRI, ChildType.HIPRI, ChildType.HEADER, ChildType.HIPRI);
clearInvocations(mNssl);
mSectionsManager.updateSectionBoundaries();
// THEN the LO section header is removed
verify(mNssl).removeView(mSectionsManager.getGentleHeaderView());
}
@Test
public void testDoNothingIfHeaderAlreadyRemoved() {
// GIVEN a stack with only HI rows
setStackState(ChildType.HIPRI, ChildType.HIPRI, ChildType.HIPRI);
// WHEN we update the sections headers
mSectionsManager.updateSectionBoundaries();
// THEN we don't add any section headers
verify(mNssl, never()).addView(eq(mSectionsManager.getGentleHeaderView()), anyInt());
}
@Test
public void testMoveHeaderForward() {
// GIVEN a stack that originally had a header between the HI and LO sections
setStackState(
ChildType.HIPRI,
ChildType.HIPRI,
ChildType.HIPRI,
ChildType.LOPRI);
mSectionsManager.updateSectionBoundaries();
// WHEN the LO section moves forward
setStackState(
ChildType.HIPRI,
ChildType.HIPRI,
ChildType.LOPRI,
ChildType.HEADER,
ChildType.LOPRI);
mSectionsManager.updateSectionBoundaries();
// THEN the LO section header is also moved forward
verify(mNssl).changeViewPosition(mSectionsManager.getGentleHeaderView(), 2);
}
@Test
public void testMoveHeaderBackward() {
// GIVEN a stack that originally had a header between the HI and LO sections
setStackState(
ChildType.HIPRI,
ChildType.LOPRI,
ChildType.LOPRI,
ChildType.LOPRI);
mSectionsManager.updateSectionBoundaries();
// WHEN the LO section moves backward
setStackState(
ChildType.HIPRI,
ChildType.HEADER,
ChildType.HIPRI,
ChildType.HIPRI,
ChildType.LOPRI);
mSectionsManager.updateSectionBoundaries();
// THEN the LO section header is also moved backward (with appropriate index shifting)
verify(mNssl).changeViewPosition(mSectionsManager.getGentleHeaderView(), 3);
}
@Test
public void testHeaderRemovedFromTransientParent() {
// GIVEN a stack where the header is animating away
setStackState(
ChildType.HIPRI,
ChildType.LOPRI,
ChildType.LOPRI,
ChildType.LOPRI);
mSectionsManager.updateSectionBoundaries();
setStackState(
ChildType.HIPRI,
ChildType.HEADER);
mSectionsManager.updateSectionBoundaries();
clearInvocations(mNssl);
ViewGroup transientParent = mock(ViewGroup.class);
mSectionsManager.getGentleHeaderView().setTransientContainer(transientParent);
// WHEN the LO section reappears
setStackState(
ChildType.HIPRI,
ChildType.LOPRI);
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);
}
private enum ChildType { HEADER, HIPRI, LOPRI }
private void setStackState(ChildType... children) {
when(mNssl.getChildCount()).thenReturn(children.length);
for (int i = 0; i < children.length; i++) {
View child;
switch (children[i]) {
case HEADER:
child = mSectionsManager.getGentleHeaderView();
break;
case HIPRI:
case LOPRI:
ExpandableNotificationRow notifRow = mock(ExpandableNotificationRow.class,
RETURNS_DEEP_STUBS);
when(notifRow.getVisibility()).thenReturn(View.VISIBLE);
when(notifRow.getEntry().isHighPriority())
.thenReturn(children[i] == ChildType.HIPRI);
when(notifRow.getParent()).thenReturn(mNssl);
child = notifRow;
break;
default:
throw new RuntimeException("Unknown ChildType: " + children[i]);
}
when(mNssl.getChildAt(i)).thenReturn(child);
when(mNssl.indexOfChild(child)).thenReturn(i);
}
}
}

View File

@@ -46,6 +46,7 @@ import androidx.test.runner.AndroidJUnit4;
import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.nano.MetricsProto;
import com.android.systemui.ActivityStarterDelegate;
import com.android.systemui.Dependency;
import com.android.systemui.ExpandHelper;
import com.android.systemui.InitController;
@@ -118,6 +119,11 @@ public class NotificationStackScrollLayoutTest extends SysuiTestCase {
@Before
@UiThreadTest
public void setUp() throws Exception {
mOriginalInterruptionModelSetting = Settings.Secure.getInt(mContext.getContentResolver(),
NOTIFICATION_NEW_INTERRUPTION_MODEL, 0);
Settings.Secure.putInt(mContext.getContentResolver(),
NOTIFICATION_NEW_INTERRUPTION_MODEL, 1);
// Inject dependencies before initializing the layout
mDependency.injectTestDependency(
NotificationBlockingHelperManager.class,
@@ -146,7 +152,8 @@ public class NotificationStackScrollLayoutTest extends SysuiTestCase {
mStackScrollerInternal = new NotificationStackScrollLayout(getContext(), null,
true /* allowLongPress */, mNotificationRoundnessManager,
new AmbientPulseManager(mContext),
mock(DynamicPrivacyController.class));
mock(DynamicPrivacyController.class),
mock(ActivityStarterDelegate.class));
mStackScroller = spy(mStackScrollerInternal);
mStackScroller.setShelf(notificationShelf);
mStackScroller.setStatusBar(mBar);
@@ -166,11 +173,6 @@ public class NotificationStackScrollLayoutTest extends SysuiTestCase {
doNothing().when(mExpandHelper).cancelImmediately();
doNothing().when(notificationShelf).setAnimationsEnabled(anyBoolean());
doNothing().when(notificationShelf).fadeInTranslating();
mOriginalInterruptionModelSetting = Settings.Secure.getInt(mContext.getContentResolver(),
NOTIFICATION_NEW_INTERRUPTION_MODEL, 0);
Settings.Secure.putInt(mContext.getContentResolver(),
NOTIFICATION_NEW_INTERRUPTION_MODEL, 1);
}
@After
@@ -349,62 +351,6 @@ public class NotificationStackScrollLayoutTest extends SysuiTestCase {
verify(mStackScroller).changeViewPosition(any(FooterView.class), eq(-1 /* end */));
}
@Test
public void testUpdateGapIndex_allHighPriority() {
when(mStackScroller.getChildCount()).thenReturn(3);
for (int i = 0; i < 3; i++) {
ExpandableNotificationRow row = mock(ExpandableNotificationRow.class,
RETURNS_DEEP_STUBS);
String key = Integer.toString(i);
when(row.getStatusBarNotification().getKey()).thenReturn(key);
when(row.getEntry().isHighPriority()).thenReturn(true);
when(mStackScroller.getChildAt(i)).thenReturn(row);
}
mStackScroller.updateSectionBoundaries();
assertEquals(-1, mStackScroller.getSectionBoundaryIndex(0));
}
@Test
public void testUpdateGapIndex_allLowPriority() {
when(mStackScroller.getChildCount()).thenReturn(3);
for (int i = 0; i < 3; i++) {
ExpandableNotificationRow row = mock(ExpandableNotificationRow.class,
RETURNS_DEEP_STUBS);
String key = Integer.toString(i);
when(row.getStatusBarNotification().getKey()).thenReturn(key);
when(row.getEntry().isHighPriority()).thenReturn(false);
when(mStackScroller.getChildAt(i)).thenReturn(row);
}
mStackScroller.updateSectionBoundaries();
assertEquals(-1, mStackScroller.getSectionBoundaryIndex(0));
}
@Test
public void testUpdateGapIndex_gapExists() {
when(mStackScroller.getChildCount()).thenReturn(6);
for (int i = 0; i < 6; i++) {
ExpandableNotificationRow row = mock(ExpandableNotificationRow.class,
RETURNS_DEEP_STUBS);
String key = Integer.toString(i);
when(row.getStatusBarNotification().getKey()).thenReturn(key);
when(row.getEntry().isHighPriority()).thenReturn(i < 3);
when(mStackScroller.getChildAt(i)).thenReturn(row);
}
mStackScroller.updateSectionBoundaries();
assertEquals(3, mStackScroller.getSectionBoundaryIndex(0));
}
@Test
public void testUpdateGapIndex_empty() {
when(mStackScroller.getChildCount()).thenReturn(0);
mStackScroller.updateSectionBoundaries();
assertEquals(-1, mStackScroller.getSectionBoundaryIndex(0));
}
@Test
public void testOnDensityOrFontScaleChanged_reInflatesFooterViews() {
clearInvocations(mStackScroller);