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