diff options
author | Siarhei Vishniakou <svv@google.com> | 2017-03-31 17:23:39 -0700 |
---|---|---|
committer | Siarhei Vishniakou <svv@google.com> | 2017-05-12 21:01:02 +0000 |
commit | 55656e4cb923d1aeef4445b3aaa3a6ce3f35c90a (patch) | |
tree | 323e1b593b44221d81b24b43c7fbe237a6710396 | |
parent | 97208056febe682001bef70988105dcdb5d52316 (diff) |
Fix GamepadTestCase#testButtonA CTS test
Hid command, JNI layer:
- Removed dependency of the hid device on libandroid_runtime
and libutils. Using ALooper from libandroid to process callbacks from
/dev/uhid file descriptor.
- Switched to using "CREATE2" and "INPUT2" constructs in uhid driver
Hid command, Java layer:
- Removed delay workarounds, user now responsible for waiting for
onInputDeviceChanged notification prior to using the hid commands.
UiAutomation:
- Added a new executeShellCommandRw function that allows bidirectional
communication to shell command
platform.xml:
- Added uhid permissions to bluetooth stack for /dev/uhid access
- CTS test now consistently passes
Bug: 34052337
Test: CTS test case invoked with the following command:
run cts -t android.hardware.input.cts.tests.GamepadTestCase
-m CtsHardwareTestCases --skip-system-status-check
com.android.compatibility.common.tradefed.targetprep.NetworkConnectivityChecker
Change-Id: Ic916c513b7e652b1e25675f0f38f0f1f3a65d214
-rw-r--r-- | api/test-current.txt | 1 | ||||
-rw-r--r-- | cmds/hid/README.md | 145 | ||||
-rw-r--r-- | cmds/hid/jni/Android.mk | 9 | ||||
-rw-r--r-- | cmds/hid/jni/com_android_commands_hid_Device.cpp | 125 | ||||
-rw-r--r-- | cmds/hid/jni/com_android_commands_hid_Device.h | 9 | ||||
-rw-r--r-- | cmds/hid/src/com/android/commands/hid/Device.java | 26 | ||||
-rw-r--r-- | cmds/hid/src/com/android/commands/hid/Hid.java | 13 | ||||
-rw-r--r-- | core/java/android/app/IUiAutomationConnection.aidl | 3 | ||||
-rw-r--r-- | core/java/android/app/UiAutomation.java | 59 | ||||
-rw-r--r-- | core/java/android/app/UiAutomationConnection.java | 110 |
10 files changed, 385 insertions, 115 deletions
diff --git a/api/test-current.txt b/api/test-current.txt index 84fe4c5c4f77..586b22bf4c6e 100644 --- a/api/test-current.txt +++ b/api/test-current.txt @@ -5989,6 +5989,7 @@ package android.app { method public void destroy(); method public android.view.accessibility.AccessibilityEvent executeAndWaitForEvent(java.lang.Runnable, android.app.UiAutomation.AccessibilityEventFilter, long) throws java.util.concurrent.TimeoutException; method public android.os.ParcelFileDescriptor executeShellCommand(java.lang.String); + method public android.os.ParcelFileDescriptor[] executeShellCommandRw(java.lang.String); method public android.view.accessibility.AccessibilityNodeInfo findFocus(int); method public android.view.accessibility.AccessibilityNodeInfo getRootInActiveWindow(); method public final android.accessibilityservice.AccessibilityServiceInfo getServiceInfo(); diff --git a/cmds/hid/README.md b/cmds/hid/README.md new file mode 100644 index 000000000000..7e22d08eeaeb --- /dev/null +++ b/cmds/hid/README.md @@ -0,0 +1,145 @@ +# Usage +## Two options to use the hid command: +### 1. Interactive through stdin: +type `hid -` 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 HID events. +For an example, see the cts test case at: [InputTestCase.java][2] + +When using another program to control hid 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 behaviour. +In the current implementation of the hid command, the hid binary will wait +for the file descriptor to the uhid node to send the UHID_START and UHID_OPEN +signals before returning. However, this is not sufficient. These signals +only notify the readiness of the kernel driver, +but do not take into account the inputflinger framework. + + +### 2. Using a file as an input: +type `hid <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 hid 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 uhid 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 | +| descriptor | byte array | USB HID report descriptor | + +Device ID is used for matching the subsequent commands to a specific device +to avoid ambiguity when multiple devices are registered. + +USB HID report descriptor should be generated according the the USB HID spec +and can be checked by reverse parsing using a variety of tools, for example +[usbdescreqparser][5]. + +Example: +```json +{ + "id": 1, + "command": "register", + "name": "Odie (Test)", + "vid": 0x18d1, + "pid": 0x2c40, + "descriptor": [0x05, 0x01, 0x09, 0x05, 0xa1, 0x01, 0x85, 0x01, 0x05, 0x09, 0x0a, 0x01, 0x00, + 0x0a, 0x02, 0x00, 0x0a, 0x04, 0x00, 0x0a, 0x05, 0x00, 0x0a, 0x07, 0x00, 0x0a, 0x08, 0x00, + 0x0a, 0x0e, 0x00, 0x0a, 0x0f, 0x00, 0x0a, 0x0d, 0x00, 0x05, 0x0c, 0x0a, 0x24, 0x02, 0x0a, + 0x23, 0x02, 0x15, 0x00, 0x25, 0x01, 0x75, 0x01, 0x95, 0x0b, 0x81, 0x02, 0x75, 0x01, 0x95, + 0x01, 0x81, 0x03, 0x05, 0x01, 0x75, 0x04, 0x95, 0x01, 0x25, 0x07, 0x46, 0x3b, 0x01, 0x66, + 0x14, 0x00, 0x09, 0x39, 0x81, 0x42, 0x66, 0x00, 0x00, 0x09, 0x01, 0xa1, 0x00, 0x09, 0x30, + 0x09, 0x31, 0x09, 0x32, 0x09, 0x35, 0x05, 0x02, 0x09, 0xc5, 0x09, 0xc4, 0x15, 0x00, 0x26, + 0xff, 0x00, 0x35, 0x00, 0x46, 0xff, 0x00, 0x75, 0x08, 0x95, 0x06, 0x81, 0x02, 0xc0, 0x85, + 0x02, 0x05, 0x08, 0x0a, 0x01, 0x00, 0x0a, 0x02, 0x00, 0x0a, 0x03, 0x00, 0x0a, 0x04, 0x00, + 0x15, 0x00, 0x25, 0x01, 0x75, 0x01, 0x95, 0x04, 0x91, 0x02, 0x75, 0x04, 0x95, 0x01, 0x91, + 0x03, 0xc0, 0x05, 0x0c, 0x09, 0x01, 0xa1, 0x01, 0x85, 0x03, 0x05, 0x01, 0x09, 0x06, 0xa1, + 0x02, 0x05, 0x06, 0x09, 0x20, 0x15, 0x00, 0x26, 0xff, 0x00, 0x75, 0x08, 0x95, 0x01, 0x81, + 0x02, 0x06, 0xbc, 0xff, 0x0a, 0xad, 0xbd, 0x75, 0x08, 0x95, 0x06, 0x81, 0x02, 0xc0, 0xc0] +} +``` +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. `report` +Send a report to the HID device + +| Field | Type | Description | +|:-------------:|:-------------:|:-------------------------- | +| id | integer | Device id | +| command | string | Must be set to "report" | +| report | byte array | Report data to send | + +Example: +```json +{ + "id": 1, + "command": "report", + "report": [0x01, 0x01, 0x80, 0x7f, 0x7f, 0x7f, 0x7f, 0x00, 0x00] +} +``` + +### Sending a joystick button press event +To send a button press event on a joystick device: +1. Register the joystick device +2. Send button down event with coordinates ABS_X, ABS_Y, ABS_Z, and ABS_RZ +at the center of the range. If the coordinates are not centered, this event +will generate a motion event within the input framework, in addition to the +button press event. The range can be determined from the uhid report descriptor. +3. Send the button up event with the same coordinates as in 2. +4. Check that the button press event was received. + +### 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 linux input subsystem does not generate events for those values +that remain unchanged. For example, if there are two events sent to the driver, +and both events have the same value of ABS_X, then ABS_X coordinate +will not be reported. +3. The description of joystick actions is available [here][6]. +4. Joysticks are split axes. When an analog stick is in a resting state, +the reported coordinates are at the center of the range. +5. 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]: https://developer.android.com/training/game-controllers/controller-input.html#button +[5]: http://eleccelerator.com/usbdescreqparser/ +[6]: https://developer.android.com/training/game-controllers/controller-input.html
\ No newline at end of file diff --git a/cmds/hid/jni/Android.mk b/cmds/hid/jni/Android.mk index d41d39d27f5b..86f4e012a943 100644 --- a/cmds/hid/jni/Android.mk +++ b/cmds/hid/jni/Android.mk @@ -6,14 +6,9 @@ LOCAL_SRC_FILES := \ com_android_commands_hid_Device.cpp LOCAL_C_INCLUDES := \ - $(JNI_H_INCLUDE) \ - frameworks/base/core/jni + $(JNI_H_INCLUDE) -LOCAL_SHARED_LIBRARIES := \ - libandroid_runtime \ - liblog \ - libnativehelper \ - libutils +LOCAL_LDLIBS += -landroid -llog -lnativehelper LOCAL_MODULE := libhidcommand_jni LOCAL_MODULE_TAGS := optional diff --git a/cmds/hid/jni/com_android_commands_hid_Device.cpp b/cmds/hid/jni/com_android_commands_hid_Device.cpp index 1ea33ced7bbf..107dc863ef66 100644 --- a/cmds/hid/jni/com_android_commands_hid_Device.cpp +++ b/cmds/hid/jni/com_android_commands_hid_Device.cpp @@ -26,17 +26,17 @@ #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> +#include <android/looper.h> +#include <android/log.h> + +#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,LOG_TAG,__VA_ARGS__) +#define LOGW(...) __android_log_print(ANDROID_LOG_WARN,LOG_TAG,__VA_ARGS__) +#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG,LOG_TAG,__VA_ARGS__) +#define LOGI(...) __android_log_print(ANDROID_LOG_INFO,LOG_TAG,__VA_ARGS__) namespace android { namespace uhid { @@ -56,59 +56,67 @@ static int handleLooperEvents(int /* fd */, int events, void* data) { static void checkAndClearException(JNIEnv* env, const char* methodName) { if (env->ExceptionCheck()) { - ALOGE("An exception was thrown by callback '%s'.", methodName); - LOGE_EX(env); + LOGE("An exception was thrown by callback '%s'.", methodName); env->ExceptionClear(); } } DeviceCallback::DeviceCallback(JNIEnv* env, jobject callback) : - mCallbackObject(env->NewGlobalRef(callback)) { } + mCallbackObject(env->NewGlobalRef(callback)) { + env->GetJavaVM(&mJavaVM); + } DeviceCallback::~DeviceCallback() { - JNIEnv* env = AndroidRuntime::getJNIEnv(); + JNIEnv* env = getJNIEnv(); env->DeleteGlobalRef(mCallbackObject); } void DeviceCallback::onDeviceError() { - JNIEnv* env = AndroidRuntime::getJNIEnv(); + JNIEnv* env = getJNIEnv(); env->CallVoidMethod(mCallbackObject, gDeviceCallbackClassInfo.onDeviceError); checkAndClearException(env, "onDeviceError"); } void DeviceCallback::onDeviceOpen() { - JNIEnv* env = AndroidRuntime::getJNIEnv(); + JNIEnv* env = getJNIEnv(); env->CallVoidMethod(mCallbackObject, gDeviceCallbackClassInfo.onDeviceOpen); checkAndClearException(env, "onDeviceOpen"); } +JNIEnv* DeviceCallback::getJNIEnv() { + JNIEnv* env; + mJavaVM->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6); + return env; +} + 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) { + std::unique_ptr<DeviceCallback> callback) { int fd = ::open(UHID_PATH, O_RDWR | O_CLOEXEC); if (fd < 0) { - ALOGE("Failed to open uhid: %s", strerror(errno)); + LOGE("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; + ev.type = UHID_CREATE2; + strncpy((char*)ev.u.create2.name, name, UHID_MAX_NAME_LENGTH); + memcpy(&ev.u.create2.rd_data, descriptor.get(), + descriptorSize * sizeof(ev.u.create2.rd_data[0])); + ev.u.create2.rd_size = descriptorSize; + ev.u.create2.bus = BUS_BLUETOOTH; + ev.u.create2.vendor = vid; + ev.u.create2.product = pid; + ev.u.create2.version = 0; + ev.u.create2.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)); + LOGE("Failed to create uhid node: %s", strerror(errno)); return nullptr; } @@ -116,20 +124,30 @@ Device* Device::open(int32_t id, const char* name, int32_t vid, int32_t pid, 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)); + LOGE("uhid node failed to start: %s", strerror(errno)); return nullptr; } - - return new Device(id, fd, std::move(callback), looper); + return new Device(id, fd, std::move(callback)); } -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(int32_t id, int fd, std::unique_ptr<DeviceCallback> callback) : + mId(id), mFd(fd), mDeviceCallback(std::move(callback)) { + ALooper* aLooper = ALooper_forThread(); + if (aLooper == NULL) { + LOGE("Could not get ALooper, ALooper_forThread returned NULL"); + aLooper = ALooper_prepare(ALOOPER_PREPARE_ALLOW_NON_CALLBACKS); + } + ALooper_addFd(aLooper, fd, 0, ALOOPER_EVENT_INPUT, handleLooperEvents, + reinterpret_cast<void*>(this)); } Device::~Device() { - mLooper->removeFd(mFd); + ALooper* looper = ALooper_forThread(); + if (looper != NULL) { + ALooper_removeFd(looper, mFd); + } else { + LOGE("Could not remove fd, ALooper_forThread() returned NULL!"); + } struct uhid_event ev; memset(&ev, 0, sizeof(ev)); ev.type = UHID_DESTROY; @@ -141,25 +159,25 @@ Device::~Device() { 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); + ev.type = UHID_INPUT2; + ev.u.input2.size = reportSize; + memcpy(&ev.u.input2.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)); + LOGE("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); + if (events & (ALOOPER_EVENT_ERROR | ALOOPER_EVENT_HANGUP)) { + LOGE("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)); + LOGE("Failed to read from uhid node: %s", strerror(errno)); mDeviceCallback->onDeviceError(); return 0; } @@ -184,7 +202,7 @@ std::unique_ptr<uint8_t[]> getData(JNIEnv* env, jbyteArray javaArray, size_t& ou } static jlong openDevice(JNIEnv* env, jclass /* clazz */, jstring rawName, jint id, jint vid, jint pid, - jbyteArray rawDescriptor, jobject queue, jobject callback) { + jbyteArray rawDescriptor, jobject callback) { ScopedUtfChars name(env, rawName); if (name.c_str() == nullptr) { return 0; @@ -194,20 +212,21 @@ static jlong openDevice(JNIEnv* env, jclass /* clazz */, jstring rawName, jint i 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)); + std::move(desc), size, std::move(cb)); return reinterpret_cast<jlong>(d); } -static void sendReport(JNIEnv* env, jclass /* clazz */, jlong ptr,jbyteArray rawReport) { +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); + } else { + LOGE("Could not send report, Device* is null!"); } } @@ -220,7 +239,7 @@ static void closeDevice(JNIEnv* /* env */, jclass /* clazz */, jlong ptr) { static JNINativeMethod sMethods[] = { { "nativeOpenDevice", - "(Ljava/lang/String;III[BLandroid/os/MessageQueue;" + "(Ljava/lang/String;III[B" "Lcom/android/commands/hid/Device$DeviceCallback;)J", reinterpret_cast<void*>(openDevice) }, { "nativeSendReport", "(J[B)V", reinterpret_cast<void*>(sendReport) }, @@ -228,11 +247,21 @@ static JNINativeMethod sMethods[] = { }; int register_com_android_commands_hid_Device(JNIEnv* env) { - jclass clazz = FindClassOrDie(env, "com/android/commands/hid/Device$DeviceCallback"); + jclass clazz = env->FindClass("com/android/commands/hid/Device$DeviceCallback"); + if (clazz == NULL) { + LOGE("Unable to find class 'DeviceCallback'"); + return JNI_ERR; + } uhid::gDeviceCallbackClassInfo.onDeviceOpen = - GetMethodIDOrDie(env, clazz, "onDeviceOpen", "()V"); - uhid::gDeviceCallbackClassInfo.onDeviceError= - GetMethodIDOrDie(env, clazz, "onDeviceError", "()V"); + env->GetMethodID(clazz, "onDeviceOpen", "()V"); + uhid::gDeviceCallbackClassInfo.onDeviceError = + env->GetMethodID(clazz, "onDeviceError", "()V"); + if (uhid::gDeviceCallbackClassInfo.onDeviceOpen == NULL || + uhid::gDeviceCallbackClassInfo.onDeviceError == NULL) { + LOGE("Unable to obtain onDeviceOpen or onDeviceError methods"); + return JNI_ERR; + } + return jniRegisterNativeMethods(env, "com/android/commands/hid/Device", sMethods, NELEM(sMethods)); } diff --git a/cmds/hid/jni/com_android_commands_hid_Device.h b/cmds/hid/jni/com_android_commands_hid_Device.h index 6c5899eeb861..149456d8c10d 100644 --- a/cmds/hid/jni/com_android_commands_hid_Device.h +++ b/cmds/hid/jni/com_android_commands_hid_Device.h @@ -17,8 +17,6 @@ #include <memory> #include <jni.h> -#include <utils/Looper.h> -#include <utils/StrongPointer.h> namespace android { namespace uhid { @@ -32,16 +30,18 @@ public: void onDeviceError(); private: + JNIEnv* getJNIEnv(); jobject mCallbackObject; + JavaVM* mJavaVM; }; 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); + std::unique_ptr<DeviceCallback> callback); - Device(int32_t id, int fd, std::unique_ptr<DeviceCallback> callback, sp<Looper> looper); + Device(int32_t id, int fd, std::unique_ptr<DeviceCallback> callback); ~Device(); void sendReport(uint8_t* report, size_t reportSize); @@ -53,7 +53,6 @@ private: int32_t mId; int mFd; std::unique_ptr<DeviceCallback> mDeviceCallback; - sp<Looper> mLooper; }; diff --git a/cmds/hid/src/com/android/commands/hid/Device.java b/cmds/hid/src/com/android/commands/hid/Device.java index dbe883bd1136..8c52a8ed1e09 100644 --- a/cmds/hid/src/com/android/commands/hid/Device.java +++ b/cmds/hid/src/com/android/commands/hid/Device.java @@ -29,22 +29,14 @@ 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 long mTimeToSend; private final Object mCond = new Object(); @@ -53,7 +45,7 @@ public class Device { } private static native long nativeOpenDevice(String name, int id, int vid, int pid, - byte[] descriptor, MessageQueue queue, DeviceCallback callback); + byte[] descriptor, DeviceCallback callback); private static native void nativeSendReport(long ptr, byte[] data); private static native void nativeCloseDevice(long ptr); @@ -74,22 +66,22 @@ public class Device { args.arg2 = descriptor; args.arg3 = report; mHandler.obtainMessage(MSG_OPEN_DEVICE, args).sendToTarget(); - mEventTime = SystemClock.uptimeMillis() + MIN_WAIT_FOR_FIRST_EVENT; + mTimeToSend = SystemClock.uptimeMillis(); } public void sendReport(byte[] report) { Message msg = mHandler.obtainMessage(MSG_SEND_REPORT, report); - mHandler.sendMessageAtTime(msg, mEventTime); + // if two messages are sent at identical time, they will be processed in order received + mHandler.sendMessageAtTime(msg, mTimeToSend); } public void addDelay(int delay) { - mEventTime += delay; + mTimeToSend = Math.max(SystemClock.uptimeMillis(), mTimeToSend) + delay; } public void close() { Message msg = mHandler.obtainMessage(MSG_CLOSE_DEVICE); - msg.setAsynchronous(true); - mHandler.sendMessageAtTime(msg, mEventTime + 1); + mHandler.sendMessageAtTime(msg, Math.max(SystemClock.uptimeMillis(), mTimeToSend) + 1); try { synchronized (mCond) { mCond.wait(); @@ -111,8 +103,7 @@ public class Device { 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); + (byte[]) args.arg2, new DeviceCallback()); pauseEvents(); break; case MSG_SEND_REPORT: @@ -155,6 +146,7 @@ public class Device { } public void onDeviceError() { + Log.e(TAG, "Device error occurred, closing /dev/uhid"); Message msg = mHandler.obtainMessage(MSG_CLOSE_DEVICE); msg.setAsynchronous(true); msg.sendToTarget(); diff --git a/cmds/hid/src/com/android/commands/hid/Hid.java b/cmds/hid/src/com/android/commands/hid/Hid.java index 976a78249bec..234e47f12dee 100644 --- a/cmds/hid/src/com/android/commands/hid/Hid.java +++ b/cmds/hid/src/com/android/commands/hid/Hid.java @@ -16,7 +16,6 @@ package com.android.commands.hid; -import android.os.SystemClock; import android.util.JsonReader; import android.util.JsonToken; import android.util.Log; @@ -91,7 +90,6 @@ public class Hid { } } - private void process(Event e) { final int index = mDevices.indexOfKey(e.getId()); if (index >= 0) { @@ -101,10 +99,16 @@ public class Hid { } else if (Event.COMMAND_REPORT.equals(e.getCommand())) { d.sendReport(e.getReport()); } else { - error("Unknown command \"" + e.getCommand() + "\". Ignoring event."); + 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 { + } else if (Event.COMMAND_REGISTER.equals(e.getCommand())) { registerDevice(e); + } else { + Log.e(TAG, "Unknown device id specified. Ignoring event."); } } @@ -124,7 +128,6 @@ public class Hid { } 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/core/java/android/app/IUiAutomationConnection.aidl b/core/java/android/app/IUiAutomationConnection.aidl index 7640e75ba153..b26117d3d31c 100644 --- a/core/java/android/app/IUiAutomationConnection.aidl +++ b/core/java/android/app/IUiAutomationConnection.aidl @@ -42,7 +42,8 @@ interface IUiAutomationConnection { WindowContentFrameStats getWindowContentFrameStats(int windowId); void clearWindowAnimationFrameStats(); WindowAnimationFrameStats getWindowAnimationFrameStats(); - void executeShellCommand(String command, in ParcelFileDescriptor fd); + void executeShellCommand(String command, in ParcelFileDescriptor sink, + in ParcelFileDescriptor source); void grantRuntimePermission(String packageName, String permission, int userId); void revokeRuntimePermission(String packageName, String permission, int userId); diff --git a/core/java/android/app/UiAutomation.java b/core/java/android/app/UiAutomation.java index 18e7599e1846..c99de5ddb776 100644 --- a/core/java/android/app/UiAutomation.java +++ b/core/java/android/app/UiAutomation.java @@ -975,11 +975,11 @@ public final class UiAutomation { } /** - * Executes a shell command. This method returs a file descriptor that points + * Executes a shell command. This method returns a file descriptor that points * to the standard output stream. The command execution is similar to running * "adb shell <command>" from a host connected to the device. * <p> - * <strong>Note:</strong> It is your responsibility to close the retunred file + * <strong>Note:</strong> It is your responsibility to close the returned file * descriptor once you are done reading. * </p> * @@ -1000,7 +1000,7 @@ public final class UiAutomation { sink = pipe[1]; // Calling out without a lock held. - mUiAutomationConnection.executeShellCommand(command, sink); + mUiAutomationConnection.executeShellCommand(command, sink, null); } catch (IOException ioe) { Log.e(LOG_TAG, "Error executing shell command!", ioe); } catch (RemoteException re) { @@ -1012,6 +1012,59 @@ public final class UiAutomation { return source; } + /** + * Executes a shell command. This method returns two file descriptors, + * one that points to the standard output stream (element at index 0), and one that points + * to the standard input stream (element at index 1). The command execution is similar + * to running "adb shell <command>" from a host connected to the device. + * <p> + * <strong>Note:</strong> It is your responsibility to close the returned file + * descriptors once you are done reading/writing. + * </p> + * + * @param command The command to execute. + * @return File descriptors (out, in) to the standard output/input streams. + * + * @hide + */ + @TestApi + public ParcelFileDescriptor[] executeShellCommandRw(String command) { + synchronized (mLock) { + throwIfNotConnectedLocked(); + } + + ParcelFileDescriptor source_read = null; + ParcelFileDescriptor sink_read = null; + + ParcelFileDescriptor source_write = null; + ParcelFileDescriptor sink_write = null; + + try { + ParcelFileDescriptor[] pipe_read = ParcelFileDescriptor.createPipe(); + source_read = pipe_read[0]; + sink_read = pipe_read[1]; + + ParcelFileDescriptor[] pipe_write = ParcelFileDescriptor.createPipe(); + source_write = pipe_write[0]; + sink_write = pipe_write[1]; + + // Calling out without a lock held. + mUiAutomationConnection.executeShellCommand(command, sink_read, source_write); + } catch (IOException ioe) { + Log.e(LOG_TAG, "Error executing shell command!", ioe); + } catch (RemoteException re) { + Log.e(LOG_TAG, "Error executing shell command!", re); + } finally { + IoUtils.closeQuietly(sink_read); + IoUtils.closeQuietly(source_write); + } + + ParcelFileDescriptor[] result = new ParcelFileDescriptor[2]; + result[0] = source_read; + result[1] = sink_write; + return result; + } + private static float getDegreesForRotation(int value) { switch (value) { case Surface.ROTATION_90: { diff --git a/core/java/android/app/UiAutomationConnection.java b/core/java/android/app/UiAutomationConnection.java index 9960df63b537..5e414b837f79 100644 --- a/core/java/android/app/UiAutomationConnection.java +++ b/core/java/android/app/UiAutomationConnection.java @@ -36,9 +36,11 @@ import android.view.WindowAnimationFrameStats; import android.view.WindowContentFrameStats; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.IAccessibilityManager; +import android.util.Log; import libcore.io.IoUtils; +import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; @@ -55,6 +57,8 @@ import java.io.OutputStream; */ public final class UiAutomationConnection extends IUiAutomationConnection.Stub { + private static final String TAG = "UiAutomationConnection"; + private static final int INITIAL_FROZEN_ROTATION_UNSPECIFIED = -1; private final IWindowManager mWindowManager = IWindowManager.Stub.asInterface( @@ -267,47 +271,95 @@ public final class UiAutomationConnection extends IUiAutomationConnection.Stub { } } + public class Repeater implements Runnable { + // Continuously read readFrom and write back to writeTo until EOF is encountered + private final InputStream readFrom; + private final OutputStream writeTo; + public Repeater (InputStream readFrom, OutputStream writeTo) { + this.readFrom = readFrom; + this.writeTo = writeTo; + } + @Override + public void run() { + try { + final byte[] buffer = new byte[8192]; + int readByteCount; + while (true) { + readByteCount = readFrom.read(buffer); + if (readByteCount < 0) { + break; + } + writeTo.write(buffer, 0, readByteCount); + writeTo.flush(); + } + } catch (IOException ioe) { + throw new RuntimeException("Error while reading/writing ", ioe); + } finally { + IoUtils.closeQuietly(readFrom); + IoUtils.closeQuietly(writeTo); + } + } + } + @Override - public void executeShellCommand(final String command, final ParcelFileDescriptor sink) - throws RemoteException { + public void executeShellCommand(final String command, final ParcelFileDescriptor sink, + final ParcelFileDescriptor source) throws RemoteException { synchronized (mLock) { throwIfCalledByNotTrustedUidLocked(); throwIfShutdownLocked(); throwIfNotConnectedLocked(); } + final java.lang.Process process; - Thread streamReader = new Thread() { - public void run() { - InputStream in = null; - OutputStream out = null; - java.lang.Process process = null; + try { + process = Runtime.getRuntime().exec(command); + } catch (IOException exc) { + throw new RuntimeException("Error running shell command '" + command + "'", exc); + } + + // Read from process and write to pipe + final Thread readFromProcess; + if (sink != null) { + InputStream sink_in = process.getInputStream();; + OutputStream sink_out = new FileOutputStream(sink.getFileDescriptor()); + + readFromProcess = new Thread(new Repeater(sink_in, sink_out)); + readFromProcess.start(); + } else { + readFromProcess = null; + } + // Read from pipe and write to process + final Thread writeToProcess; + if (source != null) { + OutputStream source_out = process.getOutputStream(); + InputStream source_in = new FileInputStream(source.getFileDescriptor()); + + writeToProcess = new Thread(new Repeater(source_in, source_out)); + writeToProcess.start(); + } else { + writeToProcess = null; + } + + Thread cleanup = new Thread(new Runnable() { + @Override + public void run() { try { - process = Runtime.getRuntime().exec(command); - - in = process.getInputStream(); - out = new FileOutputStream(sink.getFileDescriptor()); - - final byte[] buffer = new byte[8192]; - while (true) { - final int readByteCount = in.read(buffer); - if (readByteCount < 0) { - break; - } - out.write(buffer, 0, readByteCount); + if (writeToProcess != null) { + writeToProcess.join(); } - } catch (IOException ioe) { - throw new RuntimeException("Error running shell command", ioe); - } finally { - if (process != null) { - process.destroy(); + if (readFromProcess != null) { + readFromProcess.join(); } - IoUtils.closeQuietly(out); - IoUtils.closeQuietly(sink); + } catch (InterruptedException exc) { + Log.e(TAG, "At least one of the threads was interrupted"); + } + IoUtils.closeQuietly(sink); + IoUtils.closeQuietly(source); + process.destroy(); } - }; - }; - streamReader.start(); + }); + cleanup.start(); } @Override |