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.
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.
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
17 * Authors: Travis Reitter <treitter@gmail.com>
28 #include <glib/gi18n.h>
30 #include <hildon/hildon.h>
31 #include <rtm-glib/rtm-error.h>
32 #include <rtm-glib/rtm-glib.h>
34 #include "milk-cache.h"
35 #include "milk-auth.h"
36 #include "milk-dialogs.h"
38 G_DEFINE_TYPE (MilkCache, milk_cache, G_TYPE_OBJECT);
40 /* less expensive than G_TYPE_INSTANCE_GET_PRIVATE */
41 #define MILK_CACHE_PRIVATE(o) ((MILK_CACHE ((o)))->priv)
43 #define RTM_API_KEY "81f5c6c904aeafbbc914d9845d250ea8"
44 #define RTM_SHARED_SECRET "b08b15419378f913"
46 /* FIXME: centralize this generic data dir */
47 #define DATA_DIR ".milk"
48 #define DB_BASENAME "tasks.db"
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
54 struct _MilkCachePrivate
74 static guint signals[LAST_SIGNAL];
76 static MilkCache *default_cache = NULL;
81 static char *filename = NULL;
84 filename = g_build_filename (g_get_home_dir (), DATA_DIR, NULL);
92 static char *filename = NULL;
95 filename = g_build_filename (get_data_dir (), DB_BASENAME,
102 get_schema_version (sqlite3 *db)
106 sqlite3_stmt *query = NULL;
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;
114 while (status = sqlite3_step (query)) {
115 if (status == SQLITE_DONE) {
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;
123 version = sqlite3_column_int (query, 0);
126 get_schema_version_OUT:
127 sqlite3_finalize (query);
133 db_transaction_begin (sqlite3 *db)
137 if (sqlite3_exec (db, "BEGIN;", NULL, NULL, &err)) {
138 g_error ("failed to begin transaction: %s\n",
139 sqlite3_errmsg (db));
145 db_transaction_commit (sqlite3 *db)
149 if (sqlite3_exec (db, "COMMIT;", NULL, NULL, &err)) {
150 g_error ("failed to commit transaction: %s\n",
151 sqlite3_errmsg (db));
157 db_transaction_rollback (sqlite3 *db)
161 if (sqlite3_exec (db, "ROLLBACK;", NULL, NULL, &err)) {
162 g_error ("failed to rollback transaction: %s\n",
163 sqlite3_errmsg (db));
169 db_set_schema_version (sqlite3 *db,
172 gboolean success = TRUE;
176 statement = g_strdup_printf ("PRAGMA user_version = %d;", version);
178 if (sqlite3_exec (db, statement, NULL, NULL, &err)) {
179 g_warning ("failed to update schema version: %s\n",
180 sqlite3_errmsg (db));
191 db_get_key_value (sqlite3 *db,
195 sqlite3_stmt *query = NULL;
199 statement = g_strdup_printf ("SELECT value FROM key_values "
200 "WHERE key = '%s';", key);
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;
208 while (status = sqlite3_step (query)) {
209 if (status == SQLITE_DONE) {
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;
217 value = g_strdup (sqlite3_column_text (query, 0));
220 get_schema_version_OUT:
221 sqlite3_finalize (query);
228 db_date_column_to_timeval (sqlite3_stmt *query,
232 const gchar *date_str;
234 date_str = sqlite3_column_text (query, colnum);
236 if (date_str && date_str[0] != '\0' && !g_strcmp0 (date_str, "(null)"))
237 return g_time_val_from_iso8601 (date_str, timeval);
243 db_get_tasks_active (sqlite3 *db)
246 sqlite3_stmt *query = NULL;
250 statement = g_strdup_printf ("SELECT "
251 "task_id, name, due_date FROM tasks "
253 "delete_date IS NULL AND "
254 "complete_date IS NULL"
257 if (sqlite3_prepare_v2 (db, statement, -1, &query, NULL)) {
258 g_warning ("failed to prepare statement: %s\n",
259 sqlite3_errmsg (db));
260 goto get_schema_version_OUT;
263 while (status = sqlite3_step (query)) {
268 if (status == SQLITE_DONE) {
270 } else if (status != SQLITE_ROW) {
271 g_warning ("error stepping through SQL statement: %s",
272 sqlite3_errmsg (db));
273 goto get_schema_version_OUT;
276 task = rtm_task_new ();
278 rtm_task_set_id (task, (char*)sqlite3_column_text (query, 0));
279 rtm_task_set_name (task, (char*)sqlite3_column_text (query, 1));
281 if (db_date_column_to_timeval (query, 2, &timeval))
282 rtm_task_set_due_date (task, &timeval);
284 tasks = g_list_prepend (tasks, task);
287 get_schema_version_OUT:
288 sqlite3_finalize (query);
295 db_get_last_sync (sqlite3 *db)
297 return db_get_key_value (db, "last_sync");
301 db_set_key_value (sqlite3 *db,
305 gboolean success = TRUE;
309 /* FIXME: probably safer to create them per-schema update and then fail
310 * if they don't exist */
311 statement = g_strdup_printf ("INSERT OR REPLACE INTO key_values "
312 "('key', 'value') VALUES ('%s', '%s');", key, value);
314 if (sqlite3_exec (db, statement, NULL, NULL, &err)) {
315 g_warning ("failed to update schema version: %s\n",
316 sqlite3_errmsg (db));
327 db_set_last_sync (sqlite3 *db,
328 const char *last_sync)
330 return db_set_key_value (db, "last_sync", last_sync);
334 db_update_schema_0_to_1 (sqlite3 *db)
336 sqlite3_stmt *query = NULL;
339 db_transaction_begin (db);
341 /* FIXME: actually create the required triggers */
343 /* FIXME: ugh... sqlite supports foreign keys, but don't actualyl
344 * /enforce them/ (thanks, guys...); here's a way to enforce them using
347 * http://www.justatheory.com/computers/databases/sqlite/
349 * it seems to be fixed in sqlite 3.6.19, but even Karmic has 3.6.16
350 * (and Fremantle has 3.6.14)
353 if (sqlite3_exec (db, "CREATE TABLE task_ids "
354 "(task_id TEXT PRIMARY KEY);", NULL, NULL, &err)) {
355 g_warning ("failed to create tasks table: %s\n",
356 sqlite3_errmsg (db));
357 goto db_update_schema_0_to_1_ERROR;
361 /* XXX: there is a subtle race here where we can add a task on the
362 * server but disconnect before we get a confirmation, and change the
363 * name on the server before we recieve it. Then we'll end up
364 * re-submitting this task with its original name again (until we don't
365 * hit this condition again). The risk is fairly low, but it's still
368 /* insert a special task ID (NULL) to designate that we haven't
369 * gotten a confirmation for adding the task yet (and thus haven't
370 * gotten its server-side ID) */
371 if (sqlite3_exec (db, "INSERT INTO task_ids "
372 "(task_id) values (NULL);", NULL, NULL, &err)) {
373 g_warning ("failed to insert special 'unset' value: %s\n",
374 sqlite3_errmsg (db));
375 goto db_update_schema_0_to_1_ERROR;
379 if (sqlite3_exec (db, "CREATE TABLE tasks ("
380 "local_id INTEGER PRIMARY KEY NOT NULL,"
382 " CONSTRAINT fk_task_id "
383 " REFERENCES task_ids(task_id) "
384 " ON DELETE CASCADE,"
385 "name TEXT NOT NULL,"
386 "local_changes BOOLEAN DEFAULT 0,"
389 "complete_date TEXT,"
392 ");", NULL, NULL, &err)) {
393 g_warning ("failed to create tasks table: %s\n",
394 sqlite3_errmsg (db));
395 goto db_update_schema_0_to_1_ERROR;
399 if (sqlite3_exec (db, "CREATE TABLE key_values ("
400 "key TEXT PRIMARY KEY NOT NULL,"
402 ");", NULL, NULL, &err)) {
403 g_warning ("failed to create key_values table: %s\n",
404 sqlite3_errmsg (db));
405 goto db_update_schema_0_to_1_ERROR;
409 if (!db_set_last_sync (db, "0"))
410 goto db_update_schema_0_to_1_ERROR;
412 if (!db_set_schema_version (db, 1))
413 goto db_update_schema_0_to_1_ERROR;
415 db_transaction_commit (db);
419 db_update_schema_0_to_1_ERROR:
420 db_transaction_rollback (db);
421 sqlite3_finalize (query);
427 db_update_schema (sqlite3 *db)
429 gboolean (*update_funcs[]) (sqlite3 *)= {
430 db_update_schema_0_to_1,
433 gint schema_version = -1;
434 gint latest_version = G_N_ELEMENTS (update_funcs);
437 schema_version = get_schema_version (db);
439 if (schema_version > latest_version) {
440 g_error ("your database is newer than this version of the "
441 "app knows how to deal with -- bailing...");
442 } else if (schema_version == latest_version) {
446 for (i = schema_version; i < latest_version; i++) {
447 if (!update_funcs[i] (db)) {
448 g_error ("error upgrading from schema version %d to %d",
450 /* FIXME: probably better to just wipe the cache and
451 * start over, rather than crash (which would probably
452 * prevent us from ever automatically recovering) */
465 if (!g_file_test (get_data_dir (), G_FILE_TEST_EXISTS)) {
468 if (g_mkdir_with_parents (get_data_dir (), S_IRWXU | S_IRWXG)) {
470 g_error ("Can't create the data dir %s: %s; giving up...",
471 get_data_dir (), strerror (errno));
474 status = sqlite3_open (get_db_filename (), &db);
477 GError *error = NULL;
479 g_warning ("Can't open database: %s; deleting it...\n",
480 sqlite3_errmsg (db));
484 /* FIXME: open a banner warning that any pending tasks may have
485 * been lost (but actually just move the file as a backup file)
487 file = g_file_new_for_path (get_db_filename ());
488 if (!g_file_delete (file, NULL, &error)) {
489 g_error ("Could not delete the broken database: %s; "
490 "giving up...", error->message);
492 g_clear_error (&error);
498 status = sqlite3_open (get_db_filename (), &db);
509 time_val_to_iso8601 (GTimeVal *val)
512 g_strdup_printf ("'%s'", g_time_val_to_iso8601 (val)) :
517 sql_escape_quotes (const char *str)
525 /* escape all single quotes as double quotes (as is normal in SQL) */
526 tokens = g_strsplit (str, "'", -1);
527 final_str = g_strjoinv("''", tokens);
534 db_insert_or_update_local_task (sqlite3 *db,
536 const char *local_id,
537 gboolean local_changes)
539 gboolean success = TRUE;
542 GTimeVal *due, *deleted, *completed;
543 char *name_str, *due_str, *deleted_str, *completed_str;
546 const char *taskseries_id;
549 name_str = sql_escape_quotes (rtm_task_get_name (task));
550 due = rtm_task_get_due_date (task);
551 deleted = rtm_task_get_deleted_date (task);
552 completed = rtm_task_get_completed_date (task);
553 due_str = time_val_to_iso8601 (due);
554 deleted_str = time_val_to_iso8601 (deleted);
555 completed_str = time_val_to_iso8601 (completed);
557 /* FIXME: it doesn't actually have to be this complicated; see the
558 * actual code above and below */
560 * 1. add a new internal ID ('local_id') that's unique in the table
562 * 2. Add another 1-column table ('task_ids') with PRIMARY KEY column
564 * 3. prep-populate tasks_ids with a single special value (NULL, if
565 * possible). This will be used to designate "not yet written back to
567 * 4. make tasks.task_id have a foreign key constraint on
569 * 5. when inserting new tasks (ie, before they've been written back to
570 * the server), set their tasks.task_id to NULL (or whatever special
572 * 6. when we recieve tasks, check to see if we get a match for SELECT
573 * local_id from tasks WHERE
574 * tasks.task_id = rtm_task_get_id (task) AND tasks.name = name_str;
575 * 6.a. if we have a match, UPDATE tasks SET task_id, name, ...
576 * WHERE local_id = <local_id match>
577 * 6.b. if we didn't have a match, INSERT INTO tasks (task_id,
578 * name, ...) VALUES (rtm_task_get_id (task), ...);
579 * 7. these "pending" tasks can be treated normally when being
580 * manipulated locally
581 * 8. any task changed locally needs to have its 'local_changes' field
586 /* if we insert NULL for the local_id, it will nicely replace it with
587 * the next automatic value. This way we can use a single statement to
588 * update the other fields if the task already exists or create the task
589 * (with a new local_id) if necessary */
591 task_id = rtm_task_get_id (task);
592 task_id = task_id ? task_id : "NULL";
594 list_id = rtm_task_get_list_id (task);
595 list_id = list_id ? list_id : "NULL";
597 taskseries_id = rtm_task_get_taskseries_id (task);
598 taskseries_id = taskseries_id ? taskseries_id : "NULL";
600 /* FIXME: cut this? */
601 if (!rtm_task_get_list_id (task)) {
602 g_warning ("caching a task without a list ID -- this can "
603 "cause problems later");
606 if (!rtm_task_get_taskseries_id (task)) {
607 g_warning ("caching a task without a task series ID -- this "
608 "can cause problems later");
611 /* all but the name fields are already quoted or NULL */
612 statement = g_strdup_printf ("INSERT OR REPLACE INTO tasks "
613 "('local_id','task_id','name','due_date','delete_date',"
614 "'complete_date','list_id','taskseries_id',"
616 "VALUES (%s, %s, '%s', %s, %s, %s, %s, %s, %d)"
626 local_changes ? 1 : 0);
629 g_free (deleted_str);
630 g_free (completed_str);
632 if (sqlite3_exec (db, statement, NULL, NULL, &err)) {
633 g_warning ("failed to insert or update task in cache: %s\n",
634 sqlite3_errmsg (db));
645 cache_tasks_notify (MilkCache *cache,
649 MilkCachePrivate *priv;
652 g_return_if_fail (MILK_IS_CACHE (cache));
654 priv = MILK_CACHE_PRIVATE (cache);
656 /* FIXME: make the signals just emit all at once as a list, not
658 for (l = tasks; l; l = l->next) {
659 g_signal_emit (cache, signal_id, 0, l->data);
664 db_add_local_only_task (sqlite3 *db,
669 /* FIXME: cut this */
670 g_debug ("attempting to create new local-only task with name %s",
673 task = rtm_task_new ();
674 rtm_task_set_name (task, (char*) name);
676 if (!db_insert_or_update_local_task (db, task, "NULL", TRUE)) {
677 g_object_unref (task);
685 db_insert_or_update_task (sqlite3 *db,
687 gboolean local_changes,
688 gboolean *task_existed)
690 gboolean success = TRUE;
693 GTimeVal *due, *deleted, *completed;
696 sqlite3_stmt *query = NULL;
697 char *local_id = NULL;
698 char *local_id_formatted = NULL;
700 task_id = rtm_task_get_id (task);
701 g_return_val_if_fail (task_id, FALSE);
703 /* FIXME: should probably use a begin...commit block around this all */
705 /* FIXME: this needs a whole bucket of clean-up.
707 /* FIXME: make these all prepared statements */
709 statement = g_strdup_printf ("INSERT OR REPLACE INTO task_ids (task_id) VALUES ('%s');", task_id);
710 if (sqlite3_exec (db, statement, NULL, NULL, &err)) {
711 g_warning ("failed to insert task id %s in cache: %s\n",
712 task_id, sqlite3_errmsg (db));
719 /* try to find an existing task that we've already received from the
721 statement = g_strdup_printf ("SELECT local_id FROM tasks NATURAL JOIN task_ids WHERE task_id = '%s';", task_id);
723 if (sqlite3_prepare_v2 (db, statement, -1, &query, NULL)) {
724 g_warning ("failed to prepare statement: %s\n",
725 sqlite3_errmsg (db));
726 /* FIXME: use a goto instead, so we can carefully free any
727 * necessary memory */
731 while (status = sqlite3_step (query)) {
732 if (status == SQLITE_DONE) {
734 } else if (status != SQLITE_ROW) {
735 g_warning ("error stepping through SQL statement: %s",
736 sqlite3_errmsg (db));
737 /* FIXME: use a goto instead, so we can carefully free
738 * any necessary memory */
742 local_id = g_strdup (sqlite3_column_text (query, 0));
748 /* otherwise, try to find a matching task that we've added locally but
749 * haven't gotten confirmed by the server yet */
753 /* FIXME: cut this */
754 g_debug ("trying to update a local-only task");
756 name_str = sql_escape_quotes (rtm_task_get_name (task));
758 statement = g_strdup_printf ("SELECT local_id FROM tasks WHERE name = '%s';", name_str);
761 if (sqlite3_prepare_v2 (db, statement, -1, &query, NULL)) {
762 g_warning ("failed to prepare statement: %s\n",
763 sqlite3_errmsg (db));
764 /* FIXME: use a goto instead, so we can carefully free any
765 * necessary memory */
769 while (status = sqlite3_step (query)) {
770 if (status == SQLITE_DONE) {
772 } else if (status != SQLITE_ROW) {
773 g_warning ("error stepping through SQL statement: %s",
774 sqlite3_errmsg (db));
775 /* FIXME: use a goto instead, so we can carefully free
776 * any necessary memory */
780 local_id = g_strdup (sqlite3_column_text (query, 0));
788 *task_existed = (local_id != NULL);
790 /* FIXME: cut this */
791 g_debug ("got local_id: %s", local_id);
793 local_id_formatted = local_id ?
794 g_strdup_printf ("'%s'", local_id) :
798 /* FIXME: cut this */
799 g_debug ("formatted local_id:\n%s", local_id_formatted);
801 success &= db_insert_or_update_local_task (db, task,
802 local_id_formatted, local_changes);
803 g_free (local_id_formatted);
809 db_get_tasks_to_add_names (MilkCache *cache)
811 MilkCachePrivate *priv;
814 sqlite3_stmt *query = NULL;
818 priv = MILK_CACHE_PRIVATE (cache);
820 statement = g_strdup_printf ("SELECT name FROM tasks "
821 "WHERE task_id IS NULL;");
823 if (sqlite3_prepare_v2 (priv->db, statement, -1, &query, NULL)) {
824 g_warning ("failed to prepare statement: %s\n",
825 sqlite3_errmsg (priv->db));
826 goto db_get_tasks_to_add_names_ERROR;
829 while ((status = sqlite3_step (query))) {
830 if (status == SQLITE_DONE) {
832 } else if (status != SQLITE_ROW) {
833 g_warning ("error stepping through SQL statement: %s",
834 sqlite3_errmsg (priv->db));
835 goto db_get_tasks_to_add_names_ERROR;
838 names = g_list_prepend (names,
839 g_strdup (sqlite3_column_text (query, 0)));
842 goto db_get_tasks_to_add_names_OUT;
844 db_get_tasks_to_add_names_ERROR:
845 g_list_foreach (names, (GFunc) g_free, NULL);
849 db_get_tasks_to_add_names_OUT:
858 db_get_tasks_to_change (MilkCache *cache)
860 MilkCachePrivate *priv;
863 sqlite3_stmt *query = NULL;
867 priv = MILK_CACHE_PRIVATE (cache);
869 statement = g_strdup_printf ("SELECT task_id,name,due_date,list_id,"
870 "taskseries_id FROM tasks "
871 "WHERE local_changes=1;");
873 if (sqlite3_prepare_v2 (priv->db, statement, -1, &query, NULL)) {
874 g_warning ("failed to prepare statement: %s\n",
875 sqlite3_errmsg (priv->db));
876 goto db_get_tasks_to_change_ERROR;
879 while ((status = sqlite3_step (query))) {
883 if (status == SQLITE_DONE) {
885 } else if (status != SQLITE_ROW) {
886 g_warning ("error stepping through SQL statement: %s",
887 sqlite3_errmsg (priv->db));
888 goto db_get_tasks_to_change_ERROR;
891 task = rtm_task_new ();
892 rtm_task_set_id (task, (gchar*) sqlite3_column_text (query, 0));
893 rtm_task_set_name (task,
894 (gchar*) sqlite3_column_text (query, 1));
896 if (db_date_column_to_timeval (query, 2, &timeval))
897 rtm_task_set_due_date (task, &timeval);
899 rtm_task_set_list_id (task,
900 (gchar*) sqlite3_column_text (query, 3));
902 rtm_task_set_taskseries_id (task,
903 (gchar*) sqlite3_column_text (query, 4));
905 tasks = g_list_prepend (tasks, task);
908 goto db_get_tasks_to_change_OUT;
910 db_get_tasks_to_change_ERROR:
911 g_list_foreach (tasks, (GFunc) g_object_unref, NULL);
915 db_get_tasks_to_change_OUT:
924 cache_send_new_tasks (MilkCache *cache,
927 MilkCachePrivate *priv;
928 gboolean success = TRUE;
931 priv = MILK_CACHE_PRIVATE (cache);
933 /* FIXME: have a single function to get the sets of (new, changed,
934 * completed, deleted) tasks, then deal with each of them here */
935 names = db_get_tasks_to_add_names (cache);
937 /* FIXME: cut this */
938 g_debug ("trying to send %d new tasks", g_list_length (names));
940 /* FIXME: this entire block needs to be sequential as a whole but also
941 * async as a whole */
943 GList *tasks_added = NULL;
946 tasks_added = milk_auth_tasks_add (priv->auth, timeline, names);
948 for (l = tasks_added; l; l = l->next) {
949 /* FIXME: cut this */
950 g_debug (G_STRLOC ": trying to add task ID to "
951 "newly-inserted task: '%s' (%s);"
954 " taskseries ID: '%s', "
956 rtm_task_get_name (l->data),
957 rtm_task_get_id (l->data),
958 rtm_task_get_priority (l->data),
959 rtm_task_get_list_id (l->data),
960 rtm_task_get_taskseries_id (l->data)
963 /* mark these as having local changes so we'll send all
964 * there non-name attributes when we send the changes,
965 * in the next step */
966 db_insert_or_update_task (priv->db, l->data, TRUE,
970 /* not the most complete verification, but probably fine */
971 success &= (g_list_length (tasks_added) ==
972 g_list_length (names));
974 g_list_foreach (tasks_added, (GFunc) g_object_unref, NULL);
975 g_list_free (tasks_added);
978 g_list_foreach (names, (GFunc) g_free, NULL);
984 /* FIXME: cut this */
986 set_due_date (RtmTask *task,
987 const char *date_str)
989 GTimeVal timeval = {0};
991 /* FIXME: cut this */
992 g_debug ("going to decode date: '%s'", date_str);
994 g_time_val_from_iso8601 (date_str, &timeval);
996 rtm_task_set_due_date (task, &timeval);
1000 db_tasks_mark_as_synced (MilkCache *cache,
1003 MilkCachePrivate *priv;
1004 gboolean success = TRUE;
1005 GString *tasks_builder = NULL;
1008 gboolean first = TRUE;
1012 priv = MILK_CACHE_PRIVATE (cache);
1014 tasks_builder = g_string_new ("");
1015 for (l = tasks; l; l = l->next) {
1018 format = first ? "%s" : ",%s";
1020 g_string_append_printf (tasks_builder, format,
1021 rtm_task_get_id (l->data));
1024 task_ids = g_string_free (tasks_builder, FALSE);
1026 statement = g_strdup_printf ("UPDATE tasks "
1027 "SET local_changes=0 "
1028 "WHERE task_id IN (%s);",
1031 if (sqlite3_exec (priv->db, statement, NULL, NULL, &err)) {
1032 g_warning ("failed to acknowledge local changes were pushed "
1033 "to the server: %s\n",
1034 sqlite3_errmsg (priv->db));
1045 cache_send_changed_tasks (MilkCache *cache,
1048 MilkCachePrivate *priv;
1049 gboolean success = TRUE;
1050 GList *tasks_to_change;
1053 priv = MILK_CACHE_PRIVATE (cache);
1055 tasks_to_change = db_get_tasks_to_change (cache);
1057 if (!tasks_to_change)
1060 tasks_sent = milk_auth_tasks_send_changes (priv->auth,
1061 timeline, tasks_to_change);
1063 /* as above, if we miss any of these, the worst case is just resending
1064 * them later (vs. data loss or false caching) -- it's still not great
1065 * if you're on a flakey network, though */
1066 success &= (g_list_length (tasks_sent) ==
1067 g_list_length (tasks_to_change));
1069 success &= db_tasks_mark_as_synced (cache, tasks_sent);
1071 /* FIXME: cut this */
1072 g_debug ("successfully updated all the tasks: %d", success);
1074 g_list_foreach (tasks_to_change, (GFunc) g_object_unref, NULL);
1075 g_list_free (tasks_to_change);
1076 g_list_free (tasks_sent);
1081 /* FIXME: make this async */
1083 cache_send_changes (MilkCache *cache)
1085 MilkCachePrivate *priv;
1086 gboolean success = TRUE;
1089 priv = MILK_CACHE_PRIVATE (cache);
1091 /* FIXME: this makes the send_changes_hint fairly pointless, though we
1092 * may want to rename this function similarly */
1093 if (milk_auth_get_state (priv->auth) != MILK_AUTH_STATE_CONNECTED) {
1094 g_warning ("trying to send changes before auth has connected - we shouldn't be doing this (at least not until we've connected once)");
1099 /* FIXME: cut this */
1100 g_debug (G_STRLOC ": sending new (and updated tasks, once implemented) ");
1102 timeline = milk_auth_timeline_create (priv->auth, NULL);
1104 success &= cache_send_new_tasks (cache, timeline);
1105 success &= cache_send_changed_tasks (cache, timeline);
1107 /* FIXME: also get all the deleted tasks and delete them, and all the
1108 * completed tasks (complete_date IS NOT NULL && local_changes=1), and
1109 * apply those on the server */
1111 /* FIXME: cut this */
1112 g_debug ("looks like we successfully added the pending tasks");
1114 /* XXX: if we use the same timeline for creating these new tasks as
1115 * updating their attributes, we won't need to have this insane 2-step
1116 * process, right? (and thus updating the local_changes above shouldn't
1124 /* FIXME: make this async */
1126 cache_receive_changes (MilkCache *cache)
1128 MilkCachePrivate *priv = MILK_CACHE_PRIVATE (cache);
1131 GList *added = NULL, *changed = NULL, *finished = NULL;
1132 GTimeVal current_time;
1134 GError *error = NULL;
1136 if (milk_auth_get_state (priv->auth) != MILK_AUTH_STATE_CONNECTED) {
1140 g_get_current_time (¤t_time);
1141 new_sync = g_time_val_to_iso8601 (¤t_time);
1143 rtm_tasks = milk_auth_get_tasks (priv->auth, priv->last_sync, &error);
1145 g_error (G_STRLOC ": failed to retrieve latest tasks: %s",
1147 g_clear_error (&error);
1148 goto cache_receive_changes_ERROR;
1151 /* We don't wrap this in a begin...commit...rollback because it's better
1152 * to let the individual items get committed automatically. If one of
1153 * them fails, then we won't update the "last_sync" date. So the next
1154 * time we sync, we'll start from where we did last time, and so on,
1155 * until they all succeed at once. Any tasks that are already in the DB
1156 * will be "updated", whether their content actually changed or not
1158 for (l = rtm_tasks; l; l = g_list_delete_link (l, l)) {
1161 gboolean task_existed;
1163 task = RTM_TASK (l->data);
1165 if (!db_insert_or_update_task (priv->db, task, FALSE,
1167 goto cache_receive_changes_ERROR;
1169 /* FIXME: read the task back out of the DB for any sort
1170 * of normalization to happen transparently, and for
1171 * more commonality in the code reading out of the DB
1174 /* Task is deleted or completed */
1175 if (task_is_finished (task)) {
1176 finished = g_list_prepend (finished, task);
1178 /* Task has been changed */
1179 } else if (task_existed) {
1180 changed = g_list_prepend (changed, task);
1184 added = g_list_prepend (added, task);
1188 cache_tasks_notify (cache, added, signals[TASK_ADDED]);
1189 cache_tasks_notify (cache, changed, signals[TASK_CHANGED]);
1190 cache_tasks_notify (cache, finished, signals[TASK_FINISHED]);
1192 g_list_free (added);
1193 g_list_free (changed);
1194 g_list_free (finished);
1196 if (db_set_last_sync (priv->db, new_sync)) {
1197 g_free (priv->last_sync);
1198 priv->last_sync = new_sync;
1200 g_warning ("failed to set new sync timestamp to %d", new_sync);
1201 goto cache_receive_changes_ERROR;
1206 cache_receive_changes_ERROR:
1213 cache_send_receive_changes (MilkCache *cache)
1215 return cache_send_changes (cache) && cache_receive_changes (cache);
1219 restart_send_receive_poll (MilkCache *cache,
1220 gboolean poll_first)
1222 MilkCachePrivate *priv;
1224 priv = MILK_CACHE_PRIVATE (cache);
1226 /* FIXME: cut this */
1227 g_debug ("restarting the send/receive poll");
1229 if (priv->update_id)
1230 g_source_remove (priv->update_id);
1233 cache_send_receive_changes (cache);
1235 priv->update_id = g_timeout_add (CACHE_UPDATE_PERIOD,
1236 (GSourceFunc) cache_send_receive_changes, cache);
1239 task_is_finished (RtmTask *task)
1241 return (rtm_task_get_completed_date (task) ||
1242 rtm_task_get_deleted_date (task));
1246 auth_notify_cb (MilkAuth *auth,
1250 if (milk_auth_get_state (auth) == MILK_AUTH_STATE_CONNECTED) {
1251 cache_send_receive_changes (cache);
1256 milk_cache_set_auth (MilkCache *cache,
1259 MilkCachePrivate *priv;
1261 g_return_if_fail (MILK_IS_CACHE (cache));
1262 g_return_if_fail (MILK_IS_AUTH (auth));
1264 priv = MILK_CACHE_PRIVATE (cache);
1267 g_object_unref (priv->auth);
1269 priv->auth = g_object_ref (auth);
1271 restart_send_receive_poll (cache, FALSE);
1273 g_signal_emit (cache, signals[CLEARED], 0);
1275 g_signal_connect (priv->auth, "notify::state",
1276 G_CALLBACK (auth_notify_cb), cache);
1277 auth_notify_cb (priv->auth, NULL, cache);
1281 milk_cache_get_property (GObject *object,
1286 MilkCachePrivate *priv = MILK_CACHE_PRIVATE (object);
1288 switch (property_id)
1291 g_value_set_object (value, priv->auth);
1295 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id,
1301 milk_cache_set_property (GObject *object,
1303 const GValue *value,
1306 MilkCachePrivate *priv;
1309 cache = MILK_CACHE (object);
1310 priv = MILK_CACHE_PRIVATE (cache);
1312 switch (property_id)
1315 milk_cache_set_auth (cache, g_value_get_object (value));
1319 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id,
1325 milk_cache_get_active_tasks (MilkCache *cache)
1327 MilkCachePrivate *priv;
1330 g_return_val_if_fail (MILK_IS_CACHE (cache), NULL);
1332 priv = MILK_CACHE_PRIVATE (cache);
1334 tasks = db_get_tasks_active (priv->db);
1340 milk_cache_timeline_create (MilkCache *cache,
1343 MilkCachePrivate *priv;
1345 g_return_val_if_fail (MILK_IS_CACHE (cache), NULL);
1347 priv = MILK_CACHE_PRIVATE (cache);
1349 return milk_auth_timeline_create (priv->auth, error);
1352 /* FIXME: is the timeline argument even useful here? We should be able to just
1353 * assume it's being added right now, right? */
1355 /* FIXME: either fill in error appropriately or cut it as an argument, so the
1356 * caller doesn't assume it will be meaningful if we return NULL */
1358 milk_cache_task_add (MilkCache *cache,
1363 MilkCachePrivate *priv;
1366 g_return_val_if_fail (MILK_IS_CACHE (cache), NULL);
1368 priv = MILK_CACHE_PRIVATE (cache);
1370 task = db_add_local_only_task (priv->db, name);
1374 tasks = g_list_prepend (NULL, task);
1375 cache_tasks_notify (cache, tasks, signals[TASK_ADDED]);
1376 restart_send_receive_poll (cache, TRUE);
1378 g_list_free (tasks);
1385 milk_cache_task_complete (MilkCache *cache,
1390 MilkCachePrivate *priv;
1392 g_return_val_if_fail (MILK_IS_CACHE (cache), NULL);
1394 priv = MILK_CACHE_PRIVATE (cache);
1396 /* FIXME: mark the task as "to-complete" and set up the periodic task
1397 * that pushes the changes to the server; then immediately emit the
1398 * "task-finished" signal, so the model will immediately reflect that */
1400 return milk_auth_task_complete (priv->auth, timeline, task, error);
1404 milk_cache_task_delete (MilkCache *cache,
1409 MilkCachePrivate *priv;
1411 g_return_val_if_fail (MILK_IS_CACHE (cache), NULL);
1413 priv = MILK_CACHE_PRIVATE (cache);
1415 /* FIXME: mark the task as "to-delete" and set up the periodic task that
1416 * pushes the changes to the server; then immediately emit the
1417 * "task-finished" signal, so the model will immediately reflect that */
1419 return milk_auth_task_delete (priv->auth, timeline, task, error);
1422 /* XXX: this won't be necessary when the auth handles this transparently; or at
1423 * least this will merely be a signal to the auth that we're ready to
1424 * authenticate when it is */
1426 milk_cache_authenticate (MilkCache *cache)
1428 MilkCachePrivate *priv;
1430 g_return_if_fail (MILK_IS_CACHE (cache));
1432 priv = MILK_CACHE_PRIVATE (cache);
1434 milk_auth_log_in (priv->auth);
1438 milk_cache_dispose (GObject *object)
1440 MilkCachePrivate *priv = MILK_CACHE_PRIVATE (object);
1443 g_object_unref (priv->auth);
1447 if (priv->update_id) {
1448 g_source_remove (priv->update_id);
1449 priv->update_id = 0;
1454 milk_cache_finalize (GObject *object)
1456 MilkCachePrivate *priv = MILK_CACHE_PRIVATE (object);
1458 g_free (priv->last_sync);
1460 /* FIXME: should we do this at atexit() instead, for better safety? */
1461 sqlite3_close (priv->db);
1465 milk_cache_class_init (MilkCacheClass *klass)
1467 GObjectClass *object_class = G_OBJECT_CLASS (klass);
1469 g_type_class_add_private (klass, sizeof (MilkCachePrivate));
1471 object_class->get_property = milk_cache_get_property;
1472 object_class->set_property = milk_cache_set_property;
1473 object_class->dispose = milk_cache_dispose;
1474 object_class->finalize = milk_cache_finalize;
1476 g_object_class_install_property
1481 "Authentication proxy",
1482 "Remember The Milk authentication proxy.",
1484 G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY |
1485 G_PARAM_STATIC_STRINGS));
1487 signals[CLEARED] = g_signal_new
1489 MILK_TYPE_CACHE, G_SIGNAL_RUN_LAST, 0, NULL, NULL,
1490 g_cclosure_marshal_VOID__VOID, G_TYPE_NONE, 0);
1492 signals[TASK_ADDED] = g_signal_new
1494 MILK_TYPE_CACHE, G_SIGNAL_RUN_LAST, 0, NULL, NULL,
1495 g_cclosure_marshal_VOID__OBJECT, G_TYPE_NONE, 1,
1498 signals[TASK_FINISHED] = g_signal_new
1500 MILK_TYPE_CACHE, G_SIGNAL_RUN_LAST, 0, NULL, NULL,
1501 g_cclosure_marshal_VOID__OBJECT, G_TYPE_NONE, 1, RTM_TYPE_TASK);
1503 signals[TASK_CHANGED] = g_signal_new
1505 MILK_TYPE_CACHE, G_SIGNAL_RUN_LAST, 0, NULL, NULL,
1506 g_cclosure_marshal_VOID__OBJECT, G_TYPE_NONE, 1,
1511 milk_cache_init (MilkCache *self)
1513 MilkCachePrivate *priv;
1515 self->priv = priv = G_TYPE_INSTANCE_GET_PRIVATE (
1516 self, MILK_TYPE_CACHE, MilkCachePrivate);
1518 /* open the DB, creating a new one if necessary */
1519 priv->db = db_open ();
1520 db_update_schema (priv->db);
1521 priv->last_sync = db_get_last_sync (priv->db);
1525 milk_cache_get_default ()
1527 if (!default_cache) {
1528 default_cache = g_object_new (MILK_TYPE_CACHE,
1529 "auth", milk_auth_get_default (),
1533 return default_cache;