blob: 209b9122565afb9a5fd74945b82c64f5f2907672 [file] [log] [blame]
/*
* Copyright 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.hardware.input.cts.tests;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import android.app.Instrumentation;
import android.hardware.input.cts.InputCallback;
import android.hardware.input.cts.InputCtsActivity;
import android.util.Log;
import android.view.InputDevice;
import android.view.InputEvent;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.rule.ActivityTestRule;
import com.android.compatibility.common.util.PollingCheck;
import com.android.cts.input.InputJsonParser;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
public abstract class InputTestCase {
private static final String TAG = "InputTestCase";
private static final float TOLERANCE = 0.005f;
private final BlockingQueue<InputEvent> mEvents;
protected final Instrumentation mInstrumentation = InstrumentationRegistry.getInstrumentation();
private InputListener mInputListener;
private View mDecorView;
protected InputJsonParser mParser;
// Stores the name of the currently running test
protected String mCurrentTestCase;
private int mRegisterResourceId; // raw resource that contains json for registering a hid device
protected int mVid;
protected int mPid;
// State used for motion events
private int mLastButtonState;
InputTestCase(int registerResourceId) {
mEvents = new LinkedBlockingQueue<>();
mInputListener = new InputListener();
mRegisterResourceId = registerResourceId;
}
@Rule
public ActivityTestRule<InputCtsActivity> mActivityRule =
new ActivityTestRule<>(InputCtsActivity.class);
@Before
public void setUp() throws Exception {
mActivityRule.getActivity().clearUnhandleKeyCode();
mActivityRule.getActivity().setInputCallback(mInputListener);
mDecorView = mActivityRule.getActivity().getWindow().getDecorView();
mParser = new InputJsonParser(mInstrumentation.getTargetContext());
mVid = mParser.readVendorId(mRegisterResourceId);
mPid = mParser.readProductId(mRegisterResourceId);
int deviceId = mParser.readDeviceId(mRegisterResourceId);
String registerCommand = mParser.readRegisterCommand(mRegisterResourceId);
setUpDevice(deviceId, mParser.readVendorId(mRegisterResourceId),
mParser.readProductId(mRegisterResourceId),
mParser.readSources(mRegisterResourceId), registerCommand);
mEvents.clear();
}
@After
public void tearDown() throws Exception {
tearDownDevice();
}
// To be implemented by device specific test case.
protected abstract void setUpDevice(int id, int vendorId, int productId, int sources,
String registerCommand);
protected abstract void tearDownDevice();
protected abstract void testInputDeviceEvents(int resourceId);
/**
* Asserts that the application received a {@link android.view.KeyEvent} with the given
* metadata.
*
* If other KeyEvents are received by the application prior to the expected KeyEvent, or no
* KeyEvents are received within a reasonable amount of time, then this will throw an
* {@link AssertionError}.
*
* Only action, source, keyCode and metaState are being compared.
*/
private void assertReceivedKeyEvent(@NonNull KeyEvent expectedKeyEvent) {
KeyEvent receivedKeyEvent = waitForKey();
if (receivedKeyEvent == null) {
failWithMessage("Did not receive " + expectedKeyEvent);
}
assertEquals(mCurrentTestCase + " (action)",
expectedKeyEvent.getAction(), receivedKeyEvent.getAction());
assertSource(mCurrentTestCase, expectedKeyEvent, receivedKeyEvent);
assertEquals(mCurrentTestCase + " (keycode)",
expectedKeyEvent.getKeyCode(), receivedKeyEvent.getKeyCode());
assertMetaState(mCurrentTestCase, expectedKeyEvent.getMetaState(),
receivedKeyEvent.getMetaState());
}
private void assertReceivedMotionEvent(@NonNull MotionEvent expectedEvent) {
MotionEvent event = waitForMotion();
/*
If the test fails here, one thing to try is to forcefully add a delay after the device
added callback has been received, but before any hid data has been written to the device.
We already wait for all of the proper callbacks here and in other places of the stack, but
it appears that the device sometimes is still not ready to receive hid data. If any data
gets written to the device in that state, it will disappear,
and no events will be generated.
*/
if (event == null) {
failWithMessage("Did not receive " + expectedEvent);
}
if (event.getHistorySize() > 0) {
failWithMessage("expected each MotionEvent to only have a single entry");
}
assertEquals(mCurrentTestCase + " (action)",
expectedEvent.getAction(), event.getAction());
assertSource(mCurrentTestCase, expectedEvent, event);
assertEquals(mCurrentTestCase + " (button state)",
expectedEvent.getButtonState(), event.getButtonState());
if (event.getActionMasked() == MotionEvent.ACTION_BUTTON_PRESS
|| event.getActionMasked() == MotionEvent.ACTION_BUTTON_RELEASE) {
// Only checking getActionButton() for ACTION_BUTTON_PRESS or ACTION_BUTTON_RELEASE
// because for actions other than ACTION_BUTTON_PRESS and ACTION_BUTTON_RELEASE the
// returned value of getActionButton() is undefined.
assertEquals(mCurrentTestCase + " (action button)",
mLastButtonState ^ event.getButtonState(), event.getActionButton());
mLastButtonState = event.getButtonState();
}
assertAxis(mCurrentTestCase, expectedEvent, event);
}
/**
* Asserts motion event axis values. Separate this into a different method to allow individual
* test case to specify it.
*
* @param expectedSource expected source flag specified in JSON files.
* @param actualSource actual source flag received in the test app.
*/
void assertAxis(String testCase, MotionEvent expectedEvent, MotionEvent actualEvent) {
for (int i = 0; i < actualEvent.getPointerCount(); i++) {
for (int axis = MotionEvent.AXIS_X; axis <= MotionEvent.AXIS_GENERIC_16; axis++) {
assertEquals(testCase + " pointer " + i
+ " (" + MotionEvent.axisToString(axis) + ")",
expectedEvent.getAxisValue(axis, i), actualEvent.getAxisValue(axis, i),
TOLERANCE);
}
}
}
/**
* Asserts source flags. Separate this into a different method to allow individual test case to
* specify it.
* The input source check verifies if actual source is equal or a subset of the expected source.
* With Linux kernel 4.18 or later the input hid driver could register multiple evdev devices
* when the HID descriptor has HID usages for different applications. Android frameworks will
* create multiple KeyboardInputMappers for each of the evdev device, and each
* KeyboardInputMapper will generate key events with source of the evdev device it belongs to.
* As long as the source of these key events is a subset of expected source, we consider it as
* a valid source.
*
* @param expected expected event with source flag specified in JSON files.
* @param actual actual event with source flag received in the test app.
*/
private void assertSource(String testCase, InputEvent expected, InputEvent actual) {
assertNotEquals(testCase + " (source)", InputDevice.SOURCE_CLASS_NONE, actual.getSource());
assertTrue(testCase + " (source)", expected.isFromSource(actual.getSource()));
}
/**
* Asserts meta states. Separate this into a different method to allow individual test case to
* specify it.
*
* @param expectedMetaState expected meta state specified in JSON files.
* @param actualMetaState actual meta state received in the test app.
*/
void assertMetaState(String testCase, int expectedMetaState, int actualMetaState) {
assertEquals(testCase + " (meta state)", expectedMetaState, actualMetaState);
}
/**
* Assert that no more events have been received by the application.
*
* If any more events have been received by the application, this will cause failure.
*/
private void assertNoMoreEvents() {
mInstrumentation.waitForIdleSync();
InputEvent event = mEvents.poll();
if (event == null) {
return;
}
failWithMessage("extraneous events generated: " + event);
}
protected void verifyEvents(List<InputEvent> events) {
// Make sure we received the expected input events
if (events.size() == 0) {
// If no event is expected we need to wait for event until timeout and fail on
// any unexpected event received caused by the HID report injection.
InputEvent event = waitForEvent();
if (event != null) {
fail(mCurrentTestCase + " : Received unexpected event " + event);
}
return;
}
for (int i = 0; i < events.size(); i++) {
final InputEvent event = events.get(i);
try {
if (event instanceof MotionEvent) {
assertReceivedMotionEvent((MotionEvent) event);
continue;
}
if (event instanceof KeyEvent) {
assertReceivedKeyEvent((KeyEvent) event);
continue;
}
} catch (AssertionError error) {
throw new AssertionError("Assertion on entry " + i + " failed.", error);
}
fail("Entry " + i + " is neither a KeyEvent nor a MotionEvent: " + event);
}
assertNoMoreEvents();
}
protected void testInputEvents(int resourceId) {
testInputDeviceEvents(resourceId);
assertNoMoreEvents();
}
private InputEvent waitForEvent() {
try {
return mEvents.poll(1, TimeUnit.SECONDS);
} catch (InterruptedException e) {
failWithMessage("unexpectedly interrupted while waiting for InputEvent");
return null;
}
}
// Ignore Motion event received during the 5 seconds timeout period. Return on the first Key
// event received.
private KeyEvent waitForKey() {
for (int i = 0; i < 5; i++) {
InputEvent event = waitForEvent();
if (event instanceof KeyEvent) {
return (KeyEvent) event;
}
}
return null;
}
// Ignore Key event received during the 5 seconds timeout period. Return on the first Motion
// event received.
private MotionEvent waitForMotion() {
for (int i = 0; i < 5; i++) {
InputEvent event = waitForEvent();
if (event instanceof MotionEvent) {
return (MotionEvent) event;
}
}
return null;
}
/**
* Since MotionEvents are batched together based on overall system timings (i.e. vsync), we
* can't rely on them always showing up batched in the same way. In order to make sure our
* test results are consistent, we instead split up the batches so they end up in a
* consistent and reproducible stream.
*
* Note, however, that this ignores the problem of resampling, as we still don't know how to
* distinguish resampled events from real events. Only the latter will be consistent and
* reproducible.
*
* @param event The (potentially) batched MotionEvent
* @return List of MotionEvents, with each event guaranteed to have zero history size, and
* should otherwise be equivalent to the original batch MotionEvent.
*/
private static List<MotionEvent> splitBatchedMotionEvent(MotionEvent event) {
List<MotionEvent> events = new ArrayList<>();
final int historySize = event.getHistorySize();
final int pointerCount = event.getPointerCount();
MotionEvent.PointerProperties[] properties =
new MotionEvent.PointerProperties[pointerCount];
MotionEvent.PointerCoords[] currentCoords = new MotionEvent.PointerCoords[pointerCount];
for (int p = 0; p < pointerCount; p++) {
properties[p] = new MotionEvent.PointerProperties();
event.getPointerProperties(p, properties[p]);
currentCoords[p] = new MotionEvent.PointerCoords();
event.getPointerCoords(p, currentCoords[p]);
}
for (int h = 0; h < historySize; h++) {
long eventTime = event.getHistoricalEventTime(h);
MotionEvent.PointerCoords[] coords = new MotionEvent.PointerCoords[pointerCount];
for (int p = 0; p < pointerCount; p++) {
coords[p] = new MotionEvent.PointerCoords();
event.getHistoricalPointerCoords(p, h, coords[p]);
}
MotionEvent singleEvent =
MotionEvent.obtain(event.getDownTime(), eventTime, event.getAction(),
pointerCount, properties, coords,
event.getMetaState(), event.getButtonState(),
event.getXPrecision(), event.getYPrecision(),
event.getDeviceId(), event.getEdgeFlags(),
event.getSource(), event.getFlags());
singleEvent.setActionButton(event.getActionButton());
events.add(singleEvent);
}
MotionEvent singleEvent =
MotionEvent.obtain(event.getDownTime(), event.getEventTime(), event.getAction(),
pointerCount, properties, currentCoords,
event.getMetaState(), event.getButtonState(),
event.getXPrecision(), event.getYPrecision(),
event.getDeviceId(), event.getEdgeFlags(),
event.getSource(), event.getFlags());
singleEvent.setActionButton(event.getActionButton());
events.add(singleEvent);
return events;
}
/**
* Append the name of the currently executing test case to the fail message.
* Dump out the events queue to help debug.
*/
private void failWithMessage(String message) {
if (mEvents.isEmpty()) {
Log.i(TAG, "The events queue is empty");
} else {
Log.e(TAG, "There are additional events received by the test activity:");
for (InputEvent event : mEvents) {
Log.i(TAG, event.toString());
}
}
fail(mCurrentTestCase + ": " + message);
}
private class InputListener implements InputCallback {
@Override
public void onKeyEvent(KeyEvent ev) {
try {
mEvents.put(new KeyEvent(ev));
} catch (InterruptedException ex) {
failWithMessage("interrupted while adding a KeyEvent to the queue");
}
}
@Override
public void onMotionEvent(MotionEvent ev) {
try {
for (MotionEvent event : splitBatchedMotionEvent(ev)) {
mEvents.put(event);
}
} catch (InterruptedException ex) {
failWithMessage("interrupted while adding a MotionEvent to the queue");
}
}
}
protected void requestFocusSync() {
mActivityRule.getActivity().runOnUiThread(() -> {
mDecorView.setFocusable(true);
mDecorView.setFocusableInTouchMode(true);
mDecorView.requestFocus();
});
PollingCheck.waitFor(mDecorView::hasFocus);
}
protected class PointerCaptureSession implements AutoCloseable {
protected PointerCaptureSession() {
requestFocusSync();
ensurePointerCaptureState(true);
}
@Override
public void close() {
ensurePointerCaptureState(false);
}
private void ensurePointerCaptureState(boolean enable) {
final CountDownLatch latch = new CountDownLatch(1);
mActivityRule.getActivity().setPointerCaptureCallback(hasCapture -> {
if (enable == hasCapture) {
latch.countDown();
}
});
mActivityRule.getActivity().runOnUiThread(enable ? mDecorView::requestPointerCapture
: mDecorView::releasePointerCapture);
try {
if (!latch.await(60, TimeUnit.SECONDS)) {
throw new IllegalStateException(
"Did not receive callback after "
+ (enable ? "enabling" : "disabling")
+ " Pointer Capture.");
}
} catch (InterruptedException e) {
throw new IllegalStateException(
"Interrupted while waiting for Pointer Capture state.");
} finally {
mActivityRule.getActivity().setPointerCaptureCallback(null);
}
assertEquals("The view's Pointer Capture state did not match.", enable,
mDecorView.hasPointerCapture());
}
}
}