1 /*
2  * Copyright 2015 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 android.hardware.input.cts.tests;
18 
19 import static org.junit.Assert.assertEquals;
20 import static org.junit.Assert.assertNotEquals;
21 import static org.junit.Assert.assertTrue;
22 import static org.junit.Assert.fail;
23 
24 import android.app.Instrumentation;
25 import android.hardware.input.cts.InputCallback;
26 import android.hardware.input.cts.InputCtsActivity;
27 import android.util.Log;
28 import android.view.InputDevice;
29 import android.view.InputEvent;
30 import android.view.KeyEvent;
31 import android.view.MotionEvent;
32 import android.view.View;
33 
34 import androidx.annotation.NonNull;
35 import androidx.test.platform.app.InstrumentationRegistry;
36 import androidx.test.rule.ActivityTestRule;
37 
38 import com.android.compatibility.common.util.PollingCheck;
39 import com.android.cts.input.InputJsonParser;
40 
41 import org.junit.After;
42 import org.junit.Before;
43 import org.junit.Rule;
44 
45 import java.util.ArrayList;
46 import java.util.List;
47 import java.util.concurrent.BlockingQueue;
48 import java.util.concurrent.CountDownLatch;
49 import java.util.concurrent.LinkedBlockingQueue;
50 import java.util.concurrent.TimeUnit;
51 
52 public abstract class InputTestCase {
53     private static final String TAG = "InputTestCase";
54     private static final float TOLERANCE = 0.005f;
55 
56     private final BlockingQueue<InputEvent> mEvents;
57 
58     private InputListener mInputListener;
59     private View mDecorView;
60 
61     protected Instrumentation mInstrumentation;
62     protected InputJsonParser mParser;
63     // Stores the name of the currently running test
64     protected String mCurrentTestCase;
65     private int mRegisterResourceId; // raw resource that contains json for registering a hid device
66     protected int mVid;
67     protected int mPid;
68 
69     // State used for motion events
70     private int mLastButtonState;
71 
InputTestCase(int registerResourceId)72     InputTestCase(int registerResourceId) {
73         mEvents = new LinkedBlockingQueue<>();
74         mInputListener = new InputListener();
75         mRegisterResourceId = registerResourceId;
76     }
77 
78     @Rule
79     public ActivityTestRule<InputCtsActivity> mActivityRule =
80         new ActivityTestRule<>(InputCtsActivity.class);
81 
82     @Before
setUp()83     public void setUp() throws Exception {
84         mInstrumentation = InstrumentationRegistry.getInstrumentation();
85         mActivityRule.getActivity().clearUnhandleKeyCode();
86         mDecorView = mActivityRule.getActivity().getWindow().getDecorView();
87         mParser = new InputJsonParser(mInstrumentation.getTargetContext());
88         mVid = mParser.readVendorId(mRegisterResourceId);
89         mPid = mParser.readProductId(mRegisterResourceId);
90         int deviceId = mParser.readDeviceId(mRegisterResourceId);
91         String registerCommand = mParser.readRegisterCommand(mRegisterResourceId);
92         setUpDevice(deviceId, mParser.readVendorId(mRegisterResourceId),
93                 mParser.readProductId(mRegisterResourceId),
94                 mParser.readSources(mRegisterResourceId), registerCommand);
95         mEvents.clear();
96     }
97 
98     @After
tearDown()99     public void tearDown() throws Exception {
100         tearDownDevice();
101     }
102 
103     // To be implemented by device specific test case.
setUpDevice(int id, int vendorId, int productId, int sources, String registerCommand)104     protected abstract void setUpDevice(int id, int vendorId, int productId, int sources,
105             String registerCommand);
106 
tearDownDevice()107     protected abstract void tearDownDevice();
108 
testInputDeviceEvents(int resourceId)109     protected abstract void testInputDeviceEvents(int resourceId);
110 
111     /**
112      * Asserts that the application received a {@link android.view.KeyEvent} with the given
113      * metadata.
114      *
115      * If other KeyEvents are received by the application prior to the expected KeyEvent, or no
116      * KeyEvents are received within a reasonable amount of time, then this will throw an
117      * {@link AssertionError}.
118      *
119      * Only action, source, keyCode and metaState are being compared.
120      */
assertReceivedKeyEvent(@onNull KeyEvent expectedKeyEvent)121     private void assertReceivedKeyEvent(@NonNull KeyEvent expectedKeyEvent) {
122         KeyEvent receivedKeyEvent = waitForKey();
123         if (receivedKeyEvent == null) {
124             failWithMessage("Did not receive " + expectedKeyEvent);
125         }
126         assertEquals(mCurrentTestCase + " (action)",
127                 expectedKeyEvent.getAction(), receivedKeyEvent.getAction());
128         assertSource(mCurrentTestCase, expectedKeyEvent, receivedKeyEvent);
129         assertEquals(mCurrentTestCase + " (keycode)",
130                 expectedKeyEvent.getKeyCode(), receivedKeyEvent.getKeyCode());
131         assertMetaState(mCurrentTestCase, expectedKeyEvent.getMetaState(),
132                 receivedKeyEvent.getMetaState());
133     }
134 
assertReceivedMotionEvent(@onNull MotionEvent expectedEvent)135     private void assertReceivedMotionEvent(@NonNull MotionEvent expectedEvent) {
136         MotionEvent event = waitForMotion();
137         /*
138          If the test fails here, one thing to try is to forcefully add a delay after the device
139          added callback has been received, but before any hid data has been written to the device.
140          We already wait for all of the proper callbacks here and in other places of the stack, but
141          it appears that the device sometimes is still not ready to receive hid data. If any data
142          gets written to the device in that state, it will disappear,
143          and no events will be generated.
144           */
145 
146         if (event == null) {
147             failWithMessage("Did not receive " + expectedEvent);
148         }
149         if (event.getHistorySize() > 0) {
150             failWithMessage("expected each MotionEvent to only have a single entry");
151         }
152         assertEquals(mCurrentTestCase + " (action)",
153                 expectedEvent.getAction(), event.getAction());
154         assertSource(mCurrentTestCase, expectedEvent, event);
155         assertEquals(mCurrentTestCase + " (button state)",
156                 expectedEvent.getButtonState(), event.getButtonState());
157         if (event.getActionMasked() == MotionEvent.ACTION_BUTTON_PRESS
158                 || event.getActionMasked() == MotionEvent.ACTION_BUTTON_RELEASE) {
159             // Only checking getActionButton() for ACTION_BUTTON_PRESS or ACTION_BUTTON_RELEASE
160             // because for actions other than ACTION_BUTTON_PRESS and ACTION_BUTTON_RELEASE the
161             // returned value of getActionButton() is undefined.
162             assertEquals(mCurrentTestCase + " (action button)",
163                     mLastButtonState ^ event.getButtonState(), event.getActionButton());
164             mLastButtonState = event.getButtonState();
165         }
166         assertAxis(mCurrentTestCase, expectedEvent, event);
167     }
168 
169     /**
170      * Asserts motion event axis values. Separate this into a different method to allow individual
171      * test case to specify it.
172      *
173      * @param expectedSource expected source flag specified in JSON files.
174      * @param actualSource actual source flag received in the test app.
175      */
assertAxis(String testCase, MotionEvent expectedEvent, MotionEvent actualEvent)176     void assertAxis(String testCase, MotionEvent expectedEvent, MotionEvent actualEvent) {
177         for (int i = 0; i < actualEvent.getPointerCount(); i++) {
178             for (int axis = MotionEvent.AXIS_X; axis <= MotionEvent.AXIS_GENERIC_16; axis++) {
179                 assertEquals(testCase + " pointer " + i
180                         + " (" + MotionEvent.axisToString(axis) + ")",
181                         expectedEvent.getAxisValue(axis, i), actualEvent.getAxisValue(axis, i),
182                         TOLERANCE);
183             }
184         }
185     }
186 
187     /**
188      * Asserts source flags. Separate this into a different method to allow individual test case to
189      * specify it.
190      * The input source check verifies if actual source is equal or a subset of the expected source.
191      * With Linux kernel 4.18 or later the input hid driver could register multiple evdev devices
192      * when the HID descriptor has HID usages for different applications. Android frameworks will
193      * create multiple KeyboardInputMappers for each of the evdev device, and each
194      * KeyboardInputMapper will generate key events with source of the evdev device it belongs to.
195      * As long as the source of these key events is a subset of expected source, we consider it as
196      * a valid source.
197      *
198      * @param expected expected event with source flag specified in JSON files.
199      * @param actual actual event with source flag received in the test app.
200      */
assertSource(String testCase, InputEvent expected, InputEvent actual)201     private void assertSource(String testCase, InputEvent expected, InputEvent actual) {
202         assertNotEquals(testCase + " (source)", InputDevice.SOURCE_CLASS_NONE, actual.getSource());
203         assertTrue(testCase + " (source)", expected.isFromSource(actual.getSource()));
204     }
205 
206     /**
207      * Asserts meta states. Separate this into a different method to allow individual test case to
208      * specify it.
209      *
210      * @param expectedMetaState expected meta state specified in JSON files.
211      * @param actualMetaState actual meta state received in the test app.
212      */
assertMetaState(String testCase, int expectedMetaState, int actualMetaState)213     void assertMetaState(String testCase, int expectedMetaState, int actualMetaState) {
214         assertEquals(testCase + " (meta state)", expectedMetaState, actualMetaState);
215     }
216 
217     /**
218      * Assert that no more events have been received by the application.
219      *
220      * If any more events have been received by the application, this will cause failure.
221      */
assertNoMoreEvents()222     private void assertNoMoreEvents() {
223         mInstrumentation.waitForIdleSync();
224         InputEvent event = mEvents.poll();
225         if (event == null) {
226             return;
227         }
228         failWithMessage("extraneous events generated: " + event);
229     }
230 
verifyEvents(List<InputEvent> events)231     protected void verifyEvents(List<InputEvent> events) {
232         mActivityRule.getActivity().setInputCallback(mInputListener);
233         // Make sure we received the expected input events
234         if (events.size() == 0) {
235             // If no event is expected we need to wait for event until timeout and fail on
236             // any unexpected event received caused by the HID report injection.
237             InputEvent event = waitForEvent();
238             if (event != null) {
239                 fail(mCurrentTestCase + " : Received unexpected event " + event);
240             }
241             return;
242         }
243         for (int i = 0; i < events.size(); i++) {
244             final InputEvent event = events.get(i);
245             try {
246                 if (event instanceof MotionEvent) {
247                     assertReceivedMotionEvent((MotionEvent) event);
248                     continue;
249                 }
250                 if (event instanceof KeyEvent) {
251                     assertReceivedKeyEvent((KeyEvent) event);
252                     continue;
253                 }
254             } catch (AssertionError error) {
255                 throw new AssertionError("Assertion on entry " + i + " failed.", error);
256             }
257             fail("Entry " + i + " is neither a KeyEvent nor a MotionEvent: " + event);
258         }
259         assertNoMoreEvents();
260     }
261 
testInputEvents(int resourceId)262     protected void testInputEvents(int resourceId) {
263         testInputDeviceEvents(resourceId);
264         assertNoMoreEvents();
265     }
266 
waitForEvent()267     private InputEvent waitForEvent() {
268         try {
269             return mEvents.poll(1, TimeUnit.SECONDS);
270         } catch (InterruptedException e) {
271             failWithMessage("unexpectedly interrupted while waiting for InputEvent");
272             return null;
273         }
274     }
275 
276     // Ignore Motion event received during the 5 seconds timeout period. Return on the first Key
277     // event received.
waitForKey()278     private KeyEvent waitForKey() {
279         for (int i = 0; i < 5; i++) {
280             InputEvent event = waitForEvent();
281             if (event instanceof KeyEvent) {
282                 return (KeyEvent) event;
283             }
284         }
285         return null;
286     }
287 
288     // Ignore Key event received during the 5 seconds timeout period. Return on the first Motion
289     // event received.
waitForMotion()290     private MotionEvent waitForMotion() {
291         for (int i = 0; i < 5; i++) {
292             InputEvent event = waitForEvent();
293             if (event instanceof MotionEvent) {
294                 return (MotionEvent) event;
295             }
296         }
297         return null;
298     }
299 
300     /**
301      * Since MotionEvents are batched together based on overall system timings (i.e. vsync), we
302      * can't rely on them always showing up batched in the same way. In order to make sure our
303      * test results are consistent, we instead split up the batches so they end up in a
304      * consistent and reproducible stream.
305      *
306      * Note, however, that this ignores the problem of resampling, as we still don't know how to
307      * distinguish resampled events from real events. Only the latter will be consistent and
308      * reproducible.
309      *
310      * @param event The (potentially) batched MotionEvent
311      * @return List of MotionEvents, with each event guaranteed to have zero history size, and
312      * should otherwise be equivalent to the original batch MotionEvent.
313      */
splitBatchedMotionEvent(MotionEvent event)314     private static List<MotionEvent> splitBatchedMotionEvent(MotionEvent event) {
315         List<MotionEvent> events = new ArrayList<>();
316         final int historySize = event.getHistorySize();
317         final int pointerCount = event.getPointerCount();
318         MotionEvent.PointerProperties[] properties =
319                 new MotionEvent.PointerProperties[pointerCount];
320         MotionEvent.PointerCoords[] currentCoords = new MotionEvent.PointerCoords[pointerCount];
321         for (int p = 0; p < pointerCount; p++) {
322             properties[p] = new MotionEvent.PointerProperties();
323             event.getPointerProperties(p, properties[p]);
324             currentCoords[p] = new MotionEvent.PointerCoords();
325             event.getPointerCoords(p, currentCoords[p]);
326         }
327         for (int h = 0; h < historySize; h++) {
328             long eventTime = event.getHistoricalEventTime(h);
329             MotionEvent.PointerCoords[] coords = new MotionEvent.PointerCoords[pointerCount];
330 
331             for (int p = 0; p < pointerCount; p++) {
332                 coords[p] = new MotionEvent.PointerCoords();
333                 event.getHistoricalPointerCoords(p, h, coords[p]);
334             }
335             MotionEvent singleEvent =
336                     MotionEvent.obtain(event.getDownTime(), eventTime, event.getAction(),
337                             pointerCount, properties, coords,
338                             event.getMetaState(), event.getButtonState(),
339                             event.getXPrecision(), event.getYPrecision(),
340                             event.getDeviceId(), event.getEdgeFlags(),
341                             event.getSource(), event.getFlags());
342             singleEvent.setActionButton(event.getActionButton());
343             events.add(singleEvent);
344         }
345 
346         MotionEvent singleEvent =
347                 MotionEvent.obtain(event.getDownTime(), event.getEventTime(), event.getAction(),
348                         pointerCount, properties, currentCoords,
349                         event.getMetaState(), event.getButtonState(),
350                         event.getXPrecision(), event.getYPrecision(),
351                         event.getDeviceId(), event.getEdgeFlags(),
352                         event.getSource(), event.getFlags());
353         singleEvent.setActionButton(event.getActionButton());
354         events.add(singleEvent);
355         return events;
356     }
357 
358     /**
359      * Append the name of the currently executing test case to the fail message.
360      * Dump out the events queue to help debug.
361      */
failWithMessage(String message)362     private void failWithMessage(String message) {
363         if (mEvents.isEmpty()) {
364             Log.i(TAG, "The events queue is empty");
365         } else {
366             Log.e(TAG, "There are additional events received by the test activity:");
367             for (InputEvent event : mEvents) {
368                 Log.i(TAG, event.toString());
369             }
370         }
371         fail(mCurrentTestCase + ": " + message);
372     }
373 
374     private class InputListener implements InputCallback {
375         @Override
onKeyEvent(KeyEvent ev)376         public void onKeyEvent(KeyEvent ev) {
377             try {
378                 mEvents.put(new KeyEvent(ev));
379             } catch (InterruptedException ex) {
380                 failWithMessage("interrupted while adding a KeyEvent to the queue");
381             }
382         }
383 
384         @Override
onMotionEvent(MotionEvent ev)385         public void onMotionEvent(MotionEvent ev) {
386             try {
387                 for (MotionEvent event : splitBatchedMotionEvent(ev)) {
388                     mEvents.put(event);
389                 }
390             } catch (InterruptedException ex) {
391                 failWithMessage("interrupted while adding a MotionEvent to the queue");
392             }
393         }
394     }
395 
requestFocusSync()396     protected void requestFocusSync() {
397         mActivityRule.getActivity().runOnUiThread(() -> {
398             mDecorView.setFocusable(true);
399             mDecorView.setFocusableInTouchMode(true);
400             mDecorView.requestFocus();
401         });
402         PollingCheck.waitFor(mDecorView::hasFocus);
403     }
404 
405     protected class PointerCaptureSession implements AutoCloseable {
PointerCaptureSession()406         protected PointerCaptureSession() {
407             requestFocusSync();
408             ensurePointerCaptureState(true);
409         }
410 
411         @Override
close()412         public void close() {
413             ensurePointerCaptureState(false);
414         }
415 
ensurePointerCaptureState(boolean enable)416         private void ensurePointerCaptureState(boolean enable) {
417             final CountDownLatch latch = new CountDownLatch(1);
418             mActivityRule.getActivity().setPointerCaptureCallback(hasCapture -> {
419                 if (enable == hasCapture) {
420                     latch.countDown();
421                 }
422             });
423             mActivityRule.getActivity().runOnUiThread(enable ? mDecorView::requestPointerCapture
424                     : mDecorView::releasePointerCapture);
425             try {
426                 if (!latch.await(60, TimeUnit.SECONDS)) {
427                     throw new IllegalStateException(
428                             "Did not receive callback after "
429                                     + (enable ? "enabling" : "disabling")
430                                     + " Pointer Capture.");
431                 }
432             } catch (InterruptedException e) {
433                 throw new IllegalStateException(
434                         "Interrupted while waiting for Pointer Capture state.");
435             } finally {
436                 mActivityRule.getActivity().setPointerCaptureCallback(null);
437             }
438             assertEquals("The view's Pointer Capture state did not match.", enable,
439                     mDecorView.hasPointerCapture());
440         }
441     }
442 }
443