blob: 72d8ff3f35f54ecb0922724eb3a119bcecbf59a9 [file] [log] [blame]
/*
* 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.notification.collection.GroupEntry.ROOT_ENTRY;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
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.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
import static java.util.Objects.requireNonNull;
import android.os.RemoteException;
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper;
import androidx.annotation.NonNull;
import androidx.test.filters.SmallTest;
import com.android.internal.statusbar.IStatusBarService;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.statusbar.NotificationLockscreenUserManager;
import com.android.systemui.statusbar.RankingBuilder;
import com.android.systemui.statusbar.notification.SectionClassifier;
import com.android.systemui.statusbar.notification.collection.GroupEntry;
import com.android.systemui.statusbar.notification.collection.GroupEntryBuilder;
import com.android.systemui.statusbar.notification.collection.ListEntry;
import com.android.systemui.statusbar.notification.collection.NotifPipeline;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder;
import com.android.systemui.statusbar.notification.collection.inflation.BindEventManagerImpl;
import com.android.systemui.statusbar.notification.collection.inflation.NotifInflater;
import com.android.systemui.statusbar.notification.collection.inflation.NotifUiAdjustmentProvider;
import com.android.systemui.statusbar.notification.collection.listbuilder.NotifSection;
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.listbuilder.pluggable.NotifSectioner;
import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener;
import com.android.systemui.statusbar.notification.collection.render.NotifViewBarn;
import com.android.systemui.statusbar.notification.row.NotifInflationErrorManager;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.Spy;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@SmallTest
@RunWith(AndroidTestingRunner.class)
@TestableLooper.RunWithLooper
public class PreparationCoordinatorTest extends SysuiTestCase {
private NotifCollectionListener mCollectionListener;
private OnBeforeFinalizeFilterListener mBeforeFilterListener;
private NotifFilter mUninflatedFilter;
private NotifFilter mInflationErrorFilter;
private NotifInflationErrorManager mErrorManager;
private NotificationEntry mEntry;
private Exception mInflationError;
@Captor private ArgumentCaptor<NotifCollectionListener> mCollectionListenerCaptor;
@Captor private ArgumentCaptor<OnBeforeFinalizeFilterListener> mBeforeFilterListenerCaptor;
@Captor private ArgumentCaptor<NotifInflater.Params> mParamsCaptor;
@Mock private NotifSectioner mNotifSectioner;
@Mock private NotifSection mNotifSection;
@Mock private NotifPipeline mNotifPipeline;
@Mock private IStatusBarService mService;
@Mock private BindEventManagerImpl mBindEventManagerImpl;
@Mock private NotificationLockscreenUserManager mLockscreenUserManager;
@Spy private FakeNotifInflater mNotifInflater = new FakeNotifInflater();
private final SectionClassifier mSectionClassifier = new SectionClassifier();
private NotifUiAdjustmentProvider mAdjustmentProvider;
@NonNull
private NotificationEntryBuilder getNotificationEntryBuilder() {
return new NotificationEntryBuilder().setSection(mNotifSection);
}
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
mAdjustmentProvider =
new NotifUiAdjustmentProvider(mLockscreenUserManager, mSectionClassifier);
mEntry = getNotificationEntryBuilder().setParent(ROOT_ENTRY).build();
mInflationError = new Exception(TEST_MESSAGE);
mErrorManager = new NotifInflationErrorManager();
when(mNotifSection.getSectioner()).thenReturn(mNotifSectioner);
setSectionIsLowPriority(false);
PreparationCoordinator coordinator = new PreparationCoordinator(
mock(PreparationCoordinatorLogger.class),
mNotifInflater,
mErrorManager,
mock(NotifViewBarn.class),
mAdjustmentProvider,
mService,
mBindEventManagerImpl,
TEST_CHILD_BIND_CUTOFF,
TEST_MAX_GROUP_DELAY);
ArgumentCaptor<NotifFilter> filterCaptor = ArgumentCaptor.forClass(NotifFilter.class);
coordinator.attach(mNotifPipeline);
verify(mNotifPipeline, times(2)).addFinalizeFilter(filterCaptor.capture());
List<NotifFilter> filters = filterCaptor.getAllValues();
mInflationErrorFilter = filters.get(0);
mUninflatedFilter = filters.get(1);
verify(mNotifPipeline).addCollectionListener(mCollectionListenerCaptor.capture());
mCollectionListener = mCollectionListenerCaptor.getValue();
verify(mNotifPipeline).addOnBeforeFinalizeFilterListener(
mBeforeFilterListenerCaptor.capture());
mBeforeFilterListener = mBeforeFilterListenerCaptor.getValue();
mCollectionListener.onEntryInit(mEntry);
}
@Test
public void testErrorLogsToService() throws RemoteException {
// WHEN an entry has an inflation error.
mErrorManager.setInflationError(mEntry, mInflationError);
// THEN we log to status bar service.
verify(mService).onNotificationError(
eq(mEntry.getSbn().getPackageName()),
eq(mEntry.getSbn().getTag()),
eq(mEntry.getSbn().getId()),
eq(mEntry.getSbn().getUid()),
eq(mEntry.getSbn().getInitialPid()),
eq(mInflationError.getMessage()),
eq(mEntry.getSbn().getUser().getIdentifier()));
}
@Test
public void testFiltersOutErroredNotifications() {
// WHEN an entry has an inflation error.
mErrorManager.setInflationError(mEntry, mInflationError);
// THEN we filter it from the notification list.
assertTrue(mInflationErrorFilter.shouldFilterOut(mEntry, 0));
}
@Test
public void testInflatesNewNotification() {
// WHEN there is a new notification
mCollectionListener.onEntryAdded(mEntry);
mBeforeFilterListener.onBeforeFinalizeFilter(List.of(mEntry));
// THEN we inflate it
verify(mNotifInflater).inflateViews(eq(mEntry), any(), any());
// THEN we filter it out until it's done inflating.
assertTrue(mUninflatedFilter.shouldFilterOut(mEntry, 0));
}
@Test
public void testRebindsInflatedNotificationsOnUpdate() {
// GIVEN an inflated notification
mCollectionListener.onEntryAdded(mEntry);
mBeforeFilterListener.onBeforeFinalizeFilter(List.of(mEntry));
verify(mNotifInflater).inflateViews(eq(mEntry), any(), any());
mNotifInflater.invokeInflateCallbackForEntry(mEntry);
// WHEN notification is updated
mCollectionListener.onEntryUpdated(mEntry);
mBeforeFilterListener.onBeforeFinalizeFilter(List.of(mEntry));
// THEN we rebind it
verify(mNotifInflater).rebindViews(eq(mEntry), any(), any());
// THEN we do not filter it because it's not the first inflation.
assertFalse(mUninflatedFilter.shouldFilterOut(mEntry, 0));
}
@Test
public void testEntrySmartReplyAdditionWillRebindViews() {
// GIVEN an inflated notification
mCollectionListener.onEntryAdded(mEntry);
mBeforeFilterListener.onBeforeFinalizeFilter(List.of(mEntry));
verify(mNotifInflater).inflateViews(eq(mEntry), any(), any());
mNotifInflater.invokeInflateCallbackForEntry(mEntry);
// WHEN notification ranking now has smart replies
mEntry.setRanking(new RankingBuilder(mEntry.getRanking()).setSmartReplies("yes").build());
mBeforeFilterListener.onBeforeFinalizeFilter(List.of(mEntry));
// THEN we rebind it
verify(mNotifInflater).rebindViews(eq(mEntry), any(), any());
// THEN we do not filter it because it's not the first inflation.
assertFalse(mUninflatedFilter.shouldFilterOut(mEntry, 0));
}
@Test
public void testEntryChangedToMinimizedSectionWillRebindViews() {
// GIVEN an inflated notification
mCollectionListener.onEntryAdded(mEntry);
mBeforeFilterListener.onBeforeFinalizeFilter(List.of(mEntry));
verify(mNotifInflater).inflateViews(eq(mEntry), mParamsCaptor.capture(), any());
assertFalse(mParamsCaptor.getValue().isLowPriority());
mNotifInflater.invokeInflateCallbackForEntry(mEntry);
// WHEN notification moves to a min priority section
setSectionIsLowPriority(true);
mBeforeFilterListener.onBeforeFinalizeFilter(List.of(mEntry));
// THEN we rebind it
verify(mNotifInflater).rebindViews(eq(mEntry), mParamsCaptor.capture(), any());
assertTrue(mParamsCaptor.getValue().isLowPriority());
// THEN we do not filter it because it's not the first inflation.
assertFalse(mUninflatedFilter.shouldFilterOut(mEntry, 0));
}
@Test
public void testMinimizedEntryMovedIntoGroupWillRebindViews() {
// GIVEN an inflated, minimized notification
setSectionIsLowPriority(true);
mCollectionListener.onEntryAdded(mEntry);
mBeforeFilterListener.onBeforeFinalizeFilter(List.of(mEntry));
verify(mNotifInflater).inflateViews(eq(mEntry), mParamsCaptor.capture(), any());
assertTrue(mParamsCaptor.getValue().isLowPriority());
mNotifInflater.invokeInflateCallbackForEntry(mEntry);
// WHEN notification is moved under a parent
NotificationEntryBuilder.setNewParent(mEntry, mock(GroupEntry.class));
mBeforeFilterListener.onBeforeFinalizeFilter(List.of(mEntry));
// THEN we rebind it as not-minimized
verify(mNotifInflater).rebindViews(eq(mEntry), mParamsCaptor.capture(), any());
assertFalse(mParamsCaptor.getValue().isLowPriority());
// THEN we do not filter it because it's not the first inflation.
assertFalse(mUninflatedFilter.shouldFilterOut(mEntry, 0));
}
@Test
public void testEntryRankChangeWillNotRebindViews() {
// GIVEN an inflated notification
mCollectionListener.onEntryAdded(mEntry);
mBeforeFilterListener.onBeforeFinalizeFilter(List.of(mEntry));
verify(mNotifInflater).inflateViews(eq(mEntry), any(), any());
mNotifInflater.invokeInflateCallbackForEntry(mEntry);
// WHEN notification ranking changes rank, which does not affect views
mEntry.setRanking(new RankingBuilder(mEntry.getRanking()).setRank(100).build());
mBeforeFilterListener.onBeforeFinalizeFilter(List.of(mEntry));
// THEN we do not rebind it
verify(mNotifInflater, never()).rebindViews(eq(mEntry), any(), any());
// THEN we do not filter it because it's not the first inflation.
assertFalse(mUninflatedFilter.shouldFilterOut(mEntry, 0));
}
@Test
public void testDoesntFilterInflatedNotifs() {
// GIVEN an inflated notification
mCollectionListener.onEntryAdded(mEntry);
mBeforeFilterListener.onBeforeFinalizeFilter(List.of(mEntry));
verify(mNotifInflater).inflateViews(eq(mEntry), any(), any());
mNotifInflater.invokeInflateCallbackForEntry(mEntry);
// THEN it isn't filtered from shade list
assertFalse(mUninflatedFilter.shouldFilterOut(mEntry, 0));
}
@Test
public void testCutoffGroupChildrenNotInflated() {
// WHEN there is a new notification group is posted
int id = 0;
NotificationEntry summary = getNotificationEntryBuilder()
.setOverrideGroupKey(TEST_GROUP_KEY)
.setId(id++)
.build();
List<NotificationEntry> children = new ArrayList<>();
for (int i = 0; i < TEST_CHILD_BIND_CUTOFF + 1; i++) {
NotificationEntry child = getNotificationEntryBuilder()
.setOverrideGroupKey(TEST_GROUP_KEY)
.setId(id++)
.build();
children.add(child);
}
GroupEntry groupEntry = new GroupEntryBuilder()
.setKey(TEST_GROUP_KEY)
.setSummary(summary)
.setChildren(children)
.build();
mCollectionListener.onEntryInit(summary);
for (NotificationEntry entry : children) {
mCollectionListener.onEntryInit(entry);
}
mCollectionListener.onEntryAdded(summary);
for (NotificationEntry entry : children) {
mCollectionListener.onEntryAdded(entry);
}
mBeforeFilterListener.onBeforeFinalizeFilter(List.of(groupEntry));
// THEN we inflate up to the cut-off only
for (int i = 0; i < children.size(); i++) {
if (i < TEST_CHILD_BIND_CUTOFF) {
verify(mNotifInflater).inflateViews(eq(children.get(i)), any(), any());
} else {
verify(mNotifInflater, never()).inflateViews(eq(children.get(i)), any(), any());
}
}
}
@Test
public void testPartiallyInflatedGroupsAreFilteredOut() {
// GIVEN a newly-posted group with a summary and two children
final GroupEntry group = new GroupEntryBuilder()
.setCreationTime(400)
.setSummary(getNotificationEntryBuilder().setId(1).build())
.addChild(getNotificationEntryBuilder().setId(2).build())
.addChild(getNotificationEntryBuilder().setId(3).build())
.build();
fireAddEvents(List.of(group));
final NotificationEntry child0 = group.getChildren().get(0);
mBeforeFilterListener.onBeforeFinalizeFilter(List.of(group));
// WHEN one of this children finishes inflating
mNotifInflater.invokeInflateCallbackForEntry(child0);
// THEN the inflated child is still filtered out
assertTrue(mUninflatedFilter.shouldFilterOut(child0, 401));
}
@Test
public void testPartiallyInflatedGroupsAreFilteredOutSummaryVersion() {
// GIVEN a newly-posted group with a summary and two children
final GroupEntry group = new GroupEntryBuilder()
.setCreationTime(400)
.setSummary(getNotificationEntryBuilder().setId(1).build())
.addChild(getNotificationEntryBuilder().setId(2).build())
.addChild(getNotificationEntryBuilder().setId(3).build())
.build();
fireAddEvents(List.of(group));
final NotificationEntry summary = group.getSummary();
final NotificationEntry child0 = group.getChildren().get(0);
final NotificationEntry child1 = group.getChildren().get(1);
mBeforeFilterListener.onBeforeFinalizeFilter(List.of(group));
// WHEN all of the children (but not the summary) finish inflating
mNotifInflater.invokeInflateCallbackForEntry(child0);
mNotifInflater.invokeInflateCallbackForEntry(child1);
// THEN the entire group is still filtered out
assertTrue(mUninflatedFilter.shouldFilterOut(summary, 401));
assertTrue(mUninflatedFilter.shouldFilterOut(child0, 401));
assertTrue(mUninflatedFilter.shouldFilterOut(child1, 401));
}
@Test
public void testCompletedInflatedGroupsAreReleased() {
// GIVEN a newly-posted group with a summary and two children
final GroupEntry group = new GroupEntryBuilder()
.setCreationTime(400)
.setSummary(getNotificationEntryBuilder().setId(1).build())
.addChild(getNotificationEntryBuilder().setId(2).build())
.addChild(getNotificationEntryBuilder().setId(3).build())
.build();
fireAddEvents(List.of(group));
final NotificationEntry summary = group.getSummary();
final NotificationEntry child0 = group.getChildren().get(0);
final NotificationEntry child1 = group.getChildren().get(1);
mBeforeFilterListener.onBeforeFinalizeFilter(List.of(group));
// WHEN all of the children (and the summary) finish inflating
mNotifInflater.invokeInflateCallbackForEntry(child0);
mNotifInflater.invokeInflateCallbackForEntry(child1);
mNotifInflater.invokeInflateCallbackForEntry(summary);
// THEN the entire group is still filtered out
assertFalse(mUninflatedFilter.shouldFilterOut(summary, 401));
assertFalse(mUninflatedFilter.shouldFilterOut(child0, 401));
assertFalse(mUninflatedFilter.shouldFilterOut(child1, 401));
}
@Test
public void testCallConversationManagerBindWhenInflated() {
mBeforeFilterListener.onBeforeFinalizeFilter(List.of(mEntry));
mNotifInflater.getInflateCallback(mEntry).onInflationFinished(mEntry, null);
verify(mBindEventManagerImpl, times(1)).notifyViewBound(eq(mEntry));
verifyNoMoreInteractions(mBindEventManagerImpl);
}
@Test
public void testPartiallyInflatedGroupsAreReleasedAfterTimeout() {
// GIVEN a newly-posted group with a summary and two children
final GroupEntry group = new GroupEntryBuilder()
.setCreationTime(400)
.setSummary(getNotificationEntryBuilder().setId(1).build())
.addChild(getNotificationEntryBuilder().setId(2).build())
.addChild(getNotificationEntryBuilder().setId(3).build())
.build();
fireAddEvents(List.of(group));
final NotificationEntry child0 = group.getChildren().get(0);
mBeforeFilterListener.onBeforeFinalizeFilter(List.of(group));
// WHEN one of this children finishes inflating and enough time passes
mNotifInflater.invokeInflateCallbackForEntry(child0);
// THEN the inflated child is not filtered out even though the rest of the group hasn't
// finished inflating yet
assertTrue(mUninflatedFilter.shouldFilterOut(child0, TEST_MAX_GROUP_DELAY + 1));
}
private static class FakeNotifInflater implements NotifInflater {
private final Map<NotificationEntry, InflationCallback> mInflateCallbacks = new HashMap<>();
@Override
public void inflateViews(@NonNull NotificationEntry entry, @NonNull Params params,
@NonNull InflationCallback callback) {
mInflateCallbacks.put(entry, callback);
}
@Override
public void rebindViews(@NonNull NotificationEntry entry, @NonNull Params params,
@NonNull InflationCallback callback) {
}
@Override
public void abortInflation(@NonNull NotificationEntry entry) {
}
public InflationCallback getInflateCallback(NotificationEntry entry) {
return requireNonNull(mInflateCallbacks.get(entry));
}
public void invokeInflateCallbackForEntry(NotificationEntry entry) {
getInflateCallback(entry).onInflationFinished(entry, entry.getRowController());
}
@Override
public void releaseViews(@NonNull NotificationEntry entry) {
}
}
private void fireAddEvents(List<? extends ListEntry> entries) {
for (ListEntry entry : entries) {
if (entry instanceof GroupEntry) {
GroupEntry ge = (GroupEntry) entry;
fireAddEvents(ge.getSummary());
fireAddEvents(ge.getChildren());
} else {
fireAddEvents((NotificationEntry) entry);
}
}
}
private void fireAddEvents(NotificationEntry entry) {
mCollectionListener.onEntryInit(entry);
mCollectionListener.onEntryAdded(entry);
}
private static final String TEST_MESSAGE = "TEST_MESSAGE";
private static final String TEST_GROUP_KEY = "TEST_GROUP_KEY";
private static final int TEST_CHILD_BIND_CUTOFF = 9;
private static final int TEST_MAX_GROUP_DELAY = 100;
private void setSectionIsLowPriority(boolean minimized) {
mSectionClassifier.setMinimizedSections(minimized
? Collections.singleton(mNotifSection.getSectioner())
: Collections.emptyList());
}
}