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