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