diff --git a/proto/protocol.xml b/proto/protocol.xml
index c01c800..6d74daf 100644
--- a/proto/protocol.xml
+++ b/proto/protocol.xml
@@ -612,6 +612,12 @@
It is a protocol violation to send this request for a client
of an ei_handshake.context_type other than sender.
+
+ It is up to the EIS implementation to reset the device state when a
+ stop_emulating event is received. The recommendation is that the device
+ is set to a neutral state such that all touches, buttons, keys are logically up.
+ A client should send the corresponding events before stop_emulating
+ to avoid any ambiguity on event interpretation.
@@ -1214,6 +1220,10 @@
It is a protocol violation to send this request for a client
of an ei_handshake.context_type other than sender.
+
+ A client should send a ei_button.button release event before
+ ei_device.stop_emulating to avoid any ambiguity on interpretation
+ of button events.
@@ -1290,6 +1300,10 @@
It is a protocol violation to send this request for a client
of an ei_handshake.context_type other than sender.
+
+ A client should send a ei_key.key release event before
+ ei_device.stop_emulating to avoid any ambiguity on interpretation
+ of key events.
@@ -1442,6 +1456,10 @@
It is a protocol violation to send a touch down in the same
frame as a touch motion or touch up.
+
+ A client should send a ei_touch.up or ei_touch.cancel event before
+ ei_device.stop_emulating to avoid any ambiguity on interpretation of touch
+ events.
diff --git a/src/libei.h b/src/libei.h
index 151b24e..70fbc06 100644
--- a/src/libei.h
+++ b/src/libei.h
@@ -396,13 +396,24 @@ enum ei_event_type {
/**
* Any events sent from this device will be discarded until the next
- * resume. The state of a device is not expected to change between
- * pause/resume - for any significant state changes the server is
- * expected to remove the device instead.
+ * resume.
+ *
+ * Pausing a device resets the logical state of the device to neutral.
+ * This includes:
+ * - any buttons or keys logically down are released
+ * - any modifiers logically down are released
+ * - any touches logically down are released
+ *
+ * Sender clients must wait until @ref EI_EVENT_DEVICE_RESUMED
+ * before sending events.
*/
EI_EVENT_DEVICE_PAUSED,
+
/**
* The client may send events.
+ *
+ * Once resumed, a sender client may call ei_device_start_emulating()
+ * and begin emulating events.
*/
EI_EVENT_DEVICE_RESUMED,
diff --git a/src/libeis-device.c b/src/libeis-device.c
index a7bf00d..a2345fb 100644
--- a/src/libeis-device.c
+++ b/src/libeis-device.c
@@ -25,6 +25,7 @@
#include "config.h"
#include
+#include
#include "util-macros.h"
#include "util-bits.h"
@@ -428,6 +429,13 @@ client_msg_button(struct eis_button *button, uint32_t btn, uint32_t state)
"Button event for non-button device");
}
+ if (btn >= KEY_CNT)
+ return brei_result_new(EIS_CONNECTION_DISCONNECT_REASON_PROTOCOL,
+ "Button event for invalid button %x (KEY_CNT is %#x)", btn, KEY_CNT);
+
+ if (!eis_device_update_key_button_state(device, btn, state))
+ return NULL;
+
if (device->state == EIS_DEVICE_STATE_EMULATING) {
eis_queue_pointer_button_event(device, btn, !!state);
return NULL;
@@ -598,6 +606,13 @@ client_msg_keyboard_key(struct eis_keyboard *keyboard, uint32_t key, uint32_t st
"Key event for non-keyboard device");
}
+ if (key >= KEY_CNT)
+ return brei_result_new(EIS_CONNECTION_DISCONNECT_REASON_PROTOCOL,
+ "Key event for invalid key %x (KEY_CNT is %#x)", key, KEY_CNT);
+
+ if (!eis_device_update_key_button_state(device, key, state))
+ return NULL;
+
if (device->state == EIS_DEVICE_STATE_EMULATING) {
eis_queue_keyboard_key_event(device, key, !!state);
return NULL;
@@ -627,6 +642,35 @@ eis_device_get_keyboard_interface(struct eis_device *device)
return &keyboard_interface;
}
+/* Returns true and the position of the touch with the given ID, or
+ * false and the first position that is available
+ */
+static bool
+find_touch(struct eis_device *device, uint32_t touchid, size_t *index)
+{
+ ssize_t first_available = -1;
+ for (size_t i = 0; i < ARRAY_LENGTH(device->touch_state.down); i++) {
+ if (device->touch_state.down[i] != UINT64_MAX) {
+ if (device->touch_state.down[i] == touchid) {
+ if (index)
+ *index = i;
+ return true;
+ }
+ } else if (first_available < 0) {
+ first_available = i;
+ }
+ }
+
+ if (index) {
+ if (first_available < 0)
+ *index = EIS_MAX_TOUCHES;
+ else
+ *index = (size_t)first_available;
+ }
+
+ return false;
+}
+
static struct brei_result *
client_msg_touch_down(struct eis_touchscreen *touchscreen,
uint32_t touchid, float x, float y)
@@ -641,6 +685,17 @@ client_msg_touch_down(struct eis_touchscreen *touchscreen,
}
if (device->state == EIS_DEVICE_STATE_EMULATING) {
+ size_t first_available;
+ if (find_touch(device, touchid, &first_available)) {
+ return brei_result_new(EIS_CONNECTION_DISCONNECT_REASON_PROTOCOL,
+ "Touch down event for duplicated touch ID");
+ }
+
+ if (first_available >= EIS_MAX_TOUCHES)
+ return brei_result_new(EIS_CONNECTION_DISCONNECT_REASON_ERROR,
+ "Too many simultaneous touch events");
+
+ device->touch_state.down[first_available] = touchid;
eis_queue_touch_down_event(device, touchid, x, y);
return NULL;
}
@@ -662,13 +717,26 @@ client_msg_touch_motion(struct eis_touchscreen *touchscreen,
}
if (device->state == EIS_DEVICE_STATE_EMULATING) {
- eis_queue_touch_motion_event(device, touchid, x, y);
+ /* Silently ignore motion for non-existing touches */
+ if (find_touch(device, touchid, NULL))
+ eis_queue_touch_motion_event(device, touchid, x, y);
return NULL;
}
return maybe_error_on_device_state(device, "touch motion");
}
+static bool
+release_touch(struct eis_device *device, uint32_t touchid)
+{
+ size_t index;
+ bool rc = find_touch(device, touchid, &index);
+ if (rc)
+ device->touch_state.down[index] = UINT64_MAX;
+
+ return rc;
+}
+
static struct brei_result *
client_msg_touch_up(struct eis_touchscreen *touchscreen, uint32_t touchid)
{
@@ -681,8 +749,11 @@ client_msg_touch_up(struct eis_touchscreen *touchscreen, uint32_t touchid)
"Touch up event for non-touch device");
}
- if (device->state == EIS_DEVICE_STATE_EMULATING) {
- eis_queue_touch_up_event(device, touchid);
+ /* End the touch locally even if we're not emulating, but
+ * silently ignore touch end/cancel for non-existing touches */
+ if (release_touch(device, touchid)) {
+ if (device->state == EIS_DEVICE_STATE_EMULATING)
+ eis_queue_touch_up_event(device, touchid);
return NULL;
}
@@ -707,8 +778,11 @@ client_msg_touch_cancel(struct eis_touchscreen *touchscreen, uint32_t touchid)
"Touch cancel event for touchscreen version v1");
}
- if (device->state == EIS_DEVICE_STATE_EMULATING) {
- eis_queue_touch_cancel_event(device, touchid);
+ /* End the touch locally even if we're not emulating, but
+ * silently ignore touch end/cancel for non-existing touches */
+ if (release_touch(device, touchid)) {
+ if (device->state == EIS_DEVICE_STATE_EMULATING)
+ eis_queue_touch_cancel_event(device, touchid);
return NULL;
}
@@ -762,6 +836,10 @@ eis_seat_new_device(struct eis_seat *seat)
list_append(&seat->devices, &device->link);
+ for (size_t i = 0; i < ARRAY_LENGTH(device->touch_state.down); i++) {
+ device->touch_state.down[i] = UINT64_MAX;
+ }
+
return eis_device_ref(device);
}
@@ -1447,6 +1525,12 @@ eis_device_pause(struct eis_device *device)
device->state = EIS_DEVICE_STATE_PAUSED;
eis_device_event_paused(device, eis_client_get_next_serial(client));
+
+ memset(device->key_button_state.down, 0, sizeof(device->key_button_state.down));
+
+ for (size_t i = 0; i < ARRAY_LENGTH(device->touch_state.down); i++) {
+ device->touch_state.down[i] = UINT64_MAX;
+ }
}
_public_ void
diff --git a/src/libeis-device.h b/src/libeis-device.h
index 5f6f2a1..3d060a0 100644
--- a/src/libeis-device.h
+++ b/src/libeis-device.h
@@ -26,10 +26,16 @@
#include "libeis.h"
+#include "util-bits.h"
#include "util-object.h"
#include "util-list.h"
#include "brei-shared.h"
+#define KEY_MAX 0x2ffU
+#define KEY_CNT (KEY_MAX + 1)
+
+#define EIS_MAX_TOUCHES 16
+
enum eis_device_state {
EIS_DEVICE_STATE_NEW,
EIS_DEVICE_STATE_AWAITING_READY,
@@ -75,6 +81,13 @@ struct eis_device {
bool x_is_cancelled, y_is_cancelled;
} scroll_state;
+ struct {
+ unsigned char down[NCHARS(KEY_CNT)];
+ } key_button_state;
+
+ struct {
+ uint64_t down[EIS_MAX_TOUCHES]; /* touch id */
+ } touch_state;
};
struct eis_touch {
@@ -118,6 +131,21 @@ OBJECT_DECLARE_GETTER(eis_device, button_interface, const struct eis_button_inte
OBJECT_DECLARE_GETTER(eis_device, keyboard_interface, const struct eis_keyboard_interface *);
OBJECT_DECLARE_GETTER(eis_device, touchscreen_interface, const struct eis_touchscreen_interface *);
+static inline bool
+eis_device_update_key_button_state(struct eis_device *device, uint32_t key_btn, uint32_t state)
+{
+ if (state) {
+ if (bit_is_set(device->key_button_state.down, key_btn))
+ return false;
+ set_bit(device->key_button_state.down, key_btn);
+ } else {
+ if (!bit_is_set(device->key_button_state.down, key_btn))
+ return false;
+ clear_bit(device->key_button_state.down, key_btn);
+ }
+ return true;
+}
+
void
eis_device_set_client_keymap(struct eis_device *device,
enum eis_keymap_type type,
diff --git a/src/libeis.h b/src/libeis.h
index 17b2619..27ce2de 100644
--- a/src/libeis.h
+++ b/src/libeis.h
@@ -1349,11 +1349,12 @@ eis_device_remove(struct eis_device *device);
* a number of events from a device after it has been paused and must
* update its internal state accordingly.
*
- * Pause/resume should only be used for short-term event delaying, a client
- * will expect that the device's state has not changed between pause and
- * resume. Where a device's state changes on the EIS implementation side (e.g.
- * buttons or keys are forcibly released), the device should be removed and
- * re-added as new device.
+ * Pausing a device resets the logical state of the device to neutral.
+ * This includes:
+ * - any buttons or keys logically down are released
+ * - any modifiers logically down are released
+ * - any touches logically down are released
+ * No events will be sent for these releases back to a neutral state.
*
* @param device A connected device
*/
diff --git a/test/test-ei-device.c b/test/test-ei-device.c
index 261de35..782b8e4 100644
--- a/test/test-ei-device.c
+++ b/test/test-ei-device.c
@@ -390,10 +390,20 @@ MUNIT_TEST(test_ei_device_button_button)
struct ei_device *device = peck_ei_get_default_pointer(peck);
ei_device_button_button(device, BTN_LEFT, true);
ei_device_frame(device, peck_ei_now(peck));
+
+ /* double press is quietly ignored */
+ ei_device_button_button(device, BTN_LEFT, true);
+ ei_device_frame(device, peck_ei_now(peck));
+
ei_device_button_button(device, BTN_RIGHT, true);
ei_device_frame(device, peck_ei_now(peck));
ei_device_button_button(device, BTN_RIGHT, false);
ei_device_frame(device, peck_ei_now(peck));
+
+ /* double release is quietly ignored */
+ ei_device_button_button(device, BTN_RIGHT, false);
+ ei_device_frame(device, peck_ei_now(peck));
+
ei_device_button_button(device, BTN_LEFT, false);
ei_device_frame(device, peck_ei_now(peck));
}
@@ -421,6 +431,7 @@ MUNIT_TEST(test_ei_device_button_button)
munit_assert_int(eis_event_button_get_button(lu), ==, BTN_LEFT);
munit_assert_false(eis_event_button_get_is_press(lu));
+ peck_assert_no_eis_events(eis);
}
return MUNIT_OK;
@@ -439,6 +450,15 @@ MUNIT_TEST(test_ei_device_keyboard_key)
struct ei_device *device = peck_ei_get_default_keyboard(peck);
ei_device_keyboard_key(device, KEY_Q, true);
ei_device_frame(device, peck_ei_now(peck));
+
+ /* Double press is quietly ignored */
+ ei_device_keyboard_key(device, KEY_Q, true);
+ ei_device_frame(device, peck_ei_now(peck));
+
+ ei_device_keyboard_key(device, KEY_Q, false);
+ ei_device_frame(device, peck_ei_now(peck));
+
+ /* Double release is quietly ignored */
ei_device_keyboard_key(device, KEY_Q, false);
ei_device_frame(device, peck_ei_now(peck));
}
@@ -1503,6 +1523,96 @@ MUNIT_TEST(test_ei_device_multitouch)
return MUNIT_OK;
}
+MUNIT_TEST(test_ei_device_touch_up_after_paused)
+{
+ _unref_(peck) *peck = peck_new();
+ _unref_(ei_device) *device = NULL;
+ _unref_(eis_device) *eis_device = NULL;
+
+ peck_enable_eis_behavior(peck, PECK_EIS_BEHAVIOR_ACCEPT_ALL);
+ peck_enable_eis_behavior(peck, PECK_EIS_BEHAVIOR_ADD_TOUCH);
+ peck_enable_ei_behavior(peck, PECK_EI_BEHAVIOR_AUTODEVICES);
+ peck_dispatch_until_stable(peck);
+
+ _unref_(ei_touch) *t1 = NULL;
+ _unref_(ei_touch) *t2 = NULL;
+
+ with_client(peck) {
+ device = ei_device_ref(peck_ei_get_default_touch(peck));
+ t1 = ei_device_touch_new(device);
+ t2 = ei_device_touch_new(device);
+ ei_touch_down(t1, 1, 2);
+ ei_touch_down(t2, 3, 4);
+ ei_device_frame(device, peck_ei_now(peck));
+ }
+
+ peck_dispatch_until_stable(peck);
+
+ with_server(peck) {
+ _unref_(eis_event) *down1 = peck_eis_touch_down(eis, 1, 2);
+ _unref_(eis_event) *down2 = peck_eis_touch_down(eis, 3, 4);
+
+ peck_assert_no_eis_events(eis); /* drain the frame */
+
+ eis_device = eis_device_ref(eis_event_get_device(down1));
+ eis_device_pause(eis_device);
+ }
+
+ /* No ei dispatch here */
+ peck_dispatch_eis(peck);
+
+ with_client(peck) {
+ /* These events will arrive when the device is paused and
+ * are discarded by EIS */
+ ei_touch_up(t1);
+ ei_touch_up(t2);
+ ei_device_frame(device, peck_ei_now(peck));
+ ei_touch_unref(t1);
+ ei_touch_unref(t2);
+ }
+
+ peck_dispatch_until_stable(peck);
+
+ with_client(peck) {
+ _unref_(ei_event) *pause = peck_ei_next_event(ei, EI_EVENT_DEVICE_PAUSED);
+ }
+
+ with_server(peck) {
+ /* touch up and empty frame were discarded */
+ peck_assert_no_eis_events(eis);
+ eis_device_resume(eis_device);
+ }
+
+ peck_dispatch_until_stable(peck);
+
+ with_client(peck) {
+ ei_device_start_emulating(device, 123);
+
+ /* The C API doesn't allow us to set a touch id
+ * so we can't really test for the correct behavior.
+ * All we can do is exercise most of the code by creating
+ * new touches and hope.
+ */
+ t1 = ei_device_touch_new(device);
+ t2 = ei_device_touch_new(device);
+ ei_touch_down(t1, 1, 2);
+ ei_touch_down(t2, 3, 4);
+ ei_device_frame(device, peck_ei_now(peck));
+ }
+
+ peck_dispatch_until_stable(peck);
+
+ with_server(peck) {
+ _unref_(eis_event) *down1 = peck_eis_touch_down(eis, 1, 2);
+ _unref_(eis_event) *down2 = peck_eis_touch_down(eis, 3, 4);
+ peck_assert_no_eis_events(eis); /* drain the frame */
+ }
+
+ peck_dispatch_until_stable(peck);
+
+ return MUNIT_OK;
+}
+
#if HAVE_MEMFD_CREATE
MUNIT_TEST(test_ei_keymap_invalid)
{
diff --git a/test/test_protocol.py b/test/test_protocol.py
index 98d2f14..6c2e5c6 100644
--- a/test/test_protocol.py
+++ b/test/test_protocol.py
@@ -1259,3 +1259,90 @@ class TestEiProtocol:
else:
ei.callback_roundtrip()
assert status.disconnected is False
+
+ def test_touch_disconnect_on_duplicate_id(self, eis):
+ """
+ Ensure EIS disconnects us when we use a duplicate touch id
+ """
+
+ ei = eis.ei
+
+ @dataclass
+ class Status:
+ device: EiDevice = None
+ touchscreen: Optional[EiTouchscreen] = None
+ disconnected: bool = False
+ resumed: bool = False
+ serial: int = 0
+
+ status = Status()
+
+ def on_interface(device, object, name, version, new_objects):
+ logger.debug(
+ "new capability",
+ device=device,
+ object=object,
+ name=name,
+ version=version,
+ )
+ if name == InterfaceName.EI_TOUCHSCREEN:
+ assert status.touchscreen is None
+ status.touchscreen = new_objects["object"]
+
+ def on_device_resumed(device, serial):
+ status.resumed = True
+ status.serial = serial
+
+ def on_new_device(seat, device, version, new_objects):
+ logger.debug("new device", object=new_objects["device"])
+ status.device = new_objects["device"]
+ status.device.connect("Interface", on_interface)
+ status.device.connect("Resumed", on_device_resumed)
+
+ def on_new_object(o: Interface):
+ logger.debug("new object", object=o)
+ if o.name == InterfaceName.EI_SEAT:
+ ei.seat_fill_capability_masks(o)
+ o.connect("Device", on_new_device)
+
+ ei.context.connect("register", on_new_object)
+ ei.dispatch()
+
+ def on_disconnected(connection, last_serial, reason, explanation):
+ status.disconnected = True
+
+ def on_connection(setup, serial, id, version, new_objects={}):
+ connection = new_objects["connection"]
+ connection.connect("Disconnected", on_disconnected)
+
+ setup = ei.handshake
+ setup.connect("Connection", on_connection)
+ ei.init_default_sender_connection(interface_versions={"ei_touchscreen": 1})
+
+ ei.wait_for_seat()
+ seat = ei.seats[0]
+ ei.send(seat.Bind(seat.bind_mask([InterfaceName.EI_TOUCHSCREEN])))
+ ei.wait_for(lambda: status.touchscreen and status.resumed)
+
+ assert status.touchscreen is not None
+
+ ei.send(status.device.StartEmulating(status.serial, 123))
+ logger.debug("Sending touch events")
+ touchid = 1
+ touchscreen = status.touchscreen
+ device = status.device
+ ei.send(touchscreen.Down(touchid, 10, 20))
+ ei.send(device.Frame(status.serial, int(time.time())))
+ ei.send(touchscreen.Motion(touchid, 10, 25))
+ ei.send(device.Frame(status.serial, int(time.time())))
+
+ ei.send(touchscreen.Down(touchid, 10, 20))
+ try:
+ ei.send(device.Frame(status.serial, int(time.time())))
+ except BrokenPipeError:
+ pass
+
+ ei.dispatch()
+ ei.wait_for(lambda: status.disconnected)
+
+ assert status.disconnected is True