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