1 /*
2  * biji-memo-provider.c
3  * Copyright (C) Pierre-Yves LUYTEN 2013 <py@luyten.fr>
4  *
5  * bijiben is free software: you can redistribute it and/or modify it
6  * under the terms of the GNU General Public License as published by the
7  * Free Software Foundation, either version 3 of the License, or
8  * (at your option) any later version.
9  *
10  * bijiben is distributed in the hope that it will be useful, but
11  * WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
13  * See the GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License along
16  * with this program.  If not, see <http://www.gnu.org/licenses/>.
17  */
18 
19 
20 /*
21  * http://tools.ietf.org/html/rfc2445
22  *
23  * Evolution UI offers to sync Memo to local computer
24  * TODO: check this
25  */
26 
27 
28 #include <libecal/libecal.h>        /* ECalClient      */
29 #include "biji-tracker.h"
30 #include "biji-memo-provider.h"
31 #include "biji-memo-note.h"
32 
33 
34 #define MINER_ID "gn:memo:miner:fd48e15b-2460-4761-a7be-942616102fa6"
35 
36 struct _BijiMemoProvider
37 {
38   BijiProvider         parent_instance;
39 
40   BijiProviderInfo     info;
41   ESource             *source;
42   ECalClient          *client;
43   GtkWidget           *icon;
44 
45   /* Startup */
46   GSList              *memos;
47   GHashTable          *tracker;
48   GHashTable          *items;
49   GHashTable          *archives;
50   GQueue              *queue;
51 };
52 
53 
54 
55 
56 G_DEFINE_TYPE (BijiMemoProvider, biji_memo_provider, BIJI_TYPE_PROVIDER);
57 
58 
59 /* Properties */
60 enum {
61   PROP_0,
62   PROP_SOURCE,
63   BIJI_MEMO_PROP
64 };
65 
66 
67 
68 static GParamSpec *properties[BIJI_MEMO_PROP] = { NULL, };
69 
70 /* Memos */
71 
72 
73 gboolean
time_val_from_icaltime(ICalTime * itt,glong * result)74 time_val_from_icaltime (ICalTime *itt, glong *result)
75 {
76   g_autoptr(GDateTime) t = NULL;
77   g_autofree char *iso = NULL;
78 
79   if (!itt)
80     return FALSE;
81 
82   iso = isodate_from_time_t (i_cal_time_as_timet (itt));
83 
84   t = g_date_time_new_from_iso8601 (iso, NULL);
85   if (t == NULL)
86     {
87       return FALSE;
88     }
89 
90   *result = g_date_time_to_unix (t);
91   return TRUE;
92 }
93 
94 
95 
96 ICalTime *
icaltime_from_time_val(glong t)97 icaltime_from_time_val (glong t)
98 {
99   g_autoptr(GDateTime) tv = NULL;
100   ICalTime *out;
101 
102   tv = g_date_time_new_from_unix_utc (t);
103   out = i_cal_time_new_from_day_of_year (g_date_time_get_day_of_year (tv),
104                                          g_date_time_get_year (tv));
105 
106   return out;
107 }
108 
109 
110 
111 typedef struct
112 {
113   ECalComponent     *ecal;
114   BijiMemoProvider  *self;
115   BijiInfoSet        set;
116 } BijiMemoItem;
117 
118 
119 
120 static BijiMemoItem *
memo_item_new(BijiMemoProvider * self)121 memo_item_new (BijiMemoProvider *self)
122 {
123   BijiMemoItem *item;
124 
125   item = g_slice_new (BijiMemoItem);
126   item->ecal = NULL;
127   item->self = self;
128 
129   item->set.content = NULL;
130   item->set.mtime = 0;
131   item->set.created = 0;
132   item->set.title = NULL;
133   item->set.url = NULL;
134 
135   return item;
136 }
137 
138 
139 /* ECalComponent do not transfer much values */
140 static void
memo_item_free(BijiMemoItem * item)141 memo_item_free (BijiMemoItem *item)
142 {
143   g_free (item->set.content);
144   g_slice_free (BijiMemoItem, item);
145 }
146 
147 
148 
149 /* No color, hence we use default color */
150 
151 static void
create_note_from_item(BijiMemoItem * item)152 create_note_from_item (BijiMemoItem *item)
153 {
154   BijiNoteObj *note;
155   GdkRGBA color;
156   BijiManager *manager;
157 
158   manager = biji_provider_get_manager (BIJI_PROVIDER (item->self));
159   note = biji_memo_note_new_from_info (item->self,
160                                        manager,
161                                        &item->set,
162 				       item->ecal,
163                                        item->set.content,
164                                        item->self->client);
165 
166   biji_manager_get_default_color (manager, &color);
167   biji_note_obj_set_rgba (note, &color);
168   g_hash_table_replace (item->self->items,
169                         g_strdup (item->set.url),
170                         note);
171 }
172 
173 
174 
175 static void
trash(gpointer urn_uuid,gpointer self)176 trash (gpointer urn_uuid, gpointer self)
177 {
178   biji_tracker_trash_resource (
179       biji_provider_get_manager (BIJI_PROVIDER (self)), (gchar*) urn_uuid);
180 }
181 
182 
183 static void
handle_next_item(BijiMemoProvider * self)184 handle_next_item (BijiMemoProvider *self)
185 {
186   BijiMemoItem *item;
187   GList        *list;
188 
189   item = g_queue_pop_head (self->queue);
190 
191   if (item != NULL)
192   {
193     g_hash_table_remove (self->tracker, item->set.url);
194 
195     create_note_from_item (item);
196     /* debug pour tracker. Il faut en plus datasource->urn */
197     g_debug ("created=%"G_GINT64_FORMAT, item->set.created);
198     g_debug ("title=%s", item->set.title);
199     g_debug ("url=%s", item->set.url);
200     g_debug ("content=%s\n================\n\n\n", item->set.content);
201 
202     biji_tracker_ensure_resource_from_info (
203     biji_provider_get_manager (BIJI_PROVIDER (self)), &item->set);
204 
205     //memo_item_free (item);
206     handle_next_item (self);
207   }
208 
209 
210   /* Loading and notes creation over. Provide notes to manager */
211   else
212   {
213     /* Post load tracker db clean-up */
214     list = g_hash_table_get_values (self->tracker);
215     g_list_foreach (list, trash, self);
216     g_list_free (list);
217 
218     /* Now simply provide data to controller */
219     list = g_hash_table_get_values (self->items);
220     BIJI_PROVIDER_GET_CLASS (self)->notify_loaded (BIJI_PROVIDER (self), list, BIJI_LIVING_ITEMS);
221     g_list_free (list);
222   }
223 }
224 
225 
226 
227 static void
on_object_list_got(GObject * obj,GAsyncResult * res,gpointer user_data)228 on_object_list_got (GObject      *obj,
229                     GAsyncResult *res,
230                     gpointer      user_data)
231 {
232   GSList *l, *desc, *ll;
233   GError *error;
234   BijiMemoProvider *self = BIJI_MEMO_PROVIDER (user_data);
235 
236   error = NULL;
237   e_cal_client_get_object_list_finish (self->client,
238                                        res,
239                                        &self->memos,
240                                        &error);
241 
242   if (error)
243   {
244     g_warning ("e cal get obj list fin %s", error->message);
245     g_error_free (error);
246     return;
247   }
248 
249   for (l = self->memos; l != NULL; l = l->next)
250   {
251     ECalComponent      *co; /* Memo */
252     ECalComponentText  *text;
253     BijiMemoItem       *item;
254     ICalTime           *t;
255     glong               time, dtstart;
256     ECalComponentDateTime *tz;
257 
258 
259     item = memo_item_new (self);
260     item->set.datasource_urn = g_strdup (self->info.datasource);
261     co = item->ecal = e_cal_component_new_from_icalcomponent (l->data);
262 
263     /* Summary, url */
264     text = e_cal_component_get_summary (co);
265     item->set.title = text ? g_strdup (e_cal_component_text_get_value (text)) : NULL;
266     e_cal_component_text_free (text);
267     item->set.url = g_strdup (e_cal_component_get_uid (co));
268 
269 
270     /* Last modified, created */
271     tz = e_cal_component_get_dtstart (co);
272     if (time_val_from_icaltime (tz ? e_cal_component_datetime_get_value (tz) : NULL, &time))
273       dtstart = time;
274     else
275       dtstart = 0;
276     e_cal_component_datetime_free (tz);
277 
278     t = e_cal_component_get_last_modified (co); /* or dtstart */
279     if (time_val_from_icaltime (t, &time))
280       item->set.mtime = time;
281     else
282       item->set.mtime = dtstart;
283     g_clear_object (&t);
284 
285     t = e_cal_component_get_created (co); /* or dtstart */
286     if (time_val_from_icaltime (t, &time))
287       item->set.created = time;
288     else
289       item->set.created = dtstart;
290     g_clear_object (&t);
291 
292     /* Description */
293     desc = e_cal_component_get_descriptions (co);
294     for (ll=desc; ll!=NULL; ll=ll->next)
295     {
296       ECalComponentText *txt = ll->data;
297 
298       if (txt && e_cal_component_text_get_value (txt) != NULL)
299       {
300         item->set.content = g_strdup (e_cal_component_text_get_value (txt));
301 	break;
302       }
303     }
304 
305     if (item->set.content == NULL)
306       g_warning ("note %s has no content", item->set.title);
307 
308 
309     g_slist_free_full (desc, e_cal_component_text_free);
310     g_queue_push_head (self->queue, item);
311   }
312 
313   handle_next_item (self);
314 }
315 
316 
317 
318 
319 static const gchar *
i_want_all_memos(void)320 i_want_all_memos (void)
321 {
322   return "occur-in-time-range? (make-time \"19820421T204153Z\") (make-time \"20820423T232416Z\")";
323 }
324 
325 
326 
327 /* Stock all existing urn-uuid. Then parse files */
328 static void
on_notes_mined(GObject * source_object,GAsyncResult * res,gpointer user_data)329 on_notes_mined (GObject       *source_object,
330                 GAsyncResult  *res,
331                 gpointer       user_data)
332 {
333   BijiMemoProvider        *self;
334   TrackerSparqlConnection *connect;
335   TrackerSparqlCursor     *cursor;
336   GError                  *error;
337 
338   self = user_data;
339   connect = TRACKER_SPARQL_CONNECTION (source_object);
340   error = NULL;
341   cursor = tracker_sparql_connection_query_finish (connect, res, &error);
342 
343   if (error)
344   {
345     g_warning ("%s", error->message);
346     g_error_free (error);
347   }
348 
349   if (cursor)
350   {
351     while (tracker_sparql_cursor_next (cursor, NULL, NULL))
352     {
353       g_hash_table_insert (self->tracker,
354                            g_strdup (tracker_sparql_cursor_get_string (cursor, 0, NULL)),
355                            g_strdup (tracker_sparql_cursor_get_string (cursor, 1, NULL)));
356 
357     }
358 
359     g_object_unref (cursor);
360   }
361 
362   e_cal_client_get_object_list (self->client,
363                                 i_want_all_memos(), /* sexp, not null */
364                                 NULL, /* Cancellable */
365                                 on_object_list_got,
366                                 self);
367 }
368 
369 
370 
371 /* From gnome-calendar -> ported to GdkRGBA */
372 static GdkPixbuf*
get_pixbuf_from_color(GdkRGBA * color,gint size)373 get_pixbuf_from_color    (GdkRGBA              *color, gint size)
374 {
375   cairo_surface_t *surface;
376   cairo_t *cr;
377   gint width, height;
378   GdkPixbuf *pix;
379 
380 
381   width = height = size;
382   surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, width, height);
383   cr = cairo_create (surface);
384 
385   cairo_set_source_rgb (cr,
386                         color->red,
387                         color->green,
388                         color->blue);
389   cairo_rectangle (cr, 0, 0, width, height);
390   cairo_fill (cr);
391   cairo_destroy (cr);
392   pix = gdk_pixbuf_get_from_surface (surface,
393                                      0, 0,
394                                      width, height);
395   cairo_surface_destroy (surface);
396   return pix;
397 }
398 
399 
400 static gboolean
_get_icon(BijiMemoProvider * self,GtkWidget ** result)401 _get_icon (BijiMemoProvider *self,
402            GtkWidget        **result)
403 {
404   ESourceExtension *ext;
405   const gchar      *color;
406   GdkRGBA           rgba;
407   GdkPixbuf        *pix;
408 
409   ext = e_source_get_extension (self->source, E_SOURCE_EXTENSION_MEMO_LIST);
410   color = e_source_selectable_get_color (E_SOURCE_SELECTABLE (ext));
411 
412   if (color == NULL || !gdk_rgba_parse (&rgba, color))
413     return FALSE;
414 
415   pix = get_pixbuf_from_color (&rgba, 40);
416   *result = gtk_image_new_from_pixbuf (pix);
417   g_object_set (G_OBJECT (*result), "margin", 4, NULL);
418   g_object_unref (pix);
419 
420   return TRUE;
421 }
422 
423 
424 
425 /*
426  * Once the client is connected,
427  * mine notes for this.
428  */
429 static void
on_client_connected(GObject * obj,GAsyncResult * res,gpointer user_data)430 on_client_connected (GObject      *obj,
431                      GAsyncResult *res,
432                      gpointer      user_data)
433 {
434   GError *error = NULL;
435   gchar *query;
436   BijiMemoProvider *self = BIJI_MEMO_PROVIDER (user_data);
437 
438   e_cal_client_connect_finish (res, &error);
439 
440   if (error)
441   {
442       g_warning ("On Client Connected : %s", error->message);
443       return;
444   }
445 
446   self->client = E_CAL_CLIENT (obj);
447   query = g_strdup_printf ("SELECT ?url ?urn WHERE {?urn a nfo:Note; "
448                            " nie:dataSource '%s' ; nie:url ?url}",
449                            self->info.datasource);
450 
451   tracker_sparql_connection_query_async (
452       biji_manager_get_tracker_connection (
453         biji_provider_get_manager (BIJI_PROVIDER (self))),
454       query,
455       NULL,
456       on_notes_mined,
457       self);
458 
459   g_free (query);
460 }
461 
462 
463 /* GObject */
464 
465 
466 
467 static void
biji_memo_provider_constructed(GObject * obj)468 biji_memo_provider_constructed (GObject *obj)
469 {
470   BijiMemoProvider        *self;
471 
472   G_OBJECT_CLASS (biji_memo_provider_parent_class)->constructed (obj);
473 
474   self = BIJI_MEMO_PROVIDER (obj);
475 
476   /* Info */
477   self->info.unique_id = e_source_get_uid (self->source);
478   self->info.datasource = g_strdup_printf ("memo:%s",
479                                            self->info.unique_id);
480   self->info.name = g_strdup (e_source_get_display_name (self->source));
481   if (!_get_icon (self, &self->info.icon))
482      self->info.icon = gtk_image_new_from_icon_name ("user-home", GTK_ICON_SIZE_INVALID);
483 
484   gtk_image_set_pixel_size (GTK_IMAGE (self->info.icon), 48);
485   g_object_ref (self->info.icon);
486 
487   e_cal_client_connect (self->source,
488                         E_CAL_CLIENT_SOURCE_TYPE_MEMOS,
489 			10, /* wait up to 10 seconds until the memo list is connected */
490                         NULL, /* cancel */
491                         on_client_connected,
492                         self);
493 }
494 
495 
496 
497 static void
on_object_created(GObject * client,GAsyncResult * res,gpointer user_data)498 on_object_created (GObject      *client,
499                    GAsyncResult *res,
500 		   gpointer      user_data)
501 {
502   GError *error = NULL;
503   gchar  *out_uid;
504 
505   e_cal_client_create_object_finish (E_CAL_CLIENT (client),
506                                      res,
507                                      &out_uid,
508 				     &error);
509 
510   if (error)
511   {
512     g_warning ("%s", error->message);
513     g_error_free (error);
514     return;
515   }
516 
517   g_free (out_uid);
518 }
519 
520 
521 
522 static BijiNoteObj *
memo_create_note(BijiProvider * provider,const gchar * str)523 memo_create_note (BijiProvider *provider,
524                   const gchar  *str)
525 {
526   BijiMemoProvider *self = BIJI_MEMO_PROVIDER (provider);
527   BijiInfoSet    info;
528   BijiNoteObj   *note;
529   ICalComponent *icalcomp;
530   ECalComponent *comp;
531   gchar         *title, *html;
532   time_t         dttime;
533   ECalComponentDateTime *dt;
534   ICalTimezone  *zone;
535   ICalTime      *itt;
536 
537 
538   title = NULL;
539 
540   if (str)
541   {
542     title = biji_manager_get_unique_title (
543       biji_provider_get_manager (provider), str);
544   }
545 
546   info.url = NULL;
547   info.title = title;
548   info.mtime = g_get_real_time () / G_USEC_PER_SEC;
549   info.content = title;
550   info.created = info.mtime;
551 
552   e_cal_client_get_default_object_sync (
553     self->client, &icalcomp, NULL, NULL);
554 
555   if (icalcomp == NULL)
556     icalcomp = i_cal_component_new (I_CAL_VJOURNAL_COMPONENT);
557 
558   comp = e_cal_component_new ();
559   if (!e_cal_component_set_icalcomponent (comp, icalcomp))
560   {
561     g_object_unref (icalcomp);
562     e_cal_component_set_new_vtype (comp, E_CAL_COMPONENT_JOURNAL);
563   }
564 
565 
566   /* Dates and commit sequence */
567   dttime = time (NULL);
568   zone = e_cal_client_get_default_timezone (self->client);
569 
570   if (dttime)
571   {
572     itt = i_cal_time_new_from_timet_with_zone (
573       dttime, FALSE, zone);
574     dt = e_cal_component_datetime_new_take (itt, g_strdup (zone ? i_cal_timezone_get_tzid (zone) : NULL));
575 
576     e_cal_component_set_dtstart (comp, dt);
577     e_cal_component_set_dtend (comp, dt);
578     e_cal_component_set_last_modified (comp, itt);
579     e_cal_component_set_created (comp, itt);
580 
581     e_cal_component_datetime_free (dt);
582   }
583 
584   if (dttime)
585     e_cal_component_commit_sequence (comp);
586 
587 
588   /* make sure the component has an UID and info get it */
589   if (! (info.url = (gchar *) i_cal_component_get_uid (icalcomp)))
590   {
591     gchar *uid;
592 
593     uid = e_util_generate_uid ();
594     i_cal_component_set_uid (icalcomp, uid);
595     info.url = uid;
596   }
597 
598 
599   /* Create the note, push the new vjournal */
600   note = biji_memo_note_new_from_info (
601     self,
602     biji_provider_get_manager (provider),
603     &info,
604     comp,
605     str,
606     self->client);
607 
608 
609   biji_note_obj_set_title (note, title);
610   biji_note_obj_set_raw_text (note, title);
611   html = html_from_plain_text (title);
612   biji_note_obj_set_html (note, html);
613   g_free (html);
614 
615 
616   e_cal_client_create_object (self->client,
617                               icalcomp,
618                               E_CAL_OPERATION_FLAG_NONE,
619                               NULL, /* GCancellable */
620                               on_object_created,
621                               self);
622 
623   g_free (title);
624 
625   return note;
626 }
627 
628 
629 
630 
631 static void
biji_memo_provider_init(BijiMemoProvider * self)632 biji_memo_provider_init (BijiMemoProvider *self)
633 {
634   self->queue = g_queue_new ();
635   self->items = g_hash_table_new_full (g_str_hash, g_str_equal,
636                                        g_free, g_object_unref);
637   self->tracker = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free);
638 }
639 
640 static void
biji_memo_provider_finalize(GObject * object)641 biji_memo_provider_finalize (GObject *object)
642 {
643   BijiMemoProvider *self = BIJI_MEMO_PROVIDER (object);
644 
645   g_slist_free_full (self->memos, g_object_unref);
646 
647   g_hash_table_unref (self->tracker);
648   g_hash_table_unref (self->items);
649   g_queue_free_full (self->queue, (GDestroyNotify) memo_item_free);
650 
651   G_OBJECT_CLASS (biji_memo_provider_parent_class)->finalize (object);
652 }
653 
654 
655 
656 static void
biji_memo_provider_set_property(GObject * object,guint property_id,const GValue * value,GParamSpec * pspec)657 biji_memo_provider_set_property (GObject      *object,
658                                  guint         property_id,
659                                  const GValue *value,
660                                  GParamSpec   *pspec)
661 {
662   BijiMemoProvider *self = BIJI_MEMO_PROVIDER (object);
663 
664 
665   switch (property_id)
666     {
667     case PROP_SOURCE:
668       self->source = g_value_get_object (value);
669       break;
670     default:
671       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
672       break;
673     }
674 }
675 
676 
677 static void
biji_memo_provider_get_property(GObject * object,guint property_id,GValue * value,GParamSpec * pspec)678 biji_memo_provider_get_property (GObject    *object,
679                                  guint       property_id,
680                                  GValue     *value,
681                                  GParamSpec *pspec)
682 {
683   BijiMemoProvider *self = BIJI_MEMO_PROVIDER (object);
684 
685   switch (property_id)
686     {
687     case PROP_SOURCE:
688       g_value_set_object (value, self->source);
689       break;
690     default:
691       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
692       break;
693     }
694 }
695 
696 
697 
698 static const BijiProviderInfo *
memo_provider_get_info(BijiProvider * provider)699 memo_provider_get_info (BijiProvider *provider)
700 {
701   return &(BIJI_MEMO_PROVIDER (provider)->info);
702 }
703 
704 
705 static void
biji_memo_provider_class_init(BijiMemoProviderClass * klass)706 biji_memo_provider_class_init (BijiMemoProviderClass *klass)
707 {
708   GObjectClass* object_class = G_OBJECT_CLASS (klass);
709   BijiProviderClass *provider_class = BIJI_PROVIDER_CLASS (klass);
710 
711   object_class->finalize = biji_memo_provider_finalize;
712   object_class->constructed = biji_memo_provider_constructed;
713   object_class->get_property = biji_memo_provider_get_property;
714   object_class->set_property = biji_memo_provider_set_property;
715 
716   provider_class->get_info = memo_provider_get_info;
717   provider_class->create_new_note = memo_create_note;
718   provider_class->create_note_full = NULL;
719 
720   properties[PROP_SOURCE] =
721     g_param_spec_object ("source",
722                          "Provider ESource",
723                          "ESource Memo associated to Provider",
724                          E_TYPE_SOURCE,
725                          G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY);
726 
727   g_object_class_install_properties (object_class, BIJI_MEMO_PROP, properties);
728 }
729 
730 
731 
732 BijiProvider *
biji_memo_provider_new(BijiManager * manager,ESource * source)733 biji_memo_provider_new (BijiManager *manager,
734                         ESource     *source)
735 {
736     return g_object_new (BIJI_TYPE_MEMO_PROVIDER,
737                          "manager", manager,
738                          "source",  source,
739                          NULL);
740 }
741