| /* |
| * 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()); |
| } |
| } |
| } |