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, priority, list_id, taskseries_id, "
255 "delete_date IS NULL AND "
256 "complete_date IS NULL"
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;
265 while (status = sqlite3_step (query)) {
270 if (status == SQLITE_DONE) {
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;
278 task = rtm_task_new ();
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));
289 if (db_date_column_to_timeval (query, 5, &timeval))
290 rtm_task_set_due_date (task, &timeval);
292 tasks = g_list_prepend (tasks, task);
295 get_schema_version_OUT:
296 sqlite3_finalize (query);
303 db_get_last_sync (sqlite3 *db)
305 return db_get_key_value (db, "last_sync");
309 db_set_key_value (sqlite3 *db,
313 gboolean success = TRUE;
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);
322 if (sqlite3_exec (db, statement, NULL, NULL, &err)) {
323 g_warning ("failed to update schema version: %s\n",
324 sqlite3_errmsg (db));
335 db_set_last_sync (sqlite3 *db,
336 const char *last_sync)
338 return db_set_key_value (db, "last_sync", last_sync);
342 db_update_schema_0_to_1 (sqlite3 *db)
344 sqlite3_stmt *query = NULL;
347 db_transaction_begin (db);
349 /* FIXME: actually create the required triggers */
351 /* FIXME: ugh... sqlite supports foreign keys, but don't actualyl
352 * /enforce them/ (thanks, guys...); here's a way to enforce them using
355 * http://www.justatheory.com/computers/databases/sqlite/
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)
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;
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
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;
387 if (sqlite3_exec (db, "CREATE TABLE tasks ("
388 "local_id INTEGER PRIMARY KEY NOT NULL,"
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,"
398 "complete_date 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;
408 if (sqlite3_exec (db, "CREATE TABLE key_values ("
409 "key TEXT PRIMARY KEY NOT NULL,"
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;
418 if (!db_set_last_sync (db, "0"))
419 goto db_update_schema_0_to_1_ERROR;
421 if (!db_set_schema_version (db, 1))
422 goto db_update_schema_0_to_1_ERROR;
424 db_transaction_commit (db);
428 db_update_schema_0_to_1_ERROR:
429 db_transaction_rollback (db);
430 sqlite3_finalize (query);
436 db_update_schema (sqlite3 *db)
438 gboolean (*update_funcs[]) (sqlite3 *)= {
439 db_update_schema_0_to_1,
442 gint schema_version = -1;
443 gint latest_version = G_N_ELEMENTS (update_funcs);
446 schema_version = get_schema_version (db);
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) {
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",
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) */
474 if (!g_file_test (get_data_dir (), G_FILE_TEST_EXISTS)) {
477 if (g_mkdir_with_parents (get_data_dir (), S_IRWXU | S_IRWXG)) {
479 g_error ("Can't create the data dir %s: %s; giving up...",
480 get_data_dir (), strerror (errno));
483 status = sqlite3_open (get_db_filename (), &db);
486 GError *error = NULL;
488 g_warning ("Can't open database: %s; deleting it...\n",
489 sqlite3_errmsg (db));
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)
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);
501 g_clear_error (&error);
507 status = sqlite3_open (get_db_filename (), &db);
518 time_val_to_iso8601 (GTimeVal *val)
521 g_strdup_printf ("'%s'", g_time_val_to_iso8601 (val)) :
526 sql_escape_quotes (const char *str)
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);
543 db_insert_or_update_local_task (sqlite3 *db,
545 const char *local_id,
546 gboolean local_changes)
548 gboolean success = TRUE;
551 GTimeVal *due, *deleted, *completed;
552 char *name_str, *due_str, *deleted_str, *completed_str;
554 const char *priority;
556 const char *taskseries_id;
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);
567 /* FIXME: it doesn't actually have to be this complicated; see the
568 * actual code above and below */
570 * 1. add a new internal ID ('local_id') that's unique in the table
572 * 2. Add another 1-column table ('task_ids') with PRIMARY KEY column
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
577 * 4. make tasks.task_id have a foreign key constraint on
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
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
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 */
601 task_id = rtm_task_get_id (task);
602 task_id = task_id ? task_id : "NULL";
604 priority = rtm_task_get_priority (task);
605 priority = priority ? priority : "N";
607 list_id = rtm_task_get_list_id (task);
608 list_id = list_id ? list_id : "NULL";
610 taskseries_id = rtm_task_get_taskseries_id (task);
611 taskseries_id = taskseries_id ? taskseries_id : "NULL";
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");
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");
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)"
640 local_changes ? 1 : 0);
643 g_free (deleted_str);
644 g_free (completed_str);
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));
659 cache_tasks_notify (MilkCache *cache,
663 MilkCachePrivate *priv;
666 g_return_if_fail (MILK_IS_CACHE (cache));
668 priv = MILK_CACHE_PRIVATE (cache);
670 /* FIXME: make the signals just emit all at once as a list, not
672 for (l = tasks; l; l = l->next) {
673 g_signal_emit (cache, signal_id, 0, l->data);
678 db_add_local_only_task (sqlite3 *db,
683 /* FIXME: cut this */
684 g_debug ("attempting to create new local-only task with name %s",
687 task = rtm_task_new ();
688 rtm_task_set_name (task, (char*) name);
690 if (!db_insert_or_update_local_task (db, task, "NULL", TRUE)) {
691 g_object_unref (task);
699 db_insert_or_update_task (sqlite3 *db,
701 gboolean local_changes,
702 gboolean *task_existed)
704 gboolean success = TRUE;
707 GTimeVal *due, *deleted, *completed;
710 sqlite3_stmt *query = NULL;
711 char *local_id = NULL;
712 char *local_id_formatted = NULL;
714 task_id = rtm_task_get_id (task);
715 g_return_val_if_fail (task_id, FALSE);
717 /* FIXME: should probably use a begin...commit block around this all */
719 /* FIXME: this needs a whole bucket of clean-up.
721 /* FIXME: make these all prepared statements */
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));
733 /* try to find an existing task that we've already received from the
735 statement = g_strdup_printf ("SELECT local_id FROM tasks NATURAL JOIN task_ids WHERE task_id = '%s';", task_id);
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 */
745 while (status = sqlite3_step (query)) {
746 if (status == SQLITE_DONE) {
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 */
756 local_id = g_strdup (sqlite3_column_text (query, 0));
762 /* otherwise, try to find a matching task that we've added locally but
763 * haven't gotten confirmed by the server yet */
767 /* FIXME: cut this */
768 g_debug ("trying to update a local-only task");
770 name_str = sql_escape_quotes (rtm_task_get_name (task));
772 statement = g_strdup_printf ("SELECT local_id FROM tasks WHERE name = '%s';", name_str);
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 */
783 while (status = sqlite3_step (query)) {
784 if (status == SQLITE_DONE) {
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 */
794 local_id = g_strdup (sqlite3_column_text (query, 0));
802 *task_existed = (local_id != NULL);
804 /* FIXME: cut this */
805 g_debug ("got local_id: %s", local_id);
807 local_id_formatted = local_id ?
808 g_strdup_printf ("'%s'", local_id) :
812 /* FIXME: cut this */
813 g_debug ("formatted local_id:\n%s", local_id_formatted);
815 success &= db_insert_or_update_local_task (db, task,
816 local_id_formatted, local_changes);
818 g_free (local_id_formatted);
824 db_get_tasks_to_add_names (MilkCache *cache)
826 MilkCachePrivate *priv;
829 sqlite3_stmt *query = NULL;
833 priv = MILK_CACHE_PRIVATE (cache);
835 statement = g_strdup_printf ("SELECT name FROM tasks "
836 "WHERE task_id IS NULL;");
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;
844 while ((status = sqlite3_step (query))) {
845 if (status == SQLITE_DONE) {
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;
853 names = g_list_prepend (names,
854 g_strdup (sqlite3_column_text (query, 0)));
857 goto db_get_tasks_to_add_names_OUT;
859 db_get_tasks_to_add_names_ERROR:
860 g_list_foreach (names, (GFunc) g_free, NULL);
864 db_get_tasks_to_add_names_OUT:
873 db_get_tasks_to_change (MilkCache *cache)
875 MilkCachePrivate *priv;
878 sqlite3_stmt *query = NULL;
882 priv = MILK_CACHE_PRIVATE (cache);
884 statement = g_strdup_printf ("SELECT task_id,name,due_date,list_id,"
885 "taskseries_id FROM tasks "
886 "WHERE local_changes=1;");
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;
894 while ((status = sqlite3_step (query))) {
898 if (status == SQLITE_DONE) {
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;
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));
911 if (db_date_column_to_timeval (query, 2, &timeval))
912 rtm_task_set_due_date (task, &timeval);
914 rtm_task_set_list_id (task,
915 (gchar*) sqlite3_column_text (query, 3));
917 rtm_task_set_taskseries_id (task,
918 (gchar*) sqlite3_column_text (query, 4));
920 tasks = g_list_prepend (tasks, task);
923 goto db_get_tasks_to_change_OUT;
925 db_get_tasks_to_change_ERROR:
926 g_list_foreach (tasks, (GFunc) g_object_unref, NULL);
930 db_get_tasks_to_change_OUT:
939 cache_send_new_tasks (MilkCache *cache,
942 MilkCachePrivate *priv;
943 gboolean success = TRUE;
946 priv = MILK_CACHE_PRIVATE (cache);
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);
952 /* FIXME: cut this */
953 g_debug ("trying to send %d new tasks", g_list_length (names));
955 /* FIXME: this entire block needs to be sequential as a whole but also
956 * async as a whole */
958 GList *tasks_added = NULL;
961 tasks_added = milk_auth_tasks_add (priv->auth, timeline, names);
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);"
969 " taskseries ID: '%s', "
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)
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,
985 /* not the most complete verification, but probably fine */
986 success &= (g_list_length (tasks_added) ==
987 g_list_length (names));
989 g_list_foreach (tasks_added, (GFunc) g_object_unref, NULL);
990 g_list_free (tasks_added);
993 g_list_foreach (names, (GFunc) g_free, NULL);
999 /* FIXME: cut this */
1001 set_due_date (RtmTask *task,
1002 const char *date_str)
1004 GTimeVal timeval = {0};
1006 /* FIXME: cut this */
1007 g_debug ("going to decode date: '%s'", date_str);
1009 g_time_val_from_iso8601 (date_str, &timeval);
1011 rtm_task_set_due_date (task, &timeval);
1015 db_tasks_mark_as_synced (MilkCache *cache,
1018 MilkCachePrivate *priv;
1019 gboolean success = TRUE;
1020 GString *tasks_builder = NULL;
1023 gboolean first = TRUE;
1027 priv = MILK_CACHE_PRIVATE (cache);
1029 tasks_builder = g_string_new ("");
1030 for (l = tasks; l; l = l->next) {
1033 format = first ? "%s" : ",%s";
1035 g_string_append_printf (tasks_builder, format,
1036 rtm_task_get_id (l->data));
1039 task_ids = g_string_free (tasks_builder, FALSE);
1041 statement = g_strdup_printf ("UPDATE tasks "
1042 "SET local_changes=0 "
1043 "WHERE task_id IN (%s);",
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));
1060 cache_send_changed_tasks (MilkCache *cache,
1063 MilkCachePrivate *priv;
1064 gboolean success = TRUE;
1065 GList *tasks_to_change;
1068 priv = MILK_CACHE_PRIVATE (cache);
1070 tasks_to_change = db_get_tasks_to_change (cache);
1072 if (!tasks_to_change)
1075 tasks_sent = milk_auth_tasks_send_changes (priv->auth,
1076 timeline, tasks_to_change);
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));
1084 success &= db_tasks_mark_as_synced (cache, tasks_sent);
1086 /* FIXME: cut this */
1087 g_debug ("successfully updated all the tasks: %d", success);
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);
1096 /* FIXME: make this async */
1098 cache_send_changes (MilkCache *cache)
1100 MilkCachePrivate *priv;
1101 gboolean success = TRUE;
1104 priv = MILK_CACHE_PRIVATE (cache);
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)");
1114 /* FIXME: cut this */
1115 g_debug (G_STRLOC ": sending new (and updated tasks, once implemented) ");
1117 timeline = milk_auth_timeline_create (priv->auth, NULL);
1119 success &= cache_send_new_tasks (cache, timeline);
1120 success &= cache_send_changed_tasks (cache, timeline);
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 */
1126 /* FIXME: cut this */
1127 g_debug ("looks like we successfully added the pending tasks");
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
1139 /* FIXME: make this async */
1141 cache_receive_changes (MilkCache *cache)
1143 MilkCachePrivate *priv = MILK_CACHE_PRIVATE (cache);
1146 GList *added = NULL, *changed = NULL, *finished = NULL;
1147 GTimeVal current_time;
1149 GError *error = NULL;
1151 if (milk_auth_get_state (priv->auth) != MILK_AUTH_STATE_CONNECTED) {
1155 g_get_current_time (¤t_time);
1156 new_sync = g_time_val_to_iso8601 (¤t_time);
1158 rtm_tasks = milk_auth_get_tasks (priv->auth, priv->last_sync, &error);
1160 g_error (G_STRLOC ": failed to retrieve latest tasks: %s",
1162 g_clear_error (&error);
1163 goto cache_receive_changes_ERROR;
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
1173 for (l = rtm_tasks; l; l = g_list_delete_link (l, l)) {
1176 gboolean task_existed;
1178 task = RTM_TASK (l->data);
1180 if (!db_insert_or_update_task (priv->db, task, FALSE,
1182 goto cache_receive_changes_ERROR;
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
1189 /* Task is deleted or completed */
1190 if (task_is_finished (task)) {
1191 finished = g_list_prepend (finished, task);
1193 /* Task has been changed */
1194 } else if (task_existed) {
1195 changed = g_list_prepend (changed, task);
1199 added = g_list_prepend (added, task);
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]);
1207 g_list_free (added);
1208 g_list_free (changed);
1209 g_list_free (finished);
1211 if (db_set_last_sync (priv->db, new_sync)) {
1212 g_free (priv->last_sync);
1213 priv->last_sync = new_sync;
1215 g_warning ("failed to set new sync timestamp to %d", new_sync);
1216 goto cache_receive_changes_ERROR;
1221 cache_receive_changes_ERROR:
1228 cache_send_receive_changes (MilkCache *cache)
1230 cache_send_changes (cache);
1231 cache_receive_changes (cache);
1237 restart_send_receive_poll (MilkCache *cache,
1238 gboolean poll_first)
1240 MilkCachePrivate *priv;
1242 priv = MILK_CACHE_PRIVATE (cache);
1244 if (priv->update_id)
1245 g_source_remove (priv->update_id);
1248 cache_send_receive_changes (cache);
1250 priv->update_id = g_timeout_add (CACHE_UPDATE_PERIOD,
1251 (GSourceFunc) cache_send_receive_changes, cache);
1254 task_is_finished (RtmTask *task)
1256 return (rtm_task_get_completed_date (task) ||
1257 rtm_task_get_deleted_date (task));
1261 auth_notify_cb (MilkAuth *auth,
1265 if (milk_auth_get_state (auth) == MILK_AUTH_STATE_CONNECTED) {
1266 cache_send_receive_changes (cache);
1271 milk_cache_set_auth (MilkCache *cache,
1274 MilkCachePrivate *priv;
1276 g_return_if_fail (MILK_IS_CACHE (cache));
1277 g_return_if_fail (MILK_IS_AUTH (auth));
1279 priv = MILK_CACHE_PRIVATE (cache);
1282 g_object_unref (priv->auth);
1284 priv->auth = g_object_ref (auth);
1286 restart_send_receive_poll (cache, FALSE);
1288 g_signal_emit (cache, signals[CLEARED], 0);
1290 g_signal_connect (priv->auth, "notify::state",
1291 G_CALLBACK (auth_notify_cb), cache);
1292 auth_notify_cb (priv->auth, NULL, cache);
1296 milk_cache_get_property (GObject *object,
1301 MilkCachePrivate *priv = MILK_CACHE_PRIVATE (object);
1303 switch (property_id)
1306 g_value_set_object (value, priv->auth);
1310 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id,
1316 milk_cache_set_property (GObject *object,
1318 const GValue *value,
1321 MilkCachePrivate *priv;
1324 cache = MILK_CACHE (object);
1325 priv = MILK_CACHE_PRIVATE (cache);
1327 switch (property_id)
1330 milk_cache_set_auth (cache, g_value_get_object (value));
1334 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id,
1340 milk_cache_get_active_tasks (MilkCache *cache)
1342 MilkCachePrivate *priv;
1345 g_return_val_if_fail (MILK_IS_CACHE (cache), NULL);
1347 priv = MILK_CACHE_PRIVATE (cache);
1349 tasks = db_get_tasks_active (priv->db);
1355 milk_cache_timeline_create (MilkCache *cache,
1358 MilkCachePrivate *priv;
1360 g_return_val_if_fail (MILK_IS_CACHE (cache), NULL);
1362 priv = MILK_CACHE_PRIVATE (cache);
1364 return milk_auth_timeline_create (priv->auth, error);
1367 /* FIXME: is the timeline argument even useful here? We should be able to just
1368 * assume it's being added right now, right? */
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 */
1373 milk_cache_task_add (MilkCache *cache,
1378 MilkCachePrivate *priv;
1381 g_return_val_if_fail (MILK_IS_CACHE (cache), NULL);
1383 priv = MILK_CACHE_PRIVATE (cache);
1385 task = db_add_local_only_task (priv->db, name);
1389 tasks = g_list_prepend (NULL, task);
1390 cache_tasks_notify (cache, tasks, signals[TASK_ADDED]);
1391 restart_send_receive_poll (cache, TRUE);
1393 g_list_free (tasks);
1400 milk_cache_task_complete (MilkCache *cache,
1405 MilkCachePrivate *priv;
1407 g_return_val_if_fail (MILK_IS_CACHE (cache), NULL);
1409 priv = MILK_CACHE_PRIVATE (cache);
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 */
1415 return milk_auth_task_complete (priv->auth, timeline, task, error);
1419 milk_cache_task_delete (MilkCache *cache,
1424 MilkCachePrivate *priv;
1426 g_return_val_if_fail (MILK_IS_CACHE (cache), NULL);
1428 priv = MILK_CACHE_PRIVATE (cache);
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 */
1434 return milk_auth_task_delete (priv->auth, timeline, task, error);
1438 milk_cache_task_set_priority (MilkCache *cache,
1441 const char *priority,
1444 MilkCachePrivate *priv;
1446 g_return_val_if_fail (MILK_IS_CACHE (cache), NULL);
1448 priv = MILK_CACHE_PRIVATE (cache);
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 */
1454 return milk_auth_task_set_priority (priv->auth, timeline, task,
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 */
1462 milk_cache_authenticate (MilkCache *cache)
1464 MilkCachePrivate *priv;
1466 g_return_if_fail (MILK_IS_CACHE (cache));
1468 priv = MILK_CACHE_PRIVATE (cache);
1470 milk_auth_log_in (priv->auth);
1474 milk_cache_dispose (GObject *object)
1476 MilkCachePrivate *priv = MILK_CACHE_PRIVATE (object);
1479 g_object_unref (priv->auth);
1483 if (priv->update_id) {
1484 g_source_remove (priv->update_id);
1485 priv->update_id = 0;
1490 milk_cache_finalize (GObject *object)
1492 MilkCachePrivate *priv = MILK_CACHE_PRIVATE (object);
1494 g_free (priv->last_sync);
1496 /* FIXME: should we do this at atexit() instead, for better safety? */
1497 sqlite3_close (priv->db);
1501 milk_cache_class_init (MilkCacheClass *klass)
1503 GObjectClass *object_class = G_OBJECT_CLASS (klass);
1505 g_type_class_add_private (klass, sizeof (MilkCachePrivate));
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;
1512 g_object_class_install_property
1517 "Authentication proxy",
1518 "Remember The Milk authentication proxy.",
1520 G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY |
1521 G_PARAM_STATIC_STRINGS));
1523 signals[CLEARED] = g_signal_new
1525 MILK_TYPE_CACHE, G_SIGNAL_RUN_LAST, 0, NULL, NULL,
1526 g_cclosure_marshal_VOID__VOID, G_TYPE_NONE, 0);
1528 signals[TASK_ADDED] = g_signal_new
1530 MILK_TYPE_CACHE, G_SIGNAL_RUN_LAST, 0, NULL, NULL,
1531 g_cclosure_marshal_VOID__OBJECT, G_TYPE_NONE, 1,
1534 signals[TASK_FINISHED] = g_signal_new
1536 MILK_TYPE_CACHE, G_SIGNAL_RUN_LAST, 0, NULL, NULL,
1537 g_cclosure_marshal_VOID__OBJECT, G_TYPE_NONE, 1, RTM_TYPE_TASK);
1539 signals[TASK_CHANGED] = g_signal_new
1541 MILK_TYPE_CACHE, G_SIGNAL_RUN_LAST, 0, NULL, NULL,
1542 g_cclosure_marshal_VOID__OBJECT, G_TYPE_NONE, 1,
1547 milk_cache_init (MilkCache *self)
1549 MilkCachePrivate *priv;
1551 self->priv = priv = G_TYPE_INSTANCE_GET_PRIVATE (
1552 self, MILK_TYPE_CACHE, MilkCachePrivate);
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);
1561 milk_cache_get_default ()
1563 if (!default_cache) {
1564 default_cache = g_object_new (MILK_TYPE_CACHE,
1565 "auth", milk_auth_get_default (),
1569 return default_cache;