Add "Gentle Notifications" header to gentle section

am: 9eb06333c4

Change-Id: Ib283b0375bef39f1aa153db9f345135bdb72ce29
This commit is contained in:
Ned Burns
2019-05-07 14:45:15 -07:00
committed by android-build-merger
12 changed files with 672 additions and 200 deletions

View File

@@ -0,0 +1,65 @@
<!--
~ 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:visibility="gone"
android:layout_width="@dimen/notification_section_header_height"
android:layout_height="@dimen/notification_section_header_height"
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,228 @@
/*
* 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.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;
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);
if (oldPos != -1) {
mParent.addView(mGentleHeader, oldPos);
}
}
/** 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);
}
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);
}
}

View File

@@ -87,6 +87,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;
@@ -498,6 +499,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 +513,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 +525,13 @@ public class NotificationStackScrollLayout extends ViewGroup implements ScrollAd
}
mAmbientPulseManager = ambientPulseManager;
mAmbientState = new AmbientState(context);
mSectionsManager =
new NotificationSectionsManager(
this,
activityStarter,
NotificationUtils.useNewInterruptionModel(context));
mSectionsManager.inflateViews(context);
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 +638,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements ScrollAd
inflateFooterView();
inflateEmptyShadeView();
updateFooter();
mSectionsManager.inflateViews(mContext);
}
@Override
@@ -739,6 +749,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)
@@ -2580,41 +2591,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 +3156,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 +3174,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 +4533,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;
@@ -5540,7 +5473,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements ScrollAd
}
@ShadeViewRefactor(RefactorComponent.SHADE_VIEW)
public void clearAllNotifications() {
private void clearAllNotifications() {
// animate-swipe all dismissable notifications, then animate the shade closed
int numChildren = getChildCount();
@@ -5813,27 +5746,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() {

View File

@@ -0,0 +1,82 @@
/*
* 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));
}
@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);
}
}

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

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