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