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