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 org.mockito.ArgumentMatchers.any;
22 import static org.mockito.ArgumentMatchers.anyInt;
23 import static org.mockito.ArgumentMatchers.eq;
24 import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
25 import static org.mockito.Mockito.clearInvocations;
26 import static org.mockito.Mockito.mock;
27 import static org.mockito.Mockito.never;
28 import static org.mockito.Mockito.verify;
29 import static org.mockito.Mockito.when;
30 
31 import android.testing.AndroidTestingRunner;
32 import android.testing.TestableLooper;
33 import android.util.AttributeSet;
34 import android.view.LayoutInflater;
35 import android.view.View;
36 import android.view.ViewGroup;
37 
38 import androidx.test.filters.SmallTest;
39 
40 import com.android.systemui.ActivityStarterDelegate;
41 import com.android.systemui.SysuiTestCase;
42 import com.android.systemui.plugins.statusbar.StatusBarStateController;
43 import com.android.systemui.statusbar.StatusBarState;
44 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
45 import com.android.systemui.statusbar.policy.ConfigurationController;
46 
47 import org.junit.Before;
48 import org.junit.Rule;
49 import org.junit.Test;
50 import org.junit.runner.RunWith;
51 import org.mockito.Mock;
52 import org.mockito.junit.MockitoJUnit;
53 import org.mockito.junit.MockitoRule;
54 
55 @SmallTest
56 @RunWith(AndroidTestingRunner.class)
57 @TestableLooper.RunWithLooper
58 public class NotificationSectionsManagerTest extends SysuiTestCase {
59 
60     @Rule public final MockitoRule mockitoRule = MockitoJUnit.rule();
61 
62     @Mock private NotificationStackScrollLayout mNssl;
63     @Mock private ActivityStarterDelegate mActivityStarterDelegate;
64     @Mock private StatusBarStateController mStatusBarStateController;
65     @Mock private ConfigurationController mConfigurationController;
66 
67     private NotificationSectionsManager mSectionsManager;
68 
69     @Before
setUp()70     public void setUp() {
71         mSectionsManager =
72                 new NotificationSectionsManager(
73                         mNssl,
74                         mActivityStarterDelegate,
75                         mStatusBarStateController,
76                         mConfigurationController,
77                         true);
78         // Required in order for the header inflation to work properly
79         when(mNssl.generateLayoutParams(any(AttributeSet.class)))
80                 .thenReturn(new ViewGroup.LayoutParams(WRAP_CONTENT, WRAP_CONTENT));
81         mSectionsManager.initialize(LayoutInflater.from(mContext));
82         when(mNssl.indexOfChild(any(View.class))).thenReturn(-1);
83         when(mStatusBarStateController.getState()).thenReturn(StatusBarState.SHADE);
84     }
85 
86     @Test(expected =  IllegalStateException.class)
testDuplicateInitializeThrows()87     public void testDuplicateInitializeThrows() {
88         mSectionsManager.initialize(LayoutInflater.from(mContext));
89     }
90 
91     @Test
testInsertHeader()92     public void testInsertHeader() {
93         // GIVEN a stack with HI and LO rows but no section headers
94         setStackState(ChildType.HIPRI, ChildType.HIPRI, ChildType.HIPRI, ChildType.LOPRI);
95 
96         // WHEN we update the section headers
97         mSectionsManager.updateSectionBoundaries();
98 
99         // THEN a LO section header is added
100         verify(mNssl).addView(mSectionsManager.getGentleHeaderView(), 3);
101     }
102 
103     @Test
testRemoveHeader()104     public void testRemoveHeader() {
105         // GIVEN a stack that originally had a header between the HI and LO sections
106         setStackState(ChildType.HIPRI, ChildType.HIPRI, ChildType.LOPRI);
107         mSectionsManager.updateSectionBoundaries();
108 
109         // WHEN the last LO row is replaced with a HI row
110         setStackState(ChildType.HIPRI, ChildType.HIPRI, ChildType.HEADER, ChildType.HIPRI);
111         clearInvocations(mNssl);
112         mSectionsManager.updateSectionBoundaries();
113 
114         // THEN the LO section header is removed
115         verify(mNssl).removeView(mSectionsManager.getGentleHeaderView());
116     }
117 
118     @Test
testDoNothingIfHeaderAlreadyRemoved()119     public void testDoNothingIfHeaderAlreadyRemoved() {
120         // GIVEN a stack with only HI rows
121         setStackState(ChildType.HIPRI, ChildType.HIPRI, ChildType.HIPRI);
122 
123         // WHEN we update the sections headers
124         mSectionsManager.updateSectionBoundaries();
125 
126         // THEN we don't add any section headers
127         verify(mNssl, never()).addView(eq(mSectionsManager.getGentleHeaderView()), anyInt());
128     }
129 
130     @Test
testMoveHeaderForward()131     public void testMoveHeaderForward() {
132         // GIVEN a stack that originally had a header between the HI and LO sections
133         setStackState(
134                 ChildType.HIPRI,
135                 ChildType.HIPRI,
136                 ChildType.HIPRI,
137                 ChildType.LOPRI);
138         mSectionsManager.updateSectionBoundaries();
139 
140         // WHEN the LO section moves forward
141         setStackState(
142                 ChildType.HIPRI,
143                 ChildType.HIPRI,
144                 ChildType.LOPRI,
145                 ChildType.HEADER,
146                 ChildType.LOPRI);
147         mSectionsManager.updateSectionBoundaries();
148 
149         // THEN the LO section header is also moved forward
150         verify(mNssl).changeViewPosition(mSectionsManager.getGentleHeaderView(), 2);
151     }
152 
153     @Test
testMoveHeaderBackward()154     public void testMoveHeaderBackward() {
155         // GIVEN a stack that originally had a header between the HI and LO sections
156         setStackState(
157                 ChildType.HIPRI,
158                 ChildType.LOPRI,
159                 ChildType.LOPRI,
160                 ChildType.LOPRI);
161         mSectionsManager.updateSectionBoundaries();
162 
163         // WHEN the LO section moves backward
164         setStackState(
165                 ChildType.HIPRI,
166                 ChildType.HEADER,
167                 ChildType.HIPRI,
168                 ChildType.HIPRI,
169                 ChildType.LOPRI);
170         mSectionsManager.updateSectionBoundaries();
171 
172         // THEN the LO section header is also moved backward (with appropriate index shifting)
173         verify(mNssl).changeViewPosition(mSectionsManager.getGentleHeaderView(), 3);
174     }
175 
176     @Test
testHeaderRemovedFromTransientParent()177     public void testHeaderRemovedFromTransientParent() {
178         // GIVEN a stack where the header is animating away
179         setStackState(
180                 ChildType.HIPRI,
181                 ChildType.LOPRI,
182                 ChildType.LOPRI,
183                 ChildType.LOPRI);
184         mSectionsManager.updateSectionBoundaries();
185         setStackState(
186                 ChildType.HIPRI,
187                 ChildType.HEADER);
188         mSectionsManager.updateSectionBoundaries();
189         clearInvocations(mNssl);
190 
191         ViewGroup transientParent = mock(ViewGroup.class);
192         mSectionsManager.getGentleHeaderView().setTransientContainer(transientParent);
193 
194         // WHEN the LO section reappears
195         setStackState(
196                 ChildType.HIPRI,
197                 ChildType.LOPRI);
198         mSectionsManager.updateSectionBoundaries();
199 
200         // THEN the header is first removed from the transient parent before being added to the
201         // NSSL.
202         verify(transientParent).removeTransientView(mSectionsManager.getGentleHeaderView());
203         verify(mNssl).addView(mSectionsManager.getGentleHeaderView(), 1);
204     }
205 
206     @Test
testHeaderNotShownOnLockscreen()207     public void testHeaderNotShownOnLockscreen() {
208         // GIVEN a stack of HI and LO notifs on the lockscreen
209         when(mStatusBarStateController.getState()).thenReturn(StatusBarState.KEYGUARD);
210         setStackState(ChildType.HIPRI, ChildType.HIPRI, ChildType.HIPRI, ChildType.LOPRI);
211 
212         // WHEN we update the section headers
213         mSectionsManager.updateSectionBoundaries();
214 
215         // Then the section header is not added
216         verify(mNssl, never()).addView(eq(mSectionsManager.getGentleHeaderView()), anyInt());
217     }
218 
219     @Test
testHeaderShownWhenEnterLockscreen()220     public void testHeaderShownWhenEnterLockscreen() {
221         // GIVEN a stack of HI and LO notifs on the lockscreen
222         when(mStatusBarStateController.getState()).thenReturn(StatusBarState.KEYGUARD);
223         setStackState(ChildType.HIPRI, ChildType.HIPRI, ChildType.HIPRI, ChildType.LOPRI);
224         mSectionsManager.updateSectionBoundaries();
225 
226         // WHEN we unlock
227         when(mStatusBarStateController.getState()).thenReturn(StatusBarState.SHADE);
228         mSectionsManager.updateSectionBoundaries();
229 
230         // Then the section header is added
231         verify(mNssl).addView(mSectionsManager.getGentleHeaderView(), 3);
232     }
233 
234     @Test
testHeaderHiddenWhenEnterLockscreen()235     public void testHeaderHiddenWhenEnterLockscreen() {
236         // GIVEN a stack of HI and LO notifs on the shade
237         when(mStatusBarStateController.getState()).thenReturn(StatusBarState.SHADE_LOCKED);
238         setStackState(ChildType.HIPRI, ChildType.HIPRI, ChildType.HIPRI, ChildType.LOPRI);
239         mSectionsManager.updateSectionBoundaries();
240 
241         // WHEN we go back to the keyguard
242         when(mStatusBarStateController.getState()).thenReturn(StatusBarState.KEYGUARD);
243         mSectionsManager.updateSectionBoundaries();
244 
245         // Then the section header is removed
246         verify(mNssl).removeView(eq(mSectionsManager.getGentleHeaderView()));
247     }
248 
249     private enum ChildType { HEADER, HIPRI, LOPRI }
250 
setStackState(ChildType... children)251     private void setStackState(ChildType... children) {
252         when(mNssl.getChildCount()).thenReturn(children.length);
253         for (int i = 0; i < children.length; i++) {
254             View child;
255             switch (children[i]) {
256                 case HEADER:
257                     child = mSectionsManager.getGentleHeaderView();
258                     break;
259                 case HIPRI:
260                 case LOPRI:
261                     ExpandableNotificationRow notifRow = mock(ExpandableNotificationRow.class,
262                             RETURNS_DEEP_STUBS);
263                     when(notifRow.getVisibility()).thenReturn(View.VISIBLE);
264                     when(notifRow.getEntry().isHighPriority())
265                             .thenReturn(children[i] == ChildType.HIPRI);
266                     when(notifRow.getEntry().isTopBucket())
267                             .thenReturn(children[i] == ChildType.HIPRI);
268                     when(notifRow.getParent()).thenReturn(mNssl);
269                     child = notifRow;
270                     break;
271                 default:
272                     throw new RuntimeException("Unknown ChildType: " + children[i]);
273             }
274             when(mNssl.getChildAt(i)).thenReturn(child);
275             when(mNssl.indexOfChild(child)).thenReturn(i);
276         }
277     }
278 }
279