1 /* 2 * Copyright 2024 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.server.input.debug; 18 19 import static android.view.InputDevice.SOURCE_MOUSE; 20 import static android.view.InputDevice.SOURCE_TOUCHSCREEN; 21 22 import static org.junit.Assert.assertEquals; 23 import static org.mockito.ArgumentMatchers.any; 24 import static org.mockito.Mockito.doAnswer; 25 import static org.mockito.Mockito.times; 26 import static org.mockito.Mockito.verify; 27 import static org.mockito.Mockito.when; 28 29 import android.graphics.Color; 30 import android.graphics.Rect; 31 import android.graphics.drawable.ColorDrawable; 32 import android.hardware.input.InputManager; 33 import android.testing.TestableContext; 34 import android.view.InputDevice; 35 import android.view.MotionEvent; 36 import android.view.View; 37 import android.view.ViewConfiguration; 38 import android.view.WindowInsets; 39 import android.view.WindowManager; 40 import android.view.WindowMetrics; 41 import android.widget.TextView; 42 43 import androidx.test.platform.app.InstrumentationRegistry; 44 import androidx.test.runner.AndroidJUnit4; 45 46 import com.android.cts.input.MotionEventBuilder; 47 import com.android.cts.input.PointerBuilder; 48 import com.android.server.input.TouchpadFingerState; 49 import com.android.server.input.TouchpadHardwareProperties; 50 import com.android.server.input.TouchpadHardwareState; 51 52 import org.junit.Before; 53 import org.junit.Rule; 54 import org.junit.Test; 55 import org.junit.runner.RunWith; 56 import org.mockito.ArgumentCaptor; 57 import org.mockito.Mock; 58 import org.mockito.MockitoAnnotations; 59 60 import java.util.function.Consumer; 61 62 /** 63 * Build/Install/Run: 64 * atest TouchpadDebugViewTest 65 */ 66 @RunWith(AndroidJUnit4.class) 67 public class TouchpadDebugViewTest { 68 private static final int TOUCHPAD_DEVICE_ID = 60; 69 70 private TouchpadDebugView mTouchpadDebugView; 71 private WindowManager.LayoutParams mWindowLayoutParams; 72 73 @Rule 74 public final TestableContext mTestableContext = 75 new TestableContext(InstrumentationRegistry.getInstrumentation().getContext()); 76 77 @Mock 78 WindowManager mWindowManager; 79 @Mock 80 InputManager mInputManager; 81 82 Rect mWindowBounds; 83 WindowMetrics mWindowMetrics; 84 85 @Before setUp()86 public void setUp() { 87 MockitoAnnotations.initMocks(this); 88 mTestableContext.addMockSystemService(WindowManager.class, mWindowManager); 89 mTestableContext.addMockSystemService(InputManager.class, mInputManager); 90 91 mWindowBounds = new Rect(0, 0, 2560, 1600); 92 mWindowMetrics = new WindowMetrics(mWindowBounds, new WindowInsets(mWindowBounds), 1.0f); 93 94 when(mWindowManager.getCurrentWindowMetrics()).thenReturn(mWindowMetrics); 95 96 InputDevice inputDevice = new InputDevice.Builder() 97 .setId(TOUCHPAD_DEVICE_ID) 98 .setSources(InputDevice.SOURCE_TOUCHPAD | SOURCE_MOUSE) 99 .setName("Test Device " + TOUCHPAD_DEVICE_ID) 100 .build(); 101 102 when(mInputManager.getInputDevice(TOUCHPAD_DEVICE_ID)).thenReturn(inputDevice); 103 104 Consumer<Integer> touchpadSwitchHandler = id -> {}; 105 106 mTouchpadDebugView = new TouchpadDebugView(mTestableContext, TOUCHPAD_DEVICE_ID, 107 new TouchpadHardwareProperties.Builder(0f, 0f, 500f, 108 500f, 45f, 47f, -4f, 5f, (short) 10, true, 109 true).build(), touchpadSwitchHandler); 110 111 mTouchpadDebugView.measure( 112 View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED), 113 View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED) 114 ); 115 116 doAnswer(invocation -> { 117 mTouchpadDebugView.layout(0, 0, mTouchpadDebugView.getMeasuredWidth(), 118 mTouchpadDebugView.getMeasuredHeight()); 119 return null; 120 }).when(mWindowManager).addView(any(), any()); 121 122 doAnswer(invocation -> { 123 mTouchpadDebugView.layout(0, 0, mTouchpadDebugView.getMeasuredWidth(), 124 mTouchpadDebugView.getMeasuredHeight()); 125 return null; 126 }).when(mWindowManager).updateViewLayout(any(), any()); 127 128 mWindowLayoutParams = mTouchpadDebugView.getWindowLayoutParams(); 129 mWindowLayoutParams.x = 20; 130 mWindowLayoutParams.y = 20; 131 132 mTouchpadDebugView.layout(0, 0, mTouchpadDebugView.getMeasuredWidth(), 133 mTouchpadDebugView.getMeasuredHeight()); 134 } 135 136 @Test testDragView()137 public void testDragView() { 138 // Initial view position relative to screen. 139 int initialX = mWindowLayoutParams.x; 140 int initialY = mWindowLayoutParams.y; 141 142 float offsetX = ViewConfiguration.get(mTestableContext).getScaledTouchSlop() + 10; 143 float offsetY = ViewConfiguration.get(mTestableContext).getScaledTouchSlop() + 10; 144 145 // Simulate ACTION_DOWN event (initial touch). 146 MotionEvent actionDown = new MotionEventBuilder(MotionEvent.ACTION_DOWN, SOURCE_TOUCHSCREEN) 147 .pointer(new PointerBuilder(0, MotionEvent.TOOL_TYPE_FINGER) 148 .x(40f) 149 .y(40f) 150 ) 151 .build(); 152 mTouchpadDebugView.dispatchTouchEvent(actionDown); 153 154 verify(mWindowManager, times(0)).updateViewLayout(any(), any()); 155 156 // Simulate ACTION_MOVE event (dragging to the right). 157 MotionEvent actionMove = new MotionEventBuilder(MotionEvent.ACTION_MOVE, SOURCE_TOUCHSCREEN) 158 .pointer(new PointerBuilder(0, MotionEvent.TOOL_TYPE_FINGER) 159 .x(40f + offsetX) 160 .y(40f + offsetY) 161 ) 162 .build(); 163 mTouchpadDebugView.dispatchTouchEvent(actionMove); 164 165 ArgumentCaptor<WindowManager.LayoutParams> mWindowLayoutParamsCaptor = 166 ArgumentCaptor.forClass(WindowManager.LayoutParams.class); 167 verify(mWindowManager).updateViewLayout(any(), mWindowLayoutParamsCaptor.capture()); 168 169 // Verify position after ACTION_MOVE 170 assertEquals(initialX + (long) offsetX, mWindowLayoutParamsCaptor.getValue().x); 171 assertEquals(initialY + (long) offsetY, mWindowLayoutParamsCaptor.getValue().y); 172 173 // Simulate ACTION_UP event (release touch). 174 MotionEvent actionUp = new MotionEventBuilder(MotionEvent.ACTION_UP, SOURCE_TOUCHSCREEN) 175 .pointer(new PointerBuilder(0, MotionEvent.TOOL_TYPE_FINGER) 176 .x(40f + offsetX) 177 .y(40f + offsetY) 178 ) 179 .build(); 180 mTouchpadDebugView.dispatchTouchEvent(actionUp); 181 182 assertEquals(initialX + (long) offsetX, mWindowLayoutParamsCaptor.getValue().x); 183 assertEquals(initialY + (long) offsetY, mWindowLayoutParamsCaptor.getValue().y); 184 } 185 186 @Test testDragViewOutOfBounds()187 public void testDragViewOutOfBounds() { 188 int initialX = mWindowLayoutParams.x; 189 int initialY = mWindowLayoutParams.y; 190 191 MotionEvent actionDown = new MotionEventBuilder(MotionEvent.ACTION_DOWN, SOURCE_TOUCHSCREEN) 192 .pointer(new PointerBuilder(0, MotionEvent.TOOL_TYPE_FINGER) 193 .x(initialX + 10f) 194 .y(initialY + 10f) 195 ) 196 .build(); 197 mTouchpadDebugView.dispatchTouchEvent(actionDown); 198 199 verify(mWindowManager, times(0)).updateViewLayout(any(), any()); 200 201 // Simulate ACTION_MOVE event (dragging far to the right and bottom, beyond screen bounds) 202 MotionEvent actionMove = new MotionEventBuilder(MotionEvent.ACTION_MOVE, SOURCE_TOUCHSCREEN) 203 .pointer(new PointerBuilder(0, MotionEvent.TOOL_TYPE_FINGER) 204 .x(mWindowBounds.width() + mTouchpadDebugView.getWidth()) 205 .y(mWindowBounds.height() + mTouchpadDebugView.getHeight()) 206 ) 207 .build(); 208 mTouchpadDebugView.dispatchTouchEvent(actionMove); 209 210 ArgumentCaptor<WindowManager.LayoutParams> mWindowLayoutParamsCaptor = 211 ArgumentCaptor.forClass(WindowManager.LayoutParams.class); 212 verify(mWindowManager).updateViewLayout(any(), mWindowLayoutParamsCaptor.capture()); 213 214 // Verify the view has been clamped to the right and bottom edges of the screen 215 assertEquals(mWindowBounds.width() - mTouchpadDebugView.getWidth(), 216 mWindowLayoutParamsCaptor.getValue().x); 217 assertEquals(mWindowBounds.height() - mTouchpadDebugView.getHeight(), 218 mWindowLayoutParamsCaptor.getValue().y); 219 220 MotionEvent actionUp = new MotionEventBuilder(MotionEvent.ACTION_UP, SOURCE_TOUCHSCREEN) 221 .pointer(new PointerBuilder(0, MotionEvent.TOOL_TYPE_FINGER) 222 .x(mWindowBounds.width() + mTouchpadDebugView.getWidth()) 223 .y(mWindowBounds.height() + mTouchpadDebugView.getHeight()) 224 ) 225 .build(); 226 mTouchpadDebugView.dispatchTouchEvent(actionUp); 227 228 // Verify the view has been clamped to the right and bottom edges of the screen 229 assertEquals(mWindowBounds.width() - mTouchpadDebugView.getWidth(), 230 mWindowLayoutParamsCaptor.getValue().x); 231 assertEquals(mWindowBounds.height() - mTouchpadDebugView.getHeight(), 232 mWindowLayoutParamsCaptor.getValue().y); 233 } 234 235 @Test testSlopOffset()236 public void testSlopOffset() { 237 int initialX = mWindowLayoutParams.x; 238 int initialY = mWindowLayoutParams.y; 239 240 float offsetX = ViewConfiguration.get(mTestableContext).getScaledTouchSlop() / 2.0f; 241 float offsetY = -(ViewConfiguration.get(mTestableContext).getScaledTouchSlop() / 2.0f); 242 243 MotionEvent actionDown = new MotionEventBuilder(MotionEvent.ACTION_DOWN, SOURCE_TOUCHSCREEN) 244 .pointer(new PointerBuilder(0, MotionEvent.TOOL_TYPE_FINGER) 245 .x(initialX) 246 .y(initialY) 247 ) 248 .build(); 249 mTouchpadDebugView.dispatchTouchEvent(actionDown); 250 251 MotionEvent actionMove = new MotionEventBuilder(MotionEvent.ACTION_MOVE, SOURCE_TOUCHSCREEN) 252 .pointer(new PointerBuilder(0, MotionEvent.TOOL_TYPE_FINGER) 253 .x(initialX + offsetX) 254 .y(initialY + offsetY) 255 ) 256 .build(); 257 mTouchpadDebugView.dispatchTouchEvent(actionMove); 258 259 MotionEvent actionUp = new MotionEventBuilder(MotionEvent.ACTION_UP, SOURCE_TOUCHSCREEN) 260 .pointer(new PointerBuilder(0, MotionEvent.TOOL_TYPE_FINGER) 261 .x(initialX) 262 .y(initialY) 263 ) 264 .build(); 265 mTouchpadDebugView.dispatchTouchEvent(actionUp); 266 267 // In this case the updateViewLayout() method wouldn't be called because the drag 268 // distance hasn't exceeded the slop 269 verify(mWindowManager, times(0)).updateViewLayout(any(), any()); 270 } 271 272 @Test testViewReturnsToInitialPositionOnCancel()273 public void testViewReturnsToInitialPositionOnCancel() { 274 int initialX = mWindowLayoutParams.x; 275 int initialY = mWindowLayoutParams.y; 276 277 float offsetX = 50; 278 float offsetY = 50; 279 280 MotionEvent actionDown = new MotionEventBuilder(MotionEvent.ACTION_DOWN, SOURCE_TOUCHSCREEN) 281 .pointer(new PointerBuilder(0, MotionEvent.TOOL_TYPE_FINGER) 282 .x(initialX) 283 .y(initialY) 284 ) 285 .build(); 286 mTouchpadDebugView.dispatchTouchEvent(actionDown); 287 288 MotionEvent actionMove = new MotionEventBuilder(MotionEvent.ACTION_MOVE, SOURCE_TOUCHSCREEN) 289 .pointer(new PointerBuilder(0, MotionEvent.TOOL_TYPE_FINGER) 290 .x(initialX + offsetX) 291 .y(initialY + offsetY) 292 ) 293 .build(); 294 mTouchpadDebugView.dispatchTouchEvent(actionMove); 295 296 ArgumentCaptor<WindowManager.LayoutParams> mWindowLayoutParamsCaptor = 297 ArgumentCaptor.forClass(WindowManager.LayoutParams.class); 298 verify(mWindowManager).updateViewLayout(any(), mWindowLayoutParamsCaptor.capture()); 299 300 assertEquals(initialX + (long) offsetX, mWindowLayoutParamsCaptor.getValue().x); 301 assertEquals(initialY + (long) offsetY, mWindowLayoutParamsCaptor.getValue().y); 302 303 // Simulate ACTION_CANCEL event (canceling the touch event stream) 304 MotionEvent actionCancel = new MotionEventBuilder(MotionEvent.ACTION_CANCEL, 305 SOURCE_TOUCHSCREEN) 306 .pointer(new PointerBuilder(0, MotionEvent.TOOL_TYPE_FINGER) 307 .x(initialX + offsetX) 308 .y(initialY + offsetY) 309 ) 310 .build(); 311 mTouchpadDebugView.dispatchTouchEvent(actionCancel); 312 313 // Verify the view returns to its initial position 314 verify(mWindowManager, times(2)).updateViewLayout(any(), 315 mWindowLayoutParamsCaptor.capture()); 316 assertEquals(initialX, mWindowLayoutParamsCaptor.getValue().x); 317 assertEquals(initialY, mWindowLayoutParamsCaptor.getValue().y); 318 } 319 320 @Test testTouchpadClick()321 public void testTouchpadClick() { 322 View child = mTouchpadDebugView.getChildAt(0); 323 324 mTouchpadDebugView.updateHardwareState( 325 new TouchpadHardwareState(0, 1 /* buttonsDown */, 0, 0, 326 new TouchpadFingerState[0]), TOUCHPAD_DEVICE_ID); 327 328 assertEquals(((ColorDrawable) child.getBackground()).getColor(), 329 Color.parseColor("#769763")); 330 331 mTouchpadDebugView.updateHardwareState( 332 new TouchpadHardwareState(0, 0 /* buttonsDown */, 0, 0, 333 new TouchpadFingerState[0]), TOUCHPAD_DEVICE_ID); 334 335 assertEquals(((ColorDrawable) child.getBackground()).getColor(), 336 Color.parseColor("#5455A9")); 337 338 mTouchpadDebugView.updateHardwareState( 339 new TouchpadHardwareState(0, 1 /* buttonsDown */, 0, 0, 340 new TouchpadFingerState[0]), TOUCHPAD_DEVICE_ID); 341 342 assertEquals(((ColorDrawable) child.getBackground()).getColor(), 343 Color.parseColor("#769763")); 344 345 // Color should not change because hardware state of a different touchpad 346 mTouchpadDebugView.updateHardwareState( 347 new TouchpadHardwareState(0, 0 /* buttonsDown */, 0, 0, 348 new TouchpadFingerState[0]), TOUCHPAD_DEVICE_ID + 1); 349 350 assertEquals(((ColorDrawable) child.getBackground()).getColor(), 351 Color.parseColor("#769763")); 352 } 353 354 @Test testTouchpadGesture()355 public void testTouchpadGesture() { 356 int gestureType = 3; 357 TextView child = mTouchpadDebugView.getGestureInfoView(); 358 359 mTouchpadDebugView.updateGestureInfo(gestureType, TOUCHPAD_DEVICE_ID); 360 assertEquals(child.getText().toString(), TouchpadDebugView.getGestureText(gestureType)); 361 362 gestureType = 6; 363 mTouchpadDebugView.updateGestureInfo(gestureType, TOUCHPAD_DEVICE_ID); 364 assertEquals(child.getText().toString(), TouchpadDebugView.getGestureText(gestureType)); 365 } 366 367 @Test testTwoFingerDrag()368 public void testTwoFingerDrag() { 369 float offsetX = ViewConfiguration.get(mTestableContext).getScaledTouchSlop() + 10; 370 float offsetY = ViewConfiguration.get(mTestableContext).getScaledTouchSlop() + 10; 371 372 // Simulate ACTION_DOWN event (gesture starts). 373 MotionEvent actionDown = new MotionEventBuilder(MotionEvent.ACTION_DOWN, SOURCE_MOUSE) 374 .pointer(new PointerBuilder(0, MotionEvent.TOOL_TYPE_FINGER) 375 .x(40f) 376 .y(40f) 377 ) 378 .classification(MotionEvent.CLASSIFICATION_TWO_FINGER_SWIPE) 379 .build(); 380 mTouchpadDebugView.dispatchTouchEvent(actionDown); 381 382 // Simulate ACTION_MOVE event (dragging with two fingers, processed as one pointer). 383 MotionEvent actionMove = new MotionEventBuilder(MotionEvent.ACTION_MOVE, SOURCE_MOUSE) 384 .pointer(new PointerBuilder(0, MotionEvent.TOOL_TYPE_FINGER) 385 .x(40f + offsetX) 386 .y(40f + offsetY) 387 ) 388 .classification(MotionEvent.CLASSIFICATION_TWO_FINGER_SWIPE) 389 .build(); 390 mTouchpadDebugView.dispatchTouchEvent(actionMove); 391 392 // Simulate ACTION_UP event (gesture ends). 393 MotionEvent actionUp = new MotionEventBuilder(MotionEvent.ACTION_UP, SOURCE_MOUSE) 394 .pointer(new PointerBuilder(0, MotionEvent.TOOL_TYPE_FINGER) 395 .x(40f + offsetX) 396 .y(40f + offsetY) 397 ) 398 .classification(MotionEvent.CLASSIFICATION_TWO_FINGER_SWIPE) 399 .build(); 400 mTouchpadDebugView.dispatchTouchEvent(actionUp); 401 402 // Verify that no updateViewLayout is called (as expected for a two-finger drag gesture). 403 verify(mWindowManager, times(0)).updateViewLayout(any(), any()); 404 } 405 406 @Test testPinchDrag()407 public void testPinchDrag() { 408 float offsetY = ViewConfiguration.get(mTestableContext).getScaledTouchSlop() + 10; 409 410 MotionEvent actionDown = new MotionEventBuilder(MotionEvent.ACTION_DOWN, SOURCE_MOUSE) 411 .pointer(new PointerBuilder(0, MotionEvent.TOOL_TYPE_FINGER) 412 .x(40f) 413 .y(40f) 414 ) 415 .classification(MotionEvent.CLASSIFICATION_PINCH) 416 .build(); 417 mTouchpadDebugView.dispatchTouchEvent(actionDown); 418 419 MotionEvent pointerDown = new MotionEventBuilder(MotionEvent.ACTION_POINTER_DOWN, 420 SOURCE_MOUSE) 421 .pointer(new PointerBuilder(0, MotionEvent.TOOL_TYPE_FINGER) 422 .x(40f) 423 .y(40f) 424 ) 425 .pointer(new PointerBuilder(1, MotionEvent.TOOL_TYPE_FINGER) 426 .x(40f) 427 .y(45f) 428 ) 429 .classification(MotionEvent.CLASSIFICATION_PINCH) 430 .build(); 431 mTouchpadDebugView.dispatchTouchEvent(pointerDown); 432 433 // Simulate ACTION_MOVE event (both fingers moving apart). 434 MotionEvent actionMove = new MotionEventBuilder(MotionEvent.ACTION_MOVE, SOURCE_MOUSE) 435 .pointer(new PointerBuilder(0, MotionEvent.TOOL_TYPE_FINGER) 436 .x(40f) 437 .y(40f - offsetY) 438 ) 439 .rawXCursorPosition(mWindowLayoutParams.x + 10f) 440 .rawYCursorPosition(mWindowLayoutParams.y + 10f) 441 .pointer(new PointerBuilder(1, MotionEvent.TOOL_TYPE_FINGER) 442 .x(40f) 443 .y(45f + offsetY) 444 ) 445 .classification(MotionEvent.CLASSIFICATION_PINCH) 446 .build(); 447 mTouchpadDebugView.dispatchTouchEvent(actionMove); 448 449 MotionEvent pointerUp = new MotionEventBuilder(MotionEvent.ACTION_POINTER_UP, SOURCE_MOUSE) 450 .pointer(new PointerBuilder(0, MotionEvent.TOOL_TYPE_FINGER) 451 .x(40f) 452 .y(40f - offsetY) 453 ) 454 .pointer(new PointerBuilder(1, MotionEvent.TOOL_TYPE_FINGER) 455 .x(40f) 456 .y(45f + offsetY) 457 ) 458 .classification(MotionEvent.CLASSIFICATION_PINCH) 459 .build(); 460 mTouchpadDebugView.dispatchTouchEvent(pointerUp); 461 462 MotionEvent actionUp = new MotionEventBuilder(MotionEvent.ACTION_UP, SOURCE_MOUSE) 463 .pointer(new PointerBuilder(0, MotionEvent.TOOL_TYPE_FINGER) 464 .x(40f) 465 .y(40f - offsetY) 466 ) 467 .classification(MotionEvent.CLASSIFICATION_PINCH) 468 .build(); 469 mTouchpadDebugView.dispatchTouchEvent(actionUp); 470 471 // Verify that no updateViewLayout is called (as expected for a two-finger drag gesture). 472 verify(mWindowManager, times(0)).updateViewLayout(any(), any()); 473 } 474 } 475