af0c7cb7b601ae63a542cc44330a43f0804c54ff
[milk] / src / milk-cache.c
1 /*
2  * This program is free software; you can redistribute it and/or
3  * modify it under the terms of the GNU General Public License as
4  * published by the Free Software Foundation; either version 2 of the
5  * License, or (at your option) any later version.
6  *
7  * This program is distributed in the hope that it will be useful,
8  * but WITHOUT ANY WARRANTY; without even the implied warranty of
9  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
10  * General Public License for more details.
11  *
12  * You should have received a copy of the GNU General Public
13  * License along with this program; if not, write to the
14  * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
15  * Boston, MA  02110-1301  USA
16  *
17  * Authors: Travis Reitter <treitter@gmail.com>
18  */
19
20 #include <config.h>
21
22 #include <stdlib.h>
23 #include <errno.h>
24 #include <string.h>
25 #include <sys/stat.h>
26 #include <sqlite3.h>
27 #include <glib.h>
28 #include <glib/gi18n.h>
29 #include <gtk/gtk.h>
30 #include <hildon/hildon.h>
31 #include <rtm-glib/rtm-error.h>
32 #include <rtm-glib/rtm-glib.h>
33
34 #include "milk-cache.h"
35 #include "milk-auth.h"
36 #include "milk-dialogs.h"
37
38 G_DEFINE_TYPE (MilkCache, milk_cache, G_TYPE_OBJECT);
39
40 /* less expensive than G_TYPE_INSTANCE_GET_PRIVATE */
41 #define MILK_CACHE_PRIVATE(o) ((MILK_CACHE ((o)))->priv)
42
43 #define RTM_API_KEY "81f5c6c904aeafbbc914d9845d250ea8"
44 #define RTM_SHARED_SECRET "b08b15419378f913"
45
46 /* FIXME: centralize this generic data dir */
47 #define DATA_DIR ".milk"
48 #define DB_BASENAME "tasks.db"
49
50 /* FIXME: make this configurable at runtime, pref. as a gconf value */
51 /* time between syncing with the server, in ms */
52 #define CACHE_UPDATE_PERIOD 5000
53
54 struct _MilkCachePrivate
55 {
56         MilkAuth *auth;
57         guint update_id;
58         char *last_sync;
59         sqlite3 *db;
60 };
61
62 enum {
63         PROP_AUTH = 1,
64 };
65
66 enum {
67         CLEARED,
68         TASK_ADDED,
69         TASK_CHANGED,
70         TASK_FINISHED,
71         LAST_SIGNAL
72 };
73
74 static guint signals[LAST_SIGNAL];
75
76 static MilkCache *default_cache = NULL;
77
78 static const char*
79 get_data_dir ()
80 {
81         static char *filename = NULL;
82
83         if (!filename)
84                 filename = g_build_filename (g_get_home_dir (), DATA_DIR, NULL);
85
86         return filename;
87 }
88
89 static const char*
90 get_db_filename ()
91 {
92         static char *filename = NULL;
93
94         if (!filename)
95                 filename = g_build_filename (get_data_dir (), DB_BASENAME,
96                                 NULL);
97
98         return filename;
99 }
100
101 static gint
102 get_schema_version (sqlite3 *db)
103 {
104         gint version = -1;
105         gint status;
106         sqlite3_stmt *query = NULL;
107
108         if (sqlite3_prepare_v2 (db, "PRAGMA user_version;", -1, &query, NULL)) {
109                 g_warning ("failed to prepare statement: %s\n",
110                                 sqlite3_errmsg (db));
111                 goto get_schema_version_OUT;
112         }
113
114         while (status = sqlite3_step (query)) {
115                 if (status == SQLITE_DONE) {
116                         break;
117                 } else if (status != SQLITE_ROW) {
118                         g_warning ("error stepping through SQL statement: %s",
119                                         sqlite3_errmsg (db));
120                         goto get_schema_version_OUT;
121                 }
122
123                 version = sqlite3_column_int (query, 0);
124         }
125
126 get_schema_version_OUT:
127         sqlite3_finalize (query);
128
129         return version;
130 }
131
132 static void
133 db_transaction_begin (sqlite3 *db)
134 {
135         char *err = NULL;
136
137         if (sqlite3_exec (db, "BEGIN;", NULL, NULL, &err)) {
138                 g_error ("failed to begin transaction: %s\n",
139                                 sqlite3_errmsg (db));
140         }
141         sqlite3_free (err);
142 }
143
144 static void
145 db_transaction_commit (sqlite3 *db)
146 {
147         char *err = NULL;
148
149         if (sqlite3_exec (db, "COMMIT;", NULL, NULL, &err)) {
150                 g_error ("failed to commit transaction: %s\n",
151                                 sqlite3_errmsg (db));
152         }
153         sqlite3_free (err);
154 }
155
156 static void
157 db_transaction_rollback (sqlite3 *db)
158 {
159         char *err = NULL;
160
161         if (sqlite3_exec (db, "ROLLBACK;", NULL, NULL, &err)) {
162                 g_error ("failed to rollback transaction: %s\n",
163                                 sqlite3_errmsg (db));
164         }
165         sqlite3_free (err);
166 }
167
168 static gboolean
169 db_set_schema_version (sqlite3 *db,
170                        guint    version)
171 {
172         gboolean success = TRUE;
173         char *statement;
174         char *err = NULL;
175
176         statement = g_strdup_printf ("PRAGMA user_version = %d;", version);
177
178         if (sqlite3_exec (db, statement, NULL, NULL, &err)) {
179                 g_warning ("failed to update schema version: %s\n",
180                                 sqlite3_errmsg (db));
181                 success = FALSE;
182         }
183
184         g_free (statement);
185         sqlite3_free (err);
186
187         return success;
188 }
189
190 static char*
191 db_get_key_value (sqlite3    *db,
192                   const char *key)
193 {
194         gint status;
195         sqlite3_stmt *query = NULL;
196         char *statement;
197         char *value = NULL;
198
199         statement = g_strdup_printf ("SELECT value FROM key_values "
200                         "WHERE key = '%s';", key);
201
202         if (sqlite3_prepare_v2 (db, statement, -1, &query, NULL)) {
203                 g_warning ("failed to prepare statement: %s\n",
204                                 sqlite3_errmsg (db));
205                 goto get_schema_version_OUT;
206         }
207
208         while (status = sqlite3_step (query)) {
209                 if (status == SQLITE_DONE) {
210                         break;
211                 } else if (status != SQLITE_ROW) {
212                         g_warning ("error stepping through SQL statement: %s",
213                                         sqlite3_errmsg (db));
214                         goto get_schema_version_OUT;
215                 }
216
217                 value = g_strdup (sqlite3_column_text (query, 0));
218         }
219
220 get_schema_version_OUT:
221         sqlite3_finalize (query);
222         g_free (statement);
223
224         return value;
225 }
226
227 static gboolean
228 db_date_column_to_timeval (sqlite3_stmt *query,
229                            guint         colnum,
230                            GTimeVal     *timeval)
231 {
232         const gchar *date_str;
233
234         date_str = sqlite3_column_text (query, colnum);
235
236         if (date_str && date_str[0] != '\0' && !g_strcmp0 (date_str, "(null)"))
237                 return g_time_val_from_iso8601 (date_str, timeval);
238
239         return FALSE;
240 }
241
242 static GList*
243 db_get_tasks_active (sqlite3 *db)
244 {
245         gint status;
246         sqlite3_stmt *query = NULL;
247         char *statement;
248         GList *tasks = NULL;
249
250         statement = g_strdup_printf ("SELECT "
251                         "task_id, name, due_date FROM tasks "
252                         "WHERE "
253                                 "delete_date IS NULL AND "
254                                 "complete_date IS NULL"
255                         ";");
256
257         if (sqlite3_prepare_v2 (db, statement, -1, &query, NULL)) {
258                 g_warning ("failed to prepare statement: %s\n",
259                                 sqlite3_errmsg (db));
260                 goto get_schema_version_OUT;
261         }
262
263         while (status = sqlite3_step (query)) {
264                 RtmTask *task;
265                 const char *due;
266                 GTimeVal timeval;
267
268                 if (status == SQLITE_DONE) {
269                         break;
270                 } else if (status != SQLITE_ROW) {
271                         g_warning ("error stepping through SQL statement: %s",
272                                         sqlite3_errmsg (db));
273                         goto get_schema_version_OUT;
274                 }
275
276                 task = rtm_task_new ();
277
278                 rtm_task_set_id   (task, (char*)sqlite3_column_text (query, 0));
279                 rtm_task_set_name (task, (char*)sqlite3_column_text (query, 1));
280
281                 if (db_date_column_to_timeval (query, 2, &timeval))
282                         rtm_task_set_due_date (task, &timeval);
283
284                 tasks = g_list_prepend (tasks, task);
285         }
286
287 get_schema_version_OUT:
288         sqlite3_finalize (query);
289         g_free (statement);
290
291         return tasks;
292 }
293
294 static char*
295 db_get_last_sync (sqlite3 *db)
296 {
297         return db_get_key_value (db, "last_sync");
298 }
299
300 static gboolean
301 db_set_key_value (sqlite3    *db,
302                   const char *key,
303                   const char *value)
304 {
305         gboolean success = TRUE;
306         char *statement;
307         char *err = NULL;
308
309         /* FIXME: probably safer to create them per-schema update and then fail
310          * if they don't exist */
311         statement = g_strdup_printf ("INSERT OR REPLACE INTO key_values "
312                         "('key', 'value') VALUES ('%s', '%s');", key, value);
313
314         if (sqlite3_exec (db, statement, NULL, NULL, &err)) {
315                 g_warning ("failed to update schema version: %s\n",
316                                 sqlite3_errmsg (db));
317                 success = FALSE;
318         }
319
320         g_free (statement);
321         sqlite3_free (err);
322
323         return success;
324 }
325
326 static gboolean
327 db_set_last_sync (sqlite3    *db,
328                   const char *last_sync)
329 {
330         return db_set_key_value (db, "last_sync", last_sync);
331 }
332
333 static gboolean
334 db_update_schema_0_to_1 (sqlite3 *db)
335 {
336         sqlite3_stmt *query = NULL;
337         char *err = NULL;
338
339         db_transaction_begin (db);
340
341         /* FIXME: actually create the required triggers */
342
343         /* FIXME: ugh... sqlite supports foreign keys, but don't actualyl
344          * /enforce them/ (thanks, guys...); here's a way to enforce them using
345          * triggers:
346          *
347          * http://www.justatheory.com/computers/databases/sqlite/
348          *
349          * it seems to be fixed in sqlite 3.6.19, but even Karmic has 3.6.16
350          * (and Fremantle has 3.6.14)
351          * */
352
353         if (sqlite3_exec (db, "CREATE TABLE task_ids "
354                         "(task_id TEXT PRIMARY KEY);", NULL, NULL, &err)) {
355                 g_warning ("failed to create tasks table: %s\n",
356                                 sqlite3_errmsg (db));
357                 goto db_update_schema_0_to_1_ERROR;
358         }
359         sqlite3_free (err);
360
361         /* XXX: there is a subtle race here where we can add a task on the
362          * server but disconnect before we get a confirmation, and change the
363          * name on the server before we recieve it. Then we'll end up
364          * re-submitting this task with its original name again (until we don't
365          * hit this condition again). The risk is fairly low, but it's still
366          * there */
367
368         /* insert a special task ID (NULL) to designate that we haven't
369          * gotten a confirmation for adding the task yet (and thus haven't
370          * gotten its server-side ID) */
371         if (sqlite3_exec (db, "INSERT INTO task_ids "
372                         "(task_id) values (NULL);", NULL, NULL, &err)) {
373                 g_warning ("failed to insert special 'unset' value: %s\n",
374                                 sqlite3_errmsg (db));
375                 goto db_update_schema_0_to_1_ERROR;
376         }
377         sqlite3_free (err);
378
379         if (sqlite3_exec (db, "CREATE TABLE tasks ("
380                         "local_id INTEGER PRIMARY KEY NOT NULL,"
381                         "task_id"
382                         "       CONSTRAINT fk_task_id "
383                         "               REFERENCES task_ids(task_id) "
384                         "       ON DELETE CASCADE,"
385                         "name TEXT NOT NULL,"
386                         "local_changes BOOLEAN DEFAULT 0,"
387                         "due_date TEXT,"
388                         "delete_date TEXT,"
389                         "complete_date TEXT,"
390                         "list_id TEXT,"
391                         "taskseries_id TEXT"
392                         ");", NULL, NULL, &err)) {
393                 g_warning ("failed to create tasks table: %s\n",
394                                 sqlite3_errmsg (db));
395                 goto db_update_schema_0_to_1_ERROR;
396         }
397         sqlite3_free (err);
398
399         if (sqlite3_exec (db, "CREATE TABLE key_values ("
400                         "key TEXT PRIMARY KEY NOT NULL,"
401                         "value TEXT"
402                         ");", NULL, NULL, &err)) {
403                 g_warning ("failed to create key_values table: %s\n",
404                                 sqlite3_errmsg (db));
405                 goto db_update_schema_0_to_1_ERROR;
406         }
407         sqlite3_free (err);
408
409         if (!db_set_last_sync (db, "0"))
410                 goto db_update_schema_0_to_1_ERROR;
411
412         if (!db_set_schema_version (db, 1))
413                 goto db_update_schema_0_to_1_ERROR;
414
415         db_transaction_commit (db);
416
417         return TRUE;
418
419 db_update_schema_0_to_1_ERROR:
420         db_transaction_rollback (db);
421         sqlite3_finalize (query);
422
423         return FALSE;
424 }
425
426 static gboolean
427 db_update_schema (sqlite3 *db)
428 {
429         gboolean (*update_funcs[]) (sqlite3 *)= {
430                 db_update_schema_0_to_1,
431         };
432         gint i;
433         gint schema_version = -1;
434         gint latest_version = G_N_ELEMENTS (update_funcs);
435         gint status;
436
437         schema_version = get_schema_version (db);
438
439         if (schema_version > latest_version) {
440                 g_error ("your database is newer than this version of the "
441                         "app knows how to deal with -- bailing...");
442         } else if (schema_version == latest_version) {
443                 return TRUE;
444         }
445
446         for (i = schema_version; i < latest_version; i++) {
447                 if (!update_funcs[i] (db)) {
448                         g_error ("error upgrading from schema version %d to %d",
449                                         i, i+1);
450                         /* FIXME: probably better to just wipe the cache and
451                          * start over, rather than crash (which would probably
452                          * prevent us from ever automatically recovering) */
453                 }
454         }
455
456         return TRUE;
457 }
458
459 static sqlite3*
460 db_open ()
461 {
462         sqlite3 *db = NULL;
463         gint status;
464
465         if (!g_file_test (get_data_dir (), G_FILE_TEST_EXISTS)) {
466
467         }
468         if (g_mkdir_with_parents (get_data_dir (), S_IRWXU | S_IRWXG)) {
469
470                 g_error ("Can't create the data dir %s: %s; giving up...",
471                                 get_data_dir (), strerror (errno));
472         }
473
474         status = sqlite3_open (get_db_filename (), &db);
475         if (status){
476                 GFile *file;
477                 GError *error = NULL;
478
479                 g_warning ("Can't open database: %s; deleting it...\n",
480                                 sqlite3_errmsg (db));
481                 sqlite3_close (db);
482                 db = NULL;
483
484                 /* FIXME: open a banner warning that any pending tasks may have
485                  * been lost (but actually just move the file as a backup file)
486                  */
487                 file = g_file_new_for_path (get_db_filename ());
488                 if (!g_file_delete (file, NULL, &error)) {
489                         g_error ("Could not delete the broken database: %s; "
490                                         "giving up...", error->message);
491
492                         g_clear_error (&error);
493                         exit (1);
494                 }
495         }
496
497         if (!db) {
498                 status = sqlite3_open (get_db_filename (), &db);
499                 if (status){
500                         sqlite3_close (db);
501                         exit (1);
502                 }
503         }
504
505         return db;
506 }
507
508 static char*
509 time_val_to_iso8601 (GTimeVal *val)
510 {
511         return val ?
512                 g_strdup_printf ("'%s'", g_time_val_to_iso8601 (val)) :
513                 g_strdup ("NULL");
514 }
515
516 static char*
517 sql_escape_quotes (const char *str)
518 {
519         char **tokens;
520         char *final_str;
521
522         if (!str)
523                 return NULL;
524
525         /* escape all single quotes as double quotes (as is normal in SQL) */
526         tokens = g_strsplit (str, "'", -1);
527         final_str = g_strjoinv("''", tokens);
528         g_strfreev (tokens);
529
530         return final_str;
531 }
532
533 static gboolean
534 db_insert_or_update_local_task (sqlite3    *db,
535                                 RtmTask    *task,
536                                 const char *local_id,
537                                 gboolean    local_changes)
538 {
539         gboolean success = TRUE;
540         char *statement;
541         char *err = NULL;
542         GTimeVal *due, *deleted, *completed;
543         char *name_str, *due_str, *deleted_str, *completed_str;
544         const char *task_id;
545         const char *list_id;
546         const char *taskseries_id;
547         gint status;
548
549         name_str = sql_escape_quotes (rtm_task_get_name (task));
550         due = rtm_task_get_due_date (task);
551         deleted = rtm_task_get_deleted_date (task);
552         completed = rtm_task_get_completed_date (task);
553         due_str = time_val_to_iso8601 (due);
554         deleted_str = time_val_to_iso8601 (deleted);
555         completed_str = time_val_to_iso8601 (completed);
556
557         /* FIXME: it doesn't actually have to be this complicated; see the
558          * actual code above and below  */
559         /* FIXME:
560          * 1. add a new internal ID ('local_id') that's unique in the table
561          * 'tasks'.
562          * 2. Add another 1-column table ('task_ids') with PRIMARY KEY column
563          * 'task_id'
564          * 3. prep-populate tasks_ids with a single special value (NULL, if
565          * possible). This will be used to designate "not yet written back to
566          * server"
567          * 4. make tasks.task_id have a foreign key constraint on
568          * task_ids.task_id
569          * 5. when inserting new tasks (ie, before they've been written back to
570          * the server), set their tasks.task_id to NULL (or whatever special
571          * value we're using)
572          * 6. when we recieve tasks, check to see if we get a match for SELECT
573          * local_id from tasks WHERE
574          * tasks.task_id = rtm_task_get_id (task) AND tasks.name = name_str;
575          *      6.a. if we have a match, UPDATE tasks SET task_id, name, ...
576          *      WHERE local_id = <local_id match>
577          *      6.b. if we didn't have a match, INSERT INTO tasks (task_id,
578          *      name, ...) VALUES (rtm_task_get_id (task), ...);
579          * 7. these "pending" tasks can be treated normally when being
580          * manipulated locally
581          * 8. any task changed locally needs to have its 'local_changes' field
582          * set to TRUE
583          */
584
585
586         /* if we insert NULL for the local_id, it will nicely replace it with
587          * the next automatic value. This way we can use a single statement to
588          * update the other fields if the task already exists or create the task
589          * (with a new local_id) if necessary */
590
591         task_id = rtm_task_get_id (task);
592         task_id = task_id ? task_id : "NULL";
593
594         list_id = rtm_task_get_list_id (task);
595         list_id = list_id ? list_id : "NULL";
596
597         taskseries_id = rtm_task_get_taskseries_id (task);
598         taskseries_id = taskseries_id ? taskseries_id : "NULL";
599
600         /* FIXME: cut this? */
601         if (!rtm_task_get_list_id (task)) {
602                 g_warning ("caching a task without a list ID -- this can "
603                                 "cause problems later");
604         }
605
606         if (!rtm_task_get_taskseries_id (task)) {
607                 g_warning ("caching a task without a task series ID -- this "
608                                 "can cause problems later");
609         }
610
611         /* all but the name fields are already quoted or NULL */
612         statement = g_strdup_printf ("INSERT OR REPLACE INTO tasks "
613                         "('local_id','task_id','name','due_date','delete_date',"
614                         "'complete_date','list_id','taskseries_id',"
615                         "'local_changes') "
616                         "VALUES (%s, %s, '%s', %s, %s, %s, %s, %s, %d)"
617                         ";",
618                         local_id,
619                         task_id,
620                         name_str,
621                         due_str,
622                         deleted_str,
623                         completed_str,
624                         list_id,
625                         taskseries_id,
626                         local_changes ? 1 : 0);
627         g_free (name_str);
628         g_free (due_str);
629         g_free (deleted_str);
630         g_free (completed_str);
631
632         if (sqlite3_exec (db, statement, NULL, NULL, &err)) {
633                 g_warning ("failed to insert or update task in cache: %s\n",
634                                 sqlite3_errmsg (db));
635                 success = FALSE;
636         }
637
638         g_free (statement);
639         sqlite3_free (err);
640
641         return success;
642 }
643
644 static void
645 cache_tasks_notify (MilkCache *cache,
646                     GList     *tasks,
647                     guint      signal_id)
648 {
649         MilkCachePrivate *priv;
650         GList *l;
651
652         g_return_if_fail (MILK_IS_CACHE (cache));
653
654         priv = MILK_CACHE_PRIVATE (cache);
655
656         /* FIXME: make the signals just emit all at once as a list, not
657          * individually */
658         for (l = tasks; l; l = l->next) {
659                 g_signal_emit (cache, signal_id, 0, l->data);
660         }
661 }
662
663 static RtmTask*
664 db_add_local_only_task (sqlite3    *db,
665                         const char *name)
666 {
667         RtmTask *task;
668
669         /* FIXME: cut this */
670         g_debug ("attempting to create new local-only task with name %s",
671                         name);
672
673         task = rtm_task_new ();
674         rtm_task_set_name (task, (char*) name);
675
676         if (!db_insert_or_update_local_task (db, task, "NULL", TRUE)) {
677                 g_object_unref (task);
678                 task = NULL;
679         }
680
681         return task;
682 }
683
684 static gboolean
685 db_insert_or_update_task (sqlite3  *db,
686                           RtmTask  *task,
687                           gboolean  local_changes,
688                           gboolean *task_existed)
689 {
690         gboolean success = TRUE;
691         char *statement;
692         char *err = NULL;
693         GTimeVal *due, *deleted, *completed;
694         const char *task_id;
695         gint status;
696         sqlite3_stmt *query = NULL;
697         char *local_id = NULL;
698         char *local_id_formatted = NULL;
699
700         task_id = rtm_task_get_id (task);
701         g_return_val_if_fail (task_id, FALSE);
702
703         /* FIXME: should probably use a begin...commit block around this all */
704
705         /* FIXME: this needs a whole bucket of clean-up.
706          */
707         /* FIXME: make these all prepared statements */
708
709         statement = g_strdup_printf ("INSERT OR REPLACE INTO task_ids (task_id) VALUES ('%s');", task_id);
710         if (sqlite3_exec (db, statement, NULL, NULL, &err)) {
711                 g_warning ("failed to insert task id %s in cache: %s\n",
712                                 task_id, sqlite3_errmsg (db));
713                 success = FALSE;
714         }
715
716         g_free (statement);
717         sqlite3_free (err);
718
719         /* try to find an existing task that we've already received from the
720          * server */
721         statement = g_strdup_printf ("SELECT local_id FROM tasks NATURAL JOIN task_ids WHERE task_id = '%s';", task_id);
722
723         if (sqlite3_prepare_v2 (db, statement, -1, &query, NULL)) {
724                 g_warning ("failed to prepare statement: %s\n",
725                                 sqlite3_errmsg (db));
726                 /* FIXME: use a goto instead, so we can carefully free any
727                  * necessary memory */
728                 return FALSE;
729         }
730
731         while (status = sqlite3_step (query)) {
732                 if (status == SQLITE_DONE) {
733                         break;
734                 } else if (status != SQLITE_ROW) {
735                         g_warning ("error stepping through SQL statement: %s",
736                                         sqlite3_errmsg (db));
737                         /* FIXME: use a goto instead, so we can carefully free
738                          * any necessary memory */
739                         return FALSE;
740                 }
741
742                 local_id = g_strdup (sqlite3_column_text (query, 0));
743         }
744
745         g_free (statement);
746         sqlite3_free (err);
747
748         /* otherwise, try to find a matching task that we've added locally but
749          * haven't gotten confirmed by the server yet */
750         if (!local_id) {
751                 char *name_str;
752
753                 /* FIXME: cut this */
754                 g_debug ("trying to update a local-only task");
755
756                 name_str = sql_escape_quotes (rtm_task_get_name (task));
757
758                 statement = g_strdup_printf ("SELECT local_id FROM tasks WHERE name = '%s';", name_str);
759                 g_free (name_str);
760
761                 if (sqlite3_prepare_v2 (db, statement, -1, &query, NULL)) {
762                         g_warning ("failed to prepare statement: %s\n",
763                                         sqlite3_errmsg (db));
764                         /* FIXME: use a goto instead, so we can carefully free any
765                         * necessary memory */
766                         return FALSE;
767                 }
768
769                 while (status = sqlite3_step (query)) {
770                         if (status == SQLITE_DONE) {
771                                 break;
772                         } else if (status != SQLITE_ROW) {
773                                 g_warning ("error stepping through SQL statement: %s",
774                                                 sqlite3_errmsg (db));
775                                 /* FIXME: use a goto instead, so we can carefully free
776                                 * any necessary memory */
777                                 return FALSE;
778                         }
779
780                         local_id = g_strdup (sqlite3_column_text (query, 0));
781                 }
782
783                 g_free (statement);
784                 sqlite3_free (err);
785         }
786
787         if (task_existed)
788                 *task_existed = (local_id != NULL);
789
790         /* FIXME: cut this */
791         g_debug ("got local_id: %s", local_id);
792
793         local_id_formatted = local_id ?
794                 g_strdup_printf ("'%s'", local_id) :
795                 g_strdup ("NULL");
796         g_free (local_id);
797
798         /* FIXME: cut this */
799         g_debug ("formatted local_id:\n%s", local_id_formatted);
800
801         success &= db_insert_or_update_local_task (db, task,
802                         local_id_formatted, local_changes);
803         g_free (local_id_formatted);
804
805         return success;
806 }
807
808 static GList*
809 db_get_tasks_to_add_names (MilkCache *cache)
810 {
811         MilkCachePrivate *priv;
812         char *statement;
813         char *err = NULL;
814         sqlite3_stmt *query = NULL;
815         GList *names = NULL;
816         gint status;
817
818         priv = MILK_CACHE_PRIVATE (cache);
819
820         statement = g_strdup_printf ("SELECT name FROM tasks "
821                         "WHERE task_id IS NULL;");
822
823         if (sqlite3_prepare_v2 (priv->db, statement, -1, &query, NULL)) {
824                 g_warning ("failed to prepare statement: %s\n",
825                                 sqlite3_errmsg (priv->db));
826                 goto db_get_tasks_to_add_names_ERROR;
827         }
828
829         while ((status = sqlite3_step (query))) {
830                 if (status == SQLITE_DONE) {
831                         break;
832                 } else if (status != SQLITE_ROW) {
833                         g_warning ("error stepping through SQL statement: %s",
834                                         sqlite3_errmsg (priv->db));
835                         goto db_get_tasks_to_add_names_ERROR;
836                 }
837
838                 names = g_list_prepend (names,
839                                 g_strdup (sqlite3_column_text (query, 0)));
840         }
841
842         goto db_get_tasks_to_add_names_OUT;
843
844 db_get_tasks_to_add_names_ERROR:
845         g_list_foreach (names, (GFunc) g_free, NULL);
846         g_list_free (names);
847         names = NULL;
848
849 db_get_tasks_to_add_names_OUT:
850
851         g_free (statement);
852         sqlite3_free (err);
853
854         return names;
855 }
856
857 static GList*
858 db_get_tasks_to_change (MilkCache *cache)
859 {
860         MilkCachePrivate *priv;
861         char *statement;
862         char *err = NULL;
863         sqlite3_stmt *query = NULL;
864         GList *tasks = NULL;
865         gint status;
866
867         priv = MILK_CACHE_PRIVATE (cache);
868
869         statement = g_strdup_printf ("SELECT task_id,name,due_date,list_id,"
870                         "taskseries_id FROM tasks "
871                         "WHERE local_changes=1;");
872
873         if (sqlite3_prepare_v2 (priv->db, statement, -1, &query, NULL)) {
874                 g_warning ("failed to prepare statement: %s\n",
875                                 sqlite3_errmsg (priv->db));
876                 goto db_get_tasks_to_change_ERROR;
877         }
878
879         while ((status = sqlite3_step (query))) {
880                 RtmTask *task;
881                 GTimeVal timeval;
882
883                 if (status == SQLITE_DONE) {
884                         break;
885                 } else if (status != SQLITE_ROW) {
886                         g_warning ("error stepping through SQL statement: %s",
887                                         sqlite3_errmsg (priv->db));
888                         goto db_get_tasks_to_change_ERROR;
889                 }
890
891                 task = rtm_task_new ();
892                 rtm_task_set_id (task, (gchar*) sqlite3_column_text (query, 0));
893                 rtm_task_set_name (task,
894                                 (gchar*) sqlite3_column_text (query, 1));
895
896                 if (db_date_column_to_timeval (query, 2, &timeval))
897                         rtm_task_set_due_date (task, &timeval);
898
899                 rtm_task_set_list_id (task,
900                                 (gchar*) sqlite3_column_text (query, 3));
901
902                 rtm_task_set_taskseries_id (task,
903                                 (gchar*) sqlite3_column_text (query, 4));
904
905                 tasks = g_list_prepend (tasks, task);
906         }
907
908         goto db_get_tasks_to_change_OUT;
909
910 db_get_tasks_to_change_ERROR:
911         g_list_foreach (tasks, (GFunc) g_object_unref, NULL);
912         g_list_free (tasks);
913         tasks = NULL;
914
915 db_get_tasks_to_change_OUT:
916
917         g_free (statement);
918         sqlite3_free (err);
919
920         return tasks;
921 }
922
923 static gboolean
924 cache_send_new_tasks (MilkCache *cache,
925                       char      *timeline)
926 {
927         MilkCachePrivate *priv;
928         gboolean success = TRUE;
929         GList *names = NULL;
930
931         priv = MILK_CACHE_PRIVATE (cache);
932
933         /* FIXME: have a single function to get the sets of (new, changed,
934          * completed, deleted) tasks, then deal with each of them here */
935         names = db_get_tasks_to_add_names (cache);
936
937         /* FIXME: cut this */
938         g_debug ("trying to send %d new tasks", g_list_length (names));
939
940         /* FIXME: this entire block needs to be sequential as a whole but also
941          * async as a whole */
942         if (names) {
943                 GList *tasks_added = NULL;
944                 GList *l;
945
946                 tasks_added = milk_auth_tasks_add (priv->auth, timeline, names);
947
948                 for (l = tasks_added; l; l = l->next) {
949                         /* FIXME: cut this */
950                         g_debug (G_STRLOC ": trying to add task ID to "
951                                         "newly-inserted task: '%s' (%s);"
952                                         " priority: '%s', "
953                                         " list ID: '%s', "
954                                         " taskseries ID: '%s', "
955                                         ,
956                                         rtm_task_get_name (l->data),
957                                         rtm_task_get_id (l->data),
958                                         rtm_task_get_priority (l->data),
959                                         rtm_task_get_list_id (l->data),
960                                         rtm_task_get_taskseries_id (l->data)
961                                         );
962
963                         /* mark these as having local changes so we'll send all
964                          * there non-name attributes when we send the changes,
965                          * in the next step */
966                         db_insert_or_update_task (priv->db, l->data, TRUE,
967                                                   NULL);
968                 }
969
970                 /* not the most complete verification, but probably fine */
971                 success &= (g_list_length (tasks_added) ==
972                                 g_list_length (names));
973
974                 g_list_foreach (tasks_added, (GFunc) g_object_unref, NULL);
975                 g_list_free (tasks_added);
976         }
977
978         g_list_foreach (names, (GFunc) g_free, NULL);
979         g_list_free (names);
980
981         return success;
982 }
983
984 /* FIXME: cut this */
985 static void
986 set_due_date (RtmTask *task,
987                 const char *date_str)
988 {
989         GTimeVal timeval = {0};
990
991         /* FIXME: cut this */
992         g_debug ("going to decode date: '%s'", date_str);
993
994         g_time_val_from_iso8601 (date_str, &timeval);
995
996         rtm_task_set_due_date (task, &timeval);
997 }
998
999 static gboolean
1000 db_tasks_mark_as_synced (MilkCache *cache,
1001                          GList     *tasks)
1002 {
1003         MilkCachePrivate *priv;
1004         gboolean success = TRUE;
1005         GString *tasks_builder = NULL;
1006         GList *l;
1007         char *task_ids;
1008         gboolean first = TRUE;
1009         char *statement;
1010         char *err = NULL;
1011
1012         priv = MILK_CACHE_PRIVATE (cache);
1013
1014         tasks_builder = g_string_new ("");
1015         for (l = tasks; l; l = l->next) {
1016                 const char *format;
1017
1018                 format = first ? "%s" : ",%s";
1019
1020                 g_string_append_printf (tasks_builder, format,
1021                                 rtm_task_get_id (l->data));
1022                 first = FALSE;
1023         }
1024         task_ids = g_string_free (tasks_builder, FALSE);
1025
1026         statement = g_strdup_printf ("UPDATE tasks "
1027                         "SET local_changes=0 "
1028                         "WHERE task_id IN (%s);",
1029                         task_ids);
1030
1031         if (sqlite3_exec (priv->db, statement, NULL, NULL, &err)) {
1032                 g_warning ("failed to acknowledge local changes were pushed "
1033                                 "to the server: %s\n",
1034                                 sqlite3_errmsg (priv->db));
1035                 success = FALSE;
1036         }
1037
1038         g_free (statement);
1039         sqlite3_free (err);
1040
1041         g_free (task_ids);
1042 }
1043
1044 static gboolean
1045 cache_send_changed_tasks (MilkCache *cache,
1046                           char      *timeline)
1047 {
1048         MilkCachePrivate *priv;
1049         gboolean success = TRUE;
1050         GList *tasks_to_change;
1051         GList *tasks_sent;
1052
1053         priv = MILK_CACHE_PRIVATE (cache);
1054
1055         tasks_to_change = db_get_tasks_to_change (cache);
1056
1057         if (!tasks_to_change)
1058                 return;
1059
1060         tasks_sent = milk_auth_tasks_send_changes (priv->auth,
1061                         timeline, tasks_to_change);
1062
1063         /* as above, if we miss any of these, the worst case is just resending
1064          * them later (vs. data loss or false caching) -- it's still not great
1065          * if you're on a flakey network, though */
1066         success &= (g_list_length (tasks_sent) ==
1067                         g_list_length (tasks_to_change));
1068
1069         success &= db_tasks_mark_as_synced (cache, tasks_sent);
1070
1071         /* FIXME: cut this */
1072         g_debug ("successfully updated all the tasks: %d", success);
1073
1074         g_list_foreach (tasks_to_change, (GFunc) g_object_unref, NULL);
1075         g_list_free (tasks_to_change);
1076         g_list_free (tasks_sent);
1077
1078         return success;
1079 }
1080
1081 /* FIXME: make this async */
1082 static gboolean
1083 cache_send_changes (MilkCache *cache)
1084 {
1085         MilkCachePrivate *priv;
1086         gboolean success = TRUE;
1087         char *timeline;
1088
1089         priv = MILK_CACHE_PRIVATE (cache);
1090
1091         /* FIXME: this makes the send_changes_hint fairly pointless, though we
1092          * may want to rename this function similarly */
1093         if (milk_auth_get_state (priv->auth) != MILK_AUTH_STATE_CONNECTED) {
1094                 g_warning ("trying to send changes before auth has connected - we shouldn't be doing this (at least not until we've connected once)");
1095
1096                 return;
1097         }
1098
1099         /* FIXME: cut this */
1100         g_debug (G_STRLOC ": sending new (and updated tasks, once implemented) ");
1101
1102         timeline = milk_auth_timeline_create (priv->auth, NULL);
1103
1104         success &= cache_send_new_tasks (cache, timeline);
1105         success &= cache_send_changed_tasks (cache, timeline);
1106
1107         /* FIXME: also get all the deleted tasks and delete them, and all the
1108          * completed tasks (complete_date IS NOT NULL && local_changes=1), and
1109          * apply those on the server */
1110
1111         /* FIXME: cut this */
1112         g_debug ("looks like we successfully added the pending tasks");
1113
1114         /* XXX: if we use the same timeline for creating these new tasks as
1115          * updating their attributes, we won't need to have this insane 2-step
1116          * process, right? (and thus updating the local_changes above shouldn't
1117          * be necessary) */
1118
1119         g_free (timeline);
1120
1121         return success;
1122 }
1123
1124 /* FIXME: make this async */
1125 static gboolean
1126 cache_receive_changes (MilkCache *cache)
1127 {
1128         MilkCachePrivate *priv = MILK_CACHE_PRIVATE (cache);
1129         GList *rtm_tasks;
1130         GList *l;
1131         GList *added = NULL, *changed = NULL, *finished = NULL;
1132         GTimeVal current_time;
1133         char *new_sync;
1134         GError *error = NULL;
1135
1136         if (milk_auth_get_state (priv->auth) != MILK_AUTH_STATE_CONNECTED) {
1137                 return TRUE;
1138         }
1139
1140         g_get_current_time (&current_time);
1141         new_sync = g_time_val_to_iso8601 (&current_time);
1142
1143         rtm_tasks = milk_auth_get_tasks (priv->auth, priv->last_sync, &error);
1144         if (error) {
1145                 g_error (G_STRLOC ": failed to retrieve latest tasks: %s",
1146                                 error->message);
1147                 g_clear_error (&error);
1148                 goto cache_receive_changes_ERROR;
1149         }
1150
1151         /* We don't wrap this in a begin...commit...rollback because it's better
1152          * to let the individual items get committed automatically. If one of
1153          * them fails, then we won't update the "last_sync" date. So the next
1154          * time we sync, we'll start from where we did last time, and so on,
1155          * until they all succeed at once. Any tasks that are already in the DB
1156          * will be "updated", whether their content actually changed or not
1157          * (likely not) */
1158         for (l = rtm_tasks; l; l = g_list_delete_link (l, l)) {
1159                 GtkTreeIter iter;
1160                 RtmTask *task;
1161                 gboolean task_existed;
1162
1163                 task = RTM_TASK (l->data);
1164
1165                 if (!db_insert_or_update_task (priv->db, task, FALSE,
1166                                                &task_existed))
1167                         goto cache_receive_changes_ERROR;
1168
1169                         /* FIXME: read the task back out of the DB for any sort
1170                          * of normalization to happen transparently, and for
1171                          * more commonality in the code reading out of the DB
1172                          */
1173
1174                 /* Task is deleted or completed */
1175                 if (task_is_finished (task)) {
1176                         finished = g_list_prepend (finished, task);
1177
1178                 /* Task has been changed */
1179                 } else if (task_existed) {
1180                         changed = g_list_prepend (changed, task);
1181
1182                 /* Task is new */
1183                 } else {
1184                         added = g_list_prepend (added, task);
1185                 }
1186         }
1187
1188         cache_tasks_notify (cache, added, signals[TASK_ADDED]);
1189         cache_tasks_notify (cache, changed, signals[TASK_CHANGED]);
1190         cache_tasks_notify (cache, finished, signals[TASK_FINISHED]);
1191
1192         g_list_free (added);
1193         g_list_free (changed);
1194         g_list_free (finished);
1195
1196         if (db_set_last_sync (priv->db, new_sync)) {
1197                 g_free (priv->last_sync);
1198                 priv->last_sync = new_sync;
1199         } else {
1200                 g_warning ("failed to set new sync timestamp to %d", new_sync);
1201                 goto cache_receive_changes_ERROR;
1202         }
1203
1204         return TRUE;
1205
1206 cache_receive_changes_ERROR:
1207         g_free (new_sync);
1208
1209         return FALSE;
1210 }
1211
1212 static gboolean
1213 cache_send_receive_changes (MilkCache *cache)
1214 {
1215         return cache_send_changes (cache) && cache_receive_changes (cache);
1216 }
1217
1218 static void
1219 restart_send_receive_poll (MilkCache *cache,
1220                            gboolean   poll_first)
1221 {
1222         MilkCachePrivate *priv;
1223
1224         priv = MILK_CACHE_PRIVATE (cache);
1225
1226         /* FIXME: cut this */
1227         g_debug ("restarting the send/receive poll");
1228
1229         if (priv->update_id)
1230                 g_source_remove (priv->update_id);
1231
1232         if (poll_first)
1233                 cache_send_receive_changes (cache);
1234
1235         priv->update_id = g_timeout_add (CACHE_UPDATE_PERIOD,
1236                         (GSourceFunc) cache_send_receive_changes, cache);
1237 }
1238
1239 task_is_finished (RtmTask *task)
1240 {
1241         return (rtm_task_get_completed_date (task) ||
1242                 rtm_task_get_deleted_date (task));
1243 }
1244
1245 static void
1246 auth_notify_cb (MilkAuth   *auth,
1247                 GParamSpec *spec,
1248                 MilkCache  *cache)
1249 {
1250         if (milk_auth_get_state (auth) == MILK_AUTH_STATE_CONNECTED) {
1251                 cache_send_receive_changes (cache);
1252         }
1253 }
1254
1255 void
1256 milk_cache_set_auth (MilkCache *cache,
1257                      MilkAuth  *auth)
1258 {
1259         MilkCachePrivate *priv;
1260
1261         g_return_if_fail (MILK_IS_CACHE (cache));
1262         g_return_if_fail (MILK_IS_AUTH (auth));
1263
1264         priv = MILK_CACHE_PRIVATE (cache);
1265
1266         if (priv->auth) {
1267                 g_object_unref (priv->auth);
1268         }
1269         priv->auth = g_object_ref (auth);
1270
1271         restart_send_receive_poll (cache, FALSE);
1272
1273         g_signal_emit (cache, signals[CLEARED], 0);
1274
1275         g_signal_connect (priv->auth, "notify::state",
1276                           G_CALLBACK (auth_notify_cb), cache);
1277         auth_notify_cb (priv->auth, NULL, cache);
1278 }
1279
1280 static void
1281 milk_cache_get_property (GObject    *object,
1282                          guint       property_id,
1283                          GValue     *value,
1284                          GParamSpec *pspec)
1285 {
1286         MilkCachePrivate *priv = MILK_CACHE_PRIVATE (object);
1287
1288         switch (property_id)
1289         {
1290                 case PROP_AUTH:
1291                         g_value_set_object (value, priv->auth);
1292                 break;
1293
1294                 default:
1295                         G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id,
1296                                         pspec);
1297         }
1298 }
1299
1300 static void
1301 milk_cache_set_property (GObject      *object,
1302                          guint         property_id,
1303                          const GValue *value,
1304                          GParamSpec   *pspec)
1305 {
1306         MilkCachePrivate *priv;
1307         MilkCache *cache;
1308
1309         cache = MILK_CACHE (object);
1310         priv = MILK_CACHE_PRIVATE (cache);
1311
1312         switch (property_id)
1313         {
1314                 case PROP_AUTH:
1315                         milk_cache_set_auth (cache, g_value_get_object (value));
1316                 break;
1317
1318                 default:
1319                         G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id,
1320                                         pspec);
1321         }
1322 }
1323
1324 GList*
1325 milk_cache_get_active_tasks (MilkCache *cache)
1326 {
1327         MilkCachePrivate *priv;
1328         GList *tasks;
1329
1330         g_return_val_if_fail (MILK_IS_CACHE (cache), NULL);
1331
1332         priv = MILK_CACHE_PRIVATE (cache);
1333
1334         tasks = db_get_tasks_active (priv->db);
1335
1336         return tasks;
1337 }
1338
1339 char*
1340 milk_cache_timeline_create (MilkCache  *cache,
1341                             GError    **error)
1342 {
1343         MilkCachePrivate *priv;
1344
1345         g_return_val_if_fail (MILK_IS_CACHE (cache), NULL);
1346
1347         priv = MILK_CACHE_PRIVATE (cache);
1348
1349         return milk_auth_timeline_create (priv->auth, error);
1350 }
1351
1352 /* FIXME: is the timeline argument even useful here? We should be able to just
1353  * assume it's being added right now, right? */
1354
1355 /* FIXME: either fill in error appropriately or cut it as an argument, so the
1356  * caller doesn't assume it will be meaningful if we return NULL */
1357 RtmTask*
1358 milk_cache_task_add (MilkCache   *cache,
1359                      char        *timeline,
1360                      const char  *name,
1361                      GError     **error)
1362 {
1363         MilkCachePrivate *priv;
1364         RtmTask *task;
1365
1366         g_return_val_if_fail (MILK_IS_CACHE (cache), NULL);
1367
1368         priv = MILK_CACHE_PRIVATE (cache);
1369
1370         task = db_add_local_only_task (priv->db, name);
1371         if (task) {
1372                 GList *tasks;
1373
1374                 tasks = g_list_prepend (NULL, task);
1375                 cache_tasks_notify (cache, tasks, signals[TASK_ADDED]);
1376                 restart_send_receive_poll (cache, TRUE);
1377
1378                 g_list_free (tasks);
1379         }
1380
1381         return task;
1382 }
1383
1384 char*
1385 milk_cache_task_complete (MilkCache  *cache,
1386                           char       *timeline,
1387                           RtmTask    *task,
1388                           GError    **error)
1389 {
1390         MilkCachePrivate *priv;
1391
1392         g_return_val_if_fail (MILK_IS_CACHE (cache), NULL);
1393
1394         priv = MILK_CACHE_PRIVATE (cache);
1395
1396         /* FIXME: mark the task as "to-complete" and set up the periodic task
1397          * that pushes the changes to the server; then immediately emit the
1398          * "task-finished" signal, so the model will immediately reflect that */
1399
1400         return milk_auth_task_complete (priv->auth, timeline, task, error);
1401 }
1402
1403 char*
1404 milk_cache_task_delete (MilkCache  *cache,
1405                         char       *timeline,
1406                         RtmTask    *task,
1407                         GError    **error)
1408 {
1409         MilkCachePrivate *priv;
1410
1411         g_return_val_if_fail (MILK_IS_CACHE (cache), NULL);
1412
1413         priv = MILK_CACHE_PRIVATE (cache);
1414
1415         /* FIXME: mark the task as "to-delete" and set up the periodic task that
1416          * pushes the changes to the server; then immediately emit the
1417          * "task-finished" signal, so the model will immediately reflect that */
1418
1419         return milk_auth_task_delete (priv->auth, timeline, task, error);
1420 }
1421
1422 /* XXX: this won't be necessary when the auth handles this transparently; or at
1423  * least this will merely be a signal to the auth that we're ready to
1424  * authenticate when it is */
1425 void
1426 milk_cache_authenticate (MilkCache *cache)
1427 {
1428         MilkCachePrivate *priv;
1429
1430         g_return_if_fail (MILK_IS_CACHE (cache));
1431
1432         priv = MILK_CACHE_PRIVATE (cache);
1433
1434         milk_auth_log_in (priv->auth);
1435 }
1436
1437 static void
1438 milk_cache_dispose (GObject *object)
1439 {
1440         MilkCachePrivate *priv = MILK_CACHE_PRIVATE (object);
1441
1442         if (priv->auth) {
1443                 g_object_unref (priv->auth);
1444                 priv->auth = NULL;
1445         }
1446
1447         if (priv->update_id) {
1448                 g_source_remove (priv->update_id);
1449                 priv->update_id = 0;
1450         }
1451 }
1452
1453 static void
1454 milk_cache_finalize (GObject *object)
1455 {
1456         MilkCachePrivate *priv = MILK_CACHE_PRIVATE (object);
1457
1458         g_free (priv->last_sync);
1459
1460         /* FIXME: should we do this at atexit() instead, for better safety? */
1461         sqlite3_close (priv->db);
1462 }
1463
1464 static void
1465 milk_cache_class_init (MilkCacheClass *klass)
1466 {
1467         GObjectClass *object_class = G_OBJECT_CLASS (klass);
1468
1469         g_type_class_add_private (klass, sizeof (MilkCachePrivate));
1470
1471         object_class->get_property = milk_cache_get_property;
1472         object_class->set_property = milk_cache_set_property;
1473         object_class->dispose = milk_cache_dispose;
1474         object_class->finalize = milk_cache_finalize;
1475
1476         g_object_class_install_property
1477                 (object_class,
1478                  PROP_AUTH,
1479                  g_param_spec_object
1480                          ("auth",
1481                           "Authentication proxy",
1482                           "Remember The Milk authentication proxy.",
1483                           MILK_TYPE_AUTH,
1484                           G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY |
1485                           G_PARAM_STATIC_STRINGS));
1486
1487         signals[CLEARED] = g_signal_new
1488                 ("cleared",
1489                 MILK_TYPE_CACHE, G_SIGNAL_RUN_LAST, 0, NULL, NULL,
1490                 g_cclosure_marshal_VOID__VOID, G_TYPE_NONE, 0);
1491
1492         signals[TASK_ADDED] = g_signal_new
1493                 ("task-added",
1494                  MILK_TYPE_CACHE, G_SIGNAL_RUN_LAST, 0, NULL, NULL,
1495                  g_cclosure_marshal_VOID__OBJECT, G_TYPE_NONE, 1,
1496                  RTM_TYPE_TASK);
1497
1498         signals[TASK_FINISHED] = g_signal_new
1499                 ("task-finished",
1500                 MILK_TYPE_CACHE, G_SIGNAL_RUN_LAST, 0, NULL, NULL,
1501                 g_cclosure_marshal_VOID__OBJECT, G_TYPE_NONE, 1, RTM_TYPE_TASK);
1502
1503         signals[TASK_CHANGED] = g_signal_new
1504                 ("task-changed",
1505                  MILK_TYPE_CACHE, G_SIGNAL_RUN_LAST, 0, NULL, NULL,
1506                  g_cclosure_marshal_VOID__OBJECT, G_TYPE_NONE, 1,
1507                  RTM_TYPE_TASK);
1508 }
1509
1510 static void
1511 milk_cache_init (MilkCache *self)
1512 {
1513         MilkCachePrivate *priv;
1514
1515         self->priv = priv = G_TYPE_INSTANCE_GET_PRIVATE (
1516                         self, MILK_TYPE_CACHE, MilkCachePrivate);
1517
1518         /* open the DB, creating a new one if necessary */
1519         priv->db = db_open ();
1520         db_update_schema (priv->db);
1521         priv->last_sync = db_get_last_sync (priv->db);
1522 }
1523
1524 MilkCache*
1525 milk_cache_get_default ()
1526 {
1527         if (!default_cache) {
1528                 default_cache = g_object_new (MILK_TYPE_CACHE,
1529                                 "auth", milk_auth_get_default (),
1530                                 NULL);
1531         }
1532
1533         return default_cache;
1534 }