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:
Siarhei Vishniakou
2019-12-06 22:43:28 -08:00
parent c490e7acde
commit f6081119aa
5 changed files with 134 additions and 21 deletions

View File

@@ -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 ||

View File

@@ -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:

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);
}