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