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