1 /*
2 * Copyright (C) 2018, Matthias Clasen
3 *
4 * This file is free software; you can redistribute it and/or modify it
5 * under the terms of the GNU Lesser General Public License as
6 * published by the Free Software Foundation, version 3.0 of the
7 * License.
8 *
9 * This file is distributed in the hope that it will be useful, but
10 * WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12 * Lesser General Public License for more details.
13 *
14 * You should have received a copy of the GNU Lesser General Public
15 * License along with this program. If not, see <http://www.gnu.org/licenses/>.
16 *
17 * SPDX-License-Identifier: LGPL-3.0-only
18 */
19
20 #include "config.h"
21
22 #include "inhibit.h"
23 #include "portal-private.h"
24
25 typedef struct {
26 XdpPortal *portal;
27 XdpParent *parent;
28 char *parent_handle;
29 XdpInhibitFlags inhibit;
30 guint signal_id;
31 guint cancelled_id;
32 char *request_path;
33 char *reason;
34 GTask *task;
35 int id;
36 } InhibitCall;
37
38 static void
inhibit_call_free(InhibitCall * call)39 inhibit_call_free (InhibitCall *call)
40 {
41 if (call->parent)
42 {
43 call->parent->parent_unexport (call->parent);
44 xdp_parent_free (call->parent);
45 }
46 g_free (call->parent_handle);
47
48 if (call->signal_id)
49 g_dbus_connection_signal_unsubscribe (call->portal->bus, call->signal_id);
50
51 if (call->cancelled_id)
52 g_signal_handler_disconnect (g_task_get_cancellable (call->task), call->cancelled_id);
53
54 g_object_unref (call->portal);
55 g_object_unref (call->task);
56
57 g_free (call->reason);
58 g_free (call->request_path);
59
60 g_free (call);
61 }
62
63 static void do_inhibit (InhibitCall *call);
64
65 static void
inhibit_parent_exported(XdpParent * parent,const char * handle,gpointer data)66 inhibit_parent_exported (XdpParent *parent,
67 const char *handle,
68 gpointer data)
69 {
70 InhibitCall *call = data;
71 call->parent_handle = g_strdup (handle);
72 do_inhibit (call);
73 }
74
75 static void
call_returned(GObject * object,GAsyncResult * result,gpointer data)76 call_returned (GObject *object,
77 GAsyncResult *result,
78 gpointer data)
79 {
80 InhibitCall *call = data;
81 GError *error = NULL;
82 g_autoptr(GVariant) ret = NULL;
83
84 ret = g_dbus_connection_call_finish (G_DBUS_CONNECTION (object), result, &error);
85 if (error)
86 {
87 g_hash_table_remove (call->portal->inhibit_handles, GINT_TO_POINTER (call->id));
88 g_task_return_error (call->task, error);
89 inhibit_call_free (call);
90 }
91 }
92
93 static void
response_received(GDBusConnection * bus,const char * sender_name,const char * object_path,const char * interface_name,const char * signal_name,GVariant * parameters,gpointer data)94 response_received (GDBusConnection *bus,
95 const char *sender_name,
96 const char *object_path,
97 const char *interface_name,
98 const char *signal_name,
99 GVariant *parameters,
100 gpointer data)
101 {
102 InhibitCall *call = data;
103 guint32 response;
104 g_autoptr(GVariant) ret = NULL;
105
106 g_variant_get (parameters, "(u@a{sv})", &response, &ret);
107
108 if (response == 0)
109 g_task_return_int (call->task, call->id);
110 else if (response == 1)
111 {
112 g_hash_table_remove (call->portal->inhibit_handles, GINT_TO_POINTER (call->id));
113 g_task_return_new_error (call->task, G_IO_ERROR, G_IO_ERROR_CANCELLED, "Account call canceled user");
114 }
115 else
116 {
117 g_hash_table_remove (call->portal->inhibit_handles, GINT_TO_POINTER (call->id));
118 g_task_return_new_error (call->task, G_IO_ERROR, G_IO_ERROR_FAILED, "Account call failed");
119 }
120
121 inhibit_call_free (call);
122 }
123
124 static void
inhibit_cancelled_cb(GCancellable * cancellable,gpointer data)125 inhibit_cancelled_cb (GCancellable *cancellable,
126 gpointer data)
127 {
128 InhibitCall *call = data;
129
130 g_debug ("inhibit cancelled, calling Close");
131 g_dbus_connection_call (call->portal->bus,
132 PORTAL_BUS_NAME,
133 call->request_path,
134 REQUEST_INTERFACE,
135 "Close",
136 NULL,
137 NULL,
138 G_DBUS_CALL_FLAGS_NONE,
139 -1,
140 NULL, NULL, NULL);
141
142 g_hash_table_remove (call->portal->inhibit_handles, GINT_TO_POINTER (call->id));
143 g_task_return_new_error (call->task, G_IO_ERROR, G_IO_ERROR_CANCELLED, "Inhibit call canceled by caller");
144
145 inhibit_call_free (call);
146 }
147
148 static void
do_inhibit(InhibitCall * call)149 do_inhibit (InhibitCall *call)
150 {
151 GVariantBuilder options;
152 g_autofree char *token = NULL;
153 GCancellable *cancellable;
154
155 if (call->parent_handle == NULL)
156 {
157 call->parent->parent_export (call->parent, inhibit_parent_exported, call);
158 return;
159 }
160
161 token = g_strdup_printf ("portal%d", g_random_int_range (0, G_MAXINT));
162 call->request_path = g_strconcat (REQUEST_PATH_PREFIX, call->portal->sender, "/", token, NULL);
163 call->signal_id = g_dbus_connection_signal_subscribe (call->portal->bus,
164 PORTAL_BUS_NAME,
165 REQUEST_INTERFACE,
166 "Response",
167 call->request_path,
168 NULL,
169 G_DBUS_SIGNAL_FLAGS_NO_MATCH_RULE,
170 response_received,
171 call,
172 NULL);
173
174 g_hash_table_insert (call->portal->inhibit_handles, GINT_TO_POINTER (call->id), g_strdup (call->request_path));
175
176 cancellable = g_task_get_cancellable (call->task);
177 if (cancellable)
178 call->cancelled_id = g_signal_connect (cancellable, "cancelled", G_CALLBACK (inhibit_cancelled_cb), call);
179
180 g_variant_builder_init (&options, G_VARIANT_TYPE_VARDICT);
181 g_variant_builder_add (&options, "{sv}", "handle_token", g_variant_new_string (token));
182 g_variant_builder_add (&options, "{sv}", "reason", g_variant_new_string (call->reason));
183
184 g_dbus_connection_call (call->portal->bus,
185 PORTAL_BUS_NAME,
186 PORTAL_OBJECT_PATH,
187 "org.freedesktop.portal.Inhibit",
188 "Inhibit",
189 g_variant_new ("(sua{sv})", call->parent_handle, call->inhibit, &options),
190 NULL,
191 G_DBUS_CALL_FLAGS_NONE,
192 -1,
193 NULL,
194 call_returned,
195 call);
196 }
197
198 /**
199 * xdp_portal_session_inhibit:
200 * @portal: a [class@Portal]
201 * @parent: (nullable): parent window information
202 * @reason: (nullable): user-visible reason for the inhibition
203 * @flags: information about what to inhibit
204 * @cancellable: (nullable): optional [class@Gio.Cancellable]
205 * @callback: (scope async): a callback to call when the request is done
206 * @data: (closure): data to pass to @callback
207 *
208 * Inhibits various session status changes.
209 *
210 * To obtain an ID that can be used to undo the inhibition, use
211 * [method@Portal.session_inhibit_finish] in the callback.
212 *
213 * To remove an active inhibitor, call [method@Portal.session_uninhibit]
214 * with the same ID.
215 */
216 void
xdp_portal_session_inhibit(XdpPortal * portal,XdpParent * parent,const char * reason,XdpInhibitFlags flags,GCancellable * cancellable,GAsyncReadyCallback callback,gpointer data)217 xdp_portal_session_inhibit (XdpPortal *portal,
218 XdpParent *parent,
219 const char *reason,
220 XdpInhibitFlags flags,
221 GCancellable *cancellable,
222 GAsyncReadyCallback callback,
223 gpointer data)
224 {
225 InhibitCall *call = NULL;
226
227 g_return_if_fail (XDP_IS_PORTAL (portal));
228 g_return_if_fail ((flags & ~(XDP_INHIBIT_FLAG_LOGOUT |
229 XDP_INHIBIT_FLAG_USER_SWITCH |
230 XDP_INHIBIT_FLAG_SUSPEND |
231 XDP_INHIBIT_FLAG_IDLE)) == 0);
232
233 if (portal->inhibit_handles == NULL)
234 portal->inhibit_handles = g_hash_table_new_full (NULL, NULL, NULL, g_free);
235
236 portal->next_inhibit_id++;
237 if (portal->next_inhibit_id < 0)
238 portal->next_inhibit_id = 1;
239
240 call = g_new0 (InhibitCall, 1);
241 call->portal = g_object_ref (portal);
242 if (parent)
243 call->parent = xdp_parent_copy (parent);
244 else
245 call->parent_handle = g_strdup ("");
246 call->inhibit = flags;
247 call->id = portal->next_inhibit_id;
248 call->reason = g_strdup (reason);
249 call->task = g_task_new (portal, cancellable, callback, data);
250 g_task_set_source_tag (call->task, xdp_portal_session_inhibit);
251
252 do_inhibit (call);
253 }
254
255 /**
256 * xdp_portal_session_inhibit_finish:
257 * @portal: a [class@Portal]
258 * @result: a [iface@Gio.AsyncResult]
259 * @error: return location for an error
260 *
261 * Finishes the inhbit request, and returns the ID of the
262 * inhibition as a positive integer. The ID can be passed to
263 * [method@Portal.session_uninhibit] to undo the inhibition.
264 *
265 * Returns: the ID of the inhibition, or -1 if there was an error
266 */
267 int
xdp_portal_session_inhibit_finish(XdpPortal * portal,GAsyncResult * result,GError ** error)268 xdp_portal_session_inhibit_finish (XdpPortal *portal,
269 GAsyncResult *result,
270 GError **error)
271 {
272 g_return_val_if_fail (XDP_IS_PORTAL (portal), -1);
273 g_return_val_if_fail (g_task_is_valid (result, portal), -1);
274 g_return_val_if_fail (g_task_get_source_tag (G_TASK (result)) == xdp_portal_session_inhibit, -1);
275
276 return g_task_propagate_int (G_TASK (result), error);
277 }
278
279 /**
280 * xdp_portal_session_uninhibit:
281 * @portal: a [class@Portal]
282 * @id: unique ID for an active inhibition
283 *
284 * Removes an inhibitor that was created by a call
285 * to [method@Portal.session_inhibit].
286 */
287 void
xdp_portal_session_uninhibit(XdpPortal * portal,int id)288 xdp_portal_session_uninhibit (XdpPortal *portal,
289 int id)
290 {
291 gpointer key;
292 g_autofree char *value = NULL;
293
294 g_return_if_fail (XDP_IS_PORTAL (portal));
295 g_return_if_fail (id > 0);
296
297 if (portal->inhibit_handles == NULL ||
298 !g_hash_table_steal_extended (portal->inhibit_handles,
299 GINT_TO_POINTER (id),
300 (gpointer *)&key,
301 (gpointer *)&value))
302 {
303 g_warning ("No inhibit handle found");
304 return;
305 }
306
307 g_dbus_connection_call (portal->bus,
308 PORTAL_BUS_NAME,
309 value,
310 REQUEST_INTERFACE,
311 "Close",
312 g_variant_new ("()"),
313 G_VARIANT_TYPE_UNIT,
314 G_DBUS_CALL_FLAGS_NONE,
315 G_MAXINT,
316 NULL, NULL, NULL);
317 }
318
319 typedef struct {
320 XdpPortal *portal;
321 XdpParent *parent;
322 char *parent_handle;
323 GTask *task;
324 char *request_path;
325 guint signal_id;
326 guint cancelled_id;
327 char *id;
328 } CreateMonitorCall;
329
330 static void
create_monitor_call_free(CreateMonitorCall * call)331 create_monitor_call_free (CreateMonitorCall *call)
332 {
333 if (call->parent)
334 {
335 call->parent->parent_unexport (call->parent);
336 xdp_parent_free (call->parent);
337 }
338 g_free (call->parent_handle);
339
340 if (call->signal_id)
341 g_dbus_connection_signal_unsubscribe (call->portal->bus, call->signal_id);
342
343 if (call->cancelled_id)
344 g_signal_handler_disconnect (g_task_get_cancellable (call->task), call->cancelled_id);
345
346 g_free (call->request_path);
347 g_free (call->id);
348
349 g_object_unref (call->portal);
350 g_object_unref (call->task);
351
352 g_free (call);
353 }
354
355 static void
session_state_changed(GDBusConnection * bus,const char * sender_name,const char * object_path,const char * interface_name,const char * signal_name,GVariant * parameters,gpointer data)356 session_state_changed (GDBusConnection *bus,
357 const char *sender_name,
358 const char *object_path,
359 const char *interface_name,
360 const char *signal_name,
361 GVariant *parameters,
362 gpointer data)
363 {
364 XdpPortal *portal = data;
365 const char *handle;
366 g_autoptr(GVariant) state = NULL;
367 gboolean screensaver_active = FALSE;
368 XdpLoginSessionState session_state = XDP_LOGIN_SESSION_RUNNING;
369
370 g_variant_get (parameters, "(&o@a{sv})", &handle, &state);
371 if (g_strcmp0 (handle, portal->session_monitor_handle) != 0)
372 {
373 g_warning ("Session monitor handle mismatch");
374 return;
375 }
376
377 g_variant_lookup (state, "screensaver-active", "b", &screensaver_active);
378 g_variant_lookup (state, "session-state", "u", &session_state);
379
380 g_signal_emit_by_name (portal, "session-state-changed",
381 screensaver_active,
382 session_state);
383 }
384
385 static void
ensure_session_monitor_connection(XdpPortal * portal)386 ensure_session_monitor_connection (XdpPortal *portal)
387 {
388 if (portal->state_changed_signal == 0)
389 portal->state_changed_signal =
390 g_dbus_connection_signal_subscribe (portal->bus,
391 PORTAL_BUS_NAME,
392 "org.freedesktop.portal.Inhibit",
393 "StateChanged",
394 PORTAL_OBJECT_PATH,
395 NULL,
396 G_DBUS_SIGNAL_FLAGS_NO_MATCH_RULE,
397 session_state_changed,
398 portal,
399 NULL);
400 }
401
402 static void
create_response_received(GDBusConnection * bus,const char * sender_name,const char * object_path,const char * interface_name,const char * signal_name,GVariant * parameters,gpointer data)403 create_response_received (GDBusConnection *bus,
404 const char *sender_name,
405 const char *object_path,
406 const char *interface_name,
407 const char *signal_name,
408 GVariant *parameters,
409 gpointer data)
410 {
411 CreateMonitorCall *call = data;
412 guint32 response;
413 g_autoptr(GVariant) ret = NULL;
414
415 if (call->cancelled_id)
416 {
417 g_signal_handler_disconnect (g_task_get_cancellable (call->task), call->cancelled_id);
418 call->cancelled_id = 0;
419 }
420
421 g_variant_get (parameters, "(u@a{sv})", &response, &ret);
422
423 if (response == 0)
424 {
425 call->portal->session_monitor_handle = g_strdup (call->id);
426 ensure_session_monitor_connection (call->portal);
427 g_task_return_boolean (call->task, TRUE);
428 }
429 else if (response == 1)
430 g_task_return_new_error (call->task, G_IO_ERROR, G_IO_ERROR_CANCELLED, "CreateMonitor canceled");
431 else
432 g_task_return_new_error (call->task, G_IO_ERROR, G_IO_ERROR_FAILED, "CreateMonitor failed");
433
434 create_monitor_call_free (call);
435 }
436
437 static void create_monitor (CreateMonitorCall *call);
438
439 static void
create_parent_exported(XdpParent * parent,const char * handle,gpointer data)440 create_parent_exported (XdpParent *parent,
441 const char *handle,
442 gpointer data)
443 {
444 CreateMonitorCall *call = data;
445 call->parent_handle = g_strdup (handle);
446 create_monitor (call);
447 }
448
449 static void
create_cancelled_cb(GCancellable * cancellable,gpointer data)450 create_cancelled_cb (GCancellable *cancellable,
451 gpointer data)
452 {
453 CreateMonitorCall *call = data;
454
455 g_dbus_connection_call (call->portal->bus,
456 PORTAL_BUS_NAME,
457 call->request_path,
458 REQUEST_INTERFACE,
459 "Close",
460 NULL,
461 NULL,
462 G_DBUS_CALL_FLAGS_NONE,
463 -1,
464 NULL, NULL, NULL);
465 }
466
467 static void
create_returned(GObject * object,GAsyncResult * result,gpointer data)468 create_returned (GObject *object,
469 GAsyncResult *result,
470 gpointer data)
471 {
472 CreateMonitorCall *call = data;
473 GError *error = NULL;
474 g_autoptr(GVariant) ret = NULL;
475
476 ret = g_dbus_connection_call_finish (G_DBUS_CONNECTION (object), result, &error);
477 if (error)
478 {
479 g_task_return_error (call->task, error);
480 create_monitor_call_free (call);
481 }
482 }
483
484 static void
create_monitor(CreateMonitorCall * call)485 create_monitor (CreateMonitorCall *call)
486 {
487 GVariantBuilder options;
488 g_autofree char *token = NULL;
489 g_autofree char *session_token = NULL;
490 GCancellable *cancellable;
491
492 if (call->portal->session_monitor_handle)
493 {
494 g_task_return_boolean (call->task, TRUE);
495 create_monitor_call_free (call);
496 return;
497 }
498
499 if (call->parent_handle == NULL)
500 {
501 call->parent->parent_export (call->parent, create_parent_exported, call);
502 return;
503 }
504
505 token = g_strdup_printf ("portal%d", g_random_int_range (0, G_MAXINT));
506 call->request_path = g_strconcat (REQUEST_PATH_PREFIX, call->portal->sender, "/", token, NULL);
507 call->signal_id = g_dbus_connection_signal_subscribe (call->portal->bus,
508 PORTAL_BUS_NAME,
509 REQUEST_INTERFACE,
510 "Response",
511 call->request_path,
512 NULL,
513 G_DBUS_SIGNAL_FLAGS_NO_MATCH_RULE,
514 create_response_received,
515 call,
516 NULL);
517
518 cancellable = g_task_get_cancellable (call->task);
519 if (cancellable)
520 call->cancelled_id = g_signal_connect (cancellable, "cancelled", G_CALLBACK (create_cancelled_cb), call);
521
522 session_token = g_strdup_printf ("portal%d", g_random_int_range (0, G_MAXINT));
523 call->id = g_strconcat (SESSION_PATH_PREFIX, call->portal->sender, "/", session_token, NULL);
524
525 g_variant_builder_init (&options, G_VARIANT_TYPE_VARDICT);
526 g_variant_builder_add (&options, "{sv}", "handle_token", g_variant_new_string (token));
527 g_variant_builder_add (&options, "{sv}", "session_handle_token", g_variant_new_string (session_token));
528 g_dbus_connection_call (call->portal->bus,
529 PORTAL_BUS_NAME,
530 PORTAL_OBJECT_PATH,
531 "org.freedesktop.portal.Inhibit",
532 "CreateMonitor",
533 g_variant_new ("(sa{sv})", call->parent_handle, &options),
534 NULL,
535 G_DBUS_CALL_FLAGS_NONE,
536 -1,
537 NULL,
538 create_returned,
539 call);
540
541 }
542
543 /**
544 * xdp_portal_session_monitor_start:
545 * @portal: a [class@Portal]
546 * @parent: (nullable): a XdpParent, or `NULL`
547 * @flags: options for this call
548 * @cancellable: (nullable): optional [class@Gio.Cancellable]
549 * @callback: (scope async): a callback to call when the request is done
550 * @data: (closure): data to pass to @callback
551 *
552 * Makes [class@Portal] start monitoring the login session state.
553 *
554 * When the state changes, the [signal@Portal::session-state-changed]
555 * signal is emitted.
556 *
557 * Use [method@Portal.session_monitor_stop] to stop monitoring.
558 */
559 void
xdp_portal_session_monitor_start(XdpPortal * portal,XdpParent * parent,XdpSessionMonitorFlags flags,GCancellable * cancellable,GAsyncReadyCallback callback,gpointer data)560 xdp_portal_session_monitor_start (XdpPortal *portal,
561 XdpParent *parent,
562 XdpSessionMonitorFlags flags,
563 GCancellable *cancellable,
564 GAsyncReadyCallback callback,
565 gpointer data)
566
567 {
568 CreateMonitorCall *call;
569
570 g_return_if_fail (XDP_IS_PORTAL (portal));
571 g_return_if_fail (flags == XDP_SESSION_MONITOR_FLAG_NONE);
572
573 call = g_new0 (CreateMonitorCall, 1);
574 call->portal = g_object_ref (portal);
575 if (parent)
576 call->parent = xdp_parent_copy (parent);
577 else
578 call->parent_handle = g_strdup ("");
579 call->task = g_task_new (portal, cancellable, callback, data);
580 g_task_set_source_tag (call->task, xdp_portal_session_monitor_start);
581
582 create_monitor (call);
583 }
584
585 /**
586 * xdp_portal_session_monitor_start_finish:
587 * @portal: a [class@Portal]
588 * @result: a [iface@Gio.AsyncResult]
589 * @error: return location for an error
590 *
591 * Finishes a session-monitor request, and returns
592 * the result in the form of boolean.
593 *
594 * Returns: `TRUE` if the request succeeded
595 */
596 gboolean
xdp_portal_session_monitor_start_finish(XdpPortal * portal,GAsyncResult * result,GError ** error)597 xdp_portal_session_monitor_start_finish (XdpPortal *portal,
598 GAsyncResult *result,
599 GError **error)
600 {
601 g_return_val_if_fail (XDP_IS_PORTAL (portal), FALSE);
602 g_return_val_if_fail (g_task_is_valid (result, portal), FALSE);
603 g_return_val_if_fail (g_task_get_source_tag (G_TASK (result)) == xdp_portal_session_monitor_start, FALSE);
604
605 return g_task_propagate_boolean (G_TASK (result), error);
606 }
607
608 /**
609 * xdp_portal_session_monitor_stop:
610 * @portal: a [class@Portal]
611 *
612 * Stops session state monitoring that was started with
613 * [method@Portal.session_monitor_start].
614 */
615 void
xdp_portal_session_monitor_stop(XdpPortal * portal)616 xdp_portal_session_monitor_stop (XdpPortal *portal)
617 {
618 g_return_if_fail (XDP_IS_PORTAL (portal));
619
620 if (portal->state_changed_signal)
621 {
622 g_dbus_connection_signal_unsubscribe (portal->bus, portal->state_changed_signal);
623 portal->state_changed_signal = 0;
624 }
625
626 if (portal->session_monitor_handle)
627 {
628 g_dbus_connection_call (portal->bus,
629 PORTAL_BUS_NAME,
630 portal->session_monitor_handle,
631 SESSION_INTERFACE,
632 "Close",
633 NULL,
634 NULL, 0, -1, NULL, NULL, NULL);
635 g_clear_pointer (&portal->session_monitor_handle, g_free);
636 }
637 }
638
639 /**
640 * xdp_portal_session_monitor_query_end_response:
641 * @portal: a [class@Portal]
642 *
643 * This method should be called within one second of
644 * receiving a [signal@Portal::session-state-changed] signal
645 * with the 'Query End' state, to acknowledge that they
646 * have handled the state change.
647 *
648 * Possible ways to handle the state change are either
649 * to call [method@Portal.session_inhibit] to prevent the
650 * session from ending, or to save your state and get
651 * ready for the session to end.
652 */
653 void
xdp_portal_session_monitor_query_end_response(XdpPortal * portal)654 xdp_portal_session_monitor_query_end_response (XdpPortal *portal)
655 {
656 g_return_if_fail (XDP_IS_PORTAL (portal));
657
658 if (portal->session_monitor_handle != NULL)
659 g_dbus_connection_call (portal->bus,
660 PORTAL_BUS_NAME,
661 PORTAL_OBJECT_PATH,
662 "org.freedesktop.portal.Inhibit",
663 "QueryEndResponse",
664 g_variant_new ("(o)", portal->session_monitor_handle),
665 NULL, 0, -1, NULL, NULL, NULL);
666 }
667