Add UHID_OUTPUT handling to hid command
Some joysticks may send UHID_OUTPUT requests during probe. They expect to receive a report in response. Provide a facility to specify the expected outputs from the driver, and a way to respond to each of these requests. Typically, this would be something like "driver: please use full report mode", "joystick: ACK". If the ACK is not received by the driver, the probe would fail, and input device would never get registered. Bug: 135136477 Test: atest NintendoSwitchProTest Change-Id: Ic2c7a73d3d4bf759a1a104324687cd01646f256e
This commit is contained in:
@@ -27,12 +27,13 @@
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
|
||||
#include <android/log.h>
|
||||
#include <android/looper.h>
|
||||
#include <jni.h>
|
||||
#include <nativehelper/JNIHelp.h>
|
||||
#include <nativehelper/ScopedLocalRef.h>
|
||||
#include <nativehelper/ScopedPrimitiveArray.h>
|
||||
#include <nativehelper/ScopedUtfChars.h>
|
||||
#include <android/looper.h>
|
||||
#include <android/log.h>
|
||||
|
||||
#include <android-base/stringprintf.h>
|
||||
|
||||
@@ -49,6 +50,7 @@ static const char* UHID_PATH = "/dev/uhid";
|
||||
static struct {
|
||||
jmethodID onDeviceOpen;
|
||||
jmethodID onDeviceGetReport;
|
||||
jmethodID onDeviceOutput;
|
||||
jmethodID onDeviceError;
|
||||
} gDeviceCallbackClassInfo;
|
||||
|
||||
@@ -64,6 +66,18 @@ static void checkAndClearException(JNIEnv* env, const char* methodName) {
|
||||
}
|
||||
}
|
||||
|
||||
static ScopedLocalRef<jbyteArray> toJbyteArray(JNIEnv* env, const std::vector<uint8_t>& vector) {
|
||||
ScopedLocalRef<jbyteArray> array(env, env->NewByteArray(vector.size()));
|
||||
if (array.get() == nullptr) {
|
||||
jniThrowException(env, "java/lang/OutOfMemoryError", nullptr);
|
||||
return array;
|
||||
}
|
||||
static_assert(sizeof(char) == sizeof(uint8_t));
|
||||
env->SetByteArrayRegion(array.get(), 0, vector.size(),
|
||||
reinterpret_cast<const signed char*>(vector.data()));
|
||||
return array;
|
||||
}
|
||||
|
||||
static std::string toString(const std::vector<uint8_t>& data) {
|
||||
std::string s = "";
|
||||
for (uint8_t b : data) {
|
||||
@@ -101,6 +115,13 @@ void DeviceCallback::onDeviceGetReport(uint32_t requestId, uint8_t reportId) {
|
||||
checkAndClearException(env, "onDeviceGetReport");
|
||||
}
|
||||
|
||||
void DeviceCallback::onDeviceOutput(const std::vector<uint8_t>& data) {
|
||||
JNIEnv* env = getJNIEnv();
|
||||
env->CallVoidMethod(mCallbackObject, gDeviceCallbackClassInfo.onDeviceOutput,
|
||||
toJbyteArray(env, data).get());
|
||||
checkAndClearException(env, "onDeviceOutput");
|
||||
}
|
||||
|
||||
JNIEnv* DeviceCallback::getJNIEnv() {
|
||||
JNIEnv* env;
|
||||
mJavaVM->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6);
|
||||
@@ -240,6 +261,12 @@ int Device::handleEvents(int events) {
|
||||
set_report.rnum, toString(data).c_str());
|
||||
break;
|
||||
}
|
||||
case UHID_OUTPUT: {
|
||||
struct uhid_output_req& output = ev.u.output;
|
||||
std::vector<uint8_t> data(output.data, output.data + output.size);
|
||||
mDeviceCallback->onDeviceOutput(data);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
LOGI("Unhandled event type: %" PRIu32, ev.type);
|
||||
break;
|
||||
@@ -332,6 +359,8 @@ int register_com_android_commands_hid_Device(JNIEnv* env) {
|
||||
env->GetMethodID(clazz, "onDeviceOpen", "()V");
|
||||
uhid::gDeviceCallbackClassInfo.onDeviceGetReport =
|
||||
env->GetMethodID(clazz, "onDeviceGetReport", "(II)V");
|
||||
uhid::gDeviceCallbackClassInfo.onDeviceOutput =
|
||||
env->GetMethodID(clazz, "onDeviceOutput", "([B)V");
|
||||
uhid::gDeviceCallbackClassInfo.onDeviceError =
|
||||
env->GetMethodID(clazz, "onDeviceError", "()V");
|
||||
if (uhid::gDeviceCallbackClassInfo.onDeviceOpen == NULL ||
|
||||
|
||||
@@ -31,6 +31,7 @@ public:
|
||||
|
||||
void onDeviceOpen();
|
||||
void onDeviceGetReport(uint32_t requestId, uint8_t reportId);
|
||||
void onDeviceOutput(const std::vector<uint8_t>& data);
|
||||
void onDeviceError();
|
||||
|
||||
private:
|
||||
|
||||
@@ -20,13 +20,16 @@ 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 android.util.SparseArray;
|
||||
|
||||
import com.android.internal.os.SomeArgs;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Arrays;
|
||||
import java.util.Map;
|
||||
|
||||
public class Device {
|
||||
private static final String TAG = "HidDevice";
|
||||
|
||||
@@ -40,6 +43,7 @@ public class Device {
|
||||
private final DeviceHandler mHandler;
|
||||
// mFeatureReports is limited to 256 entries, because the report number is 8-bit
|
||||
private final SparseArray<byte[]> mFeatureReports;
|
||||
private final Map<ByteBuffer, byte[]> mOutputs;
|
||||
private long mTimeToSend;
|
||||
|
||||
private final Object mCond = new Object();
|
||||
@@ -55,12 +59,13 @@ public class Device {
|
||||
private static native void nativeCloseDevice(long ptr);
|
||||
|
||||
public Device(int id, String name, int vid, int pid, byte[] descriptor,
|
||||
byte[] report, SparseArray<byte[]> featureReports) {
|
||||
byte[] report, SparseArray<byte[]> featureReports, Map<ByteBuffer, byte[]> outputs) {
|
||||
mId = id;
|
||||
mThread = new HandlerThread("HidDeviceHandler");
|
||||
mThread.start();
|
||||
mHandler = new DeviceHandler(mThread.getLooper());
|
||||
mFeatureReports = featureReports;
|
||||
mOutputs = outputs;
|
||||
SomeArgs args = SomeArgs.obtain();
|
||||
args.argi1 = id;
|
||||
args.argi2 = vid;
|
||||
@@ -160,6 +165,11 @@ public class Device {
|
||||
}
|
||||
|
||||
public void onDeviceGetReport(int requestId, int reportId) {
|
||||
if (mFeatureReports == null) {
|
||||
Log.e(TAG, "Received GET_REPORT request for reportId=" + reportId
|
||||
+ ", but 'feature_reports' section is not found");
|
||||
return;
|
||||
}
|
||||
byte[] report = mFeatureReports.get(reportId);
|
||||
|
||||
if (report == null) {
|
||||
@@ -176,6 +186,29 @@ public class Device {
|
||||
mHandler.sendMessageAtTime(msg, mTimeToSend);
|
||||
}
|
||||
|
||||
// native callback
|
||||
public void onDeviceOutput(byte[] data) {
|
||||
if (mOutputs == null) {
|
||||
Log.e(TAG, "Received OUTPUT request, but 'outputs' section is not found");
|
||||
return;
|
||||
}
|
||||
byte[] response = mOutputs.get(ByteBuffer.wrap(data));
|
||||
if (response == null) {
|
||||
Log.i(TAG,
|
||||
"Requested response for output " + Arrays.toString(data) + " is not found");
|
||||
return;
|
||||
}
|
||||
|
||||
Message msg;
|
||||
msg = mHandler.obtainMessage(MSG_SEND_REPORT, response);
|
||||
|
||||
// Message is set to asynchronous so it won't be blocked by synchronization
|
||||
// barrier during UHID_OPEN. This is necessary for drivers that do
|
||||
// UHID_OUTPUT requests during probe, and expect a response right away.
|
||||
msg.setAsynchronous(true);
|
||||
mHandler.sendMessageAtTime(msg, mTimeToSend);
|
||||
}
|
||||
|
||||
public void onDeviceError() {
|
||||
Log.e(TAG, "Device error occurred, closing /dev/uhid");
|
||||
Message msg = mHandler.obtainMessage(MSG_CLOSE_DEVICE);
|
||||
|
||||
@@ -21,10 +21,13 @@ import android.util.JsonToken;
|
||||
import android.util.Log;
|
||||
import android.util.SparseArray;
|
||||
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class Event {
|
||||
private static final String TAG = "HidEvent";
|
||||
@@ -41,6 +44,7 @@ public class Event {
|
||||
private int mPid;
|
||||
private byte[] mReport;
|
||||
private SparseArray<byte[]> mFeatureReports;
|
||||
private Map<ByteBuffer, byte[]> mOutputs;
|
||||
private int mDuration;
|
||||
|
||||
public int getId() {
|
||||
@@ -75,6 +79,10 @@ public class Event {
|
||||
return mFeatureReports;
|
||||
}
|
||||
|
||||
public Map<ByteBuffer, byte[]> getOutputs() {
|
||||
return mOutputs;
|
||||
}
|
||||
|
||||
public int getDuration() {
|
||||
return mDuration;
|
||||
}
|
||||
@@ -88,6 +96,7 @@ public class Event {
|
||||
+ ", pid=" + mPid
|
||||
+ ", report=" + Arrays.toString(mReport)
|
||||
+ ", feature_reports=" + mFeatureReports.toString()
|
||||
+ ", outputs=" + mOutputs.toString()
|
||||
+ ", duration=" + mDuration
|
||||
+ "}";
|
||||
}
|
||||
@@ -123,6 +132,10 @@ public class Event {
|
||||
mEvent.mFeatureReports = reports;
|
||||
}
|
||||
|
||||
public void setOutputs(Map<ByteBuffer, byte[]> outputs) {
|
||||
mEvent.mOutputs = outputs;
|
||||
}
|
||||
|
||||
public void setVid(int vid) {
|
||||
mEvent.mVid = vid;
|
||||
}
|
||||
@@ -199,6 +212,9 @@ public class Event {
|
||||
case "feature_reports":
|
||||
eb.setFeatureReports(readFeatureReports());
|
||||
break;
|
||||
case "outputs":
|
||||
eb.setOutputs(readOutputs());
|
||||
break;
|
||||
case "duration":
|
||||
eb.setDuration(readInt());
|
||||
break;
|
||||
@@ -250,7 +266,7 @@ public class Event {
|
||||
|
||||
private SparseArray<byte[]> readFeatureReports()
|
||||
throws IllegalStateException, IOException {
|
||||
SparseArray<byte[]> featureReports = new SparseArray();
|
||||
SparseArray<byte[]> featureReports = new SparseArray<>();
|
||||
try {
|
||||
mReader.beginArray();
|
||||
while (mReader.hasNext()) {
|
||||
@@ -276,17 +292,60 @@ public class Event {
|
||||
}
|
||||
}
|
||||
mReader.endObject();
|
||||
if (data != null)
|
||||
if (data != null) {
|
||||
featureReports.put(id, data);
|
||||
}
|
||||
}
|
||||
mReader.endArray();
|
||||
} catch (IllegalStateException|NumberFormatException e) {
|
||||
} catch (IllegalStateException | NumberFormatException e) {
|
||||
consumeRemainingElements();
|
||||
mReader.endArray();
|
||||
throw new IllegalStateException("Encountered malformed data.", e);
|
||||
} finally {
|
||||
return featureReports;
|
||||
}
|
||||
return featureReports;
|
||||
}
|
||||
|
||||
private Map<ByteBuffer, byte[]> readOutputs()
|
||||
throws IllegalStateException, IOException {
|
||||
Map<ByteBuffer, byte[]> outputs = new HashMap<>();
|
||||
|
||||
try {
|
||||
mReader.beginArray();
|
||||
while (mReader.hasNext()) {
|
||||
byte[] output = null;
|
||||
byte[] response = null;
|
||||
mReader.beginObject();
|
||||
while (mReader.hasNext()) {
|
||||
String name = mReader.nextName();
|
||||
switch (name) {
|
||||
case "description":
|
||||
// Description is only used to keep track of the output responses
|
||||
mReader.nextString();
|
||||
break;
|
||||
case "output":
|
||||
output = readData();
|
||||
break;
|
||||
case "response":
|
||||
response = readData();
|
||||
break;
|
||||
default:
|
||||
consumeRemainingElements();
|
||||
mReader.endObject();
|
||||
throw new IllegalStateException("Invalid key in outputs: " + name);
|
||||
}
|
||||
}
|
||||
mReader.endObject();
|
||||
if (output != null) {
|
||||
outputs.put(ByteBuffer.wrap(output), response);
|
||||
}
|
||||
}
|
||||
mReader.endArray();
|
||||
} catch (IllegalStateException | NumberFormatException e) {
|
||||
consumeRemainingElements();
|
||||
mReader.endArray();
|
||||
throw new IllegalStateException("Encountered malformed data.", e);
|
||||
}
|
||||
return outputs;
|
||||
}
|
||||
|
||||
private void consumeRemainingElements() throws IOException {
|
||||
@@ -296,10 +355,6 @@ public class Event {
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
@@ -16,22 +16,17 @@
|
||||
|
||||
package com.android.commands.hid;
|
||||
|
||||
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.IOException;
|
||||
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";
|
||||
@@ -119,7 +114,7 @@ public class Hid {
|
||||
}
|
||||
int id = e.getId();
|
||||
Device d = new Device(id, e.getName(), e.getVendorId(), e.getProductId(),
|
||||
e.getDescriptor(), e.getReport(), e.getFeatureReports());
|
||||
e.getDescriptor(), e.getReport(), e.getFeatureReports(), e.getOutputs());
|
||||
mDevices.append(id, d);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user