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