blob: 0c6409b38d21d5530a0142ee87dee36a21c685ef [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.row;
import static android.app.Notification.FLAG_BUBBLE;
import static android.app.NotificationManager.IMPORTANCE_DEFAULT;
import static android.app.NotificationManager.IMPORTANCE_HIGH;
import static com.android.systemui.statusbar.NotificationEntryHelper.modifyRanking;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import android.annotation.Nullable;
import android.app.ActivityManager;
import android.app.Notification;
import android.app.Notification.BubbleMetadata;
import android.app.NotificationChannel;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.pm.LauncherApps;
import android.graphics.drawable.Icon;
import android.os.UserHandle;
import android.service.notification.StatusBarNotification;
import android.testing.TestableLooper;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.widget.RemoteViews;
import com.android.systemui.TestableDependency;
import com.android.systemui.bubbles.BubbleController;
import com.android.systemui.bubbles.BubblesTestActivity;
import com.android.systemui.media.MediaFeatureFlag;
import com.android.systemui.plugins.FalsingManager;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.statusbar.NotificationMediaManager;
import com.android.systemui.statusbar.NotificationRemoteInputManager;
import com.android.systemui.statusbar.SmartReplyController;
import com.android.systemui.statusbar.notification.ConversationNotificationProcessor;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder;
import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection;
import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener;
import com.android.systemui.statusbar.notification.icon.IconBuilder;
import com.android.systemui.statusbar.notification.icon.IconManager;
import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier;
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow.ExpansionLogger;
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow.OnExpandClickListener;
import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.InflationFlag;
import com.android.systemui.statusbar.phone.ConfigurationControllerImpl;
import com.android.systemui.statusbar.phone.HeadsUpManagerPhone;
import com.android.systemui.statusbar.phone.KeyguardBypassController;
import com.android.systemui.statusbar.phone.NotificationGroupManager;
import com.android.systemui.statusbar.phone.NotificationShadeWindowController;
import com.android.systemui.statusbar.policy.SmartReplyConstants;
import com.android.systemui.tests.R;
import org.mockito.ArgumentCaptor;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
/**
* A helper class to create {@link ExpandableNotificationRow} (for both individual and group
* notifications).
*/
public class NotificationTestHelper {
/** Package name for testing purposes. */
public static final String PKG = "com.android.systemui";
/** System UI id for testing purposes. */
public static final int UID = 1000;
/** Current {@link UserHandle} of the system. */
public static final UserHandle USER_HANDLE = UserHandle.of(ActivityManager.getCurrentUser());
private static final String GROUP_KEY = "gruKey";
private static final String APP_NAME = "appName";
private final Context mContext;
private final TestableLooper mTestLooper;
private int mId;
private final NotificationGroupManager mGroupManager;
private ExpandableNotificationRow mRow;
private HeadsUpManagerPhone mHeadsUpManager;
private final NotifBindPipeline mBindPipeline;
private final NotifCollectionListener mBindPipelineEntryListener;
private final RowContentBindStage mBindStage;
private final IconManager mIconManager;
private StatusBarStateController mStatusBarStateController;
private final PeopleNotificationIdentifier mPeopleNotificationIdentifier;
public NotificationTestHelper(
Context context,
TestableDependency dependency,
TestableLooper testLooper) {
mContext = context;
mTestLooper = testLooper;
dependency.injectMockDependency(NotificationMediaManager.class);
dependency.injectMockDependency(BubbleController.class);
dependency.injectMockDependency(NotificationShadeWindowController.class);
mStatusBarStateController = mock(StatusBarStateController.class);
mGroupManager = new NotificationGroupManager(
mStatusBarStateController,
() -> mock(PeopleNotificationIdentifier.class));
mHeadsUpManager = new HeadsUpManagerPhone(mContext, mStatusBarStateController,
mock(KeyguardBypassController.class), mock(NotificationGroupManager.class),
mock(ConfigurationControllerImpl.class));
mGroupManager.setHeadsUpManager(mHeadsUpManager);
mIconManager = new IconManager(
mock(CommonNotifCollection.class),
mock(LauncherApps.class),
new IconBuilder(mContext));
NotificationContentInflater contentBinder = new NotificationContentInflater(
mock(NotifRemoteViewCache.class),
mock(NotificationRemoteInputManager.class),
() -> mock(SmartReplyConstants.class),
() -> mock(SmartReplyController.class),
mock(ConversationNotificationProcessor.class),
mock(MediaFeatureFlag.class),
mock(Executor.class));
contentBinder.setInflateSynchronously(true);
mBindStage = new RowContentBindStage(contentBinder,
mock(NotifInflationErrorManager.class),
mock(RowContentBindStageLogger.class));
CommonNotifCollection collection = mock(CommonNotifCollection.class);
mBindPipeline = new NotifBindPipeline(
collection,
mock(NotifBindPipelineLogger.class),
mTestLooper.getLooper());
mBindPipeline.setStage(mBindStage);
ArgumentCaptor<NotifCollectionListener> collectionListenerCaptor =
ArgumentCaptor.forClass(NotifCollectionListener.class);
verify(collection).addCollectionListener(collectionListenerCaptor.capture());
mBindPipelineEntryListener = collectionListenerCaptor.getValue();
mPeopleNotificationIdentifier = mock(PeopleNotificationIdentifier.class);
}
/**
* Creates a generic row.
*
* @return a generic row with no special properties.
* @throws Exception
*/
public ExpandableNotificationRow createRow() throws Exception {
return createRow(PKG, UID, USER_HANDLE);
}
/**
* Create a row with the package and user id specified.
*
* @param pkg package
* @param uid user id
* @return a row with a notification using the package and user id
* @throws Exception
*/
public ExpandableNotificationRow createRow(String pkg, int uid, UserHandle userHandle)
throws Exception {
return createRow(pkg, uid, userHandle, false /* isGroupSummary */, null /* groupKey */);
}
/**
* Creates a row based off the notification given.
*
* @param notification the notification
* @return a row built off the notification
* @throws Exception
*/
public ExpandableNotificationRow createRow(Notification notification) throws Exception {
return generateRow(notification, PKG, UID, USER_HANDLE, 0 /* extraInflationFlags */);
}
/**
* Create a row with the specified content views inflated in addition to the default.
*
* @param extraInflationFlags the flags corresponding to the additional content views that
* should be inflated
* @return a row with the specified content views inflated in addition to the default
* @throws Exception
*/
public ExpandableNotificationRow createRow(@InflationFlag int extraInflationFlags)
throws Exception {
return generateRow(createNotification(), PKG, UID, USER_HANDLE, extraInflationFlags);
}
/**
* Returns an {@link ExpandableNotificationRow} group with the given number of child
* notifications.
*/
public ExpandableNotificationRow createGroup(int numChildren) throws Exception {
ExpandableNotificationRow row = createGroupSummary(GROUP_KEY);
for (int i = 0; i < numChildren; i++) {
ExpandableNotificationRow childRow = createGroupChild(GROUP_KEY);
row.addChildNotification(childRow);
}
return row;
}
/** Returns a group notification with 2 child notifications. */
public ExpandableNotificationRow createGroup() throws Exception {
return createGroup(2);
}
private ExpandableNotificationRow createGroupSummary(String groupkey) throws Exception {
return createRow(PKG, UID, USER_HANDLE, true /* isGroupSummary */, groupkey);
}
private ExpandableNotificationRow createGroupChild(String groupkey) throws Exception {
return createRow(PKG, UID, USER_HANDLE, false /* isGroupSummary */, groupkey);
}
/**
* Returns an {@link ExpandableNotificationRow} that should be shown as a bubble.
*/
public ExpandableNotificationRow createBubbleInGroup()
throws Exception {
return createBubble(makeBubbleMetadata(null), PKG, true);
}
/**
* Returns an {@link ExpandableNotificationRow} that should be shown as a bubble.
*/
public ExpandableNotificationRow createBubble()
throws Exception {
return createBubble(makeBubbleMetadata(null), PKG, false);
}
/**
* Returns an {@link ExpandableNotificationRow} that should be shown as a bubble.
*
* @param deleteIntent the intent to assign to {@link BubbleMetadata#deleteIntent}
*/
public ExpandableNotificationRow createBubble(@Nullable PendingIntent deleteIntent)
throws Exception {
return createBubble(makeBubbleMetadata(deleteIntent), PKG, false);
}
/**
* Returns an {@link ExpandableNotificationRow} that should be shown as a bubble.
*
* @param bubbleMetadata the {@link BubbleMetadata} to use
*/
public ExpandableNotificationRow createBubble(BubbleMetadata bubbleMetadata, String pkg)
throws Exception {
return createBubble(bubbleMetadata, pkg, false);
}
private ExpandableNotificationRow createBubble(BubbleMetadata bubbleMetadata, String pkg,
boolean inGroup)
throws Exception {
Notification n = createNotification(false /* isGroupSummary */,
inGroup ? GROUP_KEY : null /* groupKey */, bubbleMetadata);
n.flags |= FLAG_BUBBLE;
ExpandableNotificationRow row = generateRow(n, pkg, UID, USER_HANDLE,
0 /* extraInflationFlags */, IMPORTANCE_HIGH);
modifyRanking(row.getEntry())
.setCanBubble(true)
.build();
return row;
}
/**
* Creates a notification row with the given details.
*
* @param pkg package used for creating a {@link StatusBarNotification}
* @param uid uid used for creating a {@link StatusBarNotification}
* @param isGroupSummary whether the notification row is a group summary
* @param groupKey the group key for the notification group used across notifications
* @return a row with that's either a standalone notification or a group notification if the
* groupKey is non-null
* @throws Exception
*/
private ExpandableNotificationRow createRow(
String pkg,
int uid,
UserHandle userHandle,
boolean isGroupSummary,
@Nullable String groupKey)
throws Exception {
Notification notif = createNotification(isGroupSummary, groupKey);
return generateRow(notif, pkg, uid, userHandle, 0 /* inflationFlags */);
}
/**
* Creates a generic notification.
*
* @return a notification with no special properties
*/
public Notification createNotification() {
return createNotification(false /* isGroupSummary */, null /* groupKey */);
}
/**
* Creates a notification with the given parameters.
*
* @param isGroupSummary whether the notification is a group summary
* @param groupKey the group key for the notification group used across notifications
* @return a notification that is in the group specified or standalone if unspecified
*/
private Notification createNotification(boolean isGroupSummary, @Nullable String groupKey) {
return createNotification(isGroupSummary, groupKey, null /* bubble metadata */);
}
/**
* Creates a notification with the given parameters.
*
* @param isGroupSummary whether the notification is a group summary
* @param groupKey the group key for the notification group used across notifications
* @param bubbleMetadata the bubble metadata to use for this notification if it exists.
* @return a notification that is in the group specified or standalone if unspecified
*/
public Notification createNotification(boolean isGroupSummary,
@Nullable String groupKey, @Nullable BubbleMetadata bubbleMetadata) {
Notification publicVersion = new Notification.Builder(mContext).setSmallIcon(
R.drawable.ic_person)
.setCustomContentView(new RemoteViews(mContext.getPackageName(),
R.layout.custom_view_dark))
.build();
Notification.Builder notificationBuilder = new Notification.Builder(mContext, "channelId")
.setSmallIcon(R.drawable.ic_person)
.setContentTitle("Title")
.setContentText("Text")
.setPublicVersion(publicVersion)
.setStyle(new Notification.BigTextStyle().bigText("Big Text"));
if (isGroupSummary) {
notificationBuilder.setGroupSummary(true);
}
if (!TextUtils.isEmpty(groupKey)) {
notificationBuilder.setGroup(groupKey);
}
if (bubbleMetadata != null) {
notificationBuilder.setBubbleMetadata(bubbleMetadata);
}
return notificationBuilder.build();
}
public StatusBarStateController getStatusBarStateController() {
return mStatusBarStateController;
}
private ExpandableNotificationRow generateRow(
Notification notification,
String pkg,
int uid,
UserHandle userHandle,
@InflationFlag int extraInflationFlags)
throws Exception {
return generateRow(notification, pkg, uid, userHandle, extraInflationFlags,
IMPORTANCE_DEFAULT);
}
private ExpandableNotificationRow generateRow(
Notification notification,
String pkg,
int uid,
UserHandle userHandle,
@InflationFlag int extraInflationFlags,
int importance)
throws Exception {
LayoutInflater inflater = (LayoutInflater) mContext.getSystemService(
mContext.LAYOUT_INFLATER_SERVICE);
mRow = (ExpandableNotificationRow) inflater.inflate(
R.layout.status_bar_notification_row,
null /* root */,
false /* attachToRoot */);
ExpandableNotificationRow row = mRow;
final NotificationChannel channel =
new NotificationChannel(
notification.getChannelId(),
notification.getChannelId(),
importance);
channel.setBlockable(true);
NotificationEntry entry = new NotificationEntryBuilder()
.setPkg(pkg)
.setOpPkg(pkg)
.setId(mId++)
.setUid(uid)
.setInitialPid(2000)
.setNotification(notification)
.setUser(userHandle)
.setPostTime(System.currentTimeMillis())
.setChannel(channel)
.build();
entry.setRow(row);
mIconManager.createIcons(entry);
row.setEntry(entry);
mBindPipelineEntryListener.onEntryInit(entry);
mBindPipeline.manageRow(entry, row);
row.initialize(
APP_NAME,
entry.getKey(),
mock(ExpansionLogger.class),
mock(KeyguardBypassController.class),
mGroupManager,
mHeadsUpManager,
mBindStage,
mock(OnExpandClickListener.class),
mock(NotificationMediaManager.class),
mock(ExpandableNotificationRow.OnAppOpsClickListener.class),
mock(FalsingManager.class),
mStatusBarStateController,
mPeopleNotificationIdentifier);
row.setAboveShelfChangedListener(aboveShelf -> { });
mBindStage.getStageParams(entry).requireContentViews(extraInflationFlags);
inflateAndWait(entry);
// This would be done as part of onAsyncInflationFinished, but we skip large amounts of
// the callback chain, so we need to make up for not adding it to the group manager
// here.
mGroupManager.onEntryAdded(entry);
return row;
}
private void inflateAndWait(NotificationEntry entry) throws Exception {
CountDownLatch countDownLatch = new CountDownLatch(1);
mBindStage.requestRebind(entry, en -> countDownLatch.countDown());
mTestLooper.processAllMessages();
assertTrue(countDownLatch.await(500, TimeUnit.MILLISECONDS));
}
private BubbleMetadata makeBubbleMetadata(PendingIntent deleteIntent) {
Intent target = new Intent(mContext, BubblesTestActivity.class);
PendingIntent bubbleIntent = PendingIntent.getActivity(mContext, 0, target, 0);
return new BubbleMetadata.Builder(bubbleIntent,
Icon.createWithResource(mContext, R.drawable.android))
.setDeleteIntent(deleteIntent)
.setDesiredHeight(314)
.build();
}
}