1 /*
2  * Copyright (C) 2022 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.shade;
18 
19 import static android.view.MotionEvent.ACTION_DOWN;
20 import static android.view.MotionEvent.ACTION_MOVE;
21 import static android.view.MotionEvent.ACTION_POINTER_DOWN;
22 import static android.view.MotionEvent.ACTION_UP;
23 import static android.view.MotionEvent.BUTTON_SECONDARY;
24 import static android.view.MotionEvent.BUTTON_STYLUS_PRIMARY;
25 
26 import static com.android.systemui.statusbar.StatusBarState.KEYGUARD;
27 import static com.android.systemui.statusbar.StatusBarState.SHADE;
28 
29 import static com.google.common.truth.Truth.assertThat;
30 
31 import static org.mockito.ArgumentMatchers.anyFloat;
32 import static org.mockito.ArgumentMatchers.eq;
33 import static org.mockito.Mockito.atLeastOnce;
34 import static org.mockito.Mockito.times;
35 import static org.mockito.Mockito.verify;
36 import static org.mockito.Mockito.when;
37 
38 import android.testing.AndroidTestingRunner;
39 import android.testing.TestableLooper;
40 import android.view.MotionEvent;
41 
42 import androidx.test.filters.SmallTest;
43 
44 import com.android.systemui.R;
45 import com.android.systemui.plugins.qs.QS;
46 
47 import org.junit.Test;
48 import org.junit.runner.RunWith;
49 import org.mockito.ArgumentCaptor;
50 
51 import java.util.List;
52 
53 @SmallTest
54 @RunWith(AndroidTestingRunner.class)
55 @TestableLooper.RunWithLooper(setAsMainLooper = true)
56 public class QuickSettingsControllerTest extends QuickSettingsControllerBaseTest {
57 
58     @Test
testCloseQsSideEffects()59     public void testCloseQsSideEffects() {
60         enableSplitShade(true);
61         mQsController.setExpandImmediate(true);
62         mQsController.setExpanded(true);
63         mQsController.closeQs();
64 
65         assertThat(mQsController.getExpanded()).isEqualTo(false);
66         assertThat(mQsController.isExpandImmediate()).isEqualTo(false);
67     }
68 
69     @Test
testLargeScreenHeaderMadeActiveForLargeScreen()70     public void testLargeScreenHeaderMadeActiveForLargeScreen() {
71         mStatusBarStateController.setState(SHADE);
72         when(mResources.getBoolean(R.bool.config_use_large_screen_shade_header)).thenReturn(true);
73         mQsController.updateResources();
74         verify(mShadeHeaderController).setLargeScreenActive(true);
75 
76         when(mResources.getBoolean(R.bool.config_use_large_screen_shade_header)).thenReturn(false);
77         mQsController.updateResources();
78         verify(mShadeHeaderController).setLargeScreenActive(false);
79     }
80 
81     @Test
testPanelStaysOpenWhenClosingQs()82     public void testPanelStaysOpenWhenClosingQs() {
83         mQsController.setShadeExpansion(/* shadeExpandedHeight= */ 1, /* expandedFraction=*/ 1);
84 
85         float shadeExpandedHeight = mQsController.getShadeExpandedHeight();
86         mQsController.animateCloseQs(false);
87 
88         assertThat(mQsController.getShadeExpandedHeight()).isEqualTo(shadeExpandedHeight);
89     }
90 
91     @Test
interceptTouchEvent_withinQs_shadeExpanded_startsQsTracking()92     public void interceptTouchEvent_withinQs_shadeExpanded_startsQsTracking() {
93         mQsController.setQs(mQs);
94 
95         mQsController.setShadeExpansion(/* shadeExpandedHeight= */ 1, /* expandedFraction=*/ 1);
96         mQsController.onIntercept(
97                 createMotionEvent(0, 0, ACTION_DOWN));
98         mQsController.onIntercept(
99                 createMotionEvent(0, 500, ACTION_MOVE));
100 
101         assertThat(mQsController.isTracking()).isTrue();
102     }
103 
104     @Test
interceptTouchEvent_withinQs_shadeExpanded_inSplitShade_doesNotStartQsTracking()105     public void interceptTouchEvent_withinQs_shadeExpanded_inSplitShade_doesNotStartQsTracking() {
106         enableSplitShade(true);
107         mQsController.setQs(mQs);
108 
109         mQsController.setShadeExpansion(/* shadeExpandedHeight= */ 1, /* expandedFraction=*/ 1);
110         mQsController.onIntercept(
111                 createMotionEvent(0, 0, ACTION_DOWN));
112         mQsController.onIntercept(
113                 createMotionEvent(0, 500, ACTION_MOVE));
114 
115         assertThat(mQsController.isTracking()).isFalse();
116     }
117 
118     @Test
interceptTouch_downBetweenFullyCollapsedAndExpanded()119     public void interceptTouch_downBetweenFullyCollapsedAndExpanded() {
120         mQsController.setQs(mQs);
121         when(mQs.getDesiredHeight()).thenReturn(QS_FRAME_BOTTOM);
122         mQsController.onHeightChanged();
123         mQsController.setExpansionHeight(QS_FRAME_BOTTOM / 2f);
124 
125         assertThat(mQsController.onIntercept(
126                 createMotionEvent(0, QS_FRAME_BOTTOM / 2, ACTION_DOWN))).isTrue();
127     }
128 
129     @Test
onTouch_moveActionSetsCorrectExpansionHeight()130     public void onTouch_moveActionSetsCorrectExpansionHeight() {
131         mQsController.setQs(mQs);
132         when(mQs.getDesiredHeight()).thenReturn(QS_FRAME_BOTTOM);
133         mQsController.onHeightChanged();
134         mQsController.setExpansionHeight(QS_FRAME_BOTTOM / 2f);
135         mQsController.handleTouch(
136                 createMotionEvent(0, QS_FRAME_BOTTOM / 4, ACTION_DOWN), false, false);
137         assertThat(mQsController.isTracking()).isTrue();
138         mQsController.handleTouch(
139                 createMotionEvent(0, QS_FRAME_BOTTOM / 4 + 1, ACTION_MOVE), false, false);
140 
141         assertThat(mQsController.getExpansionHeight()).isEqualTo(QS_FRAME_BOTTOM / 2 + 1);
142     }
143 
144     @Test
handleTouch_downActionInQsArea()145     public void handleTouch_downActionInQsArea() {
146         mQsController.setQs(mQs);
147         mQsController.setBarState(SHADE);
148         mQsController.setShadeExpansion(/* shadeExpandedHeight= */ 1, /* expandedFraction=*/ 0.5f);
149 
150         MotionEvent event =
151                 createMotionEvent(QS_FRAME_WIDTH / 2, QS_FRAME_BOTTOM / 2, ACTION_DOWN);
152         mQsController.handleTouch(event, false, false);
153 
154         assertThat(mQsController.isTracking()).isTrue();
155         assertThat(mQsController.getInitialTouchY()).isEqualTo(QS_FRAME_BOTTOM / 2);
156     }
157 
158     @Test
handleTouch_qsTouchedWhileCollapsingDisablesTracking()159     public void handleTouch_qsTouchedWhileCollapsingDisablesTracking() {
160         mQsController.handleTouch(
161                 createMotionEvent(0, QS_FRAME_BOTTOM, ACTION_DOWN), false, false);
162         mQsController.setLastShadeFlingWasExpanding(false);
163         mQsController.handleTouch(
164                 createMotionEvent(0, QS_FRAME_BOTTOM / 2, ACTION_MOVE), false, true);
165         MotionEvent secondTouch = createMotionEvent(0, QS_FRAME_TOP, ACTION_DOWN);
166         mQsController.handleTouch(secondTouch, false, true);
167         assertThat(mQsController.isTracking()).isFalse();
168     }
169 
170     @Test
handleTouch_qsTouchedWhileExpanding()171     public void handleTouch_qsTouchedWhileExpanding() {
172         mQsController.setQs(mQs);
173         mQsController.handleTouch(
174                 createMotionEvent(100, 100, ACTION_DOWN), false, false);
175         mQsController.handleTouch(
176                 createMotionEvent(0, QS_FRAME_BOTTOM / 2, ACTION_MOVE), false, false);
177         mQsController.setLastShadeFlingWasExpanding(true);
178         mQsController.handleTouch(
179                 createMotionEvent(0, QS_FRAME_TOP, ACTION_DOWN), false, false);
180         assertThat(mQsController.isTracking()).isTrue();
181     }
182 
183     @Test
handleTouch_isConflictingExpansionGestureSet()184     public void handleTouch_isConflictingExpansionGestureSet() {
185         assertThat(mQsController.isConflictingExpansionGesture()).isFalse();
186         mQsController.setShadeExpansion(/* shadeExpandedHeight= */ 1, /* expandedFraction=*/ 1);
187         mQsController.handleTouch(MotionEvent.obtain(0L /* downTime */,
188                 0L /* eventTime */, ACTION_DOWN, 0f /* x */, 0f /* y */,
189                 0 /* metaState */), false, false);
190         assertThat(mQsController.isConflictingExpansionGesture()).isTrue();
191     }
192 
193     @Test
handleTouch_isConflictingExpansionGestureSet_cancel()194     public void handleTouch_isConflictingExpansionGestureSet_cancel() {
195         mQsController.setShadeExpansion(/* shadeExpandedHeight= */ 1, /* expandedFraction=*/ 1);
196         mQsController.handleTouch(createMotionEvent(0, 0, ACTION_DOWN), false, false);
197         assertThat(mQsController.isConflictingExpansionGesture()).isTrue();
198         mQsController.handleTouch(createMotionEvent(0, 0, ACTION_UP), true, true);
199         assertThat(mQsController.isConflictingExpansionGesture()).isFalse();
200     }
201 
202     @Test
handleTouch_twoFingerExpandPossibleConditions()203     public void handleTouch_twoFingerExpandPossibleConditions() {
204         assertThat(mQsController.isTwoFingerExpandPossible()).isFalse();
205         mQsController.handleTouch(createMotionEvent(0, 0, ACTION_DOWN), true, false);
206         assertThat(mQsController.isTwoFingerExpandPossible()).isTrue();
207     }
208 
209     @Test
handleTouch_twoFingerDrag()210     public void handleTouch_twoFingerDrag() {
211         mQsController.setQs(mQs);
212         mQsController.setStatusBarMinHeight(1);
213         mQsController.setTwoFingerExpandPossible(true);
214         mQsController.handleTouch(
215                 createMultitouchMotionEvent(ACTION_POINTER_DOWN), false, false);
216         assertThat(mQsController.isExpandImmediate()).isTrue();
217         verify(mQs).setListening(true);
218     }
219 
220     @Test
onQsFragmentAttached_fullWidth_setsFullWidthTrueOnQS()221     public void onQsFragmentAttached_fullWidth_setsFullWidthTrueOnQS() {
222         setIsFullWidth(true);
223         mFragmentListener.onFragmentViewCreated(QS.TAG, mQSFragment);
224 
225         verify(mQSFragment).setIsNotificationPanelFullWidth(true);
226     }
227 
228     @Test
onQsFragmentAttached_notFullWidth_setsFullWidthFalseOnQS()229     public void onQsFragmentAttached_notFullWidth_setsFullWidthFalseOnQS() {
230         setIsFullWidth(false);
231         mFragmentListener.onFragmentViewCreated(QS.TAG, mQSFragment);
232 
233         verify(mQSFragment).setIsNotificationPanelFullWidth(false);
234     }
235 
236     @Test
setQsExpansion_lockscreenShadeTransitionInProgress_usesLockscreenSquishiness()237     public void setQsExpansion_lockscreenShadeTransitionInProgress_usesLockscreenSquishiness() {
238         float squishinessFraction = 0.456f;
239         mQsController.setQs(mQs);
240         when(mLockscreenShadeTransitionController.getQsSquishTransitionFraction())
241                 .thenReturn(squishinessFraction);
242         when(mNotificationStackScrollLayoutController.getNotificationSquishinessFraction())
243                 .thenReturn(0.987f);
244         // Call setTransitionToFullShadeAmount to get into the full shade transition in progress
245         // state.
246         mLockscreenShadeTransitionCallback.setTransitionToFullShadeAmount(234, false, 0);
247 
248         mQsController.setExpansionHeight(123);
249 
250         // First for setTransitionToFullShadeAmount and then setQsExpansion
251         verify(mQs, times(2)).setQsExpansion(anyFloat(), anyFloat(), anyFloat(),
252                 eq(squishinessFraction)
253         );
254     }
255 
256     @Test
setQsExpansion_lockscreenShadeTransitionNotInProgress_usesStandardSquishiness()257     public void setQsExpansion_lockscreenShadeTransitionNotInProgress_usesStandardSquishiness() {
258         float lsSquishinessFraction = 0.456f;
259         float nsslSquishinessFraction = 0.987f;
260         mQsController.setQs(mQs);
261         when(mLockscreenShadeTransitionController.getQsSquishTransitionFraction())
262                 .thenReturn(lsSquishinessFraction);
263         when(mNotificationStackScrollLayoutController.getNotificationSquishinessFraction())
264                 .thenReturn(nsslSquishinessFraction);
265 
266         mQsController.setExpansionHeight(123);
267 
268         verify(mQs).setQsExpansion(anyFloat(), anyFloat(), anyFloat(), eq(nsslSquishinessFraction)
269         );
270     }
271 
272     @Test
updateExpansion_expandImmediateOrAlreadyExpanded_usesFullSquishiness()273     public void updateExpansion_expandImmediateOrAlreadyExpanded_usesFullSquishiness() {
274         mQsController.setQs(mQs);
275         when(mQs.getDesiredHeight()).thenReturn(100);
276         mQsController.onHeightChanged();
277 
278         mQsController.setExpandImmediate(true);
279         mQsController.setExpanded(false);
280         mQsController.updateExpansion();
281         mQsController.setExpandImmediate(false);
282         mQsController.setExpanded(true);
283         mQsController.updateExpansion();
284         verify(mQs, times(2)).setQsExpansion(0, 0, 0, 1);
285     }
286 
287     @Test
shadeExpanded_onKeyguard()288     public void shadeExpanded_onKeyguard() {
289         mStatusBarStateController.setState(KEYGUARD);
290         // set maxQsExpansion in NPVC
291         int maxQsExpansion = 123;
292         mQsController.setQs(mQs);
293         when(mQs.getDesiredHeight()).thenReturn(maxQsExpansion);
294 
295         int oldMaxHeight = mQsController.updateHeightsOnShadeLayoutChange();
296         mQsController.handleShadeLayoutChanged(oldMaxHeight);
297 
298         mQsController.setExpansionHeight(maxQsExpansion);
299         assertThat(mQsController.computeExpansionFraction()).isEqualTo(1f);
300     }
301 
302     @Test
handleTouch_splitShadeAndtouchXOutsideQs()303     public void handleTouch_splitShadeAndtouchXOutsideQs() {
304         enableSplitShade(true);
305 
306         assertThat(mQsController.handleTouch(createMotionEvent(
307                         QS_FRAME_WIDTH + 1, QS_FRAME_BOTTOM - 1, ACTION_DOWN),
308                 false, false)).isFalse();
309     }
310 
311     @Test
isOpenQsEvent_twoFingerDrag()312     public void isOpenQsEvent_twoFingerDrag() {
313         assertThat(mQsController.isOpenQsEvent(
314                 createMultitouchMotionEvent(ACTION_POINTER_DOWN))).isTrue();
315     }
316 
317     @Test
isOpenQsEvent_stylusButtonClickDrag()318     public void isOpenQsEvent_stylusButtonClickDrag() {
319         MotionEvent event = createMotionEvent(0, 0, ACTION_DOWN);
320         event.setButtonState(BUTTON_STYLUS_PRIMARY);
321 
322         assertThat(mQsController.isOpenQsEvent(event)).isTrue();
323     }
324 
325     @Test
isOpenQsEvent_mouseButtonClickDrag()326     public void isOpenQsEvent_mouseButtonClickDrag() {
327         MotionEvent event = createMotionEvent(0, 0, ACTION_DOWN);
328         event.setButtonState(BUTTON_SECONDARY);
329 
330         assertThat(mQsController.isOpenQsEvent(event)).isTrue();
331     }
332 
333     @Test
shadeClosed_onLockscreen_inSplitShade_setsQsNotVisible()334     public void shadeClosed_onLockscreen_inSplitShade_setsQsNotVisible() {
335         mQsController.setQs(mQs);
336         enableSplitShade(true);
337         lockScreen();
338 
339         closeLockedQS();
340 
341         assertQsVisible(false);
342     }
343 
344     @Test
shadeOpened_onLockscreen_inSplitShade_setsQsVisible()345     public void shadeOpened_onLockscreen_inSplitShade_setsQsVisible() {
346         mQsController.setQs(mQs);
347         enableSplitShade(true);
348         lockScreen();
349 
350         openLockedQS();
351 
352         assertQsVisible(true);
353     }
354 
355     @Test
shadeClosed_onLockscreen_inSingleShade_setsQsNotVisible()356     public void shadeClosed_onLockscreen_inSingleShade_setsQsNotVisible() {
357         mQsController.setQs(mQs);
358         enableSplitShade(false);
359         lockScreen();
360 
361         closeLockedQS();
362 
363         verify(mQs).setQsVisible(false);
364     }
365 
366     @Test
shadeOpened_onLockscreen_inSingleShade_setsQsVisible()367     public void shadeOpened_onLockscreen_inSingleShade_setsQsVisible() {
368         mQsController.setQs(mQs);
369         enableSplitShade(false);
370         lockScreen();
371 
372         openLockedQS();
373 
374         verify(mQs).setQsVisible(true);
375     }
376 
377     @Test
calculateBottomCornerRadius_scrimScaleMax()378     public void calculateBottomCornerRadius_scrimScaleMax() {
379         when(mScrimController.getBackScaling()).thenReturn(1.0f);
380         assertThat(mQsController.calculateBottomCornerRadius(0.0f)).isEqualTo(0);
381     }
382 
383     @Test
calculateBottomCornerRadius_scrimScaleMin()384     public void calculateBottomCornerRadius_scrimScaleMin() {
385         when(mScrimController.getBackScaling())
386                 .thenReturn(mNotificationPanelViewController.SHADE_BACK_ANIM_MIN_SCALE);
387         assertThat(mQsController.calculateBottomCornerRadius(0.0f))
388                 .isEqualTo(mQsController.getScrimCornerRadius());
389     }
390 
391     @Test
calculateBottomCornerRadius_scrimScaleCutoff()392     public void calculateBottomCornerRadius_scrimScaleCutoff() {
393         float ratio = 1 / mQsController.calculateBottomRadiusProgress();
394         float cutoffScale = 1 - mNotificationPanelViewController.SHADE_BACK_ANIM_MIN_SCALE / ratio;
395         when(mScrimController.getBackScaling())
396                 .thenReturn(cutoffScale);
397         assertThat(mQsController.calculateBottomCornerRadius(0.0f))
398                 .isEqualTo(mQsController.getScrimCornerRadius());
399     }
400 
lockScreen()401     private void lockScreen() {
402         mQsController.setBarState(KEYGUARD);
403     }
404 
openLockedQS()405     private void openLockedQS() {
406         when(mLockscreenShadeTransitionController.getQSDragProgress())
407                 .thenReturn((float) DEFAULT_HEIGHT);
408         mLockscreenShadeTransitionCallback.setTransitionToFullShadeAmount(
409                 /* pxAmount= */ DEFAULT_HEIGHT,
410                 /* animate=*/ false,
411                 /* delay= */ 0
412         );
413     }
414 
closeLockedQS()415     private void closeLockedQS() {
416         when(mLockscreenShadeTransitionController.getQSDragProgress()).thenReturn(0f);
417         mLockscreenShadeTransitionCallback.setTransitionToFullShadeAmount(
418                 /* pxAmount= */ 0,
419                 /* animate=*/ false,
420                 /* delay= */ 0
421         );
422     }
423 
setSplitShadeHeightProperties()424     private void setSplitShadeHeightProperties() {
425         // In split shade, min = max
426         when(mQs.getQsMinExpansionHeight()).thenReturn(DEFAULT_MIN_HEIGHT_SPLIT_SHADE);
427         when(mQs.getDesiredHeight()).thenReturn(DEFAULT_HEIGHT);
428         mQsController.updateMinHeight();
429         mQsController.onHeightChanged();
430     }
431 
setDefaultHeightProperties()432     private void setDefaultHeightProperties() {
433         when(mQs.getQsMinExpansionHeight()).thenReturn(DEFAULT_MIN_HEIGHT);
434         when(mQs.getDesiredHeight()).thenReturn(DEFAULT_HEIGHT);
435         mQsController.updateMinHeight();
436         mQsController.onHeightChanged();
437     }
438 
createMotionEvent(int x, int y, int action)439     private static MotionEvent createMotionEvent(int x, int y, int action) {
440         return MotionEvent.obtain(0, 0, action, x, y, 0);
441     }
442 
443     // Creates an empty multitouch event for now
createMultitouchMotionEvent(int action)444     private static MotionEvent createMultitouchMotionEvent(int action) {
445         return MotionEvent.obtain(0, 0, action, 2,
446                 new MotionEvent.PointerProperties[] {
447                         new MotionEvent.PointerProperties(),
448                         new MotionEvent.PointerProperties()
449                 },
450                 new MotionEvent.PointerCoords[] {
451                         new MotionEvent.PointerCoords(),
452                         new MotionEvent.PointerCoords()
453                 }, 0, 0, 0, 0, 0, 0, 0, 0);
454     }
455 
enableSplitShade(boolean enabled)456     private void enableSplitShade(boolean enabled) {
457         when(mResources.getBoolean(R.bool.config_use_split_notification_shade)).thenReturn(enabled);
458         mQsController.updateResources();
459         if (enabled) {
460             setSplitShadeHeightProperties();
461         } else {
462             setDefaultHeightProperties();
463         }
464     }
465 
setIsFullWidth(boolean fullWidth)466     private void setIsFullWidth(boolean fullWidth) {
467         mQsController.setNotificationPanelFullWidth(fullWidth);
468         triggerLayoutChange();
469     }
470 
triggerLayoutChange()471     private void triggerLayoutChange() {
472         int oldMaxHeight = mQsController.updateHeightsOnShadeLayoutChange();
473         mQsController.handleShadeLayoutChanged(oldMaxHeight);
474     }
475 
assertQsVisible(boolean visible)476     private void assertQsVisible(boolean visible) {
477         ArgumentCaptor<Boolean> visibilityCaptor = ArgumentCaptor.forClass(Boolean.class);
478         verify(mQs, atLeastOnce()).setQsVisible(visibilityCaptor.capture());
479         List<Boolean> allVisibilities = visibilityCaptor.getAllValues();
480         boolean lastVisibility = allVisibilities.get(allVisibilities.size() - 1);
481         assertThat(lastVisibility).isEqualTo(visible);
482     }
483 }
484