diff options
-rw-r--r-- | PREUPLOAD.cfg | 1 | ||||
-rw-r--r-- | cmds/uinput/Android.bp | 18 | ||||
-rw-r--r-- | cmds/uinput/MODULE_LICENSE_APACHE2 | 0 | ||||
-rw-r--r-- | cmds/uinput/NOTICE | 190 | ||||
-rw-r--r-- | cmds/uinput/README.md | 166 | ||||
-rw-r--r-- | cmds/uinput/jni/Android.bp | 23 | ||||
-rw-r--r-- | cmds/uinput/jni/com_android_commands_uinput_Device.cpp | 351 | ||||
-rw-r--r-- | cmds/uinput/jni/com_android_commands_uinput_Device.h | 67 | ||||
-rw-r--r-- | cmds/uinput/src/com/android/commands/uinput/Device.java | 232 | ||||
-rw-r--r-- | cmds/uinput/src/com/android/commands/uinput/Event.java | 454 | ||||
-rw-r--r-- | cmds/uinput/src/com/android/commands/uinput/InputAbsInfo.aidl | 26 | ||||
-rw-r--r-- | cmds/uinput/src/com/android/commands/uinput/Uinput.java | 140 | ||||
-rwxr-xr-x | cmds/uinput/uinput | 9 |
13 files changed, 1677 insertions, 0 deletions
diff --git a/PREUPLOAD.cfg b/PREUPLOAD.cfg index 2fd2e33bbc37..fc5efc6e03ac 100644 --- a/PREUPLOAD.cfg +++ b/PREUPLOAD.cfg @@ -6,6 +6,7 @@ clang_format = true clang_format = --commit ${PREUPLOAD_COMMIT} --style file --extensions c,h,cc,cpp cmds/hid/ cmds/input/ + cmds/uinput/ core/jni/ libs/input/ services/core/jni/ diff --git a/cmds/uinput/Android.bp b/cmds/uinput/Android.bp new file mode 100644 index 000000000000..0d7fed2a15c7 --- /dev/null +++ b/cmds/uinput/Android.bp @@ -0,0 +1,18 @@ +// Copyright 2020 The Android Open Source Project +// + +java_binary { + name: "uinput", + wrapper: "uinput", + srcs: ["**/*.java", + ":uinputcommand_aidl" + ], + required: ["libuinputcommand_jni"], +} + +filegroup { + name: "uinputcommand_aidl", + srcs: [ + "src/com/android/commands/uinput/InputAbsInfo.aidl", + ], +}
\ No newline at end of file diff --git a/cmds/uinput/MODULE_LICENSE_APACHE2 b/cmds/uinput/MODULE_LICENSE_APACHE2 new file mode 100644 index 000000000000..e69de29bb2d1 --- /dev/null +++ b/cmds/uinput/MODULE_LICENSE_APACHE2 diff --git a/cmds/uinput/NOTICE b/cmds/uinput/NOTICE new file mode 100644 index 000000000000..c5b1efa7aac7 --- /dev/null +++ b/cmds/uinput/NOTICE @@ -0,0 +1,190 @@ + + Copyright (c) 2005-2008, 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. + + 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. + + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + diff --git a/cmds/uinput/README.md b/cmds/uinput/README.md new file mode 100644 index 000000000000..47e1dad9ccd6 --- /dev/null +++ b/cmds/uinput/README.md @@ -0,0 +1,166 @@ +# Usage +## Two options to use the uinput command: +### 1. Interactive through stdin: +type `uinput -` into the terminal, then type/paste commands to send to the binary. +Use Ctrl+D to signal end of stream to the binary (EOF). + +This mode can be also used from an app to send uinput events. +For an example, see the cts test case at: [InputTestCase.java][2] + +When using another program to control uinput in interactive mode, registering a +new input device (for example, a bluetooth joystick) should be the first step. +After the device is added, you need to wait for the _onInputDeviceAdded_ +(see [InputDeviceListener][1]) notification before issuing commands +to the device. +Failure to do so will cause missed events and inconsistent behavior. + +### 2. Using a file as an input: +type `uinput <filename>`, and the file will be used an an input to the binary. +You must add a sufficient delay after a "register" command to ensure device +is ready. The interactive mode is the recommended method of communicating +with the uinput binary. + +All of the input commands should be in pseudo-JSON format as documented below. +See examples [here][3]. + +The file can have multiple commands one after the other (which is not strictly +legal JSON format, as this would imply multiple root elements). + +## Command description + +1. `register` +Register a new uinput device + +| Field | Type | Description | +|:-------------:|:-------------:|:-------------------------- | +| id | integer | Device id | +| command | string | Must be set to "register" | +| name | string | Device name | +| vid | 16-bit integer| Vendor id | +| pid | 16-bit integer| Product id | +| bus | string | Bus that device should use | +| configuration | int array | uinput device configuration| +| ff_effects_max| integer | ff_effects_max value | +| abs_info | array | ABS axes information | + +Device ID is used for matching the subsequent commands to a specific device +to avoid ambiguity when multiple devices are registered. + +Device bus is used to determine how the uinput device is connected to the host. +The options are "usb" and "bluetooth". + +Device configuration is used to configure uinput device. "type" field provides the UI_SET_* +control code, and data is a vector of control values to be sent to uinput device, depends on +the control code. + +| Field | Type | Description | +|:-------------:|:-------------:|:-------------------------- | +| type | integer | UI_SET_ control type | +| data | int array | control values | + +Device ff_effects_max must be provided if FFBIT is set. + +Device abs_info fields are provided to set the device axes information. It is an array of below +objects: +| Field | Type | Description | +|:-------------:|:-------------:|:-------------------------- | +| code | integer | Axis code | +| info | object | ABS information object | + +ABS information object is defined as below: +| Field | Type | Description | +|:-------------:|:-------------:|:-------------------------- | +| value | integer | Latest reported value | +| minimum | integer | Minimum value for the axis | +| maximum | integer | Maximum value for the axis | +| fuzz | integer | fuzz value for noise filter| +| flat | integer | values to be discarded | +| resolution | integer | resolution of axis | + +See [struct input_absinfo][4]) definitions. + +Example: +```json + +{ + "id": 1, + "command": "register", + "name": "Keyboard (Test)", + "vid": 0x18d2, + "pid": 0x2c42, + "bus": "usb", + "configuration":[ + {"type":100, "data":[1, 21]}, // UI_SET_EVBIT : EV_KEY and EV_FF + {"type":101, "data":[11, 2, 3, 4]}, // UI_SET_KEYBIT : KEY_0 KEY_1 KEY_2 KEY_3 + {"type":107, "data":[80]} // UI_SET_FFBIT : FF_RUMBLE + ], + "ff_effects_max" : 1, + "abs_info": [ + {"code":1, "info": {"value":20, "minimum":-255, + "maximum":255, "fuzz":0, "flat":0, "resolution":1} + }, + {"code":8, "info": {"value":-50, "minimum":-255, + "maximum":255, "fuzz":0, "flat":0, "resolution":1} + } + ] +} + +``` +2. `delay` +Add a delay to command processing + +| Field | Type | Description | +|:-------------:|:-------------:|:-------------------------- | +| id | integer | Device id | +| command | string | Must be set to "delay" | +| duration | integer | Delay in milliseconds | + +Example: +```json +{ + "id": 1, + "command": "delay", + "duration": 10 +} +``` + +3. `inject` +Send an array of uinput event packets [type, code, value] to the uinput device + +| Field | Type | Description | +|:-------------:|:-------------:|:-------------------------- | +| id | integer | Device id | +| command | string | Must be set to "inject" | +| events | integer array | events to inject | + +The "events" parameter is an array of integers, encapsulates evdev input_event type, code and value, +see the example below. + +Example: +```json +{ + "id": 1, + "command": "inject", + "events": [0x01, 0xb, 0x1, // EV_KEY, KEY_0, DOWN + 0x00, 0x00, 0x00, // EV_SYN, SYN_REPORT, 0 + 0x01, 0x0b, 0x00, // EV_KEY, KEY_0, UP + 0x00, 0x00, 0x00, // EV_SYN, SYN_REPORT, 0 + 0x01, 0x2, 0x1, // EV_KEY, KEY_1, DOWN + 0x00, 0x00, 0x01, // EV_SYN, SYN_REPORT, 0 + 0x01, 0x02, 0x00, // EV_KEY, KEY_1, UP + 0x00, 0x00, 0x01 // EV_SYN, SYN_REPORT, 0 + ] +} +``` + +### Notes +1. As soon as EOF is reached (either in interactive mode, or in file mode), +the device that was created will be unregistered. There is no +explicit command for unregistering a device. +2. The `getevent` utility can used to print out the key events +for debugging purposes. + +[1]: https://developer.android.com/reference/android/hardware/input/InputManager.InputDeviceListener.html +[2]: ../../../../cts/tests/tests/hardware/src/android/hardware/input/cts/tests/InputTestCase.java +[3]: ../../../../cts/tests/tests/hardware/res/raw/ +[4]: ../../../../bionic/libc/kernel/uapi/linux/input.h diff --git a/cmds/uinput/jni/Android.bp b/cmds/uinput/jni/Android.bp new file mode 100644 index 000000000000..199bbbd35274 --- /dev/null +++ b/cmds/uinput/jni/Android.bp @@ -0,0 +1,23 @@ +cc_library_shared { + name: "libuinputcommand_jni", + + srcs: [ + "com_android_commands_uinput_Device.cpp", + ":uinputcommand_aidl", + ], + + shared_libs: [ + "libandroid", + "libandroid_runtime_lazy", + "libbase", + "libbinder", + "liblog", + "libnativehelper", + ], + + cflags: [ + "-Wall", + "-Wextra", + "-Werror", + ], +} diff --git a/cmds/uinput/jni/com_android_commands_uinput_Device.cpp b/cmds/uinput/jni/com_android_commands_uinput_Device.cpp new file mode 100644 index 000000000000..06fa2aac2c7e --- /dev/null +++ b/cmds/uinput/jni/com_android_commands_uinput_Device.cpp @@ -0,0 +1,351 @@ +/* + * Copyright (C) 2020 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. + */ + +#define LOG_TAG "UinputCommandDevice" + +#include <linux/uinput.h> + +#include <fcntl.h> +#include <inttypes.h> +#include <time.h> +#include <unistd.h> +#include <algorithm> +#include <array> +#include <cstdio> +#include <cstring> +#include <iterator> +#include <memory> +#include <vector> + +#include <android/looper.h> +#include <android_os_Parcel.h> +#include <jni.h> +#include <log/log.h> +#include <nativehelper/JNIHelp.h> +#include <nativehelper/ScopedLocalRef.h> +#include <nativehelper/ScopedPrimitiveArray.h> +#include <nativehelper/ScopedUtfChars.h> + +#include <android-base/stringprintf.h> + +#include "com_android_commands_uinput_Device.h" + +namespace android { +namespace uinput { + +using src::com::android::commands::uinput::InputAbsInfo; + +static constexpr const char* UINPUT_PATH = "/dev/uinput"; + +static struct { + jmethodID onDeviceConfigure; + jmethodID onDeviceVibrating; + jmethodID onDeviceError; +} gDeviceCallbackClassInfo; + +static void checkAndClearException(JNIEnv* env, const char* methodName) { + if (env->ExceptionCheck()) { + ALOGE("An exception was thrown by callback '%s'.", methodName); + env->ExceptionClear(); + } +} + +DeviceCallback::DeviceCallback(JNIEnv* env, jobject callback) + : mCallbackObject(env->NewGlobalRef(callback)) { + env->GetJavaVM(&mJavaVM); +} + +DeviceCallback::~DeviceCallback() { + JNIEnv* env = getJNIEnv(); + env->DeleteGlobalRef(mCallbackObject); +} + +void DeviceCallback::onDeviceError() { + JNIEnv* env = getJNIEnv(); + env->CallVoidMethod(mCallbackObject, gDeviceCallbackClassInfo.onDeviceError); + checkAndClearException(env, "onDeviceError"); +} + +void DeviceCallback::onDeviceConfigure(int handle) { + JNIEnv* env = getJNIEnv(); + env->CallVoidMethod(mCallbackObject, gDeviceCallbackClassInfo.onDeviceConfigure, handle); + checkAndClearException(env, "onDeviceConfigure"); +} + +void DeviceCallback::onDeviceVibrating(int value) { + JNIEnv* env = getJNIEnv(); + env->CallVoidMethod(mCallbackObject, gDeviceCallbackClassInfo.onDeviceVibrating, value); + checkAndClearException(env, "onDeviceVibrating"); +} + +JNIEnv* DeviceCallback::getJNIEnv() { + JNIEnv* env; + mJavaVM->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6); + return env; +} + +std::unique_ptr<UinputDevice> UinputDevice::open(int32_t id, const char* name, int32_t vid, + int32_t pid, uint16_t bus, uint32_t ffEffectsMax, + std::unique_ptr<DeviceCallback> callback) { + android::base::unique_fd fd(::open(UINPUT_PATH, O_RDWR | O_NONBLOCK | O_CLOEXEC)); + if (!fd.ok()) { + ALOGE("Failed to open uinput: %s", strerror(errno)); + return nullptr; + } + + int32_t version; + ::ioctl(fd, UI_GET_VERSION, &version); + if (version < 5) { + ALOGE("Kernel version %d older than 5 is not supported", version); + return nullptr; + } + + struct uinput_setup setupDescriptor; + memset(&setupDescriptor, 0, sizeof(setupDescriptor)); + strlcpy(setupDescriptor.name, name, UINPUT_MAX_NAME_SIZE); + setupDescriptor.id.version = 1; + setupDescriptor.id.bustype = bus; + setupDescriptor.id.vendor = vid; + setupDescriptor.id.product = pid; + setupDescriptor.ff_effects_max = ffEffectsMax; + + // Request device configuration. + callback->onDeviceConfigure(fd.get()); + + // register the input device + if (::ioctl(fd, UI_DEV_SETUP, &setupDescriptor)) { + ALOGE("UI_DEV_SETUP ioctl failed on fd %d: %s.", fd.get(), strerror(errno)); + return nullptr; + } + + if (::ioctl(fd, UI_DEV_CREATE) != 0) { + ALOGE("Unable to create uinput device: %s.", strerror(errno)); + return nullptr; + } + + // using 'new' to access non-public constructor + return std::unique_ptr<UinputDevice>(new UinputDevice(id, std::move(fd), std::move(callback))); +} + +UinputDevice::UinputDevice(int32_t id, android::base::unique_fd fd, + std::unique_ptr<DeviceCallback> callback) + : mId(id), mFd(std::move(fd)), mDeviceCallback(std::move(callback)) { + ALooper* aLooper = ALooper_forThread(); + if (aLooper == nullptr) { + ALOGE("Could not get ALooper, ALooper_forThread returned NULL"); + aLooper = ALooper_prepare(ALOOPER_PREPARE_ALLOW_NON_CALLBACKS); + } + ALooper_addFd( + aLooper, mFd, 0, ALOOPER_EVENT_INPUT, + [](int, int events, void* data) { + UinputDevice* d = reinterpret_cast<UinputDevice*>(data); + return d->handleEvents(events); + }, + reinterpret_cast<void*>(this)); + ALOGI("uinput device %d created: version = %d, fd = %d", mId, UINPUT_VERSION, mFd.get()); +} + +UinputDevice::~UinputDevice() { + ::ioctl(mFd, UI_DEV_DESTROY); +} + +void UinputDevice::injectEvent(uint16_t type, uint16_t code, int32_t value) { + struct input_event event = {}; + event.type = type; + event.code = code; + event.value = value; + timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + TIMESPEC_TO_TIMEVAL(&event.time, &ts); + + if (::write(mFd, &event, sizeof(input_event)) < 0) { + ALOGE("Could not write event %" PRIu16 " %" PRIu16 " with value %" PRId32 " : %s", type, + code, value, strerror(errno)); + } +} + +int UinputDevice::handleEvents(int events) { + if (events & (ALOOPER_EVENT_ERROR | ALOOPER_EVENT_HANGUP)) { + ALOGE("uinput node was closed or an error occurred. events=0x%x", events); + mDeviceCallback->onDeviceError(); + return 0; + } + struct input_event ev; + ssize_t ret = ::read(mFd, &ev, sizeof(ev)); + if (ret < 0) { + ALOGE("Failed to read from uinput node: %s", strerror(errno)); + mDeviceCallback->onDeviceError(); + return 0; + } + + switch (ev.type) { + case EV_UINPUT: { + if (ev.code == UI_FF_UPLOAD) { + struct uinput_ff_upload ff_upload; + ff_upload.request_id = ev.value; + ::ioctl(mFd, UI_BEGIN_FF_UPLOAD, &ff_upload); + ff_upload.retval = 0; + ::ioctl(mFd, UI_END_FF_UPLOAD, &ff_upload); + } else if (ev.code == UI_FF_ERASE) { + struct uinput_ff_erase ff_erase; + ff_erase.request_id = ev.value; + ::ioctl(mFd, UI_BEGIN_FF_ERASE, &ff_erase); + ff_erase.retval = 0; + ::ioctl(mFd, UI_END_FF_ERASE, &ff_erase); + } + break; + } + case EV_FF: { + ALOGI("EV_FF effect = %d value = %d", ev.code, ev.value); + mDeviceCallback->onDeviceVibrating(ev.value); + break; + } + default: { + ALOGI("Unhandled event type: %" PRIu32, ev.type); + break; + } + } + + return 1; +} + +} // namespace uinput + +std::vector<int32_t> toVector(JNIEnv* env, jintArray javaArray) { + std::vector<int32_t> data; + if (javaArray == nullptr) { + return data; + } + + ScopedIntArrayRO scopedArray(env, javaArray); + size_t size = scopedArray.size(); + data.reserve(size); + for (size_t i = 0; i < size; i++) { + data.push_back(static_cast<int32_t>(scopedArray[i])); + } + return data; +} + +static jlong openUinputDevice(JNIEnv* env, jclass /* clazz */, jstring rawName, jint id, jint vid, + jint pid, jint bus, jint ffEffectsMax, jobject callback) { + ScopedUtfChars name(env, rawName); + if (name.c_str() == nullptr) { + return 0; + } + + std::unique_ptr<uinput::DeviceCallback> cb = + std::make_unique<uinput::DeviceCallback>(env, callback); + + std::unique_ptr<uinput::UinputDevice> d = + uinput::UinputDevice::open(id, name.c_str(), vid, pid, bus, ffEffectsMax, + std::move(cb)); + return reinterpret_cast<jlong>(d.release()); +} + +static void closeUinputDevice(JNIEnv* /* env */, jclass /* clazz */, jlong ptr) { + uinput::UinputDevice* d = reinterpret_cast<uinput::UinputDevice*>(ptr); + if (d != nullptr) { + delete d; + } +} + +static void injectEvent(JNIEnv* /* env */, jclass /* clazz */, jlong ptr, jint type, jint code, + jint value) { + uinput::UinputDevice* d = reinterpret_cast<uinput::UinputDevice*>(ptr); + if (d != nullptr) { + d->injectEvent(static_cast<uint16_t>(type), static_cast<uint16_t>(code), + static_cast<int32_t>(value)); + } else { + ALOGE("Could not inject event, Device* is null!"); + } +} + +static void configure(JNIEnv* env, jclass /* clazz */, jint handle, jint code, + jintArray rawConfigs) { + std::vector<int32_t> configs = toVector(env, rawConfigs); + // Configure uinput device, with user specified code and value. + for (auto& config : configs) { + ::ioctl(static_cast<int>(handle), _IOW(UINPUT_IOCTL_BASE, code, int), config); + } +} + +static void setAbsInfo(JNIEnv* env, jclass /* clazz */, jint handle, jint axisCode, + jobject infoObj) { + Parcel* parcel = parcelForJavaObject(env, infoObj); + uinput::InputAbsInfo info; + + info.readFromParcel(parcel); + + struct uinput_abs_setup absSetup; + absSetup.code = axisCode; + absSetup.absinfo.maximum = info.maximum; + absSetup.absinfo.minimum = info.minimum; + absSetup.absinfo.value = info.value; + absSetup.absinfo.fuzz = info.fuzz; + absSetup.absinfo.flat = info.flat; + absSetup.absinfo.resolution = info.resolution; + + ::ioctl(static_cast<int>(handle), UI_ABS_SETUP, &absSetup); +} + +static JNINativeMethod sMethods[] = { + {"nativeOpenUinputDevice", + "(Ljava/lang/String;IIIII" + "Lcom/android/commands/uinput/Device$DeviceCallback;)J", + reinterpret_cast<void*>(openUinputDevice)}, + {"nativeInjectEvent", "(JIII)V", reinterpret_cast<void*>(injectEvent)}, + {"nativeConfigure", "(II[I)V", reinterpret_cast<void*>(configure)}, + {"nativeSetAbsInfo", "(IILandroid/os/Parcel;)V", reinterpret_cast<void*>(setAbsInfo)}, + {"nativeCloseUinputDevice", "(J)V", reinterpret_cast<void*>(closeUinputDevice)}, +}; + +int register_com_android_commands_uinput_Device(JNIEnv* env) { + jclass clazz = env->FindClass("com/android/commands/uinput/Device$DeviceCallback"); + if (clazz == nullptr) { + ALOGE("Unable to find class 'DeviceCallback'"); + return JNI_ERR; + } + + uinput::gDeviceCallbackClassInfo.onDeviceConfigure = + env->GetMethodID(clazz, "onDeviceConfigure", "(I)V"); + uinput::gDeviceCallbackClassInfo.onDeviceVibrating = + env->GetMethodID(clazz, "onDeviceVibrating", "(I)V"); + uinput::gDeviceCallbackClassInfo.onDeviceError = + env->GetMethodID(clazz, "onDeviceError", "()V"); + if (uinput::gDeviceCallbackClassInfo.onDeviceConfigure == nullptr || + uinput::gDeviceCallbackClassInfo.onDeviceError == nullptr || + uinput::gDeviceCallbackClassInfo.onDeviceVibrating == nullptr) { + ALOGE("Unable to obtain onDeviceConfigure or onDeviceError or onDeviceVibrating methods"); + return JNI_ERR; + } + return jniRegisterNativeMethods(env, "com/android/commands/uinput/Device", sMethods, + NELEM(sMethods)); +} + +} // namespace android + +jint JNI_OnLoad(JavaVM* jvm, void*) { + JNIEnv* env = nullptr; + if (jvm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6)) { + return JNI_ERR; + } + + if (android::register_com_android_commands_uinput_Device(env) < 0) { + return JNI_ERR; + } + + return JNI_VERSION_1_6; +} diff --git a/cmds/uinput/jni/com_android_commands_uinput_Device.h b/cmds/uinput/jni/com_android_commands_uinput_Device.h new file mode 100644 index 000000000000..5a9a06cfb32e --- /dev/null +++ b/cmds/uinput/jni/com_android_commands_uinput_Device.h @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2020 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. + */ + +#include <memory> +#include <vector> + +#include <jni.h> +#include <linux/input.h> + +#include <android-base/unique_fd.h> +#include "src/com/android/commands/uinput/InputAbsInfo.h" + +namespace android { +namespace uinput { + +class DeviceCallback { +public: + DeviceCallback(JNIEnv* env, jobject callback); + ~DeviceCallback(); + + void onDeviceOpen(); + void onDeviceGetReport(uint32_t requestId, uint8_t reportId); + void onDeviceOutput(const std::vector<uint8_t>& data); + void onDeviceConfigure(int handle); + void onDeviceVibrating(int value); + void onDeviceError(); + +private: + JNIEnv* getJNIEnv(); + jobject mCallbackObject; + JavaVM* mJavaVM; +}; + +class UinputDevice { +public: + static std::unique_ptr<UinputDevice> open(int32_t id, const char* name, int32_t vid, + int32_t pid, uint16_t bus, uint32_t ff_effects_max, + std::unique_ptr<DeviceCallback> callback); + + virtual ~UinputDevice(); + + void injectEvent(uint16_t type, uint16_t code, int32_t value); + int handleEvents(int events); + +private: + UinputDevice(int32_t id, android::base::unique_fd fd, std::unique_ptr<DeviceCallback> callback); + + int32_t mId; + android::base::unique_fd mFd; + std::unique_ptr<DeviceCallback> mDeviceCallback; +}; + +} // namespace uinput +} // namespace android diff --git a/cmds/uinput/src/com/android/commands/uinput/Device.java b/cmds/uinput/src/com/android/commands/uinput/Device.java new file mode 100644 index 000000000000..62bee7b964bd --- /dev/null +++ b/cmds/uinput/src/com/android/commands/uinput/Device.java @@ -0,0 +1,232 @@ +/* + * Copyright (C) 2020 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.commands.uinput; + +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Looper; +import android.os.Message; +import android.os.Parcel; +import android.os.SystemClock; +import android.util.Log; +import android.util.SparseArray; + +import com.android.internal.os.SomeArgs; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.IOException; +import java.io.OutputStream; + +import src.com.android.commands.uinput.InputAbsInfo; + +/** + * Device class defines uinput device interfaces of device operations, for device open, close, + * configuration, events injection. + */ +public class Device { + private static final String TAG = "UinputDevice"; + + private static final int MSG_OPEN_UINPUT_DEVICE = 1; + private static final int MSG_CLOSE_UINPUT_DEVICE = 2; + private static final int MSG_INJECT_EVENT = 3; + + private final int mId; + private final HandlerThread mThread; + private final DeviceHandler mHandler; + // mConfiguration is sparse array of ioctl code and array of values. + private final SparseArray<int[]> mConfiguration; + private final SparseArray<InputAbsInfo> mAbsInfo; + private final OutputStream mOutputStream; + private final Object mCond = new Object(); + private long mTimeToSend; + + static { + System.loadLibrary("uinputcommand_jni"); + } + + private static native long nativeOpenUinputDevice(String name, int id, int vid, int pid, + int bus, int ffEffectsMax, DeviceCallback callback); + private static native void nativeCloseUinputDevice(long ptr); + private static native void nativeInjectEvent(long ptr, int type, int code, int value); + private static native void nativeConfigure(int handle, int code, int[] configs); + private static native void nativeSetAbsInfo(int handle, int axisCode, Parcel axisParcel); + + public Device(int id, String name, int vid, int pid, int bus, + SparseArray<int[]> configuration, int ffEffectsMax, + SparseArray<InputAbsInfo> absInfo) { + mId = id; + mThread = new HandlerThread("UinputDeviceHandler"); + mThread.start(); + mHandler = new DeviceHandler(mThread.getLooper()); + mConfiguration = configuration; + mAbsInfo = absInfo; + mOutputStream = System.out; + SomeArgs args = SomeArgs.obtain(); + args.argi1 = id; + args.argi2 = vid; + args.argi3 = pid; + args.argi4 = bus; + args.argi5 = ffEffectsMax; + if (name != null) { + args.arg1 = name; + } else { + args.arg1 = id + ":" + vid + ":" + pid; + } + + mHandler.obtainMessage(MSG_OPEN_UINPUT_DEVICE, args).sendToTarget(); + mTimeToSend = SystemClock.uptimeMillis(); + } + + /** + * Inject uinput events to device + * + * @param events Array of raw uinput events. + */ + public void injectEvent(int[] events) { + // if two messages are sent at identical time, they will be processed in order received + Message msg = mHandler.obtainMessage(MSG_INJECT_EVENT, events); + mHandler.sendMessageAtTime(msg, mTimeToSend); + } + + /** + * Impose a delay to the device for execution. + * + * @param delay Time to delay in unit of milliseconds. + */ + public void addDelay(int delay) { + mTimeToSend = Math.max(SystemClock.uptimeMillis(), mTimeToSend) + delay; + } + + /** + * Close an uinput device. + * + */ + public void close() { + Message msg = mHandler.obtainMessage(MSG_CLOSE_UINPUT_DEVICE); + mHandler.sendMessageAtTime(msg, Math.max(SystemClock.uptimeMillis(), mTimeToSend) + 1); + try { + synchronized (mCond) { + mCond.wait(); + } + } catch (InterruptedException ignore) { + } + } + + private class DeviceHandler extends Handler { + private long mPtr; + private int mBarrierToken; + + DeviceHandler(Looper looper) { + super(looper); + } + + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MSG_OPEN_UINPUT_DEVICE: + SomeArgs args = (SomeArgs) msg.obj; + mPtr = nativeOpenUinputDevice((String) args.arg1, args.argi1, args.argi2, + args.argi3, args.argi4, args.argi5, + new DeviceCallback()); + break; + case MSG_INJECT_EVENT: + if (mPtr != 0) { + int[] events = (int[]) msg.obj; + for (int pos = 0; pos + 2 < events.length; pos += 3) { + nativeInjectEvent(mPtr, events[pos], events[pos + 1], events[pos + 2]); + } + } + break; + case MSG_CLOSE_UINPUT_DEVICE: + if (mPtr != 0) { + nativeCloseUinputDevice(mPtr); + getLooper().quitSafely(); + mPtr = 0; + } else { + Log.e(TAG, "Tried to close already closed device."); + } + Log.i(TAG, "Device closed."); + synchronized (mCond) { + mCond.notify(); + } + break; + default: + throw new IllegalArgumentException("Unknown device message"); + } + } + + public void pauseEvents() { + mBarrierToken = getLooper().myQueue().postSyncBarrier(); + } + + public void resumeEvents() { + getLooper().myQueue().removeSyncBarrier(mBarrierToken); + mBarrierToken = 0; + } + } + + private class DeviceCallback { + public void onDeviceOpen() { + mHandler.resumeEvents(); + } + + public void onDeviceConfigure(int handle) { + for (int i = 0; i < mConfiguration.size(); i++) { + int key = mConfiguration.keyAt(i); + int[] data = mConfiguration.get(key); + nativeConfigure(handle, key, data); + } + + if (mAbsInfo != null) { + for (int i = 0; i < mAbsInfo.size(); i++) { + int key = mAbsInfo.keyAt(i); + InputAbsInfo info = mAbsInfo.get(key); + Parcel parcel = Parcel.obtain(); + info.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + nativeSetAbsInfo(handle, key, parcel); + } + } + } + + public void onDeviceVibrating(int value) { + JSONObject json = new JSONObject(); + try { + json.put("reason", "vibrating"); + json.put("id", mId); + json.put("status", value); + } catch (JSONException e) { + throw new RuntimeException("Could not create JSON object ", e); + } + try { + mOutputStream.write(json.toString().getBytes()); + mOutputStream.flush(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public void onDeviceError() { + Log.e(TAG, "Device error occurred, closing /dev/uinput"); + Message msg = mHandler.obtainMessage(MSG_CLOSE_UINPUT_DEVICE); + msg.setAsynchronous(true); + msg.sendToTarget(); + } + } +} diff --git a/cmds/uinput/src/com/android/commands/uinput/Event.java b/cmds/uinput/src/com/android/commands/uinput/Event.java new file mode 100644 index 000000000000..c4ba05054eda --- /dev/null +++ b/cmds/uinput/src/com/android/commands/uinput/Event.java @@ -0,0 +1,454 @@ +/* + * Copyright (C) 2020 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.commands.uinput; + +import android.util.JsonReader; +import android.util.JsonToken; +import android.util.Log; +import android.util.SparseArray; + +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.Arrays; + +import src.com.android.commands.uinput.InputAbsInfo; + +/** + * An event is a JSON file defined action event to instruct uinput to perform a command like + * device registration or uinput events injection. + */ +public class Event { + private static final String TAG = "UinputEvent"; + + public static final String COMMAND_REGISTER = "register"; + public static final String COMMAND_DELAY = "delay"; + public static final String COMMAND_INJECT = "inject"; + private static final int ABS_CNT = 64; + + // These constants come from "include/uapi/linux/input.h" in the kernel + enum Bus { + USB(0x03), BLUETOOTH(0x05); + private final int mValue; + + Bus(int value) { + mValue = value; + } + + int getValue() { + return mValue; + } + } + + private int mId; + private String mCommand; + private String mName; + private int mVid; + private int mPid; + private Bus mBus; + private int[] mInjections; + private SparseArray<int[]> mConfiguration; + private int mDuration; + private int mFfEffectsMax = 0; + private SparseArray<InputAbsInfo> mAbsInfo; + + public int getId() { + return mId; + } + + public String getCommand() { + return mCommand; + } + + public String getName() { + return mName; + } + + public int getVendorId() { + return mVid; + } + + public int getProductId() { + return mPid; + } + + public int getBus() { + return mBus.getValue(); + } + + public int[] getInjections() { + return mInjections; + } + + public SparseArray<int[]> getConfiguration() { + return mConfiguration; + } + + public int getDuration() { + return mDuration; + } + + public int getFfEffectsMax() { + return mFfEffectsMax; + } + + public SparseArray<InputAbsInfo> getAbsInfo() { + return mAbsInfo; + } + + /** + * Convert an event to String. + */ + public String toString() { + return "Event{id=" + mId + + ", command=" + mCommand + + ", name=" + mName + + ", vid=" + mVid + + ", pid=" + mPid + + ", bus=" + mBus + + ", events=" + Arrays.toString(mInjections) + + ", configuration=" + mConfiguration + + ", duration=" + mDuration + + ", ff_effects_max=" + mFfEffectsMax + + "}"; + } + + private static class Builder { + private Event mEvent; + + Builder() { + mEvent = new Event(); + } + + public void setId(int id) { + mEvent.mId = id; + } + + private void setCommand(String command) { + mEvent.mCommand = command; + } + + public void setName(String name) { + mEvent.mName = name; + } + + public void setInjections(int[] events) { + mEvent.mInjections = events; + } + + public void setConfiguration(SparseArray<int[]> configuration) { + mEvent.mConfiguration = configuration; + } + + public void setVid(int vid) { + mEvent.mVid = vid; + } + + public void setPid(int pid) { + mEvent.mPid = pid; + } + + public void setBus(Bus bus) { + mEvent.mBus = bus; + } + + public void setDuration(int duration) { + mEvent.mDuration = duration; + } + + public void setFfEffectsMax(int ffEffectsMax) { + mEvent.mFfEffectsMax = ffEffectsMax; + } + + public void setAbsInfo(SparseArray<InputAbsInfo> absInfo) { + mEvent.mAbsInfo = absInfo; + } + + public Event build() { + if (mEvent.mId == -1) { + throw new IllegalStateException("No event id"); + } else if (mEvent.mCommand == null) { + throw new IllegalStateException("Event does not contain a command"); + } + if (COMMAND_REGISTER.equals(mEvent.mCommand)) { + if (mEvent.mConfiguration == null) { + throw new IllegalStateException( + "Device registration is missing configuration"); + } + } else if (COMMAND_DELAY.equals(mEvent.mCommand)) { + if (mEvent.mDuration <= 0) { + throw new IllegalStateException("Delay has missing or invalid duration"); + } + } else if (COMMAND_INJECT.equals(mEvent.mCommand)) { + if (mEvent.mInjections == null) { + throw new IllegalStateException("Inject command is missing injection data"); + } + } else { + throw new IllegalStateException("Unknown command " + mEvent.mCommand); + } + return mEvent; + } + } + + /** + * A class that parses the JSON event format from an input stream to build device events. + */ + public static class Reader { + private JsonReader mReader; + + public Reader(InputStreamReader in) { + mReader = new JsonReader(in); + mReader.setLenient(true); + } + + /** + * Get next event entry from JSON file reader. + */ + public Event getNextEvent() throws IOException { + Event e = null; + while (e == null && mReader.peek() != JsonToken.END_DOCUMENT) { + Event.Builder eb = new Event.Builder(); + try { + mReader.beginObject(); + while (mReader.hasNext()) { + String name = mReader.nextName(); + switch (name) { + case "id": + eb.setId(readInt()); + break; + case "command": + eb.setCommand(mReader.nextString()); + break; + case "name": + eb.setName(mReader.nextString()); + break; + case "vid": + eb.setVid(readInt()); + break; + case "pid": + eb.setPid(readInt()); + break; + case "bus": + eb.setBus(readBus()); + break; + case "events": + int[] injections = readIntList().stream() + .mapToInt(Integer::intValue).toArray(); + eb.setInjections(injections); + break; + case "configuration": + eb.setConfiguration(readConfiguration()); + break; + case "ff_effects_max": + eb.setFfEffectsMax(readInt()); + break; + case "abs_info": + eb.setAbsInfo(readAbsInfoArray()); + break; + case "duration": + eb.setDuration(readInt()); + break; + default: + mReader.skipValue(); + } + } + mReader.endObject(); + } catch (IllegalStateException ex) { + error("Error reading in object, ignoring.", ex); + consumeRemainingElements(); + mReader.endObject(); + continue; + } + e = eb.build(); + } + + return e; + } + + private ArrayList<Integer> readIntList() throws IOException { + ArrayList<Integer> data = new ArrayList<Integer>(); + try { + mReader.beginArray(); + while (mReader.hasNext()) { + data.add(Integer.decode(mReader.nextString())); + } + mReader.endArray(); + } catch (IllegalStateException | NumberFormatException e) { + consumeRemainingElements(); + mReader.endArray(); + throw new IllegalStateException("Encountered malformed data.", e); + } + return data; + } + + private byte[] readData() throws IOException { + ArrayList<Integer> data = readIntList(); + byte[] rawData = new byte[data.size()]; + for (int i = 0; i < data.size(); i++) { + int d = data.get(i); + if ((d & 0xFF) != d) { + throw new IllegalStateException("Invalid data, all values must be byte-sized"); + } + rawData[i] = (byte) d; + } + return rawData; + } + + private int readInt() throws IOException { + String val = mReader.nextString(); + return Integer.decode(val); + } + + private Bus readBus() throws IOException { + String val = mReader.nextString(); + return Bus.valueOf(val.toUpperCase()); + } + + private SparseArray<int[]> readConfiguration() + throws IllegalStateException, IOException { + SparseArray<int[]> configuration = new SparseArray<>(); + try { + mReader.beginArray(); + while (mReader.hasNext()) { + int type = 0; + int[] data = null; + mReader.beginObject(); + while (mReader.hasNext()) { + String name = mReader.nextName(); + switch (name) { + case "type": + type = readInt(); + break; + case "data": + data = readIntList().stream() + .mapToInt(Integer::intValue).toArray(); + break; + default: + consumeRemainingElements(); + mReader.endObject(); + throw new IllegalStateException( + "Invalid key in device configuration: " + name); + } + } + mReader.endObject(); + if (data != null) { + configuration.put(type, data); + } + } + mReader.endArray(); + } catch (IllegalStateException | NumberFormatException e) { + consumeRemainingElements(); + mReader.endArray(); + throw new IllegalStateException("Encountered malformed data.", e); + } + return configuration; + } + + private InputAbsInfo readAbsInfo() throws IllegalStateException, IOException { + InputAbsInfo absInfo = new InputAbsInfo(); + try { + mReader.beginObject(); + while (mReader.hasNext()) { + String name = mReader.nextName(); + switch (name) { + case "value": + absInfo.value = readInt(); + break; + case "minimum": + absInfo.minimum = readInt(); + break; + case "maximum": + absInfo.maximum = readInt(); + break; + case "fuzz": + absInfo.fuzz = readInt(); + break; + case "flat": + absInfo.flat = readInt(); + break; + case "resolution": + absInfo.resolution = readInt(); + break; + default: + consumeRemainingElements(); + mReader.endObject(); + throw new IllegalStateException("Invalid key in abs info: " + name); + } + } + mReader.endObject(); + } catch (IllegalStateException | NumberFormatException e) { + consumeRemainingElements(); + mReader.endObject(); + throw new IllegalStateException("Encountered malformed data.", e); + } + return absInfo; + } + + private SparseArray<InputAbsInfo> readAbsInfoArray() + throws IllegalStateException, IOException { + SparseArray<InputAbsInfo> infoArray = new SparseArray<>(); + try { + mReader.beginArray(); + while (mReader.hasNext()) { + int type = 0; + InputAbsInfo absInfo = null; + mReader.beginObject(); + while (mReader.hasNext()) { + String name = mReader.nextName(); + switch (name) { + case "code": + type = readInt(); + break; + case "info": + absInfo = readAbsInfo(); + break; + default: + consumeRemainingElements(); + mReader.endObject(); + throw new IllegalStateException("Invalid key in abs info array: " + + name); + } + } + mReader.endObject(); + if (absInfo != null) { + infoArray.put(type, absInfo); + } + } + mReader.endArray(); + } catch (IllegalStateException | NumberFormatException e) { + consumeRemainingElements(); + mReader.endArray(); + throw new IllegalStateException("Encountered malformed data.", e); + } + return infoArray; + } + + private void consumeRemainingElements() throws IOException { + while (mReader.hasNext()) { + mReader.skipValue(); + } + } + } + + private static void error(String msg, Exception e) { + System.out.println(msg); + Log.e(TAG, msg); + if (e != null) { + Log.e(TAG, Log.getStackTraceString(e)); + } + } +} diff --git a/cmds/uinput/src/com/android/commands/uinput/InputAbsInfo.aidl b/cmds/uinput/src/com/android/commands/uinput/InputAbsInfo.aidl new file mode 100644 index 000000000000..88c57f2c8965 --- /dev/null +++ b/cmds/uinput/src/com/android/commands/uinput/InputAbsInfo.aidl @@ -0,0 +1,26 @@ +/* +** +** Copyright 2020, 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 src.com.android.commands.uinput; + +parcelable InputAbsInfo { + int value; + int minimum; + int maximum; + int fuzz; + int flat; + int resolution; +} diff --git a/cmds/uinput/src/com/android/commands/uinput/Uinput.java b/cmds/uinput/src/com/android/commands/uinput/Uinput.java new file mode 100644 index 000000000000..f7601a2f7c07 --- /dev/null +++ b/cmds/uinput/src/com/android/commands/uinput/Uinput.java @@ -0,0 +1,140 @@ +/* + * Copyright (C) 2020 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.commands.uinput; + +import android.util.Log; +import android.util.SparseArray; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.UnsupportedEncodingException; + +/** + * Uinput class encapsulates execution of "uinput" command. It parses the provided input stream + * parameters as JSON file format, extract event entries and perform commands of event entries. + * Uinput device will be created when performing registration command and used to inject events. + */ +public class Uinput { + private static final String TAG = "UINPUT"; + + private final Event.Reader mReader; + private final SparseArray<Device> mDevices; + + private static void usage() { + error("Usage: uinput [FILE]"); + } + + /** + * Commandline "uinput" binary main entry + */ + public static void main(String[] args) { + if (args.length != 1) { + usage(); + System.exit(1); + } + + InputStream stream = null; + try { + if (args[0].equals("-")) { + stream = System.in; + } else { + File f = new File(args[0]); + stream = new FileInputStream(f); + } + (new Uinput(stream)).run(); + } catch (Exception e) { + error("Uinput injection failed.", e); + System.exit(1); + } finally { + try { + stream.close(); + } catch (IOException e) { + } + } + } + + private Uinput(InputStream in) { + mDevices = new SparseArray<Device>(); + try { + mReader = new Event.Reader(new InputStreamReader(in, "UTF-8")); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); + } + } + + private void run() { + try { + Event e = null; + while ((e = mReader.getNextEvent()) != null) { + process(e); + } + } catch (IOException ex) { + error("Error reading in events.", ex); + } + + for (int i = 0; i < mDevices.size(); i++) { + mDevices.valueAt(i).close(); + } + } + + private void process(Event e) { + final int index = mDevices.indexOfKey(e.getId()); + if (index >= 0) { + Device d = mDevices.valueAt(index); + if (Event.COMMAND_DELAY.equals(e.getCommand())) { + d.addDelay(e.getDuration()); + } else if (Event.COMMAND_INJECT.equals(e.getCommand())) { + d.injectEvent(e.getInjections()); + } else { + if (Event.COMMAND_REGISTER.equals(e.getCommand())) { + error("Device id=" + e.getId() + " is already registered. Ignoring event."); + } else { + error("Unknown command \"" + e.getCommand() + "\". Ignoring event."); + } + } + } else if (Event.COMMAND_REGISTER.equals(e.getCommand())) { + registerDevice(e); + } else { + Log.e(TAG, "Unknown device id specified. Ignoring event."); + } + } + + private void registerDevice(Event e) { + if (!Event.COMMAND_REGISTER.equals(e.getCommand())) { + throw new IllegalStateException( + "Tried to send command \"" + e.getCommand() + "\" to an unregistered device!"); + } + int id = e.getId(); + Device d = new Device(id, e.getName(), e.getVendorId(), e.getProductId(), e.getBus(), + e.getConfiguration(), e.getFfEffectsMax(), e.getAbsInfo()); + mDevices.append(id, d); + } + + private static void error(String msg) { + error(msg, null); + } + + private static void error(String msg, Exception e) { + Log.e(TAG, msg); + if (e != null) { + Log.e(TAG, Log.getStackTraceString(e)); + } + } +} diff --git a/cmds/uinput/uinput b/cmds/uinput/uinput new file mode 100755 index 000000000000..ab2770ee2043 --- /dev/null +++ b/cmds/uinput/uinput @@ -0,0 +1,9 @@ +#!/system/bin/sh + +# Preload the native portion libuinputcommand_jni.so to bypass the dependency +# checks in the Java classloader, which prohibit dependencies that aren't +# listed in system/core/rootdir/etc/public.libraries.android.txt. +export LD_PRELOAD=libuinputcommand_jni.so + +export CLASSPATH=/system/framework/uinput.jar +exec app_process /system/bin com.android.commands.uinput.Uinput "$@" |