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