Track existing pointers on the virtual touchscreen.

The way we write touch input events use to be stateless, when an action
UP events on a pointer is received, we convert it to a BTN_TOUCH UP
event with that pointer id. However, in multi-touch protocol, an BTN up
is only sent when there's no pointers in touch whereas the lift of a
single pointer is marked by setting tracking id to -1.

This change added a map from the touchscreen to its existing touches, so
that we only send the BTN up when the corresponding entry becomes empty.

Test: atest VirtualTouchscreenTest
Bug: 256172250
Change-Id: I7df865288c0e180db8c6f81c4ad2505913d56492
This commit is contained in:
Zixuan Qu
2023-01-20 18:23:21 +00:00
parent c3df8d6a04
commit fb58ee746a

View File

@@ -29,8 +29,17 @@
#include <utils/Log.h>
#include <map>
#include <set>
#include <string>
/**
* Log debug messages about native virtual input devices.
* Enable this via "adb shell setprop log.tag.InputController DEBUG"
*/
static bool isDebug() {
return __android_log_is_loggable(ANDROID_LOG_DEBUG, LOG_TAG, ANDROID_LOG_INFO);
}
namespace android {
enum class DeviceType {
@@ -194,6 +203,15 @@ static std::map<int, int> KEY_CODE_MAPPING = {
{AKEYCODE_NUMPAD_COMMA, KEY_KPCOMMA},
};
/*
* Map from the uinput touchscreen fd to the pointers present in the previous touch events that
* hasn't been lifted.
* We only allow pointer id to go up to MAX_POINTERS because the maximum slots of virtual
* touchscreen is set up with MAX_POINTERS. Note that in other cases Android allows pointer id to go
* up to MAX_POINTERS_ID.
*/
static std::map<int32_t, std::bitset<MAX_POINTERS>> unreleasedTouches;
/** Creates a new uinput device and assigns a file descriptor. */
static int openUinput(const char* readableName, jint vendorId, jint productId, const char* phys,
DeviceType deviceType, jint screenHeight, jint screenWidth) {
@@ -366,6 +384,12 @@ static int nativeOpenUinputTouchscreen(JNIEnv* env, jobject thiz, jstring name,
static bool nativeCloseUinput(JNIEnv* env, jobject thiz, jint fd) {
ioctl(fd, UI_DEV_DESTROY);
if (auto touchesOnFd = unreleasedTouches.find(fd); touchesOnFd != unreleasedTouches.end()) {
const size_t remainingPointers = touchesOnFd->second.size();
unreleasedTouches.erase(touchesOnFd);
ALOGW_IF(remainingPointers > 0, "Closing touchscreen %d, erased %zu unreleased pointers.",
fd, remainingPointers);
}
return close(fd);
}
@@ -425,6 +449,69 @@ static bool nativeWriteButtonEvent(JNIEnv* env, jobject thiz, jint fd, jint butt
return true;
}
static bool handleTouchUp(int fd, int pointerId) {
if (!writeInputEvent(fd, EV_ABS, ABS_MT_TRACKING_ID, static_cast<int32_t>(-1))) {
return false;
}
auto touchesOnFd = unreleasedTouches.find(fd);
if (touchesOnFd == unreleasedTouches.end()) {
ALOGE("PointerId %d action UP received with no prior events on touchscreen %d.", pointerId,
fd);
return false;
}
ALOGD_IF(isDebug(), "Unreleased touches found for touchscreen %d in the map", fd);
// When a pointer is no longer in touch, remove the pointer id from the corresponding
// entry in the unreleased touches map.
if (pointerId < 0 || pointerId >= MAX_POINTERS) {
ALOGE("Virtual touch event has invalid pointer id %d; value must be between 0 and %zu",
pointerId, MAX_POINTERS - 1);
return false;
}
if (!touchesOnFd->second.test(pointerId)) {
ALOGE("PointerId %d action UP received with no prior action DOWN on touchscreen %d.",
pointerId, fd);
return false;
}
touchesOnFd->second.reset(pointerId);
ALOGD_IF(isDebug(), "Pointer %d erased from the touchscreen %d", pointerId, fd);
// Only sends the BTN UP event when there's no pointers on the touchscreen.
if (touchesOnFd->second.none()) {
unreleasedTouches.erase(touchesOnFd);
if (!writeInputEvent(fd, EV_KEY, BTN_TOUCH, static_cast<int32_t>(UinputAction::RELEASE))) {
return false;
}
ALOGD_IF(isDebug(), "No pointers on touchscreen %d, BTN UP event sent.", fd);
}
return true;
}
static bool handleTouchDown(int fd, int pointerId) {
// When a new pointer is down on the touchscreen, add the pointer id in the corresponding
// entry in the unreleased touches map.
auto touchesOnFd = unreleasedTouches.find(fd);
if (touchesOnFd == unreleasedTouches.end()) {
// Only sends the BTN Down event when the first pointer on the touchscreen is down.
if (!writeInputEvent(fd, EV_KEY, BTN_TOUCH, static_cast<int32_t>(UinputAction::PRESS))) {
return false;
}
touchesOnFd = unreleasedTouches.insert({fd, {}}).first;
ALOGD_IF(isDebug(), "New touchscreen with fd %d added in the unreleased touches map.", fd);
}
if (touchesOnFd->second.test(pointerId)) {
ALOGE("Repetitive action DOWN event received on a pointer %d that is already down.",
pointerId);
return false;
}
touchesOnFd->second.set(pointerId);
ALOGD_IF(isDebug(), "Added pointer %d under touchscreen %d in the map", pointerId, fd);
if (!writeInputEvent(fd, EV_ABS, ABS_MT_TRACKING_ID, static_cast<int32_t>(pointerId))) {
return false;
}
return true;
}
static bool nativeWriteTouchEvent(JNIEnv* env, jobject thiz, jint fd, jint pointerId, jint toolType,
jint action, jfloat locationX, jfloat locationY, jfloat pressure,
jfloat majorAxisSize) {
@@ -446,15 +533,10 @@ static bool nativeWriteTouchEvent(JNIEnv* env, jobject thiz, jint fd, jint point
return false;
}
UinputAction uinputAction = actionIterator->second;
if (uinputAction == UinputAction::PRESS || uinputAction == UinputAction::RELEASE) {
if (!writeInputEvent(fd, EV_KEY, BTN_TOUCH, static_cast<int32_t>(uinputAction))) {
return false;
}
if (!writeInputEvent(fd, EV_ABS, ABS_MT_TRACKING_ID,
static_cast<int32_t>(uinputAction == UinputAction::PRESS ? pointerId
: -1))) {
return false;
}
if (uinputAction == UinputAction::PRESS && !handleTouchDown(fd, pointerId)) {
return false;
} else if (uinputAction == UinputAction::RELEASE && !handleTouchUp(fd, pointerId)) {
return false;
}
if (!writeInputEvent(fd, EV_ABS, ABS_MT_POSITION_X, locationX)) {
return false;