]> git.0d.be Git - empathy.git/blob - libempathy/empathy-dispatcher.c
Merge branch 'sasl'
[empathy.git] / libempathy / empathy-dispatcher.c
1 /* * Copyright (C) 2007-2009 Collabora Ltd.
2  *
3  * This library is free software; you can redistribute it and/or
4  * modify it under the terms of the GNU Lesser General Public
5  * License as published by the Free Software Foundation; either
6  * version 2.1 of the License, or (at your option) any later version.
7  *
8  * This library is distributed in the hope that it will be useful,
9  * but WITHOUT ANY WARRANTY; without even the implied warranty of
10  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
11  * Lesser General Public License for more details.
12  *
13  * You should have received a copy of the GNU Lesser General Public
14  * License along with this library; if not, write to the Free Software
15  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
16  *
17  * Authors: Xavier Claessens <xclaesse@gmail.com>
18  *          Sjoerd Simons <sjoerd.simons@collabora.co.uk>
19  *          Cosimo Cecchi <cosimo.cecchi@collabora.co.uk>
20  */
21
22 #define DISPATCHER_BUS_NAME TP_CLIENT_BUS_NAME_BASE "Empathy"
23 #define DISPATCHER_OBJECT_PATH TP_CLIENT_OBJECT_PATH_BASE "Empathy"
24
25 #include <config.h>
26
27 #include <string.h>
28
29 #include <glib/gi18n-lib.h>
30
31 #include <telepathy-glib/account-manager.h>
32 #include <telepathy-glib/enums.h>
33 #include <telepathy-glib/connection.h>
34 #include <telepathy-glib/util.h>
35 #include <telepathy-glib/dbus.h>
36 #include <telepathy-glib/proxy-subclass.h>
37 #include <telepathy-glib/gtypes.h>
38 #include <telepathy-glib/defs.h>
39 #include <telepathy-glib/interfaces.h>
40
41 #include <extensions/extensions.h>
42
43 #include "empathy-dispatcher.h"
44 #include "empathy-utils.h"
45 #include "empathy-tp-contact-factory.h"
46 #include "empathy-chatroom-manager.h"
47 #include "empathy-utils.h"
48
49 #define DEBUG_FLAG EMPATHY_DEBUG_DISPATCHER
50 #include <libempathy/empathy-debug.h>
51
52 #define GET_PRIV(obj) EMPATHY_GET_PRIV (obj, EmpathyDispatcher)
53 typedef struct
54 {
55   gboolean dispose_has_run;
56
57   TpAccountManager *account_manager;
58   /* connection to connection data mapping */
59   GHashTable *connections;
60   GHashTable *outstanding_classes_requests;
61
62   /* reffed (TpAccount *) => gulong
63    * Signal handler ID of the "status-changed" signal */
64   GHashTable *status_changed_handlers;
65 } EmpathyDispatcherPriv;
66
67 G_DEFINE_TYPE (EmpathyDispatcher, empathy_dispatcher, G_TYPE_OBJECT);
68
69 static EmpathyDispatcher *dispatcher = NULL;
70
71 static void dispatcher_init_connection_if_needed (
72     EmpathyDispatcher *dispatcher,
73     TpConnection *connection);
74
75 static GList * empathy_dispatcher_find_channel_classes
76   (EmpathyDispatcher *dispatcher, TpConnection *connection,
77    const gchar *channel_type, guint handle_type, GArray *fixed_properties);
78
79 typedef struct
80 {
81   TpChannel *channel;
82   /* Channel type specific wrapper object */
83   GObject *channel_wrapper;
84 } DispatchData;
85
86 typedef struct
87 {
88   /* List of requestable channel classes */
89   GPtrArray *requestable_channels;
90 } ConnectionData;
91
92 typedef struct
93 {
94   EmpathyDispatcher *dispatcher;
95   TpConnection *connection;
96   char *channel_type;
97   guint handle_type;
98   GArray *properties;
99   EmpathyDispatcherFindChannelClassCb *callback;
100   gpointer user_data;
101 } FindChannelRequest;
102
103 static ConnectionData *
104 new_connection_data (void)
105 {
106   return g_slice_new0 (ConnectionData);
107 }
108
109 static void
110 free_connection_data (ConnectionData *cd)
111 {
112   guint i;
113
114   if (cd->requestable_channels  != NULL)
115     {
116       for (i = 0 ; i < cd->requestable_channels->len ; i++)
117           g_value_array_free (
118             g_ptr_array_index (cd->requestable_channels, i));
119       g_ptr_array_free (cd->requestable_channels, TRUE);
120     }
121 }
122
123 static void
124 free_find_channel_request (FindChannelRequest *r)
125 {
126   guint idx;
127   char *str;
128
129   g_object_unref (r->dispatcher);
130   g_free (r->channel_type);
131
132   if (r->properties != NULL)
133     {
134       for (idx = 0; idx < r->properties->len ; idx++)
135         {
136           str = g_array_index (r->properties, char *, idx);
137           g_free (str);
138         }
139
140       g_array_free (r->properties, TRUE);
141     }
142
143   g_slice_free (FindChannelRequest, r);
144 }
145
146 static void
147 dispatcher_connection_invalidated_cb (TpConnection *connection,
148                                       guint domain,
149                                       gint code,
150                                       gchar *message,
151                                       EmpathyDispatcher *self)
152 {
153   EmpathyDispatcherPriv *priv = GET_PRIV (self);
154
155   DEBUG ("Error: %s", message);
156
157   g_hash_table_remove (priv->connections, connection);
158 }
159
160 static void
161 got_connection_rcc (EmpathyDispatcher *self,
162     TpConnection *connection)
163 {
164   EmpathyDispatcherPriv *priv = GET_PRIV (self);
165   TpCapabilities *caps;
166   ConnectionData *cd;
167   GList *requests, *l;
168   FindChannelRequest *request;
169   GList *retval;
170
171   caps = tp_connection_get_capabilities (connection);
172   g_assert (caps != NULL);
173
174   cd = g_hash_table_lookup (priv->connections, connection);
175   g_assert (cd != NULL);
176
177   cd->requestable_channels = g_boxed_copy (
178     TP_ARRAY_TYPE_REQUESTABLE_CHANNEL_CLASS_LIST,
179     tp_capabilities_get_channel_classes (caps));
180
181   requests = g_hash_table_lookup (priv->outstanding_classes_requests,
182       connection);
183
184   for (l = requests; l != NULL; l = l->next)
185     {
186       request = l->data;
187
188       retval = empathy_dispatcher_find_channel_classes (self,
189           connection, request->channel_type,
190           request->handle_type, request->properties);
191       request->callback (retval, request->user_data);
192
193       free_find_channel_request (request);
194       g_list_free (retval);
195     }
196
197   g_list_free (requests);
198
199   g_hash_table_remove (priv->outstanding_classes_requests, connection);
200 }
201
202 static void
203 connection_prepare_cb (GObject *source,
204     GAsyncResult *result,
205     gpointer user_data)
206 {
207   EmpathyDispatcher *self = EMPATHY_DISPATCHER (user_data);
208   GError *error = NULL;
209   TpConnection *connection = (TpConnection *) source;
210
211   if (!tp_proxy_prepare_finish (source, result, &error))
212     {
213       DEBUG ("Error: %s", error->message);
214
215       g_error_free (error);
216       goto out;
217     }
218
219   got_connection_rcc (self, connection);
220
221 out:
222   g_object_unref (self);
223 }
224
225 static void
226 dispatcher_init_connection_if_needed (EmpathyDispatcher *self,
227     TpConnection *connection)
228 {
229   EmpathyDispatcherPriv *priv = GET_PRIV (self);
230   GQuark features[] = { TP_CONNECTION_FEATURE_CORE,
231          TP_CONNECTION_FEATURE_CAPABILITIES,
232          0 };
233
234   if (g_hash_table_lookup (priv->connections, connection) != NULL)
235     return;
236
237   g_hash_table_insert (priv->connections, g_object_ref (connection),
238     new_connection_data ());
239
240   g_signal_connect (connection, "invalidated",
241     G_CALLBACK (dispatcher_connection_invalidated_cb), self);
242
243   /* Ensure to keep the self object alive while preparing the connection */
244   g_object_ref (self);
245
246   tp_proxy_prepare_async (connection, features, connection_prepare_cb, self);
247 }
248
249 static void
250 dispatcher_status_changed_cb (TpAccount *account,
251                               guint old_status,
252                               guint new_status,
253                               guint reason,
254                               gchar *dbus_error_name,
255                               GHashTable *details,
256                               EmpathyDispatcher *self)
257 {
258   TpConnection *conn = tp_account_get_connection (account);
259
260   if (conn != NULL)
261     dispatcher_init_connection_if_needed (self, conn);
262 }
263
264 static GObject *
265 dispatcher_constructor (GType type,
266                         guint n_construct_params,
267                         GObjectConstructParam *construct_params)
268 {
269   GObject *retval;
270   EmpathyDispatcherPriv *priv;
271
272   if (dispatcher != NULL)
273     return g_object_ref (dispatcher);
274
275   retval = G_OBJECT_CLASS (empathy_dispatcher_parent_class)->constructor
276     (type, n_construct_params, construct_params);
277
278   dispatcher = EMPATHY_DISPATCHER (retval);
279   g_object_add_weak_pointer (retval, (gpointer) &dispatcher);
280
281   priv = GET_PRIV (dispatcher);
282
283   return retval;
284 }
285
286 static void
287 dispatcher_dispose (GObject *object)
288 {
289   EmpathyDispatcherPriv *priv = GET_PRIV (object);
290   GHashTableIter iter;
291   gpointer connection;
292
293   if (priv->dispose_has_run)
294     return;
295
296   priv->dispose_has_run = TRUE;
297
298   g_hash_table_iter_init (&iter, priv->connections);
299   while (g_hash_table_iter_next (&iter, &connection, NULL))
300     {
301       g_signal_handlers_disconnect_by_func (connection,
302           dispatcher_connection_invalidated_cb, object);
303     }
304
305   g_hash_table_destroy (priv->connections);
306   priv->connections = NULL;
307
308   G_OBJECT_CLASS (empathy_dispatcher_parent_class)->dispose (object);
309 }
310
311 static void
312 dispatcher_finalize (GObject *object)
313 {
314   EmpathyDispatcherPriv *priv = GET_PRIV (object);
315   GHashTableIter iter;
316   gpointer connection;
317   GList *list;
318   gpointer account, id;
319
320   g_hash_table_iter_init (&iter, priv->outstanding_classes_requests);
321   while (g_hash_table_iter_next (&iter, &connection, (gpointer *) &list))
322     {
323       g_list_foreach (list, (GFunc) free_find_channel_request, NULL);
324       g_list_free (list);
325     }
326
327   g_hash_table_iter_init (&iter, priv->status_changed_handlers);
328   while (g_hash_table_iter_next (&iter, &account, &id))
329     {
330       g_signal_handler_disconnect (account, GPOINTER_TO_UINT (id));
331     }
332   g_hash_table_destroy (priv->status_changed_handlers);
333
334   g_object_unref (priv->account_manager);
335
336   g_hash_table_destroy (priv->outstanding_classes_requests);
337 }
338
339 static void
340 empathy_dispatcher_class_init (EmpathyDispatcherClass *klass)
341 {
342   GObjectClass *object_class = G_OBJECT_CLASS (klass);
343
344   object_class->dispose = dispatcher_dispose;
345   object_class->finalize = dispatcher_finalize;
346   object_class->constructor = dispatcher_constructor;
347
348   g_type_class_add_private (object_class, sizeof (EmpathyDispatcherPriv));
349 }
350
351 static void
352 connect_account (EmpathyDispatcher *self,
353     TpAccount *account)
354 {
355   EmpathyDispatcherPriv *priv = GET_PRIV (self);
356   TpConnection *conn = tp_account_get_connection (account);
357   gulong id;
358
359   id = GPOINTER_TO_UINT (g_hash_table_lookup (priv->status_changed_handlers,
360         account));
361
362   if (id != 0)
363     return;
364
365   if (conn != NULL)
366     dispatcher_status_changed_cb (account, 0, 0, 0, NULL, NULL, self);
367
368   id = g_signal_connect (account, "status-changed",
369       G_CALLBACK (dispatcher_status_changed_cb), self);
370
371   g_hash_table_insert (priv->status_changed_handlers, g_object_ref (account),
372       GUINT_TO_POINTER (id));
373 }
374
375 static void
376 account_manager_prepared_cb (GObject *source_object,
377                              GAsyncResult *result,
378                              gpointer user_data)
379 {
380   GList *accounts, *l;
381   EmpathyDispatcher *self = user_data;
382   TpAccountManager *account_manager = TP_ACCOUNT_MANAGER (source_object);
383   GError *error = NULL;
384
385   if (!tp_account_manager_prepare_finish (account_manager, result, &error))
386     {
387       DEBUG ("Failed to prepare account manager: %s", error->message);
388       g_error_free (error);
389       return;
390     }
391
392   accounts = tp_account_manager_get_valid_accounts (account_manager);
393   for (l = accounts; l; l = l->next)
394     {
395       TpAccount *a = l->data;
396
397       connect_account (self, a);
398     }
399
400   g_list_foreach (accounts, (GFunc) g_object_ref, NULL);
401   g_list_free (accounts);
402 }
403
404 static void
405 account_prepare_cb (GObject *source_object,
406     GAsyncResult *result,
407     gpointer user_data)
408 {
409   EmpathyDispatcher *self = user_data;
410   TpAccount *account = TP_ACCOUNT (source_object);
411   GError *error = NULL;
412
413   if (!tp_account_prepare_finish (account, result, &error))
414     {
415       DEBUG ("Failed to prepare account: %s", error->message);
416       g_error_free (error);
417       return;
418     }
419
420   connect_account (self, account);
421 }
422
423 static void
424 account_validity_changed_cb (TpAccountManager *manager,
425     TpAccount *account,
426     gboolean valid,
427     gpointer user_data)
428 {
429   if (!valid)
430     return;
431
432   tp_account_prepare_async (account, NULL, account_prepare_cb, user_data);
433 }
434
435 static void
436 empathy_dispatcher_init (EmpathyDispatcher *self)
437 {
438   EmpathyDispatcherPriv *priv = G_TYPE_INSTANCE_GET_PRIVATE (self,
439     EMPATHY_TYPE_DISPATCHER, EmpathyDispatcherPriv);
440
441   self->priv = priv;
442   priv->account_manager = tp_account_manager_dup ();
443
444   priv->connections = g_hash_table_new_full (g_direct_hash, g_direct_equal,
445     g_object_unref, (GDestroyNotify) free_connection_data);
446
447   priv->outstanding_classes_requests = g_hash_table_new_full (g_direct_hash,
448     g_direct_equal, g_object_unref, NULL);
449
450   tp_account_manager_prepare_async (priv->account_manager, NULL,
451       account_manager_prepared_cb, self);
452
453   tp_g_signal_connect_object (priv->account_manager,
454       "account-validity-changed", G_CALLBACK (account_validity_changed_cb),
455       self, 0);
456
457   priv->status_changed_handlers = g_hash_table_new_full (NULL, NULL,
458       (GDestroyNotify) g_object_unref, NULL);
459 }
460
461 EmpathyDispatcher *
462 empathy_dispatcher_dup_singleton (void)
463 {
464   return EMPATHY_DISPATCHER (g_object_new (EMPATHY_TYPE_DISPATCHER, NULL));
465 }
466
467 void
468 empathy_dispatcher_chat_with_contact (EmpathyContact *contact,
469     gint64 timestamp)
470 {
471   empathy_dispatcher_chat_with_contact_id (
472       empathy_contact_get_account (contact), empathy_contact_get_id (contact),
473       timestamp);
474 }
475
476 static void
477 ensure_text_channel_cb (GObject *source,
478     GAsyncResult *result,
479     gpointer user_data)
480 {
481   GError *error = NULL;
482
483   if (!tp_account_channel_request_ensure_channel_finish (
484         TP_ACCOUNT_CHANNEL_REQUEST (source), result, &error))
485     {
486       DEBUG ("Failed to ensure text channel: %s", error->message);
487       g_error_free (error);
488     }
489 }
490
491 void
492 empathy_dispatcher_chat_with_contact_id (TpAccount *account,
493     const gchar *contact_id,
494     gint64 timestamp)
495 {
496   GHashTable *request;
497   TpAccountChannelRequest *req;
498
499   request = tp_asv_new (
500       TP_PROP_CHANNEL_CHANNEL_TYPE, G_TYPE_STRING,
501         TP_IFACE_CHANNEL_TYPE_TEXT,
502       TP_PROP_CHANNEL_TARGET_HANDLE_TYPE, G_TYPE_UINT, TP_HANDLE_TYPE_CONTACT,
503       TP_PROP_CHANNEL_TARGET_ID, G_TYPE_STRING, contact_id,
504       NULL);
505
506   req = tp_account_channel_request_new (account, request, timestamp);
507
508   tp_account_channel_request_ensure_channel_async (req, NULL, NULL,
509       ensure_text_channel_cb, NULL);
510
511   g_hash_table_unref (request);
512   g_object_unref (req);
513 }
514
515 void
516 empathy_dispatcher_join_muc (TpAccount *account,
517     const gchar *room_name,
518     gint64 timestamp)
519 {
520   GHashTable *request;
521   TpAccountChannelRequest *req;
522
523   request = tp_asv_new (
524       TP_PROP_CHANNEL_CHANNEL_TYPE, G_TYPE_STRING,
525         TP_IFACE_CHANNEL_TYPE_TEXT,
526       TP_PROP_CHANNEL_TARGET_HANDLE_TYPE, G_TYPE_UINT, TP_HANDLE_TYPE_ROOM,
527       TP_PROP_CHANNEL_TARGET_ID, G_TYPE_STRING, room_name,
528       NULL);
529
530   req = tp_account_channel_request_new (account, request, timestamp);
531
532   tp_account_channel_request_ensure_channel_async (req, NULL, NULL,
533       ensure_text_channel_cb, NULL);
534
535   g_hash_table_unref (request);
536   g_object_unref (req);
537 }
538
539 static gboolean
540 channel_class_matches (GValueArray *class,
541                        const char *channel_type,
542                        guint handle_type,
543                        GArray *fixed_properties)
544 {
545   GHashTable *fprops;
546   GValue *v;
547   const char *c_type;
548   guint h_type;
549   gboolean valid;
550
551   v = g_value_array_get_nth (class, 0);
552
553   /* if the class doesn't match channel type discard it. */
554   fprops = g_value_get_boxed (v);
555   c_type = tp_asv_get_string (fprops, TP_IFACE_CHANNEL ".ChannelType");
556
557   if (tp_strdiff (channel_type, c_type))
558     return FALSE;
559
560   /* we have the right channel type, see if the handle type matches */
561   h_type = tp_asv_get_uint32 (fprops,
562                               TP_IFACE_CHANNEL ".TargetHandleType", &valid);
563
564   if (!valid || (handle_type != h_type && handle_type != TP_UNKNOWN_HANDLE_TYPE))
565     return FALSE;
566
567   if (fixed_properties != NULL)
568     {
569       gpointer h_key, h_val;
570       guint idx;
571       GHashTableIter iter;
572       gboolean found;
573
574       g_hash_table_iter_init (&iter, fprops);
575
576       while (g_hash_table_iter_next (&iter, &h_key, &h_val))
577         {
578           /* discard ChannelType and TargetHandleType, as we already
579            * checked them.
580            */
581           if (!tp_strdiff ((char *) h_key, TP_IFACE_CHANNEL ".ChannelType") ||
582               !tp_strdiff
583                 ((char *) h_key, TP_IFACE_CHANNEL ".TargetHandleType"))
584             continue;
585
586           found = FALSE;
587
588           for (idx = 0; idx < fixed_properties->len; idx++)
589             {
590               /* if |key| doesn't exist in |fixed_properties|, discard
591                * the class.
592                */
593               if (!tp_strdiff
594                     ((char *) h_key,
595                      g_array_index (fixed_properties, char *, idx)))
596                 {
597                   found = TRUE;
598                   /* exit the for() loop */
599                   break;
600                 }
601             }
602
603           if (!found)
604             return FALSE;
605         }
606     }
607   else
608     {
609       /* if no fixed_properties are specified, discard the classes
610        * with some fixed properties other than the two we already
611        * checked.
612        */
613       if (g_hash_table_size (fprops) > 2)
614         return FALSE;
615     }
616
617   return TRUE;
618 }
619
620 static GList *
621 empathy_dispatcher_find_channel_classes (EmpathyDispatcher *self,
622                                          TpConnection *connection,
623                                          const gchar *channel_type,
624                                          guint handle_type,
625                                          GArray *fixed_properties)
626 {
627   EmpathyDispatcherPriv *priv = GET_PRIV (self);
628   GValueArray *class;
629   GPtrArray *classes;
630   GList *matching_classes;
631   guint i;
632   ConnectionData *cd;
633
634   g_return_val_if_fail (channel_type != NULL, NULL);
635
636   cd = g_hash_table_lookup (priv->connections, connection);
637
638   if (cd == NULL)
639     return NULL;
640
641   classes = cd->requestable_channels;
642   if (classes == NULL)
643     return NULL;
644
645   matching_classes = NULL;
646
647   for (i = 0; i < classes->len; i++)
648     {
649       class = g_ptr_array_index (classes, i);
650
651       if (!channel_class_matches
652           (class, channel_type, handle_type, fixed_properties))
653         continue;
654
655       matching_classes = g_list_prepend (matching_classes, class);
656     }
657
658   return matching_classes;
659 }
660
661 static GArray *
662 setup_varargs (va_list var_args,
663                const char *channel_namespace,
664                const char *first_property_name)
665 {
666   const char *name;
667   char *name_full;
668   GArray *properties;
669
670   if (first_property_name == NULL)
671     return NULL;
672
673   name = first_property_name;
674   properties = g_array_new (TRUE, TRUE, sizeof (char *));
675
676   while (name != NULL)
677     {
678       name_full = g_strdup (name);
679       properties = g_array_append_val (properties, name_full);
680       name = va_arg (var_args, char *);
681     }
682
683   return properties;
684 }
685
686 /**
687  * empathy_dispatcher_find_requestable_channel_classes:
688  * @dispatcher: an #EmpathyDispatcher
689  * @connection: a #TpConnection
690  * @channel_type: a string identifying the type of the channel to lookup
691  * @handle_type: the handle type for the channel, or %TP_UNKNOWN_HANDLE_TYPE
692  *               if you don't care about the channel's target handle type
693  * @first_property_name: %NULL, or the name of the first fixed property,
694  * followed optionally by more names, followed by %NULL.
695  *
696  * Returns all the channel classes that a client can request for the connection
697  * @connection, of the type identified by @channel_type, @handle_type and the
698  * fixed properties list.
699  * The classes which are compatible with a fixed properties list (i.e. those
700  * that will be returned by this function) are intended as those that do not
701  * contain any fixed property other than those in the list; note that this
702  * doesn't guarantee that all the classes compatible with the list will contain
703  * all the requested fixed properties, so the clients will have to filter
704  * the returned list themselves.
705  * If @first_property_name is %NULL, only the classes with no other fixed
706  * properties than ChannelType and TargetHandleType will be returned.
707  * Note that this function may return %NULL without performing any lookup if
708  * @connection is not ready. To ensure that @connection is always ready,
709  * use the empathy_dispatcher_find_requestable_channel_classes_async() variant.
710  *
711  * Return value: a #GList of #GValueArray objects, where the first element in
712  * the array is a #GHashTable of the fixed properties, and the second is
713  * a #GStrv of the allowed properties for the class. The list should be free'd
714  * with g_list_free() when done, but the objects inside the list are owned
715  * by the #EmpathyDispatcher and must not be modified.
716  */
717 GList *
718 empathy_dispatcher_find_requestable_channel_classes
719                                  (EmpathyDispatcher *self,
720                                   TpConnection *connection,
721                                   const gchar *channel_type,
722                                   guint handle_type,
723                                   const char *first_property_name,
724                                   ...)
725 {
726   va_list var_args;
727   GArray *properties;
728   EmpathyDispatcherPriv *priv;
729   GList *retval;
730   guint idx;
731   char *str;
732
733   g_return_val_if_fail (EMPATHY_IS_DISPATCHER (self), NULL);
734   g_return_val_if_fail (TP_IS_CONNECTION (connection), NULL);
735   g_return_val_if_fail (channel_type != NULL, NULL);
736
737   priv = GET_PRIV (self);
738
739   va_start (var_args, first_property_name);
740
741   properties = setup_varargs (var_args, channel_type, first_property_name);
742
743   va_end (var_args);
744
745   retval = empathy_dispatcher_find_channel_classes (self, connection,
746     channel_type, handle_type, properties);
747
748   if (properties != NULL)
749     {
750       /* free the properties array */
751       for (idx = 0; idx < properties->len ; idx++)
752         {
753           str = g_array_index (properties, char *, idx);
754           g_free (str);
755         }
756
757       g_array_free (properties, TRUE);
758     }
759
760   return retval;
761 }