Expanded testing for joystick actions.

Currently, only a single button on one joystick model
is tested in CTS. Here we expand the testing to include
additional KeyEvents and MotionEvents for Asus Gamepad
as well as the Sony DualShock 4. The eventual goal
is to add testing for all buttons and sticks for all
joysticks for which we have key layouts, thus allowing
us to ensure that a certain joystick model is supported
by all Android devices.

Rename the resource and the test files to follow a "company_model"
convention, so that multiple joystick models by the same company will
be shown next to each other (in alphabetic order).

Also, add an input library for cts tests. It will contain useful code
that may be utilized by different CTS test modules. Currently, it only
contains HidDevice for a simpler interface to use /dev/uhid.
It could also be used for testing shortcuts, for example (if it emulates
a keyboard instead of a joystick).

Since the "KEYCODE_" prefix being optional in ag/4031155 is not in aosp
yet, add another temporary workaround here.

Add another temporary workaround SystemClock.sleep(500). Without this,
the test is currently flaking. Even though we are sure to only write the
events after UHID_OPEN has been received (and after device added
callback from input manager has been received), monitoring "getevent
-lt" during the test execution reveals that no events are actually
emitted from the virtual device in the case where the test fails. This
suggests that UHID_OPEN callback is not sufficient to ensure that
writing reports to /dev/uhid is safe.

Note: this is a cherry-pick of ag/2983529 with an additional
cherry-pick of ag/3800049

Test: atest AsusGamepadTestCase SonyDualshock4TestCase
Bug: 36069459
Bug: 111431828
Bug: 110270125
Change-Id: If0606b61feea9d4c293874d4c851b7611fbe2a9e
Merged-In: If0606b61feea9d4c293874d4c851b7611fbe2a9e
diff --git a/PREUPLOAD.cfg b/PREUPLOAD.cfg
index 5f3e99f..38077f0 100644
--- a/PREUPLOAD.cfg
+++ b/PREUPLOAD.cfg
@@ -2,11 +2,13 @@
 checkstyle_hook = ${REPO_ROOT}/prebuilts/checkstyle/checkstyle.py --sha ${PREUPLOAD_COMMIT}
                   -fw apps/CtsVerifier/src/com/android/cts/verifier/usb/
                       apps/CtsVerifierUSBCompanion/
+                      libs/
                       tests/autofillservice/
                       tests/tests/animation/
+                      tests/tests/graphics/
+                      tests/tests/hardware/
                       tests/tests/print/
                       tests/tests/text/
-                      tests/tests/graphics/
                       tests/tests/transition/
                       tests/tests/uirendering/
                       tests/tests/view/
diff --git a/libs/input/Android.bp b/libs/input/Android.bp
new file mode 100644
index 0000000..82044c5
--- /dev/null
+++ b/libs/input/Android.bp
@@ -0,0 +1,19 @@
+// Copyright (C) 2018 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.
+
+java_library_static {
+    name: "cts-input-lib",
+    sdk_version: "test_current",
+    srcs: ["src/**/*.java"],
+}
\ No newline at end of file
diff --git a/libs/input/src/com/android/input/HidDevice.java b/libs/input/src/com/android/input/HidDevice.java
new file mode 100644
index 0000000..f28edb5
--- /dev/null
+++ b/libs/input/src/com/android/input/HidDevice.java
@@ -0,0 +1,178 @@
+/*
+ * Copyright (C) 2018 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 com.android.cts.input;
+
+import android.app.Instrumentation;
+import android.app.UiAutomation;
+import android.hardware.input.InputManager;
+import android.os.ParcelFileDescriptor;
+import android.os.SystemClock;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Represents a virtual HID device registered through /dev/uhid.
+ */
+public final class HidDevice implements InputManager.InputDeviceListener {
+    private static final String TAG = "HidDevice";
+    // hid executable expects "-" argument to read from stdin instead of a file
+    private static final String HID_COMMAND = "hid -";
+
+    private final int mId; // // initialized from the json file
+
+    private OutputStream mOutputStream;
+    private Instrumentation mInstrumentation;
+
+    private volatile CountDownLatch mDeviceAddedSignal; // to wait for onInputDeviceAdded signal
+
+    public HidDevice(Instrumentation instrumentation, int deviceId, String registerCommand) {
+        mInstrumentation = instrumentation;
+        setupPipes();
+
+        mInstrumentation.runOnMainSync(new Runnable(){
+            @Override
+            public void run() {
+                InputManager inputManager =
+                        mInstrumentation.getContext().getSystemService(InputManager.class);
+                inputManager.registerInputDeviceListener(HidDevice.this, null);
+            }
+        });
+
+        mId = deviceId;
+        registerInputDevice(registerCommand);
+    }
+
+    /**
+     * Register an input device. May cause a failure if the device added notification
+     * is not received within the timeout period
+     *
+     * @param registerCommand The full json command that specifies how to register this device
+     */
+    private void registerInputDevice(String registerCommand) {
+        mDeviceAddedSignal = new CountDownLatch(1);
+        writeHidCommands(registerCommand.getBytes());
+        try {
+            // Found that in kernel 3.10, the device registration takes a very long time
+            // The wait can be decreased to 2 seconds after kernel 3.10 is no longer supported
+            mDeviceAddedSignal.await(20L, TimeUnit.SECONDS);
+            if (mDeviceAddedSignal.getCount() != 0) {
+                throw new RuntimeException("Did not receive device added notification in time");
+            }
+        } catch (InterruptedException ex) {
+            throw new RuntimeException(
+                    "Unexpectedly interrupted while waiting for device added notification.");
+        }
+        // Even though the device has been added, it still may not be ready to process the events
+        // right away. This seems to be a kernel bug.
+        // Add a small delay here to ensure device is "ready".
+        SystemClock.sleep(500);
+    }
+
+    /**
+     * Add a delay between processing events.
+     *
+     * @param milliSeconds The delay in milliseconds.
+     */
+    public void delay(int milliSeconds) {
+        JSONObject json = new JSONObject();
+        try {
+            json.put("command", "delay");
+            json.put("id", mId);
+            json.put("duration", milliSeconds);
+        } catch (JSONException e) {
+            throw new RuntimeException(
+                    "Could not create JSON object to delay " + milliSeconds + " milliseconds");
+        }
+        writeHidCommands(json.toString().getBytes());
+    }
+
+    /**
+     * Send a HID report to the device. The report should follow the report descriptor
+     * that was specified during device registration.
+     * An example report:
+     * String report = "[0x01, 0x00, 0x00, 0x02]";
+     *
+     * @param report The report to send (a JSON-formatted array of hex)
+     */
+    public void sendHidReport(String report) {
+        JSONObject json = new JSONObject();
+        try {
+            json.put("command", "report");
+            json.put("id", mId);
+            json.put("report", new JSONArray(report));
+        } catch (JSONException e) {
+            throw new RuntimeException("Could not process HID report: " + report);
+        }
+        writeHidCommands(json.toString().getBytes());
+    }
+
+    private static void closeQuietly(AutoCloseable closeable) {
+        if (closeable != null) {
+            try {
+                closeable.close();
+            } catch (RuntimeException rethrown) {
+                throw rethrown;
+            } catch (Exception ignored) {
+            }
+        }
+    }
+
+    /**
+     * Close the device, which would cause the associated input device to unregister.
+     */
+    public void close() {
+        closeQuietly(mOutputStream);
+    }
+
+    private void setupPipes() {
+        UiAutomation ui = mInstrumentation.getUiAutomation();
+        ParcelFileDescriptor[] pipes = ui.executeShellCommandRw(HID_COMMAND);
+
+        mOutputStream = new ParcelFileDescriptor.AutoCloseOutputStream(pipes[1]);
+        closeQuietly(pipes[0]); // hid command is write-only
+    }
+
+    private void writeHidCommands(byte[] bytes) {
+        try {
+            mOutputStream.write(bytes);
+            mOutputStream.flush();
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    // InputManager.InputDeviceListener functions
+    @Override
+    public void onInputDeviceAdded(int deviceId) {
+        mDeviceAddedSignal.countDown();
+    }
+
+    @Override
+    public void onInputDeviceChanged(int deviceId) {
+    }
+
+    @Override
+    public void onInputDeviceRemoved(int deviceId) {
+    }
+}
diff --git a/libs/input/src/com/android/input/HidJsonParser.java b/libs/input/src/com/android/input/HidJsonParser.java
new file mode 100644
index 0000000..6cd0528
--- /dev/null
+++ b/libs/input/src/com/android/input/HidJsonParser.java
@@ -0,0 +1,255 @@
+/*
+ * Copyright (C) 2018 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 com.android.cts.input;
+
+import android.content.Context;
+import android.view.InputDevice;
+import android.view.InputEvent;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+
+/**
+ * Parse json resource file that contains the test commands for HidDevice
+ *
+ * For files containing reports and input events, each entry should be in the following format:
+ * <code>
+ * {"name": "test case name",
+ *  "reports": reports,
+ *  "events": input_events
+ * }
+ * </code>
+ *
+ * {@code reports} - an array of strings that contain hex arrays.
+ * {@code input_events} - an array of dicts in the following format:
+ * <code>
+ * {"action": "down|move|up", "axes": {"axis_x": x, "axis_y": y}, "keycode": "button_a"}
+ * </code>
+ * {@code "axes"} should only be defined for motion events, and {@code "keycode"} for key events.
+ * Timestamps will not be checked.
+
+ * Example:
+ * <code>
+ * [{ "name": "press button A",
+ *    "reports": ["report1",
+ *                "report2",
+ *                "report3"
+ *               ],
+ *    "events": [{"action": "down", "axes": {"axis_y": 0.5, "axis_x": 0.1}},
+ *               {"action": "move", "axes": {"axis_y": 0.0, "axis_x": 0.0}}
+ *              ]
+ *  },
+ *  ... more tests like that
+ * ]
+ * </code>
+ */
+public class HidJsonParser {
+    private static final String TAG = "JsonParser";
+
+    private Context mContext;
+
+    public HidJsonParser(Context context) {
+        mContext = context;
+    }
+
+    /**
+     * Convenience function to create JSONArray from resource.
+     * The resource specified should contain JSON array as the top-level structure.
+     *
+     * @param resourceId The resourceId that contains the json data (typically inside R.raw)
+     */
+    private JSONArray getJsonArrayFromResource(int resourceId) {
+        String data = readRawResource(resourceId);
+        try {
+            return new JSONArray(data);
+        } catch (JSONException e) {
+            throw new RuntimeException(
+                    "Could not parse resource " + resourceId + ", received: " + data);
+        }
+    }
+
+    /**
+     * Convenience function to read in an entire file as a String.
+     *
+     * @param id resourceId of the file
+     * @return contents of the raw resource file as a String
+     */
+    private String readRawResource(int id) {
+        InputStream inputStream = mContext.getResources().openRawResource(id);
+        try {
+            return readFully(inputStream);
+        } catch (IOException e) {
+            throw new RuntimeException("Could not read resource id " + id);
+        }
+    }
+
+    /**
+     * Read register command from raw resource.
+     *
+     * @param resourceId the raw resource id that contains the command
+     * @return the command to register device that can be passed to HidDevice constructor
+     */
+    public String readRegisterCommand(int resourceId) {
+        return readRawResource(resourceId);
+    }
+
+    /**
+     * Read entire input stream until no data remains.
+     *
+     * @param inputStream
+     * @return content of the input stream
+     * @throws IOException
+     */
+    private String readFully(InputStream inputStream) throws IOException {
+        OutputStream baos = new ByteArrayOutputStream();
+        byte[] buffer = new byte[1024];
+        int read = inputStream.read(buffer);
+        while (read >= 0) {
+            baos.write(buffer, 0, read);
+            read = inputStream.read(buffer);
+        }
+        return baos.toString();
+    }
+
+    /**
+     * Extract the device id from the raw resource file. This is needed in order to register
+     * a HidDevice.
+     *
+     * @param resourceId resorce file that contains the register command.
+     * @return hid device id
+     */
+    public int readDeviceId(int resourceId) {
+        try {
+            JSONObject json = new JSONObject(readRawResource(resourceId));
+            return json.getInt("id");
+        } catch (JSONException e) {
+            throw new RuntimeException("Could not read device id from resource " + resourceId);
+        }
+    }
+
+    /**
+     * Read json resource, and return a {@code List} of HidTestData, which contains
+     * the name of each test, along with the HID reports and the expected input events.
+     */
+    public List<HidTestData> getTestData(int resourceId) {
+        JSONArray json = getJsonArrayFromResource(resourceId);
+        List<HidTestData> tests = new ArrayList<HidTestData>();
+        for (int testCaseNumber = 0; testCaseNumber < json.length(); testCaseNumber++) {
+            HidTestData testData = new HidTestData();
+
+            try {
+                JSONObject testcaseEntry = json.getJSONObject(testCaseNumber);
+                testData.name = testcaseEntry.getString("name");
+                JSONArray reports = testcaseEntry.getJSONArray("reports");
+
+                for (int i = 0; i < reports.length(); i++) {
+                    String report = reports.getString(i);
+                    testData.reports.add(report);
+                }
+
+                JSONArray events = testcaseEntry.getJSONArray("events");
+                for (int i = 0; i < events.length(); i++) {
+                    JSONObject entry = events.getJSONObject(i);
+
+                    InputEvent event = null;
+                    if (entry.has("keycode")) {
+                        event = parseKeyEvent(entry);
+                    } else if (entry.has("axes")) {
+                        event = parseMotionEvent(entry);
+                    } else {
+                        throw new RuntimeException(
+                                "Input event is not specified correctly. Received: " + entry);
+                    }
+                    testData.events.add(event);
+                }
+                tests.add(testData);
+            } catch (JSONException e) {
+                throw new RuntimeException("Could not process entry " + testCaseNumber);
+            }
+        }
+        return tests;
+    }
+
+    private KeyEvent parseKeyEvent(JSONObject entry) throws JSONException {
+        int action = keyActionFromString(entry.getString("action"));
+        String keyCodeStr = entry.getString("keycode");
+        if (!keyCodeStr.startsWith("KEYCODE_")) {
+            keyCodeStr = "KEYCODE_" + keyCodeStr;
+        }
+        int keyCode = KeyEvent.keyCodeFromString(keyCodeStr);
+        return new KeyEvent(action, keyCode);
+    }
+
+    private MotionEvent parseMotionEvent(JSONObject entry) throws JSONException {
+        MotionEvent.PointerProperties[] properties = new MotionEvent.PointerProperties[1];
+        properties[0] = new MotionEvent.PointerProperties();
+        properties[0].id = 0;
+        properties[0].toolType = MotionEvent.TOOL_TYPE_UNKNOWN;
+
+        MotionEvent.PointerCoords[] coords = new MotionEvent.PointerCoords[1];
+        coords[0] = new MotionEvent.PointerCoords();
+
+        JSONObject axes = entry.getJSONObject("axes");
+        Iterator<String> keys = axes.keys();
+        while (keys.hasNext()) {
+            String axis = keys.next();
+            float value = (float) axes.getDouble(axis);
+            coords[0].setAxisValue(MotionEvent.axisFromString(axis), value);
+        }
+
+        int action = motionActionFromString(entry.getString("action"));
+        // Only care about axes and action here. Times are not checked
+        MotionEvent event = MotionEvent.obtain(/* downTime */ 0, /* eventTime */ 0, action,
+                /* pointercount */ 1, properties, coords, 0, 0, 0f, 0f,
+                0, 0, InputDevice.SOURCE_JOYSTICK, 0);
+        return event;
+    }
+
+    private int keyActionFromString(String action) {
+        switch (action.toUpperCase()) {
+            case "DOWN":
+                return KeyEvent.ACTION_DOWN;
+            case "UP":
+                return KeyEvent.ACTION_UP;
+        }
+        throw new RuntimeException("Unknown action specified: " + action);
+    }
+
+    private int motionActionFromString(String action) {
+        switch (action.toUpperCase()) {
+            case "DOWN":
+                return MotionEvent.ACTION_DOWN;
+            case "MOVE":
+                return MotionEvent.ACTION_MOVE;
+            case "UP":
+                return MotionEvent.ACTION_UP;
+        }
+        throw new RuntimeException("Unknown action specified: " + action);
+    }
+}
diff --git a/libs/input/src/com/android/input/HidTestData.java b/libs/input/src/com/android/input/HidTestData.java
new file mode 100644
index 0000000..e980dc4
--- /dev/null
+++ b/libs/input/src/com/android/input/HidTestData.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2018 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 com.android.cts.input;
+
+import android.view.InputEvent;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Data class that stores HID test data.
+ *
+ * There need not be a 1:1 mapping from reports to events. It is possible that some reports may
+ * generate more than 1 event (maybe 2 buttons were pressed simultaneously, for example).
+ */
+public class HidTestData {
+    // Name of the test
+    public String name;
+
+    // HID reports that are used as input to /dev/uhid
+    public List<String> reports = new ArrayList<String>();
+
+    // InputEvent's that are expected to be produced after sending out the reports.
+    public List<InputEvent> events = new ArrayList<InputEvent>();
+}
diff --git a/tests/tests/hardware/Android.mk b/tests/tests/hardware/Android.mk
index 4e8aaa7..f4d23de 100644
--- a/tests/tests/hardware/Android.mk
+++ b/tests/tests/hardware/Android.mk
@@ -30,7 +30,9 @@
 
 LOCAL_STATIC_JAVA_LIBRARIES := \
     android-support-test \
+    androidx.annotation_annotation \
     compatibility-device-util \
+    cts-input-lib \
     ctstestrunner \
     mockito-target-minus-junit4 \
     platform-test-annotations \
diff --git a/tests/tests/hardware/res/raw/asus_gamepad_keyeventtests.json b/tests/tests/hardware/res/raw/asus_gamepad_keyeventtests.json
new file mode 100644
index 0000000..73d77b5
--- /dev/null
+++ b/tests/tests/hardware/res/raw/asus_gamepad_keyeventtests.json
@@ -0,0 +1,121 @@
+[
+  {
+    "name": "Press BUTTON_A",
+    "reports": [
+      [0x01, 0x01, 0x80, 0x80, 0x80, 0x80, 0x80, 0x00, 0x00],
+      [0x01, 0x00, 0x80, 0x80, 0x80, 0x80, 0x80, 0x00, 0x00]
+    ],
+    "events": [
+      {"action": "DOWN", "keycode": "BUTTON_A"},
+      {"action": "UP", "keycode": "BUTTON_A"}
+    ]
+  },
+
+  {
+    "name": "Press BUTTON_B",
+    "reports": [
+      [0x01, 0x02, 0x80, 0x80, 0x80, 0x80, 0x80, 0x00, 0x00],
+      [0x01, 0x00, 0x80, 0x80, 0x80, 0x80, 0x80, 0x00, 0x00]
+    ],
+    "events": [
+      {"action": "DOWN", "keycode": "BUTTON_B"},
+      {"action": "UP", "keycode": "BUTTON_B"}
+    ]
+  },
+
+  {
+    "name": "Press BUTTON_X",
+    "reports": [
+      [0x01, 0x04, 0x80, 0x80, 0x80, 0x80, 0x80, 0x00, 0x00],
+      [0x01, 0x00, 0x80, 0x80, 0x80, 0x80, 0x80, 0x00, 0x00]
+    ],
+    "events": [
+      {"action": "DOWN", "keycode": "BUTTON_X"},
+      {"action": "UP", "keycode": "BUTTON_X"}
+    ]
+  },
+
+  {
+    "name": "Press BUTTON_Y",
+    "reports": [
+      [0x01, 0x08, 0x80, 0x80, 0x80, 0x80, 0x80, 0x00, 0x00],
+      [0x01, 0x00, 0x80, 0x80, 0x80, 0x80, 0x80, 0x00, 0x00]
+    ],
+    "events": [
+      {"action": "DOWN", "keycode": "BUTTON_Y"},
+      {"action": "UP", "keycode": "BUTTON_Y"}
+    ]
+  },
+
+  {
+    "name": "Press BUTTON_L1",
+    "reports": [
+      [0x01, 0x10, 0x80, 0x80, 0x80, 0x80, 0x80, 0x00, 0x00],
+      [0x01, 0x00, 0x80, 0x80, 0x80, 0x80, 0x80, 0x00, 0x00]
+    ],
+    "events": [
+      {"action": "DOWN", "keycode": "BUTTON_L1"},
+      {"action": "UP", "keycode": "BUTTON_L1"}
+    ]
+  },
+
+  {
+    "name": "Press BUTTON_R1",
+    "reports": [
+      [0x01, 0x20, 0x80, 0x80, 0x80, 0x80, 0x80, 0x00, 0x00],
+      [0x01, 0x00, 0x80, 0x80, 0x80, 0x80, 0x80, 0x00, 0x00]
+    ],
+    "events": [
+      {"action": "DOWN", "keycode": "BUTTON_R1"},
+      {"action": "UP", "keycode": "BUTTON_R1"}
+    ]
+  },
+
+  {
+    "name": "Press BUTTON_THUMBL",
+    "reports": [
+      [0x01, 0x40, 0x80, 0x80, 0x80, 0x80, 0x80, 0x00, 0x00],
+      [0x01, 0x00, 0x80, 0x80, 0x80, 0x80, 0x80, 0x00, 0x00]
+    ],
+    "events": [
+      {"action": "DOWN", "keycode": "BUTTON_THUMBL"},
+      {"action": "UP", "keycode": "BUTTON_THUMBL"}
+    ]
+  },
+
+  {
+    "name": "Press BUTTON_THUMBR",
+    "reports": [
+      [0x01, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x00, 0x00],
+      [0x01, 0x00, 0x80, 0x80, 0x80, 0x80, 0x80, 0x00, 0x00]
+    ],
+    "events": [
+      {"action": "DOWN", "keycode": "BUTTON_THUMBR"},
+      {"action": "UP", "keycode": "BUTTON_THUMBR"}
+    ]
+  },
+
+  {
+    "name": "Press POWER button (the button in the center)",
+    "reports": [
+      [0x01, 0x00, 0x81, 0x80, 0x80, 0x80, 0x80, 0x00, 0x00],
+      [0x01, 0x00, 0x80, 0x80, 0x80, 0x80, 0x80, 0x00, 0x00]
+    ],
+    "events": [
+      {"action": "DOWN", "keycode": "BUTTON_MODE"},
+      {"action": "UP", "keycode": "BUTTON_MODE"}
+    ]
+  },
+
+  {
+    "name": "Press BACK button (left arrow)",
+    "reports": [
+      [0x01, 0x00, 0x82, 0x80, 0x80, 0x80, 0x80, 0x00, 0x00],
+      [0x01, 0x00, 0x80, 0x80, 0x80, 0x80, 0x80, 0x00, 0x00]
+    ],
+    "events": [
+      {"action": "DOWN", "keycode": "BACK"},
+      {"action": "UP", "keycode": "BACK"}
+    ]
+  }
+]
\ No newline at end of file
diff --git a/tests/tests/hardware/res/raw/asus_gamepad_motioneventtests.json b/tests/tests/hardware/res/raw/asus_gamepad_motioneventtests.json
new file mode 100644
index 0000000..e104040
--- /dev/null
+++ b/tests/tests/hardware/res/raw/asus_gamepad_motioneventtests.json
@@ -0,0 +1,256 @@
+[
+  {
+    "name": "Sanity check - should not produce any events",
+    "reports": [
+      [0x01, 0x00, 0x80, 0x83, 0x80, 0x80, 0x80, 0x00, 0x00]
+    ],
+    "events": []
+  },
+
+  {
+    "name": "Left stick - press down (all axes)",
+    "reports": [
+      [0x01, 0x00, 0x80, 0x87, 0x89, 0x72, 0x80, 0x00, 0x00],
+      [0x01, 0x00, 0x80, 0x93, 0xf7, 0x71, 0x80, 0x00, 0x00],
+      [0x01, 0x00, 0x80, 0xa0, 0xff, 0x71, 0x80, 0x00, 0x00],
+      [0x01, 0x00, 0x80, 0x83, 0x73, 0x71, 0x80, 0x00, 0x00],
+      [0x01, 0x00, 0x80, 0x83, 0x80, 0x72, 0x80, 0x00, 0x00]
+                ],
+    "events": [
+      {"action": "MOVE", "axes": {"AXIS_X": 0.059, "AXIS_Y": 0.0745, "AXIS_Z": -0.106}},
+      {"action": "MOVE", "axes": {"AXIS_X": 0.153, "AXIS_Y": 0.9373, "AXIS_Z": -0.106}},
+      {"action": "MOVE", "axes": {"AXIS_X": 0.255, "AXIS_Y": 1.0000, "AXIS_Z": -0.106}},
+      {"action": "MOVE", "axes": {"AXIS_X": 0.027, "AXIS_Y": -0.098, "AXIS_Z": -0.106}},
+      {"action": "MOVE", "axes": {"AXIS_X": 0.027, "AXIS_Y": 0.0039, "AXIS_Z": -0.106}}
+    ]
+  },
+
+  {
+    "name": "Press left DPAD key",
+    "reports": [
+      [0x01, 0x00, 0x60, 0x80, 0x80, 0x80, 0x80, 0x00, 0x00],
+      [0x01, 0x00, 0x80, 0x80, 0x80, 0x80, 0x80, 0x00, 0x00]
+    ],
+    "events": [
+      {"action": "MOVE", "axes": {"AXIS_HAT_X": -1}},
+      {"action": "MOVE", "axes": {"AXIS_HAT_X": 0}}
+    ]
+  },
+
+  {
+    "name": "Press right DPAD key",
+    "reports": [
+      [0x01, 0x00, 0x20, 0x80, 0x80, 0x80, 0x80, 0x00, 0x00],
+      [0x01, 0x00, 0x80, 0x80, 0x80, 0x80, 0x80, 0x00, 0x00]
+    ],
+    "events": [
+      {"action": "MOVE", "axes": {"AXIS_HAT_X": 1}},
+      {"action": "MOVE", "axes": {"AXIS_HAT_X": 0}}
+    ]
+  },
+
+  {
+    "name": "Press up DPAD key",
+    "reports": [
+      [0x01, 0x00, 0x00, 0x80, 0x80, 0x80, 0x80, 0x00, 0x00],
+      [0x01, 0x00, 0x80, 0x80, 0x80, 0x80, 0x80, 0x00, 0x00]
+    ],
+    "events": [
+      {"action": "MOVE", "axes": {"AXIS_HAT_Y": -1}},
+      {"action": "MOVE", "axes": {"AXIS_HAT_Y": 0}}
+    ]
+  },
+
+  {
+    "name": "Press down DPAD key",
+    "reports": [
+      [0x01, 0x00, 0x40, 0x80, 0x80, 0x80, 0x80, 0x00, 0x00],
+      [0x01, 0x00, 0x80, 0x80, 0x80, 0x80, 0x80, 0x00, 0x00]
+    ],
+    "events": [
+      {"action": "MOVE", "axes": {"AXIS_HAT_Y": 1}},
+      {"action": "MOVE", "axes": {"AXIS_HAT_Y": 0}}
+    ]
+  },
+
+  {
+    "name": "Left stick - press left",
+    "reports": [
+      [0x01, 0x00, 0x80, 0x16, 0x80, 0x80, 0x80, 0x00, 0x00],
+      [0x01, 0x00, 0x80, 0x00, 0x80, 0x80, 0x80, 0x00, 0x00],
+      [0x01, 0x00, 0x80, 0x20, 0x80, 0x80, 0x80, 0x00, 0x00],
+      [0x01, 0x00, 0x80, 0x7a, 0x80, 0x80, 0x80, 0x00, 0x00]
+    ],
+    "events": [
+      {"action": "MOVE", "axes": {"AXIS_X": -0.827}},
+      {"action": "MOVE", "axes": {"AXIS_X": -1.0}},
+      {"action": "MOVE", "axes": {"AXIS_X": -0.749}},
+      {"action": "MOVE", "axes": {"AXIS_X": -0.043}}
+    ]
+  },
+
+  {
+    "name": "Left stick - press right",
+    "reports": [
+      [0x01, 0x00, 0x80, 0xd3, 0x80, 0x80, 0x80, 0x00, 0x00],
+      [0x01, 0x00, 0x80, 0xff, 0x80, 0x80, 0x80, 0x00, 0x00],
+      [0x01, 0x00, 0x80, 0x74, 0x80, 0x80, 0x80, 0x00, 0x00],
+      [0x01, 0x00, 0x80, 0x7f, 0x80, 0x80, 0x80, 0x00, 0x00]
+    ],
+    "events": [
+      {"action": "MOVE", "axes": {"AXIS_X": 0.655}},
+      {"action": "MOVE", "axes": {"AXIS_X": 1.0}},
+      {"action": "MOVE", "axes": {"AXIS_X": -0.090}},
+      {"action": "MOVE", "axes": {"AXIS_X": -0.004}}
+    ]
+  },
+
+  {
+    "name": "Left stick - press up",
+    "reports": [
+      [0x01, 0x00, 0x80, 0x80, 0x7c, 0x80, 0x80, 0x00, 0x00],
+      [0x01, 0x00, 0x80, 0x80, 0x55, 0x80, 0x80, 0x00, 0x00],
+      [0x01, 0x00, 0x80, 0x80, 0x20, 0x80, 0x80, 0x00, 0x00],
+      [0x01, 0x00, 0x80, 0x80, 0x00, 0x80, 0x80, 0x00, 0x00],
+      [0x01, 0x00, 0x80, 0x80, 0x09, 0x80, 0x80, 0x00, 0x00],
+      [0x01, 0x00, 0x80, 0x80, 0x4a, 0x80, 0x80, 0x00, 0x00],
+      [0x01, 0x00, 0x80, 0x80, 0x80, 0x80, 0x80, 0x00, 0x00]
+    ],
+    "events": [
+      {"action": "MOVE", "axes": {"AXIS_Y": -0.031}},
+      {"action": "MOVE", "axes": {"AXIS_Y": -0.333}},
+      {"action": "MOVE", "axes": {"AXIS_Y": -0.749}},
+      {"action": "MOVE", "axes": {"AXIS_Y": -1.0}},
+      {"action": "MOVE", "axes": {"AXIS_Y": -0.929}},
+      {"action": "MOVE", "axes": {"AXIS_Y": -0.420}},
+      {"action": "MOVE", "axes": {"AXIS_Y": 0.004}}
+    ]
+  },
+
+  {
+    "name": "Left stick - press down",
+    "reports": [
+      [0x01, 0x00, 0x80, 0x80, 0x97, 0x80, 0x80, 0x00, 0x00],
+      [0x01, 0x00, 0x80, 0x80, 0xff, 0x80, 0x80, 0x00, 0x00],
+      [0x01, 0x00, 0x80, 0x80, 0xd1, 0x80, 0x80, 0x00, 0x00],
+      [0x01, 0x00, 0x80, 0x80, 0x80, 0x80, 0x80, 0x00, 0x00]
+    ],
+    "events": [
+      {"action": "MOVE", "axes": {"AXIS_Y": 0.184}},
+      {"action": "MOVE", "axes": {"AXIS_Y": 1.0}},
+      {"action": "MOVE", "axes": {"AXIS_Y": 0.639}},
+      {"action": "MOVE", "axes": {"AXIS_Y": 0.004}}
+    ]
+  },
+
+  {
+    "name": "Right stick - press left",
+    "reports": [
+      [0x01, 0x00, 0x80, 0x80, 0x80, 0x66, 0x80, 0x00, 0x00],
+      [0x01, 0x00, 0x80, 0x80, 0x80, 0x13, 0x80, 0x00, 0x00],
+      [0x01, 0x00, 0x80, 0x80, 0x80, 0x00, 0x80, 0x00, 0x00],
+      [0x01, 0x00, 0x80, 0x80, 0x80, 0x21, 0x80, 0x00, 0x00],
+      [0x01, 0x00, 0x80, 0x80, 0x80, 0x80, 0x80, 0x00, 0x00]
+    ],
+    "events": [
+      {"action": "MOVE", "axes": {"AXIS_Z": -0.200}},
+      {"action": "MOVE", "axes": {"AXIS_Z": -0.851}},
+      {"action": "MOVE", "axes": {"AXIS_Z": -1.0}},
+      {"action": "MOVE", "axes": {"AXIS_Z": -0.74}},
+      {"action": "MOVE", "axes": {"AXIS_Z": 0.004}}
+    ]
+  },
+
+  {
+    "name": "Right stick - press right",
+    "reports": [
+      [0x01, 0x00, 0x80, 0x80, 0x80, 0x8e, 0x80, 0x00, 0x00],
+      [0x01, 0x00, 0x80, 0x80, 0x80, 0x9d, 0x80, 0x00, 0x00],
+      [0x01, 0x00, 0x80, 0x80, 0x80, 0xc4, 0x80, 0x00, 0x00],
+      [0x01, 0x00, 0x80, 0x80, 0x80, 0xeb, 0x80, 0x00, 0x00],
+      [0x01, 0x00, 0x80, 0x80, 0x80, 0xff, 0x80, 0x00, 0x00],
+      [0x01, 0x00, 0x80, 0x80, 0x80, 0xcf, 0x80, 0x00, 0x00],
+      [0x01, 0x00, 0x80, 0x80, 0x80, 0x93, 0x80, 0x00, 0x00],
+      [0x01, 0x00, 0x80, 0x80, 0x80, 0x8c, 0x80, 0x00, 0x00]
+    ],
+    "events": [
+      {"action": "MOVE", "axes": {"AXIS_Z": 0.114}},
+      {"action": "MOVE", "axes": {"AXIS_Z": 0.231}},
+      {"action": "MOVE", "axes": {"AXIS_Z": 0.537}},
+      {"action": "MOVE", "axes": {"AXIS_Z": 0.843}},
+      {"action": "MOVE", "axes": {"AXIS_Z": 1.0}},
+      {"action": "MOVE", "axes": {"AXIS_Z": 0.624}},
+      {"action": "MOVE", "axes": {"AXIS_Z": 0.153}},
+      {"action": "MOVE", "axes": {"AXIS_Z": 0.098}}
+    ]
+  },
+
+  {
+    "name": "Right stick - press up",
+    "reports": [
+      [0x01, 0x00, 0x80, 0x80, 0x80, 0x80, 0x61, 0x00, 0x00],
+      [0x01, 0x00, 0x80, 0x80, 0x80, 0x80, 0x00, 0x00, 0x00],
+      [0x01, 0x00, 0x80, 0x80, 0x80, 0x80, 0x55, 0x00, 0x00],
+      [0x01, 0x00, 0x80, 0x80, 0x80, 0x80, 0x80, 0x00, 0x00]
+    ],
+    "events": [
+      {"action": "MOVE", "axes": {"AXIS_RZ": -0.239}},
+      {"action": "MOVE", "axes": {"AXIS_RZ": -1.0}},
+      {"action": "MOVE", "axes": {"AXIS_RZ": -0.333}},
+      {"action": "MOVE", "axes": {"AXIS_RZ": 0.004}}
+    ]
+  },
+
+  {
+    "name": "Right stick - press down",
+    "reports": [
+      [0x01, 0x00, 0x80, 0x80, 0x80, 0x80, 0x83, 0x00, 0x00],
+      [0x01, 0x00, 0x80, 0x80, 0x80, 0x80, 0x90, 0x00, 0x00],
+      [0x01, 0x00, 0x80, 0x80, 0x80, 0x80, 0xff, 0x00, 0x00],
+      [0x01, 0x00, 0x80, 0x80, 0x80, 0x80, 0x54, 0x00, 0x00],
+      [0x01, 0x00, 0x80, 0x80, 0x80, 0x80, 0x82, 0x00, 0x00],
+      [0x01, 0x00, 0x80, 0x80, 0x80, 0x80, 0x80, 0x00, 0x00]
+    ],
+    "events": [
+      {"action": "MOVE", "axes": {"AXIS_RZ": 0.129}},
+      {"action": "MOVE", "axes": {"AXIS_RZ": 1.0}},
+      {"action": "MOVE", "axes": {"AXIS_RZ": -0.341}},
+      {"action": "MOVE", "axes": {"AXIS_RZ": 0.020}},
+      {"action": "MOVE", "axes": {"AXIS_RZ": 0.004}}
+    ]
+  },
+
+  {
+    "name": "Left trigger - quick press",
+    "reports": [
+      [0x01, 0x00, 0x80, 0x80, 0x80, 0x80, 0x80, 0xa6, 0x00],
+      [0x01, 0x00, 0x80, 0x80, 0x80, 0x80, 0x80, 0xff, 0x00],
+      [0x01, 0x00, 0x80, 0x80, 0x80, 0x80, 0x80, 0x90, 0x00],
+      [0x01, 0x00, 0x80, 0x80, 0x80, 0x80, 0x80, 0x00, 0x00]
+    ],
+    "events": [
+      {"action": "MOVE", "axes": {"AXIS_LTRIGGER": 0.651, "AXIS_BRAKE": 0.651}},
+      {"action": "MOVE", "axes": {"AXIS_LTRIGGER": 1.0, "AXIS_BRAKE": 1.0}},
+      {"action": "MOVE", "axes": {"AXIS_LTRIGGER": 0.565, "AXIS_BRAKE": 0.565}},
+      {"action": "MOVE", "axes": {"AXIS_LTRIGGER": 0, "AXIS_BRAKE": 0}}
+    ]
+  },
+
+  {
+    "name": "Right trigger - quick press",
+    "reports": [
+      [0x01, 0x00, 0x80, 0x80, 0x80, 0x80, 0x80, 0x00, 0xaf],
+      [0x01, 0x00, 0x80, 0x80, 0x80, 0x80, 0x80, 0x00, 0xff],
+      [0x01, 0x00, 0x80, 0x80, 0x80, 0x80, 0x80, 0x00, 0xa5],
+      [0x01, 0x00, 0x80, 0x80, 0x80, 0x80, 0x80, 0x00, 0x00]
+    ],
+    "events": [
+      {"action": "MOVE", "axes": {"AXIS_RTRIGGER": 0.686, "AXIS_GAS": 0.686}},
+      {"action": "MOVE", "axes": {"AXIS_RTRIGGER": 1.0, "AXIS_GAS": 1.0}},
+      {"action": "MOVE", "axes": {"AXIS_RTRIGGER": 0.647, "AXIS_GAS": 0.647}},
+      {"action": "MOVE", "axes": {"AXIS_RTRIGGER": 0, "AXIS_GAS": 0}}
+    ]
+  }
+
+
+]
\ No newline at end of file
diff --git a/tests/tests/hardware/res/raw/gamepad_register_device.json b/tests/tests/hardware/res/raw/asus_gamepad_register.json
similarity index 100%
rename from tests/tests/hardware/res/raw/gamepad_register_device.json
rename to tests/tests/hardware/res/raw/asus_gamepad_register.json
diff --git a/tests/tests/hardware/res/raw/gamepad_button_a_down.json b/tests/tests/hardware/res/raw/gamepad_button_a_down.json
deleted file mode 100644
index 21f5186..0000000
--- a/tests/tests/hardware/res/raw/gamepad_button_a_down.json
+++ /dev/null
@@ -1,5 +0,0 @@
-{
-  "id": 1,
-  "command": "report",
-  "report": [0x01, 0x01, 0x80, 0x7f, 0x7f, 0x7f, 0x7f, 0x00, 0x00]
-}
diff --git a/tests/tests/hardware/res/raw/gamepad_button_a_up.json b/tests/tests/hardware/res/raw/gamepad_button_a_up.json
deleted file mode 100644
index ab1eb0e..0000000
--- a/tests/tests/hardware/res/raw/gamepad_button_a_up.json
+++ /dev/null
@@ -1,5 +0,0 @@
-{
-  "id": 1,
-  "command": "report",
-  "report": [0x01, 0x00, 0x80, 0x7f, 0x7f, 0x7f, 0x7f, 0x00, 0x00]
-}
\ No newline at end of file
diff --git a/tests/tests/hardware/res/raw/gamepad_delay.json b/tests/tests/hardware/res/raw/gamepad_delay.json
deleted file mode 100644
index a25c3dd..0000000
--- a/tests/tests/hardware/res/raw/gamepad_delay.json
+++ /dev/null
@@ -1,5 +0,0 @@
-{
-  "id": 1,
-  "command": "delay",
-  "duration": 10
-}
diff --git a/tests/tests/hardware/res/raw/sony_dualshock4_keyeventtests.json b/tests/tests/hardware/res/raw/sony_dualshock4_keyeventtests.json
new file mode 100644
index 0000000..ab69dd3
--- /dev/null
+++ b/tests/tests/hardware/res/raw/sony_dualshock4_keyeventtests.json
@@ -0,0 +1,25 @@
+[
+  {
+    "name": "Press BUTTON_A",
+    "reports": [
+      [0x01, 0x81, 0x7f, 0x7e, 0x80, 0x28, 0x00, 0x00, 0x00, 0x00],
+      [0x01, 0x81, 0x7f, 0x7e, 0x80, 0x08, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "events": [
+      {"action": "DOWN", "keycode": "BUTTON_A"},
+      {"action": "UP", "keycode": "BUTTON_A"}
+    ]
+  },
+
+  {
+    "name": "Press BUTTON_X",
+    "reports": [
+      [0x01, 0x81, 0x7f, 0x7e, 0x80, 0x18, 0x00, 0x00, 0x00, 0x00],
+      [0x01, 0x81, 0x7f, 0x7e, 0x80, 0x08, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "events": [
+      {"action": "DOWN", "keycode": "BUTTON_X"},
+      {"action": "UP", "keycode": "BUTTON_X"}
+    ]
+  }
+]
diff --git a/tests/tests/hardware/res/raw/sony_dualshock4_register.json b/tests/tests/hardware/res/raw/sony_dualshock4_register.json
new file mode 100644
index 0000000..d91ed17
--- /dev/null
+++ b/tests/tests/hardware/res/raw/sony_dualshock4_register.json
@@ -0,0 +1,34 @@
+{
+  "id": 1,
+  "command": "register",
+  "name": "Sony DS4 Joystick (Test)",
+  "vid": 0x054c,
+  "pid": 0x09cc,
+  "descriptor": [0x05, 0x01, 0x09, 0x05, 0xa1, 0x01, 0x85, 0x01, 0x09, 0x30, 0x09, 0x31, 0x09, 0x32,
+    0x09, 0x35, 0x15, 0x00, 0x26, 0xff, 0x00, 0x75, 0x08, 0x95, 0x04, 0x81, 0x02, 0x09, 0x39, 0x15,
+    0x00, 0x25, 0x07, 0x75, 0x04, 0x95, 0x01, 0x81, 0x42, 0x05, 0x09, 0x19, 0x01, 0x29, 0x0e, 0x15,
+    0x00, 0x25, 0x01, 0x75, 0x01, 0x95, 0x0e, 0x81, 0x02, 0x75, 0x06, 0x95, 0x01, 0x81, 0x01, 0x05,
+    0x01, 0x09, 0x33, 0x09, 0x34, 0x15, 0x00, 0x26, 0xff, 0x00, 0x75, 0x08, 0x95, 0x02, 0x81, 0x02,
+    0x06, 0x04, 0xff, 0x85, 0x02, 0x09, 0x24, 0x95, 0x24, 0xb1, 0x02, 0x85, 0xa3, 0x09, 0x25, 0x95,
+    0x30, 0xb1, 0x02, 0x85, 0x05, 0x09, 0x26, 0x95, 0x28, 0xb1, 0x02, 0x85, 0x06, 0x09, 0x27, 0x95,
+    0x34, 0xb1, 0x02, 0x85, 0x07, 0x09, 0x28, 0x95, 0x30, 0xb1, 0x02, 0x85, 0x08, 0x09, 0x29, 0x95,
+    0x2f, 0xb1, 0x02, 0x85, 0x09, 0x09, 0x2a, 0x95, 0x13, 0xb1, 0x02, 0x06, 0x03, 0xff, 0x85, 0x03,
+    0x09, 0x21, 0x95, 0x26, 0xb1, 0x02, 0x85, 0x04, 0x09, 0x22, 0x95, 0x2e, 0xb1, 0x02, 0x85, 0xf0,
+    0x09, 0x47, 0x95, 0x3f, 0xb1, 0x02, 0x85, 0xf1, 0x09, 0x48, 0x95, 0x3f, 0xb1, 0x02, 0x85, 0xf2,
+    0x09, 0x49, 0x95, 0x0f, 0xb1, 0x02, 0x06, 0x00, 0xff, 0x85, 0x11, 0x09, 0x20, 0x15, 0x00, 0x26,
+    0xff, 0x00, 0x75, 0x08, 0x95, 0x4d, 0x81, 0x02, 0x09, 0x21, 0x91, 0x02, 0x85, 0x12, 0x09, 0x22,
+    0x95, 0x8d, 0x81, 0x02, 0x09, 0x23, 0x91, 0x02, 0x85, 0x13, 0x09, 0x24, 0x95, 0xcd, 0x81, 0x02,
+    0x09, 0x25, 0x91, 0x02, 0x85, 0x14, 0x09, 0x26, 0x96, 0x0d, 0x01, 0x81, 0x02, 0x09, 0x27, 0x91,
+    0x02, 0x85, 0x15, 0x09, 0x28, 0x96, 0x4d, 0x01, 0x81, 0x02, 0x09, 0x29, 0x91, 0x02, 0x85, 0x16,
+    0x09, 0x2a, 0x96, 0x8d, 0x01, 0x81, 0x02, 0x09, 0x2b, 0x91, 0x02, 0x85, 0x17, 0x09, 0x2c, 0x96,
+    0xcd, 0x01, 0x81, 0x02, 0x09, 0x2d, 0x91, 0x02, 0x85, 0x18, 0x09, 0x2e, 0x96, 0x0d, 0x02, 0x81,
+    0x02, 0x09, 0x2f, 0x91, 0x02, 0x85, 0x19, 0x09, 0x30, 0x96, 0x22, 0x02, 0x81, 0x02, 0x09, 0x31,
+    0x91, 0x02, 0x06, 0x80, 0xff, 0x85, 0x82, 0x09, 0x22, 0x95, 0x3f, 0xb1, 0x02, 0x85, 0x83, 0x09,
+    0x23, 0xb1, 0x02, 0x85, 0x84, 0x09, 0x24, 0xb1, 0x02, 0x85, 0x90, 0x09, 0x30, 0xb1, 0x02, 0x85,
+    0x91, 0x09, 0x31, 0xb1, 0x02, 0x85, 0x92, 0x09, 0x32, 0xb1, 0x02, 0x85, 0x93, 0x09, 0x33, 0xb1,
+    0x02, 0x85, 0xa0, 0x09, 0x40, 0xb1, 0x02, 0x85, 0xa4, 0x09, 0x44, 0xb1, 0x02, 0x85, 0xa7, 0x09,
+    0x45, 0xb1, 0x02, 0x85, 0xa8, 0x09, 0x45, 0xb1, 0x02, 0x85, 0xa9, 0x09, 0x45, 0xb1, 0x02, 0x85,
+    0xaa, 0x09, 0x45, 0xb1, 0x02, 0x85, 0xab, 0x09, 0x45, 0xb1, 0x02, 0x85, 0xac, 0x09, 0x45, 0xb1,
+    0x02, 0x85, 0xad, 0x09, 0x45, 0xb1, 0x02, 0x85, 0xb1, 0x09, 0x45, 0xb1, 0x02, 0x85, 0xb2, 0x09,
+    0x46, 0xb1, 0x02, 0x85, 0xb3, 0x09, 0x45, 0xb1, 0x02, 0x85, 0xb4, 0x09, 0x46, 0xb1, 0x02, 0xc0]
+}
diff --git a/tests/tests/hardware/src/android/hardware/input/cts/InputCallback.java b/tests/tests/hardware/src/android/hardware/input/cts/InputCallback.java
index b4bda4e..d0842c1 100644
--- a/tests/tests/hardware/src/android/hardware/input/cts/InputCallback.java
+++ b/tests/tests/hardware/src/android/hardware/input/cts/InputCallback.java
@@ -20,9 +20,6 @@
 import android.view.MotionEvent;
 
 public interface InputCallback {
-    public void onKeyEvent(KeyEvent ev);
-    public void onMotionEvent(MotionEvent ev);
-    public void onInputDeviceAdded(int deviceId);
-    public void onInputDeviceRemoved(int deviceId);
-    public void onInputDeviceChanged(int deviceId);
+    void onKeyEvent(KeyEvent ev);
+    void onMotionEvent(MotionEvent ev);
 }
diff --git a/tests/tests/hardware/src/android/hardware/input/cts/InputCtsActivity.java b/tests/tests/hardware/src/android/hardware/input/cts/InputCtsActivity.java
index 72aa056..028b18e 100644
--- a/tests/tests/hardware/src/android/hardware/input/cts/InputCtsActivity.java
+++ b/tests/tests/hardware/src/android/hardware/input/cts/InputCtsActivity.java
@@ -17,26 +17,18 @@
 package android.hardware.input.cts;
 
 import android.app.Activity;
-import android.content.Context;
-import android.hardware.input.InputManager;
-import android.hardware.input.InputManager.InputDeviceListener;
 import android.os.Bundle;
-import android.util.Log;
 import android.view.KeyEvent;
 import android.view.MotionEvent;
 
-public class InputCtsActivity extends Activity implements InputDeviceListener {
+public class InputCtsActivity extends Activity {
     private static final String TAG = "InputCtsActivity";
 
     private InputCallback mInputCallback;
 
-    private InputManager mInputManager;
-
     @Override
     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
-        mInputManager = getApplicationContext().getSystemService(InputManager.class);
-        mInputManager.registerInputDeviceListener(this, null);
     }
 
     @Override
@@ -74,20 +66,4 @@
     public void setInputCallback(InputCallback callback) {
         mInputCallback = callback;
     }
-
-    @Override
-    public void onInputDeviceAdded(int deviceId) {
-        mInputCallback.onInputDeviceAdded(deviceId);
-    }
-
-    @Override
-    public void onInputDeviceRemoved(int deviceId) {
-        mInputCallback.onInputDeviceRemoved(deviceId);
-    }
-
-    @Override
-    public void onInputDeviceChanged(int deviceId) {
-        mInputManager.getInputDevice(deviceId); // if this isn't called, won't get new notifications
-        mInputCallback.onInputDeviceChanged(deviceId);
-    }
 }
diff --git a/tests/tests/hardware/src/android/hardware/input/cts/tests/AsusGamepadTestCase.java b/tests/tests/hardware/src/android/hardware/input/cts/tests/AsusGamepadTestCase.java
new file mode 100644
index 0000000..642ac4f
--- /dev/null
+++ b/tests/tests/hardware/src/android/hardware/input/cts/tests/AsusGamepadTestCase.java
@@ -0,0 +1,42 @@
+/*
+ * 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 android.hardware.cts.R;
+import android.support.test.filters.MediumTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class AsusGamepadTestCase extends InputTestCase {
+    public AsusGamepadTestCase() {
+        super(R.raw.asus_gamepad_register);
+    }
+
+    @Test
+    public void testAllKeys() {
+        testInputEvents(R.raw.asus_gamepad_keyeventtests);
+    }
+
+    @Test
+    public void testAllMotions() {
+        testInputEvents(R.raw.asus_gamepad_motioneventtests);
+    }
+}
diff --git a/tests/tests/hardware/src/android/hardware/input/cts/tests/GamepadTestCase.java b/tests/tests/hardware/src/android/hardware/input/cts/tests/GamepadTestCase.java
deleted file mode 100644
index e44bcb4..0000000
--- a/tests/tests/hardware/src/android/hardware/input/cts/tests/GamepadTestCase.java
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * 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 android.support.test.filters.SmallTest;
-import android.support.test.runner.AndroidJUnit4;
-import android.view.KeyEvent;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import android.hardware.cts.R;
-
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-public class GamepadTestCase extends InputTestCase {
-    private static final String TAG = "GamepadTests";
-
-    @Test
-    public void testButtonA() throws Exception {
-        registerInputDevice(R.raw.gamepad_register_device);
-
-        sendHidCommands(R.raw.gamepad_button_a_down);
-        sendHidCommands(R.raw.gamepad_delay);
-        assertReceivedKeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_BUTTON_A);
-
-        sendHidCommands(R.raw.gamepad_button_a_up);
-        assertReceivedKeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_BUTTON_A);
-
-        assertNoMoreEvents();
-    }
-}
-
diff --git a/tests/tests/hardware/src/android/hardware/input/cts/tests/InputTestCase.java b/tests/tests/hardware/src/android/hardware/input/cts/tests/InputTestCase.java
index 9089529..a4edc8b 100644
--- a/tests/tests/hardware/src/android/hardware/input/cts/tests/InputTestCase.java
+++ b/tests/tests/hardware/src/android/hardware/input/cts/tests/InputTestCase.java
@@ -20,51 +20,48 @@
 import static org.junit.Assert.fail;
 
 import android.app.Instrumentation;
-import android.app.UiAutomation;
 import android.hardware.input.cts.InputCallback;
 import android.hardware.input.cts.InputCtsActivity;
-import android.os.ParcelFileDescriptor;
-import android.os.SystemClock;
 import android.support.test.InstrumentationRegistry;
 import android.support.test.rule.ActivityTestRule;
+import android.view.InputEvent;
 import android.view.KeyEvent;
 import android.view.MotionEvent;
 
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
+import androidx.annotation.NonNull;
 
-import java.util.concurrent.BlockingQueue;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.LinkedBlockingQueue;
-import java.util.concurrent.TimeUnit;
-
-import libcore.io.IoUtils;
+import com.android.cts.input.HidDevice;
+import com.android.cts.input.HidJsonParser;
+import com.android.cts.input.HidTestData;
 
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Rule;
 
-public class InputTestCase {
-    // hid executable expects "-" argument to read from stdin instead of a file
-    private static final String HID_COMMAND = "hid -";
-    private static final String[] KEY_ACTIONS = {"DOWN", "UP", "MULTIPLE"};
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.TimeUnit;
 
-    private OutputStream mOutputStream;
 
-    private final BlockingQueue<KeyEvent> mKeys;
-    private final BlockingQueue<MotionEvent> mMotions;
+public abstract class InputTestCase {
+    private static final float TOLERANCE = 0.005f;
+
+    private final BlockingQueue<InputEvent> mEvents;
+
     private InputListener mInputListener;
-
     private Instrumentation mInstrumentation;
+    private HidDevice mHidDevice;
+    private HidJsonParser mParser;
+    // Stores the name of the currently running test
+    private String mCurrentTestCase;
+    private int mRegisterResourceId; // raw resource that contains json for registering a hid device
 
-    private volatile CountDownLatch mDeviceAddedSignal; // to wait for onInputDeviceAdded signal
-
-    public InputTestCase() {
-        mKeys = new LinkedBlockingQueue<KeyEvent>();
-        mMotions = new LinkedBlockingQueue<MotionEvent>();
+    InputTestCase(int registerResourceId) {
+        mEvents = new LinkedBlockingQueue<>();
         mInputListener = new InputListener();
+        mRegisterResourceId = registerResourceId;
     }
 
     @Rule
@@ -72,55 +69,19 @@
         new ActivityTestRule<>(InputCtsActivity.class);
 
     @Before
-    public void setUp() throws Exception {
-        clearKeys();
-        clearMotions();
+    public void setUp() {
         mInstrumentation = InstrumentationRegistry.getInstrumentation();
         mActivityRule.getActivity().setInputCallback(mInputListener);
-        setupPipes();
+        mParser = new HidJsonParser(mInstrumentation.getTargetContext());
+        int hidDeviceId = mParser.readDeviceId(mRegisterResourceId);
+        String registerCommand = mParser.readRegisterCommand(mRegisterResourceId);
+        mHidDevice = new HidDevice(mInstrumentation, hidDeviceId, registerCommand);
+        mEvents.clear();
     }
 
     @After
-    public void tearDown() throws Exception {
-        IoUtils.closeQuietly(mOutputStream);
-    }
-
-    /**
-     * Register an input device. May cause a failure if the device added notification
-     * is not received within the timeout period
-     *
-     * @param resourceId The resource id from which to send the register command.
-     */
-    public void registerInputDevice(int resourceId) {
-        mDeviceAddedSignal = new CountDownLatch(1);
-        sendHidCommands(resourceId);
-        try {
-            // Found that in kernel 3.10, the device registration takes a very long time
-            // The wait can be decreased to 2 seconds after kernel 3.10 is no longer supported
-            mDeviceAddedSignal.await(20L, TimeUnit.SECONDS);
-            if (mDeviceAddedSignal.getCount() != 0) {
-                fail("Device added notification was not received in time.");
-            }
-        } catch (InterruptedException ex) {
-            fail("Unexpectedly interrupted while waiting for device added notification.");
-        }
-        SystemClock.sleep(100);
-    }
-
-    /**
-     * Sends the HID commands designated by the given resource id.
-     * The commands must be in the format expected by the `hid` shell command.
-     *
-     * @param id The resource id from which to load the HID commands. This must be a "raw"
-     * resource.
-     */
-    public void sendHidCommands(int id) {
-        try {
-            mOutputStream.write(getEvents(id).getBytes());
-            mOutputStream.flush();
-        } catch (IOException e) {
-            throw new RuntimeException(e);
-        }
+    public void tearDown() {
+        mHidDevice.close();
     }
 
     /**
@@ -131,114 +92,187 @@
      * KeyEvents are received within a reasonable amount of time, then this will throw an
      * AssertionFailedError.
      *
-     * @param action The action to expect on the next KeyEvent
-     * (e.g. {@link android.view.KeyEvent#ACTION_DOWN}).
-     * @param keyCode The expected key code of the next KeyEvent.
+     * Only action and keyCode are being compared.
      */
-    public void assertReceivedKeyEvent(int action, int keyCode) {
-        KeyEvent k = waitForKey();
-        if (k == null) {
-            fail("Timed out waiting for " + KeyEvent.keyCodeToString(keyCode)
-                    + " with action " + KEY_ACTIONS[action]);
-            return;
+    private void assertReceivedKeyEvent(@NonNull KeyEvent expectedKeyEvent) {
+        KeyEvent receivedKeyEvent = waitForKey();
+        if (receivedKeyEvent == null) {
+            fail(mCurrentTestCase + ": timed out waiting for "
+                    + KeyEvent.keyCodeToString(expectedKeyEvent.getKeyCode())
+                    + " with action " + KeyEvent.actionToString(expectedKeyEvent.getAction()));
         }
-        assertEquals(action, k.getAction());
-        assertEquals(keyCode, k.getKeyCode());
+        assertEquals(mCurrentTestCase, expectedKeyEvent.getAction(), receivedKeyEvent.getAction());
+        assertEquals(mCurrentTestCase,
+                expectedKeyEvent.getKeyCode(), receivedKeyEvent.getKeyCode());
+    }
+
+    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) {
+            fail(mCurrentTestCase + ": timed out waiting for MotionEvent");
+        }
+        if (event.getHistorySize() > 0) {
+            fail(mCurrentTestCase + ": expected each MotionEvent to only have a single entry");
+        }
+        assertEquals(mCurrentTestCase, expectedEvent.getAction(), event.getAction());
+        for (int axis = MotionEvent.AXIS_X; axis <= MotionEvent.AXIS_GENERIC_16; axis++) {
+            assertEquals(mCurrentTestCase + " (" + MotionEvent.axisToString(axis) + ")",
+                    expectedEvent.getAxisValue(axis), event.getAxisValue(axis), TOLERANCE);
+        }
     }
 
     /**
-     * Asserts that no more events have been received by the application.
+     * Assert that no more events have been received by the application.
      *
-     * If any more events have been received by the application, this throws an
-     * AssertionFailedError.
+     * If any more events have been received by the application, this will cause failure.
      */
-    public void assertNoMoreEvents() {
-        KeyEvent key;
-        MotionEvent motion;
-        if ((key = mKeys.poll()) != null) {
-            fail("Extraneous key events generated: " + key);
+    private void assertNoMoreEvents() {
+        mInstrumentation.waitForIdleSync();
+        InputEvent event = mEvents.poll();
+        if (event == null) {
+            return;
         }
-        if ((motion = mMotions.poll()) != null) {
-            fail("Extraneous motion events generated: " + motion);
-        }
+        fail(mCurrentTestCase + ": extraneous events generated: " + event);
     }
 
-    private KeyEvent waitForKey() {
+    protected void testInputEvents(int resourceId) {
+        List<HidTestData> tests = mParser.getTestData(resourceId);
+
+        for (HidTestData testData: tests) {
+            mCurrentTestCase = testData.name;
+
+            // Send all of the HID reports
+            for (int i = 0; i < testData.reports.size(); i++) {
+                final String report = testData.reports.get(i);
+                mHidDevice.sendHidReport(report);
+            }
+
+            // Make sure we received the expected input events
+            for (int i = 0; i < testData.events.size(); i++) {
+                final InputEvent event = testData.events.get(i);
+                if (event instanceof MotionEvent) {
+                    assertReceivedMotionEvent((MotionEvent) event);
+                } else if (event instanceof KeyEvent) {
+                    assertReceivedKeyEvent((KeyEvent) event);
+                } else {
+                    fail("Entry " + i + " is neither a KeyEvent nor a MotionEvent: " + event);
+                }
+            }
+        }
+        assertNoMoreEvents();
+    }
+
+    private InputEvent waitForEvent() {
         try {
-            return mKeys.poll(1, TimeUnit.SECONDS);
+            return mEvents.poll(5, TimeUnit.SECONDS);
         } catch (InterruptedException e) {
+            fail(mCurrentTestCase + ": unexpectedly interrupted while waiting for InputEvent");
             return null;
         }
     }
 
-    private void clearKeys() {
-        mKeys.clear();
-    }
-
-    private void clearMotions() {
-        mMotions.clear();
-    }
-
-    private void setupPipes() throws IOException {
-        UiAutomation ui = mInstrumentation.getUiAutomation();
-        ParcelFileDescriptor[] pipes = ui.executeShellCommandRw(HID_COMMAND);
-
-        mOutputStream = new ParcelFileDescriptor.AutoCloseOutputStream(pipes[1]);
-        IoUtils.closeQuietly(pipes[0]); // hid command is write-only
-    }
-
-    private String getEvents(int id) throws IOException {
-        InputStream is =
-            mInstrumentation.getTargetContext().getResources().openRawResource(id);
-        return readFully(is);
-    }
-
-    private static String readFully(InputStream is) throws IOException {
-        ByteArrayOutputStream baos = new ByteArrayOutputStream();
-        int read = 0;
-        byte[] buffer = new byte[1024];
-        while ((read = is.read(buffer)) >= 0) {
-            baos.write(buffer, 0, read);
+    private KeyEvent waitForKey() {
+        InputEvent event = waitForEvent();
+        if (event instanceof KeyEvent) {
+            return (KeyEvent) event;
         }
-        return baos.toString();
+        fail("Expected a KeyEvent, but received: " + event);
+        return null;
+    }
+
+    private MotionEvent waitForMotion() {
+        InputEvent event = waitForEvent();
+        if (event instanceof MotionEvent) {
+            return (MotionEvent) event;
+        }
+        fail("Expected a MotionEvent, but received: " + 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());
+            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());
+        events.add(singleEvent);
+        return events;
     }
 
     private class InputListener implements InputCallback {
         @Override
         public void onKeyEvent(KeyEvent ev) {
-            boolean done = false;
-            do {
-                try {
-                    mKeys.put(new KeyEvent(ev));
-                    done = true;
-                } catch (InterruptedException ignore) { }
-            } while (!done);
+            try {
+                mEvents.put(new KeyEvent(ev));
+            } catch (InterruptedException ex) {
+                fail(mCurrentTestCase + ": interrupted while adding a KeyEvent to the queue");
+            }
         }
 
         @Override
         public void onMotionEvent(MotionEvent ev) {
-            boolean done = false;
-            do {
-                try {
-                    mMotions.put(MotionEvent.obtain(ev));
-                    done = true;
-                } catch (InterruptedException ignore) { }
-            } while (!done);
-        }
-
-        @Override
-        public void onInputDeviceAdded(int deviceId) {
-            mDeviceAddedSignal.countDown();
-        }
-
-        @Override
-        public void onInputDeviceRemoved(int deviceId) {
-        }
-
-        @Override
-        public void onInputDeviceChanged(int deviceId) {
+            try {
+                for (MotionEvent event : splitBatchedMotionEvent(ev)) {
+                    mEvents.put(event);
+                }
+            } catch (InterruptedException ex) {
+                fail(mCurrentTestCase + ": interrupted while adding a MotionEvent to the queue");
+            }
         }
     }
-
-
 }
diff --git a/tests/tests/hardware/src/android/hardware/input/cts/tests/README.md b/tests/tests/hardware/src/android/hardware/input/cts/tests/README.md
new file mode 100644
index 0000000..a966ce7
--- /dev/null
+++ b/tests/tests/hardware/src/android/hardware/input/cts/tests/README.md
@@ -0,0 +1,18 @@
+How to add a test for a new HID device
+======================================
+
+    1. Connect the device of interest to Android
+    2. Open adb shell
+    3. Go to /sys/kernel/debug/hid/0005:0B05:4500.000F
+       Here "0005:0B05:4500.000F" is just an example, it will be different for each device.
+       Just print the /sys/kernel/debug/hid directory to see what it is for you.
+       This identifier will also change each time you reconnect the same physical device to Android.
+    4. `cat rdesc` will print the descriptor of this device
+    5. `cat events` will print the events that the device is producing
+       Once you cat the events, generate some events (by hand) on the device.
+       This will show you the hid reports that the device produces.
+
+To observe the MotionEvents that Android receives in response to the hid reports, write a small
+app that would override `dispatchGenericMotionEvent` and `dispatchKeyEvent` of an activity.
+There, print all of the event data that has changed. For MotionEvents, ensure to look at the
+historical data as well, since multiple reports could get batched into a single MotionEvent.
diff --git a/tests/tests/hardware/src/android/hardware/input/cts/tests/SonyDualshock4TestCase.java b/tests/tests/hardware/src/android/hardware/input/cts/tests/SonyDualshock4TestCase.java
new file mode 100644
index 0000000..010571a
--- /dev/null
+++ b/tests/tests/hardware/src/android/hardware/input/cts/tests/SonyDualshock4TestCase.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2018 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 android.hardware.cts.R;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class SonyDualshock4TestCase extends InputTestCase {
+
+    public SonyDualshock4TestCase() {
+        super(R.raw.sony_dualshock4_register);
+    }
+
+    @Test
+    public void testAllKeys() {
+        testInputEvents(R.raw.sony_dualshock4_keyeventtests);
+    }
+}