1 /*
2  * Copyright (C) 2020 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.systemui.statusbar.notification.row;
18 
19 import static android.app.Notification.FLAG_BUBBLE;
20 import static android.app.NotificationManager.IMPORTANCE_DEFAULT;
21 import static android.app.NotificationManager.IMPORTANCE_HIGH;
22 
23 import static com.android.systemui.statusbar.NotificationEntryHelper.modifyRanking;
24 
25 import static org.junit.Assert.assertTrue;
26 import static org.mockito.Mockito.mock;
27 import static org.mockito.Mockito.verify;
28 
29 import android.annotation.Nullable;
30 import android.app.ActivityManager;
31 import android.app.Notification;
32 import android.app.Notification.BubbleMetadata;
33 import android.app.NotificationChannel;
34 import android.app.PendingIntent;
35 import android.content.Context;
36 import android.content.Intent;
37 import android.content.pm.LauncherApps;
38 import android.graphics.drawable.Icon;
39 import android.os.UserHandle;
40 import android.service.notification.StatusBarNotification;
41 import android.testing.TestableLooper;
42 import android.text.TextUtils;
43 import android.view.LayoutInflater;
44 import android.widget.RemoteViews;
45 
46 import com.android.systemui.TestableDependency;
47 import com.android.systemui.classifier.FalsingCollectorFake;
48 import com.android.systemui.classifier.FalsingManagerFake;
49 import com.android.systemui.dump.DumpManager;
50 import com.android.systemui.media.MediaFeatureFlag;
51 import com.android.systemui.media.dialog.MediaOutputDialogFactory;
52 import com.android.systemui.plugins.statusbar.StatusBarStateController;
53 import com.android.systemui.statusbar.NotificationMediaManager;
54 import com.android.systemui.statusbar.NotificationRemoteInputManager;
55 import com.android.systemui.statusbar.NotificationShadeWindowController;
56 import com.android.systemui.statusbar.notification.ConversationNotificationProcessor;
57 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
58 import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder;
59 import com.android.systemui.statusbar.notification.collection.legacy.NotificationGroupManagerLegacy;
60 import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection;
61 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener;
62 import com.android.systemui.statusbar.notification.icon.IconBuilder;
63 import com.android.systemui.statusbar.notification.icon.IconManager;
64 import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier;
65 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow.ExpansionLogger;
66 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow.OnExpandClickListener;
67 import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.InflationFlag;
68 import com.android.systemui.statusbar.phone.ConfigurationControllerImpl;
69 import com.android.systemui.statusbar.phone.HeadsUpManagerPhone;
70 import com.android.systemui.statusbar.phone.KeyguardBypassController;
71 import com.android.systemui.statusbar.policy.InflatedSmartReplyState;
72 import com.android.systemui.statusbar.policy.InflatedSmartReplyViewHolder;
73 import com.android.systemui.statusbar.policy.SmartReplyStateInflater;
74 import com.android.systemui.tests.R;
75 import com.android.systemui.wmshell.BubblesManager;
76 import com.android.systemui.wmshell.BubblesTestActivity;
77 import com.android.wm.shell.bubbles.Bubbles;
78 
79 import org.mockito.ArgumentCaptor;
80 
81 import java.util.Optional;
82 import java.util.concurrent.CountDownLatch;
83 import java.util.concurrent.Executor;
84 import java.util.concurrent.TimeUnit;
85 
86 /**
87  * A helper class to create {@link ExpandableNotificationRow} (for both individual and group
88  * notifications).
89  */
90 public class NotificationTestHelper {
91 
92     /** Package name for testing purposes. */
93     public static final String PKG = "com.android.systemui";
94     /** System UI id for testing purposes. */
95     public static final int UID = 1000;
96     /** Current {@link UserHandle} of the system. */
97     public static final UserHandle USER_HANDLE = UserHandle.of(ActivityManager.getCurrentUser());
98 
99     private static final String GROUP_KEY = "gruKey";
100     private static final String APP_NAME = "appName";
101 
102     private final Context mContext;
103     private final TestableLooper mTestLooper;
104     private int mId;
105     private final NotificationGroupManagerLegacy mGroupMembershipManager;
106     private final NotificationGroupManagerLegacy mGroupExpansionManager;
107     private ExpandableNotificationRow mRow;
108     private HeadsUpManagerPhone mHeadsUpManager;
109     private final NotifBindPipeline mBindPipeline;
110     private final NotifCollectionListener mBindPipelineEntryListener;
111     private final RowContentBindStage mBindStage;
112     private final IconManager mIconManager;
113     private StatusBarStateController mStatusBarStateController;
114     private final PeopleNotificationIdentifier mPeopleNotificationIdentifier;
115 
NotificationTestHelper( Context context, TestableDependency dependency, TestableLooper testLooper)116     public NotificationTestHelper(
117             Context context,
118             TestableDependency dependency,
119             TestableLooper testLooper) {
120         mContext = context;
121         mTestLooper = testLooper;
122         dependency.injectMockDependency(NotificationMediaManager.class);
123         dependency.injectMockDependency(NotificationShadeWindowController.class);
124         dependency.injectMockDependency(MediaOutputDialogFactory.class);
125         mStatusBarStateController = mock(StatusBarStateController.class);
126         mGroupMembershipManager = new NotificationGroupManagerLegacy(
127                 mStatusBarStateController,
128                 () -> mock(PeopleNotificationIdentifier.class),
129                 Optional.of((mock(Bubbles.class))),
130                 mock(DumpManager.class));
131         mGroupExpansionManager = mGroupMembershipManager;
132         mHeadsUpManager = new HeadsUpManagerPhone(mContext, mStatusBarStateController,
133                 mock(KeyguardBypassController.class), mock(NotificationGroupManagerLegacy.class),
134                 mock(ConfigurationControllerImpl.class));
135         mGroupMembershipManager.setHeadsUpManager(mHeadsUpManager);
136         mIconManager = new IconManager(
137                 mock(CommonNotifCollection.class),
138                 mock(LauncherApps.class),
139                 new IconBuilder(mContext));
140 
141         NotificationContentInflater contentBinder = new NotificationContentInflater(
142                 mock(NotifRemoteViewCache.class),
143                 mock(NotificationRemoteInputManager.class),
144                 mock(ConversationNotificationProcessor.class),
145                 mock(MediaFeatureFlag.class),
146                 mock(Executor.class),
147                 new MockSmartReplyInflater());
148         contentBinder.setInflateSynchronously(true);
149         mBindStage = new RowContentBindStage(contentBinder,
150                 mock(NotifInflationErrorManager.class),
151                 mock(RowContentBindStageLogger.class));
152 
153         CommonNotifCollection collection = mock(CommonNotifCollection.class);
154 
155         mBindPipeline = new NotifBindPipeline(
156                 collection,
157                 mock(NotifBindPipelineLogger.class),
158                 mTestLooper.getLooper());
159         mBindPipeline.setStage(mBindStage);
160 
161         ArgumentCaptor<NotifCollectionListener> collectionListenerCaptor =
162                 ArgumentCaptor.forClass(NotifCollectionListener.class);
163         verify(collection).addCollectionListener(collectionListenerCaptor.capture());
164         mBindPipelineEntryListener = collectionListenerCaptor.getValue();
165         mPeopleNotificationIdentifier = mock(PeopleNotificationIdentifier.class);
166     }
167 
168     /**
169      * Creates a generic row.
170      *
171      * @return a generic row with no special properties.
172      * @throws Exception
173      */
createRow()174     public ExpandableNotificationRow createRow() throws Exception {
175         return createRow(PKG, UID, USER_HANDLE);
176     }
177 
178     /**
179      * Create a row with the package and user id specified.
180      *
181      * @param pkg package
182      * @param uid user id
183      * @return a row with a notification using the package and user id
184      * @throws Exception
185      */
createRow(String pkg, int uid, UserHandle userHandle)186     public ExpandableNotificationRow createRow(String pkg, int uid, UserHandle userHandle)
187             throws Exception {
188         return createRow(pkg, uid, userHandle, false /* isGroupSummary */, null /* groupKey */);
189     }
190 
191     /**
192      * Creates a row based off the notification given.
193      *
194      * @param notification the notification
195      * @return a row built off the notification
196      * @throws Exception
197      */
createRow(Notification notification)198     public ExpandableNotificationRow createRow(Notification notification) throws Exception {
199         return generateRow(notification, PKG, UID, USER_HANDLE, 0 /* extraInflationFlags */);
200     }
201 
202     /**
203      * Create a row with the specified content views inflated in addition to the default.
204      *
205      * @param extraInflationFlags the flags corresponding to the additional content views that
206      *                            should be inflated
207      * @return a row with the specified content views inflated in addition to the default
208      * @throws Exception
209      */
createRow(@nflationFlag int extraInflationFlags)210     public ExpandableNotificationRow createRow(@InflationFlag int extraInflationFlags)
211             throws Exception {
212         return generateRow(createNotification(), PKG, UID, USER_HANDLE, extraInflationFlags);
213     }
214 
215     /**
216      * Returns an {@link ExpandableNotificationRow} group with the given number of child
217      * notifications.
218      */
createGroup(int numChildren)219     public ExpandableNotificationRow createGroup(int numChildren) throws Exception {
220         ExpandableNotificationRow row = createGroupSummary(GROUP_KEY);
221         for (int i = 0; i < numChildren; i++) {
222             ExpandableNotificationRow childRow = createGroupChild(GROUP_KEY);
223             row.addChildNotification(childRow);
224         }
225         return row;
226     }
227 
228     /** Returns a group notification with 2 child notifications. */
createGroup()229     public ExpandableNotificationRow createGroup() throws Exception {
230         return createGroup(2);
231     }
232 
createGroupSummary(String groupkey)233     private ExpandableNotificationRow createGroupSummary(String groupkey) throws Exception {
234         return createRow(PKG, UID, USER_HANDLE, true /* isGroupSummary */, groupkey);
235     }
236 
createGroupChild(String groupkey)237     private ExpandableNotificationRow createGroupChild(String groupkey) throws Exception {
238         return createRow(PKG, UID, USER_HANDLE, false /* isGroupSummary */, groupkey);
239     }
240 
241     /**
242      * Returns an {@link ExpandableNotificationRow} that should be shown as a bubble.
243      */
createBubble()244     public ExpandableNotificationRow createBubble()
245             throws Exception {
246         Notification n = createNotification(false /* isGroupSummary */,
247                 null /* groupKey */, makeBubbleMetadata(null));
248         n.flags |= FLAG_BUBBLE;
249         ExpandableNotificationRow row = generateRow(n, PKG, UID, USER_HANDLE,
250                 0 /* extraInflationFlags */, IMPORTANCE_HIGH);
251         modifyRanking(row.getEntry())
252                 .setCanBubble(true)
253                 .build();
254         return row;
255     }
256 
257     /**
258      * Returns an {@link ExpandableNotificationRow} that should be shown as a bubble.
259      */
createShortcutBubble(String shortcutId)260     public ExpandableNotificationRow createShortcutBubble(String shortcutId)
261             throws Exception {
262         Notification n = createNotification(false /* isGroupSummary */,
263                 null /* groupKey */, makeShortcutBubbleMetadata(shortcutId));
264         n.flags |= FLAG_BUBBLE;
265         ExpandableNotificationRow row = generateRow(n, PKG, UID, USER_HANDLE,
266                 0 /* extraInflationFlags */, IMPORTANCE_HIGH);
267         modifyRanking(row.getEntry())
268                 .setCanBubble(true)
269                 .build();
270         return row;
271     }
272 
273     /**
274      * Returns an {@link ExpandableNotificationRow} that should be shown as a bubble and is part
275      * of a group of notifications.
276      */
createBubbleInGroup()277     public ExpandableNotificationRow createBubbleInGroup()
278             throws Exception {
279         Notification n = createNotification(false /* isGroupSummary */,
280                 GROUP_KEY /* groupKey */, makeBubbleMetadata(null));
281         n.flags |= FLAG_BUBBLE;
282         ExpandableNotificationRow row = generateRow(n, PKG, UID, USER_HANDLE,
283                 0 /* extraInflationFlags */, IMPORTANCE_HIGH);
284         modifyRanking(row.getEntry())
285                 .setCanBubble(true)
286                 .build();
287         return row;
288     }
289 
290     /**
291      * Returns an {@link NotificationEntry} that should be shown as a bubble.
292      *
293      * @param deleteIntent the intent to assign to {@link BubbleMetadata#deleteIntent}
294      */
createBubble(@ullable PendingIntent deleteIntent)295     public NotificationEntry createBubble(@Nullable PendingIntent deleteIntent) {
296         return createBubble(makeBubbleMetadata(deleteIntent), USER_HANDLE);
297     }
298 
299     /**
300      * Returns an {@link NotificationEntry} that should be shown as a bubble.
301      *
302      * @param handle the user to associate with this bubble.
303      */
createBubble(UserHandle handle)304     public NotificationEntry createBubble(UserHandle handle) {
305         return createBubble(makeBubbleMetadata(null), handle);
306     }
307 
308     /**
309      * Returns an {@link NotificationEntry} that should be shown as a bubble.
310      *
311      * @param userHandle the user to associate with this notification.
312      */
createBubble(BubbleMetadata metadata, UserHandle userHandle)313     private NotificationEntry createBubble(BubbleMetadata metadata, UserHandle userHandle) {
314         Notification n = createNotification(false /* isGroupSummary */, null /* groupKey */,
315                 metadata);
316         n.flags |= FLAG_BUBBLE;
317 
318         final NotificationChannel channel =
319                 new NotificationChannel(
320                         n.getChannelId(),
321                         n.getChannelId(),
322                         IMPORTANCE_HIGH);
323         channel.setBlockable(true);
324 
325         NotificationEntry entry = new NotificationEntryBuilder()
326                 .setPkg(PKG)
327                 .setOpPkg(PKG)
328                 .setId(mId++)
329                 .setUid(UID)
330                 .setInitialPid(2000)
331                 .setNotification(n)
332                 .setUser(userHandle)
333                 .setPostTime(System.currentTimeMillis())
334                 .setChannel(channel)
335                 .build();
336 
337         modifyRanking(entry)
338                 .setCanBubble(true)
339                 .build();
340         return entry;
341     }
342 
343     /**
344      * Creates a notification row with the given details.
345      *
346      * @param pkg package used for creating a {@link StatusBarNotification}
347      * @param uid uid used for creating a {@link StatusBarNotification}
348      * @param isGroupSummary whether the notification row is a group summary
349      * @param groupKey the group key for the notification group used across notifications
350      * @return a row with that's either a standalone notification or a group notification if the
351      *         groupKey is non-null
352      * @throws Exception
353      */
createRow( String pkg, int uid, UserHandle userHandle, boolean isGroupSummary, @Nullable String groupKey)354     private ExpandableNotificationRow createRow(
355             String pkg,
356             int uid,
357             UserHandle userHandle,
358             boolean isGroupSummary,
359             @Nullable String groupKey)
360             throws Exception {
361         Notification notif = createNotification(isGroupSummary, groupKey);
362         return generateRow(notif, pkg, uid, userHandle, 0 /* inflationFlags */);
363     }
364 
365     /**
366      * Creates a generic notification.
367      *
368      * @return a notification with no special properties
369      */
createNotification()370     public Notification createNotification() {
371         return createNotification(false /* isGroupSummary */, null /* groupKey */);
372     }
373 
374     /**
375      * Creates a notification with the given parameters.
376      *
377      * @param isGroupSummary whether the notification is a group summary
378      * @param groupKey the group key for the notification group used across notifications
379      * @return a notification that is in the group specified or standalone if unspecified
380      */
createNotification(boolean isGroupSummary, @Nullable String groupKey)381     private Notification createNotification(boolean isGroupSummary, @Nullable String groupKey) {
382         return createNotification(isGroupSummary, groupKey, null /* bubble metadata */);
383     }
384 
385     /**
386      * Creates a notification with the given parameters.
387      *
388      * @param isGroupSummary whether the notification is a group summary
389      * @param groupKey the group key for the notification group used across notifications
390      * @param bubbleMetadata the bubble metadata to use for this notification if it exists.
391      * @return a notification that is in the group specified or standalone if unspecified
392      */
createNotification(boolean isGroupSummary, @Nullable String groupKey, @Nullable BubbleMetadata bubbleMetadata)393     public Notification createNotification(boolean isGroupSummary,
394             @Nullable String groupKey, @Nullable BubbleMetadata bubbleMetadata) {
395         Notification publicVersion = new Notification.Builder(mContext).setSmallIcon(
396                 R.drawable.ic_person)
397                 .setCustomContentView(new RemoteViews(mContext.getPackageName(),
398                         R.layout.custom_view_dark))
399                 .build();
400         Notification.Builder notificationBuilder = new Notification.Builder(mContext, "channelId")
401                 .setSmallIcon(R.drawable.ic_person)
402                 .setContentTitle("Title")
403                 .setContentText("Text")
404                 .setPublicVersion(publicVersion)
405                 .setStyle(new Notification.BigTextStyle().bigText("Big Text"));
406         if (isGroupSummary) {
407             notificationBuilder.setGroupSummary(true);
408         }
409         if (!TextUtils.isEmpty(groupKey)) {
410             notificationBuilder.setGroup(groupKey);
411         }
412         if (bubbleMetadata != null) {
413             notificationBuilder.setBubbleMetadata(bubbleMetadata);
414         }
415         return notificationBuilder.build();
416     }
417 
getStatusBarStateController()418     public StatusBarStateController getStatusBarStateController() {
419         return mStatusBarStateController;
420     }
421 
generateRow( Notification notification, String pkg, int uid, UserHandle userHandle, @InflationFlag int extraInflationFlags)422     private ExpandableNotificationRow generateRow(
423             Notification notification,
424             String pkg,
425             int uid,
426             UserHandle userHandle,
427             @InflationFlag int extraInflationFlags)
428             throws Exception {
429         return generateRow(notification, pkg, uid, userHandle, extraInflationFlags,
430                 IMPORTANCE_DEFAULT);
431     }
432 
generateRow( Notification notification, String pkg, int uid, UserHandle userHandle, @InflationFlag int extraInflationFlags, int importance)433     private ExpandableNotificationRow generateRow(
434             Notification notification,
435             String pkg,
436             int uid,
437             UserHandle userHandle,
438             @InflationFlag int extraInflationFlags,
439             int importance)
440             throws Exception {
441         LayoutInflater inflater = (LayoutInflater) mContext.getSystemService(
442                 mContext.LAYOUT_INFLATER_SERVICE);
443         mRow = (ExpandableNotificationRow) inflater.inflate(
444                 R.layout.status_bar_notification_row,
445                 null /* root */,
446                 false /* attachToRoot */);
447         ExpandableNotificationRow row = mRow;
448 
449         final NotificationChannel channel =
450                 new NotificationChannel(
451                         notification.getChannelId(),
452                         notification.getChannelId(),
453                         importance);
454         channel.setBlockable(true);
455 
456         NotificationEntry entry = new NotificationEntryBuilder()
457                 .setPkg(pkg)
458                 .setOpPkg(pkg)
459                 .setId(mId++)
460                 .setUid(uid)
461                 .setInitialPid(2000)
462                 .setNotification(notification)
463                 .setUser(userHandle)
464                 .setPostTime(System.currentTimeMillis())
465                 .setChannel(channel)
466                 .build();
467 
468         entry.setRow(row);
469         mIconManager.createIcons(entry);
470 
471         mBindPipelineEntryListener.onEntryInit(entry);
472         mBindPipeline.manageRow(entry, row);
473 
474         row.initialize(
475                 entry,
476                 APP_NAME,
477                 entry.getKey(),
478                 mock(ExpansionLogger.class),
479                 mock(KeyguardBypassController.class),
480                 mGroupMembershipManager,
481                 mGroupExpansionManager,
482                 mHeadsUpManager,
483                 mBindStage,
484                 mock(OnExpandClickListener.class),
485                 mock(NotificationMediaManager.class),
486                 mock(ExpandableNotificationRow.CoordinateOnClickListener.class),
487                 new FalsingManagerFake(),
488                 new FalsingCollectorFake(),
489                 mStatusBarStateController,
490                 mPeopleNotificationIdentifier,
491                 mock(OnUserInteractionCallback.class),
492                 Optional.of(mock(BubblesManager.class)),
493                 mock(NotificationGutsManager.class));
494 
495         row.setAboveShelfChangedListener(aboveShelf -> { });
496         mBindStage.getStageParams(entry).requireContentViews(extraInflationFlags);
497         inflateAndWait(entry);
498 
499         // This would be done as part of onAsyncInflationFinished, but we skip large amounts of
500         // the callback chain, so we need to make up for not adding it to the group manager
501         // here.
502         mGroupMembershipManager.onEntryAdded(entry);
503         return row;
504     }
505 
inflateAndWait(NotificationEntry entry)506     private void inflateAndWait(NotificationEntry entry) throws Exception {
507         CountDownLatch countDownLatch = new CountDownLatch(1);
508         mBindStage.requestRebind(entry, en -> countDownLatch.countDown());
509         mTestLooper.processAllMessages();
510         assertTrue(countDownLatch.await(500, TimeUnit.MILLISECONDS));
511     }
512 
makeBubbleMetadata(PendingIntent deleteIntent)513     private BubbleMetadata makeBubbleMetadata(PendingIntent deleteIntent) {
514         Intent target = new Intent(mContext, BubblesTestActivity.class);
515         PendingIntent bubbleIntent = PendingIntent.getActivity(mContext, 0, target,
516                 PendingIntent.FLAG_MUTABLE);
517 
518         return new BubbleMetadata.Builder(bubbleIntent,
519                         Icon.createWithResource(mContext, R.drawable.android))
520                 .setDeleteIntent(deleteIntent)
521                 .setDesiredHeight(314)
522                 .build();
523     }
524 
makeShortcutBubbleMetadata(String shortcutId)525     private BubbleMetadata makeShortcutBubbleMetadata(String shortcutId) {
526         return new BubbleMetadata.Builder(shortcutId)
527                 .setDesiredHeight(314)
528                 .build();
529     }
530 
531     private static class MockSmartReplyInflater implements SmartReplyStateInflater {
532         @Override
inflateSmartReplyState(NotificationEntry entry)533         public InflatedSmartReplyState inflateSmartReplyState(NotificationEntry entry) {
534             return mock(InflatedSmartReplyState.class);
535         }
536 
537         @Override
inflateSmartReplyViewHolder(Context sysuiContext, Context notifPackageContext, NotificationEntry entry, InflatedSmartReplyState existingSmartReplyState, InflatedSmartReplyState newSmartReplyState)538         public InflatedSmartReplyViewHolder inflateSmartReplyViewHolder(Context sysuiContext,
539                 Context notifPackageContext, NotificationEntry entry,
540                 InflatedSmartReplyState existingSmartReplyState,
541                 InflatedSmartReplyState newSmartReplyState) {
542             return mock(InflatedSmartReplyViewHolder.class);
543         }
544     }
545 }
546