diff --git a/packages/SystemUI/res/layout/status_bar_notification_section_header.xml b/packages/SystemUI/res/layout/status_bar_notification_section_header.xml
new file mode 100644
index 0000000000000..8fe80cb8956e8
--- /dev/null
+++ b/packages/SystemUI/res/layout/status_bar_notification_section_header.xml
@@ -0,0 +1,65 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/SystemUI/res/values-night/colors.xml b/packages/SystemUI/res/values-night/colors.xml
index 89d1a19737ee6..07d81c037e215 100644
--- a/packages/SystemUI/res/values-night/colors.xml
+++ b/packages/SystemUI/res/values-night/colors.xml
@@ -49,6 +49,8 @@
@color/GM2_grey_200
@color/GM2_blue_200
+ @color/GM2_grey_200
+
@color/GM2_grey_900
diff --git a/packages/SystemUI/res/values/colors.xml b/packages/SystemUI/res/values/colors.xml
index 88466ce286a89..a3bbf092f5c17 100644
--- a/packages/SystemUI/res/values/colors.xml
+++ b/packages/SystemUI/res/values/colors.xml
@@ -97,6 +97,8 @@
#FFF87B2B
@color/GM2_blue_700
+ @color/GM2_grey_900
+
#ffffff
#77000000
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index bb7b6630cb193..1c8b2baf2e899 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -692,6 +692,9 @@
12dp
+ 40dp
+ 16dp
+
48dp
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index dc35653e5f7d8..6ba72b6b85adb 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -1102,6 +1102,12 @@
Manage
+
+ Gentle notifications
+
+
+ Clear all gentle notifications
+
Notifications paused by Do Not Disturb
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java
index ce922801551ba..8c6d1015bd4d6 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java
@@ -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 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 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() {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManager.java
new file mode 100644
index 0000000000000..ed4a6ab9c69bf
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManager.java
@@ -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);
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
index 642e2e483d89f..81f3eeef491e9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
@@ -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() {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/SectionHeaderView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/SectionHeaderView.java
new file mode 100644
index 0000000000000..c32c529110df5
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/SectionHeaderView.java
@@ -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);
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
index f97a7e6531049..60061c6a9ad2a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
@@ -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);
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManagerTest.java
new file mode 100644
index 0000000000000..67c4a92bbb023
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManagerTest.java
@@ -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);
+ }
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
index 04f911a2b0e8b..09b8062390bd8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
@@ -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);