Merge "Convert HeadsUpCoordinator/Test to Kotlin"

This commit is contained in:
Jeff DeCew
2022-01-20 21:41:04 +00:00
committed by Android (Google) Code Review
4 changed files with 438 additions and 533 deletions

View File

@@ -1,259 +0,0 @@
/*
* 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 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;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.notification.collection.coordinator.dagger.CoordinatorScope;
import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifPromoter;
import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSectioner;
import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener;
import com.android.systemui.statusbar.notification.collection.notifcollection.NotifLifetimeExtender;
import com.android.systemui.statusbar.notification.collection.render.NodeController;
import com.android.systemui.statusbar.notification.dagger.IncomingHeader;
import com.android.systemui.statusbar.notification.interruption.HeadsUpViewBinder;
import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProvider;
import com.android.systemui.statusbar.notification.stack.NotificationPriorityBucketKt;
import com.android.systemui.statusbar.policy.HeadsUpManager;
import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener;
import com.android.systemui.util.concurrency.DelayableExecutor;
import javax.inject.Inject;
/**
* 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.
*/
@CoordinatorScope
public class HeadsUpCoordinator implements Coordinator {
private static final String TAG = "HeadsUpCoordinator";
private final HeadsUpManager mHeadsUpManager;
private final HeadsUpViewBinder mHeadsUpViewBinder;
private final NotificationInterruptStateProvider mNotificationInterruptStateProvider;
private final NotificationRemoteInputManager mRemoteInputManager;
private final NodeController mIncomingHeaderController;
private final DelayableExecutor mExecutor;
private NotifLifetimeExtender.OnEndLifetimeExtensionCallback mEndLifetimeExtension;
// notifs we've extended the lifetime for
private final ArraySet<NotificationEntry> mNotifsExtendingLifetime = new ArraySet<>();
@Inject
public HeadsUpCoordinator(
HeadsUpManager headsUpManager,
HeadsUpViewBinder headsUpViewBinder,
NotificationInterruptStateProvider notificationInterruptStateProvider,
NotificationRemoteInputManager remoteInputManager,
@IncomingHeader NodeController incomingHeaderController,
@Main DelayableExecutor executor) {
mHeadsUpManager = headsUpManager;
mHeadsUpViewBinder = headsUpViewBinder;
mNotificationInterruptStateProvider = notificationInterruptStateProvider;
mRemoteInputManager = remoteInputManager;
mIncomingHeaderController = incomingHeaderController;
mExecutor = executor;
}
@Override
public void attach(NotifPipeline pipeline) {
mHeadsUpManager.addListener(mOnHeadsUpChangedListener);
pipeline.addCollectionListener(mNotifCollectionListener);
pipeline.addPromoter(mNotifPromoter);
pipeline.addNotificationLifetimeExtender(mLifetimeExtender);
}
public NotifSectioner getSectioner() {
return mNotifSectioner;
}
private void onHeadsUpViewBound(NotificationEntry entry) {
mHeadsUpManager.showNotification(entry);
}
private final NotifCollectionListener mNotifCollectionListener = new NotifCollectionListener() {
/**
* Notification was just added and if it should heads up, bind the view and then show it.
*/
@Override
public void onEntryAdded(NotificationEntry entry) {
if (mNotificationInterruptStateProvider.shouldHeadsUp(entry)) {
mHeadsUpViewBinder.bindHeadsUpView(
entry,
HeadsUpCoordinator.this::onHeadsUpViewBound);
}
}
/**
* Notification could've updated to be heads up or not heads up. Even if it did update to
* heads up, if the notification specified that it only wants to alert once, don't heads
* up again.
*/
@Override
public void onEntryUpdated(NotificationEntry entry) {
boolean hunAgain = alertAgain(entry, entry.getSbn().getNotification());
// includes check for whether this notification should be filtered:
boolean shouldHeadsUp = mNotificationInterruptStateProvider.shouldHeadsUp(entry);
final boolean wasHeadsUp = mHeadsUpManager.isAlerting(entry.getKey());
if (wasHeadsUp) {
if (shouldHeadsUp) {
mHeadsUpManager.updateNotification(entry.getKey(), hunAgain);
} else if (!mHeadsUpManager.isEntryAutoHeadsUpped(entry.getKey())) {
// We don't want this to be interrupting anymore, let's remove it
mHeadsUpManager.removeNotification(
entry.getKey(), false /* removeImmediately */);
}
} else if (shouldHeadsUp && hunAgain) {
// This notification was updated to be heads up, show it!
mHeadsUpViewBinder.bindHeadsUpView(
entry,
HeadsUpCoordinator.this::onHeadsUpViewBound);
}
}
/**
* 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.isSpinning(entryKey)
&& !FORCE_REMOTE_INPUT_HISTORY;
mHeadsUpManager.removeNotification(entry.getKey(), removeImmediatelyForRemoteInput);
}
}
@Override
public void onEntryCleanUp(NotificationEntry entry) {
mHeadsUpViewBinder.abortBindCallback(entry);
}
};
private final NotifLifetimeExtender mLifetimeExtender = new NotifLifetimeExtender() {
@Override
public @NonNull String getName() {
return TAG;
}
@Override
public void setCallback(@NonNull OnEndLifetimeExtensionCallback callback) {
mEndLifetimeExtension = callback;
}
@Override
public boolean maybeExtendLifetime(@NonNull NotificationEntry entry, int reason) {
boolean extend = !mHeadsUpManager.canRemoveImmediately(entry.getKey());
if (extend) {
if (isSticky(entry)) {
long removeAfterMillis = mHeadsUpManager.getEarliestRemovalTime(entry.getKey());
mExecutor.executeDelayed(() -> {
if (mNotifsExtendingLifetime.contains(entry)
&& mHeadsUpManager.canRemoveImmediately(entry.getKey())) {
mHeadsUpManager.removeNotification(
entry.getKey(), /* releaseImmediately */ true);
}
}, removeAfterMillis);
} else {
// remove as early as possible
mExecutor.execute(
() -> mHeadsUpManager.removeNotification(
entry.getKey(), /* releaseImmediately */ false));
}
mNotifsExtendingLifetime.add(entry);
}
return extend;
}
@Override
public void cancelLifetimeExtension(@NonNull NotificationEntry entry) {
mNotifsExtendingLifetime.remove(entry);
}
};
private final NotifPromoter mNotifPromoter = new NotifPromoter(TAG) {
@Override
public boolean shouldPromoteToTopLevel(NotificationEntry entry) {
return isCurrentlyShowingHun(entry);
}
};
private final NotifSectioner mNotifSectioner = new NotifSectioner("HeadsUp",
NotificationPriorityBucketKt.BUCKET_HEADS_UP) {
@Override
public boolean isInSection(ListEntry entry) {
return isCurrentlyShowingHun(entry);
}
@Nullable
@Override
public NodeController getHeaderNodeController() {
// TODO: remove SHOW_ALL_SECTIONS, this redundant method, and mIncomingHeaderController
if (RankingCoordinator.SHOW_ALL_SECTIONS) {
return mIncomingHeaderController;
}
return null;
}
};
private final OnHeadsUpChangedListener mOnHeadsUpChangedListener =
new OnHeadsUpChangedListener() {
@Override
public void onHeadsUpStateChanged(NotificationEntry entry, boolean isHeadsUp) {
if (!isHeadsUp) {
mHeadsUpViewBinder.unbindHeadsUpView(entry);
endNotifLifetimeExtensionIfExtended(entry);
}
}
};
private boolean isSticky(NotificationEntry entry) {
return mHeadsUpManager.isSticky(entry.getKey());
}
private boolean isCurrentlyShowingHun(ListEntry entry) {
return mHeadsUpManager.isAlerting(entry.getKey());
}
private void endNotifLifetimeExtensionIfExtended(NotificationEntry entry) {
if (mNotifsExtendingLifetime.remove(entry)) {
mEndLifetimeExtension.onEndLifetimeExtension(mLifetimeExtender, entry);
}
}
}

View File

@@ -0,0 +1,200 @@
/*
* 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 android.util.ArraySet
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
import com.android.systemui.statusbar.notification.collection.NotificationEntry
import com.android.systemui.statusbar.notification.collection.coordinator.dagger.CoordinatorScope
import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifPromoter
import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSectioner
import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener
import com.android.systemui.statusbar.notification.collection.notifcollection.NotifLifetimeExtender
import com.android.systemui.statusbar.notification.collection.notifcollection.NotifLifetimeExtender.OnEndLifetimeExtensionCallback
import com.android.systemui.statusbar.notification.collection.render.NodeController
import com.android.systemui.statusbar.notification.dagger.IncomingHeader
import com.android.systemui.statusbar.notification.interruption.HeadsUpController
import com.android.systemui.statusbar.notification.interruption.HeadsUpViewBinder
import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProvider
import com.android.systemui.statusbar.notification.stack.BUCKET_HEADS_UP
import com.android.systemui.statusbar.policy.HeadsUpManager
import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener
import com.android.systemui.util.concurrency.DelayableExecutor
import javax.inject.Inject
/**
* Coordinates heads up notification (HUN) interactions with the notification pipeline based on
* the HUN state reported by the [HeadsUpManager]. In this class we only consider one
* notification, in particular the [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 [NotifCoordinators].
* - Removed from HeadsUpManager if it's removed from the NotificationCollection.
*
* Note: The inflation callback in [PreparationCoordinator] handles showing HUNs.
*/
@CoordinatorScope
class HeadsUpCoordinator @Inject constructor(
private val mHeadsUpManager: HeadsUpManager,
private val mHeadsUpViewBinder: HeadsUpViewBinder,
private val mNotificationInterruptStateProvider: NotificationInterruptStateProvider,
private val mRemoteInputManager: NotificationRemoteInputManager,
@IncomingHeader private val mIncomingHeaderController: NodeController,
@Main private val mExecutor: DelayableExecutor
) : Coordinator {
private var mEndLifetimeExtension: OnEndLifetimeExtensionCallback? = null
// notifs we've extended the lifetime for
private val mNotifsExtendingLifetime = ArraySet<NotificationEntry>()
override fun attach(pipeline: NotifPipeline) {
mHeadsUpManager.addListener(mOnHeadsUpChangedListener)
pipeline.addCollectionListener(mNotifCollectionListener)
pipeline.addPromoter(mNotifPromoter)
pipeline.addNotificationLifetimeExtender(mLifetimeExtender)
}
private fun onHeadsUpViewBound(entry: NotificationEntry) {
mHeadsUpManager.showNotification(entry)
}
private val mNotifCollectionListener = object : NotifCollectionListener {
/**
* Notification was just added and if it should heads up, bind the view and then show it.
*/
override fun onEntryAdded(entry: NotificationEntry) {
if (mNotificationInterruptStateProvider.shouldHeadsUp(entry)) {
mHeadsUpViewBinder.bindHeadsUpView(entry) { entry -> onHeadsUpViewBound(entry) }
}
}
/**
* Notification could've updated to be heads up or not heads up. Even if it did update to
* heads up, if the notification specified that it only wants to alert once, don't heads
* up again.
*/
override fun onEntryUpdated(entry: NotificationEntry) {
val hunAgain = HeadsUpController.alertAgain(entry, entry.sbn.notification)
// includes check for whether this notification should be filtered:
val shouldHeadsUp = mNotificationInterruptStateProvider.shouldHeadsUp(entry)
val wasHeadsUp = mHeadsUpManager.isAlerting(entry.key)
if (wasHeadsUp) {
if (shouldHeadsUp) {
mHeadsUpManager.updateNotification(entry.key, hunAgain)
} else if (!mHeadsUpManager.isEntryAutoHeadsUpped(entry.key)) {
// We don't want this to be interrupting anymore, let's remove it
mHeadsUpManager.removeNotification(
entry.key, false /* removeImmediately */
)
}
} else if (shouldHeadsUp && hunAgain) {
// This notification was updated to be heads up, show it!
mHeadsUpViewBinder.bindHeadsUpView(entry) { entry -> onHeadsUpViewBound(entry) }
}
}
/**
* Stop alerting HUNs that are removed from the notification collection
*/
override fun onEntryRemoved(entry: NotificationEntry, reason: Int) {
val entryKey = entry.key
if (mHeadsUpManager.isAlerting(entryKey)) {
val removeImmediatelyForRemoteInput = (mRemoteInputManager.isSpinning(entryKey) &&
!NotificationRemoteInputManager.FORCE_REMOTE_INPUT_HISTORY)
mHeadsUpManager.removeNotification(entry.key, removeImmediatelyForRemoteInput)
}
}
override fun onEntryCleanUp(entry: NotificationEntry) {
mHeadsUpViewBinder.abortBindCallback(entry)
}
}
private val mLifetimeExtender = object : NotifLifetimeExtender {
override fun getName() = TAG
override fun setCallback(callback: OnEndLifetimeExtensionCallback) {
mEndLifetimeExtension = callback
}
override fun maybeExtendLifetime(entry: NotificationEntry, reason: Int): Boolean {
if (mHeadsUpManager.canRemoveImmediately(entry.key)) {
return false
}
if (isSticky(entry)) {
val removeAfterMillis = mHeadsUpManager.getEarliestRemovalTime(entry.key)
mExecutor.executeDelayed({
val canStillRemove = mHeadsUpManager.canRemoveImmediately(entry.key)
if (mNotifsExtendingLifetime.contains(entry) && canStillRemove) {
mHeadsUpManager.removeNotification(entry.key, /* releaseImmediately */ true)
}
}, removeAfterMillis)
} else {
mExecutor.execute {
mHeadsUpManager.removeNotification(entry.key, /* releaseImmediately */ false)
}
}
mNotifsExtendingLifetime.add(entry)
return true
}
override fun cancelLifetimeExtension(entry: NotificationEntry) {
mNotifsExtendingLifetime.remove(entry)
}
}
private val mNotifPromoter = object : NotifPromoter(TAG) {
override fun shouldPromoteToTopLevel(entry: NotificationEntry): Boolean =
isCurrentlyShowingHun(entry)
}
val sectioner = object : NotifSectioner("HeadsUp", BUCKET_HEADS_UP) {
override fun isInSection(entry: ListEntry): Boolean = isCurrentlyShowingHun(entry)
override fun getHeaderNodeController(): NodeController? =
// TODO: remove SHOW_ALL_SECTIONS, this redundant method, and mIncomingHeaderController
if (RankingCoordinator.SHOW_ALL_SECTIONS) mIncomingHeaderController else null
}
private val mOnHeadsUpChangedListener = object : OnHeadsUpChangedListener {
override fun onHeadsUpStateChanged(entry: NotificationEntry, isHeadsUp: Boolean) {
if (!isHeadsUp) {
mHeadsUpViewBinder.unbindHeadsUpView(entry)
endNotifLifetimeExtensionIfExtended(entry)
}
}
}
private fun isSticky(entry: NotificationEntry) = mHeadsUpManager.isSticky(entry.key)
private fun isCurrentlyShowingHun(entry: ListEntry) = mHeadsUpManager.isAlerting(entry.key)
private fun endNotifLifetimeExtensionIfExtended(entry: NotificationEntry) {
if (mNotifsExtendingLifetime.remove(entry)) {
mEndLifetimeExtension?.onEndLifetimeExtension(mLifetimeExtender, entry)
}
}
companion object {
private const val TAG = "HeadsUpCoordinator"
}
}

View File

@@ -1,274 +0,0 @@
/*
* 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.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;
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.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.NotifSectioner;
import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener;
import com.android.systemui.statusbar.notification.collection.notifcollection.NotifLifetimeExtender;
import com.android.systemui.statusbar.notification.collection.render.NodeController;
import com.android.systemui.statusbar.notification.interruption.HeadsUpViewBinder;
import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProvider;
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;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import java.util.ArrayList;
@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 NotifSectioner mNotifSectioner;
@Mock private NotifPipeline mNotifPipeline;
@Mock private HeadsUpManager mHeadsUpManager;
@Mock private HeadsUpViewBinder mHeadsUpViewBinder;
@Mock private NotificationInterruptStateProvider mNotificationInterruptStateProvider;
@Mock private NotificationRemoteInputManager mRemoteInputManager;
@Mock private NotifLifetimeExtender.OnEndLifetimeExtensionCallback mEndLifetimeExtension;
@Mock private NodeController mHeaderController;
private NotificationEntry mEntry;
private final FakeSystemClock mClock = new FakeSystemClock();
private final FakeExecutor mExecutor = new FakeExecutor(mClock);
private final ArrayList<NotificationEntry> mHuns = new ArrayList();
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
mCoordinator = new HeadsUpCoordinator(
mHeadsUpManager,
mHeadsUpViewBinder,
mNotificationInterruptStateProvider,
mRemoteInputManager,
mHeaderController,
mExecutor);
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());
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();
mOnHeadsUpChangedListener = headsUpChangedListenerCaptor.getValue();
mNotifSectioner = mCoordinator.getSectioner();
mNotifLifetimeExtender.setCallback(mEndLifetimeExtension);
mEntry = new NotificationEntryBuilder().build();
}
@Test
public void testCancelStickyNotification() {
when(mHeadsUpManager.isSticky(anyString())).thenReturn(true);
addHUN(mEntry);
when(mHeadsUpManager.canRemoveImmediately(anyString())).thenReturn(false, true);
when(mHeadsUpManager.getEarliestRemovalTime(anyString())).thenReturn(1000L, 0L);
assertTrue(mNotifLifetimeExtender.maybeExtendLifetime(mEntry, 0));
mClock.advanceTime(1000L);
mExecutor.runAllReady();
verify(mHeadsUpManager, times(0))
.removeNotification(anyString(), eq(false));
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.maybeExtendLifetime(mEntry, 0));
mClock.advanceTime(1000L);
mExecutor.runAllReady();
verify(mHeadsUpManager, times(0))
.removeNotification(anyString(), eq(false));
verify(mHeadsUpManager, times(0))
.removeNotification(anyString(), eq(true));
}
@Test
public void testCancelNotification() {
when(mHeadsUpManager.isSticky(anyString())).thenReturn(false);
addHUN(mEntry);
when(mHeadsUpManager.getEarliestRemovalTime(anyString())).thenReturn(1000L, 500L);
assertTrue(mNotifLifetimeExtender.maybeExtendLifetime(mEntry, 0));
mClock.advanceTime(1000L);
mExecutor.runAllReady();
verify(mHeadsUpManager, times(1))
.removeNotification(anyString(), eq(false));
verify(mHeadsUpManager, times(0))
.removeNotification(anyString(), eq(true));
}
@Test
public void testPromotesCurrentHUN() {
// GIVEN the current HUN is set to mEntry
addHUN(mEntry);
// THEN only promote the current HUN, mEntry
assertTrue(mNotifPromoter.shouldPromoteToTopLevel(mEntry));
assertFalse(mNotifPromoter.shouldPromoteToTopLevel(new NotificationEntryBuilder()
.setPkg("test-package2")
.build()));
}
@Test
public void testIncludeInSectionCurrentHUN() {
// GIVEN the current HUN is set to mEntry
addHUN(mEntry);
// THEN only section the current HUN, mEntry
assertTrue(mNotifSectioner.isInSection(mEntry));
assertFalse(mNotifSectioner.isInSection(new NotificationEntryBuilder()
.setPkg("test-package")
.build()));
}
@Test
public void testLifetimeExtendsCurrentHUN() {
// GIVEN there is a HUN, mEntry
addHUN(mEntry);
given(mHeadsUpManager.canRemoveImmediately(anyString())).willAnswer(i -> {
String key = i.getArgument(0);
for (NotificationEntry entry : mHuns) {
if (entry.getKey().equals(key)) return false;
}
return true;
});
// THEN only the current HUN, mEntry, should be lifetimeExtended
assertTrue(mNotifLifetimeExtender.maybeExtendLifetime(mEntry, /* cancellationReason */ 0));
assertFalse(mNotifLifetimeExtender.maybeExtendLifetime(
new NotificationEntryBuilder()
.setPkg("test-package")
.build(), /* cancellationReason */ 0));
}
@Test
public void testShowHUNOnInflationFinished() {
// WHEN a notification should HUN and its inflation is finished
when(mNotificationInterruptStateProvider.shouldHeadsUp(mEntry)).thenReturn(true);
ArgumentCaptor<BindCallback> bindCallbackCaptor =
ArgumentCaptor.forClass(BindCallback.class);
mCollectionListener.onEntryAdded(mEntry);
verify(mHeadsUpViewBinder).bindHeadsUpView(eq(mEntry), bindCallbackCaptor.capture());
bindCallbackCaptor.getValue().onBindFinished(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);
ArgumentCaptor<BindCallback> bindCallbackCaptor =
ArgumentCaptor.forClass(BindCallback.class);
mCollectionListener.onEntryAdded(mEntry);
// THEN we never bind the heads up view or tell HeadsUpManager to show the notification
verify(mHeadsUpViewBinder, never()).bindHeadsUpView(
eq(mEntry), bindCallbackCaptor.capture());
verify(mHeadsUpManager, never()).showNotification(mEntry);
}
@Test
public void testOnEntryRemovedRemovesHeadsUpNotification() {
// GIVEN the current HUN is mEntry
addHUN(mEntry);
// WHEN mEntry is removed from the notification collection
mCollectionListener.onEntryRemoved(mEntry, /* cancellation reason */ 0);
when(mRemoteInputManager.isSpinning(any())).thenReturn(false);
// THEN heads up manager should remove the entry
verify(mHeadsUpManager).removeNotification(mEntry.getKey(), false);
}
private void addHUN(NotificationEntry entry) {
mHuns.add(entry);
when(mHeadsUpManager.getTopEntry()).thenReturn(entry);
mOnHeadsUpChangedListener.onHeadsUpStateChanged(entry, entry != null);
}
}

View File

@@ -0,0 +1,238 @@
/*
* 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 android.testing.AndroidTestingRunner
import android.testing.TestableLooper.RunWithLooper
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.statusbar.NotificationRemoteInputManager
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.NotifSectioner
import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener
import com.android.systemui.statusbar.notification.collection.notifcollection.NotifLifetimeExtender
import com.android.systemui.statusbar.notification.collection.notifcollection.NotifLifetimeExtender.OnEndLifetimeExtensionCallback
import com.android.systemui.statusbar.notification.collection.render.NodeController
import com.android.systemui.statusbar.notification.interruption.HeadsUpViewBinder
import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProvider
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.mockito.any
import com.android.systemui.util.mockito.eq
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.withArgCaptor
import com.android.systemui.util.time.FakeSystemClock
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers.anyString
import org.mockito.BDDMockito.given
import org.mockito.Mockito.never
import org.mockito.Mockito.times
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
import java.util.ArrayList
import org.mockito.Mockito.`when` as whenever
@SmallTest
@RunWith(AndroidTestingRunner::class)
@RunWithLooper
class HeadsUpCoordinatorTest : SysuiTestCase() {
private lateinit var mCoordinator: HeadsUpCoordinator
// captured listeners and pluggables:
private lateinit var mCollectionListener: NotifCollectionListener
private lateinit var mNotifPromoter: NotifPromoter
private lateinit var mNotifLifetimeExtender: NotifLifetimeExtender
private lateinit var mOnHeadsUpChangedListener: OnHeadsUpChangedListener
private lateinit var mNotifSectioner: NotifSectioner
private val mNotifPipeline: NotifPipeline = mock()
private val mHeadsUpManager: HeadsUpManager = mock()
private val mHeadsUpViewBinder: HeadsUpViewBinder = mock()
private val mNotificationInterruptStateProvider: NotificationInterruptStateProvider = mock()
private val mRemoteInputManager: NotificationRemoteInputManager = mock()
private val mEndLifetimeExtension: OnEndLifetimeExtensionCallback = mock()
private val mHeaderController: NodeController = mock()
private lateinit var mEntry: NotificationEntry
private val mExecutor = FakeExecutor(FakeSystemClock())
private val mHuns: ArrayList<NotificationEntry> = ArrayList()
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
mCoordinator = HeadsUpCoordinator(
mHeadsUpManager,
mHeadsUpViewBinder,
mNotificationInterruptStateProvider,
mRemoteInputManager,
mHeaderController,
mExecutor)
mCoordinator.attach(mNotifPipeline)
// capture arguments:
mCollectionListener = withArgCaptor {
verify(mNotifPipeline).addCollectionListener(capture())
}
mNotifPromoter = withArgCaptor {
verify(mNotifPipeline).addPromoter(capture())
}
mNotifLifetimeExtender = withArgCaptor {
verify(mNotifPipeline).addNotificationLifetimeExtender(capture())
}
mOnHeadsUpChangedListener = withArgCaptor {
verify(mHeadsUpManager).addListener(capture())
}
given(mHeadsUpManager.allEntries).willAnswer { mHuns.stream() }
given(mHeadsUpManager.isAlerting(anyString())).willAnswer { invocation ->
val key = invocation.getArgument<String>(0)
mHuns.any { entry -> entry.key == key }
}
given(mHeadsUpManager.canRemoveImmediately(anyString())).willAnswer { invocation ->
val key = invocation.getArgument<String>(0)
!mHuns.any { entry -> entry.key == key }
}
whenever(mHeadsUpManager.getEarliestRemovalTime(anyString())).thenReturn(1000L)
mNotifSectioner = mCoordinator.sectioner
mNotifLifetimeExtender.setCallback(mEndLifetimeExtension)
mEntry = NotificationEntryBuilder().build()
}
@Test
fun testCancelStickyNotification() {
whenever(mHeadsUpManager.isSticky(anyString())).thenReturn(true)
addHUN(mEntry)
whenever(mHeadsUpManager.canRemoveImmediately(anyString())).thenReturn(false, true)
whenever(mHeadsUpManager.getEarliestRemovalTime(anyString())).thenReturn(1000L, 0L)
assertTrue(mNotifLifetimeExtender.maybeExtendLifetime(mEntry, 0))
mExecutor.advanceClockToLast()
mExecutor.runAllReady()
verify(mHeadsUpManager, times(0)).removeNotification(anyString(), eq(false))
verify(mHeadsUpManager, times(1)).removeNotification(anyString(), eq(true))
}
@Test
fun testCancelUpdatedStickyNotification() {
whenever(mHeadsUpManager.isSticky(anyString())).thenReturn(true)
addHUN(mEntry)
whenever(mHeadsUpManager.getEarliestRemovalTime(anyString())).thenReturn(1000L, 500L)
assertTrue(mNotifLifetimeExtender.maybeExtendLifetime(mEntry, 0))
mExecutor.advanceClockToLast()
mExecutor.runAllReady()
verify(mHeadsUpManager, times(0)).removeNotification(anyString(), eq(false))
verify(mHeadsUpManager, times(0)).removeNotification(anyString(), eq(true))
}
@Test
fun testCancelNotification() {
whenever(mHeadsUpManager.isSticky(anyString())).thenReturn(false)
addHUN(mEntry)
whenever(mHeadsUpManager.getEarliestRemovalTime(anyString())).thenReturn(1000L, 500L)
assertTrue(mNotifLifetimeExtender.maybeExtendLifetime(mEntry, 0))
mExecutor.advanceClockToLast()
mExecutor.runAllReady()
verify(mHeadsUpManager, times(1)).removeNotification(anyString(), eq(false))
verify(mHeadsUpManager, times(0)).removeNotification(anyString(), eq(true))
}
@Test
fun testPromotesCurrentHUN() {
// GIVEN the current HUN is set to mEntry
addHUN(mEntry)
// THEN only promote the current HUN, mEntry
assertTrue(mNotifPromoter.shouldPromoteToTopLevel(mEntry))
assertFalse(mNotifPromoter.shouldPromoteToTopLevel(NotificationEntryBuilder()
.setPkg("test-package2")
.build()))
}
@Test
fun testIncludeInSectionCurrentHUN() {
// GIVEN the current HUN is set to mEntry
addHUN(mEntry)
// THEN only section the current HUN, mEntry
assertTrue(mNotifSectioner.isInSection(mEntry))
assertFalse(mNotifSectioner.isInSection(NotificationEntryBuilder()
.setPkg("test-package")
.build()))
}
@Test
fun testLifetimeExtendsCurrentHUN() {
// GIVEN there is a HUN, mEntry
addHUN(mEntry)
// THEN only the current HUN, mEntry, should be lifetimeExtended
assertTrue(mNotifLifetimeExtender.maybeExtendLifetime(mEntry, /* cancellationReason */ 0))
assertFalse(mNotifLifetimeExtender.maybeExtendLifetime(
NotificationEntryBuilder()
.setPkg("test-package")
.build(), /* cancellationReason */ 0))
}
@Test
fun testShowHUNOnInflationFinished() {
// WHEN a notification should HUN and its inflation is finished
whenever(mNotificationInterruptStateProvider.shouldHeadsUp(mEntry)).thenReturn(true)
mCollectionListener.onEntryAdded(mEntry)
withArgCaptor<BindCallback> {
verify(mHeadsUpViewBinder).bindHeadsUpView(eq(mEntry), capture())
}.onBindFinished(mEntry)
// THEN we tell the HeadsUpManager to show the notification
verify(mHeadsUpManager).showNotification(mEntry)
}
@Test
fun testNoHUNOnInflationFinished() {
// WHEN a notification shouldn't HUN and its inflation is finished
whenever(mNotificationInterruptStateProvider.shouldHeadsUp(mEntry)).thenReturn(false)
mCollectionListener.onEntryAdded(mEntry)
// THEN we never bind the heads up view or tell HeadsUpManager to show the notification
verify(mHeadsUpViewBinder, never()).bindHeadsUpView(eq(mEntry), any())
verify(mHeadsUpManager, never()).showNotification(mEntry)
}
@Test
fun testOnEntryRemovedRemovesHeadsUpNotification() {
// GIVEN the current HUN is mEntry
addHUN(mEntry)
// WHEN mEntry is removed from the notification collection
mCollectionListener.onEntryRemoved(mEntry, /* cancellation reason */ 0)
whenever(mRemoteInputManager.isSpinning(any())).thenReturn(false)
// THEN heads up manager should remove the entry
verify(mHeadsUpManager).removeNotification(mEntry.key, false)
}
private fun addHUN(entry: NotificationEntry) {
mHuns.add(entry)
whenever(mHeadsUpManager.topEntry).thenReturn(entry)
mOnHeadsUpChangedListener.onHeadsUpStateChanged(entry, true)
}
}