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