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 /* FIXME: get the priority */
251 statement = g_strdup_printf ("SELECT "
252 "task_id, name, priority, due_date FROM tasks "
254 "delete_date IS NULL AND "
255 "complete_date IS NULL"
258 if (sqlite3_prepare_v2 (db, statement, -1, &query, NULL)) {
259 g_warning ("failed to prepare statement: %s\n",
260 sqlite3_errmsg (db));
261 goto get_schema_version_OUT;
264 while (status = sqlite3_step (query)) {
269 if (status == SQLITE_DONE) {
271 } else if (status != SQLITE_ROW) {
272 g_warning ("error stepping through SQL statement: %s",
273 sqlite3_errmsg (db));
274 goto get_schema_version_OUT;
277 task = rtm_task_new ();
279 rtm_task_set_id (task, (char*)sqlite3_column_text (query, 0));
280 rtm_task_set_name (task, (char*)sqlite3_column_text (query, 1));
281 rtm_task_set_priority (task,
282 (char*)sqlite3_column_text (query, 2));
284 if (db_date_column_to_timeval (query, 3, &timeval))
285 rtm_task_set_due_date (task, &timeval);
287 tasks = g_list_prepend (tasks, task);
290 get_schema_version_OUT:
291 sqlite3_finalize (query);
298 db_get_last_sync (sqlite3 *db)
300 return db_get_key_value (db, "last_sync");
304 db_set_key_value (sqlite3 *db,
308 gboolean success = TRUE;
312 /* FIXME: probably safer to create them per-schema update and then fail
313 * if they don't exist */
314 statement = g_strdup_printf ("INSERT OR REPLACE INTO key_values "
315 "('key', 'value') VALUES ('%s', '%s');", key, value);
317 if (sqlite3_exec (db, statement, NULL, NULL, &err)) {
318 g_warning ("failed to update schema version: %s\n",
319 sqlite3_errmsg (db));
330 db_set_last_sync (sqlite3 *db,
331 const char *last_sync)
333 return db_set_key_value (db, "last_sync", last_sync);
337 db_update_schema_0_to_1 (sqlite3 *db)
339 sqlite3_stmt *query = NULL;
342 db_transaction_begin (db);
344 /* FIXME: actually create the required triggers */
346 /* FIXME: ugh... sqlite supports foreign keys, but don't actualyl
347 * /enforce them/ (thanks, guys...); here's a way to enforce them using
350 * http://www.justatheory.com/computers/databases/sqlite/
352 * it seems to be fixed in sqlite 3.6.19, but even Karmic has 3.6.16
353 * (and Fremantle has 3.6.14)
356 if (sqlite3_exec (db, "CREATE TABLE task_ids "
357 "(task_id TEXT PRIMARY KEY);", NULL, NULL, &err)) {
358 g_warning ("failed to create tasks table: %s\n",
359 sqlite3_errmsg (db));
360 goto db_update_schema_0_to_1_ERROR;
364 /* XXX: there is a subtle race here where we can add a task on the
365 * server but disconnect before we get a confirmation, and change the
366 * name on the server before we recieve it. Then we'll end up
367 * re-submitting this task with its original name again (until we don't
368 * hit this condition again). The risk is fairly low, but it's still
371 /* insert a special task ID (NULL) to designate that we haven't
372 * gotten a confirmation for adding the task yet (and thus haven't
373 * gotten its server-side ID) */
374 if (sqlite3_exec (db, "INSERT INTO task_ids "
375 "(task_id) values (NULL);", NULL, NULL, &err)) {
376 g_warning ("failed to insert special 'unset' value: %s\n",
377 sqlite3_errmsg (db));
378 goto db_update_schema_0_to_1_ERROR;
382 if (sqlite3_exec (db, "CREATE TABLE tasks ("
383 "local_id INTEGER PRIMARY KEY NOT NULL,"
385 " CONSTRAINT fk_task_id "
386 " REFERENCES task_ids(task_id) "
387 " ON DELETE CASCADE,"
388 "name TEXT NOT NULL,"
389 "local_changes BOOLEAN DEFAULT 0,"
393 "complete_date TEXT,"
396 ");", NULL, NULL, &err)) {
397 g_warning ("failed to create tasks table: %s\n",
398 sqlite3_errmsg (db));
399 goto db_update_schema_0_to_1_ERROR;
403 if (sqlite3_exec (db, "CREATE TABLE key_values ("
404 "key TEXT PRIMARY KEY NOT NULL,"
406 ");", NULL, NULL, &err)) {
407 g_warning ("failed to create key_values table: %s\n",
408 sqlite3_errmsg (db));
409 goto db_update_schema_0_to_1_ERROR;
413 if (!db_set_last_sync (db, "0"))
414 goto db_update_schema_0_to_1_ERROR;
416 if (!db_set_schema_version (db, 1))
417 goto db_update_schema_0_to_1_ERROR;
419 db_transaction_commit (db);
423 db_update_schema_0_to_1_ERROR:
424 db_transaction_rollback (db);
425 sqlite3_finalize (query);
431 db_update_schema (sqlite3 *db)
433 gboolean (*update_funcs[]) (sqlite3 *)= {
434 db_update_schema_0_to_1,
437 gint schema_version = -1;
438 gint latest_version = G_N_ELEMENTS (update_funcs);
441 schema_version = get_schema_version (db);
443 if (schema_version > latest_version) {
444 g_error ("your database is newer than this version of the "
445 "app knows how to deal with -- bailing...");
446 } else if (schema_version == latest_version) {
450 for (i = schema_version; i < latest_version; i++) {
451 if (!update_funcs[i] (db)) {
452 g_error ("error upgrading from schema version %d to %d",
454 /* FIXME: probably better to just wipe the cache and
455 * start over, rather than crash (which would probably
456 * prevent us from ever automatically recovering) */
469 if (!g_file_test (get_data_dir (), G_FILE_TEST_EXISTS)) {
472 if (g_mkdir_with_parents (get_data_dir (), S_IRWXU | S_IRWXG)) {
474 g_error ("Can't create the data dir %s: %s; giving up...",
475 get_data_dir (), strerror (errno));
478 status = sqlite3_open (get_db_filename (), &db);
481 GError *error = NULL;
483 g_warning ("Can't open database: %s; deleting it...\n",
484 sqlite3_errmsg (db));
488 /* FIXME: open a banner warning that any pending tasks may have
489 * been lost (but actually just move the file as a backup file)
491 file = g_file_new_for_path (get_db_filename ());
492 if (!g_file_delete (file, NULL, &error)) {
493 g_error ("Could not delete the broken database: %s; "
494 "giving up...", error->message);
496 g_clear_error (&error);
502 status = sqlite3_open (get_db_filename (), &db);
513 time_val_to_iso8601 (GTimeVal *val)
516 g_strdup_printf ("'%s'", g_time_val_to_iso8601 (val)) :
521 sql_escape_quotes (const char *str)
529 /* escape all single quotes as double quotes (as is normal in SQL) */
530 tokens = g_strsplit (str, "'", -1);
531 final_str = g_strjoinv("''", tokens);
538 db_insert_or_update_local_task (sqlite3 *db,
540 const char *local_id,
541 gboolean local_changes)
543 gboolean success = TRUE;
546 GTimeVal *due, *deleted, *completed;
547 char *name_str, *due_str, *deleted_str, *completed_str;
549 const char *priority;
551 const char *taskseries_id;
554 name_str = sql_escape_quotes (rtm_task_get_name (task));
555 due = rtm_task_get_due_date (task);
556 deleted = rtm_task_get_deleted_date (task);
557 completed = rtm_task_get_completed_date (task);
558 due_str = time_val_to_iso8601 (due);
559 deleted_str = time_val_to_iso8601 (deleted);
560 completed_str = time_val_to_iso8601 (completed);
562 /* FIXME: it doesn't actually have to be this complicated; see the
563 * actual code above and below */
565 * 1. add a new internal ID ('local_id') that's unique in the table
567 * 2. Add another 1-column table ('task_ids') with PRIMARY KEY column
569 * 3. prep-populate tasks_ids with a single special value (NULL, if
570 * possible). This will be used to designate "not yet written back to
572 * 4. make tasks.task_id have a foreign key constraint on
574 * 5. when inserting new tasks (ie, before they've been written back to
575 * the server), set their tasks.task_id to NULL (or whatever special
577 * 6. when we recieve tasks, check to see if we get a match for SELECT
578 * local_id from tasks WHERE
579 * tasks.task_id = rtm_task_get_id (task) AND tasks.name = name_str;
580 * 6.a. if we have a match, UPDATE tasks SET task_id, name, ...
581 * WHERE local_id = <local_id match>
582 * 6.b. if we didn't have a match, INSERT INTO tasks (task_id,
583 * name, ...) VALUES (rtm_task_get_id (task), ...);
584 * 7. these "pending" tasks can be treated normally when being
585 * manipulated locally
586 * 8. any task changed locally needs to have its 'local_changes' field
591 /* if we insert NULL for the local_id, it will nicely replace it with
592 * the next automatic value. This way we can use a single statement to
593 * update the other fields if the task already exists or create the task
594 * (with a new local_id) if necessary */
596 task_id = rtm_task_get_id (task);
597 task_id = task_id ? task_id : "NULL";
599 priority = rtm_task_get_priority (task);
600 priority = priority ? priority : "N";
602 list_id = rtm_task_get_list_id (task);
603 list_id = list_id ? list_id : "NULL";
605 taskseries_id = rtm_task_get_taskseries_id (task);
606 taskseries_id = taskseries_id ? taskseries_id : "NULL";
608 /* FIXME: cut this? */
609 if (!rtm_task_get_list_id (task)) {
610 g_warning ("caching a task without a list ID -- this can "
611 "cause problems later");
614 if (!rtm_task_get_taskseries_id (task)) {
615 g_warning ("caching a task without a task series ID -- this "
616 "can cause problems later");
619 /* all but the name fields are already quoted or NULL */
620 statement = g_strdup_printf ("INSERT OR REPLACE INTO tasks "
621 "('local_id','task_id','name','priority','due_date',"
622 "'delete_date','complete_date','list_id',"
623 "'taskseries_id','local_changes') "
624 "VALUES (%s, %s, '%s', '%s', %s, %s, %s, %s, %s, %d)"
635 local_changes ? 1 : 0);
638 g_free (deleted_str);
639 g_free (completed_str);
641 if (sqlite3_exec (db, statement, NULL, NULL, &err)) {
642 g_warning ("failed to insert or update task in cache: %s\n",
643 sqlite3_errmsg (db));
654 cache_tasks_notify (MilkCache *cache,
658 MilkCachePrivate *priv;
661 g_return_if_fail (MILK_IS_CACHE (cache));
663 priv = MILK_CACHE_PRIVATE (cache);
665 /* FIXME: make the signals just emit all at once as a list, not
667 for (l = tasks; l; l = l->next) {
668 g_signal_emit (cache, signal_id, 0, l->data);
673 db_add_local_only_task (sqlite3 *db,
678 /* FIXME: cut this */
679 g_debug ("attempting to create new local-only task with name %s",
682 task = rtm_task_new ();
683 rtm_task_set_name (task, (char*) name);
685 if (!db_insert_or_update_local_task (db, task, "NULL", TRUE)) {
686 g_object_unref (task);
694 db_insert_or_update_task (sqlite3 *db,
696 gboolean local_changes,
697 gboolean *task_existed)
699 gboolean success = TRUE;
702 GTimeVal *due, *deleted, *completed;
705 sqlite3_stmt *query = NULL;
706 char *local_id = NULL;
707 char *local_id_formatted = NULL;
709 task_id = rtm_task_get_id (task);
710 g_return_val_if_fail (task_id, FALSE);
712 /* FIXME: should probably use a begin...commit block around this all */
714 /* FIXME: this needs a whole bucket of clean-up.
716 /* FIXME: make these all prepared statements */
718 statement = g_strdup_printf ("INSERT OR REPLACE INTO task_ids (task_id) VALUES ('%s');", task_id);
719 if (sqlite3_exec (db, statement, NULL, NULL, &err)) {
720 g_warning ("failed to insert task id %s in cache: %s\n",
721 task_id, sqlite3_errmsg (db));
728 /* try to find an existing task that we've already received from the
730 statement = g_strdup_printf ("SELECT local_id FROM tasks NATURAL JOIN task_ids WHERE task_id = '%s';", task_id);
732 if (sqlite3_prepare_v2 (db, statement, -1, &query, NULL)) {
733 g_warning ("failed to prepare statement: %s\n",
734 sqlite3_errmsg (db));
735 /* FIXME: use a goto instead, so we can carefully free any
736 * necessary memory */
740 while (status = sqlite3_step (query)) {
741 if (status == SQLITE_DONE) {
743 } else if (status != SQLITE_ROW) {
744 g_warning ("error stepping through SQL statement: %s",
745 sqlite3_errmsg (db));
746 /* FIXME: use a goto instead, so we can carefully free
747 * any necessary memory */
751 local_id = g_strdup (sqlite3_column_text (query, 0));
757 /* otherwise, try to find a matching task that we've added locally but
758 * haven't gotten confirmed by the server yet */
762 /* FIXME: cut this */
763 g_debug ("trying to update a local-only task");
765 name_str = sql_escape_quotes (rtm_task_get_name (task));
767 statement = g_strdup_printf ("SELECT local_id FROM tasks WHERE name = '%s';", name_str);
770 if (sqlite3_prepare_v2 (db, statement, -1, &query, NULL)) {
771 g_warning ("failed to prepare statement: %s\n",
772 sqlite3_errmsg (db));
773 /* FIXME: use a goto instead, so we can carefully free any
774 * necessary memory */
778 while (status = sqlite3_step (query)) {
779 if (status == SQLITE_DONE) {
781 } else if (status != SQLITE_ROW) {
782 g_warning ("error stepping through SQL statement: %s",
783 sqlite3_errmsg (db));
784 /* FIXME: use a goto instead, so we can carefully free
785 * any necessary memory */
789 local_id = g_strdup (sqlite3_column_text (query, 0));
797 *task_existed = (local_id != NULL);
799 /* FIXME: cut this */
800 g_debug ("got local_id: %s", local_id);
802 local_id_formatted = local_id ?
803 g_strdup_printf ("'%s'", local_id) :
807 /* FIXME: cut this */
808 g_debug ("formatted local_id:\n%s", local_id_formatted);
810 success &= db_insert_or_update_local_task (db, task,
811 local_id_formatted, local_changes);
813 g_free (local_id_formatted);
819 db_get_tasks_to_add_names (MilkCache *cache)
821 MilkCachePrivate *priv;
824 sqlite3_stmt *query = NULL;
828 priv = MILK_CACHE_PRIVATE (cache);
830 statement = g_strdup_printf ("SELECT name FROM tasks "
831 "WHERE task_id IS NULL;");
833 if (sqlite3_prepare_v2 (priv->db, statement, -1, &query, NULL)) {
834 g_warning ("failed to prepare statement: %s\n",
835 sqlite3_errmsg (priv->db));
836 goto db_get_tasks_to_add_names_ERROR;
839 while ((status = sqlite3_step (query))) {
840 if (status == SQLITE_DONE) {
842 } else if (status != SQLITE_ROW) {
843 g_warning ("error stepping through SQL statement: %s",
844 sqlite3_errmsg (priv->db));
845 goto db_get_tasks_to_add_names_ERROR;
848 names = g_list_prepend (names,
849 g_strdup (sqlite3_column_text (query, 0)));
852 goto db_get_tasks_to_add_names_OUT;
854 db_get_tasks_to_add_names_ERROR:
855 g_list_foreach (names, (GFunc) g_free, NULL);
859 db_get_tasks_to_add_names_OUT:
868 db_get_tasks_to_change (MilkCache *cache)
870 MilkCachePrivate *priv;
873 sqlite3_stmt *query = NULL;
877 priv = MILK_CACHE_PRIVATE (cache);
879 statement = g_strdup_printf ("SELECT task_id,name,due_date,list_id,"
880 "taskseries_id FROM tasks "
881 "WHERE local_changes=1;");
883 if (sqlite3_prepare_v2 (priv->db, statement, -1, &query, NULL)) {
884 g_warning ("failed to prepare statement: %s\n",
885 sqlite3_errmsg (priv->db));
886 goto db_get_tasks_to_change_ERROR;
889 while ((status = sqlite3_step (query))) {
893 if (status == SQLITE_DONE) {
895 } else if (status != SQLITE_ROW) {
896 g_warning ("error stepping through SQL statement: %s",
897 sqlite3_errmsg (priv->db));
898 goto db_get_tasks_to_change_ERROR;
901 task = rtm_task_new ();
902 rtm_task_set_id (task, (gchar*) sqlite3_column_text (query, 0));
903 rtm_task_set_name (task,
904 (gchar*) sqlite3_column_text (query, 1));
906 if (db_date_column_to_timeval (query, 2, &timeval))
907 rtm_task_set_due_date (task, &timeval);
909 rtm_task_set_list_id (task,
910 (gchar*) sqlite3_column_text (query, 3));
912 rtm_task_set_taskseries_id (task,
913 (gchar*) sqlite3_column_text (query, 4));
915 tasks = g_list_prepend (tasks, task);
918 goto db_get_tasks_to_change_OUT;
920 db_get_tasks_to_change_ERROR:
921 g_list_foreach (tasks, (GFunc) g_object_unref, NULL);
925 db_get_tasks_to_change_OUT:
934 cache_send_new_tasks (MilkCache *cache,
937 MilkCachePrivate *priv;
938 gboolean success = TRUE;
941 priv = MILK_CACHE_PRIVATE (cache);
943 /* FIXME: have a single function to get the sets of (new, changed,
944 * completed, deleted) tasks, then deal with each of them here */
945 names = db_get_tasks_to_add_names (cache);
947 /* FIXME: cut this */
948 g_debug ("trying to send %d new tasks", g_list_length (names));
950 /* FIXME: this entire block needs to be sequential as a whole but also
951 * async as a whole */
953 GList *tasks_added = NULL;
956 tasks_added = milk_auth_tasks_add (priv->auth, timeline, names);
958 for (l = tasks_added; l; l = l->next) {
959 /* FIXME: cut this */
960 g_debug (G_STRLOC ": trying to add task ID to "
961 "newly-inserted task: '%s' (%s);"
964 " taskseries ID: '%s', "
966 rtm_task_get_name (l->data),
967 rtm_task_get_id (l->data),
968 rtm_task_get_priority (l->data),
969 rtm_task_get_list_id (l->data),
970 rtm_task_get_taskseries_id (l->data)
973 /* mark these as having local changes so we'll send all
974 * there non-name attributes when we send the changes,
975 * in the next step */
976 db_insert_or_update_task (priv->db, l->data, TRUE,
980 /* not the most complete verification, but probably fine */
981 success &= (g_list_length (tasks_added) ==
982 g_list_length (names));
984 g_list_foreach (tasks_added, (GFunc) g_object_unref, NULL);
985 g_list_free (tasks_added);
988 g_list_foreach (names, (GFunc) g_free, NULL);
994 /* FIXME: cut this */
996 set_due_date (RtmTask *task,
997 const char *date_str)
999 GTimeVal timeval = {0};
1001 /* FIXME: cut this */
1002 g_debug ("going to decode date: '%s'", date_str);
1004 g_time_val_from_iso8601 (date_str, &timeval);
1006 rtm_task_set_due_date (task, &timeval);
1010 db_tasks_mark_as_synced (MilkCache *cache,
1013 MilkCachePrivate *priv;
1014 gboolean success = TRUE;
1015 GString *tasks_builder = NULL;
1018 gboolean first = TRUE;
1022 priv = MILK_CACHE_PRIVATE (cache);
1024 tasks_builder = g_string_new ("");
1025 for (l = tasks; l; l = l->next) {
1028 format = first ? "%s" : ",%s";
1030 g_string_append_printf (tasks_builder, format,
1031 rtm_task_get_id (l->data));
1034 task_ids = g_string_free (tasks_builder, FALSE);
1036 statement = g_strdup_printf ("UPDATE tasks "
1037 "SET local_changes=0 "
1038 "WHERE task_id IN (%s);",
1041 if (sqlite3_exec (priv->db, statement, NULL, NULL, &err)) {
1042 g_warning ("failed to acknowledge local changes were pushed "
1043 "to the server: %s\n",
1044 sqlite3_errmsg (priv->db));
1055 cache_send_changed_tasks (MilkCache *cache,
1058 MilkCachePrivate *priv;
1059 gboolean success = TRUE;
1060 GList *tasks_to_change;
1063 priv = MILK_CACHE_PRIVATE (cache);
1065 tasks_to_change = db_get_tasks_to_change (cache);
1067 if (!tasks_to_change)
1070 tasks_sent = milk_auth_tasks_send_changes (priv->auth,
1071 timeline, tasks_to_change);
1073 /* as above, if we miss any of these, the worst case is just resending
1074 * them later (vs. data loss or false caching) -- it's still not great
1075 * if you're on a flakey network, though */
1076 success &= (g_list_length (tasks_sent) ==
1077 g_list_length (tasks_to_change));
1079 success &= db_tasks_mark_as_synced (cache, tasks_sent);
1081 /* FIXME: cut this */
1082 g_debug ("successfully updated all the tasks: %d", success);
1084 g_list_foreach (tasks_to_change, (GFunc) g_object_unref, NULL);
1085 g_list_free (tasks_to_change);
1086 g_list_free (tasks_sent);
1091 /* FIXME: make this async */
1093 cache_send_changes (MilkCache *cache)
1095 MilkCachePrivate *priv;
1096 gboolean success = TRUE;
1099 priv = MILK_CACHE_PRIVATE (cache);
1101 /* FIXME: this makes the send_changes_hint fairly pointless, though we
1102 * may want to rename this function similarly */
1103 if (milk_auth_get_state (priv->auth) != MILK_AUTH_STATE_CONNECTED) {
1104 g_warning ("trying to send changes before auth has connected - we shouldn't be doing this (at least not until we've connected once)");
1109 /* FIXME: cut this */
1110 g_debug (G_STRLOC ": sending new (and updated tasks, once implemented) ");
1112 timeline = milk_auth_timeline_create (priv->auth, NULL);
1114 success &= cache_send_new_tasks (cache, timeline);
1115 success &= cache_send_changed_tasks (cache, timeline);
1117 /* FIXME: also get all the deleted tasks and delete them, and all the
1118 * completed tasks (complete_date IS NOT NULL && local_changes=1), and
1119 * apply those on the server */
1121 /* FIXME: cut this */
1122 g_debug ("looks like we successfully added the pending tasks");
1124 /* XXX: if we use the same timeline for creating these new tasks as
1125 * updating their attributes, we won't need to have this insane 2-step
1126 * process, right? (and thus updating the local_changes above shouldn't
1134 /* FIXME: make this async */
1136 cache_receive_changes (MilkCache *cache)
1138 MilkCachePrivate *priv = MILK_CACHE_PRIVATE (cache);
1141 GList *added = NULL, *changed = NULL, *finished = NULL;
1142 GTimeVal current_time;
1144 GError *error = NULL;
1146 if (milk_auth_get_state (priv->auth) != MILK_AUTH_STATE_CONNECTED) {
1150 g_get_current_time (¤t_time);
1151 new_sync = g_time_val_to_iso8601 (¤t_time);
1153 rtm_tasks = milk_auth_get_tasks (priv->auth, priv->last_sync, &error);
1155 g_error (G_STRLOC ": failed to retrieve latest tasks: %s",
1157 g_clear_error (&error);
1158 goto cache_receive_changes_ERROR;
1161 /* We don't wrap this in a begin...commit...rollback because it's better
1162 * to let the individual items get committed automatically. If one of
1163 * them fails, then we won't update the "last_sync" date. So the next
1164 * time we sync, we'll start from where we did last time, and so on,
1165 * until they all succeed at once. Any tasks that are already in the DB
1166 * will be "updated", whether their content actually changed or not
1168 for (l = rtm_tasks; l; l = g_list_delete_link (l, l)) {
1171 gboolean task_existed;
1173 task = RTM_TASK (l->data);
1175 if (!db_insert_or_update_task (priv->db, task, FALSE,
1177 goto cache_receive_changes_ERROR;
1179 /* FIXME: read the task back out of the DB for any sort
1180 * of normalization to happen transparently, and for
1181 * more commonality in the code reading out of the DB
1184 /* Task is deleted or completed */
1185 if (task_is_finished (task)) {
1186 finished = g_list_prepend (finished, task);
1188 /* Task has been changed */
1189 } else if (task_existed) {
1190 changed = g_list_prepend (changed, task);
1194 added = g_list_prepend (added, task);
1198 cache_tasks_notify (cache, added, signals[TASK_ADDED]);
1199 cache_tasks_notify (cache, changed, signals[TASK_CHANGED]);
1200 cache_tasks_notify (cache, finished, signals[TASK_FINISHED]);
1202 g_list_free (added);
1203 g_list_free (changed);
1204 g_list_free (finished);
1206 if (db_set_last_sync (priv->db, new_sync)) {
1207 g_free (priv->last_sync);
1208 priv->last_sync = new_sync;
1210 g_warning ("failed to set new sync timestamp to %d", new_sync);
1211 goto cache_receive_changes_ERROR;
1216 cache_receive_changes_ERROR:
1223 cache_send_receive_changes (MilkCache *cache)
1225 cache_send_changes (cache);
1226 cache_receive_changes (cache);
1232 restart_send_receive_poll (MilkCache *cache,
1233 gboolean poll_first)
1235 MilkCachePrivate *priv;
1237 priv = MILK_CACHE_PRIVATE (cache);
1239 if (priv->update_id)
1240 g_source_remove (priv->update_id);
1243 cache_send_receive_changes (cache);
1245 priv->update_id = g_timeout_add (CACHE_UPDATE_PERIOD,
1246 (GSourceFunc) cache_send_receive_changes, cache);
1249 task_is_finished (RtmTask *task)
1251 return (rtm_task_get_completed_date (task) ||
1252 rtm_task_get_deleted_date (task));
1256 auth_notify_cb (MilkAuth *auth,
1260 if (milk_auth_get_state (auth) == MILK_AUTH_STATE_CONNECTED) {
1261 cache_send_receive_changes (cache);
1266 milk_cache_set_auth (MilkCache *cache,
1269 MilkCachePrivate *priv;
1271 g_return_if_fail (MILK_IS_CACHE (cache));
1272 g_return_if_fail (MILK_IS_AUTH (auth));
1274 priv = MILK_CACHE_PRIVATE (cache);
1277 g_object_unref (priv->auth);
1279 priv->auth = g_object_ref (auth);
1281 restart_send_receive_poll (cache, FALSE);
1283 g_signal_emit (cache, signals[CLEARED], 0);
1285 g_signal_connect (priv->auth, "notify::state",
1286 G_CALLBACK (auth_notify_cb), cache);
1287 auth_notify_cb (priv->auth, NULL, cache);
1291 milk_cache_get_property (GObject *object,
1296 MilkCachePrivate *priv = MILK_CACHE_PRIVATE (object);
1298 switch (property_id)
1301 g_value_set_object (value, priv->auth);
1305 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id,
1311 milk_cache_set_property (GObject *object,
1313 const GValue *value,
1316 MilkCachePrivate *priv;
1319 cache = MILK_CACHE (object);
1320 priv = MILK_CACHE_PRIVATE (cache);
1322 switch (property_id)
1325 milk_cache_set_auth (cache, g_value_get_object (value));
1329 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id,
1335 milk_cache_get_active_tasks (MilkCache *cache)
1337 MilkCachePrivate *priv;
1340 g_return_val_if_fail (MILK_IS_CACHE (cache), NULL);
1342 priv = MILK_CACHE_PRIVATE (cache);
1344 tasks = db_get_tasks_active (priv->db);
1350 milk_cache_timeline_create (MilkCache *cache,
1353 MilkCachePrivate *priv;
1355 g_return_val_if_fail (MILK_IS_CACHE (cache), NULL);
1357 priv = MILK_CACHE_PRIVATE (cache);
1359 return milk_auth_timeline_create (priv->auth, error);
1362 /* FIXME: is the timeline argument even useful here? We should be able to just
1363 * assume it's being added right now, right? */
1365 /* FIXME: either fill in error appropriately or cut it as an argument, so the
1366 * caller doesn't assume it will be meaningful if we return NULL */
1368 milk_cache_task_add (MilkCache *cache,
1373 MilkCachePrivate *priv;
1376 g_return_val_if_fail (MILK_IS_CACHE (cache), NULL);
1378 priv = MILK_CACHE_PRIVATE (cache);
1380 task = db_add_local_only_task (priv->db, name);
1384 tasks = g_list_prepend (NULL, task);
1385 cache_tasks_notify (cache, tasks, signals[TASK_ADDED]);
1386 restart_send_receive_poll (cache, TRUE);
1388 g_list_free (tasks);
1395 milk_cache_task_complete (MilkCache *cache,
1400 MilkCachePrivate *priv;
1402 g_return_val_if_fail (MILK_IS_CACHE (cache), NULL);
1404 priv = MILK_CACHE_PRIVATE (cache);
1406 /* FIXME: mark the task as "to-complete" and set up the periodic task
1407 * that pushes the changes to the server; then immediately emit the
1408 * "task-finished" signal, so the model will immediately reflect that */
1410 return milk_auth_task_complete (priv->auth, timeline, task, error);
1414 milk_cache_task_delete (MilkCache *cache,
1419 MilkCachePrivate *priv;
1421 g_return_val_if_fail (MILK_IS_CACHE (cache), NULL);
1423 priv = MILK_CACHE_PRIVATE (cache);
1425 /* FIXME: mark the task as "to-delete" and set up the periodic task that
1426 * pushes the changes to the server; then immediately emit the
1427 * "task-finished" signal, so the model will immediately reflect that */
1429 return milk_auth_task_delete (priv->auth, timeline, task, error);
1432 /* XXX: this won't be necessary when the auth handles this transparently; or at
1433 * least this will merely be a signal to the auth that we're ready to
1434 * authenticate when it is */
1436 milk_cache_authenticate (MilkCache *cache)
1438 MilkCachePrivate *priv;
1440 g_return_if_fail (MILK_IS_CACHE (cache));
1442 priv = MILK_CACHE_PRIVATE (cache);
1444 milk_auth_log_in (priv->auth);
1448 milk_cache_dispose (GObject *object)
1450 MilkCachePrivate *priv = MILK_CACHE_PRIVATE (object);
1453 g_object_unref (priv->auth);
1457 if (priv->update_id) {
1458 g_source_remove (priv->update_id);
1459 priv->update_id = 0;
1464 milk_cache_finalize (GObject *object)
1466 MilkCachePrivate *priv = MILK_CACHE_PRIVATE (object);
1468 g_free (priv->last_sync);
1470 /* FIXME: should we do this at atexit() instead, for better safety? */
1471 sqlite3_close (priv->db);
1475 milk_cache_class_init (MilkCacheClass *klass)
1477 GObjectClass *object_class = G_OBJECT_CLASS (klass);
1479 g_type_class_add_private (klass, sizeof (MilkCachePrivate));
1481 object_class->get_property = milk_cache_get_property;
1482 object_class->set_property = milk_cache_set_property;
1483 object_class->dispose = milk_cache_dispose;
1484 object_class->finalize = milk_cache_finalize;
1486 g_object_class_install_property
1491 "Authentication proxy",
1492 "Remember The Milk authentication proxy.",
1494 G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY |
1495 G_PARAM_STATIC_STRINGS));
1497 signals[CLEARED] = g_signal_new
1499 MILK_TYPE_CACHE, G_SIGNAL_RUN_LAST, 0, NULL, NULL,
1500 g_cclosure_marshal_VOID__VOID, G_TYPE_NONE, 0);
1502 signals[TASK_ADDED] = g_signal_new
1504 MILK_TYPE_CACHE, G_SIGNAL_RUN_LAST, 0, NULL, NULL,
1505 g_cclosure_marshal_VOID__OBJECT, G_TYPE_NONE, 1,
1508 signals[TASK_FINISHED] = g_signal_new
1510 MILK_TYPE_CACHE, G_SIGNAL_RUN_LAST, 0, NULL, NULL,
1511 g_cclosure_marshal_VOID__OBJECT, G_TYPE_NONE, 1, RTM_TYPE_TASK);
1513 signals[TASK_CHANGED] = g_signal_new
1515 MILK_TYPE_CACHE, G_SIGNAL_RUN_LAST, 0, NULL, NULL,
1516 g_cclosure_marshal_VOID__OBJECT, G_TYPE_NONE, 1,
1521 milk_cache_init (MilkCache *self)
1523 MilkCachePrivate *priv;
1525 self->priv = priv = G_TYPE_INSTANCE_GET_PRIVATE (
1526 self, MILK_TYPE_CACHE, MilkCachePrivate);
1528 /* open the DB, creating a new one if necessary */
1529 priv->db = db_open ();
1530 db_update_schema (priv->db);
1531 priv->last_sync = db_get_last_sync (priv->db);
1535 milk_cache_get_default ()
1537 if (!default_cache) {
1538 default_cache = g_object_new (MILK_TYPE_CACHE,
1539 "auth", milk_auth_get_default (),
1543 return default_cache;