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