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