Loading packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.java 0 → 100644 +171 −0 Original line number Diff line number Diff line /* * Copyright (C) 2020 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.collection.coordinator; import static com.android.systemui.statusbar.NotificationRemoteInputManager.FORCE_REMOTE_INPUT_HISTORY; import android.annotation.Nullable; import com.android.systemui.statusbar.NotificationRemoteInputManager; 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.listbuilder.pluggable.NotifPromoter; import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSection; import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener; import com.android.systemui.statusbar.notification.collection.notifcollection.NotifLifetimeExtender; import com.android.systemui.statusbar.policy.HeadsUpManager; import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener; import java.util.Objects; import javax.inject.Inject; import javax.inject.Singleton; /** * Coordinates heads up notification (HUN) interactions with the notification pipeline based on * the HUN state reported by the {@link HeadsUpManager}. In this class we only consider one * notification, in particular the {@link HeadsUpManager#getTopEntry()}, to be HeadsUpping at a * time even though other notifications may be queued to heads up next. * * The current HUN, but not HUNs that are queued to heads up, will be: * - Lifetime extended until it's no longer heads upping. * - Promoted out of its group if it's a child of a group. * - In the HeadsUpCoordinatorSection. Ordering is configured in {@link NotifCoordinators}. * - Removed from HeadsUpManager if it's removed from the NotificationCollection. * * Note: The inflation callback in {@link PreparationCoordinator} handles showing HUNs. */ @Singleton public class HeadsUpCoordinator implements Coordinator { private static final String TAG = "HeadsUpCoordinator"; private final HeadsUpManager mHeadsUpManager; private final NotificationRemoteInputManager mRemoteInputManager; // tracks the current HeadUpNotification reported by HeadsUpManager private @Nullable NotificationEntry mCurrentHun; private NotifLifetimeExtender.OnEndLifetimeExtensionCallback mEndLifetimeExtension; private NotificationEntry mNotifExtendingLifetime; // notif we've extended the lifetime for @Inject public HeadsUpCoordinator( HeadsUpManager headsUpManager, NotificationRemoteInputManager remoteInputManager) { mHeadsUpManager = headsUpManager; mRemoteInputManager = remoteInputManager; } @Override public void attach(NotifPipeline pipeline) { mHeadsUpManager.addListener(mOnHeadsUpChangedListener); pipeline.addCollectionListener(mNotifCollectionListener); pipeline.addPromoter(mNotifPromoter); pipeline.addNotificationLifetimeExtender(mLifetimeExtender); } @Override public NotifSection getSection() { return mNotifSection; } private final NotifCollectionListener mNotifCollectionListener = new NotifCollectionListener() { /** * Stop alerting HUNs that are removed from the notification collection */ @Override public void onEntryRemoved(NotificationEntry entry, int reason) { final String entryKey = entry.getKey(); if (mHeadsUpManager.isAlerting(entryKey)) { boolean removeImmediatelyForRemoteInput = mRemoteInputManager.getController().isSpinning(entryKey) && !FORCE_REMOTE_INPUT_HISTORY; mHeadsUpManager.removeNotification(entry.getKey(), removeImmediatelyForRemoteInput); } } }; private final NotifLifetimeExtender mLifetimeExtender = new NotifLifetimeExtender() { @Override public String getName() { return TAG; } @Override public void setCallback(OnEndLifetimeExtensionCallback callback) { mEndLifetimeExtension = callback; } @Override public boolean shouldExtendLifetime(NotificationEntry entry, int reason) { boolean isShowingHun = isCurrentlyShowingHun(entry); if (isShowingHun) { mNotifExtendingLifetime = entry; } return isShowingHun; } @Override public void cancelLifetimeExtension(NotificationEntry entry) { if (Objects.equals(mNotifExtendingLifetime, entry)) { mNotifExtendingLifetime = null; } } }; private final NotifPromoter mNotifPromoter = new NotifPromoter(TAG) { @Override public boolean shouldPromoteToTopLevel(NotificationEntry entry) { return isCurrentlyShowingHun(entry); } }; private final NotifSection mNotifSection = new NotifSection(TAG) { @Override public boolean isInSection(ListEntry entry) { return isCurrentlyShowingHun(entry); } }; private final OnHeadsUpChangedListener mOnHeadsUpChangedListener = new OnHeadsUpChangedListener() { @Override public void onHeadsUpStateChanged(NotificationEntry entry, boolean isHeadsUp) { NotificationEntry newHUN = mHeadsUpManager.getTopEntry(); if (!Objects.equals(mCurrentHun, newHUN)) { endNotifLifetimeExtension(); mCurrentHun = newHUN; mNotifPromoter.invalidateList(); mNotifSection.invalidateList(); } } }; private boolean isCurrentlyShowingHun(ListEntry entry) { return mCurrentHun == entry.getRepresentativeEntry(); } private void endNotifLifetimeExtension() { if (mNotifExtendingLifetime != null) { mEndLifetimeExtension.onEndLifetimeExtension( mLifetimeExtender, mNotifExtendingLifetime); mNotifExtendingLifetime = null; } } } packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.java +3 −2 Original line number Diff line number Diff line Loading @@ -49,6 +49,7 @@ public class NotifCoordinators implements Dumpable { public NotifCoordinators( DumpManager dumpManager, FeatureFlags featureFlags, HeadsUpCoordinator headsUpCoordinator, KeyguardCoordinator keyguardCoordinator, RankingCoordinator rankingCoordinator, ForegroundCoordinator foregroundCoordinator, Loading @@ -56,7 +57,6 @@ public class NotifCoordinators implements Dumpable { BubbleCoordinator bubbleCoordinator, PreparationCoordinator preparationCoordinator) { dumpManager.registerDumpable(TAG, this); mCoordinators.add(new HideLocallyDismissedNotifsCoordinator()); mCoordinators.add(keyguardCoordinator); mCoordinators.add(rankingCoordinator); Loading @@ -64,9 +64,10 @@ public class NotifCoordinators implements Dumpable { mCoordinators.add(deviceProvisionedCoordinator); mCoordinators.add(bubbleCoordinator); if (featureFlags.isNewNotifPipelineRenderingEnabled()) { mCoordinators.add(headsUpCoordinator); mCoordinators.add(preparationCoordinator); } // TODO: add new Coordinators here! (b/145134683, b/112656837) // TODO: add new Coordinators here! (b/112656837) // TODO: add the sections in a particular ORDER (HeadsUp < People < Alerting) for (Coordinator c : mCoordinators) { Loading packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinator.java +15 −1 Original line number Diff line number Diff line Loading @@ -33,7 +33,9 @@ import com.android.systemui.statusbar.notification.collection.inflation.NotifInf import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeFinalizeFilterListener; import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifFilter; import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener; import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProvider; import com.android.systemui.statusbar.notification.row.NotifInflationErrorManager; import com.android.systemui.statusbar.policy.HeadsUpManager; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; Loading Loading @@ -61,6 +63,8 @@ public class PreparationCoordinator implements Coordinator { private final NotifViewBarn mViewBarn; private final Map<NotificationEntry, Integer> mInflationStates = new ArrayMap<>(); private final IStatusBarService mStatusBarService; private final NotificationInterruptStateProvider mNotificationInterruptStateProvider; private final HeadsUpManager mHeadsUpManager; @Inject public PreparationCoordinator( Loading @@ -68,7 +72,10 @@ public class PreparationCoordinator implements Coordinator { NotifInflaterImpl notifInflater, NotifInflationErrorManager errorManager, NotifViewBarn viewBarn, IStatusBarService service) { IStatusBarService service, NotificationInterruptStateProvider notificationInterruptStateProvider, HeadsUpManager headsUpManager ) { mLogger = logger; mNotifInflater = notifInflater; mNotifInflater.setInflationCallback(mInflationCallback); Loading @@ -76,6 +83,8 @@ public class PreparationCoordinator implements Coordinator { mNotifErrorManager.addInflationErrorListener(mInflationErrorListener); mViewBarn = viewBarn; mStatusBarService = service; mNotificationInterruptStateProvider = notificationInterruptStateProvider; mHeadsUpManager = headsUpManager; } @Override Loading Loading @@ -149,6 +158,11 @@ public class PreparationCoordinator implements Coordinator { mLogger.logNotifInflated(entry.getKey()); mViewBarn.registerViewForEntry(entry, entry.getRow()); mInflationStates.put(entry, STATE_INFLATED); // TODO: should eventually be moved to HeadsUpCoordinator if (mNotificationInterruptStateProvider.shouldHeadsUp(entry)) { mHeadsUpManager.showNotification(entry); } mNotifInflatingFilter.invalidateList(); } }; Loading packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.java 0 → 100644 +191 −0 Original line number Diff line number Diff line /* * Copyright (C) 2020 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.collection.coordinator; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; import androidx.test.filters.SmallTest; import com.android.systemui.SysuiTestCase; import com.android.systemui.statusbar.NotificationRemoteInputManager; import com.android.systemui.statusbar.RemoteInputController; import com.android.systemui.statusbar.notification.collection.NotifPipeline; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder; import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifPromoter; import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSection; import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener; import com.android.systemui.statusbar.notification.collection.notifcollection.NotifLifetimeExtender; import com.android.systemui.statusbar.policy.HeadsUpManager; import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; @SmallTest @RunWith(AndroidTestingRunner.class) @TestableLooper.RunWithLooper public class HeadsUpCoordinatorTest extends SysuiTestCase { private HeadsUpCoordinator mCoordinator; // captured listeners and pluggables: private NotifCollectionListener mCollectionListener; private NotifPromoter mNotifPromoter; private NotifLifetimeExtender mNotifLifetimeExtender; private OnHeadsUpChangedListener mOnHeadsUpChangedListener; private NotifSection mNotifSection; @Mock private NotifPipeline mNotifPipeline; @Mock private HeadsUpManager mHeadsUpManager; @Mock private NotificationRemoteInputManager mRemoteInputManager; @Mock private RemoteInputController mRemoteInputController; @Mock private NotifLifetimeExtender.OnEndLifetimeExtensionCallback mEndLifetimeExtension; private NotificationEntry mEntry; @Before public void setUp() { MockitoAnnotations.initMocks(this); when(mRemoteInputManager.getController()).thenReturn(mRemoteInputController); mCoordinator = new HeadsUpCoordinator( mHeadsUpManager, mRemoteInputManager ); mCoordinator.attach(mNotifPipeline); // capture arguments: ArgumentCaptor<NotifCollectionListener> notifCollectionCaptor = ArgumentCaptor.forClass(NotifCollectionListener.class); ArgumentCaptor<NotifPromoter> notifPromoterCaptor = ArgumentCaptor.forClass(NotifPromoter.class); ArgumentCaptor<NotifLifetimeExtender> notifLifetimeExtenderCaptor = ArgumentCaptor.forClass(NotifLifetimeExtender.class); ArgumentCaptor<OnHeadsUpChangedListener> headsUpChangedListenerCaptor = ArgumentCaptor.forClass(OnHeadsUpChangedListener.class); verify(mNotifPipeline).addCollectionListener(notifCollectionCaptor.capture()); verify(mNotifPipeline).addPromoter(notifPromoterCaptor.capture()); verify(mNotifPipeline).addNotificationLifetimeExtender( notifLifetimeExtenderCaptor.capture()); verify(mHeadsUpManager).addListener(headsUpChangedListenerCaptor.capture()); mCollectionListener = notifCollectionCaptor.getValue(); mNotifPromoter = notifPromoterCaptor.getValue(); mNotifLifetimeExtender = notifLifetimeExtenderCaptor.getValue(); mOnHeadsUpChangedListener = headsUpChangedListenerCaptor.getValue(); mNotifSection = mCoordinator.getSection(); mNotifLifetimeExtender.setCallback(mEndLifetimeExtension); mEntry = new NotificationEntryBuilder().build(); } @Test public void testPromotesCurrentHUN() { // GIVEN the current HUN is set to mEntry setCurrentHUN(mEntry); // THEN only promote the current HUN, mEntry assertTrue(mNotifPromoter.shouldPromoteToTopLevel(mEntry)); assertFalse(mNotifPromoter.shouldPromoteToTopLevel(new NotificationEntryBuilder().build())); } @Test public void testIncludeInSectionCurrentHUN() { // GIVEN the current HUN is set to mEntry setCurrentHUN(mEntry); // THEN only section the current HUN, mEntry assertTrue(mNotifSection.isInSection(mEntry)); assertFalse(mNotifSection.isInSection(new NotificationEntryBuilder().build())); } @Test public void testLifetimeExtendsCurrentHUN() { // GIVEN there is a HUN, mEntry setCurrentHUN(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); } @Test public void testOnEntryRemovedRemovesHeadsUpNotification() { // GIVEN the current HUN is mEntry setCurrentHUN(mEntry); // WHEN mEntry is removed from the notification collection mCollectionListener.onEntryRemoved(mEntry, /* cancellation reason */ 0); when(mRemoteInputController.isSpinning(any())).thenReturn(false); // THEN heads up manager should remove the entry verify(mHeadsUpManager).removeNotification(mEntry.getKey(), false); } private void setCurrentHUN(NotificationEntry 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); } } packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorTest.java +29 −1 Original line number Diff line number Diff line Loading @@ -20,8 +20,10 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.os.RemoteException; import android.testing.AndroidTestingRunner; Loading @@ -39,7 +41,9 @@ import com.android.systemui.statusbar.notification.collection.NotificationEntryB import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeFinalizeFilterListener; import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifFilter; import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener; import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProvider; import com.android.systemui.statusbar.notification.row.NotifInflationErrorManager; import com.android.systemui.statusbar.policy.HeadsUpManager; import org.junit.Before; import org.junit.Test; Loading Loading @@ -74,6 +78,8 @@ public class PreparationCoordinatorTest extends SysuiTestCase { @Mock private NotifPipeline mNotifPipeline; @Mock private IStatusBarService mService; @Mock private NotifInflaterImpl mNotifInflater; @Mock private NotificationInterruptStateProvider mNotificationInterruptStateProvider; @Mock private HeadsUpManager mHeadsUpManager; @Before public void setUp() { Loading @@ -88,7 +94,9 @@ public class PreparationCoordinatorTest extends SysuiTestCase { mNotifInflater, mErrorManager, mock(NotifViewBarn.class), mService); mService, mNotificationInterruptStateProvider, mHeadsUpManager); ArgumentCaptor<NotifFilter> filterCaptor = ArgumentCaptor.forClass(NotifFilter.class); mCoordinator.attach(mNotifPipeline); Loading Loading @@ -172,4 +180,24 @@ public class PreparationCoordinatorTest extends SysuiTestCase { // THEN it isn't filtered from shade list assertFalse(mUninflatedFilter.shouldFilterOut(mEntry, 0)); } @Test public void testShowHUNOnInflationFinished() { // WHEN a notification should HUN and its inflation is finished when(mNotificationInterruptStateProvider.shouldHeadsUp(mEntry)).thenReturn(true); mCallback.onInflationFinished(mEntry); // THEN we tell the HeadsUpManager to show the notification verify(mHeadsUpManager).showNotification(mEntry); } @Test public void testNoHUNOnInflationFinished() { // WHEN a notification shouldn't HUN and its inflation is finished when(mNotificationInterruptStateProvider.shouldHeadsUp(mEntry)).thenReturn(false); mCallback.onInflationFinished(mEntry); // THEN we never tell the HeadsUpManager to show the notification verify(mHeadsUpManager, never()).showNotification(mEntry); } } Loading
packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.java 0 → 100644 +171 −0 Original line number Diff line number Diff line /* * Copyright (C) 2020 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.collection.coordinator; import static com.android.systemui.statusbar.NotificationRemoteInputManager.FORCE_REMOTE_INPUT_HISTORY; import android.annotation.Nullable; import com.android.systemui.statusbar.NotificationRemoteInputManager; 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.listbuilder.pluggable.NotifPromoter; import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSection; import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener; import com.android.systemui.statusbar.notification.collection.notifcollection.NotifLifetimeExtender; import com.android.systemui.statusbar.policy.HeadsUpManager; import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener; import java.util.Objects; import javax.inject.Inject; import javax.inject.Singleton; /** * Coordinates heads up notification (HUN) interactions with the notification pipeline based on * the HUN state reported by the {@link HeadsUpManager}. In this class we only consider one * notification, in particular the {@link HeadsUpManager#getTopEntry()}, to be HeadsUpping at a * time even though other notifications may be queued to heads up next. * * The current HUN, but not HUNs that are queued to heads up, will be: * - Lifetime extended until it's no longer heads upping. * - Promoted out of its group if it's a child of a group. * - In the HeadsUpCoordinatorSection. Ordering is configured in {@link NotifCoordinators}. * - Removed from HeadsUpManager if it's removed from the NotificationCollection. * * Note: The inflation callback in {@link PreparationCoordinator} handles showing HUNs. */ @Singleton public class HeadsUpCoordinator implements Coordinator { private static final String TAG = "HeadsUpCoordinator"; private final HeadsUpManager mHeadsUpManager; private final NotificationRemoteInputManager mRemoteInputManager; // tracks the current HeadUpNotification reported by HeadsUpManager private @Nullable NotificationEntry mCurrentHun; private NotifLifetimeExtender.OnEndLifetimeExtensionCallback mEndLifetimeExtension; private NotificationEntry mNotifExtendingLifetime; // notif we've extended the lifetime for @Inject public HeadsUpCoordinator( HeadsUpManager headsUpManager, NotificationRemoteInputManager remoteInputManager) { mHeadsUpManager = headsUpManager; mRemoteInputManager = remoteInputManager; } @Override public void attach(NotifPipeline pipeline) { mHeadsUpManager.addListener(mOnHeadsUpChangedListener); pipeline.addCollectionListener(mNotifCollectionListener); pipeline.addPromoter(mNotifPromoter); pipeline.addNotificationLifetimeExtender(mLifetimeExtender); } @Override public NotifSection getSection() { return mNotifSection; } private final NotifCollectionListener mNotifCollectionListener = new NotifCollectionListener() { /** * Stop alerting HUNs that are removed from the notification collection */ @Override public void onEntryRemoved(NotificationEntry entry, int reason) { final String entryKey = entry.getKey(); if (mHeadsUpManager.isAlerting(entryKey)) { boolean removeImmediatelyForRemoteInput = mRemoteInputManager.getController().isSpinning(entryKey) && !FORCE_REMOTE_INPUT_HISTORY; mHeadsUpManager.removeNotification(entry.getKey(), removeImmediatelyForRemoteInput); } } }; private final NotifLifetimeExtender mLifetimeExtender = new NotifLifetimeExtender() { @Override public String getName() { return TAG; } @Override public void setCallback(OnEndLifetimeExtensionCallback callback) { mEndLifetimeExtension = callback; } @Override public boolean shouldExtendLifetime(NotificationEntry entry, int reason) { boolean isShowingHun = isCurrentlyShowingHun(entry); if (isShowingHun) { mNotifExtendingLifetime = entry; } return isShowingHun; } @Override public void cancelLifetimeExtension(NotificationEntry entry) { if (Objects.equals(mNotifExtendingLifetime, entry)) { mNotifExtendingLifetime = null; } } }; private final NotifPromoter mNotifPromoter = new NotifPromoter(TAG) { @Override public boolean shouldPromoteToTopLevel(NotificationEntry entry) { return isCurrentlyShowingHun(entry); } }; private final NotifSection mNotifSection = new NotifSection(TAG) { @Override public boolean isInSection(ListEntry entry) { return isCurrentlyShowingHun(entry); } }; private final OnHeadsUpChangedListener mOnHeadsUpChangedListener = new OnHeadsUpChangedListener() { @Override public void onHeadsUpStateChanged(NotificationEntry entry, boolean isHeadsUp) { NotificationEntry newHUN = mHeadsUpManager.getTopEntry(); if (!Objects.equals(mCurrentHun, newHUN)) { endNotifLifetimeExtension(); mCurrentHun = newHUN; mNotifPromoter.invalidateList(); mNotifSection.invalidateList(); } } }; private boolean isCurrentlyShowingHun(ListEntry entry) { return mCurrentHun == entry.getRepresentativeEntry(); } private void endNotifLifetimeExtension() { if (mNotifExtendingLifetime != null) { mEndLifetimeExtension.onEndLifetimeExtension( mLifetimeExtender, mNotifExtendingLifetime); mNotifExtendingLifetime = null; } } }
packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.java +3 −2 Original line number Diff line number Diff line Loading @@ -49,6 +49,7 @@ public class NotifCoordinators implements Dumpable { public NotifCoordinators( DumpManager dumpManager, FeatureFlags featureFlags, HeadsUpCoordinator headsUpCoordinator, KeyguardCoordinator keyguardCoordinator, RankingCoordinator rankingCoordinator, ForegroundCoordinator foregroundCoordinator, Loading @@ -56,7 +57,6 @@ public class NotifCoordinators implements Dumpable { BubbleCoordinator bubbleCoordinator, PreparationCoordinator preparationCoordinator) { dumpManager.registerDumpable(TAG, this); mCoordinators.add(new HideLocallyDismissedNotifsCoordinator()); mCoordinators.add(keyguardCoordinator); mCoordinators.add(rankingCoordinator); Loading @@ -64,9 +64,10 @@ public class NotifCoordinators implements Dumpable { mCoordinators.add(deviceProvisionedCoordinator); mCoordinators.add(bubbleCoordinator); if (featureFlags.isNewNotifPipelineRenderingEnabled()) { mCoordinators.add(headsUpCoordinator); mCoordinators.add(preparationCoordinator); } // TODO: add new Coordinators here! (b/145134683, b/112656837) // TODO: add new Coordinators here! (b/112656837) // TODO: add the sections in a particular ORDER (HeadsUp < People < Alerting) for (Coordinator c : mCoordinators) { Loading
packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinator.java +15 −1 Original line number Diff line number Diff line Loading @@ -33,7 +33,9 @@ import com.android.systemui.statusbar.notification.collection.inflation.NotifInf import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeFinalizeFilterListener; import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifFilter; import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener; import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProvider; import com.android.systemui.statusbar.notification.row.NotifInflationErrorManager; import com.android.systemui.statusbar.policy.HeadsUpManager; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; Loading Loading @@ -61,6 +63,8 @@ public class PreparationCoordinator implements Coordinator { private final NotifViewBarn mViewBarn; private final Map<NotificationEntry, Integer> mInflationStates = new ArrayMap<>(); private final IStatusBarService mStatusBarService; private final NotificationInterruptStateProvider mNotificationInterruptStateProvider; private final HeadsUpManager mHeadsUpManager; @Inject public PreparationCoordinator( Loading @@ -68,7 +72,10 @@ public class PreparationCoordinator implements Coordinator { NotifInflaterImpl notifInflater, NotifInflationErrorManager errorManager, NotifViewBarn viewBarn, IStatusBarService service) { IStatusBarService service, NotificationInterruptStateProvider notificationInterruptStateProvider, HeadsUpManager headsUpManager ) { mLogger = logger; mNotifInflater = notifInflater; mNotifInflater.setInflationCallback(mInflationCallback); Loading @@ -76,6 +83,8 @@ public class PreparationCoordinator implements Coordinator { mNotifErrorManager.addInflationErrorListener(mInflationErrorListener); mViewBarn = viewBarn; mStatusBarService = service; mNotificationInterruptStateProvider = notificationInterruptStateProvider; mHeadsUpManager = headsUpManager; } @Override Loading Loading @@ -149,6 +158,11 @@ public class PreparationCoordinator implements Coordinator { mLogger.logNotifInflated(entry.getKey()); mViewBarn.registerViewForEntry(entry, entry.getRow()); mInflationStates.put(entry, STATE_INFLATED); // TODO: should eventually be moved to HeadsUpCoordinator if (mNotificationInterruptStateProvider.shouldHeadsUp(entry)) { mHeadsUpManager.showNotification(entry); } mNotifInflatingFilter.invalidateList(); } }; Loading
packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.java 0 → 100644 +191 −0 Original line number Diff line number Diff line /* * Copyright (C) 2020 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.collection.coordinator; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; import androidx.test.filters.SmallTest; import com.android.systemui.SysuiTestCase; import com.android.systemui.statusbar.NotificationRemoteInputManager; import com.android.systemui.statusbar.RemoteInputController; import com.android.systemui.statusbar.notification.collection.NotifPipeline; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder; import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifPromoter; import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSection; import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener; import com.android.systemui.statusbar.notification.collection.notifcollection.NotifLifetimeExtender; import com.android.systemui.statusbar.policy.HeadsUpManager; import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; @SmallTest @RunWith(AndroidTestingRunner.class) @TestableLooper.RunWithLooper public class HeadsUpCoordinatorTest extends SysuiTestCase { private HeadsUpCoordinator mCoordinator; // captured listeners and pluggables: private NotifCollectionListener mCollectionListener; private NotifPromoter mNotifPromoter; private NotifLifetimeExtender mNotifLifetimeExtender; private OnHeadsUpChangedListener mOnHeadsUpChangedListener; private NotifSection mNotifSection; @Mock private NotifPipeline mNotifPipeline; @Mock private HeadsUpManager mHeadsUpManager; @Mock private NotificationRemoteInputManager mRemoteInputManager; @Mock private RemoteInputController mRemoteInputController; @Mock private NotifLifetimeExtender.OnEndLifetimeExtensionCallback mEndLifetimeExtension; private NotificationEntry mEntry; @Before public void setUp() { MockitoAnnotations.initMocks(this); when(mRemoteInputManager.getController()).thenReturn(mRemoteInputController); mCoordinator = new HeadsUpCoordinator( mHeadsUpManager, mRemoteInputManager ); mCoordinator.attach(mNotifPipeline); // capture arguments: ArgumentCaptor<NotifCollectionListener> notifCollectionCaptor = ArgumentCaptor.forClass(NotifCollectionListener.class); ArgumentCaptor<NotifPromoter> notifPromoterCaptor = ArgumentCaptor.forClass(NotifPromoter.class); ArgumentCaptor<NotifLifetimeExtender> notifLifetimeExtenderCaptor = ArgumentCaptor.forClass(NotifLifetimeExtender.class); ArgumentCaptor<OnHeadsUpChangedListener> headsUpChangedListenerCaptor = ArgumentCaptor.forClass(OnHeadsUpChangedListener.class); verify(mNotifPipeline).addCollectionListener(notifCollectionCaptor.capture()); verify(mNotifPipeline).addPromoter(notifPromoterCaptor.capture()); verify(mNotifPipeline).addNotificationLifetimeExtender( notifLifetimeExtenderCaptor.capture()); verify(mHeadsUpManager).addListener(headsUpChangedListenerCaptor.capture()); mCollectionListener = notifCollectionCaptor.getValue(); mNotifPromoter = notifPromoterCaptor.getValue(); mNotifLifetimeExtender = notifLifetimeExtenderCaptor.getValue(); mOnHeadsUpChangedListener = headsUpChangedListenerCaptor.getValue(); mNotifSection = mCoordinator.getSection(); mNotifLifetimeExtender.setCallback(mEndLifetimeExtension); mEntry = new NotificationEntryBuilder().build(); } @Test public void testPromotesCurrentHUN() { // GIVEN the current HUN is set to mEntry setCurrentHUN(mEntry); // THEN only promote the current HUN, mEntry assertTrue(mNotifPromoter.shouldPromoteToTopLevel(mEntry)); assertFalse(mNotifPromoter.shouldPromoteToTopLevel(new NotificationEntryBuilder().build())); } @Test public void testIncludeInSectionCurrentHUN() { // GIVEN the current HUN is set to mEntry setCurrentHUN(mEntry); // THEN only section the current HUN, mEntry assertTrue(mNotifSection.isInSection(mEntry)); assertFalse(mNotifSection.isInSection(new NotificationEntryBuilder().build())); } @Test public void testLifetimeExtendsCurrentHUN() { // GIVEN there is a HUN, mEntry setCurrentHUN(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); } @Test public void testOnEntryRemovedRemovesHeadsUpNotification() { // GIVEN the current HUN is mEntry setCurrentHUN(mEntry); // WHEN mEntry is removed from the notification collection mCollectionListener.onEntryRemoved(mEntry, /* cancellation reason */ 0); when(mRemoteInputController.isSpinning(any())).thenReturn(false); // THEN heads up manager should remove the entry verify(mHeadsUpManager).removeNotification(mEntry.getKey(), false); } private void setCurrentHUN(NotificationEntry 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); } }
packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorTest.java +29 −1 Original line number Diff line number Diff line Loading @@ -20,8 +20,10 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.os.RemoteException; import android.testing.AndroidTestingRunner; Loading @@ -39,7 +41,9 @@ import com.android.systemui.statusbar.notification.collection.NotificationEntryB import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeFinalizeFilterListener; import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifFilter; import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener; import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProvider; import com.android.systemui.statusbar.notification.row.NotifInflationErrorManager; import com.android.systemui.statusbar.policy.HeadsUpManager; import org.junit.Before; import org.junit.Test; Loading Loading @@ -74,6 +78,8 @@ public class PreparationCoordinatorTest extends SysuiTestCase { @Mock private NotifPipeline mNotifPipeline; @Mock private IStatusBarService mService; @Mock private NotifInflaterImpl mNotifInflater; @Mock private NotificationInterruptStateProvider mNotificationInterruptStateProvider; @Mock private HeadsUpManager mHeadsUpManager; @Before public void setUp() { Loading @@ -88,7 +94,9 @@ public class PreparationCoordinatorTest extends SysuiTestCase { mNotifInflater, mErrorManager, mock(NotifViewBarn.class), mService); mService, mNotificationInterruptStateProvider, mHeadsUpManager); ArgumentCaptor<NotifFilter> filterCaptor = ArgumentCaptor.forClass(NotifFilter.class); mCoordinator.attach(mNotifPipeline); Loading Loading @@ -172,4 +180,24 @@ public class PreparationCoordinatorTest extends SysuiTestCase { // THEN it isn't filtered from shade list assertFalse(mUninflatedFilter.shouldFilterOut(mEntry, 0)); } @Test public void testShowHUNOnInflationFinished() { // WHEN a notification should HUN and its inflation is finished when(mNotificationInterruptStateProvider.shouldHeadsUp(mEntry)).thenReturn(true); mCallback.onInflationFinished(mEntry); // THEN we tell the HeadsUpManager to show the notification verify(mHeadsUpManager).showNotification(mEntry); } @Test public void testNoHUNOnInflationFinished() { // WHEN a notification shouldn't HUN and its inflation is finished when(mNotificationInterruptStateProvider.shouldHeadsUp(mEntry)).thenReturn(false); mCallback.onInflationFinished(mEntry); // THEN we never tell the HeadsUpManager to show the notification verify(mHeadsUpManager, never()).showNotification(mEntry); } }