1 /*
2  * Copyright (C) 2014 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 package com.android.server.notification;
17 
18 import static android.app.NotificationManager.IMPORTANCE_DEFAULT;
19 import static android.app.NotificationManager.IMPORTANCE_LOW;
20 
21 import static android.platform.test.flag.junit.SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT;
22 import static com.google.common.truth.Truth.assertThat;
23 import static junit.framework.TestCase.assertEquals;
24 
25 import static org.junit.Assert.assertTrue;
26 import static org.mockito.ArgumentMatchers.any;
27 import static org.mockito.Matchers.anyInt;
28 import static org.mockito.Matchers.eq;
29 import static org.mockito.Mockito.mock;
30 import static org.mockito.Mockito.when;
31 
32 import android.app.Flags;
33 import android.app.Notification;
34 import android.app.NotificationChannel;
35 import android.app.NotificationManager;
36 import android.content.ContentProvider;
37 import android.content.Context;
38 import android.content.IContentProvider;
39 import android.content.pm.ApplicationInfo;
40 import android.content.pm.PackageInfo;
41 import android.content.pm.PackageManager;
42 import android.content.pm.Signature;
43 import android.media.AudioAttributes;
44 import android.net.Uri;
45 import android.os.Build;
46 import android.os.UserHandle;
47 import android.os.Vibrator;
48 import android.platform.test.annotations.DisableFlags;
49 import android.platform.test.annotations.EnableFlags;
50 import android.platform.test.flag.junit.SetFlagsRule;
51 import android.service.notification.StatusBarNotification;
52 import android.testing.TestableContentResolver;
53 
54 import androidx.test.InstrumentationRegistry;
55 import androidx.test.filters.SmallTest;
56 import androidx.test.runner.AndroidJUnit4;
57 
58 import com.android.internal.compat.IPlatformCompat;
59 import com.android.server.UiServiceTestCase;
60 
61 import org.junit.Before;
62 import org.junit.Rule;
63 import org.junit.Test;
64 import org.junit.runner.RunWith;
65 import org.mockito.Mock;
66 import org.mockito.MockitoAnnotations;
67 
68 import java.util.ArrayList;
69 import java.util.Collections;
70 import java.util.List;
71 
72 @SmallTest
73 @RunWith(AndroidJUnit4.class)
74 public class RankingHelperTest extends UiServiceTestCase {
75     private static final String UPDATED_PKG = "updatedmPkg";
76     private static final int UID2 = 1111;
77     private static final String SYSTEM_PKG = "android";
78     private static final int SYSTEM_UID= 1000;
79     private static final String TEST_CHANNEL_ID = "test_channel_id";
80     private static final String TEST_AUTHORITY = "test";
81     private static final Uri SOUND_URI =
82             Uri.parse("content://" + TEST_AUTHORITY + "/internal/audio/media/10");
83     private static final Uri CANONICAL_SOUND_URI =
84             Uri.parse("content://" + TEST_AUTHORITY
85                     + "/internal/audio/media/10?title=Test&canonical=1");
86 
87     @Mock NotificationUsageStats mUsageStats;
88     @Mock RankingHandler mHandler;
89     @Mock PackageManager mPm;
90     @Mock IContentProvider mTestIContentProvider;
91     @Mock Context mContext;
92     @Mock ZenModeHelper mMockZenModeHelper;
93     @Mock RankingConfig mConfig;
94     @Mock Vibrator mVibrator;
95 
96     private NotificationManager.Policy mTestNotificationPolicy;
97     private Notification mNotiGroupGSortA;
98     private Notification mNotiGroupGSortB;
99     private Notification mNotiNoGroup;
100     private Notification mNotiNoGroup2;
101     private Notification mNotiNoGroupSortA;
102     private NotificationRecord mRecordGroupGSortA;
103     private NotificationRecord mRecordGroupGSortB;
104     private NotificationRecord mRecordNoGroup;
105     private NotificationRecord mRecordNoGroup2;
106     private NotificationRecord mRecordNoGroupSortA;
107     private NotificationRecord mRecentlyIntrusive;
108     private NotificationRecord mNewest;
109     private RankingHelper mHelper;
110 
111     @Rule
112     public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(DEVICE_DEFAULT);
113 
114     @Before
setUp()115     public void setUp() throws Exception {
116         MockitoAnnotations.initMocks(this);
117         UserHandle mUser = UserHandle.ALL;
118 
119         final ApplicationInfo legacy = new ApplicationInfo();
120         legacy.targetSdkVersion = Build.VERSION_CODES.N_MR1;
121         final ApplicationInfo upgrade = new ApplicationInfo();
122         upgrade.targetSdkVersion = Build.VERSION_CODES.O;
123         when(mPm.getApplicationInfoAsUser(eq(mPkg), anyInt(), anyInt())).thenReturn(legacy);
124         when(mPm.getApplicationInfoAsUser(eq(UPDATED_PKG), anyInt(), anyInt())).thenReturn(upgrade);
125         when(mPm.getApplicationInfoAsUser(eq(SYSTEM_PKG), anyInt(), anyInt())).thenReturn(upgrade);
126         when(mPm.getPackageUidAsUser(eq(mPkg), anyInt())).thenReturn(mUid);
127         when(mPm.getPackageUidAsUser(eq(UPDATED_PKG), anyInt())).thenReturn(UID2);
128         when(mPm.getPackageUidAsUser(eq(SYSTEM_PKG), anyInt())).thenReturn(SYSTEM_UID);
129         PackageInfo info = mock(PackageInfo.class);
130         info.signatures = new Signature[] {mock(Signature.class)};
131         when(mPm.getPackageInfoAsUser(eq(SYSTEM_PKG), anyInt(), anyInt())).thenReturn(info);
132         when(mPm.getPackageInfoAsUser(eq(mPkg), anyInt(), anyInt()))
133                 .thenReturn(mock(PackageInfo.class));
134         when(mContext.getResources()).thenReturn(
135                 InstrumentationRegistry.getContext().getResources());
136         when(mContext.getContentResolver()).thenReturn(
137                 InstrumentationRegistry.getContext().getContentResolver());
138         when(mContext.getPackageManager()).thenReturn(mPm);
139         when(mContext.getApplicationInfo()).thenReturn(legacy);
140         when(mContext.getSystemService(Vibrator.class)).thenReturn(mVibrator);
141         TestableContentResolver contentResolver = getContext().getContentResolver();
142         contentResolver.setFallbackToExisting(false);
143 
144         ContentProvider testContentProvider = mock(ContentProvider.class);
145         when(testContentProvider.getIContentProvider()).thenReturn(mTestIContentProvider);
146         contentResolver.addProvider(TEST_AUTHORITY, testContentProvider);
147 
148         when(mTestIContentProvider.canonicalize(any(), eq(SOUND_URI)))
149                 .thenReturn(CANONICAL_SOUND_URI);
150         when(mTestIContentProvider.canonicalize(any(), eq(CANONICAL_SOUND_URI)))
151                 .thenReturn(CANONICAL_SOUND_URI);
152         when(mTestIContentProvider.uncanonicalize(any(), eq(CANONICAL_SOUND_URI)))
153                 .thenReturn(SOUND_URI);
154 
155         mTestNotificationPolicy = new NotificationManager.Policy(0, 0, 0, 0,
156                 NotificationManager.Policy.STATE_CHANNELS_BYPASSING_DND, 0);
157         when(mMockZenModeHelper.getNotificationPolicy()).thenReturn(mTestNotificationPolicy);
158         mHelper = new RankingHelper(getContext(), mHandler, mConfig, mMockZenModeHelper,
159                 mUsageStats, new String[] {ImportanceExtractor.class.getName()},
160                 mock(IPlatformCompat.class));
161 
162         mNotiGroupGSortA = new Notification.Builder(mContext, TEST_CHANNEL_ID)
163                 .setContentTitle("A")
164                 .setGroup("G")
165                 .setSortKey("A")
166                 .setWhen(1205)
167                 .build();
168         mRecordGroupGSortA = new NotificationRecord(mContext, new StatusBarNotification(
169                 mPkg, mPkg, 1, null, 0, 0, mNotiGroupGSortA, mUser,
170                 null, System.currentTimeMillis()), getLowChannel());
171 
172         mNotiGroupGSortB = new Notification.Builder(mContext, TEST_CHANNEL_ID)
173                 .setContentTitle("B")
174                 .setGroup("G")
175                 .setSortKey("B")
176                 .setWhen(1200)
177                 .build();
178         mRecordGroupGSortB = new NotificationRecord(mContext, new StatusBarNotification(
179                 mPkg, mPkg, 1, null, 0, 0, mNotiGroupGSortB, mUser,
180                 null, System.currentTimeMillis()), getLowChannel());
181 
182         mNotiNoGroup = new Notification.Builder(mContext, TEST_CHANNEL_ID)
183                 .setContentTitle("C")
184                 .setWhen(1201)
185                 .build();
186         mRecordNoGroup = new NotificationRecord(mContext, new StatusBarNotification(
187                 mPkg, mPkg, 1, null, 0, 0, mNotiNoGroup, mUser,
188                 null, System.currentTimeMillis()), getLowChannel());
189 
190         mNotiNoGroup2 = new Notification.Builder(mContext, TEST_CHANNEL_ID)
191                 .setContentTitle("D")
192                 .setWhen(1202)
193                 .build();
194         mRecordNoGroup2 = new NotificationRecord(mContext, new StatusBarNotification(
195                 mPkg, mPkg, 1, null, 0, 0, mNotiNoGroup2, mUser,
196                 null, System.currentTimeMillis()), getLowChannel());
197 
198         mNotiNoGroupSortA = new Notification.Builder(mContext, TEST_CHANNEL_ID)
199                 .setContentTitle("E")
200                 .setWhen(1201)
201                 .setSortKey("A")
202                 .build();
203         mRecordNoGroupSortA = new NotificationRecord(mContext, new StatusBarNotification(
204                 mPkg, mPkg, 1, null, 0, 0, mNotiNoGroupSortA, mUser,
205                 null, System.currentTimeMillis()), getLowChannel());
206 
207         Notification n = new Notification.Builder(mContext, TEST_CHANNEL_ID)
208                 .setContentTitle("D")
209                 .build();
210         mRecentlyIntrusive = new NotificationRecord(mContext, new StatusBarNotification(
211                 mPkg, mPkg, 1, null, 0, 0, n, mUser,
212                 null, 100), getDefaultChannel());
213         mRecentlyIntrusive.setRecentlyIntrusive(true);
214 
215         mNewest = new NotificationRecord(mContext, new StatusBarNotification(
216                 mPkg, mPkg, 2, null, 0, 0, n, mUser,
217                 null, 10000), getDefaultChannel());
218     }
219 
getLowChannel()220     private NotificationChannel getLowChannel() {
221         return new NotificationChannel(NotificationChannel.DEFAULT_CHANNEL_ID, "name",
222                 IMPORTANCE_LOW);
223     }
224 
getDefaultChannel()225     private NotificationChannel getDefaultChannel() {
226         return new NotificationChannel(NotificationChannel.DEFAULT_CHANNEL_ID, "name",
227                 IMPORTANCE_DEFAULT);
228     }
229 
230     @Test
testSortShouldRespectCritical()231     public void testSortShouldRespectCritical() throws Exception {
232         ArrayList<NotificationRecord> notificationList = new ArrayList<NotificationRecord>(7);
233         NotificationRecord critical = generateRecord(0);
234         NotificationRecord critical_ish = generateRecord(1);
235         NotificationRecord critical_notAtAll = generateRecord(100);
236 
237         notificationList.add(critical_ish);
238         notificationList.add(mRecordGroupGSortA);
239         notificationList.add(critical_notAtAll);
240         notificationList.add(mRecordGroupGSortB);
241         notificationList.add(mRecordNoGroup);
242         notificationList.add(mRecordNoGroupSortA);
243         notificationList.add(critical);
244         mHelper.sort(notificationList);
245 
246         assertTrue(mHelper.indexOf(notificationList, critical) == 0);
247         assertTrue(mHelper.indexOf(notificationList, critical_ish) == 1);
248         assertTrue(mHelper.indexOf(notificationList, critical_notAtAll) == 6);
249     }
250 
generateRecord(int criticality)251     private NotificationRecord generateRecord(int criticality) {
252         NotificationChannel channel = new NotificationChannel("a", "a", IMPORTANCE_LOW);
253         final Notification.Builder builder = new Notification.Builder(getContext())
254                 .setContentTitle("foo")
255                 .setSmallIcon(android.R.drawable.sym_def_app_icon);
256         Notification n = builder.build();
257         StatusBarNotification sbn = new StatusBarNotification("", "", 0, "", 0,
258                 0, n, UserHandle.ALL, null, System.currentTimeMillis());
259         NotificationRecord notificationRecord = new NotificationRecord(getContext(), sbn, channel);
260         notificationRecord.setCriticality(criticality);
261         return notificationRecord;
262     }
263 
264     @Test
testFindAfterRankingWithASplitGroup()265     public void testFindAfterRankingWithASplitGroup() throws Exception {
266         ArrayList<NotificationRecord> notificationList = new ArrayList<NotificationRecord>(4);
267         notificationList.add(mRecordGroupGSortA);
268         notificationList.add(mRecordGroupGSortB);
269         notificationList.add(mRecordNoGroup);
270         notificationList.add(mRecordNoGroupSortA);
271         mHelper.sort(notificationList);
272         assertTrue(mHelper.indexOf(notificationList, mRecordGroupGSortA) >= 0);
273         assertTrue(mHelper.indexOf(notificationList, mRecordGroupGSortB) >= 0);
274         assertTrue(mHelper.indexOf(notificationList, mRecordNoGroup) >= 0);
275         assertTrue(mHelper.indexOf(notificationList, mRecordNoGroupSortA) >= 0);
276     }
277 
278     @Test
testSortShouldNotThrowWithPlainNotifications()279     public void testSortShouldNotThrowWithPlainNotifications() throws Exception {
280         ArrayList<NotificationRecord> notificationList = new ArrayList<NotificationRecord>(2);
281         notificationList.add(mRecordNoGroup);
282         notificationList.add(mRecordNoGroup2);
283         mHelper.sort(notificationList);
284     }
285 
286     @Test
testSortShouldNotThrowOneSorted()287     public void testSortShouldNotThrowOneSorted() throws Exception {
288         ArrayList<NotificationRecord> notificationList = new ArrayList<NotificationRecord>(2);
289         notificationList.add(mRecordNoGroup);
290         notificationList.add(mRecordNoGroupSortA);
291         mHelper.sort(notificationList);
292     }
293 
294     @Test
testSortShouldNotThrowOneNotification()295     public void testSortShouldNotThrowOneNotification() throws Exception {
296         ArrayList<NotificationRecord> notificationList = new ArrayList<NotificationRecord>(1);
297         notificationList.add(mRecordNoGroup);
298         mHelper.sort(notificationList);
299     }
300 
301     @Test
testSortShouldNotThrowOneSortKey()302     public void testSortShouldNotThrowOneSortKey() throws Exception {
303         ArrayList<NotificationRecord> notificationList = new ArrayList<NotificationRecord>(1);
304         notificationList.add(mRecordGroupGSortB);
305         mHelper.sort(notificationList);
306     }
307 
308     @Test
testSortShouldNotThrowOnEmptyList()309     public void testSortShouldNotThrowOnEmptyList() throws Exception {
310         ArrayList<NotificationRecord> notificationList = new ArrayList<NotificationRecord>();
311         mHelper.sort(notificationList);
312     }
313 
314     @Test
testGroupNotifications_highestIsProxy()315     public void testGroupNotifications_highestIsProxy() {
316         ArrayList<NotificationRecord> notificationList = new ArrayList<>();
317         // this should be the last in the list, except it's in a group with a high child
318         Notification lowSummaryN = new Notification.Builder(mContext, "")
319                 .setGroup("group")
320                 .setGroupSummary(true)
321                 .build();
322         NotificationRecord lowSummary = new NotificationRecord(mContext, new StatusBarNotification(
323                 mPkg, mPkg, 1, "summary", 0, 0, lowSummaryN, mUser,
324                 null, System.currentTimeMillis()), getLowChannel());
325         notificationList.add(lowSummary);
326 
327         Notification lowN = new Notification.Builder(mContext, "").build();
328         NotificationRecord low = new NotificationRecord(mContext, new StatusBarNotification(
329                 mPkg, mPkg, 1, "low", 0, 0, lowN, mUser,
330                 null, System.currentTimeMillis()), getLowChannel());
331         low.setContactAffinity(0.5f);
332         notificationList.add(low);
333 
334         Notification highChildN = new Notification.Builder(mContext, "")
335                 .setGroup("group")
336                 .setGroupSummary(false)
337                 .build();
338         NotificationRecord highChild = new NotificationRecord(mContext, new StatusBarNotification(
339                 mPkg, mPkg, 1, "child", 0, 0, highChildN, mUser,
340                 null, System.currentTimeMillis()), getDefaultChannel());
341         notificationList.add(highChild);
342 
343         mHelper.sort(notificationList);
344 
345         assertEquals(lowSummary, notificationList.get(0));
346         assertEquals(highChild, notificationList.get(1));
347         assertEquals(low, notificationList.get(2));
348     }
349 
350     @Test
351     @DisableFlags({android.app.Flags.FLAG_SORT_SECTION_BY_TIME})
testSortByIntrusivenessNotRecency()352     public void testSortByIntrusivenessNotRecency() {
353         ArrayList<NotificationRecord> expected = new ArrayList<>();
354         expected.add(mRecentlyIntrusive);
355         expected.add(mNewest);
356 
357         ArrayList<NotificationRecord> actual = new ArrayList<>();
358         actual.addAll(expected);
359         Collections.shuffle(actual);
360 
361         mHelper.sort(actual);
362         assertThat(actual).containsExactlyElementsIn(expected).inOrder();
363     }
364 
365     @Test
366     @EnableFlags({android.app.Flags.FLAG_SORT_SECTION_BY_TIME})
testSortByRecencyNotIntrusiveness()367     public void testSortByRecencyNotIntrusiveness() {
368         ArrayList<NotificationRecord> expected = new ArrayList<>();
369         expected.add(mNewest);
370         expected.add(mRecentlyIntrusive);
371 
372         ArrayList<NotificationRecord> actual = new ArrayList<>();
373         actual.addAll(expected);
374         Collections.shuffle(actual);
375 
376         mHelper.sort(actual);
377         assertThat(actual).containsExactlyElementsIn(expected).inOrder();
378     }
379 
380     @Test
381     @EnableFlags(android.app.Flags.FLAG_SORT_SECTION_BY_TIME)
testSort_oldWhenChildren_unspecifiedSummary()382     public void testSort_oldWhenChildren_unspecifiedSummary() {
383         NotificationRecord child1 = new NotificationRecord(mContext,
384                 new StatusBarNotification(
385                     mPkg, mPkg, 1, null, 0, 0,
386                         new Notification.Builder(mContext, TEST_CHANNEL_ID)
387                             .setGroup("G")
388                             .setWhen(1200)
389                             .build(),
390                         mUser, null, System.currentTimeMillis()), getLowChannel());
391         NotificationRecord child2 = new NotificationRecord(mContext,
392                 new StatusBarNotification(
393                         mPkg, mPkg, 2, null, 0, 0,
394                         new Notification.Builder(mContext, TEST_CHANNEL_ID)
395                                 .setGroup("G")
396                                 .setWhen(1300)
397                                 .build(),
398                         mUser, null, System.currentTimeMillis()), getLowChannel());
399         NotificationRecord summary = new NotificationRecord(mContext,
400                 new StatusBarNotification(
401                         mPkg, mPkg, 3, null, 0, 0,
402                         new Notification.Builder(mContext, TEST_CHANNEL_ID)
403                                 .setGroup("G")
404                                 .setGroupSummary(true)
405                                 .build(),
406                         mUser, null, System.currentTimeMillis()), getLowChannel());
407 
408         // in time slightly before the children, but much earlier than the summary.
409         // will only be sorted first if the summary is not the group proxy for group G.
410         NotificationRecord unrelated = new NotificationRecord(mContext,
411                 new StatusBarNotification(
412                         mPkg, mPkg, 11, null, 0, 0,
413                         new Notification.Builder(mContext, TEST_CHANNEL_ID)
414                                 .setWhen(1500)
415                                 .build(),
416                         mUser, null, System.currentTimeMillis()), getLowChannel());
417 
418         ArrayList<NotificationRecord> expected = new ArrayList<>();
419         expected.add(unrelated);
420         expected.add(summary);
421         expected.add(child2);
422         expected.add(child1);
423 
424         ArrayList<NotificationRecord> actual = new ArrayList<>();
425         actual.addAll(expected);
426         Collections.shuffle(actual);
427 
428         mHelper.sort(actual);
429         assertThat(actual).containsExactlyElementsIn(expected).inOrder();
430     }
431 
432     @Test
433     @EnableFlags(android.app.Flags.FLAG_SORT_SECTION_BY_TIME)
testSort_oldChildren_unspecifiedSummary()434     public void testSort_oldChildren_unspecifiedSummary() {
435         NotificationRecord child1 = new NotificationRecord(mContext,
436                 new StatusBarNotification(
437                         mPkg, mPkg, 1, null, 0, 0,
438                         new Notification.Builder(mContext, TEST_CHANNEL_ID)
439                                 .setGroup("G")
440                                 .build(),
441                         mUser, null, 1200), getLowChannel());
442         NotificationRecord child2 = new NotificationRecord(mContext,
443                 new StatusBarNotification(
444                         mPkg, mPkg, 2, null, 0, 0,
445                         new Notification.Builder(mContext, TEST_CHANNEL_ID)
446                                 .setGroup("G")
447                                 .build(),
448                         mUser, null, 1300), getLowChannel());
449         NotificationRecord summary = new NotificationRecord(mContext,
450                 new StatusBarNotification(
451                         mPkg, mPkg, 3, null, 0, 0,
452                         new Notification.Builder(mContext, TEST_CHANNEL_ID)
453                                 .setGroup("G")
454                                 .setGroupSummary(true)
455                                 .build(),
456                         mUser, null, System.currentTimeMillis()), getLowChannel());
457 
458         // in time slightly before the children, but much earlier than the summary.
459         // will only be sorted first if the summary is not the group proxy for group G.
460         NotificationRecord unrelated = new NotificationRecord(mContext,
461                 new StatusBarNotification(
462                         mPkg, mPkg, 11, null, 0, 0,
463                         new Notification.Builder(mContext, TEST_CHANNEL_ID)
464                                 .setWhen(1500)
465                                 .build(),
466                         mUser, null, System.currentTimeMillis()), getLowChannel());
467 
468         ArrayList<NotificationRecord> expected = new ArrayList<>();
469         expected.add(unrelated);
470         expected.add(summary);
471         expected.add(child2);
472         expected.add(child1);
473 
474         ArrayList<NotificationRecord> actual = new ArrayList<>();
475         actual.addAll(expected);
476         Collections.shuffle(actual);
477 
478         mHelper.sort(actual);
479         assertThat(actual).containsExactlyElementsIn(expected).inOrder();
480     }
481 
482     @Test
483     @EnableFlags(android.app.Flags.FLAG_SORT_SECTION_BY_TIME)
testSort_oldChildren_oldSummary()484     public void testSort_oldChildren_oldSummary() {
485         NotificationRecord child1 = new NotificationRecord(mContext,
486                 new StatusBarNotification(
487                         mPkg, mPkg, 1, null, 0, 0,
488                         new Notification.Builder(mContext, TEST_CHANNEL_ID)
489                                 .setGroup("G")
490                                 .build(),
491                         mUser, null, 1200), getLowChannel());
492         NotificationRecord child2 = new NotificationRecord(mContext,
493                 new StatusBarNotification(
494                         mPkg, mPkg, 2, null, 0, 0,
495                         new Notification.Builder(mContext, TEST_CHANNEL_ID)
496                                 .setGroup("G")
497                                 .build(),
498                         mUser, null, 1300), getLowChannel());
499         NotificationRecord summary = new NotificationRecord(mContext,
500                 new StatusBarNotification(
501                         mPkg, mPkg, 3, null, 0, 0,
502                         new Notification.Builder(mContext, TEST_CHANNEL_ID)
503                                 .setGroup("G")
504                                 .setGroupSummary(true)
505                                 .setWhen(1600)
506                                 .build(),
507                         mUser, null, System.currentTimeMillis()), getLowChannel());
508 
509         // in time slightly before the children, but much earlier than the summary.
510         // will only be sorted first if the summary is not the group proxy for group G.
511         NotificationRecord unrelated = new NotificationRecord(mContext,
512                 new StatusBarNotification(
513                         mPkg, mPkg, 11, null, 0, 0,
514                         new Notification.Builder(mContext, TEST_CHANNEL_ID)
515                                 .setWhen(1500)
516                                 .build(),
517                         mUser, null, System.currentTimeMillis()), getLowChannel());
518 
519         ArrayList<NotificationRecord> expected = new ArrayList<>();
520         expected.add(unrelated);
521         expected.add(summary);
522         expected.add(child2);
523         expected.add(child1);
524 
525         ArrayList<NotificationRecord> actual = new ArrayList<>();
526         actual.addAll(expected);
527         Collections.shuffle(actual);
528 
529         mHelper.sort(actual);
530         assertThat(actual).containsExactlyElementsIn(expected).inOrder();
531     }
532 }
533