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