背景
前文说到需求,需要在系统应用里模拟手柄摇杆和按键。之前已经通过模拟KeyEvent实现按键,模拟MotionEvent实现摇杆大致实现了这个功能,但是遇到新的问题,有些游戏不识别按键,尤其是多人云游戏。
解决方案
大致了解之后, KeyEvent和MotionEvent里有个deviceId字段,这个字段是用来表示不同的设备的。比如多人游戏,a手柄和b手柄的deviceId是不同的,游戏根据这个来区分不同的人做的操作,游戏可能是根据这个deviceId是否存在并且可以匹配,如果不存在则可能不处理这个事件。
-
第一想法是虚构一个deviceId,游戏都是通过InputDevice.getDeviceIds 来获取全部的,于是我反射了这个类的mInputDevices,但最后效果还是不可以,游戏还是无法识别。
-
搜索到Linux提供了/dev/uinput 可以用来虚拟外接设备。从底层虚拟输入设备。声明这个设备支持了摇杆和一些按键,操控实际的手柄。
#include "com_test_CommandDispatcher.h"
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <linux/uinput.h>
#include <linux/input.h>
#include "android/log.h"
static const char *TAG="uinput";
#define LOGI(fmt, args...) __android_log_print(ANDROID_LOG_INFO, TAG, fmt, ##args)
#define LOGD(fmt, args...) __android_log_print(ANDROID_LOG_DEBUG, TAG, fmt, ##args)
#define LOGE(fmt, args...) __android_log_print(ANDROID_LOG_ERROR, TAG, fmt, ##args)
#define KEY_CUSTOM_UP 0x20
#define KEY_CUSTOM_DOWN 0x30
static struct uinput_user_dev uinput_dev;
static int uinput_fd;
extern "C"
JNIEXPORT jint JNICALL
Java_com_test_CommandDispatcher_createUinput(
JNIEnv* env,
jobject /* this */,jstring name) {
int i;
int ret = 0;
const char *strContent = env->GetStringUTFChars(name, JNI_FALSE);
uinput_fd = open("/dev/uinput", O_RDWR | O_NDELAY);
if(uinput_fd < 0){
LOGE("%s:%d\n", __func__, __LINE__);
return -1;//error process.
}
//to set uinput dev
memset(&uinput_dev, 0, sizeof(struct uinput_user_dev));
strcpy(uinput_dev.name,strContent);
env->ReleaseStringUTFChars(name, strContent);
uinput_dev.id.version = 1;
//BUS_VIRTUAL BUS_USB时对于游戏来说这个设备是真实存在通过usb连接的
uinput_dev.id.bustype = BUS_USB;
uinput_dev.id.vendor = 0x1;
uinput_dev.id.product = 0x1;
//min 和 max必须不同
uinput_dev.absmin[ABS_X] = -1;
uinput_dev.absmax[ABS_X] = 1;
uinput_dev.absmin[ABS_Y] = -1;
uinput_dev.absmax[ABS_Y] = 1;
uinput_dev.absmin[ABS_Z] = -1;
uinput_dev.absmax[ABS_Z] = 1;
uinput_dev.absmin[ABS_RZ] = -1;
uinput_dev.absmax[ABS_RZ] = 1;
uinput_dev.absmin[ABS_THROTTLE] = -1;
uinput_dev.absmax[ABS_THROTTLE] = 1;
uinput_dev.absmin[ABS_BRAKE] = -1;
uinput_dev.absmax[ABS_BRAKE] = 1;
uinput_dev.absmin[ABS_HAT0X] = -1;
uinput_dev.absmax[ABS_HAT0X] = 1;
uinput_dev.absmin[ABS_HAT0Y] = -1;
uinput_dev.absmax[ABS_HAT0Y] = 1;
ioctl(uinput_fd, UI_SET_EVBIT, EV_SYN);
ioctl(uinput_fd, UI_SET_EVBIT, EV_KEY);
ioctl(uinput_fd, UI_SET_EVBIT, EV_MSC);
for(i = 0; i < 256; i++){
ioctl(uinput_fd, UI_SET_KEYBIT, i);
}
ioctl(uinput_fd, UI_SET_KEYBIT, BTN_THUMBL);
ioctl(uinput_fd, UI_SET_KEYBIT, BTN_THUMBR);
ioctl(uinput_fd, UI_SET_KEYBIT, BTN_X);
ioctl(uinput_fd, UI_SET_KEYBIT, BTN_A);
ioctl(uinput_fd, UI_SET_KEYBIT, BTN_Y);
ioctl(uinput_fd, UI_SET_KEYBIT, BTN_B);
ioctl(uinput_fd, UI_SET_KEYBIT, BTN_RIGHT);
ioctl(uinput_fd, UI_SET_KEYBIT, BTN_LEFT);
ioctl(uinput_fd, UI_SET_KEYBIT, BTN_SELECT);
ioctl(uinput_fd, UI_SET_KEYBIT, BTN_START);
ioctl(uinput_fd, UI_SET_KEYBIT, BTN_DPAD_LEFT);
ioctl(uinput_fd, UI_SET_KEYBIT, BTN_DPAD_RIGHT);
ioctl(uinput_fd, UI_SET_KEYBIT, BTN_DPAD_UP);
ioctl(uinput_fd, UI_SET_KEYBIT, BTN_DPAD_DOWN);
ioctl(uinput_fd, UI_SET_EVBIT, EV_ABS);
ioctl(uinput_fd, UI_SET_ABSBIT, ABS_HAT0X);
ioctl(uinput_fd, UI_SET_ABSBIT, ABS_HAT0Y);
ioctl(uinput_fd, UI_SET_ABSBIT, ABS_X);
ioctl(uinput_fd, UI_SET_ABSBIT, ABS_Y);
ioctl(uinput_fd, UI_SET_ABSBIT, ABS_Z);
ioctl(uinput_fd, UI_SET_ABSBIT, ABS_RZ);
ioctl(uinput_fd, UI_SET_ABSBIT, ABS_THROTTLE);
ioctl(uinput_fd, UI_SET_ABSBIT, ABS_BRAKE);
ioctl(uinput_fd, UI_SET_MSCBIT, KEY_CUSTOM_UP);
ioctl(uinput_fd, UI_SET_MSCBIT, KEY_CUSTOM_DOWN);
ret = write(uinput_fd, &uinput_dev, sizeof(struct uinput_user_dev));
if(ret < 0){
LOGE("%s:%d\n", __func__, __LINE__);
return ret;//error process.
}
ret = ioctl(uinput_fd, UI_DEV_CREATE);
if(ret < 0){
LOGE("%s:%d\n", __func__, __LINE__);
close(uinput_fd);
return ret;//error process.
}
return 1;
}
JNIEXPORT void JNICALL Java_com_test_CommandDispatcher_closeUinput
(JNIEnv *, jobject) {
close(uinput_fd);
}
createUinput 传入的参数就是设备名,调用方法后,
通过InputDevice.getDeviceIds()拿到所有的deviceId,
通过InputDevice.getDevice(deviceId) 拿到device,
判断device.getName和设备名一致时,device.getId()就是deviceId.
后续模拟事件都使用该id即可。
其他
这种在native层通过写/dev/uinput虚拟出设备, 然后在Android层模拟各种事件实现其实是比较取巧的办法,虚拟出设备后,其实是可以往这个设备节点写数据,达到模拟输入的效果的。参考下面第二个文档。