Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 4ca0163c authored by Andrei Litvin's avatar Andrei Litvin
Browse files

Defines Gamepad-specific keys and axis in UinputBridge.

Adds the ability to open gamepads in the JNI layer of server.tv.
Major changes:
  - defined gamepad specific keys and axes
  - centralized /dev/uinput file descriptor management during the
    open method (since gamepad and remote have similar open logic)
  - added gamepad open, keysend and axis send methods.

Test: Android remote service still works on a clean build & flash.
     (tested remote service on my phone and UI navigation).
Test: as part of a larger chage chain, opened gamepad devices
      and observed /dev/input/event* behaviour for axes and keys.
Bug: 150775713
Bug: 150776008
Bug: 150784056
Change-Id: I2eee6844dd85346753ec62430c234cc7e0179d3b
parent 25eee749
Loading
Loading
Loading
Loading
+70 −4
Original line number Diff line number Diff line
@@ -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);
@@ -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) {
@@ -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;
@@ -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) {
+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_
+1 −1
Original line number Diff line number Diff line
@@ -110,4 +110,4 @@ static Key KEYS[] = {

} // namespace android

#endif // SERVICE_JNI_KEYS_H_
#endif // ANDROIDTVREMOTE_SERVICE_JNI_KEYS_H_
+256 −55
Original line number Diff line number Diff line
@@ -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"
@@ -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);
@@ -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) {
@@ -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) {
@@ -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;
@@ -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 {
@@ -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);
@@ -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);
@@ -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);
        }
@@ -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);
@@ -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) {