diff options
author | Michael Wright <michaelwr@google.com> | 2015-06-03 13:46:48 +0100 |
---|---|---|
committer | Michael Wright <michaelwr@google.com> | 2015-06-12 11:49:29 +0100 |
commit | 1f2c7688c1f673790d61645632ae5e1838f021a4 (patch) | |
tree | 73fcbb2d92ccc790cf7bf62b31c8c2d28f6264b3 /cmds/hid | |
parent | 6a5c0e10b192fe70e237c83bc752e2cbf66ffe0e (diff) | |
download | frameworks_base-1f2c7688c1f673790d61645632ae5e1838f021a4.zip frameworks_base-1f2c7688c1f673790d61645632ae5e1838f021a4.tar.gz frameworks_base-1f2c7688c1f673790d61645632ae5e1838f021a4.tar.bz2 |
Add new `hid` command.
This allows the shell user to inject HID events.
Change-Id: I37faff576299ff14092b61ed39f2a1c086f672a5
Diffstat (limited to 'cmds/hid')
-rw-r--r-- | cmds/hid/Android.mk | 18 | ||||
-rw-r--r-- | cmds/hid/MODULE_LICENSE_APACHE2 | 0 | ||||
-rw-r--r-- | cmds/hid/NOTICE | 190 | ||||
-rwxr-xr-x | cmds/hid/hid | 8 | ||||
-rw-r--r-- | cmds/hid/jni/Android.mk | 23 | ||||
-rw-r--r-- | cmds/hid/jni/com_android_commands_hid_Device.cpp | 253 | ||||
-rw-r--r-- | cmds/hid/jni/com_android_commands_hid_Device.h | 61 | ||||
-rw-r--r-- | cmds/hid/src/com/android/commands/hid/Device.java | 163 | ||||
-rw-r--r-- | cmds/hid/src/com/android/commands/hid/Event.java | 255 | ||||
-rw-r--r-- | cmds/hid/src/com/android/commands/hid/Hid.java | 133 |
10 files changed, 1104 insertions, 0 deletions
diff --git a/cmds/hid/Android.mk b/cmds/hid/Android.mk new file mode 100644 index 0000000..ff3691d --- /dev/null +++ b/cmds/hid/Android.mk @@ -0,0 +1,18 @@ +# Copyright 2015 The Android Open Source Project +# +LOCAL_PATH:= $(call my-dir) + +include $(CLEAR_VARS) +LOCAL_SRC_FILES := $(call all-subdir-java-files) +LOCAL_MODULE := hid +LOCAL_JNI_SHARED_LIBRARIES := libhidcommand_jni +include $(BUILD_JAVA_LIBRARY) + +include $(CLEAR_VARS) +LOCAL_MODULE := hid +LOCAL_SRC_FILES := hid +LOCAL_MODULE_TAGS := optional +LOCAL_MODULE_CLASS := EXECUTABLES +include $(BUILD_PREBUILT) + +include $(call all-makefiles-under,$(LOCAL_PATH)) diff --git a/cmds/hid/MODULE_LICENSE_APACHE2 b/cmds/hid/MODULE_LICENSE_APACHE2 new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/cmds/hid/MODULE_LICENSE_APACHE2 diff --git a/cmds/hid/NOTICE b/cmds/hid/NOTICE new file mode 100644 index 0000000..c5b1efa --- /dev/null +++ b/cmds/hid/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/hid/hid b/cmds/hid/hid new file mode 100755 index 0000000..2359fcd --- /dev/null +++ b/cmds/hid/hid @@ -0,0 +1,8 @@ +#!/system/bin/sh +# +# Script to start "hid" on the device, which has a very rudimentary +# shell. +# +base=/system +export CLASSPATH=$base/framework/hid.jar +exec app_process $base/bin com.android.commands.hid.Hid "$@" diff --git a/cmds/hid/jni/Android.mk b/cmds/hid/jni/Android.mk new file mode 100644 index 0000000..8163a9d --- /dev/null +++ b/cmds/hid/jni/Android.mk @@ -0,0 +1,23 @@ +LOCAL_PATH := $(call my-dir) + +include $(CLEAR_VARS) + +LOCAL_SRC_FILES := \ + com_android_commands_hid_Device.cpp + +LOCAL_C_INCLUDES := \ + $(JNI_H_INCLUDE) \ + frameworks/base/core/jni + +LOCAL_SHARED_LIBRARIES := \ + libandroid_runtime \ + liblog \ + libnativehelper \ + libutils + +LOCAL_MODULE := libhidcommand_jni +LOCAL_MODULE_TAGS := optional + +LOCAL_CFLAGS += -Wall + +include $(BUILD_SHARED_LIBRARY) diff --git a/cmds/hid/jni/com_android_commands_hid_Device.cpp b/cmds/hid/jni/com_android_commands_hid_Device.cpp new file mode 100644 index 0000000..4278e7d --- /dev/null +++ b/cmds/hid/jni/com_android_commands_hid_Device.cpp @@ -0,0 +1,253 @@ +/* + * Copyright (C) 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. + */ + +#define LOG_TAG "HidCommandDevice" + +#include "com_android_commands_hid_Device.h" + +#include <linux/uhid.h> + +#include <fcntl.h> +#include <cstdio> +#include <cstring> +#include <memory> +#include <unistd.h> + +#include <android_runtime/AndroidRuntime.h> +#include <android_runtime/Log.h> +#include <android_os_MessageQueue.h> +#include <core_jni_helpers.h> +#include <jni.h> +#include <JNIHelp.h> +#include <ScopedPrimitiveArray.h> +#include <ScopedUtfChars.h> +#include <utils/Log.h> +#include <utils/Looper.h> +#include <utils/StrongPointer.h> + +namespace android { +namespace uhid { + +static const char* UHID_PATH = "/dev/uhid"; +static const size_t UHID_MAX_NAME_LENGTH = 128; + +static struct { + jmethodID onDeviceOpen; + jmethodID onDeviceError; +} gDeviceCallbackClassInfo; + +static int handleLooperEvents(int fd, int events, void* data) { + Device* d = reinterpret_cast<Device*>(data); + return d->handleEvents(events); +} + +static void checkAndClearException(JNIEnv* env, const char* methodName) { + if (env->ExceptionCheck()) { + ALOGE("An exception was thrown by callback '%s'.", methodName); + LOGE_EX(env); + env->ExceptionClear(); + } +} + +DeviceCallback::DeviceCallback(JNIEnv* env, jobject callback) : + mCallbackObject(env->NewGlobalRef(callback)) { } + +DeviceCallback::~DeviceCallback() { + JNIEnv* env = AndroidRuntime::getJNIEnv(); + env->DeleteGlobalRef(mCallbackObject); +} + +void DeviceCallback::onDeviceError() { + JNIEnv* env = AndroidRuntime::getJNIEnv(); + env->CallVoidMethod(mCallbackObject, gDeviceCallbackClassInfo.onDeviceError); + checkAndClearException(env, "onDeviceError"); +} + +void DeviceCallback::onDeviceOpen() { + JNIEnv* env = AndroidRuntime::getJNIEnv(); + env->CallVoidMethod(mCallbackObject, gDeviceCallbackClassInfo.onDeviceOpen); + checkAndClearException(env, "onDeviceOpen"); +} + +Device* Device::open(int32_t id, const char* name, int32_t vid, int32_t pid, + std::unique_ptr<uint8_t[]> descriptor, size_t descriptorSize, + std::unique_ptr<DeviceCallback> callback, sp<Looper> looper) { + + int fd = ::open(UHID_PATH, O_RDWR | O_CLOEXEC); + if (fd < 0) { + ALOGE("Failed to open uhid: %s", strerror(errno)); + return nullptr; + } + + struct uhid_event ev; + memset(&ev, 0, sizeof(ev)); + ev.type = UHID_CREATE; + strncpy((char*)ev.u.create.name, name, UHID_MAX_NAME_LENGTH); + ev.u.create.rd_data = descriptor.get(); + ev.u.create.rd_size = descriptorSize; + ev.u.create.bus = BUS_BLUETOOTH; + ev.u.create.vendor = vid; + ev.u.create.product = pid; + ev.u.create.version = 0; + ev.u.create.country = 0; + + errno = 0; + ssize_t ret = TEMP_FAILURE_RETRY(::write(fd, &ev, sizeof(ev))); + if (ret < 0 || ret != sizeof(ev)) { + ::close(fd); + ALOGE("Failed to create uhid node: %s", strerror(errno)); + return nullptr; + } + + // Wait for the device to actually be created. + ret = TEMP_FAILURE_RETRY(::read(fd, &ev, sizeof(ev))); + if (ret < 0 || ev.type != UHID_START) { + ::close(fd); + ALOGE("uhid node failed to start: %s", strerror(errno)); + return nullptr; + } + + return new Device(id, fd, std::move(callback), looper); +} + +Device::Device(int32_t id, int fd, std::unique_ptr<DeviceCallback> callback, sp<Looper> looper) : + mId(id), mFd(fd), mDeviceCallback(std::move(callback)), mLooper(looper) { + looper->addFd(fd, 0, Looper::EVENT_INPUT, handleLooperEvents, reinterpret_cast<void*>(this)); +} + +Device::~Device() { + mLooper->removeFd(mFd); + struct uhid_event ev; + memset(&ev, 0, sizeof(ev)); + ev.type = UHID_DESTROY; + TEMP_FAILURE_RETRY(::write(mFd, &ev, sizeof(ev))); + ::close(mFd); + mFd = -1; +} + +void Device::sendReport(uint8_t* report, size_t reportSize) { + struct uhid_event ev; + memset(&ev, 0, sizeof(ev)); + ev.type = UHID_INPUT; + ev.u.input.size = reportSize; + memcpy(&ev.u.input.data, report, reportSize); + ssize_t ret = TEMP_FAILURE_RETRY(::write(mFd, &ev, sizeof(ev))); + if (ret < 0 || ret != sizeof(ev)) { + ALOGE("Failed to send hid event: %s", strerror(errno)); + } +} + +int Device::handleEvents(int events) { + if (events & (Looper::EVENT_ERROR | Looper::EVENT_HANGUP)) { + ALOGE("uhid node was closed or an error occurred. events=0x%x", events); + mDeviceCallback->onDeviceError(); + return 0; + } + struct uhid_event ev; + ssize_t ret = TEMP_FAILURE_RETRY(::read(mFd, &ev, sizeof(ev))); + if (ret < 0) { + ALOGE("Failed to read from uhid node: %s", strerror(errno)); + mDeviceCallback->onDeviceError(); + return 0; + } + + if (ev.type == UHID_OPEN) { + mDeviceCallback->onDeviceOpen(); + } + + return 1; +} + +} // namespace uhid + +std::unique_ptr<uint8_t[]> getData(JNIEnv* env, jbyteArray javaArray, size_t& outSize) { + ScopedByteArrayRO scopedArray(env, javaArray); + outSize = scopedArray.size(); + std::unique_ptr<uint8_t[]> data(new uint8_t[outSize]); + for (size_t i = 0; i < outSize; i++) { + data[i] = static_cast<uint8_t>(scopedArray[i]); + } + return data; +} + +static jlong openDevice(JNIEnv* env, jclass clazz, jstring rawName, jint id, jint vid, jint pid, + jbyteArray rawDescriptor, jobject queue, jobject callback) { + ScopedUtfChars name(env, rawName); + if (name.c_str() == nullptr) { + return 0; + } + + size_t size; + std::unique_ptr<uint8_t[]> desc = getData(env, rawDescriptor, size); + + std::unique_ptr<uhid::DeviceCallback> cb(new uhid::DeviceCallback(env, callback)); + sp<Looper> looper = android_os_MessageQueue_getMessageQueue(env, queue)->getLooper(); + + uhid::Device* d = uhid::Device::open( + id, reinterpret_cast<const char*>(name.c_str()), vid, pid, + std::move(desc), size, std::move(cb), std::move(looper)); + return reinterpret_cast<jlong>(d); +} + +static void sendReport(JNIEnv* env, jclass clazz, jlong ptr,jbyteArray rawReport) { + size_t size; + std::unique_ptr<uint8_t[]> report = getData(env, rawReport, size); + uhid::Device* d = reinterpret_cast<uhid::Device*>(ptr); + if (d) { + d->sendReport(report.get(), size); + } +} + +static void closeDevice(JNIEnv* env, jclass clazz, jlong ptr) { + uhid::Device* d = reinterpret_cast<uhid::Device*>(ptr); + if (d) { + delete d; + } +} + +static JNINativeMethod sMethods[] = { + { "nativeOpenDevice", + "(Ljava/lang/String;III[BLandroid/os/MessageQueue;" + "Lcom/android/commands/hid/Device$DeviceCallback;)J", + reinterpret_cast<void*>(openDevice) }, + { "nativeSendReport", "(J[B)V", reinterpret_cast<void*>(sendReport) }, + { "nativeCloseDevice", "(J)V", reinterpret_cast<void*>(closeDevice) }, +}; + +int register_com_android_commands_hid_Device(JNIEnv* env) { + jclass clazz = FindClassOrDie(env, "com/android/commands/hid/Device$DeviceCallback"); + uhid::gDeviceCallbackClassInfo.onDeviceOpen = + GetMethodIDOrDie(env, clazz, "onDeviceOpen", "()V"); + uhid::gDeviceCallbackClassInfo.onDeviceError= + GetMethodIDOrDie(env, clazz, "onDeviceError", "()V"); + return jniRegisterNativeMethods(env, "com/android/commands/hid/Device", + sMethods, NELEM(sMethods)); +} + +} // namespace android + +jint JNI_OnLoad(JavaVM* jvm, void*) { + JNIEnv *env = NULL; + if (jvm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6)) { + return JNI_ERR; + } + + if (android::register_com_android_commands_hid_Device(env) < 0 ){ + return JNI_ERR; + } + + return JNI_VERSION_1_6; +} diff --git a/cmds/hid/jni/com_android_commands_hid_Device.h b/cmds/hid/jni/com_android_commands_hid_Device.h new file mode 100644 index 0000000..6c5899e --- /dev/null +++ b/cmds/hid/jni/com_android_commands_hid_Device.h @@ -0,0 +1,61 @@ +/* + * Copyright (C) 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. + */ + +#include <memory> + +#include <jni.h> +#include <utils/Looper.h> +#include <utils/StrongPointer.h> + +namespace android { +namespace uhid { + +class DeviceCallback { +public: + DeviceCallback(JNIEnv* env, jobject callback); + ~DeviceCallback(); + + void onDeviceOpen(); + void onDeviceError(); + +private: + jobject mCallbackObject; +}; + +class Device { +public: + static Device* open(int32_t id, const char* name, int32_t vid, int32_t pid, + std::unique_ptr<uint8_t[]> descriptor, size_t descriptorSize, + std::unique_ptr<DeviceCallback> callback, sp<Looper> looper); + + Device(int32_t id, int fd, std::unique_ptr<DeviceCallback> callback, sp<Looper> looper); + ~Device(); + + void sendReport(uint8_t* report, size_t reportSize); + void close(); + + int handleEvents(int events); + +private: + int32_t mId; + int mFd; + std::unique_ptr<DeviceCallback> mDeviceCallback; + sp<Looper> mLooper; +}; + + +} // namespace uhid +} // namespace android diff --git a/cmds/hid/src/com/android/commands/hid/Device.java b/cmds/hid/src/com/android/commands/hid/Device.java new file mode 100644 index 0000000..dbe883b --- /dev/null +++ b/cmds/hid/src/com/android/commands/hid/Device.java @@ -0,0 +1,163 @@ +/* + * Copyright (C) 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 com.android.commands.hid; + +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Looper; +import android.os.Message; +import android.os.MessageQueue; +import android.os.SystemClock; +import android.util.Log; + +import com.android.internal.os.SomeArgs; + +public class Device { + private static final String TAG = "HidDevice"; + + // Minimum amount of time to wait before sending input events to a device. Even though we're + // guaranteed that the device has been created and opened by the input system, there's still a + // window in which the system hasn't started reading events out of it. If a stream of events + // begins in during this window (like a button down event) and *then* we start reading, we're + // liable to ignore the whole stream. + private static final int MIN_WAIT_FOR_FIRST_EVENT = 150; + + private static final int MSG_OPEN_DEVICE = 1; + private static final int MSG_SEND_REPORT = 2; + private static final int MSG_CLOSE_DEVICE = 3; + + + private final int mId; + private final HandlerThread mThread; + private final DeviceHandler mHandler; + private long mEventTime; + + private final Object mCond = new Object(); + + static { + System.loadLibrary("hidcommand_jni"); + } + + private static native long nativeOpenDevice(String name, int id, int vid, int pid, + byte[] descriptor, MessageQueue queue, DeviceCallback callback); + private static native void nativeSendReport(long ptr, byte[] data); + private static native void nativeCloseDevice(long ptr); + + public Device(int id, String name, int vid, int pid, byte[] descriptor, byte[] report) { + mId = id; + mThread = new HandlerThread("HidDeviceHandler"); + mThread.start(); + mHandler = new DeviceHandler(mThread.getLooper()); + SomeArgs args = SomeArgs.obtain(); + args.argi1 = id; + args.argi2 = vid; + args.argi3 = pid; + if (name != null) { + args.arg1 = name; + } else { + args.arg1 = id + ":" + vid + ":" + pid; + } + args.arg2 = descriptor; + args.arg3 = report; + mHandler.obtainMessage(MSG_OPEN_DEVICE, args).sendToTarget(); + mEventTime = SystemClock.uptimeMillis() + MIN_WAIT_FOR_FIRST_EVENT; + } + + public void sendReport(byte[] report) { + Message msg = mHandler.obtainMessage(MSG_SEND_REPORT, report); + mHandler.sendMessageAtTime(msg, mEventTime); + } + + public void addDelay(int delay) { + mEventTime += delay; + } + + public void close() { + Message msg = mHandler.obtainMessage(MSG_CLOSE_DEVICE); + msg.setAsynchronous(true); + mHandler.sendMessageAtTime(msg, mEventTime + 1); + try { + synchronized (mCond) { + mCond.wait(); + } + } catch (InterruptedException ignore) {} + } + + private class DeviceHandler extends Handler { + private long mPtr; + private int mBarrierToken; + + public DeviceHandler(Looper looper) { + super(looper); + } + + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MSG_OPEN_DEVICE: + SomeArgs args = (SomeArgs) msg.obj; + mPtr = nativeOpenDevice((String) args.arg1, args.argi1, args.argi2, args.argi3, + (byte[]) args.arg2, getLooper().myQueue(), new DeviceCallback()); + nativeSendReport(mPtr, (byte[]) args.arg3); + pauseEvents(); + break; + case MSG_SEND_REPORT: + if (mPtr != 0) { + nativeSendReport(mPtr, (byte[]) msg.obj); + } else { + Log.e(TAG, "Tried to send report to closed device."); + } + break; + case MSG_CLOSE_DEVICE: + if (mPtr != 0) { + nativeCloseDevice(mPtr); + getLooper().quitSafely(); + mPtr = 0; + } else { + Log.e(TAG, "Tried to close already closed device."); + } + 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 onDeviceError() { + Message msg = mHandler.obtainMessage(MSG_CLOSE_DEVICE); + msg.setAsynchronous(true); + msg.sendToTarget(); + } + } +} diff --git a/cmds/hid/src/com/android/commands/hid/Event.java b/cmds/hid/src/com/android/commands/hid/Event.java new file mode 100644 index 0000000..c6a37bd --- /dev/null +++ b/cmds/hid/src/com/android/commands/hid/Event.java @@ -0,0 +1,255 @@ +/* + * Copyright (C) 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 com.android.commands.hid; + +import android.util.JsonReader; +import android.util.JsonToken; +import android.util.Log; + +import java.io.InputStreamReader; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; + +public class Event { + private static final String TAG = "HidEvent"; + + public static final String COMMAND_REGISTER = "register"; + public static final String COMMAND_DELAY = "delay"; + public static final String COMMAND_REPORT = "report"; + + private int mId; + private String mCommand; + private String mName; + private byte[] mDescriptor; + private int mVid; + private int mPid; + private byte[] mReport; + private int mDuration; + + public int getId() { + return mId; + } + + public String getCommand() { + return mCommand; + } + + public String getName() { + return mName; + } + + public byte[] getDescriptor() { + return mDescriptor; + } + + public int getVendorId() { + return mVid; + } + + public int getProductId() { + return mPid; + } + + public byte[] getReport() { + return mReport; + } + + public int getDuration() { + return mDuration; + } + + public String toString() { + return "Event{id=" + mId + + ", command=" + String.valueOf(mCommand) + + ", name=" + String.valueOf(mName) + + ", descriptor=" + Arrays.toString(mDescriptor) + + ", vid=" + mVid + + ", pid=" + mPid + + ", report=" + Arrays.toString(mReport) + + ", duration=" + mDuration + + "}"; + } + + private static class Builder { + private Event mEvent; + + public 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 setDescriptor(byte[] descriptor) { + mEvent.mDescriptor = descriptor; + } + + public void setReport(byte[] report) { + mEvent.mReport = report; + } + + public void setVid(int vid) { + mEvent.mVid = vid; + } + + public void setPid(int pid) { + mEvent.mPid = pid; + } + + public void setDuration(int duration) { + mEvent.mDuration = duration; + } + + 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.mDescriptor == null) { + throw new IllegalStateException("Device registration is missing descriptor"); + } + } else if (COMMAND_DELAY.equals(mEvent.mCommand)) { + if (mEvent.mDuration <= 0) { + throw new IllegalStateException("Delay has missing or invalid duration"); + } + } else if (COMMAND_REPORT.equals(mEvent.mCommand)) { + if (mEvent.mReport == null) { + throw new IllegalStateException("Report command is missing report data"); + } + } + return mEvent; + } + } + + public static class Reader { + private JsonReader mReader; + + public Reader(InputStreamReader in) { + mReader = new JsonReader(in); + mReader.setLenient(true); + } + + 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 "descriptor": + eb.setDescriptor(readData()); + break; + case "name": + eb.setName(mReader.nextString()); + break; + case "vid": + eb.setVid(readInt()); + break; + case "pid": + eb.setPid(readInt()); + break; + case "report": + eb.setReport(readData()); + 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 byte[] readData() 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); + } + 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 void consumeRemainingElements() throws IOException { + while (mReader.hasNext()) { + mReader.skipValue(); + } + } + } + + private static void error(String msg) { + error(msg, null); + } + + 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/hid/src/com/android/commands/hid/Hid.java b/cmds/hid/src/com/android/commands/hid/Hid.java new file mode 100644 index 0000000..976a782 --- /dev/null +++ b/cmds/hid/src/com/android/commands/hid/Hid.java @@ -0,0 +1,133 @@ +/* + * Copyright (C) 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 com.android.commands.hid; + +import android.os.SystemClock; +import android.util.JsonReader; +import android.util.JsonToken; +import android.util.Log; +import android.util.SparseArray; + +import libcore.io.IoUtils; + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.util.ArrayList; + +public class Hid { + private static final String TAG = "HID"; + + private final Event.Reader mReader; + private final SparseArray<Device> mDevices; + + private static void usage() { + error("Usage: hid [FILE]"); + } + + 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 Hid(stream)).run(); + } catch (Exception e) { + error("HID injection failed.", e); + System.exit(1); + } finally { + IoUtils.closeQuietly(stream); + } + } + + private Hid(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_REPORT.equals(e.getCommand())) { + d.sendReport(e.getReport()); + } else { + error("Unknown command \"" + e.getCommand() + "\". Ignoring event."); + } + } else { + registerDevice(e); + } + } + + 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.getDescriptor(), e.getReport()); + mDevices.append(id, d); + } + + private static void error(String msg) { + error(msg, null); + } + + 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)); + } + } +} |