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