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