diff --git a/data/meson.build b/data/meson.build index 59e98a1f1..eea49adda 100644 --- a/data/meson.build +++ b/data/meson.build @@ -18,6 +18,7 @@ portal_sources = files( 'org.freedesktop.portal.GameMode.xml', 'org.freedesktop.portal.GlobalShortcuts.xml', 'org.freedesktop.portal.Inhibit.xml', + 'org.freedesktop.portal.InputCapture.xml', 'org.freedesktop.portal.Location.xml', 'org.freedesktop.portal.MemoryMonitor.xml', 'org.freedesktop.portal.NetworkMonitor.xml', @@ -49,6 +50,7 @@ portal_impl_sources = files( 'org.freedesktop.impl.portal.FileChooser.xml', 'org.freedesktop.impl.portal.GlobalShortcuts.xml', 'org.freedesktop.impl.portal.Inhibit.xml', + 'org.freedesktop.impl.portal.InputCapture.xml', 'org.freedesktop.impl.portal.Lockdown.xml', 'org.freedesktop.impl.portal.Notification.xml', 'org.freedesktop.impl.portal.PermissionStore.xml', diff --git a/data/org.freedesktop.impl.portal.InputCapture.xml b/data/org.freedesktop.impl.portal.InputCapture.xml new file mode 100644 index 000000000..e6b38b0bc --- /dev/null +++ b/data/org.freedesktop.impl.portal.InputCapture.xml @@ -0,0 +1,436 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/org.freedesktop.portal.InputCapture.xml b/data/org.freedesktop.portal.InputCapture.xml new file mode 100644 index 000000000..286bc0969 --- /dev/null +++ b/data/org.freedesktop.portal.InputCapture.xml @@ -0,0 +1,587 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/doc/portal-docs.xml.in b/doc/portal-docs.xml.in index d7a00d3c0..c32a8e0ad 100644 --- a/doc/portal-docs.xml.in +++ b/doc/portal-docs.xml.in @@ -105,6 +105,7 @@ + @@ -154,6 +155,7 @@ + diff --git a/src/input-capture.c b/src/input-capture.c new file mode 100644 index 000000000..9945d8239 --- /dev/null +++ b/src/input-capture.c @@ -0,0 +1,1195 @@ +/* + * Copyright © 2017-2018 Red Hat, Inc + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + */ + +#include "config.h" + +#include +#include +#include + +#include "session.h" +#include "input-capture.h" +#include "request.h" +#include "xdp-dbus.h" +#include "xdp-impl-dbus.h" +#include "xdp-utils.h" + +#define VERSION_1 1 /* Makes grep easier */ + +typedef struct _InputCapture InputCapture; +typedef struct _InputCaptureClass InputCaptureClass; + +struct _InputCapture +{ + XdpDbusInputCaptureSkeleton parent_instance; +}; + +struct _InputCaptureClass +{ + XdpDbusInputCaptureSkeletonClass parent_class; +}; + +static XdpDbusImplInputCapture *impl; +static int impl_version; +static InputCapture *input_capture; + +static GQuark quark_request_session; + +GType input_capture_get_type (void); +static void input_capture_iface_init (XdpDbusInputCaptureIface *iface); + +G_DEFINE_TYPE_WITH_CODE (InputCapture, input_capture, XDP_DBUS_TYPE_INPUT_CAPTURE_SKELETON, + G_IMPLEMENT_INTERFACE (XDP_DBUS_TYPE_INPUT_CAPTURE, + input_capture_iface_init)) + +typedef enum _InputCaptureSessionState +{ + INPUT_CAPTURE_SESSION_STATE_INIT, + INPUT_CAPTURE_SESSION_STATE_ENABLED, + INPUT_CAPTURE_SESSION_STATE_ACTIVE, + INPUT_CAPTURE_SESSION_STATE_DISABLED, + INPUT_CAPTURE_SESSION_STATE_CLOSED +} InputCaptureSessionState; + +typedef struct _InputCaptureSession +{ + Session parent; + + InputCaptureSessionState state; +} InputCaptureSession; + +typedef struct _InputCaptureSessionClass +{ + SessionClass parent_class; +} InputCaptureSessionClass; + +GType input_capture_session_get_type (void); + +G_DEFINE_TYPE (InputCaptureSession, input_capture_session, session_get_type ()) + +static gboolean +is_input_capture_session (Session *session) +{ + return G_TYPE_CHECK_INSTANCE_TYPE (session, input_capture_session_get_type ()); +} + +static InputCaptureSession * +input_capture_session_new (GVariant *options, + Request *request, + GError **error) +{ + Session *session; + GDBusInterfaceSkeleton *interface_skeleton = + G_DBUS_INTERFACE_SKELETON (request); + const char *session_token; + GDBusConnection *connection = + g_dbus_interface_skeleton_get_connection (interface_skeleton); + GDBusConnection *impl_connection = + g_dbus_proxy_get_connection (G_DBUS_PROXY (impl)); + const char *impl_dbus_name = g_dbus_proxy_get_name (G_DBUS_PROXY (impl)); + + session_token = lookup_session_token (options); + session = g_initable_new (input_capture_session_get_type (), NULL, error, + "sender", request->sender, + "app-id", xdp_app_info_get_id (request->app_info), + "token", session_token, + "connection", connection, + "impl-connection", impl_connection, + "impl-dbus-name", impl_dbus_name, + NULL); + + if (session) + g_debug ("capture input session owned by '%s' created", session->sender); + + return (InputCaptureSession*)session; +} + +static void +create_session_done (GObject *source_object, + GAsyncResult *res, + gpointer data) +{ + g_autoptr(Request) request = data; + Session *session; + guint response = 2; + GVariant *results; + gboolean should_close_session; + GVariantBuilder results_builder; + g_autoptr(GError) error = NULL; + guint capabilities = 0; + + REQUEST_AUTOLOCK (request); + + session = g_object_get_qdata (G_OBJECT (request), quark_request_session); + SESSION_AUTOLOCK_UNREF (g_object_ref (session)); + g_object_set_qdata (G_OBJECT (request), quark_request_session, NULL); + + g_variant_builder_init (&results_builder, G_VARIANT_TYPE_VARDICT); + + if (!xdp_dbus_impl_input_capture_call_create_session_finish (impl, + &response, + &results, + res, + &error)) + { + g_dbus_error_strip_remote_error (error); + g_warning ("A backend call failed: %s", error->message); + should_close_session = TRUE; + goto out; + } + + if (request->exported && response == 0) + { + if (!session_export (session, &error)) + { + g_warning ("Failed to export session: %s", error->message); + response = 2; + should_close_session = TRUE; + goto out; + } + + if (!g_variant_lookup (results, "capabilities", "u", &capabilities)) + { + g_warning ("Impl did not set capabilities"); + response = 2; + should_close_session = TRUE; + goto out; + } + + should_close_session = FALSE; + session_register (session); + + g_variant_builder_add (&results_builder, "{sv}", + "capabilities", g_variant_new_uint32 (capabilities)); + g_variant_builder_add (&results_builder, "{sv}", + "session_handle", g_variant_new ("o", session->id)); + } + else + { + should_close_session = TRUE; + } + +out: + if (request->exported) + { + xdp_dbus_request_emit_response (XDP_DBUS_REQUEST (request), + response, + g_variant_builder_end (&results_builder)); + request_unexport (request); + } + else + { + g_variant_builder_clear (&results_builder); + } + + if (should_close_session) + session_close (session, FALSE); +} + +static gboolean +validate_capabilities (const char *key, + GVariant *value, + GVariant *options, + GError **error) +{ + guint32 types = g_variant_get_uint32 (value); + + if ((types & ~(1 | 2 | 4 | 8)) != 0) + { + g_set_error (error, XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT, + "Unsupported capability: %x", types & ~(1 | 2 | 4 | 8)); + return FALSE; + } + + return TRUE; +} + +static XdpOptionKey input_capture_create_session_options[] = { + { "capabilities", G_VARIANT_TYPE_UINT32, validate_capabilities }, +}; + +static gboolean +handle_create_session (XdpDbusInputCapture *object, + GDBusMethodInvocation *invocation, + const char *arg_parent_window, + GVariant *arg_options) +{ + Request *request = request_from_invocation (invocation); + g_autoptr(GError) error = NULL; + g_autoptr(XdpDbusImplRequest) impl_request = NULL; + Session *session; + GVariantBuilder options_builder; + GVariant *options; + + REQUEST_AUTOLOCK (request); + + impl_request = + xdp_dbus_impl_request_proxy_new_sync (g_dbus_proxy_get_connection (G_DBUS_PROXY (impl)), + G_DBUS_PROXY_FLAGS_NONE, + g_dbus_proxy_get_name (G_DBUS_PROXY (impl)), + request->id, + NULL, &error); + if (!impl_request) + { + g_dbus_method_invocation_return_gerror (invocation, error); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + request_set_impl_request (request, impl_request); + request_export (request, g_dbus_method_invocation_get_connection (invocation)); + + session = (Session *)input_capture_session_new (arg_options, request, &error); + if (!session) + { + g_dbus_method_invocation_return_gerror (invocation, error); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + g_variant_builder_init (&options_builder, G_VARIANT_TYPE_VARDICT); + if (!xdp_filter_options (arg_options, &options_builder, + input_capture_create_session_options, + G_N_ELEMENTS (input_capture_create_session_options), + &error)) + { + g_dbus_method_invocation_return_gerror (invocation, error); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + options = g_variant_builder_end (&options_builder); + + g_object_set_qdata_full (G_OBJECT (request), + quark_request_session, + g_object_ref (session), + g_object_unref); + + xdp_dbus_impl_input_capture_call_create_session (impl, + request->id, + session->id, + xdp_app_info_get_id (request->app_info), + arg_parent_window, + options, + NULL, + create_session_done, + g_object_ref (request)); + + xdp_dbus_input_capture_complete_create_session (object, invocation, request->id); + + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +static void +get_zones_done (GObject *source_object, GAsyncResult *res, gpointer data) +{ + g_autoptr(Request) request = data; + Session *session; + guint response = 2; + gboolean should_close_session; + g_autoptr(GError) error = NULL; + g_autoptr(GVariant) results = NULL; + + REQUEST_AUTOLOCK (request); + + session = g_object_get_qdata (G_OBJECT (request), quark_request_session); + SESSION_AUTOLOCK_UNREF (g_object_ref (session)); + g_object_set_qdata (G_OBJECT (request), quark_request_session, NULL); + + if (!xdp_dbus_impl_input_capture_call_get_zones_finish (impl, + &response, + &results, + res, + &error)) + { + g_dbus_error_strip_remote_error (error); + g_warning ("A backend call failed: %s", error->message); + } + + should_close_session = !request->exported || response != 0; + + if (request->exported) + { + if (response != 0) + { + GVariantBuilder results_builder; + + g_variant_builder_init (&results_builder, G_VARIANT_TYPE_VARDICT); + results = g_variant_builder_end (&results_builder); + } + + xdp_dbus_request_emit_response (XDP_DBUS_REQUEST (request), response, results); + request_unexport (request); + } + + if (should_close_session) + { + session_close (session, TRUE); + } +} + +static XdpOptionKey input_capture_get_zones_options[] = { +}; + +static gboolean +handle_get_zones (XdpDbusInputCapture *object, + GDBusMethodInvocation *invocation, + const char *arg_session_handle, + GVariant *arg_options) +{ + Request *request = request_from_invocation (invocation); + Session *session; + InputCaptureSession *input_capture_session; + g_autoptr(GError) error = NULL; + g_autoptr(XdpDbusImplRequest) impl_request = NULL; + GVariantBuilder options_builder; + GVariant *options; + + REQUEST_AUTOLOCK (request); + + session = acquire_session (arg_session_handle, request); + if (!session) + { + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_ACCESS_DENIED, + "Invalid session"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + SESSION_AUTOLOCK_UNREF (session); + + if (!is_input_capture_session (session)) + { + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_FAILED, + "Invalid session"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + input_capture_session = (InputCaptureSession *)session; + + switch (input_capture_session->state) + { + case INPUT_CAPTURE_SESSION_STATE_INIT: + case INPUT_CAPTURE_SESSION_STATE_ENABLED: + case INPUT_CAPTURE_SESSION_STATE_ACTIVE: + case INPUT_CAPTURE_SESSION_STATE_DISABLED: + break; + case INPUT_CAPTURE_SESSION_STATE_CLOSED: + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_FAILED, + "Invalid session"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + impl_request = + xdp_dbus_impl_request_proxy_new_sync (g_dbus_proxy_get_connection (G_DBUS_PROXY (impl)), + G_DBUS_PROXY_FLAGS_NONE, + g_dbus_proxy_get_name (G_DBUS_PROXY (impl)), + request->id, + NULL, &error); + if (!impl_request) + { + g_dbus_method_invocation_return_gerror (invocation, error); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + request_set_impl_request (request, impl_request); + request_export (request, g_dbus_method_invocation_get_connection (invocation)); + + g_variant_builder_init (&options_builder, G_VARIANT_TYPE_VARDICT); + if (!xdp_filter_options (arg_options, &options_builder, + input_capture_get_zones_options, + G_N_ELEMENTS (input_capture_get_zones_options), + &error)) + { + g_dbus_method_invocation_return_gerror (invocation, error); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + options = g_variant_builder_end (&options_builder); + + g_object_set_qdata_full (G_OBJECT (request), + quark_request_session, + g_object_ref (session), + g_object_unref); + + xdp_dbus_impl_input_capture_call_get_zones (impl, + request->id, + arg_session_handle, + xdp_app_info_get_id (request->app_info), + options, + NULL, + get_zones_done, + g_object_ref (request)); + + xdp_dbus_input_capture_complete_get_zones (object, invocation, request->id); + + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +static void +set_pointer_barriers_done (GObject *source_object, GAsyncResult *res, gpointer data) +{ + g_autoptr(Request) request = data; + Session *session; + guint response = 2; + gboolean should_close_session; + g_autoptr(GError) error = NULL; + g_autoptr(GVariant) results = NULL; + + REQUEST_AUTOLOCK (request); + + session = g_object_get_qdata (G_OBJECT (request), quark_request_session); + SESSION_AUTOLOCK_UNREF (g_object_ref (session)); + g_object_set_qdata (G_OBJECT (request), quark_request_session, NULL); + + if (!xdp_dbus_impl_input_capture_call_set_pointer_barriers_finish (impl, + &response, + &results, + res, + &error)) + { + g_dbus_error_strip_remote_error (error); + g_warning ("A backend call failed: %s", error->message); + } + + should_close_session = !request->exported || response != 0; + + if (request->exported) + { + if (response != 0) + { + GVariantBuilder results_builder; + + g_variant_builder_init (&results_builder, G_VARIANT_TYPE_VARDICT); + results = g_variant_builder_end (&results_builder); + } + + xdp_dbus_request_emit_response (XDP_DBUS_REQUEST (request), response, results); + request_unexport (request); + } + + if (should_close_session) + { + session_close (session, TRUE); + } +} + +static XdpOptionKey input_capture_set_pointer_barriers_options[] = { +}; + +static gboolean +handle_set_pointer_barriers (XdpDbusInputCapture *object, + GDBusMethodInvocation *invocation, + const char *arg_session_handle, + GVariant *arg_options, + GVariant *arg_barriers, + guint arg_zone_set) +{ + Request *request = request_from_invocation (invocation); + Session *session; + InputCaptureSession *input_capture_session; + g_autoptr(GError) error = NULL; + g_autoptr(XdpDbusImplRequest) impl_request = NULL; + GVariantBuilder options_builder; + GVariant *options; + + REQUEST_AUTOLOCK (request); + + session = acquire_session (arg_session_handle, request); + if (!session) + { + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_ACCESS_DENIED, + "Invalid session"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + SESSION_AUTOLOCK_UNREF (session); + + if (!is_input_capture_session (session)) + { + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_FAILED, + "Invalid session"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + input_capture_session = (InputCaptureSession *)session; + + switch (input_capture_session->state) + { + case INPUT_CAPTURE_SESSION_STATE_INIT: + case INPUT_CAPTURE_SESSION_STATE_ENABLED: + case INPUT_CAPTURE_SESSION_STATE_ACTIVE: + case INPUT_CAPTURE_SESSION_STATE_DISABLED: + break; + case INPUT_CAPTURE_SESSION_STATE_CLOSED: + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_FAILED, + "Invalid session"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + impl_request = + xdp_dbus_impl_request_proxy_new_sync (g_dbus_proxy_get_connection (G_DBUS_PROXY (impl)), + G_DBUS_PROXY_FLAGS_NONE, + g_dbus_proxy_get_name (G_DBUS_PROXY (impl)), + request->id, + NULL, &error); + if (!impl_request) + { + g_dbus_method_invocation_return_gerror (invocation, error); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + request_set_impl_request (request, impl_request); + request_export (request, g_dbus_method_invocation_get_connection (invocation)); + + g_variant_builder_init (&options_builder, G_VARIANT_TYPE_VARDICT); + if (!xdp_filter_options (arg_options, &options_builder, + input_capture_set_pointer_barriers_options, + G_N_ELEMENTS (input_capture_set_pointer_barriers_options), + &error)) + { + g_dbus_method_invocation_return_gerror (invocation, error); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + options = g_variant_builder_end (&options_builder); + + g_object_set_qdata_full (G_OBJECT (request), + quark_request_session, + g_object_ref (session), + g_object_unref); + + xdp_dbus_impl_input_capture_call_set_pointer_barriers (impl, + request->id, + arg_session_handle, + xdp_app_info_get_id (request->app_info), + options, + g_variant_ref(arg_barriers), /* FIXME: validation? */ + arg_zone_set, /* FIXME: validation? */ + NULL, + set_pointer_barriers_done, + g_object_ref (request)); + + xdp_dbus_input_capture_complete_set_pointer_barriers (object, invocation, request->id); + + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +static XdpOptionKey input_capture_enable_options[] = { +}; + +static gboolean +handle_enable (XdpDbusInputCapture *object, + GDBusMethodInvocation *invocation, + const char *arg_session_handle, + GVariant *arg_options) +{ + Call *call = call_from_invocation (invocation); + Session *session; + InputCaptureSession *input_capture_session; + g_autoptr(GError) error = NULL; + g_autoptr(XdpDbusImplRequest) impl_request = NULL; + GVariantBuilder options_builder; + + session = acquire_session_from_call (arg_session_handle, call); + if (!session) + { + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_ACCESS_DENIED, + "Invalid session"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + SESSION_AUTOLOCK_UNREF (session); + + if (!is_input_capture_session (session)) + { + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_FAILED, + "Invalid session"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + input_capture_session = (InputCaptureSession *)session; + + switch (input_capture_session->state) + { + case INPUT_CAPTURE_SESSION_STATE_INIT: + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_FAILED, + "Not connected to EIS"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + case INPUT_CAPTURE_SESSION_STATE_ENABLED: + case INPUT_CAPTURE_SESSION_STATE_ACTIVE: + case INPUT_CAPTURE_SESSION_STATE_DISABLED: + break; + case INPUT_CAPTURE_SESSION_STATE_CLOSED: + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_FAILED, + "Invalid session"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + g_variant_builder_init (&options_builder, G_VARIANT_TYPE_VARDICT); + + if (!xdp_filter_options (arg_options, &options_builder, + input_capture_enable_options, + G_N_ELEMENTS (input_capture_enable_options), + &error)) + { + g_dbus_method_invocation_return_gerror (invocation, error); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + input_capture_session->state = INPUT_CAPTURE_SESSION_STATE_ENABLED; + + /* Let's be lenient and make Enable() a noop for anything but a disabled + * session. + */ + switch (input_capture_session->state) + { + case INPUT_CAPTURE_SESSION_STATE_INIT: /* ignore, handled above */ + g_assert_not_reached (); + case INPUT_CAPTURE_SESSION_STATE_ENABLED: + case INPUT_CAPTURE_SESSION_STATE_ACTIVE: + break; + case INPUT_CAPTURE_SESSION_STATE_DISABLED: + input_capture_session->state = INPUT_CAPTURE_SESSION_STATE_ENABLED; + break; + case INPUT_CAPTURE_SESSION_STATE_CLOSED: /* ignore, handled above */ + g_assert_not_reached (); + } + + xdp_dbus_impl_input_capture_call_enable (impl, + arg_session_handle, + xdp_app_info_get_id (call->app_info), + g_variant_builder_end (&options_builder), + NULL, + NULL, + NULL); + + xdp_dbus_input_capture_complete_enable (object, invocation); + + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +static XdpOptionKey input_capture_disable_options[] = { +}; + +static gboolean +handle_disable (XdpDbusInputCapture *object, + GDBusMethodInvocation *invocation, + const char *arg_session_handle, + GVariant *arg_options) +{ + Call *call = call_from_invocation (invocation); + Session *session; + InputCaptureSession *input_capture_session; + g_autoptr(GError) error = NULL; + g_autoptr(XdpDbusImplRequest) impl_request = NULL; + GVariantBuilder options_builder; + + session = acquire_session_from_call (arg_session_handle, call); + if (!session) + { + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_ACCESS_DENIED, + "Invalid session"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + SESSION_AUTOLOCK_UNREF (session); + + if (!is_input_capture_session (session)) + { + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_FAILED, + "Invalid session"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + input_capture_session = (InputCaptureSession *)session; + + switch (input_capture_session->state) + { + case INPUT_CAPTURE_SESSION_STATE_INIT: + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_FAILED, + "Not connected to EIS"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + case INPUT_CAPTURE_SESSION_STATE_ENABLED: + case INPUT_CAPTURE_SESSION_STATE_ACTIVE: + case INPUT_CAPTURE_SESSION_STATE_DISABLED: + break; + case INPUT_CAPTURE_SESSION_STATE_CLOSED: + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_FAILED, + "Invalid session"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + g_variant_builder_init (&options_builder, G_VARIANT_TYPE_VARDICT); + if (!xdp_filter_options (arg_options, &options_builder, + input_capture_disable_options, + G_N_ELEMENTS (input_capture_disable_options), + &error)) + { + g_dbus_method_invocation_return_gerror (invocation, error); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + /* We need to be lenient, a caller may call Disable() before processing a + * Disabled signal. So we pretend everything's ok but only + * update our internal state in the right transitions. + */ + switch (input_capture_session->state) + { + case INPUT_CAPTURE_SESSION_STATE_INIT: /* ignore, handled above */ + g_assert_not_reached (); + case INPUT_CAPTURE_SESSION_STATE_ENABLED: + case INPUT_CAPTURE_SESSION_STATE_ACTIVE: + input_capture_session->state = INPUT_CAPTURE_SESSION_STATE_DISABLED; + break; + case INPUT_CAPTURE_SESSION_STATE_DISABLED: + break; + case INPUT_CAPTURE_SESSION_STATE_CLOSED: /* ignore, handled above */ + g_assert_not_reached (); + } + + xdp_dbus_impl_input_capture_call_disable (impl, + arg_session_handle, + xdp_app_info_get_id (call->app_info), + g_variant_builder_end (&options_builder), + NULL, + NULL, + NULL); + + xdp_dbus_input_capture_complete_disable (object, invocation); + + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +static XdpOptionKey input_capture_release_options[] = { + { "cursor_position", (const GVariantType *)"(dd)", NULL }, + { "activation_id", G_VARIANT_TYPE_UINT32, NULL }, +}; + +static gboolean +handle_release (XdpDbusInputCapture *object, + GDBusMethodInvocation *invocation, + const char *arg_session_handle, + GVariant *arg_options) +{ + Call *call = call_from_invocation (invocation); + Session *session; + InputCaptureSession *input_capture_session; + g_autoptr(GError) error = NULL; + GVariantBuilder options_builder; + + session = acquire_session_from_call (arg_session_handle, call); + if (!session) + { + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_ACCESS_DENIED, + "Invalid session"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + SESSION_AUTOLOCK_UNREF (session); + + if (!is_input_capture_session (session)) + { + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_FAILED, + "Invalid session"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + input_capture_session = (InputCaptureSession *)session; + + switch (input_capture_session->state) + { + case INPUT_CAPTURE_SESSION_STATE_INIT: + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_FAILED, + "Not connected to EIS"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + case INPUT_CAPTURE_SESSION_STATE_ENABLED: + case INPUT_CAPTURE_SESSION_STATE_ACTIVE: + case INPUT_CAPTURE_SESSION_STATE_DISABLED: + break; + case INPUT_CAPTURE_SESSION_STATE_CLOSED: + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_FAILED, + "Invalid session"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + g_variant_builder_init (&options_builder, G_VARIANT_TYPE_VARDICT); + if (!xdp_filter_options (arg_options, &options_builder, + input_capture_release_options, + G_N_ELEMENTS (input_capture_release_options), + &error)) + { + g_dbus_method_invocation_return_gerror (invocation, error); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + /* We need to be lenient, a caller may call Release() before processing a + * Deactivated/Disabled signal. So we pretend everything's ok but only + * update our internal state in the right transitions. + */ + switch (input_capture_session->state) + { + case INPUT_CAPTURE_SESSION_STATE_INIT: /* ignore, handled above */ + g_assert_not_reached (); + case INPUT_CAPTURE_SESSION_STATE_ENABLED: + break; + case INPUT_CAPTURE_SESSION_STATE_ACTIVE: + input_capture_session->state = INPUT_CAPTURE_SESSION_STATE_ENABLED; + break; + case INPUT_CAPTURE_SESSION_STATE_DISABLED: + break; + case INPUT_CAPTURE_SESSION_STATE_CLOSED: /* ignore, handled above */ + g_assert_not_reached (); + } + + xdp_dbus_impl_input_capture_call_release (impl, + arg_session_handle, + xdp_app_info_get_id (call->app_info), + g_variant_builder_end (&options_builder), + NULL, + NULL, + NULL); + + xdp_dbus_input_capture_complete_release (object, invocation); + + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +static gboolean +handle_connect_to_eis (XdpDbusInputCapture *object, + GDBusMethodInvocation *invocation, + GUnixFDList *in_fd_list, + const char *arg_session_handle, + GVariant *arg_options) +{ + Call *call = call_from_invocation (invocation); + Session *session; + InputCaptureSession *input_capture_session; + g_autoptr(GUnixFDList) out_fd_list = NULL; + g_autoptr(GError) error = NULL; + GVariantBuilder empty; + GVariant *fd; + + session = acquire_session_from_call (arg_session_handle, call); + if (!session) + { + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_ACCESS_DENIED, + "Invalid session"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + SESSION_AUTOLOCK_UNREF (session); + + if (!is_input_capture_session (session)) + { + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_ACCESS_DENIED, + "Invalid session"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + input_capture_session = (InputCaptureSession *)session; + + switch (input_capture_session->state) + { + case INPUT_CAPTURE_SESSION_STATE_INIT: + break; + case INPUT_CAPTURE_SESSION_STATE_ENABLED: + case INPUT_CAPTURE_SESSION_STATE_ACTIVE: + case INPUT_CAPTURE_SESSION_STATE_DISABLED: + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_FAILED, + "Already connected"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + case INPUT_CAPTURE_SESSION_STATE_CLOSED: + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_FAILED, + "Invalid session"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + g_variant_builder_init (&empty, G_VARIANT_TYPE_VARDICT); + + if (!xdp_dbus_impl_input_capture_call_connect_to_eis_sync (impl, + arg_session_handle, + xdp_app_info_get_id (call->app_info), + g_variant_builder_end (&empty), + in_fd_list, + &fd, + &out_fd_list, + NULL, + &error)) + { + g_warning ("Failed to ConnectToEIS: %s", error->message); + out_fd_list = g_unix_fd_list_new (); + } + + input_capture_session->state = INPUT_CAPTURE_SESSION_STATE_DISABLED; + + xdp_dbus_input_capture_complete_connect_to_eis (object, invocation, out_fd_list, fd); + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +static void +input_capture_iface_init (XdpDbusInputCaptureIface *iface) +{ + iface->handle_create_session = handle_create_session; + iface->handle_get_zones = handle_get_zones; + iface->handle_set_pointer_barriers = handle_set_pointer_barriers; + iface->handle_connect_to_eis = handle_connect_to_eis; + iface->handle_enable = handle_enable; + iface->handle_disable = handle_disable; + iface->handle_release = handle_release; + +} + +static void +pass_signal (XdpDbusImplInputCapture *impl, + const char *signal_name, + const char *session_id, + GVariant *options, + gpointer *data) +{ + GDBusConnection *connection = g_dbus_proxy_get_connection (G_DBUS_PROXY (impl)); + g_autoptr(Session) session = lookup_session (session_id); + + g_dbus_connection_emit_signal (connection, + session->sender, + "/org/freedesktop/portal/desktop", + "org.freedesktop.portal.InputCapture", + signal_name, + g_variant_new ("(o@a{sv})", session_id, options), + NULL); +} + +static void +on_disabled_cb (XdpDbusImplInputCapture *impl, + const char *session_id, + GVariant *options, + gpointer *data) +{ + g_autoptr(Session) session = lookup_session (session_id); + InputCaptureSession *input_capture_session; + + if (!is_input_capture_session (session)) + { + g_critical ("Invalid session type for signal"); + return; + } + + input_capture_session = (InputCaptureSession*)session; + + switch (input_capture_session->state) + { + case INPUT_CAPTURE_SESSION_STATE_INIT: + break; + case INPUT_CAPTURE_SESSION_STATE_ENABLED: + case INPUT_CAPTURE_SESSION_STATE_ACTIVE: + pass_signal (impl, "Disabled", session_id, options, data); + input_capture_session->state = INPUT_CAPTURE_SESSION_STATE_DISABLED; + break; + case INPUT_CAPTURE_SESSION_STATE_DISABLED: + break; + case INPUT_CAPTURE_SESSION_STATE_CLOSED: + break; + } +} + +static void +on_activated_cb (XdpDbusImplInputCapture *impl, + const char *session_id, + GVariant *options, + gpointer *data) +{ + g_autoptr(Session) session = lookup_session (session_id); + InputCaptureSession *input_capture_session; + + if (!is_input_capture_session (session)) + { + g_critical ("Invalid session type for signal"); + return; + } + + input_capture_session = (InputCaptureSession*)session; + + switch (input_capture_session->state) + { + case INPUT_CAPTURE_SESSION_STATE_INIT: + break; + case INPUT_CAPTURE_SESSION_STATE_ENABLED: + pass_signal (impl, "Activated", session_id, options, data); + input_capture_session->state = INPUT_CAPTURE_SESSION_STATE_ACTIVE; + break; + case INPUT_CAPTURE_SESSION_STATE_ACTIVE: + case INPUT_CAPTURE_SESSION_STATE_DISABLED: + case INPUT_CAPTURE_SESSION_STATE_CLOSED: + break; + } +} + +static void +on_deactivated_cb (XdpDbusImplInputCapture *impl, + const char *session_id, + GVariant *options, + gpointer *data) +{ + g_autoptr(Session) session = lookup_session (session_id); + InputCaptureSession *input_capture_session; + + if (!is_input_capture_session (session)) + { + g_critical ("Invalid session type for signal"); + return; + } + + input_capture_session = (InputCaptureSession*)session; + + switch (input_capture_session->state) + { + case INPUT_CAPTURE_SESSION_STATE_INIT: + case INPUT_CAPTURE_SESSION_STATE_ENABLED: + break; + case INPUT_CAPTURE_SESSION_STATE_ACTIVE: + pass_signal (impl, "Deactivated", session_id, options, data); + input_capture_session->state = INPUT_CAPTURE_SESSION_STATE_ACTIVE; + break; + case INPUT_CAPTURE_SESSION_STATE_DISABLED: + case INPUT_CAPTURE_SESSION_STATE_CLOSED: + break; + } +} + +static void +input_capture_init (InputCapture *input_capture) +{ + xdp_dbus_input_capture_set_version (XDP_DBUS_INPUT_CAPTURE (input_capture), VERSION_1); + unsigned int supported_capabilities; + + supported_capabilities = + xdp_dbus_impl_input_capture_get_supported_capabilities (impl); + xdp_dbus_input_capture_set_supported_capabilities (XDP_DBUS_INPUT_CAPTURE (input_capture), + supported_capabilities); + + g_signal_connect (impl, "disabled", G_CALLBACK (on_disabled_cb), input_capture); + g_signal_connect (impl, "activated", G_CALLBACK (on_activated_cb), input_capture); + g_signal_connect (impl, "deactivated", G_CALLBACK (on_deactivated_cb), input_capture); +} + +static void +input_capture_class_init (InputCaptureClass *klass) +{ + quark_request_session = + g_quark_from_static_string ("-xdp-request-capture-input-session"); +} + +GDBusInterfaceSkeleton * +input_capture_create (GDBusConnection *connection, + const char *dbus_name) +{ + g_autoptr(GError) error = NULL; + + impl = xdp_dbus_impl_input_capture_proxy_new_sync (connection, + G_DBUS_PROXY_FLAGS_NONE, + dbus_name, + DESKTOP_PORTAL_OBJECT_PATH, + NULL, + &error); + if (impl == NULL) + { + g_warning ("Failed to create capture input proxy: %s", error->message); + return NULL; + } + + impl_version = xdp_dbus_impl_input_capture_get_version (impl); + + g_dbus_proxy_set_default_timeout (G_DBUS_PROXY (impl), G_MAXINT); + + input_capture = g_object_new (input_capture_get_type (), NULL); + + return G_DBUS_INTERFACE_SKELETON (input_capture); +} + +static void +input_capture_session_close (Session *session) +{ + InputCaptureSession *input_capture_session = (InputCaptureSession *)session; + + input_capture_session->state = INPUT_CAPTURE_SESSION_STATE_CLOSED; + + g_debug ("screen cast session owned by '%s' closed", session->sender); +} + +static void +input_capture_session_finalize (GObject *object) +{ + G_OBJECT_CLASS (input_capture_session_parent_class)->finalize (object); +} + +static void +input_capture_session_init (InputCaptureSession *input_capture_session) +{ +} + +static void +input_capture_session_class_init (InputCaptureSessionClass *klass) +{ + GObjectClass *object_class; + SessionClass *session_class; + + object_class = G_OBJECT_CLASS (klass); + object_class->finalize = input_capture_session_finalize; + + session_class = (SessionClass *)klass; + session_class->close = input_capture_session_close; +} diff --git a/src/input-capture.h b/src/input-capture.h new file mode 100644 index 000000000..7db7a7b24 --- /dev/null +++ b/src/input-capture.h @@ -0,0 +1,25 @@ +/* + * Copyright © 2022 Red Hat, Inc + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + */ + +#pragma once + +#include +#include + +GDBusInterfaceSkeleton * input_capture_create (GDBusConnection *connection, + const char *dbus_name); diff --git a/src/meson.build b/src/meson.build index 6fabe46a2..8f4054c98 100644 --- a/src/meson.build +++ b/src/meson.build @@ -60,6 +60,7 @@ xdg_desktop_portal_sources = files( 'glib-backports.c', 'global-shortcuts.c', 'inhibit.c', + 'input-capture.c', 'memory-monitor.c', 'network-monitor.c', 'notification.c', diff --git a/src/request.c b/src/request.c index 12696f9e8..4216696b2 100644 --- a/src/request.c +++ b/src/request.c @@ -202,6 +202,10 @@ get_token (GDBusMethodInvocation *invocation) else if (strcmp (method, "CreateMonitor") == 0) options = g_variant_get_child_value (parameters, 1); } + else if (strcmp (interface, "org.freedesktop.portal.InputCapture") == 0) + { + options = g_variant_get_child_value (parameters, 1); + } else if (strcmp (interface, "org.freedesktop.portal.NetworkMonitor") == 0) { // no methods diff --git a/src/xdg-desktop-portal.c b/src/xdg-desktop-portal.c index 3e2bc2fd4..4a1ae22ed 100644 --- a/src/xdg-desktop-portal.c +++ b/src/xdg-desktop-portal.c @@ -43,6 +43,7 @@ #include "gamemode.h" #include "global-shortcuts.h" #include "inhibit.h" +#include "input-capture.h" #include "location.h" #include "memory-monitor.h" #include "network-monitor.h" @@ -151,6 +152,16 @@ method_needs_request (GDBusMethodInvocation *invocation) else return TRUE; } + else if (strcmp (interface, "org.freedesktop.portal.InputCapture") == 0) + { + if (strcmp (method, "ConnectToEIS") == 0 || + strcmp (method, "Enable") == 0 || + strcmp (method, "Disable") == 0 || + strcmp (method, "Release") == 0) + return FALSE; + else + return TRUE; + } else { return TRUE; @@ -373,6 +384,11 @@ on_bus_acquired (GDBusConnection *connection, export_portal_implementation ( connection, clipboard_create (connection, implementation->dbus_name)); #endif + + implementation = find_portal_implementation ("org.freedesktop.impl.portal.InputCapture"); + if (implementation != NULL) + export_portal_implementation (connection, + input_capture_create (connection, implementation->dbus_name)); } static void diff --git a/tests/portals/meson.build b/tests/portals/meson.build index 7bed78a1b..6f6ea8b64 100644 --- a/tests/portals/meson.build +++ b/tests/portals/meson.build @@ -7,6 +7,7 @@ test_portals = [ 'org.freedesktop.impl.portal.Email', 'org.freedesktop.impl.portal.FileChooser', 'org.freedesktop.impl.portal.Inhibit', + 'org.freedesktop.impl.portal.InputCapture', 'org.freedesktop.impl.portal.Lockdown', 'org.freedesktop.impl.portal.Notification', 'org.freedesktop.impl.portal.Print', diff --git a/tests/templates/inputcapture.py b/tests/templates/inputcapture.py new file mode 100644 index 000000000..06ea57959 --- /dev/null +++ b/tests/templates/inputcapture.py @@ -0,0 +1,298 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# This file is formatted with Python Black + +from collections import namedtuple +from itertools import count +from gi.repository import GLib +from tests.templates import Response, init_template_logger, ImplRequest, ImplSession + +import dbus +import dbus.service +import logging +import socket + +BUS_NAME = "org.freedesktop.impl.portal.Test" +MAIN_OBJ = "/org/freedesktop/portal/desktop" +SYSTEM_BUS = False +MAIN_IFACE = "org.freedesktop.impl.portal.InputCapture" +VERSION = 1 + +logger = logging.getLogger(f"templates.{__name__}") +logger.setLevel(logging.DEBUG) + +serials = count() + +Response = namedtuple("Response", ["response", "results"]) +Barrier = namedtuple("Barrier", ["id", "position"]) + + +def load(mock, parameters=None): + logger.debug(f"Loading parameters: {parameters}") + # Delay before Request.response + mock.delay: int = parameters.get("delay", 0) + + mock.supported_capabilities = parameters.get("supported_capabilities", 0xF) + # The actual ones we reply with in the CreateSession request + mock.capabilities = parameters.get("capabilities", None) + + mock.default_zone = parameters.get("default-zone", [(1920, 1080, 0, 0)]) + mock.current_zones = mock.default_zone + mock.current_zone_set = next(serials) + + mock.disable_delay = parameters.get("disable-delay", 0) + mock.activated_delay = parameters.get("activated-delay", 0) + mock.deactivated_delay = parameters.get("deactivated-delay", 0) + + mock.AddProperties( + MAIN_IFACE, + dbus.Dictionary( + { + "version": dbus.UInt32(parameters.get("version", VERSION)), + "SupportedCapabilities": dbus.UInt32(mock.supported_capabilities), + } + ), + ) + + mock.active_session_handles = [] + + +@dbus.service.method( + MAIN_IFACE, + in_signature="oossa{sv}", + out_signature="ua{sv}", +) +def CreateSession(self, handle, session_handle, app_id, parent_window, options): + try: + logger.debug(f"CreateSession({parent_window}, {options})") + + assert "capabilities" in options + + # Filter to the subset of supported capabilities + if self.capabilities is None: + capabilities = options["capabilities"] + else: + capabilities = self.capabilities + + capabilities &= self.supported_capabilities + response = Response(0, {}) + + response.results["capabilities"] = dbus.UInt32(capabilities) + self.active_session_handles.append(session_handle) + + logger.debug(f"CreateSession with response {response}") + + return response.response, response.results + except Exception as e: + logger.critical(e) + return (2, {}) + + +@dbus.service.method( + MAIN_IFACE, + in_signature="oosa{sv}", + out_signature="ua{sv}", +) +def GetZones(self, handle, session_handle, app_id, options): + try: + logger.debug(f"GetZones({session_handle}, {options})") + + assert session_handle in self.active_session_handles + + response = Response(0, {}) + response.results["zones"] = self.default_zone + response.results["zone_set"] = dbus.UInt32( + self.current_zone_set, variant_level=1 + ) + logger.debug(f"GetZones with response {response}") + + if response.response == 0: + self.current_zones = response.results["zones"] + + return response.response, response.results + except Exception as e: + logger.critical(e) + return (2, {}) + + +@dbus.service.method( + MAIN_IFACE, + in_signature="oosa{sv}aa{sv}u", + out_signature="ua{sv}", +) +def SetPointerBarriers( + self, handle, session_handle, app_id, options, barriers, zone_set +): + try: + logger.debug( + f"SetPointerBarriers({session_handle}, {options}, {barriers}, {zone_set})" + ) + + assert session_handle in self.active_session_handles + assert zone_set == self.current_zone_set + + self.current_barriers = [] + + failed_barriers = [] + + # Barrier sanity checks: + for b in barriers: + id = b["barrier_id"] + x1, y1, x2, y2 = b["position"] + if (x1 != x2 and y1 != y2) or (x1 == x2 and y1 == y2): + logger.debug(f"Barrier {id} is not horizontal or vertical") + failed_barriers.append(id) + continue + + for z in self.current_zones: + w, h, x, y = z + if x1 < x or x1 > x + w: + continue + if y1 < y or y1 > y + h: + continue + + # x1/y1 fit into our current zone + if x2 < x or x2 > x + w or y2 < y or y2 > y + h: + logger.debug(f"Barrier {id} spans multiple zones") + elif x1 == x2 and (x1 != x and x1 != x + w): + logger.debug(f"Barrier {id} is not on vertical edge") + elif y1 == y2 and (y1 != y and y1 != y + h): + logger.debug(f"Barrier {id} is not on horizontal edge") + else: + self.current_barriers.append(Barrier(id=id, position=b["position"])) + break + + failed_barriers.append(id) + break + else: + logger.debug(f"Barrier {id} does not fit into any zone") + failed_barriers.append(id) + continue + + response = Response(0, {}) + response.results["failed_barriers"] = dbus.Array( + [dbus.UInt32(f) for f in failed_barriers], + signature="u", + variant_level=1, + ) + + logger.debug(f"SetPointerBarriers with response {response}") + + return response.response, response.results + except Exception as e: + logger.critical(e) + return (2, {}) + + +@dbus.service.method( + MAIN_IFACE, + in_signature="osa{sv}", + out_signature="ua{sv}", +) +def Enable(self, session_handle, app_id, options): + try: + logger.debug(f"Enable({session_handle}, {options})") + + assert session_handle in self.active_session_handles + + # for use in the signals + activation_id = next(serials) + barrier = self.current_barriers[0] + pos = (barrier.position[0] + 10, barrier.position[1] + 20) + + if self.disable_delay > 0: + + def disable(): + logger.debug("emitting Disabled") + self.EmitSignal("", "Disabled", "oa{sv}", [session_handle, {}]) + + GLib.timeout_add(self.disable_delay, disable) + + if self.activated_delay > 0: + + def activated(): + logger.debug("emitting Activated") + options = { + "activation_id": dbus.UInt32(activation_id, variant_level=1), + "barrier_id": dbus.UInt32(barrier.id, variant_level=1), + "cursor_position": dbus.Struct( + pos, signature="dd", variant_level=1 + ), + } + self.EmitSignal("", "Activated", "oa{sv}", [session_handle, options]) + + GLib.timeout_add(self.activated_delay, activated) + + if self.deactivated_delay > 0: + + def deactivated(): + logger.debug("emitting Deactivated") + options = { + "activation_id": dbus.UInt32(activation_id, variant_level=1), + "cursor_position": dbus.Struct( + pos, signature="dd", variant_level=1 + ), + } + self.EmitSignal("", "Deactivated", "oa{sv}", [session_handle, options]) + + GLib.timeout_add(self.deactivated_delay, deactivated) + + except Exception as e: + logger.critical(e) + return (2, {}) + + +@dbus.service.method( + MAIN_IFACE, + in_signature="osa{sv}", + out_signature="ua{sv}", +) +def Disable(self, session_handle, app_id, options): + try: + logger.debug(f"Disable({session_handle}, {options})") + + assert session_handle in self.active_session_handles + except Exception as e: + logger.critical(e) + return (2, {}) + + +@dbus.service.method( + MAIN_IFACE, + in_signature="osa{sv}", + out_signature="ua{sv}", +) +def Release(self, session_handle, app_id, options): + try: + logger.debug(f"Release({session_handle}, {options})") + + assert session_handle in self.active_session_handles + except Exception as e: + logger.critical(e) + return (2, {}) + + +@dbus.service.method( + MAIN_IFACE, + in_signature="osa{sv}", + out_signature="h", +) +def ConnectToEIS(self, session_handle, app_id, options): + try: + logger.debug(f"ConnectToEIS({session_handle}, {options})") + + assert session_handle in self.active_session_handles + + sockets = socket.socketpair() + self.eis_socket = sockets[0] + + assert self.eis_socket.send(b"HELLO") == 5 + + fd = sockets[1] + + logger.debug(f"ConnectToEis with fd {fd.fileno()}") + + return dbus.types.UnixFd(fd) + except Exception as e: + logger.critical(e) + return -1 diff --git a/tests/test_inputcapture.py b/tests/test_inputcapture.py new file mode 100644 index 000000000..8ad5e83b4 --- /dev/null +++ b/tests/test_inputcapture.py @@ -0,0 +1,600 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# This file is formatted with Python Black + +from typing import Any, Dict, List, Tuple +from gi.repository import GLib + +from itertools import count + +import dbus +import socket + +from tests import Request, PortalTest, Session + +counter = count() + + +class TestInputCapture(PortalTest): + def create_session(self, capabilities=0xF): + """ + Call CreateSession for the given capabilities and return the + (response, results) tuple. + """ + inputcapture_intf = self.get_dbus_interface() + request = Request(self.dbus_con, inputcapture_intf) + + capabilities = dbus.UInt32(capabilities, variant_level=1) + session_handle_token = dbus.String(f"session{next(counter)}", variant_level=1) + + options = dbus.Dictionary( + { + "capabilities": capabilities, + "session_handle_token": session_handle_token, + }, + signature="sv", + ) + + response, results = request.call( + "CreateSession", parent_window="", options=options + ) + assert response == 0 + assert "session_handle" in results + assert "capabilities" in results + caps = results["capabilities"] + # Returned capabilities must be a subset of the requested ones + assert caps & ~capabilities == 0 + + self.current_session_handle = results["session_handle"] + + # Check the impl portal was called with the right args + method_calls = self.mock_interface.GetMethodCalls("CreateSession") + assert len(method_calls) > 0 + _, args = method_calls[-1] + assert args[3] == "" # parent window + assert args[4]["capabilities"] == capabilities + + return response, results + + def get_zones(self): + """ + Call GetZones and return the (response, results) tuple. + """ + inputcapture_intf = self.get_dbus_interface() + request = Request(self.dbus_con, inputcapture_intf) + options = {} + response, results = request.call( + "GetZones", session_handle=self.current_session_handle, options=options + ) + assert response == 0 + assert "zones" in results + assert "zone_set" in results + + self.current_zone_set = results["zone_set"] + + # Check the impl portal was called with the right args + method_calls = self.mock_interface.GetMethodCalls("GetZones") + assert len(method_calls) > 0 + _, args = method_calls[-1] + assert args[0] == request.handle + assert args[1] == self.current_session_handle + + return response, results + + def set_pointer_barriers(self, barriers): + inputcapture_intf = self.get_dbus_interface() + request = Request(self.dbus_con, inputcapture_intf) + options = {} + response, results = request.call( + "SetPointerBarriers", + session_handle=self.current_session_handle, + options=options, + barriers=barriers, + zone_set=self.current_zone_set, + ) + assert response == 0 + assert "failed_barriers" in results + + # Check the impl portal was called with the right args + method_calls = self.mock_interface.GetMethodCalls("SetPointerBarriers") + assert len(method_calls) > 0 + _, args = method_calls[-1] + assert args[0] == request.handle + assert args[1] == self.current_session_handle + assert args[4] == barriers + assert args[5] == self.current_zone_set + + return response, results + + def connect_to_eis(self): + inputcapture_intf = self.get_dbus_interface() + fd = inputcapture_intf.ConnectToEIS( + self.current_session_handle, dbus.Dictionary({}, signature="sv") + ) + + # Our dbusmock template sends HELLO + eis_socket = socket.fromfd(fd.take(), socket.AF_UNIX, socket.SOCK_STREAM) + hello = eis_socket.recv(10) + assert hello == b"HELLO" + + method_calls = self.mock_interface.GetMethodCalls("ConnectToEIS") + assert len(method_calls) > 0 + _, args = method_calls[-1] + assert args[0] == self.current_session_handle + + return eis_socket + + def enable(self): + inputcapture_intf = self.get_dbus_interface() + inputcapture_intf.Enable( + self.current_session_handle, dbus.Dictionary({}, signature="sv") + ) + + method_calls = self.mock_interface.GetMethodCalls("Enable") + assert len(method_calls) > 0 + _, args = method_calls[-1] + assert args[0] == self.current_session_handle + + def disable(self): + inputcapture_intf = self.get_dbus_interface() + inputcapture_intf.Disable( + self.current_session_handle, dbus.Dictionary({}, signature="sv") + ) + + method_calls = self.mock_interface.GetMethodCalls("Disable") + assert len(method_calls) > 0 + _, args = method_calls[-1] + assert args[0] == self.current_session_handle + + def release(self, activation_id: int, cursor_position=None): + options = {"activation_id": dbus.UInt32(activation_id)} + if cursor_position: + options["cursor_position"] = dbus.Struct( + list(cursor_position), signature="dd", variant_level=1 + ) + + inputcapture_intf = self.get_dbus_interface() + inputcapture_intf.Release( + self.current_session_handle, dbus.Dictionary(options, signature="sv") + ) + + method_calls = self.mock_interface.GetMethodCalls("Release") + assert len(method_calls) > 0 + _, args = method_calls[-1] + assert args[0] == self.current_session_handle + assert "activation_id" in args[2] + aid = args[2]["activation_id"] + assert aid == activation_id + if cursor_position: + assert "cursor_position" in args[2] + pos = args[2]["cursor_position"] + assert pos == cursor_position + + def test_version(self): + self.start_impl_portal() + self.start_xdp() + + properties_intf = self.get_dbus_interface("org.freedesktop.DBus.Properties") + version = properties_intf.Get("org.freedesktop.portal.InputCapture", "version") + EXPECTED_VERSION = 1 + assert version == EXPECTED_VERSION + + def test_supported_capabilities(self): + params = { + "supported_capabilities": 0b101, # KEYBOARD, POINTER, TOUCH + } + self.start_impl_portal(params) + self.start_xdp() + + properties_intf = self.get_dbus_interface("org.freedesktop.DBus.Properties") + caps = properties_intf.Get( + "org.freedesktop.portal.InputCapture", "SupportedCapabilities" + ) + assert caps == 0b101 + + def test_create_session(self): + self.start_impl_portal() + self.start_xdp() + + self.create_session(capabilities=0b1) # KEYBOARD + + # Check the impl portal was called with the right args + method_calls = self.mock_interface.GetMethodCalls("CreateSession") + assert len(method_calls) == 1 + _, args = method_calls.pop(0) + assert args[3] == "" # parent window + assert args[4]["capabilities"] == 0b1 + + def test_create_session_limited_caps(self): + params = { + "capabilities": 0b110, # TOUCH, POINTER + "supported_capabilities": 0b111, # TOUCH, POINTER, KEYBOARD + } + self.start_impl_portal(params) + self.start_xdp() + + # Request more caps than are supported + response, results = self.create_session(capabilities=0b111) + caps = results["capabilities"] + # Returned capabilities must the ones we set up in the params + assert caps == 0b110 + + # Check the impl portal was called with the right args + method_calls = self.mock_interface.GetMethodCalls("CreateSession") + assert len(method_calls) == 1 + _, args = method_calls.pop(0) + assert args[3] == "" # parent window + assert args[4]["capabilities"] == 0b111 + + def test_get_zones(self): + zones = [(1024, 768, 0, 0), (640, 480, 1024, 0)] + + params = { + "default-zone": dbus.Array( + [dbus.Struct(z, signature="uuii") for z in zones], + signature="(uuii)", + variant_level=1, + ), + } + self.start_impl_portal(params) + self.start_xdp() + + response, results = self.create_session() + response, results = self.get_zones() + for z1, z2 in zip(results["zones"], zones): + assert z1 == z2 + + # Check the impl portal was called with the right args + method_calls = self.mock_interface.GetMethodCalls("CreateSession") + assert len(method_calls) == 1 + method_calls = self.mock_interface.GetMethodCalls("GetZones") + assert len(method_calls) == 1 + + def test_set_pointer_barriers(self): + zones = [(1024, 768, 0, 0), (640, 480, 1024, 0)] + + params = { + "default-zone": dbus.Array( + [dbus.Struct(z, signature="uuii") for z in zones], + signature="(uuii)", + variant_level=1, + ), + } + self.start_impl_portal(params) + self.start_xdp() + + response, results = self.create_session() + response, results = self.get_zones() + + barriers = [ + { + "barrier_id": dbus.UInt32(10, variant_level=1), + "position": dbus.Struct( + [0, 0, 0, 768], signature="iiii", variant_level=1 + ), + }, + { + "barrier_id": dbus.UInt32(11, variant_level=1), + "position": dbus.Struct( + [0, 0, 1024, 0], signature="iiii", variant_level=1 + ), + }, + { + "barrier_id": dbus.UInt32(12, variant_level=1), + "position": dbus.Struct( + [1024, 0, 1024, 768], signature="iiii", variant_level=1 + ), + }, + { + "barrier_id": dbus.UInt32(13, variant_level=1), + "position": dbus.Struct( + [0, 768, 1024, 768], signature="iiii", variant_level=1 + ), + }, + { + "barrier_id": dbus.UInt32(14, variant_level=1), + "position": dbus.Struct( + [100, 768, 500, 768], signature="iiii", variant_level=1 + ), + }, + { + "barrier_id": dbus.UInt32(15, variant_level=1), + "position": dbus.Struct( + [1024, 0, 1024, 480], signature="iiii", variant_level=1 + ), + }, + { + "barrier_id": dbus.UInt32(16, variant_level=1), + "position": dbus.Struct( + [1024 + 640, 0, 1024 + 640, 480], signature="iiii", variant_level=1 + ), + }, + # invalid ones + { + "barrier_id": dbus.UInt32(20, variant_level=1), + "position": dbus.Struct( + [0, 1, 3, 4], signature="iiii", variant_level=1 + ), + }, + { + "barrier_id": dbus.UInt32(21, variant_level=1), + "position": dbus.Struct( + [0, 1, 1024, 1], signature="iiii", variant_level=1 + ), + }, + { + "barrier_id": dbus.UInt32(22, variant_level=1), + "position": dbus.Struct( + [1, 0, 1, 768], signature="iiii", variant_level=1 + ), + }, + { + "barrier_id": dbus.UInt32(23, variant_level=1), + "position": dbus.Struct( + [1023, 0, 1023, 768], signature="iiii", variant_level=1 + ), + }, + { + "barrier_id": dbus.UInt32(24, variant_level=1), + "position": dbus.Struct( + [0, 0, 1050, 0], signature="iiii", variant_level=1 + ), + }, + ] + response, results = self.set_pointer_barriers(barriers=barriers) + failed_barriers = results["failed_barriers"] + assert all([id >= 20 for id in failed_barriers]) + + for id in [b["barrier_id"] for b in barriers if b["barrier_id"] >= 20]: + assert id in failed_barriers + + # Check the impl portal was called with the right args + method_calls = self.mock_interface.GetMethodCalls("CreateSession") + assert len(method_calls) == 1 + method_calls = self.mock_interface.GetMethodCalls("GetZones") + assert len(method_calls) == 1 + method_calls = self.mock_interface.GetMethodCalls("SetPointerBarriers") + assert len(method_calls) == 1 + _, args = method_calls.pop(0) + assert args[4] == barriers + assert args[5] == self.current_zone_set + + def test_connect_to_eis(self): + self.start_impl_portal() + self.start_xdp() + + self.create_session() + self.get_zones() + + # The default zone is 1920x1080 + barriers = [ + { + "barrier_id": dbus.UInt32(10, variant_level=1), + "position": dbus.Struct( + [0, 0, 1920, 0], signature="iiii", variant_level=1 + ), + }, + ] + self.set_pointer_barriers(barriers) + + self.connect_to_eis() + + def test_enable_disable(self): + self.start_impl_portal() + self.start_xdp() + + self.create_session() + self.create_session() + self.get_zones() + + # The default zone is 1920x1080 + barriers = [ + { + "barrier_id": dbus.UInt32(10, variant_level=1), + "position": dbus.Struct( + [0, 0, 1920, 0], signature="iiii", variant_level=1 + ), + }, + ] + self.set_pointer_barriers(barriers) + self.connect_to_eis() + + # Disable before enable should be a noop + self.disable() + method_calls = self.mock_interface.GetMethodCalls("Disable") + assert len(method_calls) == 1 + + self.enable() + method_calls = self.mock_interface.GetMethodCalls("Enable") + assert len(method_calls) == 1 + + self.disable() + method_calls = self.mock_interface.GetMethodCalls("Disable") + assert len(method_calls) == 2 + + def test_disable_signal(self): + params = { + "disable-delay": 200, + } + self.start_impl_portal(params) + self.start_xdp() + + self.create_session() + self.get_zones() + # The default zone is 1920x1080 + barriers = [ + { + "barrier_id": dbus.UInt32(10, variant_level=1), + "position": dbus.Struct( + [0, 0, 1920, 0], signature="iiii", variant_level=1 + ), + }, + ] + self.set_pointer_barriers(barriers) + self.connect_to_eis() + + disabled_signal_received = False + + def cb_disabled(session_handle, options): + nonlocal disabled_signal_received + disabled_signal_received = True + assert session_handle == session_handle + + inputcapture_intf = self.get_dbus_interface() + inputcapture_intf.connect_to_signal("Disabled", cb_disabled) + + self.enable() + + mainloop = GLib.MainLoop() + GLib.timeout_add(500, mainloop.quit) + mainloop.run() + + assert disabled_signal_received + + def test_activated_signal(self): + params = { + "activated-delay": 200, + "deactivated-delay": 300, + } + self.start_impl_portal(params) + self.start_xdp() + + self.create_session() + self.get_zones() + # The default zone is 1920x1080 + barriers = [ + { + "barrier_id": dbus.UInt32(10, variant_level=1), + "position": dbus.Struct( + [0, 0, 1920, 0], signature="iiii", variant_level=1 + ), + }, + ] + self.set_pointer_barriers(barriers) + self.connect_to_eis() + + disabled_signal_received = False + activated_signal_received = False + deactivated_signal_received = False + + def cb_disabled(session_handle, options): + nonlocal disabled_signal_received + disabled_signal_received = True + + def cb_activated(session_handle, options): + nonlocal activated_signal_received + activated_signal_received = True + assert session_handle == session_handle + assert "activation_id" in options + assert "barrier_id" in options + assert options["barrier_id"] == 10 # template uses first barrier + assert "cursor_position" in options + assert options["cursor_position"] == ( + 10.0, + 20.0, + ) # template uses x+10, y+20 of first barrier + + def cb_deactivated(session_handle, options): + nonlocal deactivated_signal_received + deactivated_signal_received = True + assert session_handle == session_handle + assert "activation_id" in options + assert "cursor_position" in options + assert options["cursor_position"] == ( + 10.0, + 20.0, + ) # template uses x+10, y+20 of first barrier + + inputcapture_intf = self.get_dbus_interface() + inputcapture_intf.connect_to_signal("Activated", cb_activated) + inputcapture_intf.connect_to_signal("Deactivated", cb_deactivated) + inputcapture_intf.connect_to_signal("Disabled", cb_disabled) + + self.enable() + + mainloop = GLib.MainLoop() + GLib.timeout_add(500, mainloop.quit) + mainloop.run() + + assert activated_signal_received + assert deactivated_signal_received + assert not disabled_signal_received + + # Disabling should not trigger the signal + self.disable() + + mainloop = GLib.MainLoop() + GLib.timeout_add(500, mainloop.quit) + mainloop.run() + + assert not disabled_signal_received + + def test_release(self): + params = { + "activated-delay": 200, + "deactivated-delay": 1000, + "disabled-delay": 1200, + } + self.start_impl_portal(params) + self.start_xdp() + + self.create_session() + self.get_zones() + # The default zone is 1920x1080 + barriers = [ + { + "barrier_id": dbus.UInt32(10, variant_level=1), + "position": dbus.Struct( + [0, 0, 1920, 0], signature="iiii", variant_level=1 + ), + }, + ] + self.set_pointer_barriers(barriers) + self.connect_to_eis() + + disabled_signal_received = False + activated_signal_received = False + deactivated_signal_received = False + activation_id = None + + def cb_disabled(session_handle, options): + nonlocal disabled_signal_received + disabled_signal_received = True + + def cb_activated(session_handle, options): + nonlocal activated_signal_received, activation_id + activated_signal_received = True + activation_id = options["activation_id"] + + def cb_deactivated(session_handle, options): + nonlocal deactivated_signal_received + deactivated_signal_received = True + + inputcapture_intf = self.get_dbus_interface() + inputcapture_intf.connect_to_signal("Disabled", cb_activated) + inputcapture_intf.connect_to_signal("Activated", cb_activated) + inputcapture_intf.connect_to_signal("Deactivated", cb_deactivated) + + self.enable() + + mainloop = GLib.MainLoop() + GLib.timeout_add(300, mainloop.quit) + mainloop.run() + + assert activated_signal_received + assert activation_id is not None + assert not deactivated_signal_received + assert not disabled_signal_received + + self.release(cursor_position=(10.0, 50.0), activation_id=activation_id) + + # XDP should filter any signals the implementation may + # send after Release(). + + mainloop = GLib.MainLoop() + GLib.timeout_add(1000, mainloop.quit) + mainloop.run() + + # Release() implies deactivated + assert not deactivated_signal_received + assert not disabled_signal_received