GUACAMOLE-1204: Add RDP support for multi-touch events via RDPEI channel.
diff --git a/src/protocols/rdp/Makefile.am b/src/protocols/rdp/Makefile.am
index e8ddfeb..2612bdf 100644
--- a/src/protocols/rdp/Makefile.am
+++ b/src/protocols/rdp/Makefile.am
@@ -56,6 +56,7 @@
channels/rdpdr/rdpdr-messages.c \
channels/rdpdr/rdpdr-printer.c \
channels/rdpdr/rdpdr.c \
+ channels/rdpei.c \
channels/rdpsnd/rdpsnd-messages.c \
channels/rdpsnd/rdpsnd.c \
client.c \
@@ -101,6 +102,7 @@
channels/rdpdr/rdpdr-messages.h \
channels/rdpdr/rdpdr-printer.h \
channels/rdpdr/rdpdr.h \
+ channels/rdpei.h \
channels/rdpsnd/rdpsnd-messages.h \
channels/rdpsnd/rdpsnd.h \
client.h \
diff --git a/src/protocols/rdp/channels/rdpei.c b/src/protocols/rdp/channels/rdpei.c
new file mode 100644
index 0000000..f1c5d09
--- /dev/null
+++ b/src/protocols/rdp/channels/rdpei.c
@@ -0,0 +1,170 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+#include "channels/rdpei.h"
+#include "common/surface.h"
+#include "plugins/channels.h"
+#include "rdp.h"
+#include "settings.h"
+
+#include <freerdp/client/rdpei.h>
+#include <freerdp/freerdp.h>
+#include <freerdp/event.h>
+#include <guacamole/client.h>
+#include <guacamole/timestamp.h>
+
+#include <stdlib.h>
+#include <string.h>
+
+guac_rdp_rdpei* guac_rdp_rdpei_alloc() {
+
+ guac_rdp_rdpei* rdpei = malloc(sizeof(guac_rdp_rdpei));
+
+ /* Not yet connected */
+ rdpei->rdpei = NULL;
+
+ /* No active touches */
+ for (int i = 0; i < GUAC_RDP_RDPEI_MAX_TOUCHES; i++)
+ rdpei->touch[i].active = 0;
+
+ return rdpei;
+
+}
+
+void guac_rdp_rdpei_free(guac_rdp_rdpei* rdpei) {
+ free(rdpei);
+}
+
+/**
+ * Callback which associates handlers specific to Guacamole with the
+ * RdpeiClientContext instance allocated by FreeRDP to deal with received
+ * RDPEI (multi-touch input) messages.
+ *
+ * This function is called whenever a channel connects via the PubSub event
+ * system within FreeRDP, but only has any effect if the connected channel is
+ * the RDPEI channel. This specific callback is registered with the
+ * PubSub system of the relevant rdpContext when guac_rdp_rdpei_load_plugin() is
+ * called.
+ *
+ * @param context
+ * The rdpContext associated with the active RDP session.
+ *
+ * @param e
+ * Event-specific arguments, mainly the name of the channel, and a
+ * reference to the associated plugin loaded for that channel by FreeRDP.
+ */
+static void guac_rdp_rdpei_channel_connected(rdpContext* context,
+ ChannelConnectedEventArgs* e) {
+
+ guac_client* client = ((rdp_freerdp_context*) context)->client;
+ guac_rdp_client* rdp_client = (guac_rdp_client*) client->data;
+ guac_rdp_rdpei* guac_rdpei = rdp_client->rdpei;
+
+ /* Ignore connection event if it's not for the RDPEI channel */
+ if (strcmp(e->name, RDPEI_DVC_CHANNEL_NAME) != 0)
+ return;
+
+ /* Store reference to the RDPEI plugin once it's connected */
+ RdpeiClientContext* rdpei = (RdpeiClientContext*) e->pInterface;
+ guac_rdpei->rdpei = rdpei;
+
+ /* Declare level of multi-touch support */
+ guac_common_surface_set_multitouch(rdp_client->display->default_surface,
+ GUAC_RDP_RDPEI_MAX_TOUCHES);
+
+ guac_client_log(client, GUAC_LOG_DEBUG, "RDPEI channel will be used for "
+ "multi-touch support.");
+
+}
+
+void guac_rdp_rdpei_load_plugin(rdpContext* context) {
+
+ /* Subscribe to and handle channel connected events */
+ PubSub_SubscribeChannelConnected(context->pubSub,
+ (pChannelConnectedEventHandler) guac_rdp_rdpei_channel_connected);
+
+ /* Add "rdpei" channel */
+ guac_freerdp_dynamic_channel_collection_add(context->settings, "rdpei", NULL);
+
+}
+
+int guac_rdp_rdpei_touch_update(guac_rdp_rdpei* rdpei, int id, int x, int y,
+ double force) {
+
+ int contact_id; /* Ignored */
+
+ /* Track touches only if channel is connected */
+ RdpeiClientContext* context = rdpei->rdpei;
+ if (context == NULL)
+ return 1;
+
+ /* Locate active touch having provided ID */
+ guac_rdp_rdpei_touch* touch = NULL;
+ for (int i = 0; i < GUAC_RDP_RDPEI_MAX_TOUCHES; i++) {
+ if (rdpei->touch[i].active && rdpei->touch[i].id == id) {
+ touch = &rdpei->touch[i];
+ break;
+ }
+ }
+
+ /* If no such touch exists, add it */
+ if (touch == NULL) {
+ for (int i = 0; i < GUAC_RDP_RDPEI_MAX_TOUCHES; i++) {
+ if (!rdpei->touch[i].active) {
+ touch = &rdpei->touch[i];
+ touch->id = id;
+ break;
+ }
+ }
+ }
+
+ /* If the touch couldn't be added, we're already at maximum touch capacity.
+ * Drop the event. */
+ if (touch == NULL)
+ return 1;
+
+ /* Signal the end of an established touch if touch force has become zero
+ * (this should be a safe comparison, as zero has an exact representation
+ * in floating point, and the client side will use an exact value to
+ * represent the absence of a touch) */
+ if (force == 0.0) {
+
+ /* Ignore release of touches that we aren't tracking */
+ if (!touch->active)
+ return 1;
+
+ context->TouchEnd(context, id, x, y, &contact_id);
+ touch->active = 0;
+
+ }
+
+ /* Signal the start of a touch if this is the first we've seen it */
+ else if (!touch->active) {
+ context->TouchBegin(context, id, x, y, &contact_id);
+ touch->active = 1;
+ }
+
+ /* Established touches need only be updated */
+ else
+ context->TouchUpdate(context, id, x, y, &contact_id);
+
+ return 0;
+
+}
+
diff --git a/src/protocols/rdp/channels/rdpei.h b/src/protocols/rdp/channels/rdpei.h
new file mode 100644
index 0000000..5ca10c3
--- /dev/null
+++ b/src/protocols/rdp/channels/rdpei.h
@@ -0,0 +1,154 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+#ifndef GUAC_RDP_CHANNELS_RDPEI_H
+#define GUAC_RDP_CHANNELS_RDPEI_H
+
+#include "settings.h"
+
+#include <freerdp/client/rdpei.h>
+#include <freerdp/freerdp.h>
+#include <guacamole/client.h>
+#include <guacamole/timestamp.h>
+
+/**
+ * The maximum number of simultaneously-tracked touches.
+ */
+#define GUAC_RDP_RDPEI_MAX_TOUCHES 10
+
+/**
+ * A single, tracked touch contact.
+ */
+typedef struct guac_rdp_rdpei_touch {
+
+ /**
+ * Whether this touch is active (1) or inactive (0). An active touch is
+ * being tracked, while an inactive touch is simple an empty space awaiting
+ * use by some future touch event.
+ */
+ int active;
+
+ /**
+ * The unique ID representing this touch contact.
+ */
+ int id;
+
+ /**
+ * The X-coordinate of this touch, in pixels.
+ */
+ int x;
+
+ /**
+ * The Y-coordinate of this touch, in pixels.
+ */
+ int y;
+
+} guac_rdp_rdpei_touch;
+
+/**
+ * Multi-touch input module.
+ */
+typedef struct guac_rdp_rdpei {
+
+ /**
+ * RDPEI control interface.
+ */
+ RdpeiClientContext* rdpei;
+
+ /**
+ * All currently-tracked touches.
+ */
+ guac_rdp_rdpei_touch touch[GUAC_RDP_RDPEI_MAX_TOUCHES];
+
+} guac_rdp_rdpei;
+
+/**
+ * Allocates a new RDPEI module, which will ultimately control the RDPEI
+ * channel once connected. The RDPEI channel allows multi-touch input
+ * events to be sent to the RDP server.
+ *
+ * @return
+ * A newly-allocated RDPEI module.
+ */
+guac_rdp_rdpei* guac_rdp_rdpei_alloc();
+
+/**
+ * Frees the resources associated with support for the RDPEI channel. Only
+ * resources specific to Guacamole are freed. Resources specific to FreeRDP's
+ * handling of the RDPEI channel will be freed by FreeRDP. If no resources are
+ * currently allocated for RDPEI, this function has no effect.
+ *
+ * @param rdpei
+ * The RDPEI module to free.
+ */
+void guac_rdp_rdpei_free(guac_rdp_rdpei* rdpei);
+
+/**
+ * Adds FreeRDP's "rdpei" plugin to the list of dynamic virtual channel plugins
+ * to be loaded by FreeRDP's "drdynvc" plugin. The context of the plugin will
+ * automatically be assicated with the guac_rdp_rdpei instance pointed to by the
+ * current guac_rdp_client. The plugin will only be loaded once the "drdynvc"
+ * plugin is loaded. The "rdpei" plugin ultimately adds support for multi-touch
+ * input via the RDPEI channel.
+ *
+ * If failures occur, messages noting the specifics of those failures will be
+ * logged, and the RDP side of multi-touch support will not be functional.
+ *
+ * This MUST be called within the PreConnect callback of the freerdp instance
+ * for multi-touch support to be loaded.
+ *
+ * @param context
+ * The rdpContext associated with the active RDP session.
+ */
+void guac_rdp_rdpei_load_plugin(rdpContext* context);
+
+/**
+ * Reports to the RDP server that the status of a single touch contact has
+ * changed. Depending on the amount of force associated with the touch and
+ * whether the touch has been encountered before, this will result a new touch
+ * contact, updates to an existing contact, or removal of an existing contact.
+ * If the RDPEI channel has not yet been connected, touches will be ignored and
+ * dropped until it is connected.
+ *
+ * @param rdpei
+ * The RDPEI module associated with the RDP session.
+ *
+ * @param id
+ * An arbitrary integer ID unique to the touch being updated.
+ *
+ * @param x
+ * The X-coordinate of the touch, in pixels.
+ *
+ * @param y
+ * The Y-coordinate of the touch, in pixels.
+ *
+ * @param force
+ * The amount of force currently being exerted on the device by the touch
+ * contact in question, where 1.0 is the maximum amount of force
+ * representable and 0.0 indicates the contact has been lifted.
+ *
+ * @return
+ * Zero if the touch event was successfully processed, non-zero if the
+ * touch event had to be dropped.
+ */
+int guac_rdp_rdpei_touch_update(guac_rdp_rdpei* rdpei, int id, int x, int y,
+ double force);
+
+#endif
+
diff --git a/src/protocols/rdp/client.c b/src/protocols/rdp/client.c
index 07c90a3..50808dc 100644
--- a/src/protocols/rdp/client.c
+++ b/src/protocols/rdp/client.c
@@ -147,6 +147,9 @@
/* Init display update module */
rdp_client->disp = guac_rdp_disp_alloc();
+ /* Init multi-touch support module (RDPEI) */
+ rdp_client->rdpei = guac_rdp_rdpei_alloc();
+
/* Redirect FreeRDP log messages to guac_client_log() */
guac_rdp_redirect_wlog(client);
@@ -187,6 +190,9 @@
/* Free display update module */
guac_rdp_disp_free(rdp_client->disp);
+ /* Free multi-touch support module (RDPEI) */
+ guac_rdp_rdpei_free(rdp_client->rdpei);
+
/* Clean up filesystem, if allocated */
if (rdp_client->filesystem != NULL)
guac_rdp_fs_free(rdp_client->filesystem);
diff --git a/src/protocols/rdp/input.c b/src/protocols/rdp/input.c
index cb9bb10..7b31cb3 100644
--- a/src/protocols/rdp/input.c
+++ b/src/protocols/rdp/input.c
@@ -18,6 +18,7 @@
*/
#include "channels/disp.h"
+#include "channels/rdpei.h"
#include "common/cursor.h"
#include "common/display.h"
#include "common/recording.h"
@@ -122,6 +123,28 @@
return 0;
}
+int guac_rdp_user_touch_handler(guac_user* user, int id, int x, int y,
+ int x_radius, int y_radius, double angle, double force) {
+
+ guac_client* client = user->client;
+ guac_rdp_client* rdp_client = (guac_rdp_client*) client->data;
+
+ pthread_rwlock_rdlock(&(rdp_client->lock));
+
+ /* Skip if not yet connected */
+ freerdp* rdp_inst = rdp_client->rdp_inst;
+ if (rdp_inst == NULL)
+ goto complete;
+
+ /* Forward touch event along RDPEI channel */
+ guac_rdp_rdpei_touch_update(rdp_client->rdpei, id, x, y, force);
+
+complete:
+ pthread_rwlock_unlock(&(rdp_client->lock));
+
+ return 0;
+}
+
int guac_rdp_user_key_handler(guac_user* user, int keysym, int pressed) {
guac_client* client = user->client;
diff --git a/src/protocols/rdp/input.h b/src/protocols/rdp/input.h
index 60ef064..eb9e482 100644
--- a/src/protocols/rdp/input.h
+++ b/src/protocols/rdp/input.h
@@ -28,6 +28,11 @@
guac_user_mouse_handler guac_rdp_user_mouse_handler;
/**
+ * Handler for Guacamole user touch events.
+ */
+guac_user_touch_handler guac_rdp_user_touch_handler;
+
+/**
* Handler for Guacamole user key events.
*/
guac_user_key_handler guac_rdp_user_key_handler;
diff --git a/src/protocols/rdp/rdp.c b/src/protocols/rdp/rdp.c
index ce6a279..f0e38f9 100644
--- a/src/protocols/rdp/rdp.c
+++ b/src/protocols/rdp/rdp.c
@@ -27,6 +27,7 @@
#include "channels/pipe-svc.h"
#include "channels/rail.h"
#include "channels/rdpdr/rdpdr.h"
+#include "channels/rdpei.h"
#include "channels/rdpsnd/rdpsnd.h"
#include "client.h"
#include "color.h"
@@ -100,6 +101,10 @@
if (settings->resize_method == GUAC_RESIZE_DISPLAY_UPDATE)
guac_rdp_disp_load_plugin(context);
+ /* Load "rdpei" plugin for multi-touch support */
+ if (settings->enable_touch)
+ guac_rdp_rdpei_load_plugin(context);
+
/* Load "AUDIO_INPUT" plugin for audio input*/
if (settings->enable_audio_input) {
rdp_client->audio_input = guac_rdp_audio_buffer_alloc();
diff --git a/src/protocols/rdp/rdp.h b/src/protocols/rdp/rdp.h
index e65f702..cc53796 100644
--- a/src/protocols/rdp/rdp.h
+++ b/src/protocols/rdp/rdp.h
@@ -23,6 +23,7 @@
#include "channels/audio-input/audio-buffer.h"
#include "channels/cliprdr.h"
#include "channels/disp.h"
+#include "channels/rdpei.h"
#include "common/clipboard.h"
#include "common/display.h"
#include "common/list.h"
@@ -149,6 +150,11 @@
guac_rdp_disp* disp;
/**
+ * Multi-touch support module (RDPEI).
+ */
+ guac_rdp_rdpei* rdpei;
+
+ /**
* List of all available static virtual channels.
*/
guac_common_list* available_svc;
diff --git a/src/protocols/rdp/settings.c b/src/protocols/rdp/settings.c
index dd8a96f..a5d7aeb 100644
--- a/src/protocols/rdp/settings.c
+++ b/src/protocols/rdp/settings.c
@@ -108,6 +108,7 @@
"create-recording-path",
"resize-method",
"enable-audio-input",
+ "enable-touch",
"read-only",
"gateway-hostname",
@@ -528,6 +529,12 @@
IDX_ENABLE_AUDIO_INPUT,
/**
+ * "true" if multi-touch support should be enabled for the RDP connection,
+ * "false" or blank otherwise.
+ */
+ IDX_ENABLE_TOUCH,
+
+ /**
* "true" if this connection should be read-only (user input should be
* dropped), "false" or blank otherwise.
*/
@@ -1070,6 +1077,11 @@
settings->resize_method = GUAC_RESIZE_NONE;
}
+ /* Multi-touch input enable/disable */
+ settings->enable_touch =
+ guac_user_parse_args_boolean(user, GUAC_RDP_CLIENT_ARGS, argv,
+ IDX_ENABLE_TOUCH, 0);
+
/* Audio input enable/disable */
settings->enable_audio_input =
guac_user_parse_args_boolean(user, GUAC_RDP_CLIENT_ARGS, argv,
diff --git a/src/protocols/rdp/settings.h b/src/protocols/rdp/settings.h
index 323ba5d..1a6f75c 100644
--- a/src/protocols/rdp/settings.h
+++ b/src/protocols/rdp/settings.h
@@ -526,6 +526,11 @@
int enable_audio_input;
/**
+ * Whether multi-touch support is enabled.
+ */
+ int enable_touch;
+
+ /**
* The hostname of the remote desktop gateway that should be used as an
* intermediary for the remote desktop connection. If no gateway should
* be used, this will be NULL.
diff --git a/src/protocols/rdp/user.c b/src/protocols/rdp/user.c
index 351c67e..992e551 100644
--- a/src/protocols/rdp/user.c
+++ b/src/protocols/rdp/user.c
@@ -105,6 +105,10 @@
user->mouse_handler = guac_rdp_user_mouse_handler;
user->key_handler = guac_rdp_user_key_handler;
+ /* Multi-touch events */
+ if (settings->enable_touch)
+ user->touch_handler = guac_rdp_user_touch_handler;
+
/* Inbound (client to server) clipboard transfer */
if (!settings->disable_paste)
user->clipboard_handler = guac_rdp_clipboard_handler;