Files
grd/grd-rdp-dvc-input.c
2026-02-13 13:06:50 +09:00

765 lines
24 KiB
C

/*
* Copyright (C) 2025 Pascal Nowack
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation; either version 2 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
* 02111-1307, USA.
*/
#include "config.h"
#include "grd-rdp-dvc-input.h"
#include <freerdp/server/rdpei.h>
#include "grd-rdp-layout-manager.h"
#define MAX_TOUCH_CONTACTS 256
typedef enum
{
INPUT_EVENT_TYPE_TOUCH,
INPUT_EVENT_TYPE_DISMISS_HOVERING_TOUCH_CONTACT,
INPUT_EVENT_TYPE_PEN,
} InputEventType;
/*
* Contact states based on Touch Contact State Transitions
* ([MS-RDPEI] 3.1.1.1)
*/
typedef enum
{
CONTACT_STATE_OUT_OF_RANGE = 0,
CONTACT_STATE_HOVERING,
CONTACT_STATE_ENGAGED,
} ContactState;
typedef struct
{
InputEventType event_type;
/* INPUT_EVENT_TYPE_TOUCH */
RDPINPUT_TOUCH_FRAME touch_frame;
/* INPUT_EVENT_TYPE_DISMISS_HOVERING_TOUCH_CONTACT */
uint8_t touch_contact_to_dismiss;
/* INPUT_EVENT_TYPE_PEN */
/* Missing libei-API to submit (multi-)pen events */
} InputEvent;
typedef struct
{
ContactState contact_state;
GrdTouchContact *touch_contact;
gboolean ignore_contact;
} TouchContext;
struct _GrdRdpDvcInput
{
GrdRdpDvc parent;
RdpeiServerContext *rdpei_context;
gboolean channel_opened;
GrdRdpLayoutManager *layout_manager;
GrdSession *session;
GSource *event_source;
GAsyncQueue *event_queue;
gboolean pending_touch_device_frame;
GList *touch_contacts_to_dispose;
TouchContext touch_contexts[MAX_TOUCH_CONTACTS];
unsigned long touch_device_added_id;
unsigned long touch_device_removed_id;
};
G_DEFINE_TYPE (GrdRdpDvcInput, grd_rdp_dvc_input,
GRD_TYPE_RDP_DVC)
static void
grd_rdp_dvc_input_maybe_init (GrdRdpDvc *dvc)
{
GrdRdpDvcInput *input = GRD_RDP_DVC_INPUT (dvc);
RdpeiServerContext *rdpei_context;
if (input->channel_opened)
return;
rdpei_context = input->rdpei_context;
if (rdpei_context->Open (rdpei_context))
{
g_warning ("[RDP.INPUT] Failed to open channel. "
"Terminating protocol");
grd_rdp_dvc_queue_channel_tear_down (GRD_RDP_DVC (input));
return;
}
input->channel_opened = TRUE;
}
static void
dvc_creation_status (gpointer user_data,
int32_t creation_status)
{
GrdRdpDvcInput *input = user_data;
RdpeiServerContext *rdpei_context = input->rdpei_context;
if (creation_status < 0)
{
g_debug ("[RDP.INPUT] Failed to open channel (CreationStatus %i). "
"Terminating protocol", creation_status);
grd_rdp_dvc_queue_channel_tear_down (GRD_RDP_DVC (input));
return;
}
rdpei_server_send_sc_ready (rdpei_context, RDPINPUT_PROTOCOL_V300,
SC_READY_MULTIPEN_INJECTION_SUPPORTED);
}
static BOOL
input_channel_id_assigned (RdpeiServerContext *rdpei_context,
uint32_t channel_id)
{
GrdRdpDvcInput *input = rdpei_context->user_data;
GrdRdpDvc *dvc = GRD_RDP_DVC (input);
g_debug ("[RDP.INPUT] DVC channel id assigned to id %u", channel_id);
grd_rdp_dvc_subscribe_creation_status (dvc, channel_id,
dvc_creation_status,
input);
return TRUE;
}
static uint32_t
input_client_ready (RdpeiServerContext *rdpei_context)
{
g_message ("[RDP.INPUT] Client version: 0x%08X, Flags: 0x%08X, "
"Maximum simultaneous touch contacts: %u",
rdpei_context->clientVersion, rdpei_context->protocolFlags,
rdpei_context->maxTouchPoints);
return CHANNEL_RC_OK;
}
static uint32_t
input_touch (RdpeiServerContext *rdpei_context,
const RDPINPUT_TOUCH_EVENT *touch_event)
{
GrdRdpDvcInput *input = rdpei_context->user_data;
uint16_t i;
for (i = 0; i < touch_event->frameCount; ++i)
{
RDPINPUT_TOUCH_FRAME *touch_frame = &touch_event->frames[i];
InputEvent *input_event;
if (touch_frame->contactCount == 0)
continue;
input_event = g_new0 (InputEvent, 1);
input_event->event_type = INPUT_EVENT_TYPE_TOUCH;
input_event->touch_frame = *touch_frame;
input_event->touch_frame.contacts =
g_memdup2 (touch_frame->contacts, touch_frame->contactCount *
sizeof (RDPINPUT_CONTACT_DATA));
g_async_queue_push (input->event_queue, input_event);
g_source_set_ready_time (input->event_source, 0);
}
return CHANNEL_RC_OK;
}
static uint32_t
input_dismiss_hovering_touch_contact (RdpeiServerContext *rdpei_context,
uint8_t contact_id)
{
GrdRdpDvcInput *input = rdpei_context->user_data;
InputEvent *input_event;
input_event = g_new0 (InputEvent, 1);
input_event->event_type = INPUT_EVENT_TYPE_DISMISS_HOVERING_TOUCH_CONTACT;
input_event->touch_contact_to_dismiss = contact_id;
g_async_queue_push (input->event_queue, input_event);
g_source_set_ready_time (input->event_source, 0);
return CHANNEL_RC_OK;
}
static uint32_t
input_pen (RdpeiServerContext *rdpei_context,
const RDPINPUT_PEN_EVENT *pen_event)
{
/* Missing libei-API to submit (multi-)pen events */
return CHANNEL_RC_OK;
}
static void
on_touch_device_added (GrdSession *session,
GrdRdpDvcInput *input)
{
g_source_set_ready_time (input->event_source, 0);
}
static void
on_touch_device_removed (GrdSession *session,
GrdRdpDvcInput *input)
{
uint16_t i;
for (i = 0; i < MAX_TOUCH_CONTACTS; ++i)
{
TouchContext *touch_context = &input->touch_contexts[i];
GrdTouchContact *touch_contact;
if (touch_context->contact_state == CONTACT_STATE_OUT_OF_RANGE)
continue;
g_debug ("[RDP.INPUT] Resetting touch contact %u", i);
touch_contact = g_steal_pointer (&touch_context->touch_contact);
grd_session_release_touch_contact (session, touch_contact);
touch_context->contact_state = CONTACT_STATE_OUT_OF_RANGE;
/*
* When the monitor layout changes, Windows App for the iPhone resets
* each contact state. This also applies to touch contacts, which are
* still pressed by the user.
* However, for some reason, this client still continues to send
* touch-motion events for the reset touch contact. The touch position
* in these events is always the same and does not reflect the actual
* reality.
* This continues to happen until the user taps again, in which case the
* client stops these spurious touch-motion events and sends a
* touch-down event.
* So, ignore these spurious touch-motion events until a valid
* contact-flag combination is sent for the 'Out of Range'-state.
*/
touch_context->ignore_contact = TRUE;
}
}
GrdRdpDvcInput *
grd_rdp_dvc_input_new (GrdRdpLayoutManager *layout_manager,
GrdSessionRdp *session_rdp,
GrdRdpDvcHandler *dvc_handler,
HANDLE vcm)
{
GrdRdpDvcInput *input;
RdpeiServerContext *rdpei_context;
input = g_object_new (GRD_TYPE_RDP_DVC_INPUT, NULL);
rdpei_context = rdpei_server_context_new (vcm);
if (!rdpei_context)
g_error ("[RDP.INPUT] Failed to allocate server context (OOM)");
input->rdpei_context = rdpei_context;
input->layout_manager = layout_manager;
input->session = GRD_SESSION (session_rdp);
grd_rdp_dvc_initialize_base (GRD_RDP_DVC (input),
dvc_handler, session_rdp,
GRD_RDP_CHANNEL_INPUT);
rdpei_context->onChannelIdAssigned = input_channel_id_assigned;
rdpei_context->onClientReady = input_client_ready;
rdpei_context->onTouchEvent = input_touch;
rdpei_context->onTouchReleased = input_dismiss_hovering_touch_contact;
rdpei_context->onPenEvent = input_pen;
rdpei_context->user_data = input;
input->touch_device_added_id =
g_signal_connect (input->session, "touch-device-added",
G_CALLBACK (on_touch_device_added),
input);
input->touch_device_removed_id =
g_signal_connect (input->session, "touch-device-removed",
G_CALLBACK (on_touch_device_removed),
input);
return input;
}
static void
ensure_queued_touch_device_frame (GrdRdpDvcInput *input)
{
input->pending_touch_device_frame = TRUE;
}
static void
queue_touch_contact_disposal (GrdRdpDvcInput *input,
TouchContext *touch_context)
{
GrdTouchContact *touch_contact;
g_assert (touch_context->touch_contact);
touch_contact = g_steal_pointer (&touch_context->touch_contact);
input->touch_contacts_to_dispose =
g_list_prepend (input->touch_contacts_to_dispose, touch_contact);
touch_context->ignore_contact = FALSE;
}
/*
* Automata based on Touch Contact State Transitions
* ([MS-RDPEI] 3.1.1.1)
*/
static void
handle_touch_state_out_of_range (GrdRdpDvcInput *input,
RDPINPUT_CONTACT_DATA *touch_contact_data)
{
uint8_t contact_id = touch_contact_data->contactId;
TouchContext *touch_context = &input->touch_contexts[contact_id];
GrdSession *session = input->session;
g_assert (!touch_context->touch_contact);
if (touch_contact_data->contactFlags == (RDPINPUT_CONTACT_FLAG_DOWN |
RDPINPUT_CONTACT_FLAG_INRANGE |
RDPINPUT_CONTACT_FLAG_INCONTACT))
{
GrdRdpLayoutManager *layout_manager = input->layout_manager;
GrdEventMotionAbs motion_abs = {};
GrdStream *stream = NULL;
touch_context->touch_contact =
grd_session_acquire_touch_contact (session);
touch_context->ignore_contact = FALSE;
if (grd_rdp_layout_manager_transform_position (layout_manager,
touch_contact_data->x,
touch_contact_data->y,
&stream, &motion_abs))
{
grd_session_notify_touch_down (session, touch_context->touch_contact,
stream, &motion_abs);
ensure_queued_touch_device_frame (input);
}
else
{
/*
* The client does not know, when a specific touch contact is useless
* due to its position not being transformable. As a result, it will
* deem it as any other touch contact. So ignore all assigned actions
* and just let the touch contact run through all automata states.
*
* See also 3.2.5.3 Processing an RDPINPUT_TOUCH_EVENT_PDU Message
* ([MS-RDPEI])
*/
touch_context->ignore_contact = TRUE;
}
touch_context->contact_state = CONTACT_STATE_ENGAGED;
}
else if (touch_contact_data->contactFlags == (RDPINPUT_CONTACT_FLAG_UPDATE |
RDPINPUT_CONTACT_FLAG_INRANGE))
{
touch_context->touch_contact =
grd_session_acquire_touch_contact (session);
touch_context->ignore_contact = FALSE;
touch_context->contact_state = CONTACT_STATE_HOVERING;
}
else if (!touch_context->ignore_contact)
{
g_warning ("[RDP.INPUT] Protocol violation: Client sent invalid contact "
"flags 0x%08X in state 'Out of Range' for contact %u",
touch_contact_data->contactFlags, contact_id);
}
}
/*
* Automata based on Touch Contact State Transitions
* ([MS-RDPEI] 3.1.1.1)
*/
static void
handle_touch_state_hovering (GrdRdpDvcInput *input,
RDPINPUT_CONTACT_DATA *touch_contact_data)
{
uint8_t contact_id = touch_contact_data->contactId;
TouchContext *touch_context = &input->touch_contexts[contact_id];
g_assert (touch_context->touch_contact);
if (touch_contact_data->contactFlags == (RDPINPUT_CONTACT_FLAG_DOWN |
RDPINPUT_CONTACT_FLAG_INRANGE |
RDPINPUT_CONTACT_FLAG_INCONTACT))
{
GrdRdpLayoutManager *layout_manager = input->layout_manager;
GrdSession *session = input->session;
GrdEventMotionAbs motion_abs = {};
GrdStream *stream = NULL;
if (grd_rdp_layout_manager_transform_position (layout_manager,
touch_contact_data->x,
touch_contact_data->y,
&stream, &motion_abs))
{
grd_session_notify_touch_down (session, touch_context->touch_contact,
stream, &motion_abs);
ensure_queued_touch_device_frame (input);
}
else
{
/*
* The client does not know, when a specific touch contact is useless
* due to its position not being transformable. As a result, it will
* deem it as any other touch contact. So ignore all assigned actions
* and just let the touch contact run through all automata states.
*
* See also 3.2.5.3 Processing an RDPINPUT_TOUCH_EVENT_PDU Message
* ([MS-RDPEI])
*/
touch_context->ignore_contact = TRUE;
}
touch_context->contact_state = CONTACT_STATE_ENGAGED;
}
else if (touch_contact_data->contactFlags == (RDPINPUT_CONTACT_FLAG_UPDATE |
RDPINPUT_CONTACT_FLAG_INRANGE))
{
touch_context->contact_state = CONTACT_STATE_HOVERING;
}
else if (touch_contact_data->contactFlags == (RDPINPUT_CONTACT_FLAG_UPDATE |
RDPINPUT_CONTACT_FLAG_CANCELED))
{
queue_touch_contact_disposal (input, touch_context);
touch_context->contact_state = CONTACT_STATE_OUT_OF_RANGE;
}
else if (touch_contact_data->contactFlags == RDPINPUT_CONTACT_FLAG_UPDATE)
{
queue_touch_contact_disposal (input, touch_context);
touch_context->contact_state = CONTACT_STATE_OUT_OF_RANGE;
}
else
{
g_warning ("[RDP.INPUT] Protocol violation: Client sent invalid contact "
"flags 0x%08X in state 'Hovering' for contact %u",
touch_contact_data->contactFlags, contact_id);
}
}
/*
* Automata based on Touch Contact State Transitions
* ([MS-RDPEI] 3.1.1.1)
*/
static void
handle_touch_state_engaged (GrdRdpDvcInput *input,
RDPINPUT_CONTACT_DATA *touch_contact_data)
{
uint8_t contact_id = touch_contact_data->contactId;
TouchContext *touch_context = &input->touch_contexts[contact_id];
GrdSession *session = input->session;
g_assert (touch_context->touch_contact);
if (touch_contact_data->contactFlags == (RDPINPUT_CONTACT_FLAG_UPDATE |
RDPINPUT_CONTACT_FLAG_INRANGE |
RDPINPUT_CONTACT_FLAG_INCONTACT))
{
GrdRdpLayoutManager *layout_manager = input->layout_manager;
GrdEventMotionAbs motion_abs = {};
GrdStream *stream = NULL;
if (touch_context->ignore_contact ||
!grd_rdp_layout_manager_transform_position (layout_manager,
touch_contact_data->x,
touch_contact_data->y,
&stream, &motion_abs))
return;
grd_session_notify_touch_motion (session, touch_context->touch_contact,
stream, &motion_abs);
ensure_queued_touch_device_frame (input);
touch_context->contact_state = CONTACT_STATE_ENGAGED;
}
else if (touch_contact_data->contactFlags == (RDPINPUT_CONTACT_FLAG_UP |
RDPINPUT_CONTACT_FLAG_INRANGE))
{
if (!touch_context->ignore_contact)
{
grd_session_notify_touch_up (session, touch_context->touch_contact);
ensure_queued_touch_device_frame (input);
}
queue_touch_contact_disposal (input, touch_context);
touch_context->touch_contact =
grd_session_acquire_touch_contact (session);
touch_context->contact_state = CONTACT_STATE_HOVERING;
}
else if (touch_contact_data->contactFlags == (RDPINPUT_CONTACT_FLAG_UP |
RDPINPUT_CONTACT_FLAG_CANCELED))
{
if (!touch_context->ignore_contact)
{
grd_session_notify_touch_cancel (session, touch_context->touch_contact);
ensure_queued_touch_device_frame (input);
}
queue_touch_contact_disposal (input, touch_context);
touch_context->contact_state = CONTACT_STATE_OUT_OF_RANGE;
}
else if (touch_contact_data->contactFlags == RDPINPUT_CONTACT_FLAG_UP)
{
if (!touch_context->ignore_contact)
{
grd_session_notify_touch_up (session, touch_context->touch_contact);
ensure_queued_touch_device_frame (input);
}
queue_touch_contact_disposal (input, touch_context);
touch_context->contact_state = CONTACT_STATE_OUT_OF_RANGE;
}
else
{
g_warning ("[RDP.INPUT] Protocol violation: Client sent invalid contact "
"flags 0x%08X in state 'Engaged' for contact %u",
touch_contact_data->contactFlags, contact_id);
}
}
static void
process_touch_contact (GrdRdpDvcInput *input,
RDPINPUT_CONTACT_DATA *touch_contact_data)
{
uint8_t contact_id = touch_contact_data->contactId;
TouchContext *touch_context = &input->touch_contexts[contact_id];
/*
* The optional contact-rect, orientation, and pressure fields are unhandled
* due to missing libei-API to submit them
*/
switch (touch_context->contact_state)
{
case CONTACT_STATE_OUT_OF_RANGE:
handle_touch_state_out_of_range (input, touch_contact_data);
break;
case CONTACT_STATE_HOVERING:
handle_touch_state_hovering (input, touch_contact_data);
break;
case CONTACT_STATE_ENGAGED:
handle_touch_state_engaged (input, touch_contact_data);
break;
}
}
static void
maybe_notify_touch_device_frame (GrdRdpDvcInput *input)
{
GrdSession *session = input->session;
if (!input->pending_touch_device_frame)
return;
grd_session_notify_touch_device_frame (session);
input->pending_touch_device_frame = FALSE;
}
static void
dispose_touch_contacts (GrdRdpDvcInput *input)
{
GrdSession *session = input->session;
GList *l;
for (l = input->touch_contacts_to_dispose; l; l = l->next)
{
GrdTouchContact *touch_contact = l->data;
grd_session_release_touch_contact (session, touch_contact);
}
g_clear_pointer (&input->touch_contacts_to_dispose, g_list_free);
}
static void
process_touch_frame (GrdRdpDvcInput *input,
RDPINPUT_TOUCH_FRAME *touch_frame)
{
uint32_t i;
for (i = 0; i < touch_frame->contactCount; ++i)
process_touch_contact (input, &touch_frame->contacts[i]);
maybe_notify_touch_device_frame (input);
dispose_touch_contacts (input);
}
static void
dismiss_touch_contact (GrdRdpDvcInput *input,
uint8_t contact_id)
{
TouchContext *touch_context = &input->touch_contexts[contact_id];
if (!touch_context->touch_contact)
g_assert (touch_context->contact_state == CONTACT_STATE_OUT_OF_RANGE);
/* Client did not keep track of touch contact properly */
if (!touch_context->touch_contact)
return;
g_assert (touch_context->contact_state != CONTACT_STATE_OUT_OF_RANGE);
if (touch_context->contact_state == CONTACT_STATE_ENGAGED)
{
GrdSession *session = input->session;
grd_session_notify_touch_cancel (session, touch_context->touch_contact);
ensure_queued_touch_device_frame (input);
}
queue_touch_contact_disposal (input, touch_context);
maybe_notify_touch_device_frame (input);
dispose_touch_contacts (input);
touch_context->contact_state = CONTACT_STATE_OUT_OF_RANGE;
}
static void
input_event_free (InputEvent *input_event)
{
g_clear_pointer (&input_event->touch_frame.contacts, g_free);
g_free (input_event);
}
static gboolean
handle_input_events (gpointer user_data)
{
GrdRdpDvcInput *input = user_data;
InputEvent *input_event;
if (!grd_session_has_touch_device (input->session))
return G_SOURCE_CONTINUE;
while ((input_event = g_async_queue_try_pop (input->event_queue)))
{
switch (input_event->event_type)
{
case INPUT_EVENT_TYPE_TOUCH:
process_touch_frame (input, &input_event->touch_frame);
break;
case INPUT_EVENT_TYPE_DISMISS_HOVERING_TOUCH_CONTACT:
dismiss_touch_contact (input, input_event->touch_contact_to_dismiss);
break;
case INPUT_EVENT_TYPE_PEN:
/* Missing libei-API to submit (multi-)pen events */
g_assert_not_reached ();
break;
}
input_event_free (input_event);
}
return G_SOURCE_CONTINUE;
}
static void
grd_rdp_dvc_input_dispose (GObject *object)
{
GrdRdpDvcInput *input = GRD_RDP_DVC_INPUT (object);
GrdRdpDvc *dvc = GRD_RDP_DVC (input);
uint16_t i;
if (input->channel_opened)
{
input->rdpei_context->Close (input->rdpei_context);
input->channel_opened = FALSE;
}
grd_rdp_dvc_maybe_unsubscribe_creation_status (dvc);
g_clear_signal_handler (&input->touch_device_removed_id, input->session);
g_clear_signal_handler (&input->touch_device_added_id, input->session);
if (input->event_source)
{
g_source_destroy (input->event_source);
g_clear_pointer (&input->event_source, g_source_unref);
}
handle_input_events (input);
g_clear_pointer (&input->event_queue, g_async_queue_unref);
g_assert (!input->touch_contacts_to_dispose);
for (i = 0; i < MAX_TOUCH_CONTACTS; ++i)
{
TouchContext *touch_context = &input->touch_contexts[i];
GrdTouchContact *touch_contact;
touch_contact = g_steal_pointer (&touch_context->touch_contact);
if (touch_contact)
grd_session_release_touch_contact (input->session, touch_contact);
}
g_clear_pointer (&input->rdpei_context,
rdpei_server_context_free);
G_OBJECT_CLASS (grd_rdp_dvc_input_parent_class)->dispose (object);
}
static gboolean
event_source_dispatch (GSource *source,
GSourceFunc callback,
gpointer user_data)
{
g_source_set_ready_time (source, -1);
return callback (user_data);
}
static GSourceFuncs event_source_funcs =
{
.dispatch = event_source_dispatch,
};
static void
grd_rdp_dvc_input_init (GrdRdpDvcInput *input)
{
GSource *event_source;
input->event_queue = g_async_queue_new ();
event_source = g_source_new (&event_source_funcs, sizeof (GSource));
g_source_set_callback (event_source, handle_input_events,
input, NULL);
g_source_set_ready_time (event_source, -1);
g_source_attach (event_source, NULL);
input->event_source = event_source;
}
static void
grd_rdp_dvc_input_class_init (GrdRdpDvcInputClass *klass)
{
GObjectClass *object_class = G_OBJECT_CLASS (klass);
GrdRdpDvcClass *dvc_class = GRD_RDP_DVC_CLASS (klass);
object_class->dispose = grd_rdp_dvc_input_dispose;
dvc_class->maybe_init = grd_rdp_dvc_input_maybe_init;
}