• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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