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 import android.testing.TestableResources; 50 import android.text.TextUtils; 51 52 import androidx.test.ext.junit.runners.AndroidJUnit4; 53 import androidx.test.platform.app.InstrumentationRegistry; 54 55 import org.junit.Before; 56 import org.junit.Rule; 57 import org.junit.Test; 58 import org.junit.runner.RunWith; 59 import org.mockito.ArgumentCaptor; 60 import org.mockito.InOrder; 61 import org.mockito.Mock; 62 import org.mockito.Mockito; 63 import org.mockito.MockitoAnnotations; 64 65 import java.util.ArrayList; 66 import java.util.Arrays; 67 import java.util.HashMap; 68 import java.util.HashSet; 69 import java.util.List; 70 import java.util.Map; 71 import java.util.Set; 72 import java.util.stream.Collectors; 73 74 @RunWith(AndroidJUnit4.class) 75 public class PreprocessingManagerTest { 76 77 private static final String PKG = "com.package.PREPROCESSING_MANAGER_TEST"; 78 private static final String OP_PKG = "OpPackage"; 79 private static final int ID = 1; 80 private static final String TAG = "Tag"; 81 private static final int UID = 2; 82 private static final int INITIAL_PID = 3; 83 private static final String CHANNEL_ID = "CHANNEL_ID"; 84 private static final String CONTENT_TITLE = "CONTENT_TITLE"; 85 private static final String OVERRIDE_GROUP_KEY = "OVERRIDE_GROUP_KEY"; 86 private static final long POST_TIME = 12345l; 87 private static final UserHandle USER_HANDLE = new UserHandle(12); 88 private static final String GROUP_KEY_A = "GROUP_KEY_A"; 89 private static final String GROUP_KEY_B = "GROUP_KEY_B"; 90 private static final String GROUP_KEY_C = "GROUP_KEY_C"; 91 private static final int MAX_STRING_LENGTH = 10; 92 private static final int DEFAULT_MIN_GROUPING_THRESHOLD = 4; 93 @Rule 94 public final TestableContext mContext = new TestableContext( 95 InstrumentationRegistry.getInstrumentation().getTargetContext()); 96 @Mock 97 private StatusBarNotification mStatusBarNotification1; 98 @Mock 99 private StatusBarNotification mStatusBarNotification2; 100 @Mock 101 private StatusBarNotification mStatusBarNotification3; 102 @Mock 103 private StatusBarNotification mStatusBarNotification4; 104 @Mock 105 private StatusBarNotification mStatusBarNotification5; 106 @Mock 107 private StatusBarNotification mStatusBarNotification6; 108 @Mock 109 private StatusBarNotification mStatusBarNotification7; 110 @Mock 111 private StatusBarNotification mStatusBarNotification8; 112 @Mock 113 private StatusBarNotification mStatusBarNotification9; 114 @Mock 115 private StatusBarNotification mStatusBarNotification10; 116 @Mock 117 private StatusBarNotification mStatusBarNotification11; 118 @Mock 119 private StatusBarNotification mStatusBarNotification12; 120 @Mock 121 private StatusBarNotification mAdditionalStatusBarNotification; 122 @Mock 123 private StatusBarNotification mSummaryAStatusBarNotification; 124 @Mock 125 private StatusBarNotification mSummaryBStatusBarNotification; 126 @Mock 127 private StatusBarNotification mSummaryCStatusBarNotification; 128 @Mock 129 private CarUxRestrictions mCarUxRestrictions; 130 @Mock 131 private CarUxRestrictionManagerWrapper mCarUxRestrictionManagerWrapper; 132 @Mock 133 private PreprocessingManager.CallStateListener mCallStateListener1; 134 @Mock 135 private PreprocessingManager.CallStateListener mCallStateListener2; 136 @Mock 137 private Notification mMediaNotification; 138 @Mock 139 private Notification mSummaryNotification; 140 @Mock 141 private PackageManager mPackageManager; 142 @Mock 143 private NotificationDataManager mNotificationDataManager; 144 145 private PreprocessingManager mPreprocessingManager; 146 147 private Notification mForegroundNotification; 148 private Notification mBackgroundNotification; 149 private Notification mNavigationNotification; 150 151 // Following AlertEntry var names describe the type of notifications they wrap. 152 private AlertEntry mLessImportantBackground; 153 private AlertEntry mLessImportantForeground; 154 private AlertEntry mMedia; 155 private AlertEntry mNavigation; 156 private AlertEntry mImportantBackground; 157 private AlertEntry mImportantForeground; 158 private AlertEntry mImportantForeground2; 159 private AlertEntry mImportantForeground3; 160 private AlertEntry mImportantForeground4; 161 private AlertEntry mImportantForeground5; 162 private AlertEntry mImportantForeground6; 163 private AlertEntry mImportantForeground7; 164 165 private List<AlertEntry> mAlertEntries; 166 private Map<String, AlertEntry> mAlertEntriesMap; 167 private NotificationListenerService.RankingMap mRankingMap; 168 169 @Before setup()170 public void setup() throws PackageManager.NameNotFoundException { 171 MockitoAnnotations.initMocks(this); 172 173 // prevents less important foreground notifications from not being filtered due to the 174 // application and package setup. 175 PackageInfo packageInfo = mock(PackageInfo.class); 176 ApplicationInfo applicationInfo = mock(ApplicationInfo.class); 177 packageInfo.packageName = PKG; 178 when(applicationInfo.isPrivilegedApp()).thenReturn(true); 179 when(applicationInfo.isSystemApp()).thenReturn(true); 180 when(applicationInfo.isSignedWithPlatformKey()).thenReturn(true); 181 packageInfo.applicationInfo = applicationInfo; 182 when(mPackageManager.getPackageInfoAsUser(anyString(), anyInt(), anyInt())).thenReturn( 183 packageInfo); 184 mContext.setMockPackageManager(mPackageManager); 185 186 mPreprocessingManager.refreshInstance(); 187 mPreprocessingManager = PreprocessingManager.getInstance(mContext); 188 189 mForegroundNotification = generateNotification(/* isForeground= */ true, 190 /* isNavigation= */ false, /* isGroupSummary= */ true); 191 mBackgroundNotification = generateNotification(/* isForeground= */ false, 192 /* isNavigation= */ false, /* isGroupSummary= */ true); 193 mNavigationNotification = generateNotification(/* isForeground= */ true, 194 /* isNavigation= */ true, /* isGroupSummary= */ true); 195 196 when(mMediaNotification.isMediaNotification()).thenReturn(true); 197 198 // Key describes the notification that the StatusBarNotification contains. 199 when(mStatusBarNotification1.getKey()).thenReturn("KEY_LESS_IMPORTANT_BACKGROUND"); 200 when(mStatusBarNotification2.getKey()).thenReturn("KEY_LESS_IMPORTANT_FOREGROUND"); 201 when(mStatusBarNotification3.getKey()).thenReturn("KEY_MEDIA"); 202 when(mStatusBarNotification4.getKey()).thenReturn("KEY_NAVIGATION"); 203 when(mStatusBarNotification5.getKey()).thenReturn("KEY_IMPORTANT_BACKGROUND"); 204 when(mStatusBarNotification6.getKey()).thenReturn("KEY_IMPORTANT_FOREGROUND"); 205 when(mStatusBarNotification7.getKey()).thenReturn("KEY_IMPORTANT_FOREGROUND_2"); 206 when(mStatusBarNotification8.getKey()).thenReturn("KEY_IMPORTANT_FOREGROUND_3"); 207 when(mStatusBarNotification9.getKey()).thenReturn("KEY_IMPORTANT_FOREGROUND_4"); 208 when(mStatusBarNotification10.getKey()).thenReturn("KEY_IMPORTANT_FOREGROUND_5"); 209 when(mStatusBarNotification11.getKey()).thenReturn("KEY_IMPORTANT_FOREGROUND_6"); 210 when(mStatusBarNotification12.getKey()).thenReturn("KEY_IMPORTANT_FOREGROUND_7"); 211 when(mSummaryAStatusBarNotification.getKey()).thenReturn("KEY_SUMMARY_A"); 212 when(mSummaryBStatusBarNotification.getKey()).thenReturn("KEY_SUMMARY_B"); 213 when(mSummaryCStatusBarNotification.getKey()).thenReturn("KEY_SUMMARY_C"); 214 215 when(mStatusBarNotification1.getGroupKey()).thenReturn(GROUP_KEY_A); 216 when(mStatusBarNotification2.getGroupKey()).thenReturn(GROUP_KEY_B); 217 when(mStatusBarNotification3.getGroupKey()).thenReturn(GROUP_KEY_A); 218 when(mStatusBarNotification4.getGroupKey()).thenReturn(GROUP_KEY_B); 219 when(mStatusBarNotification5.getGroupKey()).thenReturn(GROUP_KEY_B); 220 when(mStatusBarNotification6.getGroupKey()).thenReturn(GROUP_KEY_C); 221 when(mSummaryAStatusBarNotification.getGroupKey()).thenReturn(GROUP_KEY_A); 222 when(mSummaryBStatusBarNotification.getGroupKey()).thenReturn(GROUP_KEY_B); 223 when(mSummaryCStatusBarNotification.getGroupKey()).thenReturn(GROUP_KEY_C); 224 225 when(mStatusBarNotification1.getNotification()).thenReturn(mBackgroundNotification); 226 when(mStatusBarNotification2.getNotification()).thenReturn(mForegroundNotification); 227 when(mStatusBarNotification3.getNotification()).thenReturn(mMediaNotification); 228 when(mStatusBarNotification4.getNotification()).thenReturn(mNavigationNotification); 229 when(mStatusBarNotification5.getNotification()).thenReturn(mBackgroundNotification); 230 when(mStatusBarNotification6.getNotification()).thenReturn(mForegroundNotification); 231 when(mStatusBarNotification7.getNotification()).thenReturn(mForegroundNotification); 232 when(mStatusBarNotification8.getNotification()).thenReturn(mForegroundNotification); 233 when(mStatusBarNotification9.getNotification()).thenReturn(mForegroundNotification); 234 when(mStatusBarNotification10.getNotification()).thenReturn(mForegroundNotification); 235 when(mStatusBarNotification11.getNotification()).thenReturn(mForegroundNotification); 236 when(mStatusBarNotification12.getNotification()).thenReturn(mForegroundNotification); 237 when(mSummaryAStatusBarNotification.getNotification()).thenReturn(mSummaryNotification); 238 when(mSummaryBStatusBarNotification.getNotification()).thenReturn(mSummaryNotification); 239 when(mSummaryCStatusBarNotification.getNotification()).thenReturn(mSummaryNotification); 240 241 when(mStatusBarNotification1.getPackageName()).thenReturn(PKG); 242 when(mStatusBarNotification2.getPackageName()).thenReturn(PKG); 243 when(mStatusBarNotification3.getPackageName()).thenReturn(PKG); 244 when(mStatusBarNotification4.getPackageName()).thenReturn(PKG); 245 when(mStatusBarNotification5.getPackageName()).thenReturn(PKG); 246 when(mStatusBarNotification6.getPackageName()).thenReturn(PKG); 247 when(mStatusBarNotification7.getPackageName()).thenReturn(PKG); 248 when(mStatusBarNotification8.getPackageName()).thenReturn(PKG); 249 when(mStatusBarNotification9.getPackageName()).thenReturn(PKG); 250 when(mStatusBarNotification10.getPackageName()).thenReturn(PKG); 251 when(mStatusBarNotification11.getPackageName()).thenReturn(PKG); 252 when(mStatusBarNotification12.getPackageName()).thenReturn(PKG); 253 when(mSummaryAStatusBarNotification.getPackageName()).thenReturn(PKG); 254 when(mSummaryBStatusBarNotification.getPackageName()).thenReturn(PKG); 255 when(mSummaryCStatusBarNotification.getPackageName()).thenReturn(PKG); 256 257 when(mSummaryNotification.isGroupSummary()).thenReturn(true); 258 259 // Always start system with no phone calls in progress. 260 Intent intent = new Intent(TelephonyManager.ACTION_PHONE_STATE_CHANGED); 261 intent.putExtra(TelephonyManager.EXTRA_STATE, TelephonyManager.EXTRA_STATE_IDLE); 262 mPreprocessingManager.mIntentReceiver.onReceive(mContext, intent); 263 264 initTestData(/* includeAdditionalNotifs= */ false); 265 } 266 267 @Test onFilter_showLessImportantNotifications_doesNotFilterNotifications()268 public void onFilter_showLessImportantNotifications_doesNotFilterNotifications() { 269 List<AlertEntry> unfiltered = mAlertEntries.stream().collect(Collectors.toList()); 270 mPreprocessingManager 271 .filter(/* showLessImportantNotifications= */ true, mAlertEntries, mRankingMap); 272 273 assertThat(mAlertEntries.equals(unfiltered)).isTrue(); 274 } 275 276 @Test onFilter_dontShowLessImportantNotifications_filtersLessImportantForeground()277 public void onFilter_dontShowLessImportantNotifications_filtersLessImportantForeground() 278 throws PackageManager.NameNotFoundException { 279 mPreprocessingManager 280 .filter( /* showLessImportantNotifications= */ false, mAlertEntries, mRankingMap); 281 282 assertThat(mAlertEntries.contains(mLessImportantBackground)).isTrue(); 283 assertThat(mAlertEntries.contains(mLessImportantForeground)).isFalse(); 284 } 285 286 @Test onFilter_dontShowLessImportantNotifications_doesNotFilterMoreImportant()287 public void onFilter_dontShowLessImportantNotifications_doesNotFilterMoreImportant() { 288 mPreprocessingManager 289 .filter(/* showLessImportantNotifications= */ false, mAlertEntries, mRankingMap); 290 291 assertThat(mAlertEntries.contains(mImportantBackground)).isTrue(); 292 assertThat(mAlertEntries.contains(mImportantForeground)).isTrue(); 293 } 294 295 @Test onFilter_dontShowLessImportantNotifications_filtersMediaAndNavigation()296 public void onFilter_dontShowLessImportantNotifications_filtersMediaAndNavigation() { 297 mPreprocessingManager 298 .filter(/* showLessImportantNotifications= */ false, mAlertEntries, mRankingMap); 299 300 assertThat(mAlertEntries.contains(mMedia)).isFalse(); 301 assertThat(mAlertEntries.contains(mNavigation)).isFalse(); 302 } 303 304 @Test onFilter_doShowLessImportantNotifications_doesNotFilterMediaOrNavigation()305 public void onFilter_doShowLessImportantNotifications_doesNotFilterMediaOrNavigation() { 306 mPreprocessingManager 307 .filter(/* showLessImportantNotifications= */ true, mAlertEntries, mRankingMap); 308 309 assertThat(mAlertEntries.contains(mMedia)).isTrue(); 310 assertThat(mAlertEntries.contains(mNavigation)).isTrue(); 311 } 312 313 @Test onFilter_doShowLessImportantNotifications_filtersCalls()314 public void onFilter_doShowLessImportantNotifications_filtersCalls() { 315 StatusBarNotification callSBN = mock(StatusBarNotification.class); 316 Notification callNotification = new Notification(); 317 callNotification.category = Notification.CATEGORY_CALL; 318 when(callSBN.getNotification()).thenReturn(callNotification); 319 List<AlertEntry> entries = new ArrayList<>(); 320 entries.add(new AlertEntry(callSBN)); 321 322 mPreprocessingManager.filter(true, entries, mRankingMap); 323 assertThat(entries).isEmpty(); 324 } 325 326 @Test onFilter_dontShowLessImportantNotifications_filtersCalls()327 public void onFilter_dontShowLessImportantNotifications_filtersCalls() { 328 StatusBarNotification callSBN = mock(StatusBarNotification.class); 329 Notification callNotification = new Notification(); 330 callNotification.category = Notification.CATEGORY_CALL; 331 when(callSBN.getNotification()).thenReturn(callNotification); 332 List<AlertEntry> entries = new ArrayList<>(); 333 entries.add(new AlertEntry(callSBN)); 334 335 mPreprocessingManager.filter(false, entries, mRankingMap); 336 assertThat(entries).isEmpty(); 337 } 338 339 @Test onOptimizeForDriving_alertEntryHasNonMessageNotification_trimsNotificationTexts()340 public void onOptimizeForDriving_alertEntryHasNonMessageNotification_trimsNotificationTexts() { 341 when(mCarUxRestrictions.getMaxRestrictedStringLength()).thenReturn(MAX_STRING_LENGTH); 342 when(mCarUxRestrictionManagerWrapper.getCurrentCarUxRestrictions()) 343 .thenReturn(mCarUxRestrictions); 344 mPreprocessingManager.setCarUxRestrictionManagerWrapper(mCarUxRestrictionManagerWrapper); 345 346 Notification nonMessageNotification = generateNotification(/* isForeground= */ true, 347 /* isNavigation= */ true, /* isGroupSummary= */ true); 348 nonMessageNotification.extras 349 .putString(Notification.EXTRA_TITLE, generateStringOfLength(100)); 350 nonMessageNotification.extras 351 .putString(Notification.EXTRA_TEXT, generateStringOfLength(100)); 352 nonMessageNotification.extras 353 .putString(Notification.EXTRA_TITLE_BIG, generateStringOfLength(100)); 354 nonMessageNotification.extras 355 .putString(Notification.EXTRA_SUMMARY_TEXT, generateStringOfLength(100)); 356 357 when(mNavigation.getNotification()).thenReturn(nonMessageNotification); 358 359 AlertEntry optimized = mPreprocessingManager.optimizeForDriving(mNavigation); 360 Bundle trimmed = optimized.getNotification().extras; 361 362 for (String key : trimmed.keySet()) { 363 switch (key) { 364 case Notification.EXTRA_TITLE: 365 case Notification.EXTRA_TEXT: 366 case Notification.EXTRA_TITLE_BIG: 367 case Notification.EXTRA_SUMMARY_TEXT: 368 CharSequence text = trimmed.getCharSequence(key); 369 assertThat(text.length() <= MAX_STRING_LENGTH).isTrue(); 370 default: 371 continue; 372 } 373 } 374 } 375 376 @Test onOptimizeForDriving_alertEntryHasMessageNotification_doesNotTrimMessageTexts()377 public void onOptimizeForDriving_alertEntryHasMessageNotification_doesNotTrimMessageTexts() { 378 when(mCarUxRestrictions.getMaxRestrictedStringLength()).thenReturn(MAX_STRING_LENGTH); 379 when(mCarUxRestrictionManagerWrapper.getCurrentCarUxRestrictions()) 380 .thenReturn(mCarUxRestrictions); 381 mPreprocessingManager.setCarUxRestrictionManagerWrapper(mCarUxRestrictionManagerWrapper); 382 383 Notification messageNotification = generateNotification(/* isForeground= */ true, 384 /* isNavigation= */ true, /* isGroupSummary= */ true); 385 messageNotification.extras 386 .putString(Notification.EXTRA_TITLE, generateStringOfLength(100)); 387 messageNotification.extras 388 .putString(Notification.EXTRA_TEXT, generateStringOfLength(100)); 389 messageNotification.extras 390 .putString(Notification.EXTRA_TITLE_BIG, generateStringOfLength(100)); 391 messageNotification.extras 392 .putString(Notification.EXTRA_SUMMARY_TEXT, generateStringOfLength(100)); 393 messageNotification.category = Notification.CATEGORY_MESSAGE; 394 395 when(mImportantForeground.getNotification()).thenReturn(messageNotification); 396 397 AlertEntry optimized = mPreprocessingManager.optimizeForDriving(mImportantForeground); 398 Bundle trimmed = optimized.getNotification().extras; 399 400 for (String key : trimmed.keySet()) { 401 switch (key) { 402 case Notification.EXTRA_TITLE: 403 case Notification.EXTRA_TEXT: 404 case Notification.EXTRA_TITLE_BIG: 405 case Notification.EXTRA_SUMMARY_TEXT: 406 CharSequence text = trimmed.getCharSequence(key); 407 assertThat(text.length() <= MAX_STRING_LENGTH).isFalse(); 408 default: 409 continue; 410 } 411 } 412 } 413 414 @Test onGroup_groupsNotificationsByGroupKey()415 public void onGroup_groupsNotificationsByGroupKey() { 416 setConfig(/* recentOld= */ true, /* launcherIcon= */ true, /* groupingThreshold= */ 2); 417 PreprocessingManager.refreshInstance(); 418 mPreprocessingManager = PreprocessingManager.getInstance(mContext); 419 List<NotificationGroup> groupResult = mPreprocessingManager.group(mAlertEntries); 420 String[] actualGroupKeys = new String[groupResult.size()]; 421 String[] expectedGroupKeys = {GROUP_KEY_A, GROUP_KEY_B, GROUP_KEY_C}; 422 423 for (int i = 0; i < groupResult.size(); i++) { 424 actualGroupKeys[i] = groupResult.get(i).getGroupKey(); 425 } 426 427 Arrays.sort(actualGroupKeys); 428 Arrays.sort(expectedGroupKeys); 429 430 assertThat(actualGroupKeys).isEqualTo(expectedGroupKeys); 431 } 432 433 @Test onGroup_highGroupingThreshold_noGroups()434 public void onGroup_highGroupingThreshold_noGroups() { 435 setConfig(/* recentOld= */ true, /* launcherIcon= */ true, DEFAULT_MIN_GROUPING_THRESHOLD); 436 PreprocessingManager.refreshInstance(); 437 mPreprocessingManager = PreprocessingManager.getInstance(mContext); 438 List<NotificationGroup> groupResult = mPreprocessingManager.group(mAlertEntries); 439 String[] actualGroupKeys = new String[groupResult.size()]; 440 String[] expectedGroupKeys = {GROUP_KEY_A, GROUP_KEY_B, GROUP_KEY_B, GROUP_KEY_C}; 441 442 for (int i = 0; i < groupResult.size(); i++) { 443 actualGroupKeys[i] = groupResult.get(i).getGroupKey(); 444 } 445 446 Arrays.sort(actualGroupKeys); 447 Arrays.sort(expectedGroupKeys); 448 449 assertThat(actualGroupKeys).isEqualTo(expectedGroupKeys); 450 } 451 452 @Test onGroup_groupsNotificationsBySeenUnseen()453 public void onGroup_groupsNotificationsBySeenUnseen() { 454 setConfig(/* recentOld= */ true, /* launcherIcon= */ true, DEFAULT_MIN_GROUPING_THRESHOLD); 455 initTestData(/* includeAdditionalNotifs= */ true); 456 PreprocessingManager.refreshInstance(); 457 mPreprocessingManager = PreprocessingManager.getInstance(mContext); 458 when(mNotificationDataManager.isNotificationSeen(mLessImportantForeground)) 459 .thenReturn(true); 460 when(mNotificationDataManager.isNotificationSeen(mLessImportantBackground)) 461 .thenReturn(true); 462 when(mNotificationDataManager.isNotificationSeen(mMedia)).thenReturn(true); 463 when(mNotificationDataManager.isNotificationSeen(mImportantBackground)).thenReturn(true); 464 when(mNotificationDataManager.isNotificationSeen(mImportantForeground)).thenReturn(true); 465 when(mNotificationDataManager.isNotificationSeen(mImportantForeground2)).thenReturn(true); 466 when(mNotificationDataManager.isNotificationSeen(mImportantForeground3)).thenReturn(true); 467 when(mNotificationDataManager.isNotificationSeen(mImportantForeground4)).thenReturn(false); 468 when(mNotificationDataManager.isNotificationSeen(mImportantForeground5)).thenReturn(false); 469 when(mNotificationDataManager.isNotificationSeen(mImportantForeground6)).thenReturn(false); 470 when(mNotificationDataManager.isNotificationSeen(mImportantForeground7)).thenReturn(false); 471 when(mNotificationDataManager.isNotificationSeen(mNavigation)).thenReturn(false); 472 when(mStatusBarNotification1.getGroupKey()).thenReturn(GROUP_KEY_A); 473 when(mStatusBarNotification2.getGroupKey()).thenReturn(GROUP_KEY_A); 474 when(mStatusBarNotification3.getGroupKey()).thenReturn(GROUP_KEY_A); 475 when(mStatusBarNotification4.getGroupKey()).thenReturn(GROUP_KEY_A); 476 when(mStatusBarNotification5.getGroupKey()).thenReturn(GROUP_KEY_A); 477 when(mStatusBarNotification6.getGroupKey()).thenReturn(GROUP_KEY_A); 478 when(mStatusBarNotification7.getGroupKey()).thenReturn(GROUP_KEY_A); 479 when(mStatusBarNotification8.getGroupKey()).thenReturn(GROUP_KEY_A); 480 when(mStatusBarNotification9.getGroupKey()).thenReturn(GROUP_KEY_A); 481 when(mStatusBarNotification10.getGroupKey()).thenReturn(GROUP_KEY_A); 482 when(mStatusBarNotification11.getGroupKey()).thenReturn(GROUP_KEY_A); 483 when(mStatusBarNotification12.getGroupKey()).thenReturn(GROUP_KEY_A); 484 485 mPreprocessingManager.setNotificationDataManager(mNotificationDataManager); 486 487 Set expectedResultUnseen = new HashSet(); 488 expectedResultUnseen.add(mImportantBackground.getKey()); 489 expectedResultUnseen.add(mNavigation.getKey()); 490 expectedResultUnseen.add(mImportantForeground4.getKey()); 491 expectedResultUnseen.add(mImportantForeground5.getKey()); 492 expectedResultUnseen.add(mImportantForeground6.getKey()); 493 expectedResultUnseen.add(mImportantForeground7.getKey()); 494 Set expectedResultSeen = new HashSet(); 495 expectedResultSeen.add(mImportantBackground.getKey()); 496 expectedResultSeen.add(mLessImportantForeground.getKey()); 497 expectedResultSeen.add(mImportantForeground2.getKey()); 498 expectedResultSeen.add(mImportantForeground3.getKey()); 499 expectedResultSeen.add(mMedia.getKey()); 500 expectedResultSeen.add(mImportantForeground.getKey()); 501 502 List<NotificationGroup> groupResult = mPreprocessingManager.group(mAlertEntries); 503 Set actualResultSeen = new HashSet(); 504 Set actualResultUnseen = new HashSet(); 505 for (int j = 0; j < groupResult.size(); j++) { 506 NotificationGroup group = groupResult.get(j); 507 List<AlertEntry> childNotifications = group.getChildNotifications(); 508 for (int i = 0; i < childNotifications.size(); i++) { 509 if (group.isSeen()) { 510 actualResultSeen.add(childNotifications.get(i).getKey()); 511 } else { 512 actualResultUnseen.add(childNotifications.get(i).getKey()); 513 } 514 } 515 if (group.getGroupSummaryNotification() != null) { 516 if (group.isSeen()) { 517 actualResultSeen.add(group.getGroupSummaryNotification().getKey()); 518 } else { 519 actualResultUnseen.add(group.getGroupSummaryNotification().getKey()); 520 } 521 } 522 } 523 assertThat(actualResultSeen).isEqualTo(expectedResultSeen); 524 assertThat(actualResultUnseen).isEqualTo(expectedResultUnseen); 525 } 526 527 @Test onGroup_autoGeneratedGroupWithNoGroupChildren_doesNotShowGroupSummary()528 public void onGroup_autoGeneratedGroupWithNoGroupChildren_doesNotShowGroupSummary() { 529 List<AlertEntry> list = new ArrayList<>(); 530 list.add(getEmptyAutoGeneratedGroupSummary()); 531 List<NotificationGroup> groupResult = mPreprocessingManager.group(list); 532 533 assertThat(groupResult.size() == 0).isTrue(); 534 } 535 536 @Test addCallStateListener_preCall_triggerChanges()537 public void addCallStateListener_preCall_triggerChanges() { 538 InOrder listenerInOrder = Mockito.inOrder(mCallStateListener1); 539 mPreprocessingManager.addCallStateListener(mCallStateListener1); 540 listenerInOrder.verify(mCallStateListener1).onCallStateChanged(false); 541 542 Intent intent = new Intent(TelephonyManager.ACTION_PHONE_STATE_CHANGED); 543 intent.putExtra(TelephonyManager.EXTRA_STATE, TelephonyManager.EXTRA_STATE_OFFHOOK); 544 mPreprocessingManager.mIntentReceiver.onReceive(mContext, intent); 545 546 listenerInOrder.verify(mCallStateListener1).onCallStateChanged(true); 547 } 548 549 @Test addCallStateListener_midCall_triggerChanges()550 public void addCallStateListener_midCall_triggerChanges() { 551 Intent intent = new Intent(TelephonyManager.ACTION_PHONE_STATE_CHANGED); 552 intent.putExtra(TelephonyManager.EXTRA_STATE, TelephonyManager.EXTRA_STATE_OFFHOOK); 553 mPreprocessingManager.mIntentReceiver.onReceive(mContext, intent); 554 555 mPreprocessingManager.addCallStateListener(mCallStateListener1); 556 557 verify(mCallStateListener1).onCallStateChanged(true); 558 } 559 560 @Test addCallStateListener_postCall_triggerChanges()561 public void addCallStateListener_postCall_triggerChanges() { 562 Intent intent = new Intent(TelephonyManager.ACTION_PHONE_STATE_CHANGED); 563 intent.putExtra(TelephonyManager.EXTRA_STATE, TelephonyManager.EXTRA_STATE_OFFHOOK); 564 mPreprocessingManager.mIntentReceiver.onReceive(mContext, intent); 565 566 intent = new Intent(TelephonyManager.ACTION_PHONE_STATE_CHANGED); 567 intent.putExtra(TelephonyManager.EXTRA_STATE, TelephonyManager.EXTRA_STATE_IDLE); 568 mPreprocessingManager.mIntentReceiver.onReceive(mContext, intent); 569 570 mPreprocessingManager.addCallStateListener(mCallStateListener1); 571 572 verify(mCallStateListener1).onCallStateChanged(false); 573 } 574 575 @Test addSameCallListenerTwice_dedupedCorrectly()576 public void addSameCallListenerTwice_dedupedCorrectly() { 577 mPreprocessingManager.addCallStateListener(mCallStateListener1); 578 579 verify(mCallStateListener1).onCallStateChanged(false); 580 mPreprocessingManager.addCallStateListener(mCallStateListener1); 581 582 verify(mCallStateListener1, times(1)).onCallStateChanged(false); 583 } 584 585 @Test removeCallStateListener_midCall_triggerChanges()586 public void removeCallStateListener_midCall_triggerChanges() { 587 Intent intent = new Intent(TelephonyManager.ACTION_PHONE_STATE_CHANGED); 588 intent.putExtra(TelephonyManager.EXTRA_STATE, TelephonyManager.EXTRA_STATE_OFFHOOK); 589 mPreprocessingManager.mIntentReceiver.onReceive(mContext, intent); 590 591 mPreprocessingManager.addCallStateListener(mCallStateListener1); 592 // Should get triggered with true before calling removeCallStateListener 593 mPreprocessingManager.removeCallStateListener(mCallStateListener1); 594 595 intent = new Intent(TelephonyManager.ACTION_PHONE_STATE_CHANGED); 596 intent.putExtra(TelephonyManager.EXTRA_STATE, TelephonyManager.EXTRA_STATE_IDLE); 597 mPreprocessingManager.mIntentReceiver.onReceive(mContext, intent); 598 599 verify(mCallStateListener1, never()).onCallStateChanged(false); 600 } 601 602 @Test multipleCallStateListeners_triggeredAppropriately()603 public void multipleCallStateListeners_triggeredAppropriately() { 604 InOrder listenerInOrder1 = Mockito.inOrder(mCallStateListener1); 605 InOrder listenerInOrder2 = Mockito.inOrder(mCallStateListener2); 606 mPreprocessingManager.addCallStateListener(mCallStateListener1); 607 listenerInOrder1.verify(mCallStateListener1).onCallStateChanged(false); 608 609 Intent intent = new Intent(TelephonyManager.ACTION_PHONE_STATE_CHANGED); 610 intent.putExtra(TelephonyManager.EXTRA_STATE, TelephonyManager.EXTRA_STATE_OFFHOOK); 611 mPreprocessingManager.mIntentReceiver.onReceive(mContext, intent); 612 613 mPreprocessingManager.addCallStateListener(mCallStateListener2); 614 mPreprocessingManager.removeCallStateListener(mCallStateListener1); 615 616 listenerInOrder1.verify(mCallStateListener1).onCallStateChanged(true); 617 listenerInOrder2.verify(mCallStateListener2).onCallStateChanged(true); 618 619 intent = new Intent(TelephonyManager.ACTION_PHONE_STATE_CHANGED); 620 intent.putExtra(TelephonyManager.EXTRA_STATE, TelephonyManager.EXTRA_STATE_IDLE); 621 mPreprocessingManager.mIntentReceiver.onReceive(mContext, intent); 622 623 // only listener 2 should be triggered w/ false 624 listenerInOrder1.verifyNoMoreInteractions(); 625 listenerInOrder2.verify(mCallStateListener2).onCallStateChanged(false); 626 } 627 628 @Test onGroup_removesNotificationGroupWithOnlySummaryNotification()629 public void onGroup_removesNotificationGroupWithOnlySummaryNotification() { 630 List<AlertEntry> list = new ArrayList<>(); 631 list.add(new AlertEntry(mSummaryCStatusBarNotification)); 632 List<NotificationGroup> groupResult = mPreprocessingManager.group(list); 633 634 assertThat(groupResult.isEmpty()).isTrue(); 635 } 636 637 @Test onGroup_splitsNotificationsBySeenAndUnseen()638 public void onGroup_splitsNotificationsBySeenAndUnseen() { 639 List<AlertEntry> list = new ArrayList<>(); 640 list.add(new AlertEntry(mSummaryCStatusBarNotification)); 641 642 List<NotificationGroup> groupResult = mPreprocessingManager.group(list); 643 644 assertThat(groupResult.isEmpty()).isTrue(); 645 } 646 647 @Test onGroup_childNotificationHasTimeStamp_groupHasMostRecentTimeStamp()648 public void onGroup_childNotificationHasTimeStamp_groupHasMostRecentTimeStamp() { 649 mBackgroundNotification.when = 0; 650 mForegroundNotification.when = 1; 651 mNavigationNotification.when = 2; 652 653 mBackgroundNotification.extras.putBoolean(Notification.EXTRA_SHOW_WHEN, true); 654 mForegroundNotification.extras.putBoolean(Notification.EXTRA_SHOW_WHEN, true); 655 mNavigationNotification.extras.putBoolean(Notification.EXTRA_SHOW_WHEN, true); 656 657 List<NotificationGroup> groupResult = mPreprocessingManager.group(mAlertEntries); 658 659 groupResult.forEach(group -> { 660 AlertEntry groupSummaryNotification = group.getGroupSummaryNotification(); 661 if (groupSummaryNotification != null 662 && groupSummaryNotification.getNotification() != null) { 663 assertThat(groupSummaryNotification.getNotification() 664 .extras.getBoolean(Notification.EXTRA_SHOW_WHEN)).isTrue(); 665 } 666 }); 667 } 668 669 @Test onRank_ranksNotificationGroups()670 public void onRank_ranksNotificationGroups() { 671 setConfig(/* recentOld= */ true, /* launcherIcon= */ true, /* groupThreshold= */ 2); 672 PreprocessingManager.refreshInstance(); 673 mPreprocessingManager = PreprocessingManager.getInstance(mContext); 674 List<NotificationGroup> groupResult = mPreprocessingManager.group(mAlertEntries); 675 List<NotificationGroup> rankResult = mPreprocessingManager.rank(groupResult, mRankingMap); 676 677 // generateRankingMap ranked the notifications in the reverse order. 678 String[] expectedOrder = { 679 GROUP_KEY_C, 680 GROUP_KEY_B, 681 GROUP_KEY_A 682 }; 683 684 for (int i = 0; i < rankResult.size(); i++) { 685 String actualGroupKey = rankResult.get(i).getGroupKey(); 686 String expectedGroupKey = expectedOrder[i]; 687 688 assertThat(actualGroupKey).isEqualTo(expectedGroupKey); 689 } 690 } 691 692 @Test onRank_ranksNotificationsInEachGroup()693 public void onRank_ranksNotificationsInEachGroup() { 694 setConfig(/* recentOld= */true, /* launcherIcon= */ true, /* groupThreshold= */ 2); 695 PreprocessingManager.refreshInstance(); 696 mPreprocessingManager = PreprocessingManager.getInstance(mContext); 697 List<NotificationGroup> groupResult = mPreprocessingManager.group(mAlertEntries); 698 List<NotificationGroup> rankResult = mPreprocessingManager.rank(groupResult, mRankingMap); 699 NotificationGroup groupB = rankResult.get(1); 700 701 // first make sure that we have Group B 702 assertThat(groupB.getGroupKey()).isEqualTo(GROUP_KEY_B); 703 704 // generateRankingMap ranked the non-background notifications in the reverse order 705 String[] expectedOrder = { 706 "KEY_NAVIGATION", 707 "KEY_LESS_IMPORTANT_FOREGROUND" 708 }; 709 710 for (int i = 0; i < groupB.getChildNotifications().size(); i++) { 711 String actualKey = groupB.getChildNotifications().get(i).getKey(); 712 String expectedGroupKey = expectedOrder[i]; 713 714 assertThat(actualKey).isEqualTo(expectedGroupKey); 715 } 716 } 717 718 @Test onAdditionalGroupAndRank_isGroupSummary_returnsTheSameGroupsAsStandardGroup()719 public void onAdditionalGroupAndRank_isGroupSummary_returnsTheSameGroupsAsStandardGroup() { 720 Notification additionalNotification = generateNotification(/* isForeground= */ false, 721 /* isNavigation= */ false, /* isGroupSummary= */ true); 722 additionalNotification.category = Notification.CATEGORY_MESSAGE; 723 when(mAdditionalStatusBarNotification.getKey()).thenReturn("ADDITIONAL"); 724 when(mAdditionalStatusBarNotification.getGroupKey()).thenReturn(GROUP_KEY_C); 725 when(mAdditionalStatusBarNotification.getNotification()).thenReturn(additionalNotification); 726 AlertEntry additionalAlertEntry = new AlertEntry(mAdditionalStatusBarNotification); 727 728 mPreprocessingManager.init(mAlertEntriesMap, mRankingMap); 729 List<AlertEntry> copy = mPreprocessingManager.filter(/* showLessImportantNotifications= */ 730 false, new ArrayList<>(mAlertEntries), mRankingMap); 731 copy.add(additionalAlertEntry); 732 copy.add(new AlertEntry(mSummaryCStatusBarNotification)); 733 List<NotificationGroup> expected = mPreprocessingManager.group(copy); 734 String[] expectedKeys = new String[expected.size()]; 735 for (int i = 0; i < expectedKeys.length; i++) { 736 expectedKeys[i] = expected.get(i).getGroupKey(); 737 } 738 739 List<NotificationGroup> actual = mPreprocessingManager 740 .additionalGroupAndRank(additionalAlertEntry, mRankingMap, /* isUpdate= */ false); 741 742 String[] actualKeys = new String[actual.size()]; 743 for (int i = 0; i < actualKeys.length; i++) { 744 actualKeys[i] = actual.get(i).getGroupKey(); 745 } 746 // We do not care about the order since they are not ranked yet. 747 Arrays.sort(actualKeys); 748 Arrays.sort(expectedKeys); 749 assertThat(actualKeys).isEqualTo(expectedKeys); 750 } 751 752 @Test onAdditionalGroupAndRank_isGroupSummary_maintainsPreviousRanking()753 public void onAdditionalGroupAndRank_isGroupSummary_maintainsPreviousRanking() { 754 Map<String, AlertEntry> testCopy = new HashMap<>(mAlertEntriesMap); 755 // Seed the list with the notifications 756 mPreprocessingManager.init(mAlertEntriesMap, mRankingMap); 757 758 String key = "NEW_KEY"; 759 String groupKey = "NEW_GROUP_KEY"; 760 Notification newNotification = generateNotification(/* isForeground= */ false, 761 /* isNavigation= */ false, /* isGroupSummary= */ true); 762 StatusBarNotification newSbn = mock(StatusBarNotification.class); 763 when(newSbn.getNotification()).thenReturn(newNotification); 764 when(newSbn.getKey()).thenReturn(key); 765 when(newSbn.getGroupKey()).thenReturn(groupKey); 766 767 AlertEntry newEntry = new AlertEntry(newSbn); 768 769 // Change the ordering, add a new notification and validate that the existing 770 // notifications don't reorder 771 AlertEntry first = mAlertEntries.get(0); 772 mAlertEntries.remove(0); 773 mAlertEntries.add(first); 774 775 List<NotificationGroup> additionalRanked = mPreprocessingManager.additionalGroupAndRank( 776 newEntry, generateRankingMap(mAlertEntries), /* isUpdate= */ false) 777 .stream() 778 .filter(g -> !g.getGroupKey().equals(groupKey)) 779 .collect(Collectors.toList()); 780 781 List<NotificationGroup> standardRanked = mPreprocessingManager.rank( 782 mPreprocessingManager.process(/* showLessImportantNotifications = */ false, 783 testCopy, mRankingMap), mRankingMap); 784 785 assertThat(additionalRanked.size()).isEqualTo(standardRanked.size()); 786 787 for (int i = 0; i < additionalRanked.size(); i++) { 788 assertThat(additionalRanked.get(i).getGroupKey()).isEqualTo( 789 standardRanked.get(i).getGroupKey()); 790 } 791 } 792 793 @Test onAdditionalGroupAndRank_isGroupSummary_prependsHighRankNotification()794 public void onAdditionalGroupAndRank_isGroupSummary_prependsHighRankNotification() { 795 // Seed the list 796 mPreprocessingManager.init(mAlertEntriesMap, mRankingMap); 797 798 String key = "NEW_KEY"; 799 String groupKey = "NEW_GROUP_KEY"; 800 Notification newNotification = generateNotification(/* isForeground= */ false, 801 /* isNavigation= */ false, /* isGroupSummary= */ true); 802 StatusBarNotification newSbn = mock(StatusBarNotification.class); 803 when(newSbn.getNotification()).thenReturn(newNotification); 804 when(newSbn.getKey()).thenReturn(key); 805 when(newSbn.getGroupKey()).thenReturn(groupKey); 806 807 AlertEntry newEntry = new AlertEntry(newSbn); 808 mAlertEntries.add(newEntry); 809 810 List<NotificationGroup> result = mPreprocessingManager.additionalGroupAndRank(newEntry, 811 generateRankingMap(mAlertEntries), /* isUpdate= */ false); 812 assertThat(result.get(0).getSingleNotification()).isEqualTo(newEntry); 813 } 814 815 @Test onAdditionalGroupAndRank_notGroupSummary_isUpdate_notificationUpdated()816 public void onAdditionalGroupAndRank_notGroupSummary_isUpdate_notificationUpdated() { 817 when(mNotificationDataManager.isNotificationSeen(mImportantForeground)).thenReturn(false); 818 // Seed the list 819 mPreprocessingManager.init(mAlertEntriesMap, mRankingMap); 820 String key = mImportantForeground.getKey(); 821 String groupKey = mImportantForeground.getStatusBarNotification().getGroupKey(); 822 Notification newNotification = generateNotification(/* isForeground= */ true, 823 /* isNavigation= */ false, /* isGroupSummary= */ false); 824 StatusBarNotification newSbn = mock(StatusBarNotification.class); 825 when(newSbn.getNotification()).thenReturn(newNotification); 826 when(newSbn.getKey()).thenReturn(key); 827 when(newSbn.getGroupKey()).thenReturn(groupKey); 828 when(newSbn.getId()).thenReturn(123); 829 AlertEntry newEntry = new AlertEntry(newSbn); 830 831 List<NotificationGroup> result = mPreprocessingManager.additionalGroupAndRank(newEntry, 832 generateRankingMap(mAlertEntries), /* isUpdate= */ true); 833 834 assertThat(result.get(0).getSingleNotification().getStatusBarNotification().getId()) 835 .isEqualTo(123); 836 } 837 838 @Test onAdditionalGroupAndRank_newNotification_setAsSeenInDataManger()839 public void onAdditionalGroupAndRank_newNotification_setAsSeenInDataManger() { 840 String key = "TEST_KEY"; 841 mPreprocessingManager.setNotificationDataManager(mNotificationDataManager); 842 mPreprocessingManager.init(mAlertEntriesMap, mRankingMap); 843 Notification newNotification = generateNotification(/* isForeground= */ false, 844 /* isNavigation= */ false, /* isGroupSummary= */ false); 845 StatusBarNotification newSbn = mock(StatusBarNotification.class); 846 when(newSbn.getNotification()).thenReturn(newNotification); 847 when(newSbn.getKey()).thenReturn(key); 848 when(newSbn.getGroupKey()).thenReturn("groupKey"); 849 AlertEntry newEntry = new AlertEntry(newSbn); 850 851 mPreprocessingManager.additionalGroupAndRank(newEntry, 852 generateRankingMap(mAlertEntries), /* isUpdate= */ false); 853 854 ArgumentCaptor<AlertEntry> arg = ArgumentCaptor.forClass(AlertEntry.class); 855 verify(mNotificationDataManager).setNotificationAsSeen(arg.capture()); 856 assertThat(arg.getValue().getKey()).isEqualTo(key); 857 } 858 859 @Test onAdditionalGroupAndRank_addToExistingGroup_groupSurpassGroupingThresholdExist()860 public void onAdditionalGroupAndRank_addToExistingGroup_groupSurpassGroupingThresholdExist() { 861 String key = "TEST_KEY"; 862 String groupKey = "TEST_GROUP_KEY"; 863 int numberOfGroupNotifications = 5; 864 mContext.getOrCreateTestableResources().addOverride( 865 R.integer.config_minimumGroupingThreshold, /* value= */ 4); 866 generateNotificationsWithSameGroupKey(numberOfGroupNotifications, groupKey); 867 generateGroupSummaryNotification(groupKey); 868 mPreprocessingManager.init(mAlertEntriesMap, mRankingMap); 869 Notification newNotification = generateNotification(/* isForeground= */ false, 870 /* isNavigation= */ false, /* isGroupSummary= */ false); 871 StatusBarNotification newSbn = mock(StatusBarNotification.class); 872 when(newSbn.getNotification()).thenReturn(newNotification); 873 when(newSbn.getKey()).thenReturn(key); 874 when(newSbn.getGroupKey()).thenReturn(groupKey); 875 AlertEntry newEntry = new AlertEntry(newSbn); 876 877 List<NotificationGroup> rawResult = mPreprocessingManager.additionalGroupAndRank(newEntry, 878 generateRankingMap(mAlertEntries), /* isUpdate= */ false); 879 880 List<NotificationGroup> resultNotificationGroups = rawResult.stream() 881 .filter(ng -> TextUtils.equals(ng.getGroupKey(), groupKey)) 882 .collect(Collectors.toList()); 883 assertThat(resultNotificationGroups.size()).isEqualTo(1); 884 List<AlertEntry> resultAlertEntries = resultNotificationGroups.get(0) 885 .getChildNotifications(); 886 assertThat(resultAlertEntries.size()).isEqualTo(numberOfGroupNotifications + 1); 887 assertThat(resultAlertEntries.get(resultAlertEntries.size() - 1).getKey()).isEqualTo(key); 888 } 889 890 @Test onAdditionalGroupAndRank_addNewNotification_notSurpassGroupingThreshold()891 public void onAdditionalGroupAndRank_addNewNotification_notSurpassGroupingThreshold() { 892 String key = "TEST_KEY"; 893 String groupKey = "TEST_GROUP_KEY"; 894 int numberOfGroupNotifications = 2; 895 mContext.getOrCreateTestableResources().addOverride( 896 R.integer.config_minimumGroupingThreshold, /* value= */ 4); 897 generateNotificationsWithSameGroupKey(numberOfGroupNotifications, groupKey); 898 generateGroupSummaryNotification(groupKey); 899 mPreprocessingManager.init(mAlertEntriesMap, mRankingMap); 900 Notification newNotification = generateNotification(/* isForeground= */ false, 901 /* isNavigation= */ false, /* isGroupSummary= */ false); 902 StatusBarNotification newSbn = mock(StatusBarNotification.class); 903 when(newSbn.getNotification()).thenReturn(newNotification); 904 when(newSbn.getKey()).thenReturn(key); 905 when(newSbn.getGroupKey()).thenReturn(groupKey); 906 AlertEntry newEntry = new AlertEntry(newSbn); 907 908 List<NotificationGroup> rawResult = mPreprocessingManager.additionalGroupAndRank(newEntry, 909 generateRankingMap(mAlertEntries), /* isUpdate= */ false); 910 911 List<NotificationGroup> resultNotificationGroups = rawResult.stream() 912 .filter(ng -> TextUtils.equals(ng.getGroupKey(), groupKey)) 913 .collect(Collectors.toList()); 914 assertThat(resultNotificationGroups.size()).isEqualTo(numberOfGroupNotifications + 1); 915 } 916 917 @Test onAdditionalGroupAndRank_createsNewGroup_surpassGroupingThreshold()918 public void onAdditionalGroupAndRank_createsNewGroup_surpassGroupingThreshold() { 919 String key = "TEST_KEY"; 920 String groupKey = "TEST_GROUP_KEY"; 921 int numberOfGroupNotifications = 3; 922 mContext.getOrCreateTestableResources().addOverride( 923 R.integer.config_minimumGroupingThreshold, /* value= */ 4); 924 generateNotificationsWithSameGroupKey(numberOfGroupNotifications, groupKey); 925 generateGroupSummaryNotification(groupKey); 926 mPreprocessingManager.init(mAlertEntriesMap, mRankingMap); 927 Notification newNotification = generateNotification(/* isForeground= */ false, 928 /* isNavigation= */ false, /* isGroupSummary= */ false); 929 StatusBarNotification newSbn = mock(StatusBarNotification.class); 930 when(newSbn.getNotification()).thenReturn(newNotification); 931 when(newSbn.getKey()).thenReturn(key); 932 when(newSbn.getGroupKey()).thenReturn(groupKey); 933 AlertEntry newEntry = new AlertEntry(newSbn); 934 935 List<NotificationGroup> rawResult = mPreprocessingManager.additionalGroupAndRank(newEntry, 936 generateRankingMap(mAlertEntries), /* isUpdate= */ false); 937 938 List<NotificationGroup> resultNotificationGroups = rawResult.stream() 939 .filter(ng -> TextUtils.equals(ng.getGroupKey(), groupKey)) 940 .collect(Collectors.toList()); 941 assertThat(resultNotificationGroups.size()).isEqualTo(1); 942 assertThat(resultNotificationGroups.get(0).getChildCount()) 943 .isEqualTo(numberOfGroupNotifications + 1); 944 } 945 946 @Test onAdditionalGroupAndRank_doesNotGroup_groupSummaryMissing()947 public void onAdditionalGroupAndRank_doesNotGroup_groupSummaryMissing() { 948 String key = "TEST_KEY"; 949 String groupKey = "TEST_GROUP_KEY"; 950 int numberOfGroupNotifications = 3; 951 mContext.getOrCreateTestableResources().addOverride( 952 R.integer.config_minimumGroupingThreshold, /* value= */ 4); 953 generateNotificationsWithSameGroupKey(numberOfGroupNotifications, groupKey); 954 mPreprocessingManager.init(mAlertEntriesMap, mRankingMap); 955 Notification newNotification = generateNotification(/* isForeground= */ false, 956 /* isNavigation= */ false, /* isGroupSummary= */ false); 957 StatusBarNotification newSbn = mock(StatusBarNotification.class); 958 when(newSbn.getNotification()).thenReturn(newNotification); 959 when(newSbn.getKey()).thenReturn(key); 960 when(newSbn.getGroupKey()).thenReturn(groupKey); 961 AlertEntry newEntry = new AlertEntry(newSbn); 962 963 List<NotificationGroup> rawResult = mPreprocessingManager.additionalGroupAndRank(newEntry, 964 generateRankingMap(mAlertEntries), /* isUpdate= */ false); 965 966 List<NotificationGroup> resultNotificationGroups = rawResult.stream() 967 .filter(ng -> TextUtils.equals(ng.getGroupKey(), groupKey)) 968 .collect(Collectors.toList()); 969 assertThat(resultNotificationGroups.size()).isEqualTo(numberOfGroupNotifications + 1); 970 } 971 972 @Test onUpdateNotifications_notificationRemoved_removesNotification()973 public void onUpdateNotifications_notificationRemoved_removesNotification() { 974 mPreprocessingManager.init(mAlertEntriesMap, mRankingMap); 975 976 List<NotificationGroup> newList = 977 mPreprocessingManager.updateNotifications( 978 /* showLessImportantNotifications= */ false, 979 mImportantForeground, 980 CarNotificationListener.NOTIFY_NOTIFICATION_REMOVED, 981 mRankingMap); 982 983 assertThat(mPreprocessingManager.getOldNotifications().containsKey( 984 mImportantForeground.getKey())).isFalse(); 985 } 986 987 @Test onUpdateNotification_notificationPosted_isUpdate_putsNotification()988 public void onUpdateNotification_notificationPosted_isUpdate_putsNotification() { 989 mPreprocessingManager.init(mAlertEntriesMap, mRankingMap); 990 int beforeSize = mPreprocessingManager.getOldNotifications().size(); 991 Notification newNotification = new Notification.Builder(mContext, CHANNEL_ID) 992 .setContentTitle("NEW_TITLE") 993 .setGroup(OVERRIDE_GROUP_KEY) 994 .setGroupSummary(false) 995 .build(); 996 newNotification.category = Notification.CATEGORY_NAVIGATION; 997 when(mImportantForeground.getStatusBarNotification().getNotification()) 998 .thenReturn(newNotification); 999 List<NotificationGroup> newList = 1000 mPreprocessingManager.updateNotifications( 1001 /* showLessImportantNotifications= */ false, 1002 mImportantForeground, 1003 CarNotificationListener.NOTIFY_NOTIFICATION_POSTED, 1004 mRankingMap); 1005 1006 int afterSize = mPreprocessingManager.getOldNotifications().size(); 1007 AlertEntry updated = (AlertEntry) mPreprocessingManager.getOldNotifications().get( 1008 mImportantForeground.getKey()); 1009 assertThat(updated).isNotNull(); 1010 assertThat(updated.getNotification().category).isEqualTo(Notification.CATEGORY_NAVIGATION); 1011 assertThat(afterSize).isEqualTo(beforeSize); 1012 } 1013 1014 @Test onUpdateNotification_notificationPosted_isNotUpdate_addsNotification()1015 public void onUpdateNotification_notificationPosted_isNotUpdate_addsNotification() { 1016 mPreprocessingManager.init(mAlertEntriesMap, mRankingMap); 1017 int beforeSize = mPreprocessingManager.getOldNotifications().size(); 1018 Notification additionalNotification = generateNotification(/* isForeground= */ true, 1019 /* isNavigation= */ false, /* isGroupSummary= */ true); 1020 additionalNotification.category = Notification.CATEGORY_MESSAGE; 1021 when(mAdditionalStatusBarNotification.getKey()).thenReturn("ADDITIONAL"); 1022 when(mAdditionalStatusBarNotification.getGroupKey()).thenReturn(GROUP_KEY_C); 1023 when(mAdditionalStatusBarNotification.getNotification()).thenReturn(additionalNotification); 1024 AlertEntry additionalAlertEntry = new AlertEntry(mAdditionalStatusBarNotification); 1025 1026 List<NotificationGroup> newList = 1027 mPreprocessingManager.updateNotifications( 1028 /* showLessImportantNotifications= */ false, 1029 additionalAlertEntry, 1030 CarNotificationListener.NOTIFY_NOTIFICATION_POSTED, 1031 mRankingMap); 1032 1033 int afterSize = mPreprocessingManager.getOldNotifications().size(); 1034 AlertEntry posted = (AlertEntry) mPreprocessingManager.getOldNotifications().get( 1035 additionalAlertEntry.getKey()); 1036 assertThat(posted).isNotNull(); 1037 assertThat(posted.getKey()).isEqualTo("ADDITIONAL"); 1038 assertThat(afterSize).isEqualTo(beforeSize + 1); 1039 } 1040 setConfig(boolean recentOld, boolean launcherIcon, int groupThreshold)1041 private void setConfig(boolean recentOld, boolean launcherIcon, int groupThreshold) { 1042 TestableResources testableResources = mContext.getOrCreateTestableResources(); 1043 testableResources.removeOverride(R.bool.config_showRecentAndOldHeaders); 1044 testableResources.removeOverride(R.bool.config_useLauncherIcon); 1045 testableResources.removeOverride(R.integer.config_minimumGroupingThreshold); 1046 testableResources.addOverride(R.bool.config_showRecentAndOldHeaders, recentOld); 1047 testableResources.addOverride(R.bool.config_useLauncherIcon, launcherIcon); 1048 testableResources.addOverride(R.integer.config_minimumGroupingThreshold, groupThreshold); 1049 } 1050 1051 /** 1052 * Wraps StatusBarNotifications with AlertEntries and generates AlertEntriesMap and 1053 * RankingsMap. 1054 */ initTestData(boolean includeAdditionalNotifs)1055 private void initTestData(boolean includeAdditionalNotifs) { 1056 mAlertEntries = new ArrayList<>(); 1057 mLessImportantBackground = new AlertEntry(mStatusBarNotification1); 1058 mLessImportantForeground = new AlertEntry(mStatusBarNotification2); 1059 mMedia = new AlertEntry(mStatusBarNotification3); 1060 mNavigation = new AlertEntry(mStatusBarNotification4); 1061 mImportantBackground = new AlertEntry(mStatusBarNotification5); 1062 mImportantForeground = new AlertEntry(mStatusBarNotification6); 1063 if (includeAdditionalNotifs) { 1064 mImportantForeground2 = new AlertEntry(mStatusBarNotification7); 1065 mImportantForeground3 = new AlertEntry(mStatusBarNotification8); 1066 mImportantForeground4 = new AlertEntry(mStatusBarNotification9); 1067 mImportantForeground5 = new AlertEntry(mStatusBarNotification10); 1068 mImportantForeground6 = new AlertEntry(mStatusBarNotification11); 1069 mImportantForeground7 = new AlertEntry(mStatusBarNotification12); 1070 } 1071 mAlertEntries.add(mLessImportantBackground); 1072 mAlertEntries.add(mLessImportantForeground); 1073 mAlertEntries.add(mMedia); 1074 mAlertEntries.add(mNavigation); 1075 mAlertEntries.add(mImportantBackground); 1076 mAlertEntries.add(mImportantForeground); 1077 if (includeAdditionalNotifs) { 1078 mAlertEntries.add(mImportantForeground2); 1079 mAlertEntries.add(mImportantForeground3); 1080 mAlertEntries.add(mImportantForeground4); 1081 mAlertEntries.add(mImportantForeground5); 1082 mAlertEntries.add(mImportantForeground6); 1083 mAlertEntries.add(mImportantForeground7); 1084 } 1085 mAlertEntriesMap = new HashMap<>(); 1086 mAlertEntriesMap.put(mLessImportantBackground.getKey(), mLessImportantBackground); 1087 mAlertEntriesMap.put(mLessImportantForeground.getKey(), mLessImportantForeground); 1088 mAlertEntriesMap.put(mMedia.getKey(), mMedia); 1089 mAlertEntriesMap.put(mNavigation.getKey(), mNavigation); 1090 mAlertEntriesMap.put(mImportantBackground.getKey(), mImportantBackground); 1091 mAlertEntriesMap.put(mImportantForeground.getKey(), mImportantForeground); 1092 if (includeAdditionalNotifs) { 1093 mAlertEntriesMap.put(mImportantForeground2.getKey(), mImportantForeground2); 1094 mAlertEntriesMap.put(mImportantForeground3.getKey(), mImportantForeground3); 1095 mAlertEntriesMap.put(mImportantForeground4.getKey(), mImportantForeground4); 1096 mAlertEntriesMap.put(mImportantForeground5.getKey(), mImportantForeground5); 1097 mAlertEntriesMap.put(mImportantForeground6.getKey(), mImportantForeground6); 1098 mAlertEntriesMap.put(mImportantForeground7.getKey(), mImportantForeground7); 1099 } 1100 mRankingMap = generateRankingMap(mAlertEntries); 1101 } 1102 getEmptyAutoGeneratedGroupSummary()1103 private AlertEntry getEmptyAutoGeneratedGroupSummary() { 1104 Notification notification = new Notification.Builder(mContext, CHANNEL_ID) 1105 .setContentTitle(CONTENT_TITLE) 1106 .setSmallIcon(android.R.drawable.sym_def_app_icon) 1107 .setGroup(OVERRIDE_GROUP_KEY) 1108 .setGroupSummary(true) 1109 .build(); 1110 StatusBarNotification statusBarNotification = new StatusBarNotification( 1111 PKG, OP_PKG, ID, TAG, UID, INITIAL_PID, notification, USER_HANDLE, 1112 OVERRIDE_GROUP_KEY, POST_TIME); 1113 statusBarNotification.setOverrideGroupKey(OVERRIDE_GROUP_KEY); 1114 1115 return new AlertEntry(statusBarNotification); 1116 } 1117 generateNotification(boolean isForeground, boolean isNavigation, boolean isGroupSummary)1118 private Notification generateNotification(boolean isForeground, boolean isNavigation, 1119 boolean isGroupSummary) { 1120 Notification notification = new Notification.Builder(mContext, CHANNEL_ID) 1121 .setContentTitle(CONTENT_TITLE) 1122 .setSmallIcon(android.R.drawable.sym_def_app_icon) 1123 .setGroup(OVERRIDE_GROUP_KEY) 1124 .setGroupSummary(isGroupSummary) 1125 .build(); 1126 1127 if (isForeground) { 1128 // this will reset flags previously set like FLAG_GROUP_SUMMARY 1129 notification.flags = Notification.FLAG_FOREGROUND_SERVICE; 1130 } 1131 1132 if (isNavigation) { 1133 notification.category = Notification.CATEGORY_NAVIGATION; 1134 } 1135 return notification; 1136 } 1137 generateStringOfLength(int length)1138 private String generateStringOfLength(int length) { 1139 String string = ""; 1140 for (int i = 0; i < length; i++) { 1141 string += "*"; 1142 } 1143 1144 return string; 1145 } 1146 1147 /** 1148 * Ranks the provided alertEntries in reverse order. 1149 * 1150 * All methods that follow afterwards help assigning diverse attributes to the {@link 1151 * android.service.notification.NotificationListenerService.Ranking} instances. 1152 */ generateRankingMap( List<AlertEntry> alertEntries)1153 private NotificationListenerService.RankingMap generateRankingMap( 1154 List<AlertEntry> alertEntries) { 1155 NotificationListenerService.Ranking[] rankings = 1156 new NotificationListenerService.Ranking[alertEntries.size()]; 1157 for (int i = 0; i < alertEntries.size(); i++) { 1158 String key = alertEntries.get(i).getKey(); 1159 int rank = alertEntries.size() - i; // ranking in reverse order; 1160 NotificationListenerService.Ranking ranking = new NotificationListenerService.Ranking(); 1161 ranking.populate( 1162 key, 1163 rank, 1164 !isIntercepted(i), 1165 getVisibilityOverride(i), 1166 getSuppressedVisualEffects(i), 1167 getImportance(i), 1168 getExplanation(key), 1169 getOverrideGroupKey(key), 1170 getChannel(key, i), 1171 getPeople(key, i), 1172 getSnoozeCriteria(key, i), 1173 getShowBadge(i), 1174 getUserSentiment(i), 1175 getHidden(i), 1176 lastAudiblyAlerted(i), 1177 getNoisy(i), 1178 getSmartActions(key, i), 1179 getSmartReplies(key, i), 1180 canBubble(i), 1181 isVisuallyInterruptive(i), 1182 isConversation(i), 1183 null, 1184 getRankingAdjustment(i), 1185 isBubble(i), 1186 0 1187 ); 1188 rankings[i] = ranking; 1189 } 1190 1191 NotificationListenerService.RankingMap rankingMap 1192 = new NotificationListenerService.RankingMap(rankings); 1193 1194 return rankingMap; 1195 } 1196 generateNotificationsWithSameGroupKey(int numberOfNotifications, String groupKey)1197 private void generateNotificationsWithSameGroupKey(int numberOfNotifications, String groupKey) { 1198 for (int i = 0; i < numberOfNotifications; i++) { 1199 String key = "BASE_KEY_" + i; 1200 Notification notification = generateNotification(/* isForeground= */ false, 1201 /* isNavigation= */ false, /* isGroupSummary= */ false); 1202 StatusBarNotification sbn = mock(StatusBarNotification.class); 1203 when(sbn.getNotification()).thenReturn(notification); 1204 when(sbn.getKey()).thenReturn(key); 1205 when(sbn.getGroupKey()).thenReturn(groupKey); 1206 AlertEntry alertEntry = new AlertEntry(sbn); 1207 mAlertEntries.add(alertEntry); 1208 mAlertEntriesMap.put(alertEntry.getKey(), alertEntry); 1209 } 1210 } 1211 generateGroupSummaryNotification(String groupKey)1212 private void generateGroupSummaryNotification(String groupKey) { 1213 Notification groupSummary = generateNotification(/* isForeground= */ false, 1214 /* isNavigation= */ false, /* isGroupSummary= */ true); 1215 StatusBarNotification sbn = mock(StatusBarNotification.class); 1216 when(sbn.getNotification()).thenReturn(groupSummary); 1217 when(sbn.getKey()).thenReturn("KEY_GROUP_SUMMARY"); 1218 when(sbn.getGroupKey()).thenReturn(groupKey); 1219 AlertEntry alertEntry = new AlertEntry(sbn); 1220 mAlertEntries.add(alertEntry); 1221 mAlertEntriesMap.put(alertEntry.getKey(), alertEntry); 1222 } 1223 getVisibilityOverride(int index)1224 private int getVisibilityOverride(int index) { 1225 return index * 9; 1226 } 1227 getOverrideGroupKey(String key)1228 private String getOverrideGroupKey(String key) { 1229 return key + key; 1230 } 1231 isIntercepted(int index)1232 private boolean isIntercepted(int index) { 1233 return index % 2 == 0; 1234 } 1235 getSuppressedVisualEffects(int index)1236 private int getSuppressedVisualEffects(int index) { 1237 return index * 2; 1238 } 1239 getImportance(int index)1240 private int getImportance(int index) { 1241 return index; 1242 } 1243 getExplanation(String key)1244 private String getExplanation(String key) { 1245 return key + "explain"; 1246 } 1247 getChannel(String key, int index)1248 private NotificationChannel getChannel(String key, int index) { 1249 return new NotificationChannel(key, key, getImportance(index)); 1250 } 1251 getShowBadge(int index)1252 private boolean getShowBadge(int index) { 1253 return index % 3 == 0; 1254 } 1255 getUserSentiment(int index)1256 private int getUserSentiment(int index) { 1257 switch (index % 3) { 1258 case 0: 1259 return USER_SENTIMENT_NEGATIVE; 1260 case 1: 1261 return USER_SENTIMENT_NEUTRAL; 1262 case 2: 1263 return USER_SENTIMENT_POSITIVE; 1264 } 1265 return USER_SENTIMENT_NEUTRAL; 1266 } 1267 getHidden(int index)1268 private boolean getHidden(int index) { 1269 return index % 2 == 0; 1270 } 1271 lastAudiblyAlerted(int index)1272 private long lastAudiblyAlerted(int index) { 1273 return index * 2000; 1274 } 1275 getNoisy(int index)1276 private boolean getNoisy(int index) { 1277 return index < 1; 1278 } 1279 getPeople(String key, int index)1280 private ArrayList<String> getPeople(String key, int index) { 1281 ArrayList<String> people = new ArrayList<>(); 1282 for (int i = 0; i < index; i++) { 1283 people.add(i + key); 1284 } 1285 return people; 1286 } 1287 getSnoozeCriteria(String key, int index)1288 private ArrayList<SnoozeCriterion> getSnoozeCriteria(String key, int index) { 1289 ArrayList<SnoozeCriterion> snooze = new ArrayList<>(); 1290 for (int i = 0; i < index; i++) { 1291 snooze.add(new SnoozeCriterion(key + i, getExplanation(key), key)); 1292 } 1293 return snooze; 1294 } 1295 getSmartActions(String key, int index)1296 private ArrayList<Notification.Action> getSmartActions(String key, int index) { 1297 ArrayList<Notification.Action> actions = new ArrayList<>(); 1298 for (int i = 0; i < index; i++) { 1299 PendingIntent intent = PendingIntent.getBroadcast( 1300 mContext, 1301 index /*requestCode*/, 1302 new Intent("ACTION_" + key), 1303 FLAG_IMMUTABLE); 1304 actions.add(new Notification.Action.Builder(null /*icon*/, key, intent).build()); 1305 } 1306 return actions; 1307 } 1308 getSmartReplies(String key, int index)1309 private ArrayList<CharSequence> getSmartReplies(String key, int index) { 1310 ArrayList<CharSequence> choices = new ArrayList<>(); 1311 for (int i = 0; i < index; i++) { 1312 choices.add("choice_" + key + "_" + i); 1313 } 1314 return choices; 1315 } 1316 canBubble(int index)1317 private boolean canBubble(int index) { 1318 return index % 4 == 0; 1319 } 1320 isVisuallyInterruptive(int index)1321 private boolean isVisuallyInterruptive(int index) { 1322 return index % 4 == 0; 1323 } 1324 isConversation(int index)1325 private boolean isConversation(int index) { 1326 return index % 4 == 0; 1327 } 1328 getRankingAdjustment(int index)1329 private int getRankingAdjustment(int index) { 1330 return index % 3 - 1; 1331 } 1332 isBubble(int index)1333 private boolean isBubble(int index) { 1334 return index % 4 == 0; 1335 } 1336 } 1337