Loading packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManager.java +124 −69 Original line number Original line Diff line number Diff line Loading @@ -16,6 +16,8 @@ package com.android.systemui.statusbar.notification.stack; package com.android.systemui.statusbar.notification.stack; import static com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.ROWS_GENTLE; import static java.lang.annotation.RetentionPolicy.SOURCE; import static java.lang.annotation.RetentionPolicy.SOURCE; import android.annotation.IntDef; import android.annotation.IntDef; Loading Loading @@ -100,14 +102,11 @@ public class NotificationSectionsManager implements StackScrollAlgorithm.Section private boolean mInitialized = false; private boolean mInitialized = false; private SectionHeaderView mGentleHeader; private SectionHeaderView mGentleHeader; private boolean mGentleHeaderVisible; @Nullable private View.OnClickListener mOnClearGentleNotifsClickListener; @Nullable private View.OnClickListener mOnClearGentleNotifsClickListener; private SectionHeaderView mAlertingHeader; private SectionHeaderView mAlertingHeader; private boolean mAlertingHeaderVisible; private PeopleHubView mPeopleHubView; private PeopleHubView mPeopleHubView; private boolean mPeopleHeaderVisible; private boolean mPeopleHubVisible = false; private boolean mPeopleHubVisible = false; @Nullable private Subscription mPeopleHubSubscription; @Nullable private Subscription mPeopleHubSubscription; Loading Loading @@ -231,88 +230,135 @@ public class NotificationSectionsManager implements StackScrollAlgorithm.Section return; return; } } // The overall strategy here is to iterate over the current children of mParent, looking // for where the sections headers are currently positioned, and where each section begins. // Then, once we find the start of a new section, we track that position as the "target" for // the section header, adjusted for the case where existing headers are in front of that // target, but won't be once they are moved / removed after the pass has completed. final boolean showHeaders = mStatusBarStateController.getState() != StatusBarState.KEYGUARD; final boolean showHeaders = mStatusBarStateController.getState() != StatusBarState.KEYGUARD; final boolean usingPeopleFiltering = mSectionsFeatureManager.isFilteringEnabled(); final boolean usingPeopleFiltering = mSectionsFeatureManager.isFilteringEnabled(); boolean peopleNotifsPresent = false; boolean peopleNotifsPresent = false; int currentPeopleHeaderIdx = -1; int peopleHeaderTarget = -1; int peopleHeaderTarget = -1; int currentAlertingHeaderIdx = -1; int alertingHeaderTarget = -1; int alertingHeaderTarget = -1; int currentGentleHeaderIdx = -1; int gentleHeaderTarget = -1; int gentleHeaderTarget = -1; int viewCount = 0; int lastNotifIndex = 0; if (showHeaders) { final int childCount = mParent.getChildCount(); final int childCount = mParent.getChildCount(); for (int i = 0; i < childCount; i++) { for (int i = 0; i < childCount; i++) { View child = mParent.getChildAt(i); View child = mParent.getChildAt(i); if (child.getVisibility() == View.GONE || !(child instanceof ExpandableNotificationRow)) { // Track the existing positions of the headers if (child == mPeopleHubView) { currentPeopleHeaderIdx = i; continue; } if (child == mAlertingHeader) { currentAlertingHeaderIdx = i; continue; } if (child == mGentleHeader) { currentGentleHeaderIdx = i; continue; continue; } } if (!(child instanceof ExpandableNotificationRow)) { continue; } lastNotifIndex = i; ExpandableNotificationRow row = (ExpandableNotificationRow) child; ExpandableNotificationRow row = (ExpandableNotificationRow) child; // Once we enter a new section, calculate the target position for the header. switch (row.getEntry().getBucket()) { switch (row.getEntry().getBucket()) { case BUCKET_HEADS_UP: break; case BUCKET_PEOPLE: case BUCKET_PEOPLE: if (peopleHeaderTarget == -1) { peopleNotifsPresent = true; peopleNotifsPresent = true; peopleHeaderTarget = viewCount; if (showHeaders && peopleHeaderTarget == -1) { viewCount++; peopleHeaderTarget = i; // Offset the target if there are other headers before this that will be // moved. if (currentPeopleHeaderIdx != -1) { peopleHeaderTarget--; } if (currentAlertingHeaderIdx != -1) { peopleHeaderTarget--; } if (currentGentleHeaderIdx != -1) { peopleHeaderTarget--; } } } break; break; case BUCKET_ALERTING: case BUCKET_ALERTING: if (usingPeopleFiltering && alertingHeaderTarget == -1) { if (showHeaders && usingPeopleFiltering && alertingHeaderTarget == -1) { alertingHeaderTarget = viewCount; alertingHeaderTarget = i; viewCount++; // Offset the target if there are other headers before this that will be // moved. if (currentAlertingHeaderIdx != -1) { alertingHeaderTarget--; } if (currentGentleHeaderIdx != -1) { alertingHeaderTarget--; } } } break; break; case BUCKET_SILENT: case BUCKET_SILENT: if (gentleHeaderTarget == -1) { if (showHeaders && gentleHeaderTarget == -1) { gentleHeaderTarget = viewCount; gentleHeaderTarget = i; viewCount++; // Offset the target if there are other headers before this that will be // moved. if (currentGentleHeaderIdx != -1) { gentleHeaderTarget--; } } } break; break; default: throw new IllegalStateException("Cannot find section bucket for view"); } } viewCount++; } } if (usingPeopleFiltering && mPeopleHubVisible && peopleHeaderTarget == -1) { if (showHeaders && usingPeopleFiltering && mPeopleHubVisible && peopleHeaderTarget == -1) { // Insert the people header even if there are no people visible, in order to show // Insert the people header even if there are no people visible, in order to show // the hub. Put it directly above the next header. // the hub. Put it directly above the next header. if (alertingHeaderTarget != -1) { if (alertingHeaderTarget != -1) { peopleHeaderTarget = alertingHeaderTarget; peopleHeaderTarget = alertingHeaderTarget; alertingHeaderTarget++; gentleHeaderTarget++; } else if (gentleHeaderTarget != -1) { } else if (gentleHeaderTarget != -1) { peopleHeaderTarget = gentleHeaderTarget; peopleHeaderTarget = gentleHeaderTarget; gentleHeaderTarget++; } else { } else { // Put it at the end of the list. // Put it at the end of the list. peopleHeaderTarget = viewCount; peopleHeaderTarget = lastNotifIndex; } } } } } // Allow swiping the people header if the section is empty // Add headers in reverse order to preserve indices mPeopleHubView.setCanSwipe(mPeopleHubVisible && !peopleNotifsPresent); adjustHeaderVisibilityAndPosition( gentleHeaderTarget, mGentleHeader, currentGentleHeaderIdx); adjustHeaderVisibilityAndPosition( alertingHeaderTarget, mAlertingHeader, currentAlertingHeaderIdx); adjustHeaderVisibilityAndPosition( peopleHeaderTarget, mPeopleHubView, currentPeopleHeaderIdx); mPeopleHeaderVisible = adjustHeaderVisibilityAndPosition( // Update headers to reflect state of section contents peopleHeaderTarget, mPeopleHubView, mPeopleHeaderVisible); mGentleHeader.setAreThereDismissableGentleNotifs( mAlertingHeaderVisible = adjustHeaderVisibilityAndPosition( mParent.hasActiveClearableNotifications(ROWS_GENTLE)); alertingHeaderTarget, mAlertingHeader, mAlertingHeaderVisible); mPeopleHubView.setCanSwipe(showHeaders && mPeopleHubVisible && !peopleNotifsPresent); mGentleHeaderVisible = adjustHeaderVisibilityAndPosition( if (peopleHeaderTarget != currentPeopleHeaderIdx) { gentleHeaderTarget, mGentleHeader, mGentleHeaderVisible); mPeopleHubView.resetTranslation(); } } } private boolean adjustHeaderVisibilityAndPosition( private void adjustHeaderVisibilityAndPosition( int targetIndex, StackScrollerDecorView header, boolean isCurrentlyVisible) { int targetPosition, StackScrollerDecorView header, int currentPosition) { if (targetIndex == -1) { if (targetPosition == -1) { if (isCurrentlyVisible) { if (currentPosition != -1) { mParent.removeView(header); mParent.removeView(header); } } return false; } else { } else { if (header instanceof SwipeableView) { if (currentPosition == -1) { ((SwipeableView) header).resetTranslation(); } if (!isCurrentlyVisible) { // If the header is animating away, it will still have a parent, so detach it first // 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 // 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. // automatically when the view's intro animation starts, but it's a fragile link. Loading @@ -321,11 +367,10 @@ public class NotificationSectionsManager implements StackScrollAlgorithm.Section header.setTransientContainer(null); header.setTransientContainer(null); } } header.setContentVisible(true); header.setContentVisible(true); mParent.addView(header, targetIndex); mParent.addView(header, targetPosition); } else if (mParent.indexOfChild(header) != targetIndex) { } else { mParent.changeViewPosition(header, targetIndex); mParent.changeViewPosition(header, targetPosition); } } return true; } } } } Loading Loading @@ -400,10 +445,20 @@ public class NotificationSectionsManager implements StackScrollAlgorithm.Section @VisibleForTesting @VisibleForTesting SectionHeaderView getGentleHeaderView() { ExpandableView getGentleHeaderView() { return mGentleHeader; return mGentleHeader; } } @VisibleForTesting ExpandableView getAlertingHeaderView() { return mAlertingHeader; } @VisibleForTesting ExpandableView getPeopleHeaderView() { return mPeopleHubView; } private final ConfigurationListener mConfigurationListener = new ConfigurationListener() { private final ConfigurationListener mConfigurationListener = new ConfigurationListener() { @Override @Override public void onLocaleListChanged() { public void onLocaleListChanged() { Loading packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManagerTest.java +129 −47 Original line number Original line Diff line number Diff line Loading @@ -19,6 +19,8 @@ package com.android.systemui.statusbar.notification.stack; import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; import static com.android.systemui.statusbar.notification.stack.NotificationSectionsManager.BUCKET_ALERTING; import static com.android.systemui.statusbar.notification.stack.NotificationSectionsManager.BUCKET_ALERTING; import static com.android.systemui.statusbar.notification.stack.NotificationSectionsManager.BUCKET_HEADS_UP; import static com.android.systemui.statusbar.notification.stack.NotificationSectionsManager.BUCKET_PEOPLE; import static com.android.systemui.statusbar.notification.stack.NotificationSectionsManager.BUCKET_SILENT; import static com.android.systemui.statusbar.notification.stack.NotificationSectionsManager.BUCKET_SILENT; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any; Loading Loading @@ -107,7 +109,7 @@ public class NotificationSectionsManagerTest extends SysuiTestCase { @Test @Test public void testInsertHeader() { public void testInsertHeader() { // GIVEN a stack with HI and LO rows but no section headers // GIVEN a stack with HI and LO rows but no section headers setStackState(ChildType.HIPRI, ChildType.HIPRI, ChildType.HIPRI, ChildType.LOPRI); setStackState(ChildType.ALERTING, ChildType.ALERTING, ChildType.ALERTING, ChildType.GENTLE); // WHEN we update the section headers // WHEN we update the section headers mSectionsManager.updateSectionBoundaries(); mSectionsManager.updateSectionBoundaries(); Loading @@ -119,11 +121,15 @@ public class NotificationSectionsManagerTest extends SysuiTestCase { @Test @Test public void testRemoveHeader() { public void testRemoveHeader() { // GIVEN a stack that originally had a header between the HI and LO sections // GIVEN a stack that originally had a header between the HI and LO sections setStackState(ChildType.HIPRI, ChildType.HIPRI, ChildType.LOPRI); setStackState(ChildType.ALERTING, ChildType.ALERTING, ChildType.GENTLE); mSectionsManager.updateSectionBoundaries(); mSectionsManager.updateSectionBoundaries(); // WHEN the last LO row is replaced with a HI row // WHEN the last LO row is replaced with a HI row setStackState(ChildType.HIPRI, ChildType.HIPRI, ChildType.HEADER, ChildType.HIPRI); setStackState( ChildType.ALERTING, ChildType.ALERTING, ChildType.GENTLE_HEADER, ChildType.ALERTING); clearInvocations(mNssl); clearInvocations(mNssl); mSectionsManager.updateSectionBoundaries(); mSectionsManager.updateSectionBoundaries(); Loading @@ -134,7 +140,7 @@ public class NotificationSectionsManagerTest extends SysuiTestCase { @Test @Test public void testDoNothingIfHeaderAlreadyRemoved() { public void testDoNothingIfHeaderAlreadyRemoved() { // GIVEN a stack with only HI rows // GIVEN a stack with only HI rows setStackState(ChildType.HIPRI, ChildType.HIPRI, ChildType.HIPRI); setStackState(ChildType.ALERTING, ChildType.ALERTING, ChildType.ALERTING); // WHEN we update the sections headers // WHEN we update the sections headers mSectionsManager.updateSectionBoundaries(); mSectionsManager.updateSectionBoundaries(); Loading @@ -147,19 +153,19 @@ public class NotificationSectionsManagerTest extends SysuiTestCase { public void testMoveHeaderForward() { public void testMoveHeaderForward() { // GIVEN a stack that originally had a header between the HI and LO sections // GIVEN a stack that originally had a header between the HI and LO sections setStackState( setStackState( ChildType.HIPRI, ChildType.ALERTING, ChildType.HIPRI, ChildType.ALERTING, ChildType.HIPRI, ChildType.ALERTING, ChildType.LOPRI); ChildType.GENTLE); mSectionsManager.updateSectionBoundaries(); mSectionsManager.updateSectionBoundaries(); // WHEN the LO section moves forward // WHEN the LO section moves forward setStackState( setStackState( ChildType.HIPRI, ChildType.ALERTING, ChildType.HIPRI, ChildType.ALERTING, ChildType.LOPRI, ChildType.GENTLE, ChildType.HEADER, ChildType.GENTLE_HEADER, ChildType.LOPRI); ChildType.GENTLE); mSectionsManager.updateSectionBoundaries(); mSectionsManager.updateSectionBoundaries(); // THEN the LO section header is also moved forward // THEN the LO section header is also moved forward Loading @@ -170,19 +176,19 @@ public class NotificationSectionsManagerTest extends SysuiTestCase { public void testMoveHeaderBackward() { public void testMoveHeaderBackward() { // GIVEN a stack that originally had a header between the HI and LO sections // GIVEN a stack that originally had a header between the HI and LO sections setStackState( setStackState( ChildType.HIPRI, ChildType.ALERTING, ChildType.LOPRI, ChildType.GENTLE, ChildType.LOPRI, ChildType.GENTLE, ChildType.LOPRI); ChildType.GENTLE); mSectionsManager.updateSectionBoundaries(); mSectionsManager.updateSectionBoundaries(); // WHEN the LO section moves backward // WHEN the LO section moves backward setStackState( setStackState( ChildType.HIPRI, ChildType.ALERTING, ChildType.HEADER, ChildType.GENTLE_HEADER, ChildType.HIPRI, ChildType.ALERTING, ChildType.HIPRI, ChildType.ALERTING, ChildType.LOPRI); ChildType.GENTLE); mSectionsManager.updateSectionBoundaries(); mSectionsManager.updateSectionBoundaries(); // THEN the LO section header is also moved backward (with appropriate index shifting) // THEN the LO section header is also moved backward (with appropriate index shifting) Loading @@ -193,14 +199,14 @@ public class NotificationSectionsManagerTest extends SysuiTestCase { public void testHeaderRemovedFromTransientParent() { public void testHeaderRemovedFromTransientParent() { // GIVEN a stack where the header is animating away // GIVEN a stack where the header is animating away setStackState( setStackState( ChildType.HIPRI, ChildType.ALERTING, ChildType.LOPRI, ChildType.GENTLE, ChildType.LOPRI, ChildType.GENTLE, ChildType.LOPRI); ChildType.GENTLE); mSectionsManager.updateSectionBoundaries(); mSectionsManager.updateSectionBoundaries(); setStackState( setStackState( ChildType.HIPRI, ChildType.ALERTING, ChildType.HEADER); ChildType.GENTLE_HEADER); mSectionsManager.updateSectionBoundaries(); mSectionsManager.updateSectionBoundaries(); clearInvocations(mNssl); clearInvocations(mNssl); Loading @@ -209,8 +215,8 @@ public class NotificationSectionsManagerTest extends SysuiTestCase { // WHEN the LO section reappears // WHEN the LO section reappears setStackState( setStackState( ChildType.HIPRI, ChildType.ALERTING, ChildType.LOPRI); ChildType.GENTLE); mSectionsManager.updateSectionBoundaries(); mSectionsManager.updateSectionBoundaries(); // THEN the header is first removed from the transient parent before being added to the // THEN the header is first removed from the transient parent before being added to the Loading @@ -223,7 +229,7 @@ public class NotificationSectionsManagerTest extends SysuiTestCase { public void testHeaderNotShownOnLockscreen() { public void testHeaderNotShownOnLockscreen() { // GIVEN a stack of HI and LO notifs on the lockscreen // GIVEN a stack of HI and LO notifs on the lockscreen when(mStatusBarStateController.getState()).thenReturn(StatusBarState.KEYGUARD); when(mStatusBarStateController.getState()).thenReturn(StatusBarState.KEYGUARD); setStackState(ChildType.HIPRI, ChildType.HIPRI, ChildType.HIPRI, ChildType.LOPRI); setStackState(ChildType.ALERTING, ChildType.ALERTING, ChildType.ALERTING, ChildType.GENTLE); // WHEN we update the section headers // WHEN we update the section headers mSectionsManager.updateSectionBoundaries(); mSectionsManager.updateSectionBoundaries(); Loading @@ -236,7 +242,7 @@ public class NotificationSectionsManagerTest extends SysuiTestCase { public void testHeaderShownWhenEnterLockscreen() { public void testHeaderShownWhenEnterLockscreen() { // GIVEN a stack of HI and LO notifs on the lockscreen // GIVEN a stack of HI and LO notifs on the lockscreen when(mStatusBarStateController.getState()).thenReturn(StatusBarState.KEYGUARD); when(mStatusBarStateController.getState()).thenReturn(StatusBarState.KEYGUARD); setStackState(ChildType.HIPRI, ChildType.HIPRI, ChildType.HIPRI, ChildType.LOPRI); setStackState(ChildType.ALERTING, ChildType.ALERTING, ChildType.ALERTING, ChildType.GENTLE); mSectionsManager.updateSectionBoundaries(); mSectionsManager.updateSectionBoundaries(); // WHEN we unlock // WHEN we unlock Loading @@ -250,37 +256,104 @@ public class NotificationSectionsManagerTest extends SysuiTestCase { @Test @Test public void testHeaderHiddenWhenEnterLockscreen() { public void testHeaderHiddenWhenEnterLockscreen() { // GIVEN a stack of HI and LO notifs on the shade // GIVEN a stack of HI and LO notifs on the shade when(mStatusBarStateController.getState()).thenReturn(StatusBarState.SHADE_LOCKED); setStackState(ChildType.ALERTING, ChildType.GENTLE_HEADER, ChildType.GENTLE); setStackState(ChildType.HIPRI, ChildType.HIPRI, ChildType.HIPRI, ChildType.LOPRI); mSectionsManager.updateSectionBoundaries(); // WHEN we go back to the keyguard // WHEN we go back to the keyguard when(mStatusBarStateController.getState()).thenReturn(StatusBarState.KEYGUARD); when(mStatusBarStateController.getState()).thenReturn(StatusBarState.KEYGUARD); mSectionsManager.updateSectionBoundaries(); mSectionsManager.updateSectionBoundaries(); // Then the section header is removed // Then the section header is removed verify(mNssl).removeView(eq(mSectionsManager.getGentleHeaderView())); verify(mNssl).removeView(mSectionsManager.getGentleHeaderView()); } @Test public void testPeopleFiltering_addHeadersFromShowingOnlyGentle() { enablePeopleFiltering(); setStackState( ChildType.GENTLE_HEADER, ChildType.PERSON, ChildType.ALERTING, ChildType.GENTLE); mSectionsManager.updateSectionBoundaries(); verify(mNssl).changeViewPosition(mSectionsManager.getGentleHeaderView(), 2); verify(mNssl).addView(mSectionsManager.getAlertingHeaderView(), 1); verify(mNssl).addView(mSectionsManager.getPeopleHeaderView(), 0); } @Test public void testPeopleFiltering_addAllHeaders() { enablePeopleFiltering(); setStackState( ChildType.PERSON, ChildType.ALERTING, ChildType.GENTLE); mSectionsManager.updateSectionBoundaries(); verify(mNssl).addView(mSectionsManager.getGentleHeaderView(), 2); verify(mNssl).addView(mSectionsManager.getAlertingHeaderView(), 1); verify(mNssl).addView(mSectionsManager.getPeopleHeaderView(), 0); } @Test public void testPeopleFiltering_moveAllHeaders() { enablePeopleFiltering(); setStackState( ChildType.PEOPLE_HEADER, ChildType.ALERTING_HEADER, ChildType.GENTLE_HEADER, ChildType.PERSON, ChildType.ALERTING, ChildType.GENTLE); mSectionsManager.updateSectionBoundaries(); verify(mNssl).changeViewPosition(mSectionsManager.getGentleHeaderView(), 4); verify(mNssl).changeViewPosition(mSectionsManager.getAlertingHeaderView(), 2); verify(mNssl).changeViewPosition(mSectionsManager.getPeopleHeaderView(), 0); } } private enum ChildType { HEADER, HIPRI, LOPRI } private void enablePeopleFiltering() { when(mSectionsFeatureManager.isFilteringEnabled()).thenReturn(true); when(mSectionsFeatureManager.getNumberOfBuckets()).thenReturn(4); } private enum ChildType { PEOPLE_HEADER, ALERTING_HEADER, GENTLE_HEADER, HEADS_UP, PERSON, ALERTING, GENTLE, OTHER } private void setStackState(ChildType... children) { private void setStackState(ChildType... children) { when(mNssl.getChildCount()).thenReturn(children.length); when(mNssl.getChildCount()).thenReturn(children.length); for (int i = 0; i < children.length; i++) { for (int i = 0; i < children.length; i++) { View child; View child; switch (children[i]) { switch (children[i]) { case HEADER: case PEOPLE_HEADER: child = mSectionsManager.getPeopleHeaderView(); break; case ALERTING_HEADER: child = mSectionsManager.getAlertingHeaderView(); break; case GENTLE_HEADER: child = mSectionsManager.getGentleHeaderView(); child = mSectionsManager.getGentleHeaderView(); break; break; case HIPRI: case HEADS_UP: case LOPRI: child = mockNotification(BUCKET_HEADS_UP); ExpandableNotificationRow notifRow = mock(ExpandableNotificationRow.class, break; RETURNS_DEEP_STUBS); case PERSON: when(notifRow.getVisibility()).thenReturn(View.VISIBLE); child = mockNotification(BUCKET_PEOPLE); when(notifRow.getEntry().getBucket()).thenReturn( break; children[i] == ChildType.HIPRI ? BUCKET_ALERTING : BUCKET_SILENT); case ALERTING: when(notifRow.getParent()).thenReturn(mNssl); child = mockNotification(BUCKET_ALERTING); child = notifRow; break; case GENTLE: child = mockNotification(BUCKET_SILENT); break; case OTHER: child = mock(View.class); when(child.getVisibility()).thenReturn(View.VISIBLE); when(child.getParent()).thenReturn(mNssl); break; break; default: default: throw new RuntimeException("Unknown ChildType: " + children[i]); throw new RuntimeException("Unknown ChildType: " + children[i]); Loading @@ -289,4 +362,13 @@ public class NotificationSectionsManagerTest extends SysuiTestCase { when(mNssl.indexOfChild(child)).thenReturn(i); when(mNssl.indexOfChild(child)).thenReturn(i); } } } } private View mockNotification(int bucket) { ExpandableNotificationRow notifRow = mock(ExpandableNotificationRow.class, RETURNS_DEEP_STUBS); when(notifRow.getVisibility()).thenReturn(View.VISIBLE); when(notifRow.getEntry().getBucket()).thenReturn(bucket); when(notifRow.getParent()).thenReturn(mNssl); return notifRow; } } } Loading
packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManager.java +124 −69 Original line number Original line Diff line number Diff line Loading @@ -16,6 +16,8 @@ package com.android.systemui.statusbar.notification.stack; package com.android.systemui.statusbar.notification.stack; import static com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.ROWS_GENTLE; import static java.lang.annotation.RetentionPolicy.SOURCE; import static java.lang.annotation.RetentionPolicy.SOURCE; import android.annotation.IntDef; import android.annotation.IntDef; Loading Loading @@ -100,14 +102,11 @@ public class NotificationSectionsManager implements StackScrollAlgorithm.Section private boolean mInitialized = false; private boolean mInitialized = false; private SectionHeaderView mGentleHeader; private SectionHeaderView mGentleHeader; private boolean mGentleHeaderVisible; @Nullable private View.OnClickListener mOnClearGentleNotifsClickListener; @Nullable private View.OnClickListener mOnClearGentleNotifsClickListener; private SectionHeaderView mAlertingHeader; private SectionHeaderView mAlertingHeader; private boolean mAlertingHeaderVisible; private PeopleHubView mPeopleHubView; private PeopleHubView mPeopleHubView; private boolean mPeopleHeaderVisible; private boolean mPeopleHubVisible = false; private boolean mPeopleHubVisible = false; @Nullable private Subscription mPeopleHubSubscription; @Nullable private Subscription mPeopleHubSubscription; Loading Loading @@ -231,88 +230,135 @@ public class NotificationSectionsManager implements StackScrollAlgorithm.Section return; return; } } // The overall strategy here is to iterate over the current children of mParent, looking // for where the sections headers are currently positioned, and where each section begins. // Then, once we find the start of a new section, we track that position as the "target" for // the section header, adjusted for the case where existing headers are in front of that // target, but won't be once they are moved / removed after the pass has completed. final boolean showHeaders = mStatusBarStateController.getState() != StatusBarState.KEYGUARD; final boolean showHeaders = mStatusBarStateController.getState() != StatusBarState.KEYGUARD; final boolean usingPeopleFiltering = mSectionsFeatureManager.isFilteringEnabled(); final boolean usingPeopleFiltering = mSectionsFeatureManager.isFilteringEnabled(); boolean peopleNotifsPresent = false; boolean peopleNotifsPresent = false; int currentPeopleHeaderIdx = -1; int peopleHeaderTarget = -1; int peopleHeaderTarget = -1; int currentAlertingHeaderIdx = -1; int alertingHeaderTarget = -1; int alertingHeaderTarget = -1; int currentGentleHeaderIdx = -1; int gentleHeaderTarget = -1; int gentleHeaderTarget = -1; int viewCount = 0; int lastNotifIndex = 0; if (showHeaders) { final int childCount = mParent.getChildCount(); final int childCount = mParent.getChildCount(); for (int i = 0; i < childCount; i++) { for (int i = 0; i < childCount; i++) { View child = mParent.getChildAt(i); View child = mParent.getChildAt(i); if (child.getVisibility() == View.GONE || !(child instanceof ExpandableNotificationRow)) { // Track the existing positions of the headers if (child == mPeopleHubView) { currentPeopleHeaderIdx = i; continue; } if (child == mAlertingHeader) { currentAlertingHeaderIdx = i; continue; } if (child == mGentleHeader) { currentGentleHeaderIdx = i; continue; continue; } } if (!(child instanceof ExpandableNotificationRow)) { continue; } lastNotifIndex = i; ExpandableNotificationRow row = (ExpandableNotificationRow) child; ExpandableNotificationRow row = (ExpandableNotificationRow) child; // Once we enter a new section, calculate the target position for the header. switch (row.getEntry().getBucket()) { switch (row.getEntry().getBucket()) { case BUCKET_HEADS_UP: break; case BUCKET_PEOPLE: case BUCKET_PEOPLE: if (peopleHeaderTarget == -1) { peopleNotifsPresent = true; peopleNotifsPresent = true; peopleHeaderTarget = viewCount; if (showHeaders && peopleHeaderTarget == -1) { viewCount++; peopleHeaderTarget = i; // Offset the target if there are other headers before this that will be // moved. if (currentPeopleHeaderIdx != -1) { peopleHeaderTarget--; } if (currentAlertingHeaderIdx != -1) { peopleHeaderTarget--; } if (currentGentleHeaderIdx != -1) { peopleHeaderTarget--; } } } break; break; case BUCKET_ALERTING: case BUCKET_ALERTING: if (usingPeopleFiltering && alertingHeaderTarget == -1) { if (showHeaders && usingPeopleFiltering && alertingHeaderTarget == -1) { alertingHeaderTarget = viewCount; alertingHeaderTarget = i; viewCount++; // Offset the target if there are other headers before this that will be // moved. if (currentAlertingHeaderIdx != -1) { alertingHeaderTarget--; } if (currentGentleHeaderIdx != -1) { alertingHeaderTarget--; } } } break; break; case BUCKET_SILENT: case BUCKET_SILENT: if (gentleHeaderTarget == -1) { if (showHeaders && gentleHeaderTarget == -1) { gentleHeaderTarget = viewCount; gentleHeaderTarget = i; viewCount++; // Offset the target if there are other headers before this that will be // moved. if (currentGentleHeaderIdx != -1) { gentleHeaderTarget--; } } } break; break; default: throw new IllegalStateException("Cannot find section bucket for view"); } } viewCount++; } } if (usingPeopleFiltering && mPeopleHubVisible && peopleHeaderTarget == -1) { if (showHeaders && usingPeopleFiltering && mPeopleHubVisible && peopleHeaderTarget == -1) { // Insert the people header even if there are no people visible, in order to show // Insert the people header even if there are no people visible, in order to show // the hub. Put it directly above the next header. // the hub. Put it directly above the next header. if (alertingHeaderTarget != -1) { if (alertingHeaderTarget != -1) { peopleHeaderTarget = alertingHeaderTarget; peopleHeaderTarget = alertingHeaderTarget; alertingHeaderTarget++; gentleHeaderTarget++; } else if (gentleHeaderTarget != -1) { } else if (gentleHeaderTarget != -1) { peopleHeaderTarget = gentleHeaderTarget; peopleHeaderTarget = gentleHeaderTarget; gentleHeaderTarget++; } else { } else { // Put it at the end of the list. // Put it at the end of the list. peopleHeaderTarget = viewCount; peopleHeaderTarget = lastNotifIndex; } } } } } // Allow swiping the people header if the section is empty // Add headers in reverse order to preserve indices mPeopleHubView.setCanSwipe(mPeopleHubVisible && !peopleNotifsPresent); adjustHeaderVisibilityAndPosition( gentleHeaderTarget, mGentleHeader, currentGentleHeaderIdx); adjustHeaderVisibilityAndPosition( alertingHeaderTarget, mAlertingHeader, currentAlertingHeaderIdx); adjustHeaderVisibilityAndPosition( peopleHeaderTarget, mPeopleHubView, currentPeopleHeaderIdx); mPeopleHeaderVisible = adjustHeaderVisibilityAndPosition( // Update headers to reflect state of section contents peopleHeaderTarget, mPeopleHubView, mPeopleHeaderVisible); mGentleHeader.setAreThereDismissableGentleNotifs( mAlertingHeaderVisible = adjustHeaderVisibilityAndPosition( mParent.hasActiveClearableNotifications(ROWS_GENTLE)); alertingHeaderTarget, mAlertingHeader, mAlertingHeaderVisible); mPeopleHubView.setCanSwipe(showHeaders && mPeopleHubVisible && !peopleNotifsPresent); mGentleHeaderVisible = adjustHeaderVisibilityAndPosition( if (peopleHeaderTarget != currentPeopleHeaderIdx) { gentleHeaderTarget, mGentleHeader, mGentleHeaderVisible); mPeopleHubView.resetTranslation(); } } } private boolean adjustHeaderVisibilityAndPosition( private void adjustHeaderVisibilityAndPosition( int targetIndex, StackScrollerDecorView header, boolean isCurrentlyVisible) { int targetPosition, StackScrollerDecorView header, int currentPosition) { if (targetIndex == -1) { if (targetPosition == -1) { if (isCurrentlyVisible) { if (currentPosition != -1) { mParent.removeView(header); mParent.removeView(header); } } return false; } else { } else { if (header instanceof SwipeableView) { if (currentPosition == -1) { ((SwipeableView) header).resetTranslation(); } if (!isCurrentlyVisible) { // If the header is animating away, it will still have a parent, so detach it first // 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 // 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. // automatically when the view's intro animation starts, but it's a fragile link. Loading @@ -321,11 +367,10 @@ public class NotificationSectionsManager implements StackScrollAlgorithm.Section header.setTransientContainer(null); header.setTransientContainer(null); } } header.setContentVisible(true); header.setContentVisible(true); mParent.addView(header, targetIndex); mParent.addView(header, targetPosition); } else if (mParent.indexOfChild(header) != targetIndex) { } else { mParent.changeViewPosition(header, targetIndex); mParent.changeViewPosition(header, targetPosition); } } return true; } } } } Loading Loading @@ -400,10 +445,20 @@ public class NotificationSectionsManager implements StackScrollAlgorithm.Section @VisibleForTesting @VisibleForTesting SectionHeaderView getGentleHeaderView() { ExpandableView getGentleHeaderView() { return mGentleHeader; return mGentleHeader; } } @VisibleForTesting ExpandableView getAlertingHeaderView() { return mAlertingHeader; } @VisibleForTesting ExpandableView getPeopleHeaderView() { return mPeopleHubView; } private final ConfigurationListener mConfigurationListener = new ConfigurationListener() { private final ConfigurationListener mConfigurationListener = new ConfigurationListener() { @Override @Override public void onLocaleListChanged() { public void onLocaleListChanged() { Loading
packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManagerTest.java +129 −47 Original line number Original line Diff line number Diff line Loading @@ -19,6 +19,8 @@ package com.android.systemui.statusbar.notification.stack; import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; import static com.android.systemui.statusbar.notification.stack.NotificationSectionsManager.BUCKET_ALERTING; import static com.android.systemui.statusbar.notification.stack.NotificationSectionsManager.BUCKET_ALERTING; import static com.android.systemui.statusbar.notification.stack.NotificationSectionsManager.BUCKET_HEADS_UP; import static com.android.systemui.statusbar.notification.stack.NotificationSectionsManager.BUCKET_PEOPLE; import static com.android.systemui.statusbar.notification.stack.NotificationSectionsManager.BUCKET_SILENT; import static com.android.systemui.statusbar.notification.stack.NotificationSectionsManager.BUCKET_SILENT; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any; Loading Loading @@ -107,7 +109,7 @@ public class NotificationSectionsManagerTest extends SysuiTestCase { @Test @Test public void testInsertHeader() { public void testInsertHeader() { // GIVEN a stack with HI and LO rows but no section headers // GIVEN a stack with HI and LO rows but no section headers setStackState(ChildType.HIPRI, ChildType.HIPRI, ChildType.HIPRI, ChildType.LOPRI); setStackState(ChildType.ALERTING, ChildType.ALERTING, ChildType.ALERTING, ChildType.GENTLE); // WHEN we update the section headers // WHEN we update the section headers mSectionsManager.updateSectionBoundaries(); mSectionsManager.updateSectionBoundaries(); Loading @@ -119,11 +121,15 @@ public class NotificationSectionsManagerTest extends SysuiTestCase { @Test @Test public void testRemoveHeader() { public void testRemoveHeader() { // GIVEN a stack that originally had a header between the HI and LO sections // GIVEN a stack that originally had a header between the HI and LO sections setStackState(ChildType.HIPRI, ChildType.HIPRI, ChildType.LOPRI); setStackState(ChildType.ALERTING, ChildType.ALERTING, ChildType.GENTLE); mSectionsManager.updateSectionBoundaries(); mSectionsManager.updateSectionBoundaries(); // WHEN the last LO row is replaced with a HI row // WHEN the last LO row is replaced with a HI row setStackState(ChildType.HIPRI, ChildType.HIPRI, ChildType.HEADER, ChildType.HIPRI); setStackState( ChildType.ALERTING, ChildType.ALERTING, ChildType.GENTLE_HEADER, ChildType.ALERTING); clearInvocations(mNssl); clearInvocations(mNssl); mSectionsManager.updateSectionBoundaries(); mSectionsManager.updateSectionBoundaries(); Loading @@ -134,7 +140,7 @@ public class NotificationSectionsManagerTest extends SysuiTestCase { @Test @Test public void testDoNothingIfHeaderAlreadyRemoved() { public void testDoNothingIfHeaderAlreadyRemoved() { // GIVEN a stack with only HI rows // GIVEN a stack with only HI rows setStackState(ChildType.HIPRI, ChildType.HIPRI, ChildType.HIPRI); setStackState(ChildType.ALERTING, ChildType.ALERTING, ChildType.ALERTING); // WHEN we update the sections headers // WHEN we update the sections headers mSectionsManager.updateSectionBoundaries(); mSectionsManager.updateSectionBoundaries(); Loading @@ -147,19 +153,19 @@ public class NotificationSectionsManagerTest extends SysuiTestCase { public void testMoveHeaderForward() { public void testMoveHeaderForward() { // GIVEN a stack that originally had a header between the HI and LO sections // GIVEN a stack that originally had a header between the HI and LO sections setStackState( setStackState( ChildType.HIPRI, ChildType.ALERTING, ChildType.HIPRI, ChildType.ALERTING, ChildType.HIPRI, ChildType.ALERTING, ChildType.LOPRI); ChildType.GENTLE); mSectionsManager.updateSectionBoundaries(); mSectionsManager.updateSectionBoundaries(); // WHEN the LO section moves forward // WHEN the LO section moves forward setStackState( setStackState( ChildType.HIPRI, ChildType.ALERTING, ChildType.HIPRI, ChildType.ALERTING, ChildType.LOPRI, ChildType.GENTLE, ChildType.HEADER, ChildType.GENTLE_HEADER, ChildType.LOPRI); ChildType.GENTLE); mSectionsManager.updateSectionBoundaries(); mSectionsManager.updateSectionBoundaries(); // THEN the LO section header is also moved forward // THEN the LO section header is also moved forward Loading @@ -170,19 +176,19 @@ public class NotificationSectionsManagerTest extends SysuiTestCase { public void testMoveHeaderBackward() { public void testMoveHeaderBackward() { // GIVEN a stack that originally had a header between the HI and LO sections // GIVEN a stack that originally had a header between the HI and LO sections setStackState( setStackState( ChildType.HIPRI, ChildType.ALERTING, ChildType.LOPRI, ChildType.GENTLE, ChildType.LOPRI, ChildType.GENTLE, ChildType.LOPRI); ChildType.GENTLE); mSectionsManager.updateSectionBoundaries(); mSectionsManager.updateSectionBoundaries(); // WHEN the LO section moves backward // WHEN the LO section moves backward setStackState( setStackState( ChildType.HIPRI, ChildType.ALERTING, ChildType.HEADER, ChildType.GENTLE_HEADER, ChildType.HIPRI, ChildType.ALERTING, ChildType.HIPRI, ChildType.ALERTING, ChildType.LOPRI); ChildType.GENTLE); mSectionsManager.updateSectionBoundaries(); mSectionsManager.updateSectionBoundaries(); // THEN the LO section header is also moved backward (with appropriate index shifting) // THEN the LO section header is also moved backward (with appropriate index shifting) Loading @@ -193,14 +199,14 @@ public class NotificationSectionsManagerTest extends SysuiTestCase { public void testHeaderRemovedFromTransientParent() { public void testHeaderRemovedFromTransientParent() { // GIVEN a stack where the header is animating away // GIVEN a stack where the header is animating away setStackState( setStackState( ChildType.HIPRI, ChildType.ALERTING, ChildType.LOPRI, ChildType.GENTLE, ChildType.LOPRI, ChildType.GENTLE, ChildType.LOPRI); ChildType.GENTLE); mSectionsManager.updateSectionBoundaries(); mSectionsManager.updateSectionBoundaries(); setStackState( setStackState( ChildType.HIPRI, ChildType.ALERTING, ChildType.HEADER); ChildType.GENTLE_HEADER); mSectionsManager.updateSectionBoundaries(); mSectionsManager.updateSectionBoundaries(); clearInvocations(mNssl); clearInvocations(mNssl); Loading @@ -209,8 +215,8 @@ public class NotificationSectionsManagerTest extends SysuiTestCase { // WHEN the LO section reappears // WHEN the LO section reappears setStackState( setStackState( ChildType.HIPRI, ChildType.ALERTING, ChildType.LOPRI); ChildType.GENTLE); mSectionsManager.updateSectionBoundaries(); mSectionsManager.updateSectionBoundaries(); // THEN the header is first removed from the transient parent before being added to the // THEN the header is first removed from the transient parent before being added to the Loading @@ -223,7 +229,7 @@ public class NotificationSectionsManagerTest extends SysuiTestCase { public void testHeaderNotShownOnLockscreen() { public void testHeaderNotShownOnLockscreen() { // GIVEN a stack of HI and LO notifs on the lockscreen // GIVEN a stack of HI and LO notifs on the lockscreen when(mStatusBarStateController.getState()).thenReturn(StatusBarState.KEYGUARD); when(mStatusBarStateController.getState()).thenReturn(StatusBarState.KEYGUARD); setStackState(ChildType.HIPRI, ChildType.HIPRI, ChildType.HIPRI, ChildType.LOPRI); setStackState(ChildType.ALERTING, ChildType.ALERTING, ChildType.ALERTING, ChildType.GENTLE); // WHEN we update the section headers // WHEN we update the section headers mSectionsManager.updateSectionBoundaries(); mSectionsManager.updateSectionBoundaries(); Loading @@ -236,7 +242,7 @@ public class NotificationSectionsManagerTest extends SysuiTestCase { public void testHeaderShownWhenEnterLockscreen() { public void testHeaderShownWhenEnterLockscreen() { // GIVEN a stack of HI and LO notifs on the lockscreen // GIVEN a stack of HI and LO notifs on the lockscreen when(mStatusBarStateController.getState()).thenReturn(StatusBarState.KEYGUARD); when(mStatusBarStateController.getState()).thenReturn(StatusBarState.KEYGUARD); setStackState(ChildType.HIPRI, ChildType.HIPRI, ChildType.HIPRI, ChildType.LOPRI); setStackState(ChildType.ALERTING, ChildType.ALERTING, ChildType.ALERTING, ChildType.GENTLE); mSectionsManager.updateSectionBoundaries(); mSectionsManager.updateSectionBoundaries(); // WHEN we unlock // WHEN we unlock Loading @@ -250,37 +256,104 @@ public class NotificationSectionsManagerTest extends SysuiTestCase { @Test @Test public void testHeaderHiddenWhenEnterLockscreen() { public void testHeaderHiddenWhenEnterLockscreen() { // GIVEN a stack of HI and LO notifs on the shade // GIVEN a stack of HI and LO notifs on the shade when(mStatusBarStateController.getState()).thenReturn(StatusBarState.SHADE_LOCKED); setStackState(ChildType.ALERTING, ChildType.GENTLE_HEADER, ChildType.GENTLE); setStackState(ChildType.HIPRI, ChildType.HIPRI, ChildType.HIPRI, ChildType.LOPRI); mSectionsManager.updateSectionBoundaries(); // WHEN we go back to the keyguard // WHEN we go back to the keyguard when(mStatusBarStateController.getState()).thenReturn(StatusBarState.KEYGUARD); when(mStatusBarStateController.getState()).thenReturn(StatusBarState.KEYGUARD); mSectionsManager.updateSectionBoundaries(); mSectionsManager.updateSectionBoundaries(); // Then the section header is removed // Then the section header is removed verify(mNssl).removeView(eq(mSectionsManager.getGentleHeaderView())); verify(mNssl).removeView(mSectionsManager.getGentleHeaderView()); } @Test public void testPeopleFiltering_addHeadersFromShowingOnlyGentle() { enablePeopleFiltering(); setStackState( ChildType.GENTLE_HEADER, ChildType.PERSON, ChildType.ALERTING, ChildType.GENTLE); mSectionsManager.updateSectionBoundaries(); verify(mNssl).changeViewPosition(mSectionsManager.getGentleHeaderView(), 2); verify(mNssl).addView(mSectionsManager.getAlertingHeaderView(), 1); verify(mNssl).addView(mSectionsManager.getPeopleHeaderView(), 0); } @Test public void testPeopleFiltering_addAllHeaders() { enablePeopleFiltering(); setStackState( ChildType.PERSON, ChildType.ALERTING, ChildType.GENTLE); mSectionsManager.updateSectionBoundaries(); verify(mNssl).addView(mSectionsManager.getGentleHeaderView(), 2); verify(mNssl).addView(mSectionsManager.getAlertingHeaderView(), 1); verify(mNssl).addView(mSectionsManager.getPeopleHeaderView(), 0); } @Test public void testPeopleFiltering_moveAllHeaders() { enablePeopleFiltering(); setStackState( ChildType.PEOPLE_HEADER, ChildType.ALERTING_HEADER, ChildType.GENTLE_HEADER, ChildType.PERSON, ChildType.ALERTING, ChildType.GENTLE); mSectionsManager.updateSectionBoundaries(); verify(mNssl).changeViewPosition(mSectionsManager.getGentleHeaderView(), 4); verify(mNssl).changeViewPosition(mSectionsManager.getAlertingHeaderView(), 2); verify(mNssl).changeViewPosition(mSectionsManager.getPeopleHeaderView(), 0); } } private enum ChildType { HEADER, HIPRI, LOPRI } private void enablePeopleFiltering() { when(mSectionsFeatureManager.isFilteringEnabled()).thenReturn(true); when(mSectionsFeatureManager.getNumberOfBuckets()).thenReturn(4); } private enum ChildType { PEOPLE_HEADER, ALERTING_HEADER, GENTLE_HEADER, HEADS_UP, PERSON, ALERTING, GENTLE, OTHER } private void setStackState(ChildType... children) { private void setStackState(ChildType... children) { when(mNssl.getChildCount()).thenReturn(children.length); when(mNssl.getChildCount()).thenReturn(children.length); for (int i = 0; i < children.length; i++) { for (int i = 0; i < children.length; i++) { View child; View child; switch (children[i]) { switch (children[i]) { case HEADER: case PEOPLE_HEADER: child = mSectionsManager.getPeopleHeaderView(); break; case ALERTING_HEADER: child = mSectionsManager.getAlertingHeaderView(); break; case GENTLE_HEADER: child = mSectionsManager.getGentleHeaderView(); child = mSectionsManager.getGentleHeaderView(); break; break; case HIPRI: case HEADS_UP: case LOPRI: child = mockNotification(BUCKET_HEADS_UP); ExpandableNotificationRow notifRow = mock(ExpandableNotificationRow.class, break; RETURNS_DEEP_STUBS); case PERSON: when(notifRow.getVisibility()).thenReturn(View.VISIBLE); child = mockNotification(BUCKET_PEOPLE); when(notifRow.getEntry().getBucket()).thenReturn( break; children[i] == ChildType.HIPRI ? BUCKET_ALERTING : BUCKET_SILENT); case ALERTING: when(notifRow.getParent()).thenReturn(mNssl); child = mockNotification(BUCKET_ALERTING); child = notifRow; break; case GENTLE: child = mockNotification(BUCKET_SILENT); break; case OTHER: child = mock(View.class); when(child.getVisibility()).thenReturn(View.VISIBLE); when(child.getParent()).thenReturn(mNssl); break; break; default: default: throw new RuntimeException("Unknown ChildType: " + children[i]); throw new RuntimeException("Unknown ChildType: " + children[i]); Loading @@ -289,4 +362,13 @@ public class NotificationSectionsManagerTest extends SysuiTestCase { when(mNssl.indexOfChild(child)).thenReturn(i); when(mNssl.indexOfChild(child)).thenReturn(i); } } } } private View mockNotification(int bucket) { ExpandableNotificationRow notifRow = mock(ExpandableNotificationRow.class, RETURNS_DEEP_STUBS); when(notifRow.getVisibility()).thenReturn(View.VISIBLE); when(notifRow.getEntry().getBucket()).thenReturn(bucket); when(notifRow.getParent()).thenReturn(mNssl); return notifRow; } } }