1 /*
2  * Copyright (C) 2019 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.systemui.statusbar.notification.stack;
18 
19 import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
20 
21 import static com.android.systemui.statusbar.notification.stack.NotificationPriorityBucketKt.BUCKET_ALERTING;
22 import static com.android.systemui.statusbar.notification.stack.NotificationPriorityBucketKt.BUCKET_FOREGROUND_SERVICE;
23 import static com.android.systemui.statusbar.notification.stack.NotificationPriorityBucketKt.BUCKET_HEADS_UP;
24 import static com.android.systemui.statusbar.notification.stack.NotificationPriorityBucketKt.BUCKET_PEOPLE;
25 import static com.android.systemui.statusbar.notification.stack.NotificationPriorityBucketKt.BUCKET_SILENT;
26 
27 import static com.google.common.truth.Truth.assertThat;
28 
29 import static org.mockito.ArgumentMatchers.any;
30 import static org.mockito.ArgumentMatchers.anyInt;
31 import static org.mockito.ArgumentMatchers.eq;
32 import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
33 import static org.mockito.Mockito.clearInvocations;
34 import static org.mockito.Mockito.doAnswer;
35 import static org.mockito.Mockito.mock;
36 import static org.mockito.Mockito.never;
37 import static org.mockito.Mockito.verify;
38 import static org.mockito.Mockito.when;
39 
40 import android.testing.AndroidTestingRunner;
41 import android.testing.TestableLooper;
42 import android.util.AttributeSet;
43 import android.view.LayoutInflater;
44 import android.view.View;
45 import android.view.ViewGroup;
46 
47 import androidx.test.filters.SmallTest;
48 
49 import com.android.systemui.ActivityStarterDelegate;
50 import com.android.systemui.SysuiTestCase;
51 import com.android.systemui.media.KeyguardMediaController;
52 import com.android.systemui.plugins.statusbar.StatusBarStateController;
53 import com.android.systemui.statusbar.StatusBarState;
54 import com.android.systemui.statusbar.notification.NotificationSectionsFeatureManager;
55 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
56 import com.android.systemui.statusbar.notification.collection.render.SectionHeaderController;
57 import com.android.systemui.statusbar.notification.people.PeopleHubViewAdapter;
58 import com.android.systemui.statusbar.notification.row.ActivatableNotificationViewController;
59 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
60 import com.android.systemui.statusbar.notification.row.dagger.NotificationRowComponent;
61 import com.android.systemui.statusbar.policy.ConfigurationController;
62 
63 import org.junit.Before;
64 import org.junit.Rule;
65 import org.junit.Test;
66 import org.junit.runner.RunWith;
67 import org.mockito.Mock;
68 import org.mockito.junit.MockitoJUnit;
69 import org.mockito.junit.MockitoRule;
70 
71 import java.util.ArrayList;
72 import java.util.List;
73 
74 @SmallTest
75 @RunWith(AndroidTestingRunner.class)
76 @TestableLooper.RunWithLooper(setAsMainLooper = true)
77 public class NotificationSectionsManagerTest extends SysuiTestCase {
78 
79     @Rule public final MockitoRule mockitoRule = MockitoJUnit.rule();
80 
81     @Mock private NotificationStackScrollLayout mNssl;
82     @Mock private ActivityStarterDelegate mActivityStarterDelegate;
83     @Mock private StatusBarStateController mStatusBarStateController;
84     @Mock private ConfigurationController mConfigurationController;
85     @Mock private PeopleHubViewAdapter mPeopleHubAdapter;
86     @Mock private KeyguardMediaController mKeyguardMediaController;
87     @Mock private NotificationSectionsFeatureManager mSectionsFeatureManager;
88     @Mock private NotificationRowComponent mNotificationRowComponent;
89     @Mock private ActivatableNotificationViewController mActivatableNotificationViewController;
90     @Mock private NotificationSectionsLogger mLogger;
91     @Mock private SectionHeaderController mIncomingHeaderController;
92     @Mock private SectionHeaderController mPeopleHeaderController;
93     @Mock private SectionHeaderController mAlertingHeaderController;
94     @Mock private SectionHeaderController mSilentHeaderController;
95 
96     private NotificationSectionsManager mSectionsManager;
97 
98     @Before
setUp()99     public void setUp() {
100         when(mSectionsFeatureManager.getNumberOfBuckets()).thenAnswer(
101                 invocation -> {
102                     int count = 2;
103                     if (mSectionsFeatureManager.isFilteringEnabled()) {
104                         count = 5;
105                     }
106                     if (mSectionsFeatureManager.isMediaControlsEnabled()) {
107                         if (!mSectionsFeatureManager.isFilteringEnabled()) {
108                             count = 5;
109                         } else {
110                             count += 1;
111                         }
112                     }
113                     return count;
114                 });
115         when(mNotificationRowComponent.getActivatableNotificationViewController())
116                 .thenReturn(mActivatableNotificationViewController);
117         when(mIncomingHeaderController.getHeaderView()).thenReturn(mock(SectionHeaderView.class));
118         when(mPeopleHeaderController.getHeaderView()).thenReturn(mock(SectionHeaderView.class));
119         when(mAlertingHeaderController.getHeaderView()).thenReturn(mock(SectionHeaderView.class));
120         when(mSilentHeaderController.getHeaderView()).thenReturn(mock(SectionHeaderView.class));
121         mSectionsManager =
122                 new NotificationSectionsManager(
123                         mStatusBarStateController,
124                         mConfigurationController,
125                         mKeyguardMediaController,
126                         mSectionsFeatureManager,
127                         mLogger,
128                         mIncomingHeaderController,
129                         mPeopleHeaderController,
130                         mAlertingHeaderController,
131                         mSilentHeaderController
132                 );
133         // Required in order for the header inflation to work properly
134         when(mNssl.generateLayoutParams(any(AttributeSet.class)))
135                 .thenReturn(new ViewGroup.LayoutParams(WRAP_CONTENT, WRAP_CONTENT));
136         mSectionsManager.initialize(mNssl, LayoutInflater.from(mContext));
137         when(mNssl.indexOfChild(any(View.class))).thenReturn(-1);
138         when(mStatusBarStateController.getState()).thenReturn(StatusBarState.SHADE);
139 
140     }
141 
142     @Test(expected =  IllegalStateException.class)
testDuplicateInitializeThrows()143     public void testDuplicateInitializeThrows() {
144         mSectionsManager.initialize(mNssl, LayoutInflater.from(mContext));
145     }
146 
147     @Test
testInsertHeader()148     public void testInsertHeader() {
149         // GIVEN a stack with HI and LO rows but no section headers
150         setStackState(
151                 ALERTING,
152                 ALERTING,
153                 ALERTING,
154                 GENTLE);
155 
156         // WHEN we update the section headers
157         mSectionsManager.updateSectionBoundaries();
158 
159         // THEN a LO section header is added
160         verify(mNssl).addView(mSectionsManager.getSilentHeaderView(), 3);
161     }
162 
163     @Test
testRemoveHeader()164     public void testRemoveHeader() {
165         // GIVEN a stack that originally had a header between the HI and LO sections
166         setStackState(
167                 ALERTING,
168                 ALERTING,
169                 GENTLE);
170         mSectionsManager.updateSectionBoundaries();
171 
172         // WHEN the last LO row is replaced with a HI row
173         setStackState(
174                 ALERTING,
175                 ALERTING,
176                 GENTLE_HEADER,
177                 ALERTING);
178         clearInvocations(mNssl);
179         mSectionsManager.updateSectionBoundaries();
180 
181         // THEN the LO section header is removed
182         verify(mNssl).removeView(mSectionsManager.getSilentHeaderView());
183     }
184 
185     @Test
testDoNothingIfHeaderAlreadyRemoved()186     public void testDoNothingIfHeaderAlreadyRemoved() {
187         // GIVEN a stack with only HI rows
188         setStackState(
189                 ALERTING,
190                 ALERTING,
191                 ALERTING);
192 
193         // WHEN we update the sections headers
194         mSectionsManager.updateSectionBoundaries();
195 
196         // THEN we don't add any section headers
197         verify(mNssl, never()).addView(eq(mSectionsManager.getSilentHeaderView()), anyInt());
198     }
199 
200     @Test
testMoveHeaderForward()201     public void testMoveHeaderForward() {
202         // GIVEN a stack that originally had a header between the HI and LO sections
203         setStackState(
204                 ALERTING,
205                 ALERTING,
206                 ALERTING,
207                 GENTLE);
208         mSectionsManager.updateSectionBoundaries();
209 
210         // WHEN the LO section moves forward
211         setStackState(
212                 ALERTING,
213                 ALERTING,
214                 GENTLE,
215                 GENTLE_HEADER,
216                 GENTLE);
217         mSectionsManager.updateSectionBoundaries();
218 
219         // THEN the LO section header is also moved forward
220         verify(mNssl).changeViewPosition(mSectionsManager.getSilentHeaderView(), 2);
221     }
222 
223     @Test
testMoveHeaderBackward()224     public void testMoveHeaderBackward() {
225         // GIVEN a stack that originally had a header between the HI and LO sections
226         setStackState(
227                 ALERTING,
228                 GENTLE,
229                 GENTLE,
230                 GENTLE);
231         mSectionsManager.updateSectionBoundaries();
232 
233         // WHEN the LO section moves backward
234         setStackState(
235                 ALERTING,
236                 GENTLE_HEADER,
237                 ALERTING,
238                 ALERTING,
239                 GENTLE);
240         mSectionsManager.updateSectionBoundaries();
241 
242         // THEN the LO section header is also moved backward (with appropriate index shifting)
243         verify(mNssl).changeViewPosition(mSectionsManager.getSilentHeaderView(), 3);
244     }
245 
246     @Test
testHeaderRemovedFromTransientParent()247     public void testHeaderRemovedFromTransientParent() {
248         // GIVEN a stack where the header is animating away
249         setStackState(
250                 ALERTING,
251                 GENTLE_HEADER);
252         mSectionsManager.updateSectionBoundaries();
253         clearInvocations(mNssl);
254 
255         SectionHeaderView silentHeaderView = mSectionsManager.getSilentHeaderView();
256         ViewGroup transientParent = mock(ViewGroup.class);
257         when(silentHeaderView.getTransientContainer()).thenReturn(transientParent);
258 
259         // WHEN the LO section reappears
260         setStackState(
261                 ALERTING,
262                 GENTLE);
263         mSectionsManager.updateSectionBoundaries();
264 
265         // THEN the header is first removed from the transient parent before being added to the
266         // NSSL.
267         verify(transientParent).removeTransientView(silentHeaderView);
268         verify(mNssl).addView(silentHeaderView, 1);
269     }
270 
271     @Test
testHeaderNotShownOnLockscreen()272     public void testHeaderNotShownOnLockscreen() {
273         // GIVEN a stack of HI and LO notifs on the lockscreen
274         when(mStatusBarStateController.getState()).thenReturn(StatusBarState.KEYGUARD);
275         setStackState(
276                 ALERTING,
277                 ALERTING,
278                 ALERTING,
279                 GENTLE);
280 
281         // WHEN we update the section headers
282         mSectionsManager.updateSectionBoundaries();
283 
284         // Then the section header is not added
285         verify(mNssl, never()).addView(eq(mSectionsManager.getSilentHeaderView()), anyInt());
286     }
287 
288     @Test
testHeaderShownWhenEnterLockscreen()289     public void testHeaderShownWhenEnterLockscreen() {
290         // GIVEN a stack of HI and LO notifs on the lockscreen
291         when(mStatusBarStateController.getState()).thenReturn(StatusBarState.KEYGUARD);
292         setStackState(
293                 ALERTING,
294                 ALERTING,
295                 ALERTING,
296                 GENTLE);
297         mSectionsManager.updateSectionBoundaries();
298 
299         // WHEN we unlock
300         when(mStatusBarStateController.getState()).thenReturn(StatusBarState.SHADE);
301         mSectionsManager.updateSectionBoundaries();
302 
303         // Then the section header is added
304         verify(mNssl).addView(mSectionsManager.getSilentHeaderView(), 3);
305     }
306 
307     @Test
testHeaderHiddenWhenEnterLockscreen()308     public void testHeaderHiddenWhenEnterLockscreen() {
309         // GIVEN a stack of HI and LO notifs on the shade
310         setStackState(
311                 ALERTING,
312                 GENTLE_HEADER,
313                 GENTLE);
314 
315         // WHEN we go back to the keyguard
316         when(mStatusBarStateController.getState()).thenReturn(StatusBarState.KEYGUARD);
317         mSectionsManager.updateSectionBoundaries();
318 
319         // Then the section header is removed
320         verify(mNssl).removeView(mSectionsManager.getSilentHeaderView());
321     }
322 
323     @Test
testPeopleFiltering_onlyAddSilentHeader()324     public void testPeopleFiltering_onlyAddSilentHeader() {
325         enablePeopleFiltering();
326 
327         setStackState(
328                 PERSON,
329                 ALERTING,
330                 GENTLE);
331         mSectionsManager.updateSectionBoundaries();
332 
333         verify(mNssl).addView(mSectionsManager.getSilentHeaderView(), 2);
334     }
335 
336     @Test
testPeopleFiltering_AlertingHunWhilePeopleVisible()337     public void testPeopleFiltering_AlertingHunWhilePeopleVisible() {
338         enablePeopleFiltering();
339 
340         setupMockStack(
341                 PEOPLE_HEADER,
342                 ALERTING,
343                 PERSON,
344                 ALERTING_HEADER,
345                 GENTLE_HEADER,
346                 GENTLE
347         );
348         mSectionsManager.updateSectionBoundaries();
349 
350         verifyMockStack(
351                 ChildType.HEADS_UP,
352                 ChildType.PERSON,
353                 ChildType.GENTLE_HEADER,
354                 ChildType.GENTLE
355         );
356     }
357 
358     @Test
testPeopleFiltering_PersonHunWhileAlertingHunVisible()359     public void testPeopleFiltering_PersonHunWhileAlertingHunVisible() {
360         enablePeopleFiltering();
361 
362         setupMockStack(
363                 PERSON,
364                 INCOMING_HEADER,
365                 ALERTING,
366                 PEOPLE_HEADER,
367                 PERSON
368         );
369         mSectionsManager.updateSectionBoundaries();
370 
371         verifyMockStack(
372                 ChildType.HEADS_UP,
373                 ChildType.HEADS_UP,
374                 ChildType.PERSON
375         );
376     }
377 
378     @Test
testPeopleFiltering_PersonHun()379     public void testPeopleFiltering_PersonHun() {
380         enablePeopleFiltering();
381 
382         setupMockStack(
383                 PERSON,
384                 PEOPLE_HEADER,
385                 PERSON
386         );
387         mSectionsManager.updateSectionBoundaries();
388 
389         verifyMockStack(
390                 ChildType.PERSON,
391                 ChildType.PERSON
392         );
393     }
394 
395     @Test
testPeopleFiltering_AlertingHunWhilePersonHunning()396     public void testPeopleFiltering_AlertingHunWhilePersonHunning() {
397         enablePeopleFiltering();
398 
399         setupMockStack(
400                 ALERTING,
401                 PERSON
402         );
403         mSectionsManager.updateSectionBoundaries();
404         verifyMockStack(
405                 ChildType.HEADS_UP,
406                 ChildType.PERSON
407         );
408     }
409 
410     @Test
testPeopleFiltering_Fsn()411     public void testPeopleFiltering_Fsn() {
412         enablePeopleFiltering();
413 
414         setupMockStack(
415                 INCOMING_HEADER,
416                 ALERTING,
417                 PEOPLE_HEADER,
418                 FSN,
419                 PERSON,
420                 ALERTING,
421                 GENTLE
422         );
423         mSectionsManager.updateSectionBoundaries();
424 
425         verifyMockStack(
426                 ChildType.HEADS_UP,
427                 ChildType.FSN,
428                 ChildType.PERSON,
429                 ChildType.ALERTING,
430                 ChildType.GENTLE_HEADER,
431                 ChildType.GENTLE
432         );
433     }
434 
435     @Test
testMediaControls_AddWhenEnterKeyguard()436     public void testMediaControls_AddWhenEnterKeyguard() {
437         enableMediaControls();
438 
439         // GIVEN a stack that doesn't include media controls
440         setStackState(ALERTING, GENTLE_HEADER, GENTLE);
441 
442         // WHEN we go back to the keyguard
443         when(mStatusBarStateController.getState()).thenReturn(StatusBarState.KEYGUARD);
444         mSectionsManager.updateSectionBoundaries();
445 
446         // Then the media controls are added
447         verify(mNssl).addView(mSectionsManager.getMediaControlsView(), 0);
448     }
449 
450     @Test
testMediaControls_AddWhenEnterKeyguardWithHeadsUp()451     public void testMediaControls_AddWhenEnterKeyguardWithHeadsUp() {
452         enableMediaControls();
453 
454         // GIVEN a stack that doesn't include media
455         setupMockStack(
456                 ALERTING,
457                 ALERTING,
458                 GENTLE_HEADER,
459                 GENTLE);
460 
461         // WHEN we go back to the keyguard
462         when(mStatusBarStateController.getState()).thenReturn(StatusBarState.KEYGUARD);
463         mSectionsManager.updateSectionBoundaries();
464 
465         verifyMockStack(
466                 ChildType.MEDIA_CONTROLS,
467                 ChildType.ALERTING,
468                 ChildType.ALERTING,
469                 ChildType.GENTLE);
470     }
471 
472     @Test
testRemoveNonSilentHeader()473     public void testRemoveNonSilentHeader() {
474         enablePeopleFiltering();
475         enableMediaControls();
476 
477         setupMockStack(
478                 MEDIA_CONTROLS,
479                 INCOMING_HEADER,
480                 PERSON,
481                 ALERTING,
482                 PEOPLE_HEADER,
483                 ALERTING_HEADER,
484                 ALERTING,
485                 ALERTING,
486                 GENTLE_HEADER,
487                 GENTLE,
488                 GENTLE
489         );
490 
491         mSectionsManager.updateSectionBoundaries();
492 
493         verifyMockStack(
494                 ChildType.MEDIA_CONTROLS,
495                 ChildType.PERSON,
496                 ChildType.ALERTING,
497                 ChildType.ALERTING,
498                 ChildType.ALERTING,
499                 ChildType.GENTLE_HEADER,
500                 ChildType.GENTLE,
501                 ChildType.GENTLE
502         );
503     }
504 
505     @Test
testExpandIncomingSection()506     public void testExpandIncomingSection() {
507         enablePeopleFiltering();
508 
509         setupMockStack(
510                 INCOMING_HEADER,
511                 PERSON,
512                 ALERTING,
513                 PEOPLE_HEADER,
514                 ALERTING,
515                 PERSON,
516                 ALERTING_HEADER,
517                 ALERTING
518         );
519 
520         mSectionsManager.updateSectionBoundaries();
521 
522         verifyMockStack(
523                 ChildType.HEADS_UP,
524                 ChildType.HEADS_UP,
525                 ChildType.HEADS_UP,
526                 ChildType.PERSON,
527                 ChildType.ALERTING
528         );
529     }
530 
531     @Test
testIgnoreGoneView()532     public void testIgnoreGoneView() {
533         enablePeopleFiltering();
534 
535         setupMockStack(
536                 PERSON.gone(),
537                 ALERTING,
538                 GENTLE
539         );
540 
541         mSectionsManager.updateSectionBoundaries();
542 
543         verifyMockStack(
544                 ChildType.PERSON,
545                 ChildType.ALERTING,
546                 ChildType.GENTLE_HEADER,
547                 ChildType.GENTLE
548         );
549     }
550 
enablePeopleFiltering()551     private void enablePeopleFiltering() {
552         when(mSectionsFeatureManager.isFilteringEnabled()).thenReturn(true);
553     }
554 
enableMediaControls()555     private void enableMediaControls() {
556         when(mSectionsFeatureManager.isMediaControlsEnabled()).thenReturn(true);
557     }
558 
559     private enum ChildType {
560         INCOMING_HEADER, MEDIA_CONTROLS, PEOPLE_HEADER, ALERTING_HEADER, GENTLE_HEADER, HEADS_UP,
561         FSN, PERSON, ALERTING, GENTLE, OTHER
562     }
563 
setStackState(StackEntry... children)564     private void setStackState(StackEntry... children) {
565         when(mNssl.getChildCount()).thenReturn(children.length);
566         for (int i = 0; i < children.length; i++) {
567             View child;
568             StackEntry entry = children[i];
569             switch (entry.mChildType) {
570                 case INCOMING_HEADER:
571                     child = mSectionsManager.getIncomingHeaderView();
572                     break;
573                 case MEDIA_CONTROLS:
574                     child = mSectionsManager.getMediaControlsView();
575                     break;
576                 case PEOPLE_HEADER:
577                     child = mSectionsManager.getPeopleHeaderView();
578                     break;
579                 case ALERTING_HEADER:
580                     child = mSectionsManager.getAlertingHeaderView();
581                     break;
582                 case GENTLE_HEADER:
583                     child = mSectionsManager.getSilentHeaderView();
584                     break;
585                 case FSN:
586                     child = mockNotification(BUCKET_FOREGROUND_SERVICE, entry.mIsGone);
587                     break;
588                 case PERSON:
589                     child = mockNotification(BUCKET_PEOPLE, entry.mIsGone);
590                     break;
591                 case ALERTING:
592                     child = mockNotification(BUCKET_ALERTING, entry.mIsGone);
593                     break;
594                 case GENTLE:
595                     child = mockNotification(BUCKET_SILENT, entry.mIsGone);
596                     break;
597                 case OTHER:
598                     child = mock(View.class);
599                     when(child.getVisibility()).thenReturn(View.VISIBLE);
600                     when(child.getParent()).thenReturn(mNssl);
601                     break;
602                 default:
603                     throw new RuntimeException("Unknown ChildType: " + children[i]);
604             }
605             when(mNssl.getChildAt(i)).thenReturn(child);
606             when(mNssl.indexOfChild(child)).thenReturn(i);
607         }
608     }
609 
mockNotification(@riorityBucket int bucket, boolean isGone)610     private View mockNotification(@PriorityBucket int bucket, boolean isGone) {
611         ExpandableNotificationRow notifRow =
612                 mock(ExpandableNotificationRow.class, RETURNS_DEEP_STUBS);
613         when(notifRow.getVisibility()).thenReturn(View.VISIBLE);
614         when(notifRow.getParent()).thenReturn(mNssl);
615 
616         NotificationEntry mockEntry = mock(NotificationEntry.class);
617         when(notifRow.getEntry()).thenReturn(mockEntry);
618 
619         int[] bucketRef = new int[] { bucket };
620         when(mockEntry.getBucket()).thenAnswer(invocation -> bucketRef[0]);
621         doAnswer(invocation -> {
622             bucketRef[0] = invocation.getArgument(0);
623             return null;
624         }).when(mockEntry).setBucket(anyInt());
625 
626         when(notifRow.getVisibility()).thenReturn(isGone ? View.GONE : View.VISIBLE);
627         return notifRow;
628     }
629 
verifyMockStack(ChildType... expected)630     private void verifyMockStack(ChildType... expected) {
631         final List<ChildType> actual = new ArrayList<>();
632         int childCount = mNssl.getChildCount();
633         for (int i = 0; i < childCount; i++) {
634             View child = mNssl.getChildAt(i);
635             if (child == mSectionsManager.getIncomingHeaderView()) {
636                 actual.add(ChildType.INCOMING_HEADER);
637                 continue;
638             }
639             if (child == mSectionsManager.getMediaControlsView()) {
640                 actual.add(ChildType.MEDIA_CONTROLS);
641                 continue;
642             }
643             if (child == mSectionsManager.getPeopleHeaderView()) {
644                 actual.add(ChildType.PEOPLE_HEADER);
645                 continue;
646             }
647             if (child == mSectionsManager.getAlertingHeaderView()) {
648                 actual.add(ChildType.ALERTING_HEADER);
649                 continue;
650             }
651             if (child == mSectionsManager.getSilentHeaderView()) {
652                 actual.add(ChildType.GENTLE_HEADER);
653                 continue;
654             }
655             if (child instanceof ExpandableNotificationRow) {
656                 switch (((ExpandableNotificationRow) child).getEntry().getBucket()) {
657                     case BUCKET_HEADS_UP:
658                         actual.add(ChildType.HEADS_UP);
659                         break;
660                     case BUCKET_FOREGROUND_SERVICE:
661                         actual.add(ChildType.FSN);
662                         break;
663                     case BUCKET_PEOPLE:
664                         actual.add(ChildType.PERSON);
665                         break;
666                     case BUCKET_ALERTING:
667                         actual.add(ChildType.ALERTING);
668                         break;
669                     case BUCKET_SILENT:
670                         actual.add(ChildType.GENTLE);
671                         break;
672                     default:
673                         actual.add(ChildType.OTHER);
674                         break;
675                 }
676                 continue;
677             }
678             actual.add(ChildType.OTHER);
679         }
680         assertThat(actual).containsExactly((Object[]) expected).inOrder();
681     }
682 
setupMockStack(StackEntry... entries)683     private void setupMockStack(StackEntry... entries) {
684         final List<View> children = new ArrayList<>();
685         when(mNssl.getChildCount()).thenAnswer(invocation -> children.size());
686         when(mNssl.getChildAt(anyInt()))
687                 .thenAnswer(invocation -> {
688                     Integer index = invocation.getArgument(0);
689                     if (index == null || index < 0 || index >= children.size()) {
690                         return null;
691                     }
692                     return children.get(index);
693                 });
694         when(mNssl.indexOfChild(any()))
695                 .thenAnswer(invocation -> children.indexOf(invocation.getArgument(0)));
696         doAnswer(invocation -> {
697             View child = invocation.getArgument(0);
698             int index = invocation.getArgument(1);
699             children.add(index, child);
700             return null;
701         }).when(mNssl).addView(any(), anyInt());
702         doAnswer(invocation -> {
703             View child = invocation.getArgument(0);
704             children.remove(child);
705             return null;
706         }).when(mNssl).removeView(any());
707         doAnswer(invocation -> {
708             View child = invocation.getArgument(0);
709             int newIndex = invocation.getArgument(1);
710             children.remove(child);
711             children.add(newIndex, child);
712             return null;
713         }).when(mNssl).changeViewPosition(any(), anyInt());
714         for (StackEntry entry : entries) {
715             View child;
716             switch (entry.mChildType) {
717                 case INCOMING_HEADER:
718                     child = mSectionsManager.getIncomingHeaderView();
719                     break;
720                 case MEDIA_CONTROLS:
721                     child = mSectionsManager.getMediaControlsView();
722                     break;
723                 case PEOPLE_HEADER:
724                     child = mSectionsManager.getPeopleHeaderView();
725                     break;
726                 case ALERTING_HEADER:
727                     child = mSectionsManager.getAlertingHeaderView();
728                     break;
729                 case GENTLE_HEADER:
730                     child = mSectionsManager.getSilentHeaderView();
731                     break;
732                 case FSN:
733                     child = mockNotification(BUCKET_FOREGROUND_SERVICE, entry.mIsGone);
734                     break;
735                 case PERSON:
736                     child = mockNotification(BUCKET_PEOPLE, entry.mIsGone);
737                     break;
738                 case ALERTING:
739                     child = mockNotification(BUCKET_ALERTING, entry.mIsGone);
740                     break;
741                 case GENTLE:
742                     child = mockNotification(BUCKET_SILENT, entry.mIsGone);
743                     break;
744                 case OTHER:
745                     child = mock(View.class);
746                     when(child.getVisibility()).thenReturn(View.VISIBLE);
747                     when(child.getParent()).thenReturn(mNssl);
748                     break;
749                 default:
750                     throw new RuntimeException("Unknown ChildType: " + entry.mChildType);
751             }
752             children.add(child);
753         }
754     }
755 
756     private static final StackEntry INCOMING_HEADER = new StackEntry(ChildType.INCOMING_HEADER);
757     private static final StackEntry MEDIA_CONTROLS = new StackEntry(ChildType.MEDIA_CONTROLS);
758     private static final StackEntry PEOPLE_HEADER = new StackEntry(ChildType.PEOPLE_HEADER);
759     private static final StackEntry ALERTING_HEADER = new StackEntry(ChildType.ALERTING_HEADER);
760     private static final StackEntry GENTLE_HEADER = new StackEntry(ChildType.GENTLE_HEADER);
761     private static final StackEntry FSN = new StackEntry(ChildType.FSN);
762     private static final StackEntry PERSON = new StackEntry(ChildType.PERSON);
763     private static final StackEntry ALERTING = new StackEntry(ChildType.ALERTING);
764     private static final StackEntry GENTLE = new StackEntry(ChildType.GENTLE);
765 
766     private static class StackEntry {
767         final ChildType mChildType;
768         final boolean mIsGone;
769 
StackEntry(ChildType childType)770         StackEntry(ChildType childType) {
771             this(childType, false);
772         }
773 
StackEntry(ChildType childType, boolean isGone)774         StackEntry(ChildType childType, boolean isGone) {
775             mChildType = childType;
776             mIsGone = isGone;
777         }
778 
gone()779         public StackEntry gone() {
780             return new StackEntry(mChildType, true);
781         }
782     }
783 }
784