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