diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ListEntry.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ListEntry.java index fd0476b76a9ae..37eacada19fc2 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ListEntry.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ListEntry.java @@ -36,7 +36,7 @@ public abstract class ListEntry { private final ListAttachState mPreviousAttachState = ListAttachState.create(); private final ListAttachState mAttachState = ListAttachState.create(); - ListEntry(String key, long creationTime) { + protected ListEntry(String key, long creationTime) { mKey = key; mCreationTime = creationTime; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilder.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilder.java index e26fa045d297d..122fb1c2f6b89 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilder.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilder.java @@ -78,6 +78,8 @@ public class ShadeListBuilder implements Dumpable { private final SystemClock mSystemClock; private final ShadeListBuilderLogger mLogger; private final NotificationInteractionTracker mInteractionTracker; + // used exclusivly by ShadeListBuilder#notifySectionEntriesUpdated + private final ArrayList mTempSectionMembers = new ArrayList<>(); private List mNotifList = new ArrayList<>(); private List mNewNotifList = new ArrayList<>(); @@ -356,7 +358,7 @@ public class ShadeListBuilder implements Dumpable { // section by our list of custom comparators dispatchOnBeforeSort(mReadOnlyNotifList); mPipelineState.incrementTo(STATE_SORTING); - sortList(); + sortListAndNotifySections(); // Step 7: Lock in our group structure and log anything that's changed since the last run mPipelineState.incrementTo(STATE_FINALIZING); @@ -382,6 +384,22 @@ public class ShadeListBuilder implements Dumpable { mIterationCount++; } + private void notifySectionEntriesUpdated() { + NotifSection currentSection = null; + mTempSectionMembers.clear(); + for (int i = 0; i < mNotifList.size(); i++) { + ListEntry currentEntry = mNotifList.get(i); + if (currentSection != currentEntry.getSection()) { + if (currentSection != null) { + currentSection.getSectioner().onEntriesUpdated(mTempSectionMembers); + mTempSectionMembers.clear(); + } + currentSection = currentEntry.getSection(); + } + mTempSectionMembers.add(currentEntry); + } + } + /** * Points mNotifList to the list stored in mNewNotifList. * Reuses the (emptied) mNotifList as mNewNotifList. @@ -713,7 +731,7 @@ public class ShadeListBuilder implements Dumpable { } } - private void sortList() { + private void sortListAndNotifySections() { // Assign sections to top-level elements and sort their children for (ListEntry entry : mNotifList) { NotifSection section = applySections(entry); @@ -728,6 +746,9 @@ public class ShadeListBuilder implements Dumpable { // Finally, sort all top-level elements mNotifList.sort(mTopLevelComparator); + + // notify sections since the list is sorted now + notifySectionEntriesUpdated(); } private void freeEmptyGroups() { @@ -937,7 +958,6 @@ public class ShadeListBuilder implements Dumpable { } entry.getAttachState().setSection(finalSection); - return finalSection; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinator.java index d556e97c1bc6b..c62214ca16fae 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinator.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinator.java @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.notification.collection.coordinator; +import android.annotation.NonNull; import android.annotation.Nullable; import com.android.systemui.dagger.SysUISingleton; @@ -27,9 +28,12 @@ import com.android.systemui.statusbar.notification.collection.listbuilder.plugga import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSectioner; import com.android.systemui.statusbar.notification.collection.provider.HighPriorityProvider; import com.android.systemui.statusbar.notification.collection.render.NodeController; +import com.android.systemui.statusbar.notification.collection.render.SectionHeaderController; import com.android.systemui.statusbar.notification.dagger.AlertingHeader; import com.android.systemui.statusbar.notification.dagger.SilentHeader; +import java.util.List; + import javax.inject.Inject; /** @@ -44,7 +48,8 @@ public class RankingCoordinator implements Coordinator { public static final boolean SHOW_ALL_SECTIONS = false; private final StatusBarStateController mStatusBarStateController; private final HighPriorityProvider mHighPriorityProvider; - private final NodeController mSilentHeaderController; + private final NodeController mSilentNodeController; + private final SectionHeaderController mSilentHeaderController; private final NodeController mAlertingHeaderController; @Inject @@ -52,10 +57,12 @@ public class RankingCoordinator implements Coordinator { StatusBarStateController statusBarStateController, HighPriorityProvider highPriorityProvider, @AlertingHeader NodeController alertingHeaderController, - @SilentHeader NodeController silentHeaderController) { + @SilentHeader SectionHeaderController silentHeaderController, + @SilentHeader NodeController silentNodeController) { mStatusBarStateController = statusBarStateController; mHighPriorityProvider = highPriorityProvider; mAlertingHeaderController = alertingHeaderController; + mSilentNodeController = silentNodeController; mSilentHeaderController = silentHeaderController; } @@ -101,7 +108,19 @@ public class RankingCoordinator implements Coordinator { @Nullable @Override public NodeController getHeaderNodeController() { - return mSilentHeaderController; + return mSilentNodeController; + } + + @Nullable + @Override + public void onEntriesUpdated(@NonNull List entries) { + for (int i = 0; i < entries.size(); i++) { + if (entries.get(i).getRepresentativeEntry().getSbn().isClearable()) { + mSilentHeaderController.setClearSectionEnabled(true); + return; + } + } + mSilentHeaderController.setClearSectionEnabled(false); } }; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/pluggable/NotifSectioner.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/pluggable/NotifSectioner.java index c8982d35c4a01..58afca8043edd 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/pluggable/NotifSectioner.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/pluggable/NotifSectioner.java @@ -23,6 +23,8 @@ import com.android.systemui.statusbar.notification.collection.ShadeListBuilder; import com.android.systemui.statusbar.notification.collection.render.NodeController; import com.android.systemui.statusbar.notification.collection.render.NodeSpec; +import java.util.List; + /** * Pluggable for participating in notif sectioning. See {@link ShadeListBuilder#setSections}. */ @@ -46,4 +48,10 @@ public abstract class NotifSectioner extends Pluggable { public @Nullable NodeController getHeaderNodeController() { return null; } + + /** + * Notify of children of this section being updated + * @param entries of this section that are borrowed (must clone to store) + */ + public void onEntriesUpdated(List entries) {} } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/SectionHeaderController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/SectionHeaderController.kt index 1311e3e756dc5..8c15647c50389 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/SectionHeaderController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/SectionHeaderController.kt @@ -33,7 +33,8 @@ import javax.inject.Inject interface SectionHeaderController { fun reinflateView(parent: ViewGroup) val headerView: SectionHeaderView? - fun setOnClearAllClickListener(listener: View.OnClickListener) + fun setClearSectionEnabled(enabled: Boolean) + fun setOnClearSectionClickListener(listener: View.OnClickListener) } @SectionHeaderScope @@ -46,6 +47,7 @@ internal class SectionHeaderNodeControllerImpl @Inject constructor( ) : NodeController, SectionHeaderController { private var _view: SectionHeaderView? = null + private var clearAllButtonEnabled = false private var clearAllClickListener: View.OnClickListener? = null private val onHeaderClickListener = View.OnClickListener { activityStarter.startActivity( @@ -76,12 +78,18 @@ internal class SectionHeaderNodeControllerImpl @Inject constructor( parent.addView(inflated, oldPos) } _view = inflated + _view?.setClearSectionButtonEnabled(clearAllButtonEnabled) } override val headerView: SectionHeaderView? get() = _view - override fun setOnClearAllClickListener(listener: View.OnClickListener) { + override fun setClearSectionEnabled(enabled: Boolean) { + clearAllButtonEnabled = enabled + _view?.setClearSectionButtonEnabled(enabled) + } + + override fun setOnClearSectionClickListener(listener: View.OnClickListener) { clearAllClickListener = listener _view?.setOnClearAllClickListener(listener) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManager.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManager.kt index 45ce20a1f08fd..6ee14b53dd732 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManager.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManager.kt @@ -350,7 +350,7 @@ class NotificationSectionsManager @Inject internal constructor( silentHeaderView?.run { val hasActiveClearableNotifications = this@NotificationSectionsManager.parent .hasActiveClearableNotifications(NotificationStackScrollLayout.ROWS_GENTLE) - setAreThereDismissableGentleNotifs(hasActiveClearableNotifications) + setClearSectionButtonEnabled(hasActiveClearableNotifications) } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java index 444e5f3c69158..83e6ca0c8998d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java @@ -813,7 +813,7 @@ public class NotificationStackScrollLayoutController { mOnAttachStateChangeListener.onViewAttachedToWindow(mView); } mView.addOnAttachStateChangeListener(mOnAttachStateChangeListener); - mSilentHeaderController.setOnClearAllClickListener(v -> clearSilentNotifications()); + mSilentHeaderController.setOnClearSectionClickListener(v -> clearSilentNotifications()); } private boolean isInVisibleLocation(NotificationEntry entry) { 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 index 99ec7548fb9dc..baf09c70f9367 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/SectionHeaderView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/SectionHeaderView.java @@ -85,8 +85,12 @@ public class SectionHeaderView extends StackScrollerDecorView { return true; } - void setAreThereDismissableGentleNotifs(boolean areThereDismissableGentleNotifs) { - mClearAllButton.setVisibility(areThereDismissableGentleNotifs ? View.VISIBLE : View.GONE); + /** + * Show the clear section [X] button + * @param enabled + */ + public void setClearSectionButtonEnabled(boolean enabled) { + mClearAllButton.setVisibility(enabled ? View.VISIBLE : View.GONE); } @Override diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilderTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilderTest.java index 3378003b0d44e..c862f97681096 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilderTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilderTest.java @@ -33,6 +33,7 @@ import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static java.util.Collections.singletonList; @@ -42,6 +43,7 @@ import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; import android.util.ArrayMap; +import androidx.annotation.Nullable; import androidx.test.filters.SmallTest; import com.android.systemui.SysuiTestCase; @@ -78,6 +80,7 @@ import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.stream.Collectors; @SmallTest @@ -607,6 +610,30 @@ public class ShadeListBuilderTest extends SysuiTestCase { assertEquals(promoter2, mEntrySet.get(3).getNotifPromoter()); } + @Test + public void testNotifSectionsChildrenUpdated() { + AtomicBoolean validChildren = new AtomicBoolean(false); + final NotifSectioner pkg1Sectioner = spy(new PackageSectioner(PACKAGE_1) { + @Nullable + @Override + public void onEntriesUpdated(List entries) { + super.onEntriesUpdated(entries); + validChildren.set(entries.size() == 2); + } + }); + mListBuilder.setSectioners(Arrays.asList(pkg1Sectioner)); + + addNotif(0, PACKAGE_4); + addNotif(1, PACKAGE_1); + addNotif(2, PACKAGE_1); + addNotif(3, PACKAGE_3); + + dispatchBuild(); + + verify(pkg1Sectioner, times(1)).onEntriesUpdated(any()); + assertTrue(validChildren.get()); + } + @Test public void testNotifSections() { // GIVEN a filter that removes all PACKAGE_4 notifs and sections that divide diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinatorTest.java index 1031d6befc368..8f241a37c5ba4 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinatorTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinatorTest.java @@ -21,17 +21,22 @@ import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_NOTIFICAT import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import android.app.Notification; +import android.service.notification.StatusBarNotification; import android.testing.AndroidTestingRunner; +import androidx.annotation.Nullable; import androidx.test.filters.SmallTest; import com.android.systemui.SysuiTestCase; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.statusbar.RankingBuilder; +import com.android.systemui.statusbar.notification.collection.ListEntry; import com.android.systemui.statusbar.notification.collection.NotifPipeline; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder; @@ -39,6 +44,7 @@ import com.android.systemui.statusbar.notification.collection.listbuilder.plugga import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSectioner; import com.android.systemui.statusbar.notification.collection.provider.HighPriorityProvider; import com.android.systemui.statusbar.notification.collection.render.NodeController; +import com.android.systemui.statusbar.notification.collection.render.SectionHeaderController; import org.junit.Before; import org.junit.Test; @@ -46,8 +52,11 @@ import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.Mock; +import org.mockito.Mockito; import org.mockito.MockitoAnnotations; +import java.util.Arrays; + @SmallTest @RunWith(AndroidTestingRunner.class) public class RankingCoordinatorTest extends SysuiTestCase { @@ -56,7 +65,8 @@ public class RankingCoordinatorTest extends SysuiTestCase { @Mock private HighPriorityProvider mHighPriorityProvider; @Mock private NotifPipeline mNotifPipeline; @Mock private NodeController mAlertingHeaderController; - @Mock private NodeController mSilentHeaderController; + @Mock private NodeController mSilentNodeController; + @Mock private SectionHeaderController mSilentHeaderController; @Captor private ArgumentCaptor mNotifFilterCaptor; @@ -72,7 +82,7 @@ public class RankingCoordinatorTest extends SysuiTestCase { MockitoAnnotations.initMocks(this); RankingCoordinator rankingCoordinator = new RankingCoordinator( mStatusBarStateController, mHighPriorityProvider, mAlertingHeaderController, - mSilentHeaderController); + mSilentHeaderController, mSilentNodeController); mEntry = new NotificationEntryBuilder().build(); rankingCoordinator.attach(mNotifPipeline); @@ -84,6 +94,28 @@ public class RankingCoordinatorTest extends SysuiTestCase { mSilentSectioner = rankingCoordinator.getSilentSectioner(); } + @Test + public void testSilentHeaderClearableChildrenUpdate() { + StatusBarNotification sbn = Mockito.mock(StatusBarNotification.class); + Mockito.doReturn("key").when(sbn).getKey(); + Mockito.doReturn(Mockito.mock(Notification.class)).when(sbn).getNotification(); + NotificationEntry entry = new NotificationEntryBuilder().setSbn(sbn).build(); + ListEntry listEntry = new ListEntry("key", 0L) { + @Nullable + @Override + public NotificationEntry getRepresentativeEntry() { + return entry; + } + }; + Mockito.doReturn(true).when(sbn).isClearable(); + mSilentSectioner.onEntriesUpdated(Arrays.asList(listEntry)); + verify(mSilentHeaderController).setClearSectionEnabled(eq(true)); + + Mockito.doReturn(false).when(sbn).isClearable(); + mSilentSectioner.onEntriesUpdated(Arrays.asList(listEntry)); + verify(mSilentHeaderController).setClearSectionEnabled(eq(false)); + } + @Test public void testUnfilteredState() { // GIVEN no suppressed visual effects + app not suspended