8f3fccbd38055a761eb3839960144c105e95d953
[platform/upstream/glib.git] / gio / gactiongroupexporter.c
1 /*
2  * Copyright © 2010 Codethink Limited
3  * Copyright © 2011 Canonical Limited
4  *
5  * This program is free software: you can redistribute it and/or modify
6  * it under the terms of the GNU Lesser General Public License as published
7  * by the Free Software Foundation; either version 2 of the licence or (at
8  * your option) any later version.
9  *
10  * This library is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  * Lesser General Public License for more details.
14  *
15  * You should have received a copy of the GNU Lesser General
16  * Public License along with this library; if not, write to the
17  * Free Software Foundation, Inc., 59 Temple Place, Suite 330,
18  * Boston, MA 02111-1307, USA.
19  *
20  * Authors: Ryan Lortie <desrt@desrt.ca>
21  */
22
23 #include "config.h"
24
25 #include "gactiongroupexporter.h"
26
27 #include "gdbusmethodinvocation.h"
28 #include "gdbusintrospection.h"
29 #include "gdbusconnection.h"
30 #include "gdbusactiongroup.h"
31 #include "gactiongroup.h"
32 #include "gapplication.h"
33 #include "gdbuserror.h"
34
35 extern GDBusActionGroupEmitHookFunc g_dbus_action_group_before_emit_hook;
36 extern GDBusActionGroupEmitHookFunc g_dbus_action_group_after_emit_hook;
37
38 /**
39  * SECTION:gactiongroupexporter
40  * @title: GActionGroup exporter
41  * @short_description: Export GActionGroups on D-Bus
42  * @see_also: #GActionGroup, #GDBusActionGroup
43  *
44  * These functions support exporting a #GActionGroup on D-Bus.
45  * The D-Bus interface that is used is a private implementation
46  * detail.
47  *
48  * To access an exported #GActionGroup remotely, use
49  * g_dbus_action_group_new() to obtain a #GDBusActionGroup.
50  */
51
52 G_GNUC_INTERNAL GVariant *
53 g_action_group_describe_action (GActionGroup *action_group,
54                                 const gchar  *name)
55 {
56   const GVariantType *type;
57   GVariantBuilder builder;
58   gboolean enabled;
59   GVariant *state;
60
61   g_variant_builder_init (&builder, G_VARIANT_TYPE ("(bgav)"));
62
63   enabled = g_action_group_get_action_enabled (action_group, name);
64   g_variant_builder_add (&builder, "b", enabled);
65
66   if ((type = g_action_group_get_action_parameter_type (action_group, name)))
67     {
68       gchar *str = g_variant_type_dup_string (type);
69       g_variant_builder_add (&builder, "g", str);
70       g_free (str);
71     }
72   else
73     g_variant_builder_add (&builder, "g", "");
74
75   g_variant_builder_open (&builder, G_VARIANT_TYPE ("av"));
76   if ((state = g_action_group_get_action_state (action_group, name)))
77     {
78       g_variant_builder_add (&builder, "v", state);
79       g_variant_unref (state);
80     }
81   g_variant_builder_close (&builder);
82
83   return g_variant_builder_end (&builder);
84 }
85
86 /* The org.gtk.Actions interface
87  * =============================
88  *
89  * This interface describes a group of actions.
90  *
91  * Each action:
92  * - has a unique string name
93  * - can be activated
94  * - optionally has a parameter type that must be given to the activation
95  * - has an enabled state that may be true or false
96  * - optionally has a state which can change value, but not type
97  *
98  * Methods
99  * -------
100  *
101  * List :: () → (as)
102  *
103  *   Lists the names of the actions exported at this object path.
104  *
105  * Describe :: (s) → (bgav)
106  *
107  *   Describes a single action, or a given name.
108  *
109  *   The return value has the following components:
110  *   b: specifies if the action is currently enabled. This is
111  *      a hint that attempting to interact with the action will
112  *      produce no effect.
113  *   g: specifies the optional parameter type. If not "",
114  *      the string specifies the type of argument that must
115  *      be passed to the activation.
116  *   av: specifies the optional state. If not empty, the array
117  *       contains the current value of the state as a variant
118  *
119  * DescribeAll :: () → (a{s(bgav)})
120  *
121  *   Describes all actions in a single round-trip.
122  *
123  *   The dictionary maps action name strings to their descriptions
124  *   (in the format discussed above).
125  *
126  * Activate :: (sava{sv}) → ()
127  *
128  *   Requests activation of the named action.
129  *
130  *   The action is named by the first parameter (s).
131  *
132  *   If the action activation requires a parameter then this parameter
133  *   must be given in the second parameter (av). If there is no parameter
134  *   to be specified, the array must be empty.
135  *
136  *   The final parameter (a{sv}) is a list of "platform data".
137  *
138  *   This method is not guaranteed to have any particular effect. The
139  *   implementation may take some action (including changing the state
140  *   of the action, if it is stateful) or it may take none at all. In
141  *   particular, callers should expect their request to be completely
142  *   ignored when the enabled flag is false (but even this is not
143  *   guaranteed).
144  *
145  * SetState :: (sva{sv}) → ()
146  *
147  *   Requests the state of an action to be changed to the given value.
148  *
149  *   The action is named by the first parameter (s).
150  *
151  *   The requested new state is given in the second parameter (v).
152  *   It must be equal in type to the existing state.
153  *
154  *   The final parameter (a{sv}) is a list of "platform data".
155  *
156  *   This method is not guaranteed to have any particular effect.
157  *   The implementation of an action can choose to ignore the requested
158  *   state change, or choose to change its state to something else or
159  *   to trigger other side effects. In particular, callers should expect
160  *   their request to be completely ignored when the enabled flag is
161  *   false (but even this is not guaranteed).
162  *
163  * Signals
164  * -------
165  *
166  * Changed :: (asa{sb}a{sv}a{s(bgav)})
167  *
168  *   Signals that some change has occured to the action group.
169  *
170  *   Four separate types of changes are possible, and the 4 parameters
171  *   of the change signal reflect these possibilities:
172  *   as: a list of removed actions
173  *   a{sb}: a list of actions that had their enabled flag changed
174  *   a{sv}: a list of actions that had their state changed
175  *   a{s(bgav)}: a list of new actions added in the same format as
176  *               the return value of the DescribeAll method
177  */
178
179 /* Using XML saves us dozens of relocations vs. using the introspection
180  * structure types.  We only need to burn cycles and memory if we
181  * actually use the exporter -- not in every single app using GIO.
182  *
183  * It's also a lot easier to read. :)
184  */
185 const char org_gtk_Actions_xml[] =
186   "<node>"
187   "  <interface name='org.gtk.Actions'>"
188   "    <method name='List'>"
189   "      <arg type='as' name='list' direction='out'/>"
190   "    </method>"
191   "    <method name='Describe'>"
192   "      <arg type='s' name='action_name' direction='in'/>"
193   "      <arg type='(bgav)' name='description' direction='out'/>"
194   "    </method>"
195   "    <method name='DescribeAll'>"
196   "      <arg type='a{s(bgav)}' name='descriptions' direction='out'/>"
197   "    </method>"
198   "    <method name='Activate'>"
199   "      <arg type='s' name='action_name' direction='in'/>"
200   "      <arg type='av' name='parameter' direction='in'/>"
201   "      <arg type='a{sv}' name='platform_data' direction='in'/>"
202   "    </method>"
203   "    <method name='SetState'>"
204   "      <arg type='s' name='action_name' direction='in'/>"
205   "      <arg type='v' name='value' direction='in'/>"
206   "      <arg type='a{sv}' name='platform_data' direction='in'/>"
207   "    </method>"
208   "    <signal name='Changed'>"
209   "      <arg type='as' name='removals'/>"
210   "      <arg type='a{sb}' name='enable_changes'/>"
211   "      <arg type='a{sv}' name='state_changes'/>"
212   "      <arg type='a{s(bgav)}' name='additions'/>"
213   "    </signal>"
214   "  </interface>"
215   "</node>";
216
217 static GDBusInterfaceInfo *org_gtk_Actions;
218
219 typedef struct
220 {
221   GActionGroup    *action_group;
222   GDBusConnection *connection;
223   GMainContext    *context;
224   gchar           *object_path;
225   GHashTable      *pending_changes;
226   GSource         *pending_source;
227 } GActionGroupExporter;
228
229 #define ACTION_ADDED_EVENT             (1u<<0)
230 #define ACTION_REMOVED_EVENT           (1u<<1)
231 #define ACTION_STATE_CHANGED_EVENT     (1u<<2)
232 #define ACTION_ENABLED_CHANGED_EVENT   (1u<<3)
233
234 static gboolean
235 g_action_group_exporter_dispatch_events (gpointer user_data)
236 {
237   GActionGroupExporter *exporter = user_data;
238   GVariantBuilder removes;
239   GVariantBuilder enabled_changes;
240   GVariantBuilder state_changes;
241   GVariantBuilder adds;
242   GHashTableIter iter;
243   gpointer value;
244   gpointer key;
245
246   g_variant_builder_init (&removes, G_VARIANT_TYPE_STRING_ARRAY);
247   g_variant_builder_init (&enabled_changes, G_VARIANT_TYPE ("a{sb}"));
248   g_variant_builder_init (&state_changes, G_VARIANT_TYPE ("a{sv}"));
249   g_variant_builder_init (&adds, G_VARIANT_TYPE ("a{s(bgav)}"));
250
251   g_hash_table_iter_init (&iter, exporter->pending_changes);
252   while (g_hash_table_iter_next (&iter, &key, &value))
253     {
254       guint events = GPOINTER_TO_INT (value);
255       const gchar *name = key;
256
257       /* Adds and removes are incompatible with enabled or state
258        * changes, but we must report at least one event.
259        */
260       g_assert (((events & (ACTION_ENABLED_CHANGED_EVENT | ACTION_STATE_CHANGED_EVENT)) == 0) !=
261                 ((events & (ACTION_REMOVED_EVENT | ACTION_ADDED_EVENT)) == 0));
262
263       if (events & ACTION_REMOVED_EVENT)
264         g_variant_builder_add (&removes, "s", name);
265
266       if (events & ACTION_ENABLED_CHANGED_EVENT)
267         {
268           gboolean enabled;
269
270           enabled = g_action_group_get_action_enabled (exporter->action_group, name);
271           g_variant_builder_add (&enabled_changes, "{sb}", name, enabled);
272         }
273
274       if (events & ACTION_STATE_CHANGED_EVENT)
275         {
276           GVariant *state;
277
278           state = g_action_group_get_action_state (exporter->action_group, name);
279           g_variant_builder_add (&state_changes, "{sv}", name, state);
280           g_variant_unref (state);
281         }
282
283       if (events & ACTION_ADDED_EVENT)
284         {
285           GVariant *description;
286
287           description = g_action_group_describe_action (exporter->action_group, name);
288           g_variant_builder_add (&adds, "{s@(bgav)}", name, description);
289         }
290     }
291
292   g_hash_table_remove_all (exporter->pending_changes);
293
294   g_dbus_connection_emit_signal (exporter->connection, NULL, exporter->object_path,
295                                  "org.gtk.Actions", "Changed",
296                                  g_variant_new ("(asa{sb}a{sv}a{s(bgav)})",
297                                                 &removes, &enabled_changes,
298                                                 &state_changes, &adds),
299                                  NULL);
300
301   exporter->pending_source = NULL;
302
303   return FALSE;
304 }
305
306 static guint
307 g_action_group_exporter_get_events (GActionGroupExporter *exporter,
308                                     const gchar          *name)
309 {
310   return (gsize) g_hash_table_lookup (exporter->pending_changes, name);
311 }
312
313 static void
314 g_action_group_exporter_set_events (GActionGroupExporter *exporter,
315                                     const gchar          *name,
316                                     guint                 events)
317 {
318   gboolean have_events;
319   gboolean is_queued;
320
321   if (events != 0)
322     g_hash_table_insert (exporter->pending_changes, g_strdup (name), GINT_TO_POINTER (events));
323   else
324     g_hash_table_remove (exporter->pending_changes, name);
325
326   have_events = g_hash_table_size (exporter->pending_changes) > 0;
327   is_queued = exporter->pending_source != NULL;
328
329   if (have_events && !is_queued)
330     {
331       GSource *source;
332
333       source = g_idle_source_new ();
334       exporter->pending_source = source;
335       g_source_set_callback (source, g_action_group_exporter_dispatch_events, exporter, NULL);
336       g_source_attach (source, exporter->context);
337       g_source_unref (source);
338     }
339
340   if (!have_events && is_queued)
341     {
342       g_source_destroy (exporter->pending_source);
343       exporter->pending_source = NULL;
344     }
345 }
346
347 static void
348 g_action_group_exporter_action_added (GActionGroup *action_group,
349                                       const gchar  *action_name,
350                                       gpointer      user_data)
351 {
352   GActionGroupExporter *exporter = user_data;
353   guint event_mask;
354
355   event_mask = g_action_group_exporter_get_events (exporter, action_name);
356
357   /* The action is new, so we should not have any pending
358    * enabled-changed or state-changed signals for it.
359    */
360   g_assert (~event_mask & (ACTION_STATE_CHANGED_EVENT |
361                            ACTION_ENABLED_CHANGED_EVENT));
362
363   event_mask |= ACTION_ADDED_EVENT;
364
365   g_action_group_exporter_set_events (exporter, action_name, event_mask);
366 }
367
368 static void
369 g_action_group_exporter_action_removed (GActionGroup *action_group,
370                                         const gchar  *action_name,
371                                         gpointer      user_data)
372 {
373   GActionGroupExporter *exporter = user_data;
374   guint event_mask;
375
376   event_mask = g_action_group_exporter_get_events (exporter, action_name);
377
378   /* If the add event for this is still queued then just cancel it since
379    * it's gone now.
380    *
381    * If the event was freshly added, there should not have been any
382    * enabled or state changed events.
383    */
384   if (event_mask & ACTION_ADDED_EVENT)
385     {
386       g_assert (~event_mask & ~(ACTION_STATE_CHANGED_EVENT | ACTION_ENABLED_CHANGED_EVENT));
387       event_mask &= ~ACTION_ADDED_EVENT;
388     }
389
390   /* Otherwise, queue a remove event.  Drop any state or enabled changes
391    * that were queued before the remove. */
392   else
393     {
394       event_mask |= ACTION_REMOVED_EVENT;
395       event_mask &= ~(ACTION_STATE_CHANGED_EVENT | ACTION_ENABLED_CHANGED_EVENT);
396     }
397
398   g_action_group_exporter_set_events (exporter, action_name, event_mask);
399 }
400
401 static void
402 g_action_group_exporter_action_state_changed (GActionGroup *action_group,
403                                               const gchar  *action_name,
404                                               GVariant     *value,
405                                               gpointer      user_data)
406 {
407   GActionGroupExporter *exporter = user_data;
408   guint event_mask;
409
410   event_mask = g_action_group_exporter_get_events (exporter, action_name);
411
412   /* If it was removed, it must have been added back.  Otherwise, why
413    * are we hearing about changes?
414    */
415   g_assert (~event_mask & ACTION_REMOVED_EVENT ||
416             event_mask & ACTION_ADDED_EVENT);
417
418   /* If it is freshly added, don't also bother with the state change
419    * signal since the updated state will be sent as part of the pending
420    * add message.
421    */
422   if (~event_mask & ACTION_ADDED_EVENT)
423     event_mask |= ACTION_STATE_CHANGED_EVENT;
424
425   g_action_group_exporter_set_events (exporter, action_name, event_mask);
426 }
427
428 static void
429 g_action_group_exporter_action_enabled_changed (GActionGroup *action_group,
430                                                 const gchar  *action_name,
431                                                 gboolean      enabled,
432                                                 gpointer      user_data)
433 {
434   GActionGroupExporter *exporter = user_data;
435   guint event_mask;
436
437   event_mask = g_action_group_exporter_get_events (exporter, action_name);
438
439   /* Reasoning as above. */
440   g_assert (~event_mask & ACTION_REMOVED_EVENT ||
441             event_mask & ACTION_ADDED_EVENT);
442
443   if (~event_mask & ACTION_ADDED_EVENT)
444     event_mask |= ACTION_ENABLED_CHANGED_EVENT;
445
446   g_action_group_exporter_set_events (exporter, action_name, event_mask);
447 }
448
449 static void
450 g_action_group_exporter_pre_emit (GActionGroupExporter *exporter,
451                                   GVariant             *platform_data)
452 {
453   if (G_IS_APPLICATION (exporter->action_group))
454     G_APPLICATION_GET_CLASS (exporter->action_group)
455       ->before_emit (G_APPLICATION (exporter->action_group), platform_data);
456
457   else if (g_dbus_action_group_before_emit_hook != NULL)
458     (* g_dbus_action_group_before_emit_hook) (exporter->action_group, platform_data);
459 }
460
461 static void
462 g_action_group_exporter_post_emit (GActionGroupExporter *exporter,
463                                    GVariant             *platform_data)
464 {
465   if (G_IS_APPLICATION (exporter->action_group))
466     G_APPLICATION_GET_CLASS (exporter->action_group)
467       ->after_emit (G_APPLICATION (exporter->action_group), platform_data);
468
469   else if (g_dbus_action_group_after_emit_hook != NULL)
470     (* g_dbus_action_group_after_emit_hook) (exporter->action_group, platform_data);
471 }
472
473 static void
474 org_gtk_Actions_method_call (GDBusConnection       *connection,
475                              const gchar           *sender,
476                              const gchar           *object_path,
477                              const gchar           *interface_name,
478                              const gchar           *method_name,
479                              GVariant              *parameters,
480                              GDBusMethodInvocation *invocation,
481                              gpointer               user_data)
482 {
483   GActionGroupExporter *exporter = user_data;
484   GVariant *result = NULL;
485
486   if (g_str_equal (method_name, "List"))
487     {
488       gchar **list;
489
490       list = g_action_group_list_actions (exporter->action_group);
491       result = g_variant_new ("(^as)", list);
492       g_strfreev (list);
493     }
494
495   else if (g_str_equal (method_name, "Describe"))
496     {
497       const gchar *name;
498       GVariant *desc;
499
500       g_variant_get (parameters, "(&s)", &name);
501       desc = g_action_group_describe_action (exporter->action_group, name);
502       result = g_variant_new ("(@(bgav))", desc);
503     }
504
505   else if (g_str_equal (method_name, "DescribeAll"))
506     {
507       GVariantBuilder builder;
508       gchar **list;
509       gint i;
510
511       list = g_action_group_list_actions (exporter->action_group);
512       g_variant_builder_init (&builder, G_VARIANT_TYPE ("a{s(bgav)}"));
513       for (i = 0; list[i]; i++)
514         {
515           const gchar *name = list[i];
516           GVariant *description;
517
518           description = g_action_group_describe_action (exporter->action_group, name);
519           g_variant_builder_add (&builder, "{s@(bgav)}", name, description);
520         }
521       result = g_variant_new ("(a{s(bgav)})", &builder);
522       g_strfreev (list);
523     }
524
525   else if (g_str_equal (method_name, "Activate"))
526     {
527       GVariant *parameter = NULL;
528       GVariant *platform_data;
529       GVariantIter *iter;
530       const gchar *name;
531
532       g_variant_get (parameters, "(&sav@a{sv})", &name, &iter, &platform_data);
533       g_variant_iter_next (iter, "v", &parameter);
534       g_variant_iter_free (iter);
535
536       g_action_group_exporter_pre_emit (exporter, platform_data);
537       g_action_group_activate_action (exporter->action_group, name, parameter);
538       g_action_group_exporter_post_emit (exporter, platform_data);
539
540       if (parameter)
541         g_variant_unref (parameter);
542
543       g_variant_unref (platform_data);
544     }
545
546   else if (g_str_equal (method_name, "SetState"))
547     {
548       GVariant *platform_data;
549       const gchar *name;
550       GVariant *state;
551
552       g_variant_get (parameters, "(&sv@a{sv})", &name, &state, &platform_data);
553       g_action_group_exporter_pre_emit (exporter, platform_data);
554       g_action_group_change_action_state (exporter->action_group, name, state);
555       g_action_group_exporter_post_emit (exporter, platform_data);
556       g_variant_unref (platform_data);
557       g_variant_unref (state);
558     }
559
560   else
561     g_assert_not_reached ();
562
563   g_dbus_method_invocation_return_value (invocation, result);
564 }
565
566 static void
567 g_action_group_exporter_free (gpointer user_data)
568 {
569   GActionGroupExporter *exporter = user_data;
570
571   g_signal_handlers_disconnect_by_func (exporter->action_group,
572                                         g_action_group_exporter_action_added, exporter);
573   g_signal_handlers_disconnect_by_func (exporter->action_group,
574                                         g_action_group_exporter_action_enabled_changed, exporter);
575   g_signal_handlers_disconnect_by_func (exporter->action_group,
576                                         g_action_group_exporter_action_state_changed, exporter);
577   g_signal_handlers_disconnect_by_func (exporter->action_group,
578                                         g_action_group_exporter_action_removed, exporter);
579
580   g_hash_table_unref (exporter->pending_changes);
581   if (exporter->pending_source)
582     g_source_destroy (exporter->pending_source);
583
584   g_main_context_unref (exporter->context);
585   g_object_unref (exporter->connection);
586   g_object_unref (exporter->action_group);
587   g_free (exporter->object_path);
588
589   g_slice_free (GActionGroupExporter, exporter);
590 }
591
592 /**
593  * g_dbus_connection_export_action_group:
594  * @connection: a #GDBusConnection
595  * @object_path: a D-Bus object path
596  * @action_group: a #GActionGroup
597  * @error: a pointer to a %NULL #GError, or %NULL
598  *
599  * Exports @action_group on @connection at @object_path.
600  *
601  * The implemented D-Bus API should be considered private.  It is
602  * subject to change in the future.
603  *
604  * A given object path can only have one action group exported on it.
605  * If this constraint is violated, the export will fail and 0 will be
606  * returned (with @error set accordingly).
607  *
608  * You can unexport the action group using
609  * g_dbus_connection_unexport_action_group() with the return value of
610  * this function.
611  *
612  * The thread default main context is taken at the time of this call.
613  * All incoming action activations and state change requests are
614  * reported from this context.  Any changes on the action group that
615  * cause it to emit signals must also come from this same context.
616  * Since incoming action activations and state change requests are
617  * rather likely to cause changes on the action group, this effectively
618  * limits a given action group to being exported from only one main
619  * context.
620  *
621  * Returns: the ID of the export (never zero), or 0 in case of failure
622  *
623  * Since: 2.32
624  **/
625 guint
626 g_dbus_connection_export_action_group (GDBusConnection  *connection,
627                                        const gchar      *object_path,
628                                        GActionGroup     *action_group,
629                                        GError          **error)
630 {
631   const GDBusInterfaceVTable vtable = {
632     org_gtk_Actions_method_call
633   };
634   GActionGroupExporter *exporter;
635   guint id;
636
637   if G_UNLIKELY (org_gtk_Actions == NULL)
638     {
639       GError *error = NULL;
640       GDBusNodeInfo *info;
641
642       info = g_dbus_node_info_new_for_xml (org_gtk_Actions_xml, &error);
643       if G_UNLIKELY (info == NULL)
644         g_error ("%s", error->message);
645       org_gtk_Actions = g_dbus_node_info_lookup_interface (info, "org.gtk.Actions");
646       g_assert (org_gtk_Actions != NULL);
647       g_dbus_interface_info_ref (org_gtk_Actions);
648       g_dbus_node_info_unref (info);
649     }
650
651   exporter = g_slice_new (GActionGroupExporter);
652   id = g_dbus_connection_register_object (connection, object_path, org_gtk_Actions, &vtable,
653                                           exporter, g_action_group_exporter_free, error);
654
655   if (id == 0)
656     {
657       g_slice_free (GActionGroupExporter, exporter);
658       return 0;
659     }
660
661   exporter->context = g_main_context_ref_thread_default ();
662   exporter->pending_changes = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
663   exporter->pending_source = NULL;
664   exporter->action_group = g_object_ref (action_group);
665   exporter->connection = g_object_ref (connection);
666   exporter->object_path = g_strdup (object_path);
667
668   g_signal_connect (action_group, "action-added",
669                     G_CALLBACK (g_action_group_exporter_action_added), exporter);
670   g_signal_connect (action_group, "action-removed",
671                     G_CALLBACK (g_action_group_exporter_action_removed), exporter);
672   g_signal_connect (action_group, "action-state-changed",
673                     G_CALLBACK (g_action_group_exporter_action_state_changed), exporter);
674   g_signal_connect (action_group, "action-enabled-changed",
675                     G_CALLBACK (g_action_group_exporter_action_enabled_changed), exporter);
676
677   return id;
678 }
679
680 /**
681  * g_dbus_connection_unexport_action_group:
682  * @connection: a #GDBusConnection
683  * @export_id: the ID from g_dbus_connection_export_action_group()
684  *
685  * Reverses the effect of a previous call to
686  * g_dbus_connection_export_action_group().
687  *
688  * It is an error to call this function with an ID that wasn't returned
689  * from g_dbus_connection_export_action_group() or to call it with the
690  * same ID more than once.
691  *
692  * Since: 2.32
693  **/
694 void
695 g_dbus_connection_unexport_action_group (GDBusConnection *connection,
696                                          guint            export_id)
697 {
698   g_dbus_connection_unregister_object (connection, export_id);
699 }