• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.dreams.touch;
18 
19 import static com.google.common.truth.Truth.assertThat;
20 
21 import static org.mockito.ArgumentMatchers.any;
22 import static org.mockito.ArgumentMatchers.anyFloat;
23 import static org.mockito.ArgumentMatchers.eq;
24 import static org.mockito.Mockito.never;
25 import static org.mockito.Mockito.reset;
26 import static org.mockito.Mockito.verify;
27 import static org.mockito.Mockito.when;
28 
29 import android.animation.AnimatorListenerAdapter;
30 import android.animation.ValueAnimator;
31 import android.graphics.Rect;
32 import android.graphics.Region;
33 import android.testing.AndroidTestingRunner;
34 import android.view.GestureDetector;
35 import android.view.GestureDetector.OnGestureListener;
36 import android.view.MotionEvent;
37 import android.view.VelocityTracker;
38 
39 import androidx.test.filters.SmallTest;
40 
41 import com.android.internal.logging.UiEventLogger;
42 import com.android.systemui.SysuiTestCase;
43 import com.android.systemui.dreams.touch.scrim.ScrimController;
44 import com.android.systemui.dreams.touch.scrim.ScrimManager;
45 import com.android.systemui.keyguard.shared.constants.KeyguardBouncerConstants;
46 import com.android.systemui.shade.ShadeExpansionChangeEvent;
47 import com.android.systemui.shared.system.InputChannelCompat;
48 import com.android.systemui.statusbar.NotificationShadeWindowController;
49 import com.android.systemui.statusbar.phone.CentralSurfaces;
50 import com.android.wm.shell.animation.FlingAnimationUtils;
51 
52 import org.junit.Before;
53 import org.junit.Test;
54 import org.junit.runner.RunWith;
55 import org.mockito.ArgumentCaptor;
56 import org.mockito.Mock;
57 import org.mockito.Mockito;
58 import org.mockito.MockitoAnnotations;
59 
60 import java.util.Optional;
61 
62 @SmallTest
63 @RunWith(AndroidTestingRunner.class)
64 public class BouncerSwipeTouchHandlerTest extends SysuiTestCase {
65     @Mock
66     CentralSurfaces mCentralSurfaces;
67 
68     @Mock
69     ScrimManager mScrimManager;
70 
71     @Mock
72     ScrimController mScrimController;
73 
74     @Mock
75     NotificationShadeWindowController mNotificationShadeWindowController;
76 
77     @Mock
78     FlingAnimationUtils mFlingAnimationUtils;
79 
80     @Mock
81     FlingAnimationUtils mFlingAnimationUtilsClosing;
82 
83     @Mock
84     DreamTouchHandler.TouchSession mTouchSession;
85 
86     BouncerSwipeTouchHandler mTouchHandler;
87 
88     @Mock
89     BouncerSwipeTouchHandler.ValueAnimatorCreator mValueAnimatorCreator;
90 
91     @Mock
92     ValueAnimator mValueAnimator;
93 
94     @Mock
95     BouncerSwipeTouchHandler.VelocityTrackerFactory mVelocityTrackerFactory;
96 
97     @Mock
98     VelocityTracker mVelocityTracker;
99 
100     @Mock
101     UiEventLogger mUiEventLogger;
102 
103     private static final float TOUCH_REGION = .3f;
104     private static final int SCREEN_WIDTH_PX = 1024;
105     private static final int SCREEN_HEIGHT_PX = 100;
106 
107     private static final Rect SCREEN_BOUNDS = new Rect(0, 0, 1024, 100);
108 
109     @Before
setup()110     public void setup() {
111         MockitoAnnotations.initMocks(this);
112         mTouchHandler = new BouncerSwipeTouchHandler(
113                 mScrimManager,
114                 Optional.of(mCentralSurfaces),
115                 mNotificationShadeWindowController,
116                 mValueAnimatorCreator,
117                 mVelocityTrackerFactory,
118                 mFlingAnimationUtils,
119                 mFlingAnimationUtilsClosing,
120                 TOUCH_REGION,
121                 mUiEventLogger);
122 
123         when(mScrimManager.getCurrentController()).thenReturn(mScrimController);
124         when(mCentralSurfaces.isBouncerShowing()).thenReturn(false);
125         when(mValueAnimatorCreator.create(anyFloat(), anyFloat())).thenReturn(mValueAnimator);
126         when(mVelocityTrackerFactory.obtain()).thenReturn(mVelocityTracker);
127         when(mFlingAnimationUtils.getMinVelocityPxPerSecond()).thenReturn(Float.MAX_VALUE);
128         when(mTouchSession.getBounds()).thenReturn(SCREEN_BOUNDS);
129     }
130 
131     /**
132      * Ensures expansion only happens when touch down happens in valid part of the screen.
133      */
134     @Test
testSessionStart()135     public void testSessionStart() {
136         final Region region = Region.obtain();
137         mTouchHandler.getTouchInitiationRegion(SCREEN_BOUNDS, region);
138 
139         final Rect bounds = region.getBounds();
140 
141         final Rect expected = new Rect();
142 
143         expected.set(0, Math.round(SCREEN_HEIGHT_PX * (1 - TOUCH_REGION)), SCREEN_WIDTH_PX,
144                 SCREEN_HEIGHT_PX);
145 
146         assertThat(bounds).isEqualTo(expected);
147 
148         mTouchHandler.onSessionStart(mTouchSession);
149         verify(mNotificationShadeWindowController).setForcePluginOpen(eq(true), any());
150         ArgumentCaptor<InputChannelCompat.InputEventListener> eventListenerCaptor =
151                 ArgumentCaptor.forClass(InputChannelCompat.InputEventListener.class);
152         ArgumentCaptor<GestureDetector.OnGestureListener> gestureListenerCaptor =
153                 ArgumentCaptor.forClass(GestureDetector.OnGestureListener.class);
154         verify(mTouchSession).registerGestureListener(gestureListenerCaptor.capture());
155         verify(mTouchSession).registerInputListener(eventListenerCaptor.capture());
156 
157         // A touch within range at the bottom of the screen should trigger listening
158         assertThat(gestureListenerCaptor.getValue()
159                 .onScroll(Mockito.mock(MotionEvent.class),
160                         Mockito.mock(MotionEvent.class),
161                         1,
162                         2)).isTrue();
163     }
164 
165     private enum Direction {
166         DOWN,
167         UP,
168     }
169 
170     /**
171      * Makes sure swiping up when bouncer initially showing doesn't change the expansion amount.
172      */
173     @Test
testSwipeUp_whenBouncerInitiallyShowing_doesNotSetExpansion()174     public void testSwipeUp_whenBouncerInitiallyShowing_doesNotSetExpansion() {
175         when(mCentralSurfaces.isBouncerShowing()).thenReturn(true);
176 
177         mTouchHandler.onSessionStart(mTouchSession);
178         ArgumentCaptor<GestureDetector.OnGestureListener> gestureListenerCaptor =
179                 ArgumentCaptor.forClass(GestureDetector.OnGestureListener.class);
180         verify(mTouchSession).registerGestureListener(gestureListenerCaptor.capture());
181 
182         final OnGestureListener gestureListener = gestureListenerCaptor.getValue();
183 
184         final float percent = .3f;
185         final float distanceY = SCREEN_HEIGHT_PX * percent;
186 
187         // Swiping up near the top of the screen where the touch initiation region is.
188         final MotionEvent event1 = MotionEvent.obtain(0, 0, MotionEvent.ACTION_MOVE,
189                 0, distanceY, 0);
190         final MotionEvent event2 = MotionEvent.obtain(0, 0, MotionEvent.ACTION_MOVE,
191                 0, 0, 0);
192 
193         assertThat(gestureListener.onScroll(event1, event2, 0, distanceY))
194                 .isTrue();
195 
196         verify(mScrimController, never()).expand(any());
197     }
198 
199     /**
200      * Makes sure swiping down when bouncer initially hidden doesn't change the expansion amount.
201      */
202     @Test
testSwipeDown_whenBouncerInitiallyHidden_doesNotSetExpansion()203     public void testSwipeDown_whenBouncerInitiallyHidden_doesNotSetExpansion() {
204         mTouchHandler.onSessionStart(mTouchSession);
205         ArgumentCaptor<GestureDetector.OnGestureListener> gestureListenerCaptor =
206                 ArgumentCaptor.forClass(GestureDetector.OnGestureListener.class);
207         verify(mTouchSession).registerGestureListener(gestureListenerCaptor.capture());
208 
209         final OnGestureListener gestureListener = gestureListenerCaptor.getValue();
210 
211         final float percent = .15f;
212         final float distanceY = SCREEN_HEIGHT_PX * percent;
213 
214         // Swiping down near the bottom of the screen where the touch initiation region is.
215         final MotionEvent event1 = MotionEvent.obtain(0, 0, MotionEvent.ACTION_MOVE,
216                 0, SCREEN_HEIGHT_PX - distanceY, 0);
217         final MotionEvent event2 = MotionEvent.obtain(0, 0, MotionEvent.ACTION_MOVE,
218                 0, SCREEN_HEIGHT_PX, 0);
219 
220         assertThat(gestureListener.onScroll(event1, event2, 0, distanceY))
221                 .isTrue();
222 
223         verify(mScrimController, never()).expand(any());
224     }
225 
226     /**
227      * Makes sure the expansion amount is proportional to (1 - scroll).
228      */
229     @Test
testSwipeUp_setsCorrectExpansionAmount()230     public void testSwipeUp_setsCorrectExpansionAmount() {
231         mTouchHandler.onSessionStart(mTouchSession);
232         ArgumentCaptor<GestureDetector.OnGestureListener> gestureListenerCaptor =
233                 ArgumentCaptor.forClass(GestureDetector.OnGestureListener.class);
234         verify(mTouchSession).registerGestureListener(gestureListenerCaptor.capture());
235 
236         final OnGestureListener gestureListener = gestureListenerCaptor.getValue();
237 
238         verifyScroll(.3f, Direction.UP, false, gestureListener);
239 
240         // Ensure that subsequent gestures are treated as expanding even if the bouncer state
241         // changes.
242         when(mCentralSurfaces.isBouncerShowing()).thenReturn(true);
243         verifyScroll(.7f, Direction.UP, false, gestureListener);
244     }
245 
246     /**
247      * Makes sure the expansion amount is proportional to scroll.
248      */
249     @Test
testSwipeDown_setsCorrectExpansionAmount()250     public void testSwipeDown_setsCorrectExpansionAmount() {
251         when(mCentralSurfaces.isBouncerShowing()).thenReturn(true);
252 
253         mTouchHandler.onSessionStart(mTouchSession);
254         ArgumentCaptor<GestureDetector.OnGestureListener> gestureListenerCaptor =
255                 ArgumentCaptor.forClass(GestureDetector.OnGestureListener.class);
256         verify(mTouchSession).registerGestureListener(gestureListenerCaptor.capture());
257 
258         final OnGestureListener gestureListener = gestureListenerCaptor.getValue();
259 
260         verifyScroll(.3f, Direction.DOWN, true, gestureListener);
261 
262         // Ensure that subsequent gestures are treated as collapsing even if the bouncer state
263         // changes.
264         when(mCentralSurfaces.isBouncerShowing()).thenReturn(false);
265         verifyScroll(.7f, Direction.DOWN, true, gestureListener);
266     }
267 
verifyScroll(float percent, Direction direction, boolean isBouncerInitiallyShowing, GestureDetector.OnGestureListener gestureListener)268     private void verifyScroll(float percent, Direction direction,
269             boolean isBouncerInitiallyShowing, GestureDetector.OnGestureListener gestureListener) {
270         final float distanceY = SCREEN_HEIGHT_PX * percent;
271 
272         final MotionEvent event1 = MotionEvent.obtain(0, 0, MotionEvent.ACTION_MOVE,
273                 0, direction == Direction.UP ? SCREEN_HEIGHT_PX : 0, 0);
274         final MotionEvent event2 = MotionEvent.obtain(0, 0, MotionEvent.ACTION_MOVE,
275                 0, direction == Direction.UP ? SCREEN_HEIGHT_PX - distanceY : distanceY, 0);
276 
277         reset(mScrimController);
278         assertThat(gestureListener.onScroll(event1, event2, 0, distanceY))
279                 .isTrue();
280 
281         // Ensure only called once
282         verify(mScrimController).expand(any());
283 
284         final float expansion = isBouncerInitiallyShowing ? percent : 1 - percent;
285         final float dragDownAmount = event2.getY() - event1.getY();
286 
287         // Ensure correct expansion passed in.
288         ShadeExpansionChangeEvent event =
289                 new ShadeExpansionChangeEvent(
290                         expansion, /* expanded= */ false, /* tracking= */ true, dragDownAmount);
291         verify(mScrimController).expand(event);
292     }
293 
294     /**
295      * Tests that ending an upward swipe before the set threshold leads to bouncer collapsing down.
296      */
297     @Test
testSwipeUpPositionBelowThreshold_collapsesBouncer()298     public void testSwipeUpPositionBelowThreshold_collapsesBouncer() {
299         final float swipeUpPercentage = .3f;
300         final float expansion = 1 - swipeUpPercentage;
301         // The upward velocity is ignored.
302         final float velocityY = -1;
303         swipeToPosition(swipeUpPercentage, Direction.UP, velocityY);
304 
305         verify(mValueAnimatorCreator).create(eq(expansion),
306                 eq(KeyguardBouncerConstants.EXPANSION_HIDDEN));
307         verify(mValueAnimator, never()).addListener(any());
308 
309         verify(mFlingAnimationUtilsClosing).apply(eq(mValueAnimator),
310                 eq(SCREEN_HEIGHT_PX * expansion),
311                 eq(SCREEN_HEIGHT_PX * KeyguardBouncerConstants.EXPANSION_HIDDEN),
312                 eq(velocityY), eq((float) SCREEN_HEIGHT_PX));
313         verify(mValueAnimator).start();
314         verify(mUiEventLogger, never()).log(any());
315     }
316 
317     /**
318      * Tests that ending an upward swipe above the set threshold will continue the expansion.
319      */
320     @Test
testSwipeUpPositionAboveThreshold_expandsBouncer()321     public void testSwipeUpPositionAboveThreshold_expandsBouncer() {
322         final float swipeUpPercentage = .7f;
323         final float expansion = 1 - swipeUpPercentage;
324         // The downward velocity is ignored.
325         final float velocityY = 1;
326         swipeToPosition(swipeUpPercentage, Direction.UP, velocityY);
327 
328         verify(mValueAnimatorCreator).create(eq(expansion),
329                 eq(KeyguardBouncerConstants.EXPANSION_VISIBLE));
330 
331         ArgumentCaptor<AnimatorListenerAdapter> endAnimationListenerCaptor =
332                 ArgumentCaptor.forClass(AnimatorListenerAdapter.class);
333         verify(mValueAnimator).addListener(endAnimationListenerCaptor.capture());
334         AnimatorListenerAdapter endAnimationListener = endAnimationListenerCaptor.getValue();
335 
336         verify(mFlingAnimationUtils).apply(eq(mValueAnimator), eq(SCREEN_HEIGHT_PX * expansion),
337                 eq(SCREEN_HEIGHT_PX * KeyguardBouncerConstants.EXPANSION_VISIBLE),
338                 eq(velocityY), eq((float) SCREEN_HEIGHT_PX));
339         verify(mValueAnimator).start();
340         verify(mUiEventLogger).log(BouncerSwipeTouchHandler.DreamEvent.DREAM_SWIPED);
341 
342         endAnimationListener.onAnimationEnd(mValueAnimator);
343         verify(mUiEventLogger).log(BouncerSwipeTouchHandler.DreamEvent.DREAM_BOUNCER_FULLY_VISIBLE);
344     }
345 
346     /**
347      * Tests that ending a downward swipe above the set threshold will continue the expansion,
348      * but will not trigger logging of the DREAM_SWIPED event.
349      */
350     @Test
testSwipeDownPositionAboveThreshold_expandsBouncer_doesNotLog()351     public void testSwipeDownPositionAboveThreshold_expandsBouncer_doesNotLog() {
352         when(mCentralSurfaces.isBouncerShowing()).thenReturn(true);
353 
354         final float swipeDownPercentage = .3f;
355         // The downward velocity is ignored.
356         final float velocityY = 1;
357         swipeToPosition(swipeDownPercentage, Direction.DOWN, velocityY);
358 
359         verify(mValueAnimatorCreator).create(eq(swipeDownPercentage),
360                 eq(KeyguardBouncerConstants.EXPANSION_VISIBLE));
361         verify(mValueAnimator, never()).addListener(any());
362 
363         verify(mFlingAnimationUtils).apply(eq(mValueAnimator),
364                 eq(SCREEN_HEIGHT_PX * swipeDownPercentage),
365                 eq(SCREEN_HEIGHT_PX * KeyguardBouncerConstants.EXPANSION_VISIBLE),
366                 eq(velocityY), eq((float) SCREEN_HEIGHT_PX));
367         verify(mValueAnimator).start();
368         verify(mUiEventLogger, never()).log(any());
369     }
370 
371     /**
372      * Tests that swiping down with a speed above the set threshold leads to bouncer collapsing
373      * down.
374      */
375     @Test
testSwipeDownVelocityAboveMin_collapsesBouncer()376     public void testSwipeDownVelocityAboveMin_collapsesBouncer() {
377         when(mCentralSurfaces.isBouncerShowing()).thenReturn(true);
378         when(mFlingAnimationUtils.getMinVelocityPxPerSecond()).thenReturn((float) 0);
379 
380         // The ending position above the set threshold is ignored.
381         final float swipeDownPercentage = .3f;
382         final float velocityY = 1;
383         swipeToPosition(swipeDownPercentage, Direction.DOWN, velocityY);
384 
385         verify(mValueAnimatorCreator).create(eq(swipeDownPercentage),
386                 eq(KeyguardBouncerConstants.EXPANSION_HIDDEN));
387         verify(mValueAnimator, never()).addListener(any());
388 
389         verify(mFlingAnimationUtilsClosing).apply(eq(mValueAnimator),
390                 eq(SCREEN_HEIGHT_PX * swipeDownPercentage),
391                 eq(SCREEN_HEIGHT_PX * KeyguardBouncerConstants.EXPANSION_HIDDEN),
392                 eq(velocityY), eq((float) SCREEN_HEIGHT_PX));
393         verify(mValueAnimator).start();
394         verify(mUiEventLogger, never()).log(any());
395     }
396 
397     /**
398      * Tests that swiping up with a speed above the set threshold will continue the expansion.
399      */
400     @Test
testSwipeUpVelocityAboveMin_expandsBouncer()401     public void testSwipeUpVelocityAboveMin_expandsBouncer() {
402         when(mFlingAnimationUtils.getMinVelocityPxPerSecond()).thenReturn((float) 0);
403 
404         // The ending position below the set threshold is ignored.
405         final float swipeUpPercentage = .3f;
406         final float expansion = 1 - swipeUpPercentage;
407         final float velocityY = -1;
408         swipeToPosition(swipeUpPercentage, Direction.UP, velocityY);
409 
410         verify(mValueAnimatorCreator).create(eq(expansion),
411                 eq(KeyguardBouncerConstants.EXPANSION_VISIBLE));
412 
413         ArgumentCaptor<AnimatorListenerAdapter> endAnimationListenerCaptor =
414                 ArgumentCaptor.forClass(AnimatorListenerAdapter.class);
415         verify(mValueAnimator).addListener(endAnimationListenerCaptor.capture());
416         AnimatorListenerAdapter endAnimationListener = endAnimationListenerCaptor.getValue();
417 
418         verify(mFlingAnimationUtils).apply(eq(mValueAnimator), eq(SCREEN_HEIGHT_PX * expansion),
419                 eq(SCREEN_HEIGHT_PX * KeyguardBouncerConstants.EXPANSION_VISIBLE),
420                 eq(velocityY), eq((float) SCREEN_HEIGHT_PX));
421         verify(mValueAnimator).start();
422         verify(mUiEventLogger).log(BouncerSwipeTouchHandler.DreamEvent.DREAM_SWIPED);
423 
424         endAnimationListener.onAnimationEnd(mValueAnimator);
425         verify(mUiEventLogger).log(BouncerSwipeTouchHandler.DreamEvent.DREAM_BOUNCER_FULLY_VISIBLE);
426     }
427 
428     /**
429      * Ensures {@link CentralSurfaces}
430      */
431     @Test
testInformBouncerShowingOnExpand()432     public void testInformBouncerShowingOnExpand() {
433         swipeToPosition(1f, Direction.UP, 0);
434     }
435 
436     /**
437      * Ensures {@link CentralSurfaces}
438      */
439     @Test
testInformBouncerHidingOnCollapse()440     public void testInformBouncerHidingOnCollapse() {
441         // Must swipe up to set initial state.
442         swipeToPosition(1f, Direction.UP, 0);
443         Mockito.clearInvocations(mCentralSurfaces);
444 
445         swipeToPosition(0f, Direction.DOWN, 0);
446     }
447 
448     @Test
testTouchSessionOnRemovedCalledTwice()449     public void testTouchSessionOnRemovedCalledTwice() {
450         mTouchHandler.onSessionStart(mTouchSession);
451         ArgumentCaptor<DreamTouchHandler.TouchSession.Callback> onRemovedCallbackCaptor =
452                 ArgumentCaptor.forClass(DreamTouchHandler.TouchSession.Callback.class);
453         verify(mTouchSession).registerCallback(onRemovedCallbackCaptor.capture());
454         onRemovedCallbackCaptor.getValue().onRemoved();
455         onRemovedCallbackCaptor.getValue().onRemoved();
456     }
457 
swipeToPosition(float percent, Direction direction, float velocityY)458     private void swipeToPosition(float percent, Direction direction, float velocityY) {
459         Mockito.clearInvocations(mTouchSession);
460         mTouchHandler.onSessionStart(mTouchSession);
461         ArgumentCaptor<GestureDetector.OnGestureListener> gestureListenerCaptor =
462                 ArgumentCaptor.forClass(GestureDetector.OnGestureListener.class);
463         verify(mTouchSession).registerGestureListener(gestureListenerCaptor.capture());
464         ArgumentCaptor<InputChannelCompat.InputEventListener> inputEventListenerCaptor =
465                 ArgumentCaptor.forClass(InputChannelCompat.InputEventListener.class);
466         verify(mTouchSession).registerInputListener(inputEventListenerCaptor.capture());
467 
468         when(mVelocityTracker.getYVelocity()).thenReturn(velocityY);
469 
470         final float distanceY = SCREEN_HEIGHT_PX * percent;
471 
472         final MotionEvent event1 = MotionEvent.obtain(0, 0, MotionEvent.ACTION_MOVE,
473                 0, direction == Direction.UP ? SCREEN_HEIGHT_PX : 0, 0);
474         final MotionEvent event2 = MotionEvent.obtain(0, 0, MotionEvent.ACTION_MOVE,
475                 0, direction == Direction.UP ? SCREEN_HEIGHT_PX - distanceY : distanceY, 0);
476 
477         assertThat(gestureListenerCaptor.getValue().onScroll(event1, event2, 0, distanceY))
478                 .isTrue();
479 
480         final MotionEvent upEvent = MotionEvent.obtain(0, 0, MotionEvent.ACTION_UP,
481                 0, 0, 0);
482 
483         inputEventListenerCaptor.getValue().onInputEvent(upEvent);
484     }
485 }
486