]> git.0d.be Git - empathy.git/blob - libempathy-gtk/empathy-location-manager.c
Merge remote-tracking branch 'origin/gnome-3-8'
[empathy.git] / libempathy-gtk / empathy-location-manager.c
1 /*
2  * Copyright (C) 2009 Collabora Ltd.
3  *
4  * This program is free software; you can redistribute it and/or
5  * modify it under the terms of the GNU General Public License as
6  * published by the Free Software Foundation; either version 2 of the
7  * License, or (at your option) any later version.
8  *
9  * This program is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
12  * General Public License for more details.
13  *
14  * You should have received a copy of the GNU General Public
15  * License along with this program; if not, write to the
16  * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
17  * Boston, MA  02110-1301  USA
18  *
19  * Authors: Pierre-Luc Beaudoin <pierre-luc.beaudoin@collabora.co.uk>
20  */
21
22 #include "config.h"
23 #include "empathy-location-manager.h"
24
25 #include <geoclue/geoclue-master.h>
26
27 #include "empathy-gsettings.h"
28 #include "empathy-location.h"
29 #include "empathy-time.h"
30
31 #define DEBUG_FLAG EMPATHY_DEBUG_LOCATION
32 #include "empathy-debug.h"
33
34 /* Seconds before updating the location */
35 #define TIMEOUT 10
36 static EmpathyLocationManager *location_manager = NULL;
37
38 struct _EmpathyLocationManagerPrivate {
39     gboolean geoclue_is_setup;
40     /* Contains the location to be sent to accounts.  Geoclue is used
41      * to populate it.  This HashTable uses Telepathy's style (string,
42      * GValue). Keys are defined in empathy-location.h
43      */
44     GHashTable *location;
45
46     GSettings *gsettings_loc;
47
48     GeoclueResourceFlags resources;
49     GeoclueMasterClient *gc_client;
50     GeocluePosition *gc_position;
51     GeoclueAddress *gc_address;
52
53     gboolean reduce_accuracy;
54     TpAccountManager *account_manager;
55
56     /* The idle id for publish_on_idle func */
57     guint timeout_id;
58 };
59
60 G_DEFINE_TYPE (EmpathyLocationManager, empathy_location_manager, G_TYPE_OBJECT);
61
62 static GObject *
63 location_manager_constructor (GType type,
64     guint n_construct_params,
65     GObjectConstructParam *construct_params)
66 {
67   GObject *retval;
68
69   if (location_manager == NULL)
70     {
71       retval = G_OBJECT_CLASS (empathy_location_manager_parent_class)->constructor
72           (type, n_construct_params, construct_params);
73
74       location_manager = EMPATHY_LOCATION_MANAGER (retval);
75       g_object_add_weak_pointer (retval, (gpointer) &location_manager);
76     }
77   else
78     {
79       retval = g_object_ref (location_manager);
80     }
81
82   return retval;
83 }
84
85 static void
86 location_manager_dispose (GObject *object)
87 {
88   EmpathyLocationManager *self = (EmpathyLocationManager *) object;
89   void (*dispose) (GObject *) =
90     G_OBJECT_CLASS (empathy_location_manager_parent_class)->dispose;
91
92   tp_clear_object (&self->priv->account_manager);
93   tp_clear_object (&self->priv->gsettings_loc);
94   tp_clear_object (&self->priv->gc_client);
95   tp_clear_object (&self->priv->gc_position);
96   tp_clear_object (&self->priv->gc_address);
97   tp_clear_pointer (&self->priv->location, g_hash_table_unref);
98
99   if (dispose != NULL)
100     dispose (object);
101 }
102
103 static void
104 empathy_location_manager_class_init (EmpathyLocationManagerClass *class)
105 {
106   GObjectClass *object_class;
107
108   object_class = G_OBJECT_CLASS (class);
109
110   object_class->constructor = location_manager_constructor;
111   object_class->dispose = location_manager_dispose;
112
113   g_type_class_add_private (object_class, sizeof (EmpathyLocationManagerPrivate));
114 }
115
116 static void
117 publish_location_cb (TpConnection *connection,
118                      const GError *error,
119                      gpointer user_data,
120                      GObject *weak_object)
121 {
122   if (error != NULL)
123       DEBUG ("Error setting location: %s", error->message);
124 }
125
126 static void
127 publish_location (EmpathyLocationManager *self,
128     TpConnection *conn,
129     gboolean force_publication)
130 {
131   guint connection_status = -1;
132
133   if (!conn)
134     return;
135
136   if (!force_publication)
137     {
138       if (!g_settings_get_boolean (self->priv->gsettings_loc,
139             EMPATHY_PREFS_LOCATION_PUBLISH))
140         return;
141     }
142
143   connection_status = tp_connection_get_status (conn, NULL);
144
145   if (connection_status != TP_CONNECTION_STATUS_CONNECTED)
146     return;
147
148   DEBUG ("Publishing %s location to connection %p",
149       (g_hash_table_size (self->priv->location) == 0 ? "empty" : ""),
150       conn);
151
152   tp_cli_connection_interface_location_call_set_location (conn, -1,
153       self->priv->location, publish_location_cb, NULL, NULL, G_OBJECT (self));
154 }
155
156 typedef struct
157 {
158   EmpathyLocationManager *self;
159   gboolean force_publication;
160 } PublishToAllData;
161
162 static void
163 publish_to_all_am_prepared_cb (GObject *source_object,
164     GAsyncResult *result,
165     gpointer user_data)
166 {
167   TpAccountManager *manager = TP_ACCOUNT_MANAGER (source_object);
168   PublishToAllData *data = user_data;
169   GList *accounts, *l;
170   GError *error = NULL;
171
172   if (!tp_proxy_prepare_finish (manager, result, &error))
173     {
174       DEBUG ("Failed to prepare account manager: %s", error->message);
175       g_error_free (error);
176       goto out;
177     }
178
179   accounts = tp_account_manager_dup_valid_accounts (manager);
180   for (l = accounts; l; l = l->next)
181     {
182       TpConnection *conn = tp_account_get_connection (TP_ACCOUNT (l->data));
183
184       if (conn != NULL)
185         publish_location (data->self, conn, data->force_publication);
186     }
187   g_list_free_full (accounts, g_object_unref);
188
189 out:
190   g_object_unref (data->self);
191   g_slice_free (PublishToAllData, data);
192 }
193
194 static void
195 publish_to_all_connections (EmpathyLocationManager *self,
196     gboolean force_publication)
197 {
198   PublishToAllData *data;
199
200   data = g_slice_new0 (PublishToAllData);
201   data->self = g_object_ref (self);
202   data->force_publication = force_publication;
203
204   tp_proxy_prepare_async (self->priv->account_manager, NULL,
205       publish_to_all_am_prepared_cb, data);
206 }
207
208 static gboolean
209 publish_on_idle (gpointer user_data)
210 {
211   EmpathyLocationManager *manager = EMPATHY_LOCATION_MANAGER (user_data);
212
213   manager->priv->timeout_id = 0;
214   publish_to_all_connections (manager, TRUE);
215   return FALSE;
216 }
217
218 static void
219 new_connection_cb (TpAccount *account,
220     guint old_status,
221     guint new_status,
222     guint reason,
223     gchar *dbus_error_name,
224     GHashTable *details,
225     gpointer user_data)
226 {
227   EmpathyLocationManager *self = user_data;
228   TpConnection *conn;
229
230   conn = tp_account_get_connection (account);
231
232   DEBUG ("New connection %p", conn);
233
234   /* Don't publish if it is already planned (ie startup) */
235   if (self->priv->timeout_id == 0)
236     {
237       publish_location (EMPATHY_LOCATION_MANAGER (self), conn,
238           FALSE);
239     }
240 }
241
242 static void
243 update_timestamp (EmpathyLocationManager *self)
244 {
245   gint64 timestamp;
246
247   timestamp = empathy_time_get_current ();
248   tp_asv_set_int64 (self->priv->location, EMPATHY_LOCATION_TIMESTAMP,
249       timestamp);
250
251   DEBUG ("\t - Timestamp: %" G_GINT64_FORMAT, timestamp);
252 }
253
254 static void
255 address_changed_cb (GeoclueAddress *address,
256                     int timestamp,
257                     GHashTable *details,
258                     GeoclueAccuracy *accuracy,
259                     gpointer user_data)
260 {
261   EmpathyLocationManager *self = user_data;
262   GeoclueAccuracyLevel level;
263   GHashTableIter iter;
264   gpointer key, value;
265
266   geoclue_accuracy_get_details (accuracy, &level, NULL, NULL);
267   DEBUG ("New address (accuracy level %d):", level);
268   /* FIXME: Publish accuracy level also considering the position's */
269
270   g_hash_table_remove (self->priv->location, EMPATHY_LOCATION_STREET);
271   g_hash_table_remove (self->priv->location, EMPATHY_LOCATION_AREA);
272   g_hash_table_remove (self->priv->location, EMPATHY_LOCATION_REGION);
273   g_hash_table_remove (self->priv->location, EMPATHY_LOCATION_COUNTRY);
274   g_hash_table_remove (self->priv->location, EMPATHY_LOCATION_COUNTRY_CODE);
275   g_hash_table_remove (self->priv->location, EMPATHY_LOCATION_POSTAL_CODE);
276
277   if (g_hash_table_size (details) == 0)
278     {
279       DEBUG ("\t - (Empty)");
280       return;
281     }
282
283   g_hash_table_iter_init (&iter, details);
284   while (g_hash_table_iter_next (&iter, &key, &value))
285     {
286       /* Discard street information if reduced accuracy is on */
287       if (self->priv->reduce_accuracy &&
288           !tp_strdiff (key, EMPATHY_LOCATION_STREET))
289         continue;
290
291       tp_asv_set_string (self->priv->location, key, value);
292
293       DEBUG ("\t - %s: %s", (gchar *) key, (gchar *) value);
294     }
295
296   update_timestamp (self);
297   if (self->priv->timeout_id == 0)
298     self->priv->timeout_id = g_timeout_add_seconds (TIMEOUT, publish_on_idle,
299         self);
300 }
301
302 static void
303 initial_address_cb (GeoclueAddress *address,
304                     int timestamp,
305                     GHashTable *details,
306                     GeoclueAccuracy *accuracy,
307                     GError *error,
308                     gpointer self)
309 {
310   if (error)
311     {
312       DEBUG ("Error: %s", error->message);
313       g_error_free (error);
314     }
315   else
316     {
317       address_changed_cb (address, timestamp, details, accuracy, self);
318     }
319 }
320
321 static void
322 position_changed_cb (GeocluePosition *position,
323                      GeocluePositionFields fields,
324                      int timestamp,
325                      double latitude,
326                      double longitude,
327                      double altitude,
328                      GeoclueAccuracy *accuracy,
329                      gpointer user_data)
330 {
331   EmpathyLocationManager *self = user_data;
332   GeoclueAccuracyLevel level;
333   gdouble mean, horizontal, vertical;
334
335   geoclue_accuracy_get_details (accuracy, &level, &horizontal, &vertical);
336   DEBUG ("New position (accuracy level %d)", level);
337   if (level == GEOCLUE_ACCURACY_LEVEL_NONE)
338     return;
339
340   if (fields & GEOCLUE_POSITION_FIELDS_LONGITUDE)
341     {
342
343       if (self->priv->reduce_accuracy)
344         /* Truncate at 1 decimal place */
345         longitude = ((int) (longitude * 10)) / 10.0;
346
347       tp_asv_set_double (self->priv->location, EMPATHY_LOCATION_LON, longitude);
348
349       DEBUG ("\t - Longitude: %f", longitude);
350     }
351   else
352     {
353       g_hash_table_remove (self->priv->location, EMPATHY_LOCATION_LON);
354     }
355
356   if (fields & GEOCLUE_POSITION_FIELDS_LATITUDE)
357     {
358       if (self->priv->reduce_accuracy)
359         /* Truncate at 1 decimal place */
360         latitude = ((int) (latitude * 10)) / 10.0;
361
362       tp_asv_set_double (self->priv->location, EMPATHY_LOCATION_LAT, latitude);
363
364       DEBUG ("\t - Latitude: %f", latitude);
365     }
366   else
367     {
368       g_hash_table_remove (self->priv->location, EMPATHY_LOCATION_LAT);
369     }
370
371   if (fields & GEOCLUE_POSITION_FIELDS_ALTITUDE)
372     {
373       tp_asv_set_double (self->priv->location, EMPATHY_LOCATION_ALT, altitude);
374
375       DEBUG ("\t - Altitude: %f", altitude);
376     }
377   else
378     {
379       g_hash_table_remove (self->priv->location, EMPATHY_LOCATION_ALT);
380     }
381
382   if (level == GEOCLUE_ACCURACY_LEVEL_DETAILED)
383     {
384       mean = (horizontal + vertical) / 2.0;
385       tp_asv_set_double (self->priv->location, EMPATHY_LOCATION_ACCURACY, mean);
386
387       DEBUG ("\t - Accuracy: %f", mean);
388     }
389   else
390     {
391       g_hash_table_remove (self->priv->location, EMPATHY_LOCATION_ACCURACY);
392     }
393
394   update_timestamp (self);
395   if (self->priv->timeout_id == 0)
396     self->priv->timeout_id = g_timeout_add_seconds (TIMEOUT, publish_on_idle,
397         self);
398 }
399
400 static void
401 initial_position_cb (GeocluePosition *position,
402                      GeocluePositionFields fields,
403                      int timestamp,
404                      double latitude,
405                      double longitude,
406                      double altitude,
407                      GeoclueAccuracy *accuracy,
408                      GError *error,
409                      gpointer self)
410 {
411   if (error)
412     {
413       DEBUG ("Error: %s", error->message);
414       g_error_free (error);
415     }
416   else
417     {
418       position_changed_cb (position, fields, timestamp, latitude, longitude,
419           altitude, accuracy, self);
420     }
421 }
422
423 static void
424 set_requirements (EmpathyLocationManager *self,
425     GeoclueSetRequirementsCallback callback)
426 {
427   geoclue_master_client_set_requirements_async (self->priv->gc_client,
428       GEOCLUE_ACCURACY_LEVEL_COUNTRY, 0, FALSE, self->priv->resources,
429       callback, self);
430 }
431
432 static void
433 update_resources_set_requirements_cb (GeoclueMasterClient *client,
434     GError *error,
435     gpointer userdata)
436 {
437   EmpathyLocationManager *self = userdata;
438
439   if (error != NULL)
440     {
441       DEBUG ("set_requirements failed: %s", error->message);
442       g_error_free (error);
443       return;
444     }
445
446   geoclue_address_get_address_async (self->priv->gc_address,
447       initial_address_cb, self);
448   geoclue_position_get_position_async (self->priv->gc_position,
449       initial_position_cb, self);
450 }
451
452 static void
453 update_resources (EmpathyLocationManager *self)
454 {
455   DEBUG ("Updating resources %d", self->priv->resources);
456
457   if (!self->priv->geoclue_is_setup)
458     return;
459
460   /* As per Geoclue bug #15126, using NONE results in no address
461    * being found as geoclue-manual report an empty address with
462    * accuracy = NONE */
463   set_requirements (self, update_resources_set_requirements_cb);
464 }
465
466 static void
467 create_address_cb (GeoclueMasterClient *client,
468     GeoclueAddress *address,
469     GError *error,
470     gpointer userdata)
471 {
472   EmpathyLocationManager *self = userdata;
473
474   if (error != NULL)
475     {
476       DEBUG ("Failed to create GeoclueAddress: %s", error->message);
477       g_error_free (error);
478       return;
479     }
480
481   self->priv->gc_address = address;
482
483   g_signal_connect (G_OBJECT (self->priv->gc_address), "address-changed",
484       G_CALLBACK (address_changed_cb), self);
485
486   self->priv->geoclue_is_setup = TRUE;
487 }
488
489 static void
490 create_position_cb (GeoclueMasterClient *client,
491     GeocluePosition *position,
492     GError *error,
493     gpointer userdata)
494 {
495   EmpathyLocationManager *self = userdata;
496
497   if (error != NULL)
498     {
499       DEBUG ("Failed to create GeocluePosition: %s", error->message);
500       g_error_free (error);
501       return;
502     }
503
504   self->priv->gc_position = position;
505
506   g_signal_connect (G_OBJECT (self->priv->gc_position), "position-changed",
507       G_CALLBACK (position_changed_cb), self);
508
509   /* Get updated when the address changes */
510   geoclue_master_client_create_address_async (self->priv->gc_client,
511       create_address_cb, self);
512 }
513
514 static void
515 create_client_set_requirements_cb (GeoclueMasterClient *client,
516     GError *error,
517     gpointer userdata)
518 {
519   EmpathyLocationManager *self = userdata;
520
521   if (error != NULL)
522     {
523       DEBUG ("set_requirements failed: %s", error->message);
524       g_error_free (error);
525       return;
526     }
527
528   /* Get updated when the position is changes */
529   geoclue_master_client_create_position_async (self->priv->gc_client,
530       create_position_cb, self);
531 }
532
533 static void
534 create_client_cb (GeoclueMaster *master,
535     GeoclueMasterClient *client,
536     char *object_path,
537     GError *error,
538     gpointer userdata)
539 {
540   EmpathyLocationManager *self = userdata;
541
542   if (error != NULL)
543     {
544       DEBUG ("Failed to create GeoclueMasterClient: %s", error->message);
545       g_error_free (error);
546       return;
547     }
548
549   /* @client seems be (transfer full) looking at the geoclue code; yeah for
550    * undocumented API... */
551   self->priv->gc_client = client;
552
553   set_requirements (self, create_client_set_requirements_cb);
554 }
555
556 static void
557 setup_geoclue (EmpathyLocationManager *self)
558 {
559   GeoclueMaster *master;
560
561   DEBUG ("Setting up Geoclue");
562   master = geoclue_master_get_default ();
563
564   geoclue_master_create_client_async (master, create_client_cb, self);
565
566   g_object_unref (master);
567  }
568
569 static void
570 publish_cb (GSettings *gsettings_loc,
571             const gchar *key,
572             gpointer user_data)
573 {
574   EmpathyLocationManager *self = EMPATHY_LOCATION_MANAGER (user_data);
575
576   DEBUG ("Publish Conf changed");
577
578   if (g_settings_get_boolean (gsettings_loc, key))
579     {
580       if (!self->priv->geoclue_is_setup)
581         setup_geoclue (self);
582       /* if still not setup than the init failed */
583       if (!self->priv->geoclue_is_setup)
584         return;
585
586       geoclue_address_get_address_async (self->priv->gc_address,
587           initial_address_cb, self);
588       geoclue_position_get_position_async (self->priv->gc_position,
589           initial_position_cb, self);
590     }
591   else
592     {
593       /* As per XEP-0080: send an empty location to have remove current
594        * location from the servers
595        */
596       g_hash_table_remove_all (self->priv->location);
597       publish_to_all_connections (self, TRUE);
598     }
599
600 }
601
602 static void
603 resource_cb (GSettings *gsettings_loc,
604              const gchar *key,
605              gpointer user_data)
606 {
607   EmpathyLocationManager *self = EMPATHY_LOCATION_MANAGER (user_data);
608   GeoclueResourceFlags resource = 0;
609
610   DEBUG ("%s changed", key);
611
612   if (!tp_strdiff (key, EMPATHY_PREFS_LOCATION_RESOURCE_NETWORK))
613     resource = GEOCLUE_RESOURCE_NETWORK;
614   if (!tp_strdiff (key, EMPATHY_PREFS_LOCATION_RESOURCE_CELL))
615     resource = GEOCLUE_RESOURCE_CELL;
616   if (!tp_strdiff (key, EMPATHY_PREFS_LOCATION_RESOURCE_GPS))
617     resource = GEOCLUE_RESOURCE_GPS;
618
619   if (g_settings_get_boolean (gsettings_loc, key))
620     self->priv->resources |= resource;
621   else
622     self->priv->resources &= ~resource;
623
624   if (self->priv->geoclue_is_setup)
625     update_resources (self);
626 }
627
628 static void
629 accuracy_cb (GSettings *gsettings_loc,
630              const gchar *key,
631              gpointer user_data)
632 {
633   EmpathyLocationManager *self = EMPATHY_LOCATION_MANAGER (user_data);
634
635   DEBUG ("%s changed", key);
636
637   self->priv->reduce_accuracy = g_settings_get_boolean (gsettings_loc, key);
638
639   if (!self->priv->geoclue_is_setup)
640     return;
641
642   geoclue_address_get_address_async (self->priv->gc_address,
643       initial_address_cb, self);
644   geoclue_position_get_position_async (self->priv->gc_position,
645       initial_position_cb, self);
646 }
647
648 static void
649 account_manager_prepared_cb (GObject *source_object,
650     GAsyncResult *result,
651     gpointer user_data)
652 {
653   GList *accounts, *l;
654   TpAccountManager *account_manager = TP_ACCOUNT_MANAGER (source_object);
655   EmpathyLocationManager *self = user_data;
656   GError *error = NULL;
657
658   if (!tp_proxy_prepare_finish (account_manager, result, &error))
659     {
660       DEBUG ("Failed to prepare account manager: %s", error->message);
661       g_error_free (error);
662       return;
663     }
664
665   accounts = tp_account_manager_dup_valid_accounts (account_manager);
666   for (l = accounts; l != NULL; l = l->next)
667     {
668       TpAccount *account = TP_ACCOUNT (l->data);
669
670       tp_g_signal_connect_object (account, "status-changed",
671           G_CALLBACK (new_connection_cb), self, 0);
672     }
673   g_list_free_full (accounts, g_object_unref);
674 }
675
676 static void
677 empathy_location_manager_init (EmpathyLocationManager *self)
678 {
679   EmpathyLocationManagerPrivate *priv = G_TYPE_INSTANCE_GET_PRIVATE (self,
680       EMPATHY_TYPE_LOCATION_MANAGER, EmpathyLocationManagerPrivate);
681
682   self->priv = priv;
683   priv->geoclue_is_setup = FALSE;
684   priv->location = tp_asv_new (NULL, NULL);
685   priv->gsettings_loc = g_settings_new (EMPATHY_PREFS_LOCATION_SCHEMA);
686
687   /* Setup account status callbacks */
688   priv->account_manager = tp_account_manager_dup ();
689
690   tp_proxy_prepare_async (priv->account_manager, NULL,
691       account_manager_prepared_cb, self);
692
693   /* Setup settings status callbacks */
694   g_signal_connect (priv->gsettings_loc,
695       "changed::" EMPATHY_PREFS_LOCATION_PUBLISH,
696       G_CALLBACK (publish_cb), self);
697   g_signal_connect (priv->gsettings_loc,
698       "changed::" EMPATHY_PREFS_LOCATION_RESOURCE_NETWORK,
699       G_CALLBACK (resource_cb), self);
700   g_signal_connect (priv->gsettings_loc,
701       "changed::" EMPATHY_PREFS_LOCATION_RESOURCE_CELL,
702       G_CALLBACK (resource_cb), self);
703   g_signal_connect (priv->gsettings_loc,
704       "changed::" EMPATHY_PREFS_LOCATION_RESOURCE_GPS,
705       G_CALLBACK (resource_cb), self);
706   g_signal_connect (priv->gsettings_loc,
707       "changed::" EMPATHY_PREFS_LOCATION_REDUCE_ACCURACY,
708       G_CALLBACK (accuracy_cb), self);
709
710   resource_cb (priv->gsettings_loc, EMPATHY_PREFS_LOCATION_RESOURCE_NETWORK,
711       self);
712   resource_cb (priv->gsettings_loc, EMPATHY_PREFS_LOCATION_RESOURCE_CELL, self);
713   resource_cb (priv->gsettings_loc, EMPATHY_PREFS_LOCATION_RESOURCE_GPS, self);
714   accuracy_cb (priv->gsettings_loc, EMPATHY_PREFS_LOCATION_REDUCE_ACCURACY,
715       self);
716   publish_cb (priv->gsettings_loc, EMPATHY_PREFS_LOCATION_PUBLISH, self);
717 }
718
719 EmpathyLocationManager *
720 empathy_location_manager_dup_singleton (void)
721 {
722   return EMPATHY_LOCATION_MANAGER (g_object_new (EMPATHY_TYPE_LOCATION_MANAGER,
723       NULL));
724 }