diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/AlertingNotificationManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/AlertingNotificationManager.java index 43b3fb10a9206..7cf0583206e11 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/AlertingNotificationManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/AlertingNotificationManager.java @@ -257,6 +257,30 @@ public abstract class AlertingNotificationManager implements NotificationLifetim return !canRemoveImmediately(entry.getKey()); } + /** + * @param key + * @return true if the entry is pinned + */ + public boolean isSticky(String key) { + AlertEntry alerting = mAlertEntries.get(key); + if (alerting != null) { + return alerting.isSticky(); + } + return false; + } + + /** + * @param key + * @return When a HUN entry should be removed in milliseconds from now + */ + public long getEarliestRemovalTime(String key) { + AlertEntry alerting = mAlertEntries.get(key); + if (alerting != null) { + return Math.max(0, alerting.mEarliestRemovaltime - mClock.currentTimeMillis()); + } + return 0; + } + @Override public void setShouldManageLifetime(NotificationEntry entry, boolean shouldExtend) { if (shouldExtend) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.java index f8b4274188f5a..ed9663e9c6727 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.java @@ -19,9 +19,12 @@ package com.android.systemui.statusbar.notification.collection.coordinator; import static com.android.systemui.statusbar.NotificationRemoteInputManager.FORCE_REMOTE_INPUT_HISTORY; import static com.android.systemui.statusbar.notification.interruption.HeadsUpController.alertAgain; +import android.util.ArraySet; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.statusbar.NotificationRemoteInputManager; import com.android.systemui.statusbar.notification.collection.ListEntry; import com.android.systemui.statusbar.notification.collection.NotifPipeline; @@ -38,8 +41,7 @@ import com.android.systemui.statusbar.notification.interruption.NotificationInte import com.android.systemui.statusbar.notification.stack.NotificationPriorityBucketKt; import com.android.systemui.statusbar.policy.HeadsUpManager; import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener; - -import java.util.Objects; +import com.android.systemui.util.concurrency.DelayableExecutor; import javax.inject.Inject; @@ -66,12 +68,11 @@ public class HeadsUpCoordinator implements Coordinator { private final NotificationInterruptStateProvider mNotificationInterruptStateProvider; private final NotificationRemoteInputManager mRemoteInputManager; private final NodeController mIncomingHeaderController; - - // tracks the current HeadUpNotification reported by HeadsUpManager - private @Nullable NotificationEntry mCurrentHun; + private final DelayableExecutor mExecutor; private NotifLifetimeExtender.OnEndLifetimeExtensionCallback mEndLifetimeExtension; - private NotificationEntry mNotifExtendingLifetime; // notif we've extended the lifetime for + // notifs we've extended the lifetime for + private final ArraySet mNotifsExtendingLifetime = new ArraySet<>(); @Inject public HeadsUpCoordinator( @@ -79,12 +80,14 @@ public class HeadsUpCoordinator implements Coordinator { HeadsUpViewBinder headsUpViewBinder, NotificationInterruptStateProvider notificationInterruptStateProvider, NotificationRemoteInputManager remoteInputManager, - @IncomingHeader NodeController incomingHeaderController) { + @IncomingHeader NodeController incomingHeaderController, + @Main DelayableExecutor executor) { mHeadsUpManager = headsUpManager; mHeadsUpViewBinder = headsUpViewBinder; mNotificationInterruptStateProvider = notificationInterruptStateProvider; mRemoteInputManager = remoteInputManager; mIncomingHeaderController = incomingHeaderController; + mExecutor = executor; } @Override @@ -178,16 +181,26 @@ public class HeadsUpCoordinator implements Coordinator { public boolean shouldExtendLifetime(@NonNull NotificationEntry entry, int reason) { boolean isShowingHun = isCurrentlyShowingHun(entry); if (isShowingHun) { - mNotifExtendingLifetime = entry; + if (isSticky(entry)) { + long removeAfterMillis = mHeadsUpManager.getEarliestRemovalTime(entry.getKey()); + if (removeAfterMillis <= 0) return false; + mExecutor.executeDelayed(() -> { + // make sure that the entry was not updated + long removeAfterMillis2 = + mHeadsUpManager.getEarliestRemovalTime(entry.getKey()); + if (mNotifsExtendingLifetime.contains(entry) && removeAfterMillis2 <= 0) { + mHeadsUpManager.removeNotification(entry.getKey(), true); + } + }, removeAfterMillis); + } + mNotifsExtendingLifetime.add(entry); } return isShowingHun; } @Override public void cancelLifetimeExtension(@NonNull NotificationEntry entry) { - if (Objects.equals(mNotifExtendingLifetime, entry)) { - mNotifExtendingLifetime = null; - } + mNotifsExtendingLifetime.remove(entry); } }; @@ -220,27 +233,24 @@ public class HeadsUpCoordinator implements Coordinator { new OnHeadsUpChangedListener() { @Override public void onHeadsUpStateChanged(NotificationEntry entry, boolean isHeadsUp) { - NotificationEntry newHUN = mHeadsUpManager.getTopEntry(); - if (!Objects.equals(mCurrentHun, newHUN)) { - mCurrentHun = newHUN; - endNotifLifetimeExtension(); - } if (!isHeadsUp) { mHeadsUpViewBinder.unbindHeadsUpView(entry); + endNotifLifetimeExtensionIfExtended(entry); } } }; - private boolean isCurrentlyShowingHun(ListEntry entry) { - return mCurrentHun == entry.getRepresentativeEntry(); + private boolean isSticky(NotificationEntry entry) { + return mHeadsUpManager.isSticky(entry.getKey()); } - private void endNotifLifetimeExtension() { - if (mNotifExtendingLifetime != null) { - mEndLifetimeExtension.onEndLifetimeExtension( - mLifetimeExtender, - mNotifExtendingLifetime); - mNotifExtendingLifetime = null; + private boolean isCurrentlyShowingHun(ListEntry entry) { + return mHeadsUpManager.isAlerting(entry.getKey()); + } + + private void endNotifLifetimeExtensionIfExtended(NotificationEntry entry) { + if (mNotifsExtendingLifetime.remove(entry)) { + mEndLifetimeExtension.onEndLifetimeExtension(mLifetimeExtender, entry); } } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.java index a3569e4060d1b..a11b46df12182 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.java @@ -19,8 +19,11 @@ package com.android.systemui.statusbar.notification.collection.coordinator; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -44,6 +47,8 @@ import com.android.systemui.statusbar.notification.interruption.NotificationInte import com.android.systemui.statusbar.notification.row.NotifBindPipeline.BindCallback; import com.android.systemui.statusbar.policy.HeadsUpManager; import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener; +import com.android.systemui.util.concurrency.FakeExecutor; +import com.android.systemui.util.time.FakeSystemClock; import org.junit.Before; import org.junit.Test; @@ -52,6 +57,8 @@ import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import java.util.ArrayList; + @SmallTest @RunWith(AndroidTestingRunner.class) @TestableLooper.RunWithLooper @@ -75,6 +82,9 @@ public class HeadsUpCoordinatorTest extends SysuiTestCase { @Mock private NodeController mHeaderController; private NotificationEntry mEntry; + private final FakeSystemClock mClock = new FakeSystemClock(); + private final FakeExecutor mExecutor = new FakeExecutor(mClock); + private final ArrayList mHuns = new ArrayList(); @Before public void setUp() { @@ -85,7 +95,8 @@ public class HeadsUpCoordinatorTest extends SysuiTestCase { mHeadsUpViewBinder, mNotificationInterruptStateProvider, mRemoteInputManager, - mHeaderController); + mHeaderController, + mExecutor); mCoordinator.attach(mNotifPipeline); @@ -105,6 +116,16 @@ public class HeadsUpCoordinatorTest extends SysuiTestCase { notifLifetimeExtenderCaptor.capture()); verify(mHeadsUpManager).addListener(headsUpChangedListenerCaptor.capture()); + given(mHeadsUpManager.getAllEntries()).willAnswer(i -> mHuns.stream()); + given(mHeadsUpManager.isAlerting(anyString())).willAnswer(i -> { + String key = i.getArgument(0); + for (NotificationEntry entry : mHuns) { + if (entry.getKey().equals(key)) return true; + } + return false; + }); + when(mHeadsUpManager.getEarliestRemovalTime(anyString())).thenReturn(1000L); + mCollectionListener = notifCollectionCaptor.getValue(); mNotifPromoter = notifPromoterCaptor.getValue(); mNotifLifetimeExtender = notifLifetimeExtenderCaptor.getValue(); @@ -115,64 +136,65 @@ public class HeadsUpCoordinatorTest extends SysuiTestCase { mEntry = new NotificationEntryBuilder().build(); } + @Test + public void testCancelStickyNotification() { + when(mHeadsUpManager.isSticky(anyString())).thenReturn(true); + addHUN(mEntry); + when(mHeadsUpManager.getEarliestRemovalTime(anyString())).thenReturn(1000L, 0L); + assertTrue(mNotifLifetimeExtender.shouldExtendLifetime(mEntry, 0)); + mClock.advanceTime(1000L); + mExecutor.runAllReady(); + verify(mHeadsUpManager, times(1)) + .removeNotification(anyString(), eq(true)); + } + + @Test + public void testCancelUpdatedStickyNotification() { + when(mHeadsUpManager.isSticky(anyString())).thenReturn(true); + addHUN(mEntry); + when(mHeadsUpManager.getEarliestRemovalTime(anyString())).thenReturn(1000L, 500L); + assertTrue(mNotifLifetimeExtender.shouldExtendLifetime(mEntry, 0)); + mClock.advanceTime(1000L); + mExecutor.runAllReady(); + verify(mHeadsUpManager, times(0)) + .removeNotification(anyString(), eq(true)); + } + @Test public void testPromotesCurrentHUN() { // GIVEN the current HUN is set to mEntry - setCurrentHUN(mEntry); + addHUN(mEntry); // THEN only promote the current HUN, mEntry assertTrue(mNotifPromoter.shouldPromoteToTopLevel(mEntry)); - assertFalse(mNotifPromoter.shouldPromoteToTopLevel(new NotificationEntryBuilder().build())); + assertFalse(mNotifPromoter.shouldPromoteToTopLevel(new NotificationEntryBuilder() + .setPkg("test-package2") + .build())); } @Test public void testIncludeInSectionCurrentHUN() { // GIVEN the current HUN is set to mEntry - setCurrentHUN(mEntry); + addHUN(mEntry); // THEN only section the current HUN, mEntry assertTrue(mNotifSectioner.isInSection(mEntry)); - assertFalse(mNotifSectioner.isInSection(new NotificationEntryBuilder().build())); + assertFalse(mNotifSectioner.isInSection(new NotificationEntryBuilder() + .setPkg("test-package") + .build())); } @Test public void testLifetimeExtendsCurrentHUN() { // GIVEN there is a HUN, mEntry - setCurrentHUN(mEntry); + addHUN(mEntry); // THEN only the current HUN, mEntry, should be lifetimeExtended assertTrue(mNotifLifetimeExtender.shouldExtendLifetime(mEntry, /* cancellationReason */ 0)); assertFalse(mNotifLifetimeExtender.shouldExtendLifetime( - new NotificationEntryBuilder().build(), /* cancellationReason */ 0)); - } - - @Test - public void testLifetimeExtensionEndsOnNewHUN() { - // GIVEN there was a HUN that was lifetime extended - setCurrentHUN(mEntry); - assertTrue(mNotifLifetimeExtender.shouldExtendLifetime( - mEntry, /* cancellation reason */ 0)); - - // WHEN there's a new HUN - NotificationEntry newHUN = new NotificationEntryBuilder().build(); - setCurrentHUN(newHUN); - - // THEN the old entry's lifetime extension should be cancelled - verify(mEndLifetimeExtension).onEndLifetimeExtension(mNotifLifetimeExtender, mEntry); - } - - @Test - public void testLifetimeExtensionEndsOnNoHUNs() { - // GIVEN there was a HUN that was lifetime extended - setCurrentHUN(mEntry); - assertTrue(mNotifLifetimeExtender.shouldExtendLifetime( - mEntry, /* cancellation reason */ 0)); - - // WHEN there's no longer a HUN - setCurrentHUN(null); - - // THEN the old entry's lifetime extension should be cancelled - verify(mEndLifetimeExtension).onEndLifetimeExtension(mNotifLifetimeExtender, mEntry); + new NotificationEntryBuilder() + .setPkg("test-package") + .build(), /* cancellationReason */ 0)); } @Test @@ -208,7 +230,7 @@ public class HeadsUpCoordinatorTest extends SysuiTestCase { @Test public void testOnEntryRemovedRemovesHeadsUpNotification() { // GIVEN the current HUN is mEntry - setCurrentHUN(mEntry); + addHUN(mEntry); // WHEN mEntry is removed from the notification collection mCollectionListener.onEntryRemoved(mEntry, /* cancellation reason */ 0); @@ -218,12 +240,9 @@ public class HeadsUpCoordinatorTest extends SysuiTestCase { verify(mHeadsUpManager).removeNotification(mEntry.getKey(), false); } - private void setCurrentHUN(NotificationEntry entry) { + private void addHUN(NotificationEntry entry) { + mHuns.add(entry); when(mHeadsUpManager.getTopEntry()).thenReturn(entry); - when(mHeadsUpManager.isAlerting(any())).thenReturn(false); - if (entry != null) { - when(mHeadsUpManager.isAlerting(entry.getKey())).thenReturn(true); - } mOnHeadsUpChangedListener.onHeadsUpStateChanged(entry, entry != null); } }