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