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