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