Loading services/core/java/com/android/server/tv/UinputBridge.java +70 −4 Original line number Diff line number Diff line Loading @@ -28,7 +28,7 @@ import java.io.IOException; public final class UinputBridge { private final CloseGuard mCloseGuard = CloseGuard.get(); private long mPtr; private IBinder mToken = null; private IBinder mToken; private static native long nativeOpen(String name, String uniqueId, int width, int height, int maxPointers); Loading @@ -39,6 +39,25 @@ public final class UinputBridge { private static native void nativeSendPointerUp(long ptr, int pointerId); private static native void nativeSendPointerSync(long ptr); /** Opens a gamepad - will support gamepad key and axis sending */ private static native long nativeGamepadOpen(String name, String uniqueId); /** Marks the specified key up/down for a gamepad */ private static native void nativeSendGamepadKey(long ptr, int keyIndex, boolean down); /** * Gamepads pre-define the following axes: * - Left joystick X, axis == ABS_X == 0, range [0, 254] * - Left joystick Y, axis == ABS_Y == 1, range [0, 254] * - Right joystick X, axis == ABS_RX == 3, range [0, 254] * - Right joystick Y, axis == ABS_RY == 4, range [0, 254] * - Left trigger, axis == ABS_Z == 2, range [0, 254] * - Right trigger, axis == ABS_RZ == 5, range [0, 254] * - DPad X, axis == ABS_HAT0X == 0x10, range [-1, 1] * - DPad Y, axis == ABS_HAT0Y == 0x11, range [-1, 1] */ private static native void nativeSendGamepadAxisValue(long ptr, int axis, int value); public UinputBridge(IBinder token, String name, int width, int height, int maxPointers) throws IOException { if (width < 1 || height < 1) { Loading @@ -58,12 +77,31 @@ public final class UinputBridge { mCloseGuard.open("close"); } /** Constructor used by static factory methods */ private UinputBridge(IBinder token, long ptr) { mPtr = ptr; mToken = token; mCloseGuard.open("close"); } /** Opens a UinputBridge that supports gamepad buttons and axes. */ public static UinputBridge openGamepad(IBinder token, String name) throws IOException { if (token == null) { throw new IllegalArgumentException("Token cannot be null"); } long ptr = nativeGamepadOpen(name, token.toString()); if (ptr == 0) { throw new IOException("Could not open uinput device " + name); } return new UinputBridge(token, ptr); } @Override protected void finalize() throws Throwable { try { if (mCloseGuard != null) { mCloseGuard.warnIfOpen(); } close(mToken); } finally { mToken = null; Loading Loading @@ -119,7 +157,35 @@ public final class UinputBridge { if (isTokenValid(token)) { nativeSendPointerSync(mPtr); } } /** Send a gamepad key * @param keyIndex - the index of the w3-spec key * @param down - is the key pressed ? */ public void sendGamepadKey(IBinder token, int keyIndex, boolean down) { if (isTokenValid(token)) { nativeSendGamepadKey(mPtr, keyIndex, down); } } /** Send a gamepad axis value. * - Left joystick X, axis == ABS_X == 0, range [0, 254] * - Left joystick Y, axis == ABS_Y == 1, range [0, 254] * - Right joystick X, axis == ABS_RX == 3, range [0, 254] * - Right joystick Y, axis == ABS_RY == 4, range [0, 254] * - Left trigger, axis == ABS_Z == 2, range [0, 254] * - Right trigger, axis == ABS_RZ == 5, range [0, 254] * - DPad X, axis == ABS_HAT0X == 0x10, range [-1, 1] * - DPad Y, axis == ABS_HAT0Y == 0x11, range [-1, 1] * * @param axis is the axis index * @param value is the value to set for that axis */ public void sendGamepadAxisValue(IBinder token, int axis, int value) { if (isTokenValid(token)) { nativeSendGamepadAxisValue(mPtr, axis, value); } } public void clear(IBinder token) { Loading services/core/jni/com_android_server_tv_GamepadKeys.h 0 → 100644 +79 −0 Original line number Diff line number Diff line #ifndef ANDROIDTVREMOTE_SERVICE_JNI_GAMEPAD_KEYS_H_ #define ANDROIDTVREMOTE_SERVICE_JNI_GAMEPAD_KEYS_H_ #include <linux/input.h> namespace android { // Follows the W3 spec for gamepad buttons and their corresponding mapping into // Linux keycodes. Note that gamepads are generally not very well standardized // and various controllers will result in different buttons. This mapping tries // to be reasonable. // // W3 Button spec: https://www.w3.org/TR/gamepad/#remapping // // Standard gamepad keycodes are added plus 2 additional buttons (e.g. Stadia // has "Assistant" and "Share", PS4 has the touchpad button). // // To generate this list, PS4, XBox, Stadia and Nintendo Switch Pro were tested. static const int GAMEPAD_KEY_CODES[19] = { // Right-side buttons. A/B/X/Y or circle/triangle/square/X or similar BTN_A, // "South", A, GAMEPAD and SOUTH have the same constant BTN_B, // "East", BTN_B, BTN_EAST have the same constant BTN_X, // "West", Note that this maps to X and NORTH in constants BTN_Y, // "North", Note that this maps to Y and WEST in constants BTN_TL, // "Left Bumper" / "L1" - Nintendo sends BTN_WEST instead BTN_TR, // "Right Bumper" / "R1" - Nintendo sends BTN_Z instead // For triggers, gamepads vary: // - Stadia sends analog values over ABS_GAS/ABS_BRAKE and sends // TriggerHappy3/4 as digital presses // - PS4 and Xbox send analog values as ABS_Z/ABS_RZ // - Nintendo Pro sends BTN_TL/BTN_TR (since bumpers behave differently) // As placeholders we chose the stadia trigger-happy values since TL/TR are // sent for bumper button presses BTN_TRIGGER_HAPPY4, // "Left Trigger" / "L2" BTN_TRIGGER_HAPPY3, // "Right Trigger" / "R2" BTN_SELECT, // "Select/Back". Often "options" or similar BTN_START, // "Start/forward". Often "hamburger" icon BTN_THUMBL, // "Left Joystick Pressed" BTN_THUMBR, // "Right Joystick Pressed" // For DPads, gamepads generally only send axis changes // on ABS_HAT0X and ABS_HAT0Y. KEY_UP, // "Digital Pad up" KEY_DOWN, // "Digital Pad down" KEY_LEFT, // "Digital Pad left" KEY_RIGHT, // "Digital Pad right" BTN_MODE, // "Main button" (Stadia/PS/XBOX/Home) BTN_TRIGGER_HAPPY1, // Extra button: "Assistant" for Stadia BTN_TRIGGER_HAPPY2, // Extra button: "Share" for Stadia }; // Defines information for an axis. struct Axis { int number; int rangeMin; int rangeMax; }; // List of all axes supported by a gamepad static const Axis GAMEPAD_AXES[] = { {ABS_X, 0, 254}, // Left joystick X {ABS_Y, 0, 254}, // Left joystick Y {ABS_RX, 0, 254}, // Right joystick X {ABS_RY, 0, 254}, // Right joystick Y {ABS_Z, 0, 254}, // Left trigger {ABS_RZ, 0, 254}, // Right trigger {ABS_HAT0X, -1, 1}, // DPad X {ABS_HAT0Y, -1, 1}, // DPad Y }; } // namespace android #endif // ANDROIDTVREMOTE_SERVICE_JNI_GAMEPAD_KEYS_H_ services/core/jni/com_android_server_tv_TvKeys.h +1 −1 Original line number Diff line number Diff line Loading @@ -110,4 +110,4 @@ static Key KEYS[] = { } // namespace android #endif // SERVICE_JNI_KEYS_H_ #endif // ANDROIDTVREMOTE_SERVICE_JNI_KEYS_H_ services/core/jni/com_android_server_tv_TvUinputBridge.cpp +256 −55 Original line number Diff line number Diff line Loading @@ -16,6 +16,7 @@ #define LOG_TAG "TvRemote-native-uiBridge" #include "com_android_server_tv_GamepadKeys.h" #include "com_android_server_tv_TvKeys.h" #include "jni.h" Loading Loading @@ -92,27 +93,156 @@ static void unassignSlot(int32_t pointerId) { } } static const int kInvalidFileDescriptor = -1; // Convenience class to manage an opened /dev/uinput device class UInputDescriptor { public: UInputDescriptor() : mFd(kInvalidFileDescriptor) { memset(&mUinputDescriptor, 0, sizeof(mUinputDescriptor)); } // Auto-closes any open /dev/uinput descriptor unless detached. ~UInputDescriptor(); // Open /dev/uinput and prepare to register // the device with the given name and unique Id bool Open(const char* name, const char* uniqueId); // Checks if the current file descriptor is valid bool IsValid() const { return mFd != kInvalidFileDescriptor; } void EnableKey(int keyCode); void EnableAxesEvents(); void EnableAxis(int axis, int rangeMin, int rangeMax); bool Create(); // Detaches from the current file descriptor // Returns the file descriptor for /dev/uniput int Detach(); private: int mFd; struct uinput_user_dev mUinputDescriptor; }; UInputDescriptor::~UInputDescriptor() { if (mFd != kInvalidFileDescriptor) { close(mFd); mFd = kInvalidFileDescriptor; } } int UInputDescriptor::Detach() { int fd = mFd; mFd = kInvalidFileDescriptor; return fd; } bool UInputDescriptor::Open(const char* name, const char* uniqueId) { if (IsValid()) { ALOGE("UInput device already open"); return false; } mFd = ::open("/dev/uinput", O_WRONLY | O_NDELAY); if (mFd < 0) { ALOGE("Cannot open /dev/uinput: %s.", strerror(errno)); mFd = kInvalidFileDescriptor; return false; } // write device unique id to the phys property ioctl(mFd, UI_SET_PHYS, uniqueId); memset(&mUinputDescriptor, 0, sizeof(mUinputDescriptor)); strlcpy(mUinputDescriptor.name, name, UINPUT_MAX_NAME_SIZE); mUinputDescriptor.id.version = 1; mUinputDescriptor.id.bustype = BUS_VIRTUAL; // All UInput devices we use process keys ioctl(mFd, UI_SET_EVBIT, EV_KEY); return true; } void UInputDescriptor::EnableKey(int keyCode) { ioctl(mFd, UI_SET_KEYBIT, keyCode); } void UInputDescriptor::EnableAxesEvents() { ioctl(mFd, UI_SET_EVBIT, EV_ABS); } void UInputDescriptor::EnableAxis(int axis, int rangeMin, int rangeMax) { if ((axis < 0) || (axis >= NELEM(mUinputDescriptor.absmin))) { ALOGE("Invalid axis number: %d", axis); return; } if (ioctl(mFd, UI_SET_ABSBIT, axis) != 0) { ALOGE("Failed to set absbit for %d", axis); } mUinputDescriptor.absmin[axis] = rangeMin; mUinputDescriptor.absmax[axis] = rangeMax; mUinputDescriptor.absfuzz[axis] = 0; mUinputDescriptor.absflat[axis] = 0; } bool UInputDescriptor::Create() { // register the input device if (write(mFd, &mUinputDescriptor, sizeof(mUinputDescriptor)) != sizeof(mUinputDescriptor)) { ALOGE("Cannot write uinput_user_dev to fd %d: %s.", mFd, strerror(errno)); return false; } if (ioctl(mFd, UI_DEV_CREATE) != 0) { ALOGE("Unable to create uinput device: %s.", strerror(errno)); return false; } ALOGV("Created uinput device, fd=%d.", mFd); return true; } class NativeConnection { public: enum class ConnectionType { kRemoteDevice, kGamepadDevice, }; ~NativeConnection(); static NativeConnection* open(const char* name, const char* uniqueId, int32_t width, int32_t height, int32_t maxPointerId); static NativeConnection* openGamepad(const char* name, const char* uniqueId); void sendEvent(int32_t type, int32_t code, int32_t value); int32_t getMaxPointers() const { return mMaxPointers; } ConnectionType getType() const { return mType; } bool IsGamepad() const { return getType() == ConnectionType::kGamepadDevice; } bool IsRemote() const { return getType() == ConnectionType::kRemoteDevice; } private: NativeConnection(int fd, int32_t maxPointers); NativeConnection(int fd, int32_t maxPointers, ConnectionType type); const int mFd; const int32_t mMaxPointers; const ConnectionType mType; }; NativeConnection::NativeConnection(int fd, int32_t maxPointers) : mFd(fd), mMaxPointers(maxPointers) { } NativeConnection::NativeConnection(int fd, int32_t maxPointers, ConnectionType type) : mFd(fd), mMaxPointers(maxPointers), mType(type) {} NativeConnection::~NativeConnection() { ALOGI("Un-Registering uinput device %d.", mFd); Loading @@ -125,44 +255,50 @@ NativeConnection* NativeConnection::open(const char* name, const char* uniqueId, ALOGI("Registering uinput device %s: touch pad size %dx%d, " "max pointers %d.", name, width, height, maxPointers); int fd = ::open("/dev/uinput", O_WRONLY | O_NDELAY); if (fd < 0) { ALOGE("Cannot open /dev/uinput: %s.", strerror(errno)); initKeysMap(); UInputDescriptor descriptor; if (!descriptor.Open(name, uniqueId)) { return nullptr; } struct uinput_user_dev uinp; memset(&uinp, 0, sizeof(struct uinput_user_dev)); strlcpy(uinp.name, name, UINPUT_MAX_NAME_SIZE); uinp.id.version = 1; uinp.id.bustype = BUS_VIRTUAL; // set the keys mapped for (size_t i = 0; i < NELEM(KEYS); i++) { descriptor.EnableKey(KEYS[i].linuxKeyCode); } // initialize keymap initKeysMap(); if (!descriptor.Create()) { return nullptr; } // write device unique id to the phys property ioctl(fd, UI_SET_PHYS, uniqueId); return new NativeConnection(descriptor.Detach(), maxPointers, ConnectionType::kRemoteDevice); } // set the keys mapped ioctl(fd, UI_SET_EVBIT, EV_KEY); for (size_t i = 0; i < NELEM(KEYS); i++) { ioctl(fd, UI_SET_KEYBIT, KEYS[i].linuxKeyCode); NativeConnection* NativeConnection::openGamepad(const char* name, const char* uniqueId) { ALOGI("Registering uinput device %s: gamepad", name); UInputDescriptor descriptor; if (!descriptor.Open(name, uniqueId)) { return nullptr; } // register the input device if (write(fd, &uinp, sizeof(uinp)) != sizeof(uinp)) { ALOGE("Cannot write uinput_user_dev to fd %d: %s.", fd, strerror(errno)); close(fd); return NULL; // set the keys mapped for gamepads for (size_t i = 0; i < NELEM(GAMEPAD_KEY_CODES); i++) { descriptor.EnableKey(GAMEPAD_KEY_CODES[i]); } if (ioctl(fd, UI_DEV_CREATE) != 0) { ALOGE("Unable to create uinput device: %s.", strerror(errno)); close(fd); // define the axes that are required descriptor.EnableAxesEvents(); for (size_t i = 0; i < NELEM(GAMEPAD_AXES); i++) { const Axis& axis = GAMEPAD_AXES[i]; descriptor.EnableAxis(axis.number, axis.rangeMin, axis.rangeMax); } if (!descriptor.Create()) { return nullptr; } ALOGV("Created uinput device, fd=%d.", fd); return new NativeConnection(fd, maxPointers); return new NativeConnection(descriptor.Detach(), 0, ConnectionType::kGamepadDevice); } void NativeConnection::sendEvent(int32_t type, int32_t code, int32_t value) { Loading @@ -174,7 +310,6 @@ void NativeConnection::sendEvent(int32_t type, int32_t code, int32_t value) { write(mFd, &iev, sizeof(iev)); } static jlong nativeOpen(JNIEnv* env, jclass clazz, jstring nameStr, jstring uniqueIdStr, jint width, jint height, jint maxPointers) { Loading @@ -186,6 +321,14 @@ static jlong nativeOpen(JNIEnv* env, jclass clazz, return reinterpret_cast<jlong>(connection); } static jlong nativeGamepadOpen(JNIEnv* env, jclass clazz, jstring nameStr, jstring uniqueIdStr) { ScopedUtfChars name(env, nameStr); ScopedUtfChars uniqueId(env, uniqueIdStr); NativeConnection* connection = NativeConnection::openGamepad(name.c_str(), uniqueId.c_str()); return reinterpret_cast<jlong>(connection); } static void nativeClose(JNIEnv* env, jclass clazz, jlong ptr) { NativeConnection* connection = reinterpret_cast<NativeConnection*>(ptr); delete connection; Loading @@ -194,6 +337,12 @@ static void nativeClose(JNIEnv* env, jclass clazz, jlong ptr) { static void nativeSendKey(JNIEnv* env, jclass clazz, jlong ptr, jint keyCode, jboolean down) { int32_t code = getLinuxKeyCode(keyCode); NativeConnection* connection = reinterpret_cast<NativeConnection*>(ptr); if (connection->IsGamepad()) { ALOGE("Invalid key even for a gamepad - need to send gamepad events"); return; } if (code != KEY_UNKNOWN) { connection->sendEvent(EV_KEY, code, down ? 1 : 0); } else { Loading @@ -201,10 +350,44 @@ static void nativeSendKey(JNIEnv* env, jclass clazz, jlong ptr, jint keyCode, jb } } static void nativeSendGamepadKey(JNIEnv* env, jclass clazz, jlong ptr, jint keyIndex, jboolean down) { NativeConnection* connection = reinterpret_cast<NativeConnection*>(ptr); if (!connection->IsGamepad()) { ALOGE("Invalid gamepad key for non-gamepad device"); return; } if ((keyIndex < 0) || (keyIndex >= NELEM(GAMEPAD_KEY_CODES))) { ALOGE("Invalid gamepad key index: %d", keyIndex); return; } connection->sendEvent(EV_KEY, GAMEPAD_KEY_CODES[keyIndex], down ? 1 : 0); } static void nativeSendGamepadAxisValue(JNIEnv* env, jclass clazz, jlong ptr, jint axis, jint value) { NativeConnection* connection = reinterpret_cast<NativeConnection*>(ptr); if (!connection->IsGamepad()) { ALOGE("Invalid axis send for non-gamepad device"); return; } connection->sendEvent(EV_ABS, axis, value); } static void nativeSendPointerDown(JNIEnv* env, jclass clazz, jlong ptr, jint pointerId, jint x, jint y) { NativeConnection* connection = reinterpret_cast<NativeConnection*>(ptr); if (connection->IsGamepad()) { ALOGE("Invalid pointer down event for a gamepad."); return; } int32_t slot = findSlot(pointerId); if (slot == SLOT_UNKNOWN) { slot = assignSlot(pointerId); Loading @@ -221,6 +404,11 @@ static void nativeSendPointerUp(JNIEnv* env, jclass clazz, jlong ptr, jint pointerId) { NativeConnection* connection = reinterpret_cast<NativeConnection*>(ptr); if (connection->IsGamepad()) { ALOGE("Invalid pointer up event for a gamepad."); return; } int32_t slot = findSlot(pointerId); if (slot != SLOT_UNKNOWN) { connection->sendEvent(EV_ABS, ABS_MT_SLOT, slot); Loading @@ -238,6 +426,7 @@ static void nativeClear(JNIEnv* env, jclass clazz, jlong ptr) { NativeConnection* connection = reinterpret_cast<NativeConnection*>(ptr); // Clear keys. if (connection->IsRemote()) { for (size_t i = 0; i < NELEM(KEYS); i++) { connection->sendEvent(EV_KEY, KEYS[i].linuxKeyCode, 0); } Loading @@ -251,6 +440,22 @@ static void nativeClear(JNIEnv* env, jclass clazz, jlong ptr) { connection->sendEvent(EV_ABS, ABS_MT_TRACKING_ID, -1); } } } else { for (size_t i = 0; i < NELEM(GAMEPAD_KEY_CODES); i++) { connection->sendEvent(EV_KEY, GAMEPAD_KEY_CODES[i], 0); } for (size_t i = 0; i < NELEM(GAMEPAD_AXES); i++) { const Axis& axis = GAMEPAD_AXES[i]; if ((axis.number == ABS_Z) || (axis.number == ABS_RZ)) { // Mark triggers unpressed connection->sendEvent(EV_ABS, axis.number, 0); } else { // Joysticks and dpad rests on center connection->sendEvent(EV_ABS, axis.number, (axis.rangeMin + axis.rangeMax) / 2); } } } // Sync pointer events connection->sendEvent(EV_SYN, SYN_REPORT, 0); Loading @@ -261,20 +466,16 @@ static void nativeClear(JNIEnv* env, jclass clazz, jlong ptr) { */ static JNINativeMethod gUinputBridgeMethods[] = { { "nativeOpen", "(Ljava/lang/String;Ljava/lang/String;III)J", (void*)nativeOpen }, { "nativeClose", "(J)V", (void*)nativeClose }, { "nativeSendKey", "(JIZ)V", (void*)nativeSendKey }, { "nativeSendPointerDown", "(JIII)V", (void*)nativeSendPointerDown }, { "nativeSendPointerUp", "(JI)V", (void*)nativeSendPointerUp }, { "nativeClear", "(J)V", (void*)nativeClear }, { "nativeSendPointerSync", "(J)V", (void*)nativeSendPointerSync }, {"nativeOpen", "(Ljava/lang/String;Ljava/lang/String;III)J", (void*)nativeOpen}, {"nativeGamepadOpen", "(Ljava/lang/String;Ljava/lang/String;)J", (void*)nativeGamepadOpen}, {"nativeClose", "(J)V", (void*)nativeClose}, {"nativeSendKey", "(JIZ)V", (void*)nativeSendKey}, {"nativeSendPointerDown", "(JIII)V", (void*)nativeSendPointerDown}, {"nativeSendPointerUp", "(JI)V", (void*)nativeSendPointerUp}, {"nativeClear", "(J)V", (void*)nativeClear}, {"nativeSendPointerSync", "(J)V", (void*)nativeSendPointerSync}, {"nativeSendGamepadKey", "(JIZ)V", (void*)nativeSendGamepadKey}, {"nativeSendGamepadAxisValue", "(JII)V", (void*)nativeSendGamepadAxisValue}, }; int register_android_server_tv_TvUinputBridge(JNIEnv* env) { Loading Loading
services/core/java/com/android/server/tv/UinputBridge.java +70 −4 Original line number Diff line number Diff line Loading @@ -28,7 +28,7 @@ import java.io.IOException; public final class UinputBridge { private final CloseGuard mCloseGuard = CloseGuard.get(); private long mPtr; private IBinder mToken = null; private IBinder mToken; private static native long nativeOpen(String name, String uniqueId, int width, int height, int maxPointers); Loading @@ -39,6 +39,25 @@ public final class UinputBridge { private static native void nativeSendPointerUp(long ptr, int pointerId); private static native void nativeSendPointerSync(long ptr); /** Opens a gamepad - will support gamepad key and axis sending */ private static native long nativeGamepadOpen(String name, String uniqueId); /** Marks the specified key up/down for a gamepad */ private static native void nativeSendGamepadKey(long ptr, int keyIndex, boolean down); /** * Gamepads pre-define the following axes: * - Left joystick X, axis == ABS_X == 0, range [0, 254] * - Left joystick Y, axis == ABS_Y == 1, range [0, 254] * - Right joystick X, axis == ABS_RX == 3, range [0, 254] * - Right joystick Y, axis == ABS_RY == 4, range [0, 254] * - Left trigger, axis == ABS_Z == 2, range [0, 254] * - Right trigger, axis == ABS_RZ == 5, range [0, 254] * - DPad X, axis == ABS_HAT0X == 0x10, range [-1, 1] * - DPad Y, axis == ABS_HAT0Y == 0x11, range [-1, 1] */ private static native void nativeSendGamepadAxisValue(long ptr, int axis, int value); public UinputBridge(IBinder token, String name, int width, int height, int maxPointers) throws IOException { if (width < 1 || height < 1) { Loading @@ -58,12 +77,31 @@ public final class UinputBridge { mCloseGuard.open("close"); } /** Constructor used by static factory methods */ private UinputBridge(IBinder token, long ptr) { mPtr = ptr; mToken = token; mCloseGuard.open("close"); } /** Opens a UinputBridge that supports gamepad buttons and axes. */ public static UinputBridge openGamepad(IBinder token, String name) throws IOException { if (token == null) { throw new IllegalArgumentException("Token cannot be null"); } long ptr = nativeGamepadOpen(name, token.toString()); if (ptr == 0) { throw new IOException("Could not open uinput device " + name); } return new UinputBridge(token, ptr); } @Override protected void finalize() throws Throwable { try { if (mCloseGuard != null) { mCloseGuard.warnIfOpen(); } close(mToken); } finally { mToken = null; Loading Loading @@ -119,7 +157,35 @@ public final class UinputBridge { if (isTokenValid(token)) { nativeSendPointerSync(mPtr); } } /** Send a gamepad key * @param keyIndex - the index of the w3-spec key * @param down - is the key pressed ? */ public void sendGamepadKey(IBinder token, int keyIndex, boolean down) { if (isTokenValid(token)) { nativeSendGamepadKey(mPtr, keyIndex, down); } } /** Send a gamepad axis value. * - Left joystick X, axis == ABS_X == 0, range [0, 254] * - Left joystick Y, axis == ABS_Y == 1, range [0, 254] * - Right joystick X, axis == ABS_RX == 3, range [0, 254] * - Right joystick Y, axis == ABS_RY == 4, range [0, 254] * - Left trigger, axis == ABS_Z == 2, range [0, 254] * - Right trigger, axis == ABS_RZ == 5, range [0, 254] * - DPad X, axis == ABS_HAT0X == 0x10, range [-1, 1] * - DPad Y, axis == ABS_HAT0Y == 0x11, range [-1, 1] * * @param axis is the axis index * @param value is the value to set for that axis */ public void sendGamepadAxisValue(IBinder token, int axis, int value) { if (isTokenValid(token)) { nativeSendGamepadAxisValue(mPtr, axis, value); } } public void clear(IBinder token) { Loading
services/core/jni/com_android_server_tv_GamepadKeys.h 0 → 100644 +79 −0 Original line number Diff line number Diff line #ifndef ANDROIDTVREMOTE_SERVICE_JNI_GAMEPAD_KEYS_H_ #define ANDROIDTVREMOTE_SERVICE_JNI_GAMEPAD_KEYS_H_ #include <linux/input.h> namespace android { // Follows the W3 spec for gamepad buttons and their corresponding mapping into // Linux keycodes. Note that gamepads are generally not very well standardized // and various controllers will result in different buttons. This mapping tries // to be reasonable. // // W3 Button spec: https://www.w3.org/TR/gamepad/#remapping // // Standard gamepad keycodes are added plus 2 additional buttons (e.g. Stadia // has "Assistant" and "Share", PS4 has the touchpad button). // // To generate this list, PS4, XBox, Stadia and Nintendo Switch Pro were tested. static const int GAMEPAD_KEY_CODES[19] = { // Right-side buttons. A/B/X/Y or circle/triangle/square/X or similar BTN_A, // "South", A, GAMEPAD and SOUTH have the same constant BTN_B, // "East", BTN_B, BTN_EAST have the same constant BTN_X, // "West", Note that this maps to X and NORTH in constants BTN_Y, // "North", Note that this maps to Y and WEST in constants BTN_TL, // "Left Bumper" / "L1" - Nintendo sends BTN_WEST instead BTN_TR, // "Right Bumper" / "R1" - Nintendo sends BTN_Z instead // For triggers, gamepads vary: // - Stadia sends analog values over ABS_GAS/ABS_BRAKE and sends // TriggerHappy3/4 as digital presses // - PS4 and Xbox send analog values as ABS_Z/ABS_RZ // - Nintendo Pro sends BTN_TL/BTN_TR (since bumpers behave differently) // As placeholders we chose the stadia trigger-happy values since TL/TR are // sent for bumper button presses BTN_TRIGGER_HAPPY4, // "Left Trigger" / "L2" BTN_TRIGGER_HAPPY3, // "Right Trigger" / "R2" BTN_SELECT, // "Select/Back". Often "options" or similar BTN_START, // "Start/forward". Often "hamburger" icon BTN_THUMBL, // "Left Joystick Pressed" BTN_THUMBR, // "Right Joystick Pressed" // For DPads, gamepads generally only send axis changes // on ABS_HAT0X and ABS_HAT0Y. KEY_UP, // "Digital Pad up" KEY_DOWN, // "Digital Pad down" KEY_LEFT, // "Digital Pad left" KEY_RIGHT, // "Digital Pad right" BTN_MODE, // "Main button" (Stadia/PS/XBOX/Home) BTN_TRIGGER_HAPPY1, // Extra button: "Assistant" for Stadia BTN_TRIGGER_HAPPY2, // Extra button: "Share" for Stadia }; // Defines information for an axis. struct Axis { int number; int rangeMin; int rangeMax; }; // List of all axes supported by a gamepad static const Axis GAMEPAD_AXES[] = { {ABS_X, 0, 254}, // Left joystick X {ABS_Y, 0, 254}, // Left joystick Y {ABS_RX, 0, 254}, // Right joystick X {ABS_RY, 0, 254}, // Right joystick Y {ABS_Z, 0, 254}, // Left trigger {ABS_RZ, 0, 254}, // Right trigger {ABS_HAT0X, -1, 1}, // DPad X {ABS_HAT0Y, -1, 1}, // DPad Y }; } // namespace android #endif // ANDROIDTVREMOTE_SERVICE_JNI_GAMEPAD_KEYS_H_
services/core/jni/com_android_server_tv_TvKeys.h +1 −1 Original line number Diff line number Diff line Loading @@ -110,4 +110,4 @@ static Key KEYS[] = { } // namespace android #endif // SERVICE_JNI_KEYS_H_ #endif // ANDROIDTVREMOTE_SERVICE_JNI_KEYS_H_
services/core/jni/com_android_server_tv_TvUinputBridge.cpp +256 −55 Original line number Diff line number Diff line Loading @@ -16,6 +16,7 @@ #define LOG_TAG "TvRemote-native-uiBridge" #include "com_android_server_tv_GamepadKeys.h" #include "com_android_server_tv_TvKeys.h" #include "jni.h" Loading Loading @@ -92,27 +93,156 @@ static void unassignSlot(int32_t pointerId) { } } static const int kInvalidFileDescriptor = -1; // Convenience class to manage an opened /dev/uinput device class UInputDescriptor { public: UInputDescriptor() : mFd(kInvalidFileDescriptor) { memset(&mUinputDescriptor, 0, sizeof(mUinputDescriptor)); } // Auto-closes any open /dev/uinput descriptor unless detached. ~UInputDescriptor(); // Open /dev/uinput and prepare to register // the device with the given name and unique Id bool Open(const char* name, const char* uniqueId); // Checks if the current file descriptor is valid bool IsValid() const { return mFd != kInvalidFileDescriptor; } void EnableKey(int keyCode); void EnableAxesEvents(); void EnableAxis(int axis, int rangeMin, int rangeMax); bool Create(); // Detaches from the current file descriptor // Returns the file descriptor for /dev/uniput int Detach(); private: int mFd; struct uinput_user_dev mUinputDescriptor; }; UInputDescriptor::~UInputDescriptor() { if (mFd != kInvalidFileDescriptor) { close(mFd); mFd = kInvalidFileDescriptor; } } int UInputDescriptor::Detach() { int fd = mFd; mFd = kInvalidFileDescriptor; return fd; } bool UInputDescriptor::Open(const char* name, const char* uniqueId) { if (IsValid()) { ALOGE("UInput device already open"); return false; } mFd = ::open("/dev/uinput", O_WRONLY | O_NDELAY); if (mFd < 0) { ALOGE("Cannot open /dev/uinput: %s.", strerror(errno)); mFd = kInvalidFileDescriptor; return false; } // write device unique id to the phys property ioctl(mFd, UI_SET_PHYS, uniqueId); memset(&mUinputDescriptor, 0, sizeof(mUinputDescriptor)); strlcpy(mUinputDescriptor.name, name, UINPUT_MAX_NAME_SIZE); mUinputDescriptor.id.version = 1; mUinputDescriptor.id.bustype = BUS_VIRTUAL; // All UInput devices we use process keys ioctl(mFd, UI_SET_EVBIT, EV_KEY); return true; } void UInputDescriptor::EnableKey(int keyCode) { ioctl(mFd, UI_SET_KEYBIT, keyCode); } void UInputDescriptor::EnableAxesEvents() { ioctl(mFd, UI_SET_EVBIT, EV_ABS); } void UInputDescriptor::EnableAxis(int axis, int rangeMin, int rangeMax) { if ((axis < 0) || (axis >= NELEM(mUinputDescriptor.absmin))) { ALOGE("Invalid axis number: %d", axis); return; } if (ioctl(mFd, UI_SET_ABSBIT, axis) != 0) { ALOGE("Failed to set absbit for %d", axis); } mUinputDescriptor.absmin[axis] = rangeMin; mUinputDescriptor.absmax[axis] = rangeMax; mUinputDescriptor.absfuzz[axis] = 0; mUinputDescriptor.absflat[axis] = 0; } bool UInputDescriptor::Create() { // register the input device if (write(mFd, &mUinputDescriptor, sizeof(mUinputDescriptor)) != sizeof(mUinputDescriptor)) { ALOGE("Cannot write uinput_user_dev to fd %d: %s.", mFd, strerror(errno)); return false; } if (ioctl(mFd, UI_DEV_CREATE) != 0) { ALOGE("Unable to create uinput device: %s.", strerror(errno)); return false; } ALOGV("Created uinput device, fd=%d.", mFd); return true; } class NativeConnection { public: enum class ConnectionType { kRemoteDevice, kGamepadDevice, }; ~NativeConnection(); static NativeConnection* open(const char* name, const char* uniqueId, int32_t width, int32_t height, int32_t maxPointerId); static NativeConnection* openGamepad(const char* name, const char* uniqueId); void sendEvent(int32_t type, int32_t code, int32_t value); int32_t getMaxPointers() const { return mMaxPointers; } ConnectionType getType() const { return mType; } bool IsGamepad() const { return getType() == ConnectionType::kGamepadDevice; } bool IsRemote() const { return getType() == ConnectionType::kRemoteDevice; } private: NativeConnection(int fd, int32_t maxPointers); NativeConnection(int fd, int32_t maxPointers, ConnectionType type); const int mFd; const int32_t mMaxPointers; const ConnectionType mType; }; NativeConnection::NativeConnection(int fd, int32_t maxPointers) : mFd(fd), mMaxPointers(maxPointers) { } NativeConnection::NativeConnection(int fd, int32_t maxPointers, ConnectionType type) : mFd(fd), mMaxPointers(maxPointers), mType(type) {} NativeConnection::~NativeConnection() { ALOGI("Un-Registering uinput device %d.", mFd); Loading @@ -125,44 +255,50 @@ NativeConnection* NativeConnection::open(const char* name, const char* uniqueId, ALOGI("Registering uinput device %s: touch pad size %dx%d, " "max pointers %d.", name, width, height, maxPointers); int fd = ::open("/dev/uinput", O_WRONLY | O_NDELAY); if (fd < 0) { ALOGE("Cannot open /dev/uinput: %s.", strerror(errno)); initKeysMap(); UInputDescriptor descriptor; if (!descriptor.Open(name, uniqueId)) { return nullptr; } struct uinput_user_dev uinp; memset(&uinp, 0, sizeof(struct uinput_user_dev)); strlcpy(uinp.name, name, UINPUT_MAX_NAME_SIZE); uinp.id.version = 1; uinp.id.bustype = BUS_VIRTUAL; // set the keys mapped for (size_t i = 0; i < NELEM(KEYS); i++) { descriptor.EnableKey(KEYS[i].linuxKeyCode); } // initialize keymap initKeysMap(); if (!descriptor.Create()) { return nullptr; } // write device unique id to the phys property ioctl(fd, UI_SET_PHYS, uniqueId); return new NativeConnection(descriptor.Detach(), maxPointers, ConnectionType::kRemoteDevice); } // set the keys mapped ioctl(fd, UI_SET_EVBIT, EV_KEY); for (size_t i = 0; i < NELEM(KEYS); i++) { ioctl(fd, UI_SET_KEYBIT, KEYS[i].linuxKeyCode); NativeConnection* NativeConnection::openGamepad(const char* name, const char* uniqueId) { ALOGI("Registering uinput device %s: gamepad", name); UInputDescriptor descriptor; if (!descriptor.Open(name, uniqueId)) { return nullptr; } // register the input device if (write(fd, &uinp, sizeof(uinp)) != sizeof(uinp)) { ALOGE("Cannot write uinput_user_dev to fd %d: %s.", fd, strerror(errno)); close(fd); return NULL; // set the keys mapped for gamepads for (size_t i = 0; i < NELEM(GAMEPAD_KEY_CODES); i++) { descriptor.EnableKey(GAMEPAD_KEY_CODES[i]); } if (ioctl(fd, UI_DEV_CREATE) != 0) { ALOGE("Unable to create uinput device: %s.", strerror(errno)); close(fd); // define the axes that are required descriptor.EnableAxesEvents(); for (size_t i = 0; i < NELEM(GAMEPAD_AXES); i++) { const Axis& axis = GAMEPAD_AXES[i]; descriptor.EnableAxis(axis.number, axis.rangeMin, axis.rangeMax); } if (!descriptor.Create()) { return nullptr; } ALOGV("Created uinput device, fd=%d.", fd); return new NativeConnection(fd, maxPointers); return new NativeConnection(descriptor.Detach(), 0, ConnectionType::kGamepadDevice); } void NativeConnection::sendEvent(int32_t type, int32_t code, int32_t value) { Loading @@ -174,7 +310,6 @@ void NativeConnection::sendEvent(int32_t type, int32_t code, int32_t value) { write(mFd, &iev, sizeof(iev)); } static jlong nativeOpen(JNIEnv* env, jclass clazz, jstring nameStr, jstring uniqueIdStr, jint width, jint height, jint maxPointers) { Loading @@ -186,6 +321,14 @@ static jlong nativeOpen(JNIEnv* env, jclass clazz, return reinterpret_cast<jlong>(connection); } static jlong nativeGamepadOpen(JNIEnv* env, jclass clazz, jstring nameStr, jstring uniqueIdStr) { ScopedUtfChars name(env, nameStr); ScopedUtfChars uniqueId(env, uniqueIdStr); NativeConnection* connection = NativeConnection::openGamepad(name.c_str(), uniqueId.c_str()); return reinterpret_cast<jlong>(connection); } static void nativeClose(JNIEnv* env, jclass clazz, jlong ptr) { NativeConnection* connection = reinterpret_cast<NativeConnection*>(ptr); delete connection; Loading @@ -194,6 +337,12 @@ static void nativeClose(JNIEnv* env, jclass clazz, jlong ptr) { static void nativeSendKey(JNIEnv* env, jclass clazz, jlong ptr, jint keyCode, jboolean down) { int32_t code = getLinuxKeyCode(keyCode); NativeConnection* connection = reinterpret_cast<NativeConnection*>(ptr); if (connection->IsGamepad()) { ALOGE("Invalid key even for a gamepad - need to send gamepad events"); return; } if (code != KEY_UNKNOWN) { connection->sendEvent(EV_KEY, code, down ? 1 : 0); } else { Loading @@ -201,10 +350,44 @@ static void nativeSendKey(JNIEnv* env, jclass clazz, jlong ptr, jint keyCode, jb } } static void nativeSendGamepadKey(JNIEnv* env, jclass clazz, jlong ptr, jint keyIndex, jboolean down) { NativeConnection* connection = reinterpret_cast<NativeConnection*>(ptr); if (!connection->IsGamepad()) { ALOGE("Invalid gamepad key for non-gamepad device"); return; } if ((keyIndex < 0) || (keyIndex >= NELEM(GAMEPAD_KEY_CODES))) { ALOGE("Invalid gamepad key index: %d", keyIndex); return; } connection->sendEvent(EV_KEY, GAMEPAD_KEY_CODES[keyIndex], down ? 1 : 0); } static void nativeSendGamepadAxisValue(JNIEnv* env, jclass clazz, jlong ptr, jint axis, jint value) { NativeConnection* connection = reinterpret_cast<NativeConnection*>(ptr); if (!connection->IsGamepad()) { ALOGE("Invalid axis send for non-gamepad device"); return; } connection->sendEvent(EV_ABS, axis, value); } static void nativeSendPointerDown(JNIEnv* env, jclass clazz, jlong ptr, jint pointerId, jint x, jint y) { NativeConnection* connection = reinterpret_cast<NativeConnection*>(ptr); if (connection->IsGamepad()) { ALOGE("Invalid pointer down event for a gamepad."); return; } int32_t slot = findSlot(pointerId); if (slot == SLOT_UNKNOWN) { slot = assignSlot(pointerId); Loading @@ -221,6 +404,11 @@ static void nativeSendPointerUp(JNIEnv* env, jclass clazz, jlong ptr, jint pointerId) { NativeConnection* connection = reinterpret_cast<NativeConnection*>(ptr); if (connection->IsGamepad()) { ALOGE("Invalid pointer up event for a gamepad."); return; } int32_t slot = findSlot(pointerId); if (slot != SLOT_UNKNOWN) { connection->sendEvent(EV_ABS, ABS_MT_SLOT, slot); Loading @@ -238,6 +426,7 @@ static void nativeClear(JNIEnv* env, jclass clazz, jlong ptr) { NativeConnection* connection = reinterpret_cast<NativeConnection*>(ptr); // Clear keys. if (connection->IsRemote()) { for (size_t i = 0; i < NELEM(KEYS); i++) { connection->sendEvent(EV_KEY, KEYS[i].linuxKeyCode, 0); } Loading @@ -251,6 +440,22 @@ static void nativeClear(JNIEnv* env, jclass clazz, jlong ptr) { connection->sendEvent(EV_ABS, ABS_MT_TRACKING_ID, -1); } } } else { for (size_t i = 0; i < NELEM(GAMEPAD_KEY_CODES); i++) { connection->sendEvent(EV_KEY, GAMEPAD_KEY_CODES[i], 0); } for (size_t i = 0; i < NELEM(GAMEPAD_AXES); i++) { const Axis& axis = GAMEPAD_AXES[i]; if ((axis.number == ABS_Z) || (axis.number == ABS_RZ)) { // Mark triggers unpressed connection->sendEvent(EV_ABS, axis.number, 0); } else { // Joysticks and dpad rests on center connection->sendEvent(EV_ABS, axis.number, (axis.rangeMin + axis.rangeMax) / 2); } } } // Sync pointer events connection->sendEvent(EV_SYN, SYN_REPORT, 0); Loading @@ -261,20 +466,16 @@ static void nativeClear(JNIEnv* env, jclass clazz, jlong ptr) { */ static JNINativeMethod gUinputBridgeMethods[] = { { "nativeOpen", "(Ljava/lang/String;Ljava/lang/String;III)J", (void*)nativeOpen }, { "nativeClose", "(J)V", (void*)nativeClose }, { "nativeSendKey", "(JIZ)V", (void*)nativeSendKey }, { "nativeSendPointerDown", "(JIII)V", (void*)nativeSendPointerDown }, { "nativeSendPointerUp", "(JI)V", (void*)nativeSendPointerUp }, { "nativeClear", "(J)V", (void*)nativeClear }, { "nativeSendPointerSync", "(J)V", (void*)nativeSendPointerSync }, {"nativeOpen", "(Ljava/lang/String;Ljava/lang/String;III)J", (void*)nativeOpen}, {"nativeGamepadOpen", "(Ljava/lang/String;Ljava/lang/String;)J", (void*)nativeGamepadOpen}, {"nativeClose", "(J)V", (void*)nativeClose}, {"nativeSendKey", "(JIZ)V", (void*)nativeSendKey}, {"nativeSendPointerDown", "(JIII)V", (void*)nativeSendPointerDown}, {"nativeSendPointerUp", "(JI)V", (void*)nativeSendPointerUp}, {"nativeClear", "(J)V", (void*)nativeClear}, {"nativeSendPointerSync", "(J)V", (void*)nativeSendPointerSync}, {"nativeSendGamepadKey", "(JIZ)V", (void*)nativeSendGamepadKey}, {"nativeSendGamepadAxisValue", "(JII)V", (void*)nativeSendGamepadAxisValue}, }; int register_android_server_tv_TvUinputBridge(JNIEnv* env) { Loading