1 /* 2 * Copyright (C) 2019 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.car.notification; 18 19 import static android.service.notification.NotificationListenerService.Ranking.USER_SENTIMENT_NEGATIVE; 20 import static android.service.notification.NotificationListenerService.Ranking.USER_SENTIMENT_NEUTRAL; 21 import static android.service.notification.NotificationListenerService.Ranking.USER_SENTIMENT_POSITIVE; 22 23 import static com.google.common.truth.Truth.assertThat; 24 25 import static org.mockito.Mockito.mock; 26 import static org.mockito.Mockito.never; 27 import static org.mockito.Mockito.times; 28 import static org.mockito.Mockito.verify; 29 import static org.mockito.Mockito.when; 30 31 import android.app.Notification; 32 import android.app.NotificationChannel; 33 import android.app.PendingIntent; 34 import android.car.drivingstate.CarUxRestrictions; 35 import android.content.Context; 36 import android.content.Intent; 37 import android.content.pm.ApplicationInfo; 38 import android.content.pm.PackageInfo; 39 import android.os.Bundle; 40 import android.os.UserHandle; 41 import android.service.notification.NotificationListenerService; 42 import android.service.notification.SnoozeCriterion; 43 import android.service.notification.StatusBarNotification; 44 import android.telephony.TelephonyManager; 45 46 import com.android.car.notification.testutils.ShadowApplicationPackageManager; 47 48 import org.junit.After; 49 import org.junit.Before; 50 import org.junit.Test; 51 import org.junit.runner.RunWith; 52 import org.mockito.InOrder; 53 import org.mockito.Mock; 54 import org.mockito.Mockito; 55 import org.mockito.MockitoAnnotations; 56 import org.robolectric.RobolectricTestRunner; 57 import org.robolectric.RuntimeEnvironment; 58 import org.robolectric.annotation.Config; 59 60 import java.util.ArrayList; 61 import java.util.Arrays; 62 import java.util.HashMap; 63 import java.util.List; 64 import java.util.Map; 65 import java.util.stream.Collectors; 66 67 @RunWith(RobolectricTestRunner.class) 68 @Config(shadows = {ShadowApplicationPackageManager.class}) 69 public class PreprocessingManagerTest { 70 71 private Context mContext; 72 73 private static final String PKG = "com.package.PREPROCESSING_MANAGER_TEST"; 74 private static final String OP_PKG = "OpPackage"; 75 private static final int ID = 1; 76 private static final String TAG = "Tag"; 77 private static final int UID = 2; 78 private static final int INITIAL_PID = 3; 79 private static final String CHANNEL_ID = "CHANNEL_ID"; 80 private static final String CONTENT_TITLE = "CONTENT_TITLE"; 81 private static final String OVERRIDE_GROUP_KEY = "OVERRIDE_GROUP_KEY"; 82 private static final long POST_TIME = 12345l; 83 private static final UserHandle USER_HANDLE = new UserHandle(12); 84 private static final String GROUP_KEY_A = "GROUP_KEY_A"; 85 private static final String GROUP_KEY_B = "GROUP_KEY_B"; 86 private static final String GROUP_KEY_C = "GROUP_KEY_C"; 87 private static final int MAX_STRING_LENGTH = 10; 88 89 private PreprocessingManager mPreprocessingManager; 90 @Mock 91 private ApplicationInfo mApplicationInfo; 92 @Mock 93 private StatusBarNotification mStatusBarNotification1; 94 @Mock 95 private StatusBarNotification mStatusBarNotification2; 96 @Mock 97 private StatusBarNotification mStatusBarNotification3; 98 @Mock 99 private StatusBarNotification mStatusBarNotification4; 100 @Mock 101 private StatusBarNotification mStatusBarNotification5; 102 @Mock 103 private StatusBarNotification mStatusBarNotification6; 104 @Mock 105 private StatusBarNotification mAdditionalStatusBarNotification; 106 @Mock 107 private StatusBarNotification mSummaryStatusBarNotification; 108 @Mock 109 private CarUxRestrictions mCarUxRestrictions; 110 @Mock 111 private CarUxRestrictionManagerWrapper mCarUxRestrictionManagerWrapper; 112 @Mock 113 private PreprocessingManager.CallStateListener mCallStateListener1; 114 @Mock 115 private PreprocessingManager.CallStateListener mCallStateListener2; 116 @Mock 117 private Notification mMediaNotification; 118 @Mock 119 private Notification mSummaryNotification; 120 private Notification mForegroundNotification; 121 private Notification mBackgroundNotification; 122 private Notification mNavigationNotification; 123 124 // Following AlertEntry var names describe the type of notifications they wrap. 125 private AlertEntry mLessImportantBackground; 126 private AlertEntry mLessImportantForeground; 127 private AlertEntry mMedia; 128 private AlertEntry mNavigation; 129 private AlertEntry mImportantBackground; 130 private AlertEntry mImportantForeground; 131 132 private List<AlertEntry> mAlertEntries; 133 private Map<String, AlertEntry> mAlertEntriesMap; 134 private NotificationListenerService.RankingMap mRankingMap; 135 136 @Before setup()137 public void setup() { 138 MockitoAnnotations.initMocks(this); 139 mContext = RuntimeEnvironment.application; 140 mPreprocessingManager = PreprocessingManager.getInstance(mContext); 141 142 mForegroundNotification = generateNotification( 143 /* isForeground= */true, /* isNavigation= */ false); 144 mBackgroundNotification = generateNotification( 145 /* isForeground= */false, /* isNavigation= */ false); 146 mNavigationNotification = generateNotification( 147 /* isForeground= */true, /* isNavigation= */ true); 148 149 150 when(mMediaNotification.isMediaNotification()).thenReturn(true); 151 152 // Key describes the notification that the StatusBarNotification contains. 153 when(mStatusBarNotification1.getKey()).thenReturn("KEY_LESS_IMPORTANT_BACKGROUND"); 154 when(mStatusBarNotification2.getKey()).thenReturn("KEY_LESS_IMPORTANT_FOREGROUND"); 155 when(mStatusBarNotification3.getKey()).thenReturn("KEY_MEDIA"); 156 when(mStatusBarNotification4.getKey()).thenReturn("KEY_NAVIGATION"); 157 when(mStatusBarNotification5.getKey()).thenReturn("KEY_IMPORTANT_BACKGROUND"); 158 when(mStatusBarNotification6.getKey()).thenReturn("KEY_IMPORTANT_FOREGROUND"); 159 when(mSummaryStatusBarNotification.getKey()).thenReturn("KEY_SUMMARY"); 160 161 when(mStatusBarNotification1.getGroupKey()).thenReturn(GROUP_KEY_A); 162 when(mStatusBarNotification2.getGroupKey()).thenReturn(GROUP_KEY_B); 163 when(mStatusBarNotification3.getGroupKey()).thenReturn(GROUP_KEY_A); 164 when(mStatusBarNotification4.getGroupKey()).thenReturn(GROUP_KEY_B); 165 when(mStatusBarNotification5.getGroupKey()).thenReturn(GROUP_KEY_B); 166 when(mStatusBarNotification6.getGroupKey()).thenReturn(GROUP_KEY_C); 167 when(mSummaryStatusBarNotification.getGroupKey()).thenReturn(GROUP_KEY_C); 168 169 when(mStatusBarNotification1.getNotification()).thenReturn(mBackgroundNotification); 170 when(mStatusBarNotification2.getNotification()).thenReturn(mForegroundNotification); 171 when(mStatusBarNotification3.getNotification()).thenReturn(mMediaNotification); 172 when(mStatusBarNotification4.getNotification()).thenReturn(mNavigationNotification); 173 when(mStatusBarNotification5.getNotification()).thenReturn(mBackgroundNotification); 174 when(mStatusBarNotification6.getNotification()).thenReturn(mForegroundNotification); 175 when(mSummaryStatusBarNotification.getNotification()).thenReturn(mSummaryNotification); 176 177 when(mSummaryNotification.isGroupSummary()).thenReturn(true); 178 179 // prevents less important foreground notifications from not being filtered due to the 180 // application and package setup. 181 when(mApplicationInfo.isSignedWithPlatformKey()).thenReturn(true); 182 when(mApplicationInfo.isSystemApp()).thenReturn(true); 183 when(mApplicationInfo.isPrivilegedApp()).thenReturn(true); 184 PackageInfo packageInfo = new PackageInfo(); 185 packageInfo.applicationInfo = mApplicationInfo; 186 ShadowApplicationPackageManager.setPackageInfo(packageInfo); 187 188 // Always start system with no phone calls in progress. 189 Intent intent = new Intent(TelephonyManager.ACTION_PHONE_STATE_CHANGED); 190 intent.putExtra(TelephonyManager.EXTRA_STATE, TelephonyManager.EXTRA_STATE_IDLE); 191 mPreprocessingManager.mIntentReceiver.onReceive(mContext, intent); 192 193 initTestData(); 194 } 195 196 @After tearDown()197 public void tearDown() { 198 ShadowApplicationPackageManager.reset(); 199 } 200 201 @Test onFilter_showLessImportantNotifications_doesNotFilterNotifications()202 public void onFilter_showLessImportantNotifications_doesNotFilterNotifications() { 203 List<AlertEntry> unfiltered = mAlertEntries.stream().collect(Collectors.toList()); 204 mPreprocessingManager 205 .filter(/* showLessImportantNotifications= */true, mAlertEntries, mRankingMap); 206 207 assertThat(mAlertEntries.equals(unfiltered)).isTrue(); 208 } 209 210 @Test onFilter_dontShowLessImportantNotifications_filtersLessImportantForeground()211 public void onFilter_dontShowLessImportantNotifications_filtersLessImportantForeground() { 212 mPreprocessingManager 213 .filter( /* showLessImportantNotifications= */ false, mAlertEntries, mRankingMap); 214 215 assertThat(mAlertEntries.contains(mLessImportantBackground)).isTrue(); 216 assertThat(mAlertEntries.contains(mLessImportantForeground)).isFalse(); 217 } 218 219 @Test onFilter_dontShowLessImportantNotifications_doesNotFilterMoreImportant()220 public void onFilter_dontShowLessImportantNotifications_doesNotFilterMoreImportant() { 221 mPreprocessingManager 222 .filter(/* showLessImportantNotifications= */false, mAlertEntries, mRankingMap); 223 224 assertThat(mAlertEntries.contains(mImportantBackground)).isTrue(); 225 assertThat(mAlertEntries.contains(mImportantForeground)).isTrue(); 226 } 227 228 @Test onFilter_dontShowLessImportantNotifications_filtersMediaAndNavigation()229 public void onFilter_dontShowLessImportantNotifications_filtersMediaAndNavigation() { 230 mPreprocessingManager 231 .filter(/* showLessImportantNotifications= */false, mAlertEntries, mRankingMap); 232 233 assertThat(mAlertEntries.contains(mMedia)).isFalse(); 234 assertThat(mAlertEntries.contains(mNavigation)).isFalse(); 235 } 236 237 @Test onFilter_doShowLessImportantNotifications_doesNotFilterMediaOrNavigation()238 public void onFilter_doShowLessImportantNotifications_doesNotFilterMediaOrNavigation() { 239 mPreprocessingManager 240 .filter(/* showLessImportantNotifications= */true, mAlertEntries, mRankingMap); 241 242 assertThat(mAlertEntries.contains(mMedia)).isTrue(); 243 assertThat(mAlertEntries.contains(mNavigation)).isTrue(); 244 } 245 246 @Test onFilter_doShowLessImportantNotifications_filtersCalls()247 public void onFilter_doShowLessImportantNotifications_filtersCalls() { 248 StatusBarNotification callSBN = mock(StatusBarNotification.class); 249 Notification callNotification = new Notification(); 250 callNotification.category = Notification.CATEGORY_CALL; 251 when(callSBN.getNotification()).thenReturn(callNotification); 252 List<AlertEntry> entries = new ArrayList<>(); 253 entries.add(new AlertEntry(callSBN)); 254 255 mPreprocessingManager.filter(true, entries, mRankingMap); 256 assertThat(entries).isEmpty(); 257 } 258 259 @Test onFilter_dontShowLessImportantNotifications_filtersCalls()260 public void onFilter_dontShowLessImportantNotifications_filtersCalls() { 261 StatusBarNotification callSBN = mock(StatusBarNotification.class); 262 Notification callNotification = new Notification(); 263 callNotification.category = Notification.CATEGORY_CALL; 264 when(callSBN.getNotification()).thenReturn(callNotification); 265 List<AlertEntry> entries = new ArrayList<>(); 266 entries.add(new AlertEntry(callSBN)); 267 268 mPreprocessingManager.filter(false, entries, mRankingMap); 269 assertThat(entries).isEmpty(); 270 } 271 272 @Test onOptimizeForDriving_alertEntryHasNonMessageNotification_trimsNotificationTexts()273 public void onOptimizeForDriving_alertEntryHasNonMessageNotification_trimsNotificationTexts() { 274 when(mCarUxRestrictions.getMaxRestrictedStringLength()).thenReturn(MAX_STRING_LENGTH); 275 when(mCarUxRestrictionManagerWrapper.getCurrentCarUxRestrictions()) 276 .thenReturn(mCarUxRestrictions); 277 mPreprocessingManager.setCarUxRestrictionManagerWrapper(mCarUxRestrictionManagerWrapper); 278 279 Notification nonMessageNotification 280 = generateNotification(/* isForeground= */ true, /* isNavigation= */ true); 281 nonMessageNotification.extras 282 .putString(Notification.EXTRA_TITLE, generateStringOfLength(100)); 283 nonMessageNotification.extras 284 .putString(Notification.EXTRA_TEXT, generateStringOfLength(100)); 285 nonMessageNotification.extras 286 .putString(Notification.EXTRA_TITLE_BIG, generateStringOfLength(100)); 287 nonMessageNotification.extras 288 .putString(Notification.EXTRA_SUMMARY_TEXT, generateStringOfLength(100)); 289 290 when(mNavigation.getNotification()).thenReturn(nonMessageNotification); 291 292 AlertEntry optimized = mPreprocessingManager.optimizeForDriving(mNavigation); 293 Bundle trimmed = optimized.getNotification().extras; 294 295 for (String key : trimmed.keySet()) { 296 switch (key) { 297 case Notification.EXTRA_TITLE: 298 case Notification.EXTRA_TEXT: 299 case Notification.EXTRA_TITLE_BIG: 300 case Notification.EXTRA_SUMMARY_TEXT: 301 CharSequence text = trimmed.getCharSequence(key); 302 assertThat(text.length() <= MAX_STRING_LENGTH).isTrue(); 303 default: 304 continue; 305 } 306 } 307 } 308 309 @Test onOptimizeForDriving_alertEntryHasMessageNotification_doesNotTrimMessageTexts()310 public void onOptimizeForDriving_alertEntryHasMessageNotification_doesNotTrimMessageTexts() { 311 when(mCarUxRestrictions.getMaxRestrictedStringLength()).thenReturn(MAX_STRING_LENGTH); 312 when(mCarUxRestrictionManagerWrapper.getCurrentCarUxRestrictions()) 313 .thenReturn(mCarUxRestrictions); 314 mPreprocessingManager.setCarUxRestrictionManagerWrapper(mCarUxRestrictionManagerWrapper); 315 316 Notification messageNotification 317 = generateNotification(/* isForeground= */ true, /* isNavigation= */ true); 318 messageNotification.extras 319 .putString(Notification.EXTRA_TITLE, generateStringOfLength(100)); 320 messageNotification.extras 321 .putString(Notification.EXTRA_TEXT, generateStringOfLength(100)); 322 messageNotification.extras 323 .putString(Notification.EXTRA_TITLE_BIG, generateStringOfLength(100)); 324 messageNotification.extras 325 .putString(Notification.EXTRA_SUMMARY_TEXT, generateStringOfLength(100)); 326 messageNotification.category = Notification.CATEGORY_MESSAGE; 327 328 when(mImportantForeground.getNotification()).thenReturn(messageNotification); 329 330 AlertEntry optimized = mPreprocessingManager.optimizeForDriving(mImportantForeground); 331 Bundle trimmed = optimized.getNotification().extras; 332 333 for (String key : trimmed.keySet()) { 334 switch (key) { 335 case Notification.EXTRA_TITLE: 336 case Notification.EXTRA_TEXT: 337 case Notification.EXTRA_TITLE_BIG: 338 case Notification.EXTRA_SUMMARY_TEXT: 339 CharSequence text = trimmed.getCharSequence(key); 340 assertThat(text.length() <= MAX_STRING_LENGTH).isFalse(); 341 default: 342 continue; 343 } 344 } 345 } 346 347 @Test onGroup_groupsNotificationsByGroupKey()348 public void onGroup_groupsNotificationsByGroupKey() { 349 List<NotificationGroup> groupResult = mPreprocessingManager.group(mAlertEntries); 350 String[] actualGroupKeys = new String[groupResult.size()]; 351 String[] expectedGroupKeys = {GROUP_KEY_A, GROUP_KEY_B, GROUP_KEY_C}; 352 353 for (int i = 0; i < groupResult.size(); i++) { 354 actualGroupKeys[i] = groupResult.get(i).getGroupKey(); 355 } 356 357 Arrays.sort(actualGroupKeys); 358 Arrays.sort(expectedGroupKeys); 359 360 assertThat(actualGroupKeys).isEqualTo(expectedGroupKeys); 361 } 362 363 @Test onGroup_autoGeneratedGroupWithNoGroupChildren_doesNotShowGroupSummary()364 public void onGroup_autoGeneratedGroupWithNoGroupChildren_doesNotShowGroupSummary() { 365 List<AlertEntry> list = new ArrayList<>(); 366 list.add(getEmptyAutoGeneratedGroupSummary()); 367 List<NotificationGroup> groupResult = mPreprocessingManager.group(list); 368 369 assertThat(groupResult.size() == 0).isTrue(); 370 } 371 372 @Test addCallStateListener_preCall_triggerChanges()373 public void addCallStateListener_preCall_triggerChanges() { 374 InOrder listenerInOrder = Mockito.inOrder(mCallStateListener1); 375 mPreprocessingManager.addCallStateListener(mCallStateListener1); 376 listenerInOrder.verify(mCallStateListener1).onCallStateChanged(false); 377 378 Intent intent = new Intent(TelephonyManager.ACTION_PHONE_STATE_CHANGED); 379 intent.putExtra(TelephonyManager.EXTRA_STATE, TelephonyManager.EXTRA_STATE_OFFHOOK); 380 mPreprocessingManager.mIntentReceiver.onReceive(mContext, intent); 381 382 listenerInOrder.verify(mCallStateListener1).onCallStateChanged(true); 383 } 384 385 @Test addCallStateListener_midCall_triggerChanges()386 public void addCallStateListener_midCall_triggerChanges() { 387 Intent intent = new Intent(TelephonyManager.ACTION_PHONE_STATE_CHANGED); 388 intent.putExtra(TelephonyManager.EXTRA_STATE, TelephonyManager.EXTRA_STATE_OFFHOOK); 389 mPreprocessingManager.mIntentReceiver.onReceive(mContext, intent); 390 391 mPreprocessingManager.addCallStateListener(mCallStateListener1); 392 393 verify(mCallStateListener1).onCallStateChanged(true); 394 } 395 396 @Test addCallStateListener_postCall_triggerChanges()397 public void addCallStateListener_postCall_triggerChanges() { 398 Intent intent = new Intent(TelephonyManager.ACTION_PHONE_STATE_CHANGED); 399 intent.putExtra(TelephonyManager.EXTRA_STATE, TelephonyManager.EXTRA_STATE_OFFHOOK); 400 mPreprocessingManager.mIntentReceiver.onReceive(mContext, intent); 401 402 intent = new Intent(TelephonyManager.ACTION_PHONE_STATE_CHANGED); 403 intent.putExtra(TelephonyManager.EXTRA_STATE, TelephonyManager.EXTRA_STATE_IDLE); 404 mPreprocessingManager.mIntentReceiver.onReceive(mContext, intent); 405 406 mPreprocessingManager.addCallStateListener(mCallStateListener1); 407 408 verify(mCallStateListener1).onCallStateChanged(false); 409 } 410 411 @Test addSameCallListenerTwice_dedupedCorrectly()412 public void addSameCallListenerTwice_dedupedCorrectly() { 413 mPreprocessingManager.addCallStateListener(mCallStateListener1); 414 415 verify(mCallStateListener1).onCallStateChanged(false); 416 mPreprocessingManager.addCallStateListener(mCallStateListener1); 417 418 verify(mCallStateListener1, times(1)).onCallStateChanged(false); 419 } 420 421 @Test removeCallStateListener_midCall_triggerChanges()422 public void removeCallStateListener_midCall_triggerChanges() { 423 Intent intent = new Intent(TelephonyManager.ACTION_PHONE_STATE_CHANGED); 424 intent.putExtra(TelephonyManager.EXTRA_STATE, TelephonyManager.EXTRA_STATE_OFFHOOK); 425 mPreprocessingManager.mIntentReceiver.onReceive(mContext, intent); 426 427 mPreprocessingManager.addCallStateListener(mCallStateListener1); 428 // Should get triggered with true before calling removeCallStateListener 429 mPreprocessingManager.removeCallStateListener(mCallStateListener1); 430 431 intent = new Intent(TelephonyManager.ACTION_PHONE_STATE_CHANGED); 432 intent.putExtra(TelephonyManager.EXTRA_STATE, TelephonyManager.EXTRA_STATE_IDLE); 433 mPreprocessingManager.mIntentReceiver.onReceive(mContext, intent); 434 435 verify(mCallStateListener1, never()).onCallStateChanged(false); 436 } 437 438 @Test multipleCallStateListeners_triggeredAppropriately()439 public void multipleCallStateListeners_triggeredAppropriately() { 440 InOrder listenerInOrder1 = Mockito.inOrder(mCallStateListener1); 441 InOrder listenerInOrder2 = Mockito.inOrder(mCallStateListener2); 442 mPreprocessingManager.addCallStateListener(mCallStateListener1); 443 listenerInOrder1.verify(mCallStateListener1).onCallStateChanged(false); 444 445 Intent intent = new Intent(TelephonyManager.ACTION_PHONE_STATE_CHANGED); 446 intent.putExtra(TelephonyManager.EXTRA_STATE, TelephonyManager.EXTRA_STATE_OFFHOOK); 447 mPreprocessingManager.mIntentReceiver.onReceive(mContext, intent); 448 449 mPreprocessingManager.addCallStateListener(mCallStateListener2); 450 mPreprocessingManager.removeCallStateListener(mCallStateListener1); 451 452 listenerInOrder1.verify(mCallStateListener1).onCallStateChanged(true); 453 listenerInOrder2.verify(mCallStateListener2).onCallStateChanged(true); 454 455 intent = new Intent(TelephonyManager.ACTION_PHONE_STATE_CHANGED); 456 intent.putExtra(TelephonyManager.EXTRA_STATE, TelephonyManager.EXTRA_STATE_IDLE); 457 mPreprocessingManager.mIntentReceiver.onReceive(mContext, intent); 458 459 // only listener 2 should be triggered w/ false 460 listenerInOrder1.verifyNoMoreInteractions(); 461 listenerInOrder2.verify(mCallStateListener2).onCallStateChanged(false); 462 } 463 464 @Test onGroup_removesNotificationGroupWithOnlySummaryNotification()465 public void onGroup_removesNotificationGroupWithOnlySummaryNotification() { 466 List<AlertEntry> list = new ArrayList<>(); 467 list.add(new AlertEntry(mSummaryStatusBarNotification)); 468 List<NotificationGroup> groupResult = mPreprocessingManager.group(list); 469 470 assertThat(groupResult.isEmpty()).isTrue(); 471 } 472 473 @Test onGroup_childNotificationHasTimeStamp_groupHasMostRecentTimeStamp()474 public void onGroup_childNotificationHasTimeStamp_groupHasMostRecentTimeStamp() { 475 mBackgroundNotification.when = 0; 476 mForegroundNotification.when = 1; 477 mNavigationNotification.when = 2; 478 479 mBackgroundNotification.extras.putBoolean(Notification.EXTRA_SHOW_WHEN, true); 480 mForegroundNotification.extras.putBoolean(Notification.EXTRA_SHOW_WHEN, true); 481 mNavigationNotification.extras.putBoolean(Notification.EXTRA_SHOW_WHEN, true); 482 483 List<NotificationGroup> groupResult = mPreprocessingManager.group(mAlertEntries); 484 485 groupResult.forEach(group -> { 486 AlertEntry groupSummaryNotification = group.getGroupSummaryNotification(); 487 if (groupSummaryNotification != null 488 && groupSummaryNotification.getNotification() != null) { 489 assertThat(groupSummaryNotification.getNotification() 490 .extras.getBoolean(Notification.EXTRA_SHOW_WHEN)).isTrue(); 491 } 492 }); 493 } 494 495 @Test onRank_ranksNotificationGroups()496 public void onRank_ranksNotificationGroups() { 497 List<NotificationGroup> groupResult = mPreprocessingManager.group(mAlertEntries); 498 List<NotificationGroup> rankResult = mPreprocessingManager.rank(groupResult, mRankingMap); 499 500 // generateRankingMap ranked the notifications in the reverse order. 501 String[] expectedOrder = { 502 GROUP_KEY_C, 503 GROUP_KEY_B, 504 GROUP_KEY_A 505 }; 506 507 for (int i = 0; i < rankResult.size(); i++) { 508 String actualGroupKey = rankResult.get(i).getGroupKey(); 509 String expectedGroupKey = expectedOrder[i]; 510 511 assertThat(actualGroupKey).isEqualTo(expectedGroupKey); 512 } 513 } 514 515 @Test onRank_ranksNotificationsInEachGroup()516 public void onRank_ranksNotificationsInEachGroup() { 517 List<NotificationGroup> groupResult = mPreprocessingManager.group(mAlertEntries); 518 List<NotificationGroup> rankResult = mPreprocessingManager.rank(groupResult, mRankingMap); 519 NotificationGroup groupB = rankResult.get(1); 520 521 // first make sure that we have Group B 522 assertThat(groupB.getGroupKey()).isEqualTo(GROUP_KEY_B); 523 524 // generateRankingMap ranked the non-background notifications in the reverse order 525 String[] expectedOrder = { 526 "KEY_NAVIGATION", 527 "KEY_LESS_IMPORTANT_FOREGROUND" 528 }; 529 530 for (int i = 0; i < groupB.getChildNotifications().size(); i++) { 531 String actualKey = groupB.getChildNotifications().get(i).getKey(); 532 String expectedGroupKey = expectedOrder[i]; 533 534 assertThat(actualKey).isEqualTo(expectedGroupKey); 535 } 536 } 537 538 @Test onAdditionalGroup_returnsTheSameGroupsAsStandardGroup()539 public void onAdditionalGroup_returnsTheSameGroupsAsStandardGroup() { 540 Notification additionalNotification = 541 generateNotification( /* isForegrond= */ true, /* isNavigation= */ false); 542 additionalNotification.category = Notification.CATEGORY_MESSAGE; 543 when(mAdditionalStatusBarNotification.getKey()).thenReturn("ADDITIONAL"); 544 when(mAdditionalStatusBarNotification.getGroupKey()).thenReturn(GROUP_KEY_C); 545 when(mAdditionalStatusBarNotification.getNotification()).thenReturn(additionalNotification); 546 AlertEntry additionalAlertEntry = new AlertEntry(mAdditionalStatusBarNotification); 547 548 mPreprocessingManager.init(mAlertEntriesMap, mRankingMap); 549 List<AlertEntry> copy = mPreprocessingManager.filter(/* showLessImportantNotifications= */ 550 false, new ArrayList<>(mAlertEntries), mRankingMap); 551 copy.add(additionalAlertEntry); 552 List<NotificationGroup> expected = mPreprocessingManager.group(copy); 553 String[] expectedKeys = new String[expected.size()]; 554 for (int i = 0; i < expectedKeys.length; i++) { 555 expectedKeys[i] = expected.get(i).getGroupKey(); 556 } 557 558 List<NotificationGroup> actual = 559 mPreprocessingManager.additionalGroup(additionalAlertEntry); 560 String[] actualKeys = new String[actual.size()]; 561 for (int i = 0; i < actualKeys.length; i++) { 562 actualKeys[i] = actual.get(i).getGroupKey(); 563 } 564 // We do not care about the order since they are not ranked yet. 565 Arrays.sort(actualKeys); 566 Arrays.sort(expectedKeys); 567 assertThat(actualKeys).isEqualTo(expectedKeys); 568 } 569 570 @Test onAdditionalRank_returnsTheSameOrderAsStandardRank()571 public void onAdditionalRank_returnsTheSameOrderAsStandardRank() { 572 List<AlertEntry> testCopy = new ArrayList<>(mAlertEntries); 573 574 List<NotificationGroup> additionalRanked = mPreprocessingManager.additionalRank( 575 mPreprocessingManager.group(mAlertEntries), mRankingMap); 576 List<NotificationGroup> standardRanked = mPreprocessingManager.rank( 577 mPreprocessingManager.group(testCopy), mRankingMap); 578 579 assertThat(additionalRanked.size()).isEqualTo(standardRanked.size()); 580 581 for (int i = 0; i < additionalRanked.size(); i++) { 582 assertThat(additionalRanked.get(i).getGroupKey()).isEqualTo( 583 standardRanked.get(i).getGroupKey()); 584 } 585 } 586 587 @Test onUpdateNotifications_notificationRemoved_removesNotification()588 public void onUpdateNotifications_notificationRemoved_removesNotification() { 589 mPreprocessingManager.init(mAlertEntriesMap, mRankingMap); 590 591 List<NotificationGroup> newList = 592 mPreprocessingManager.updateNotifications( 593 /* showLessImportantNotifications= */ false, 594 mImportantForeground, 595 CarNotificationListener.NOTIFY_NOTIFICATION_REMOVED, 596 mRankingMap); 597 598 assertThat(mPreprocessingManager.getOldNotifications().containsKey( 599 mImportantForeground.getKey())).isFalse(); 600 } 601 602 @Test onUpdateNotification_notificationPosted_isUpdate_putsNotification()603 public void onUpdateNotification_notificationPosted_isUpdate_putsNotification() { 604 mPreprocessingManager.init(mAlertEntriesMap, mRankingMap); 605 int beforeSize = mPreprocessingManager.getOldNotifications().size(); 606 Notification newNotification = new Notification.Builder(mContext, CHANNEL_ID) 607 .setContentTitle("NEW_TITLE") 608 .setGroup(OVERRIDE_GROUP_KEY) 609 .setGroupSummary(false) 610 .build(); 611 newNotification.category = Notification.CATEGORY_NAVIGATION; 612 when(mImportantForeground.getStatusBarNotification().getNotification()) 613 .thenReturn(newNotification); 614 List<NotificationGroup> newList = 615 mPreprocessingManager.updateNotifications( 616 /* showLessImportantNotifications= */ false, 617 mImportantForeground, 618 CarNotificationListener.NOTIFY_NOTIFICATION_POSTED, 619 mRankingMap); 620 621 int afterSize = mPreprocessingManager.getOldNotifications().size(); 622 AlertEntry updated = (AlertEntry) mPreprocessingManager.getOldNotifications().get( 623 mImportantForeground.getKey()); 624 assertThat(updated).isNotNull(); 625 assertThat(updated.getNotification().category).isEqualTo(Notification.CATEGORY_NAVIGATION); 626 assertThat(afterSize).isEqualTo(beforeSize); 627 } 628 629 @Test onUpdateNotification_notificationPosted_isNotUpdate_addsNotification()630 public void onUpdateNotification_notificationPosted_isNotUpdate_addsNotification() { 631 mPreprocessingManager.init(mAlertEntriesMap, mRankingMap); 632 int beforeSize = mPreprocessingManager.getOldNotifications().size(); 633 Notification additionalNotification = 634 generateNotification( /* isForegrond= */ true, /* isNavigation= */ false); 635 additionalNotification.category = Notification.CATEGORY_MESSAGE; 636 when(mAdditionalStatusBarNotification.getKey()).thenReturn("ADDITIONAL"); 637 when(mAdditionalStatusBarNotification.getGroupKey()).thenReturn(GROUP_KEY_C); 638 when(mAdditionalStatusBarNotification.getNotification()).thenReturn(additionalNotification); 639 AlertEntry additionalAlertEntry = new AlertEntry(mAdditionalStatusBarNotification); 640 641 List<NotificationGroup> newList = 642 mPreprocessingManager.updateNotifications( 643 /* showLessImportantNotifications= */ false, 644 additionalAlertEntry, 645 CarNotificationListener.NOTIFY_NOTIFICATION_POSTED, 646 mRankingMap); 647 648 int afterSize = mPreprocessingManager.getOldNotifications().size(); 649 AlertEntry posted = (AlertEntry) mPreprocessingManager.getOldNotifications().get( 650 additionalAlertEntry.getKey()); 651 assertThat(posted).isNotNull(); 652 assertThat(posted.getKey()).isEqualTo("ADDITIONAL"); 653 assertThat(afterSize).isEqualTo(beforeSize + 1); 654 } 655 656 /** 657 * Wraps StatusBarNotifications with AlertEntries and generates AlertEntriesMap and 658 * RankingsMap. 659 */ initTestData()660 private void initTestData() { 661 mAlertEntries = new ArrayList<>(); 662 mLessImportantBackground = new AlertEntry(mStatusBarNotification1); 663 mLessImportantForeground = new AlertEntry(mStatusBarNotification2); 664 mMedia = new AlertEntry(mStatusBarNotification3); 665 mNavigation = new AlertEntry(mStatusBarNotification4); 666 mImportantBackground = new AlertEntry(mStatusBarNotification5); 667 mImportantForeground = new AlertEntry(mStatusBarNotification6); 668 mAlertEntries.add(mLessImportantBackground); 669 mAlertEntries.add(mLessImportantForeground); 670 mAlertEntries.add(mMedia); 671 mAlertEntries.add(mNavigation); 672 mAlertEntries.add(mImportantBackground); 673 mAlertEntries.add(mImportantForeground); 674 mAlertEntriesMap = new HashMap<>(); 675 mAlertEntriesMap.put(mLessImportantBackground.getKey(), mLessImportantBackground); 676 mAlertEntriesMap.put(mLessImportantForeground.getKey(), mLessImportantForeground); 677 mAlertEntriesMap.put(mMedia.getKey(), mMedia); 678 mAlertEntriesMap.put(mNavigation.getKey(), mNavigation); 679 mAlertEntriesMap.put(mImportantBackground.getKey(), mImportantBackground); 680 mAlertEntriesMap.put(mImportantForeground.getKey(), mImportantForeground); 681 mRankingMap = generateRankingMap(mAlertEntries); 682 } 683 getEmptyAutoGeneratedGroupSummary()684 private AlertEntry getEmptyAutoGeneratedGroupSummary() { 685 Notification notification = new Notification.Builder(mContext, CHANNEL_ID) 686 .setContentTitle(CONTENT_TITLE) 687 .setSmallIcon(android.R.drawable.sym_def_app_icon) 688 .setGroup(OVERRIDE_GROUP_KEY) 689 .setGroupSummary(true) 690 .build(); 691 StatusBarNotification statusBarNotification = new StatusBarNotification( 692 PKG, OP_PKG, ID, TAG, UID, INITIAL_PID, notification, USER_HANDLE, 693 OVERRIDE_GROUP_KEY, POST_TIME); 694 statusBarNotification.setOverrideGroupKey(OVERRIDE_GROUP_KEY); 695 696 return new AlertEntry(statusBarNotification); 697 } 698 generateNotification(boolean isForeground, boolean isNavigation)699 private Notification generateNotification(boolean isForeground, boolean isNavigation) { 700 Notification notification = new Notification.Builder(mContext, CHANNEL_ID) 701 .setContentTitle(CONTENT_TITLE) 702 .setSmallIcon(android.R.drawable.sym_def_app_icon) 703 .setGroup(OVERRIDE_GROUP_KEY) 704 .setGroupSummary(true) 705 .build(); 706 707 if (isForeground) { 708 notification.flags = Notification.FLAG_FOREGROUND_SERVICE; 709 } 710 711 if (isNavigation) { 712 notification.category = Notification.CATEGORY_NAVIGATION; 713 } 714 return notification; 715 } 716 generateStringOfLength(int length)717 private String generateStringOfLength(int length) { 718 String string = ""; 719 for (int i = 0; i < length; i++) { 720 string += "*"; 721 } 722 723 return string; 724 } 725 726 /** 727 * Ranks the provided alertEntries in reverse order. 728 * 729 * All methods that follow afterwards help assigning diverse attributes to the {@link 730 * android.service.notification.NotificationListenerService.Ranking} instances. 731 */ generateRankingMap( List<AlertEntry> alertEntries)732 private NotificationListenerService.RankingMap generateRankingMap( 733 List<AlertEntry> alertEntries) { 734 NotificationListenerService.Ranking[] rankings = 735 new NotificationListenerService.Ranking[alertEntries.size()]; 736 for (int i = 0; i < alertEntries.size(); i++) { 737 String key = alertEntries.get(i).getKey(); 738 int rank = alertEntries.size() - i; // ranking in reverse order; 739 NotificationListenerService.Ranking ranking = new NotificationListenerService.Ranking(); 740 ranking.populate( 741 key, 742 rank, 743 !isIntercepted(i), 744 getVisibilityOverride(i), 745 getSuppressedVisualEffects(i), 746 getImportance(i), 747 getExplanation(key), 748 getOverrideGroupKey(key), 749 getChannel(key, i), 750 getPeople(key, i), 751 getSnoozeCriteria(key, i), 752 getShowBadge(i), 753 getUserSentiment(i), 754 getHidden(i), 755 lastAudiblyAlerted(i), 756 getNoisy(i), 757 getSmartActions(key, i), 758 getSmartReplies(key, i), 759 canBubble(i), 760 isVisuallyInterruptive(i), 761 isConversation(i), 762 null, 763 isBubble(i) 764 765 ); 766 rankings[i] = ranking; 767 } 768 769 NotificationListenerService.RankingMap rankingMap 770 = new NotificationListenerService.RankingMap(rankings); 771 772 return rankingMap; 773 } 774 getVisibilityOverride(int index)775 private int getVisibilityOverride(int index) { 776 return index * 9; 777 } 778 getOverrideGroupKey(String key)779 private String getOverrideGroupKey(String key) { 780 return key + key; 781 } 782 isIntercepted(int index)783 private boolean isIntercepted(int index) { 784 return index % 2 == 0; 785 } 786 getSuppressedVisualEffects(int index)787 private int getSuppressedVisualEffects(int index) { 788 return index * 2; 789 } 790 getImportance(int index)791 private int getImportance(int index) { 792 return index; 793 } 794 getExplanation(String key)795 private String getExplanation(String key) { 796 return key + "explain"; 797 } 798 getChannel(String key, int index)799 private NotificationChannel getChannel(String key, int index) { 800 return new NotificationChannel(key, key, getImportance(index)); 801 } 802 getShowBadge(int index)803 private boolean getShowBadge(int index) { 804 return index % 3 == 0; 805 } 806 getUserSentiment(int index)807 private int getUserSentiment(int index) { 808 switch (index % 3) { 809 case 0: 810 return USER_SENTIMENT_NEGATIVE; 811 case 1: 812 return USER_SENTIMENT_NEUTRAL; 813 case 2: 814 return USER_SENTIMENT_POSITIVE; 815 } 816 return USER_SENTIMENT_NEUTRAL; 817 } 818 getHidden(int index)819 private boolean getHidden(int index) { 820 return index % 2 == 0; 821 } 822 lastAudiblyAlerted(int index)823 private long lastAudiblyAlerted(int index) { 824 return index * 2000; 825 } 826 getNoisy(int index)827 private boolean getNoisy(int index) { 828 return index < 1; 829 } 830 getPeople(String key, int index)831 private ArrayList<String> getPeople(String key, int index) { 832 ArrayList<String> people = new ArrayList<>(); 833 for (int i = 0; i < index; i++) { 834 people.add(i + key); 835 } 836 return people; 837 } 838 getSnoozeCriteria(String key, int index)839 private ArrayList<SnoozeCriterion> getSnoozeCriteria(String key, int index) { 840 ArrayList<SnoozeCriterion> snooze = new ArrayList<>(); 841 for (int i = 0; i < index; i++) { 842 snooze.add(new SnoozeCriterion(key + i, getExplanation(key), key)); 843 } 844 return snooze; 845 } 846 getSmartActions(String key, int index)847 private ArrayList<Notification.Action> getSmartActions(String key, int index) { 848 ArrayList<Notification.Action> actions = new ArrayList<>(); 849 for (int i = 0; i < index; i++) { 850 PendingIntent intent = PendingIntent.getBroadcast( 851 mContext, 852 index /*requestCode*/, 853 new Intent("ACTION_" + key), 854 0 /*flags*/); 855 actions.add(new Notification.Action.Builder(null /*icon*/, key, intent).build()); 856 } 857 return actions; 858 } 859 getSmartReplies(String key, int index)860 private ArrayList<CharSequence> getSmartReplies(String key, int index) { 861 ArrayList<CharSequence> choices = new ArrayList<>(); 862 for (int i = 0; i < index; i++) { 863 choices.add("choice_" + key + "_" + i); 864 } 865 return choices; 866 } 867 canBubble(int index)868 private boolean canBubble(int index) { 869 return index % 4 == 0; 870 } 871 isVisuallyInterruptive(int index)872 private boolean isVisuallyInterruptive(int index) { 873 return index % 4 == 0; 874 } 875 isConversation(int index)876 private boolean isConversation(int index) { 877 return index % 4 == 0; 878 } 879 isBubble(int index)880 private boolean isBubble(int index) { 881 return index % 4 == 0; 882 } 883 } 884