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