Add a basic task cache
authorTravis Reitter <treitter@gmail.com>
Sun, 28 Mar 2010 17:04:33 +0000 (10:04 -0700)
committerTravis Reitter <treitter@gmail.com>
Sun, 28 Mar 2010 17:04:33 +0000 (10:04 -0700)
configure.ac
src/Makefile.am
src/milk-auth.c
src/milk-auth.h
src/milk-cache.c [new file with mode: 0644]
src/milk-cache.h [new file with mode: 0644]
src/milk-main-window.c
src/milk-task-model.c
src/milk-task-model.h

index 37db629..c4bf0f7 100644 (file)
@@ -18,6 +18,7 @@ PKG_CHECK_MODULES([MILK], [
     libcurl
     json-glib-1.0
     rtm-glib
+    sqlite3
 ])
 AC_SUBST([MILK_CFLAGS])
 AC_SUBST([MILK_LIBS])
index 1bd0b39..1253a3c 100644 (file)
@@ -11,6 +11,8 @@ bin_PROGRAMS = milk
 milk_SOURCES = \
        milk-auth.c \
        milk-auth.h \
+       milk-cache.c \
+       milk-cache.h \
        milk-dialogs.c \
        milk-dialogs.h \
        milk-main.c \
index 96155c5..c36db6a 100644 (file)
@@ -216,6 +216,50 @@ milk_auth_timeline_create (MilkAuth  *auth,
         return rtm_glib_timelines_create (priv->rtm_glib, error);
 }
 
+/* FIXME: we probably really want this to be async (but sequencable) */
+GList*
+milk_auth_tasks_add (MilkAuth  *auth,
+                     char      *timeline,
+                     GList     *names)
+{
+        gboolean success = TRUE;
+        MilkAuthPrivate *priv;
+        GList *l;
+        GList *tasks = NULL;
+        GError *error = NULL;
+
+        g_return_val_if_fail (MILK_IS_AUTH (auth), NULL);
+        g_return_val_if_fail (names, NULL);
+
+        priv = MILK_AUTH_PRIVATE (auth);
+
+        for (l = names; l; l = l->next) {
+                RtmTask *task;
+
+                /* FIXME: cut this */
+                g_debug ("trying to send task with name '%s'", l->data);
+
+                /* XXX: this uses Smart Add parsing; make this user-settable? */
+                /* XXX: the cast to char* is actually a bug in the rtm-glib API
+                 */
+                task = rtm_glib_tasks_add (priv->rtm_glib, timeline,
+                                l->data, NULL, TRUE, &error);
+                if (task) {
+                        /* FIXME: cut this */
+                        g_debug (G_STRLOC ": added task with ID '%s'",
+                                        rtm_task_get_id (task));
+
+                        tasks = g_list_prepend (tasks, task);
+                } else {
+                        g_warning ("failed to add some tasks: %s",
+                                        error->message);
+                        g_clear_error (&error);
+                }
+        }
+
+        return tasks;
+}
+
 RtmTask*
 milk_auth_task_add (MilkAuth    *auth,
                     char        *timeline,
@@ -229,11 +273,94 @@ milk_auth_task_add (MilkAuth    *auth,
         priv = MILK_AUTH_PRIVATE (auth);
 
         /* XXX: this uses Smart Add parsing; make this user-settable? */
-        /* FIXME: the cast to char* is actually a bug in the rtm-glib API */
+        /* XXX: the cast to char* is actually a bug in the rtm-glib API */
         return rtm_glib_tasks_add (priv->rtm_glib, timeline, (char*) name, NULL,
                         TRUE, error);
 }
 
+/* FIXME: we probably really want this to be async (but sequencable) */
+GList*
+milk_auth_tasks_send_changes (MilkAuth *auth,
+                              char     *timeline,
+                              GList    *tasks)
+{
+        gboolean success = TRUE;
+        MilkAuthPrivate *priv;
+        GList *l;
+        GList *tasks_sent = NULL;
+        GError *error = NULL;
+
+        g_return_val_if_fail (MILK_IS_AUTH (auth), NULL);
+        g_return_val_if_fail (tasks, NULL);
+
+        priv = MILK_AUTH_PRIVATE (auth);
+
+        for (l = tasks; l; l = l->next) {
+                RtmTask *task = l->data;
+
+                GTimeVal *tv;
+
+                /* If any of these conditions fail, libsoup ends up exploding
+                 * in a segfault (blindly de-reffing NULL); better to be safe
+                 * than sorry */
+                if (!rtm_task_get_list_id (task)) {
+                        g_warning (G_STRLOC ": task doesn't have a list ID; "
+                                        "skipping...");
+                        continue;
+                }
+
+                if (!rtm_task_get_taskseries_id (task)) {
+                        g_warning (G_STRLOC ": task doesn't have a taskseries "
+                                        "ID; skipping...");
+                        continue;
+                }
+
+                if (!rtm_task_get_id (task)) {
+                        g_warning (G_STRLOC ": task doesn't have an ID; "
+                                        "skipping...");
+                        continue;
+                }
+
+                tv = rtm_task_get_due_date (task);
+                if (tv) {
+                        char *due_str;
+                        char *tid;
+
+                        due_str = g_time_val_to_iso8601 (tv);
+
+                        /* FIXME: cut this */
+                        g_debug ("going to set due string: '%s'", due_str);
+
+                        /* XXX: this uses Smart Add parsing; make this
+                         * user-settable? */
+                        /* XXX: the cast to char* is actually a bug in the
+                         * rtm-glib API */
+                        tid = rtm_glib_tasks_set_due_date (priv->rtm_glib, timeline,
+                                        task, due_str,
+                                        /* FIXME: set this appropriately */
+                                        FALSE,
+                                        FALSE, &error);
+
+                        if (tid) {
+                                /* FIXME: this should be a set, not a list --
+                                 * we'll add each task to the set if it's been
+                                 * changed since the last send */
+                                tasks_sent = g_list_prepend (tasks_sent, task);
+                        } else {
+                                g_warning ("failed to add some tasks: %s",
+                                                error->message);
+                                g_clear_error (&error);
+                        }
+
+                        g_free (tid);
+                }
+
+                /* FIXME: handle all the other attributes, not just the date */
+        }
+
+        return tasks_sent;
+}
+
 char*
 milk_auth_task_complete (MilkAuth  *auth,
                          char      *timeline,
@@ -264,6 +391,12 @@ milk_auth_task_delete (MilkAuth  *auth,
         return rtm_glib_tasks_delete (priv->rtm_glib, timeline, task, error);
 }
 
+/* FIXME: why does this (or something above it) totally fail if we don't have a
+ * working Internet connection / resolv.conf is mangled? */
+/* FIXME: instead of this manual call, listen to the connection manager
+ * transitions -- see this:
+ * http://wiki.maemo.org/Documentation/Maemo_5_Developer_Guide/Using_Connectivity_Components/Maemo_Connectivity#Libconic_Usage
+ */
 void
 milk_auth_log_in (MilkAuth *auth)
 {
index 28faf3e..5e98254 100644 (file)
@@ -81,6 +81,12 @@ RtmTask*      milk_auth_task_add        (MilkAuth    *auth,
                                          char        *timeline,
                                          const char  *name,
                                          GError     **error);
+GList*        milk_auth_tasks_add       (MilkAuth    *auth,
+                                         char        *timeline,
+                                         GList       *names);
+GList*        milk_auth_tasks_send_changes (MilkAuth  *auth,
+                                            char      *timeline,
+                                            GList     *tasks);
 char*         milk_auth_task_complete   (MilkAuth    *auth,
                                          char        *timeline,
                                          RtmTask     *task,
diff --git a/src/milk-cache.c b/src/milk-cache.c
new file mode 100644 (file)
index 0000000..af0c7cb
--- /dev/null
@@ -0,0 +1,1534 @@
+/*
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation; either version 2 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public
+ * License along with this program; if not, write to the
+ * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
+ * Boston, MA  02110-1301  USA
+ *
+ * Authors: Travis Reitter <treitter@gmail.com>
+ */
+
+#include <config.h>
+
+#include <stdlib.h>
+#include <errno.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <sqlite3.h>
+#include <glib.h>
+#include <glib/gi18n.h>
+#include <gtk/gtk.h>
+#include <hildon/hildon.h>
+#include <rtm-glib/rtm-error.h>
+#include <rtm-glib/rtm-glib.h>
+
+#include "milk-cache.h"
+#include "milk-auth.h"
+#include "milk-dialogs.h"
+
+G_DEFINE_TYPE (MilkCache, milk_cache, G_TYPE_OBJECT);
+
+/* less expensive than G_TYPE_INSTANCE_GET_PRIVATE */
+#define MILK_CACHE_PRIVATE(o) ((MILK_CACHE ((o)))->priv)
+
+#define RTM_API_KEY "81f5c6c904aeafbbc914d9845d250ea8"
+#define RTM_SHARED_SECRET "b08b15419378f913"
+
+/* FIXME: centralize this generic data dir */
+#define DATA_DIR ".milk"
+#define DB_BASENAME "tasks.db"
+
+/* FIXME: make this configurable at runtime, pref. as a gconf value */
+/* time between syncing with the server, in ms */
+#define CACHE_UPDATE_PERIOD 5000
+
+struct _MilkCachePrivate
+{
+        MilkAuth *auth;
+        guint update_id;
+        char *last_sync;
+        sqlite3 *db;
+};
+
+enum {
+        PROP_AUTH = 1,
+};
+
+enum {
+        CLEARED,
+        TASK_ADDED,
+        TASK_CHANGED,
+        TASK_FINISHED,
+        LAST_SIGNAL
+};
+
+static guint signals[LAST_SIGNAL];
+
+static MilkCache *default_cache = NULL;
+
+static const char*
+get_data_dir ()
+{
+        static char *filename = NULL;
+
+        if (!filename)
+                filename = g_build_filename (g_get_home_dir (), DATA_DIR, NULL);
+
+        return filename;
+}
+
+static const char*
+get_db_filename ()
+{
+        static char *filename = NULL;
+
+        if (!filename)
+                filename = g_build_filename (get_data_dir (), DB_BASENAME,
+                                NULL);
+
+        return filename;
+}
+
+static gint
+get_schema_version (sqlite3 *db)
+{
+        gint version = -1;
+        gint status;
+        sqlite3_stmt *query = NULL;
+
+        if (sqlite3_prepare_v2 (db, "PRAGMA user_version;", -1, &query, NULL)) {
+                g_warning ("failed to prepare statement: %s\n",
+                                sqlite3_errmsg (db));
+                goto get_schema_version_OUT;
+        }
+
+        while (status = sqlite3_step (query)) {
+                if (status == SQLITE_DONE) {
+                        break;
+                } else if (status != SQLITE_ROW) {
+                        g_warning ("error stepping through SQL statement: %s",
+                                        sqlite3_errmsg (db));
+                        goto get_schema_version_OUT;
+                }
+
+                version = sqlite3_column_int (query, 0);
+        }
+
+get_schema_version_OUT:
+        sqlite3_finalize (query);
+
+        return version;
+}
+
+static void
+db_transaction_begin (sqlite3 *db)
+{
+        char *err = NULL;
+
+        if (sqlite3_exec (db, "BEGIN;", NULL, NULL, &err)) {
+                g_error ("failed to begin transaction: %s\n",
+                                sqlite3_errmsg (db));
+        }
+        sqlite3_free (err);
+}
+
+static void
+db_transaction_commit (sqlite3 *db)
+{
+        char *err = NULL;
+
+        if (sqlite3_exec (db, "COMMIT;", NULL, NULL, &err)) {
+                g_error ("failed to commit transaction: %s\n",
+                                sqlite3_errmsg (db));
+        }
+        sqlite3_free (err);
+}
+
+static void
+db_transaction_rollback (sqlite3 *db)
+{
+        char *err = NULL;
+
+        if (sqlite3_exec (db, "ROLLBACK;", NULL, NULL, &err)) {
+                g_error ("failed to rollback transaction: %s\n",
+                                sqlite3_errmsg (db));
+        }
+        sqlite3_free (err);
+}
+
+static gboolean
+db_set_schema_version (sqlite3 *db,
+                       guint    version)
+{
+        gboolean success = TRUE;
+        char *statement;
+        char *err = NULL;
+
+        statement = g_strdup_printf ("PRAGMA user_version = %d;", version);
+
+        if (sqlite3_exec (db, statement, NULL, NULL, &err)) {
+                g_warning ("failed to update schema version: %s\n",
+                                sqlite3_errmsg (db));
+                success = FALSE;
+        }
+
+        g_free (statement);
+        sqlite3_free (err);
+
+        return success;
+}
+
+static char*
+db_get_key_value (sqlite3    *db,
+                  const char *key)
+{
+        gint status;
+        sqlite3_stmt *query = NULL;
+        char *statement;
+        char *value = NULL;
+
+        statement = g_strdup_printf ("SELECT value FROM key_values "
+                        "WHERE key = '%s';", key);
+
+        if (sqlite3_prepare_v2 (db, statement, -1, &query, NULL)) {
+                g_warning ("failed to prepare statement: %s\n",
+                                sqlite3_errmsg (db));
+                goto get_schema_version_OUT;
+        }
+
+        while (status = sqlite3_step (query)) {
+                if (status == SQLITE_DONE) {
+                        break;
+                } else if (status != SQLITE_ROW) {
+                        g_warning ("error stepping through SQL statement: %s",
+                                        sqlite3_errmsg (db));
+                        goto get_schema_version_OUT;
+                }
+
+                value = g_strdup (sqlite3_column_text (query, 0));
+        }
+
+get_schema_version_OUT:
+        sqlite3_finalize (query);
+        g_free (statement);
+
+        return value;
+}
+
+static gboolean
+db_date_column_to_timeval (sqlite3_stmt *query,
+                           guint         colnum,
+                           GTimeVal     *timeval)
+{
+        const gchar *date_str;
+
+        date_str = sqlite3_column_text (query, colnum);
+
+        if (date_str && date_str[0] != '\0' && !g_strcmp0 (date_str, "(null)"))
+                return g_time_val_from_iso8601 (date_str, timeval);
+
+        return FALSE;
+}
+
+static GList*
+db_get_tasks_active (sqlite3 *db)
+{
+        gint status;
+        sqlite3_stmt *query = NULL;
+        char *statement;
+        GList *tasks = NULL;
+
+        statement = g_strdup_printf ("SELECT "
+                        "task_id, name, due_date FROM tasks "
+                        "WHERE "
+                                "delete_date IS NULL AND "
+                                "complete_date IS NULL"
+                        ";");
+
+        if (sqlite3_prepare_v2 (db, statement, -1, &query, NULL)) {
+                g_warning ("failed to prepare statement: %s\n",
+                                sqlite3_errmsg (db));
+                goto get_schema_version_OUT;
+        }
+
+        while (status = sqlite3_step (query)) {
+                RtmTask *task;
+                const char *due;
+                GTimeVal timeval;
+
+                if (status == SQLITE_DONE) {
+                        break;
+                } else if (status != SQLITE_ROW) {
+                        g_warning ("error stepping through SQL statement: %s",
+                                        sqlite3_errmsg (db));
+                        goto get_schema_version_OUT;
+                }
+
+                task = rtm_task_new ();
+
+                rtm_task_set_id   (task, (char*)sqlite3_column_text (query, 0));
+                rtm_task_set_name (task, (char*)sqlite3_column_text (query, 1));
+
+                if (db_date_column_to_timeval (query, 2, &timeval))
+                        rtm_task_set_due_date (task, &timeval);
+
+                tasks = g_list_prepend (tasks, task);
+        }
+
+get_schema_version_OUT:
+        sqlite3_finalize (query);
+        g_free (statement);
+
+        return tasks;
+}
+
+static char*
+db_get_last_sync (sqlite3 *db)
+{
+        return db_get_key_value (db, "last_sync");
+}
+
+static gboolean
+db_set_key_value (sqlite3    *db,
+                  const char *key,
+                  const char *value)
+{
+        gboolean success = TRUE;
+        char *statement;
+        char *err = NULL;
+
+        /* FIXME: probably safer to create them per-schema update and then fail
+         * if they don't exist */
+        statement = g_strdup_printf ("INSERT OR REPLACE INTO key_values "
+                        "('key', 'value') VALUES ('%s', '%s');", key, value);
+
+        if (sqlite3_exec (db, statement, NULL, NULL, &err)) {
+                g_warning ("failed to update schema version: %s\n",
+                                sqlite3_errmsg (db));
+                success = FALSE;
+        }
+
+        g_free (statement);
+        sqlite3_free (err);
+
+        return success;
+}
+
+static gboolean
+db_set_last_sync (sqlite3    *db,
+                  const char *last_sync)
+{
+        return db_set_key_value (db, "last_sync", last_sync);
+}
+
+static gboolean
+db_update_schema_0_to_1 (sqlite3 *db)
+{
+        sqlite3_stmt *query = NULL;
+        char *err = NULL;
+
+        db_transaction_begin (db);
+
+        /* FIXME: actually create the required triggers */
+
+        /* FIXME: ugh... sqlite supports foreign keys, but don't actualyl
+         * /enforce them/ (thanks, guys...); here's a way to enforce them using
+         * triggers:
+         *
+         * http://www.justatheory.com/computers/databases/sqlite/
+         *
+         * it seems to be fixed in sqlite 3.6.19, but even Karmic has 3.6.16
+         * (and Fremantle has 3.6.14)
+         * */
+
+        if (sqlite3_exec (db, "CREATE TABLE task_ids "
+                        "(task_id TEXT PRIMARY KEY);", NULL, NULL, &err)) {
+                g_warning ("failed to create tasks table: %s\n",
+                                sqlite3_errmsg (db));
+                goto db_update_schema_0_to_1_ERROR;
+        }
+        sqlite3_free (err);
+
+        /* XXX: there is a subtle race here where we can add a task on the
+         * server but disconnect before we get a confirmation, and change the
+         * name on the server before we recieve it. Then we'll end up
+         * re-submitting this task with its original name again (until we don't
+         * hit this condition again). The risk is fairly low, but it's still
+         * there */
+
+        /* insert a special task ID (NULL) to designate that we haven't
+         * gotten a confirmation for adding the task yet (and thus haven't
+         * gotten its server-side ID) */
+        if (sqlite3_exec (db, "INSERT INTO task_ids "
+                        "(task_id) values (NULL);", NULL, NULL, &err)) {
+                g_warning ("failed to insert special 'unset' value: %s\n",
+                                sqlite3_errmsg (db));
+                goto db_update_schema_0_to_1_ERROR;
+        }
+        sqlite3_free (err);
+
+        if (sqlite3_exec (db, "CREATE TABLE tasks ("
+                        "local_id INTEGER PRIMARY KEY NOT NULL,"
+                        "task_id"
+                        "       CONSTRAINT fk_task_id "
+                        "               REFERENCES task_ids(task_id) "
+                        "       ON DELETE CASCADE,"
+                        "name TEXT NOT NULL,"
+                        "local_changes BOOLEAN DEFAULT 0,"
+                        "due_date TEXT,"
+                        "delete_date TEXT,"
+                        "complete_date TEXT,"
+                        "list_id TEXT,"
+                        "taskseries_id TEXT"
+                        ");", NULL, NULL, &err)) {
+                g_warning ("failed to create tasks table: %s\n",
+                                sqlite3_errmsg (db));
+                goto db_update_schema_0_to_1_ERROR;
+        }
+        sqlite3_free (err);
+
+        if (sqlite3_exec (db, "CREATE TABLE key_values ("
+                        "key TEXT PRIMARY KEY NOT NULL,"
+                        "value TEXT"
+                        ");", NULL, NULL, &err)) {
+                g_warning ("failed to create key_values table: %s\n",
+                                sqlite3_errmsg (db));
+                goto db_update_schema_0_to_1_ERROR;
+        }
+        sqlite3_free (err);
+
+        if (!db_set_last_sync (db, "0"))
+                goto db_update_schema_0_to_1_ERROR;
+
+        if (!db_set_schema_version (db, 1))
+                goto db_update_schema_0_to_1_ERROR;
+
+        db_transaction_commit (db);
+
+        return TRUE;
+
+db_update_schema_0_to_1_ERROR:
+        db_transaction_rollback (db);
+        sqlite3_finalize (query);
+
+        return FALSE;
+}
+
+static gboolean
+db_update_schema (sqlite3 *db)
+{
+        gboolean (*update_funcs[]) (sqlite3 *)= {
+                db_update_schema_0_to_1,
+        };
+        gint i;
+        gint schema_version = -1;
+        gint latest_version = G_N_ELEMENTS (update_funcs);
+        gint status;
+
+        schema_version = get_schema_version (db);
+
+        if (schema_version > latest_version) {
+                g_error ("your database is newer than this version of the "
+                        "app knows how to deal with -- bailing...");
+        } else if (schema_version == latest_version) {
+                return TRUE;
+        }
+
+        for (i = schema_version; i < latest_version; i++) {
+                if (!update_funcs[i] (db)) {
+                        g_error ("error upgrading from schema version %d to %d",
+                                        i, i+1);
+                        /* FIXME: probably better to just wipe the cache and
+                         * start over, rather than crash (which would probably
+                         * prevent us from ever automatically recovering) */
+                }
+        }
+
+        return TRUE;
+}
+
+static sqlite3*
+db_open ()
+{
+        sqlite3 *db = NULL;
+        gint status;
+
+        if (!g_file_test (get_data_dir (), G_FILE_TEST_EXISTS)) {
+
+        }
+        if (g_mkdir_with_parents (get_data_dir (), S_IRWXU | S_IRWXG)) {
+
+                g_error ("Can't create the data dir %s: %s; giving up...",
+                                get_data_dir (), strerror (errno));
+        }
+
+        status = sqlite3_open (get_db_filename (), &db);
+        if (status){
+                GFile *file;
+                GError *error = NULL;
+
+                g_warning ("Can't open database: %s; deleting it...\n",
+                                sqlite3_errmsg (db));
+                sqlite3_close (db);
+                db = NULL;
+
+                /* FIXME: open a banner warning that any pending tasks may have
+                 * been lost (but actually just move the file as a backup file)
+                 */
+                file = g_file_new_for_path (get_db_filename ());
+                if (!g_file_delete (file, NULL, &error)) {
+                        g_error ("Could not delete the broken database: %s; "
+                                        "giving up...", error->message);
+
+                        g_clear_error (&error);
+                        exit (1);
+                }
+        }
+
+        if (!db) {
+                status = sqlite3_open (get_db_filename (), &db);
+                if (status){
+                        sqlite3_close (db);
+                        exit (1);
+                }
+        }
+
+        return db;
+}
+
+static char*
+time_val_to_iso8601 (GTimeVal *val)
+{
+        return val ?
+                g_strdup_printf ("'%s'", g_time_val_to_iso8601 (val)) :
+                g_strdup ("NULL");
+}
+
+static char*
+sql_escape_quotes (const char *str)
+{
+        char **tokens;
+        char *final_str;
+
+        if (!str)
+                return NULL;
+
+        /* escape all single quotes as double quotes (as is normal in SQL) */
+        tokens = g_strsplit (str, "'", -1);
+        final_str = g_strjoinv("''", tokens);
+        g_strfreev (tokens);
+
+        return final_str;
+}
+
+static gboolean
+db_insert_or_update_local_task (sqlite3    *db,
+                                RtmTask    *task,
+                                const char *local_id,
+                                gboolean    local_changes)
+{
+        gboolean success = TRUE;
+        char *statement;
+        char *err = NULL;
+        GTimeVal *due, *deleted, *completed;
+        char *name_str, *due_str, *deleted_str, *completed_str;
+        const char *task_id;
+        const char *list_id;
+        const char *taskseries_id;
+        gint status;
+
+        name_str = sql_escape_quotes (rtm_task_get_name (task));
+        due = rtm_task_get_due_date (task);
+        deleted = rtm_task_get_deleted_date (task);
+        completed = rtm_task_get_completed_date (task);
+        due_str = time_val_to_iso8601 (due);
+        deleted_str = time_val_to_iso8601 (deleted);
+        completed_str = time_val_to_iso8601 (completed);
+
+        /* FIXME: it doesn't actually have to be this complicated; see the
+         * actual code above and below  */
+        /* FIXME:
+         * 1. add a new internal ID ('local_id') that's unique in the table
+         * 'tasks'.
+         * 2. Add another 1-column table ('task_ids') with PRIMARY KEY column
+         * 'task_id'
+         * 3. prep-populate tasks_ids with a single special value (NULL, if
+         * possible). This will be used to designate "not yet written back to
+         * server"
+         * 4. make tasks.task_id have a foreign key constraint on
+         * task_ids.task_id
+         * 5. when inserting new tasks (ie, before they've been written back to
+         * the server), set their tasks.task_id to NULL (or whatever special
+         * value we're using)
+         * 6. when we recieve tasks, check to see if we get a match for SELECT
+         * local_id from tasks WHERE
+         * tasks.task_id = rtm_task_get_id (task) AND tasks.name = name_str;
+         *      6.a. if we have a match, UPDATE tasks SET task_id, name, ...
+         *      WHERE local_id = <local_id match>
+         *      6.b. if we didn't have a match, INSERT INTO tasks (task_id,
+         *      name, ...) VALUES (rtm_task_get_id (task), ...);
+         * 7. these "pending" tasks can be treated normally when being
+         * manipulated locally
+         * 8. any task changed locally needs to have its 'local_changes' field
+         * set to TRUE
+         */
+
+
+        /* if we insert NULL for the local_id, it will nicely replace it with
+         * the next automatic value. This way we can use a single statement to
+         * update the other fields if the task already exists or create the task
+         * (with a new local_id) if necessary */
+
+        task_id = rtm_task_get_id (task);
+        task_id = task_id ? task_id : "NULL";
+
+        list_id = rtm_task_get_list_id (task);
+        list_id = list_id ? list_id : "NULL";
+
+        taskseries_id = rtm_task_get_taskseries_id (task);
+        taskseries_id = taskseries_id ? taskseries_id : "NULL";
+
+        /* FIXME: cut this? */
+        if (!rtm_task_get_list_id (task)) {
+                g_warning ("caching a task without a list ID -- this can "
+                                "cause problems later");
+        }
+
+        if (!rtm_task_get_taskseries_id (task)) {
+                g_warning ("caching a task without a task series ID -- this "
+                                "can cause problems later");
+        }
+
+        /* all but the name fields are already quoted or NULL */
+        statement = g_strdup_printf ("INSERT OR REPLACE INTO tasks "
+                        "('local_id','task_id','name','due_date','delete_date',"
+                        "'complete_date','list_id','taskseries_id',"
+                        "'local_changes') "
+                        "VALUES (%s, %s, '%s', %s, %s, %s, %s, %s, %d)"
+                        ";",
+                        local_id,
+                        task_id,
+                        name_str,
+                        due_str,
+                        deleted_str,
+                        completed_str,
+                        list_id,
+                        taskseries_id,
+                        local_changes ? 1 : 0);
+        g_free (name_str);
+        g_free (due_str);
+        g_free (deleted_str);
+        g_free (completed_str);
+
+        if (sqlite3_exec (db, statement, NULL, NULL, &err)) {
+                g_warning ("failed to insert or update task in cache: %s\n",
+                                sqlite3_errmsg (db));
+                success = FALSE;
+        }
+
+        g_free (statement);
+        sqlite3_free (err);
+
+        return success;
+}
+
+static void
+cache_tasks_notify (MilkCache *cache,
+                    GList     *tasks,
+                    guint      signal_id)
+{
+        MilkCachePrivate *priv;
+        GList *l;
+
+        g_return_if_fail (MILK_IS_CACHE (cache));
+
+        priv = MILK_CACHE_PRIVATE (cache);
+
+        /* FIXME: make the signals just emit all at once as a list, not
+         * individually */
+        for (l = tasks; l; l = l->next) {
+                g_signal_emit (cache, signal_id, 0, l->data);
+        }
+}
+
+static RtmTask*
+db_add_local_only_task (sqlite3    *db,
+                        const char *name)
+{
+        RtmTask *task;
+
+        /* FIXME: cut this */
+        g_debug ("attempting to create new local-only task with name %s",
+                        name);
+
+        task = rtm_task_new ();
+        rtm_task_set_name (task, (char*) name);
+
+        if (!db_insert_or_update_local_task (db, task, "NULL", TRUE)) {
+                g_object_unref (task);
+                task = NULL;
+        }
+
+        return task;
+}
+
+static gboolean
+db_insert_or_update_task (sqlite3  *db,
+                          RtmTask  *task,
+                          gboolean  local_changes,
+                          gboolean *task_existed)
+{
+        gboolean success = TRUE;
+        char *statement;
+        char *err = NULL;
+        GTimeVal *due, *deleted, *completed;
+        const char *task_id;
+        gint status;
+        sqlite3_stmt *query = NULL;
+        char *local_id = NULL;
+        char *local_id_formatted = NULL;
+
+        task_id = rtm_task_get_id (task);
+        g_return_val_if_fail (task_id, FALSE);
+
+        /* FIXME: should probably use a begin...commit block around this all */
+
+        /* FIXME: this needs a whole bucket of clean-up.
+         */
+        /* FIXME: make these all prepared statements */
+
+        statement = g_strdup_printf ("INSERT OR REPLACE INTO task_ids (task_id) VALUES ('%s');", task_id);
+        if (sqlite3_exec (db, statement, NULL, NULL, &err)) {
+                g_warning ("failed to insert task id %s in cache: %s\n",
+                                task_id, sqlite3_errmsg (db));
+                success = FALSE;
+        }
+
+        g_free (statement);
+        sqlite3_free (err);
+
+        /* try to find an existing task that we've already received from the
+         * server */
+        statement = g_strdup_printf ("SELECT local_id FROM tasks NATURAL JOIN task_ids WHERE task_id = '%s';", task_id);
+
+        if (sqlite3_prepare_v2 (db, statement, -1, &query, NULL)) {
+                g_warning ("failed to prepare statement: %s\n",
+                                sqlite3_errmsg (db));
+                /* FIXME: use a goto instead, so we can carefully free any
+                 * necessary memory */
+                return FALSE;
+        }
+
+        while (status = sqlite3_step (query)) {
+                if (status == SQLITE_DONE) {
+                        break;
+                } else if (status != SQLITE_ROW) {
+                        g_warning ("error stepping through SQL statement: %s",
+                                        sqlite3_errmsg (db));
+                        /* FIXME: use a goto instead, so we can carefully free
+                         * any necessary memory */
+                        return FALSE;
+                }
+
+                local_id = g_strdup (sqlite3_column_text (query, 0));
+        }
+
+        g_free (statement);
+        sqlite3_free (err);
+
+        /* otherwise, try to find a matching task that we've added locally but
+         * haven't gotten confirmed by the server yet */
+        if (!local_id) {
+                char *name_str;
+
+                /* FIXME: cut this */
+                g_debug ("trying to update a local-only task");
+
+                name_str = sql_escape_quotes (rtm_task_get_name (task));
+
+                statement = g_strdup_printf ("SELECT local_id FROM tasks WHERE name = '%s';", name_str);
+                g_free (name_str);
+
+                if (sqlite3_prepare_v2 (db, statement, -1, &query, NULL)) {
+                        g_warning ("failed to prepare statement: %s\n",
+                                        sqlite3_errmsg (db));
+                        /* FIXME: use a goto instead, so we can carefully free any
+                        * necessary memory */
+                        return FALSE;
+                }
+
+                while (status = sqlite3_step (query)) {
+                        if (status == SQLITE_DONE) {
+                                break;
+                        } else if (status != SQLITE_ROW) {
+                                g_warning ("error stepping through SQL statement: %s",
+                                                sqlite3_errmsg (db));
+                                /* FIXME: use a goto instead, so we can carefully free
+                                * any necessary memory */
+                                return FALSE;
+                        }
+
+                        local_id = g_strdup (sqlite3_column_text (query, 0));
+                }
+
+                g_free (statement);
+                sqlite3_free (err);
+        }
+
+        if (task_existed)
+                *task_existed = (local_id != NULL);
+
+        /* FIXME: cut this */
+        g_debug ("got local_id: %s", local_id);
+
+        local_id_formatted = local_id ?
+                g_strdup_printf ("'%s'", local_id) :
+                g_strdup ("NULL");
+        g_free (local_id);
+
+        /* FIXME: cut this */
+        g_debug ("formatted local_id:\n%s", local_id_formatted);
+
+        success &= db_insert_or_update_local_task (db, task,
+                        local_id_formatted, local_changes);
+        g_free (local_id_formatted);
+
+        return success;
+}
+
+static GList*
+db_get_tasks_to_add_names (MilkCache *cache)
+{
+        MilkCachePrivate *priv;
+        char *statement;
+        char *err = NULL;
+        sqlite3_stmt *query = NULL;
+        GList *names = NULL;
+        gint status;
+
+        priv = MILK_CACHE_PRIVATE (cache);
+
+        statement = g_strdup_printf ("SELECT name FROM tasks "
+                        "WHERE task_id IS NULL;");
+
+        if (sqlite3_prepare_v2 (priv->db, statement, -1, &query, NULL)) {
+                g_warning ("failed to prepare statement: %s\n",
+                                sqlite3_errmsg (priv->db));
+                goto db_get_tasks_to_add_names_ERROR;
+        }
+
+        while ((status = sqlite3_step (query))) {
+                if (status == SQLITE_DONE) {
+                        break;
+                } else if (status != SQLITE_ROW) {
+                        g_warning ("error stepping through SQL statement: %s",
+                                        sqlite3_errmsg (priv->db));
+                        goto db_get_tasks_to_add_names_ERROR;
+                }
+
+                names = g_list_prepend (names,
+                                g_strdup (sqlite3_column_text (query, 0)));
+        }
+
+        goto db_get_tasks_to_add_names_OUT;
+
+db_get_tasks_to_add_names_ERROR:
+        g_list_foreach (names, (GFunc) g_free, NULL);
+        g_list_free (names);
+        names = NULL;
+
+db_get_tasks_to_add_names_OUT:
+
+        g_free (statement);
+        sqlite3_free (err);
+
+        return names;
+}
+
+static GList*
+db_get_tasks_to_change (MilkCache *cache)
+{
+        MilkCachePrivate *priv;
+        char *statement;
+        char *err = NULL;
+        sqlite3_stmt *query = NULL;
+        GList *tasks = NULL;
+        gint status;
+
+        priv = MILK_CACHE_PRIVATE (cache);
+
+        statement = g_strdup_printf ("SELECT task_id,name,due_date,list_id,"
+                        "taskseries_id FROM tasks "
+                        "WHERE local_changes=1;");
+
+        if (sqlite3_prepare_v2 (priv->db, statement, -1, &query, NULL)) {
+                g_warning ("failed to prepare statement: %s\n",
+                                sqlite3_errmsg (priv->db));
+                goto db_get_tasks_to_change_ERROR;
+        }
+
+        while ((status = sqlite3_step (query))) {
+                RtmTask *task;
+                GTimeVal timeval;
+
+                if (status == SQLITE_DONE) {
+                        break;
+                } else if (status != SQLITE_ROW) {
+                        g_warning ("error stepping through SQL statement: %s",
+                                        sqlite3_errmsg (priv->db));
+                        goto db_get_tasks_to_change_ERROR;
+                }
+
+                task = rtm_task_new ();
+                rtm_task_set_id (task, (gchar*) sqlite3_column_text (query, 0));
+                rtm_task_set_name (task,
+                                (gchar*) sqlite3_column_text (query, 1));
+
+                if (db_date_column_to_timeval (query, 2, &timeval))
+                        rtm_task_set_due_date (task, &timeval);
+
+                rtm_task_set_list_id (task,
+                                (gchar*) sqlite3_column_text (query, 3));
+
+                rtm_task_set_taskseries_id (task,
+                                (gchar*) sqlite3_column_text (query, 4));
+
+                tasks = g_list_prepend (tasks, task);
+        }
+
+        goto db_get_tasks_to_change_OUT;
+
+db_get_tasks_to_change_ERROR:
+        g_list_foreach (tasks, (GFunc) g_object_unref, NULL);
+        g_list_free (tasks);
+        tasks = NULL;
+
+db_get_tasks_to_change_OUT:
+
+        g_free (statement);
+        sqlite3_free (err);
+
+        return tasks;
+}
+
+static gboolean
+cache_send_new_tasks (MilkCache *cache,
+                      char      *timeline)
+{
+        MilkCachePrivate *priv;
+        gboolean success = TRUE;
+        GList *names = NULL;
+
+        priv = MILK_CACHE_PRIVATE (cache);
+
+        /* FIXME: have a single function to get the sets of (new, changed,
+         * completed, deleted) tasks, then deal with each of them here */
+        names = db_get_tasks_to_add_names (cache);
+
+        /* FIXME: cut this */
+        g_debug ("trying to send %d new tasks", g_list_length (names));
+
+        /* FIXME: this entire block needs to be sequential as a whole but also
+         * async as a whole */
+        if (names) {
+                GList *tasks_added = NULL;
+                GList *l;
+
+                tasks_added = milk_auth_tasks_add (priv->auth, timeline, names);
+
+                for (l = tasks_added; l; l = l->next) {
+                        /* FIXME: cut this */
+                        g_debug (G_STRLOC ": trying to add task ID to "
+                                        "newly-inserted task: '%s' (%s);"
+                                        " priority: '%s', "
+                                        " list ID: '%s', "
+                                        " taskseries ID: '%s', "
+                                        ,
+                                        rtm_task_get_name (l->data),
+                                        rtm_task_get_id (l->data),
+                                        rtm_task_get_priority (l->data),
+                                        rtm_task_get_list_id (l->data),
+                                        rtm_task_get_taskseries_id (l->data)
+                                        );
+
+                        /* mark these as having local changes so we'll send all
+                         * there non-name attributes when we send the changes,
+                         * in the next step */
+                        db_insert_or_update_task (priv->db, l->data, TRUE,
+                                                  NULL);
+                }
+
+                /* not the most complete verification, but probably fine */
+                success &= (g_list_length (tasks_added) ==
+                                g_list_length (names));
+
+                g_list_foreach (tasks_added, (GFunc) g_object_unref, NULL);
+                g_list_free (tasks_added);
+        }
+
+        g_list_foreach (names, (GFunc) g_free, NULL);
+        g_list_free (names);
+
+        return success;
+}
+
+/* FIXME: cut this */
+static void
+set_due_date (RtmTask *task,
+                const char *date_str)
+{
+        GTimeVal timeval = {0};
+
+        /* FIXME: cut this */
+        g_debug ("going to decode date: '%s'", date_str);
+
+        g_time_val_from_iso8601 (date_str, &timeval);
+
+        rtm_task_set_due_date (task, &timeval);
+}
+
+static gboolean
+db_tasks_mark_as_synced (MilkCache *cache,
+                         GList     *tasks)
+{
+        MilkCachePrivate *priv;
+        gboolean success = TRUE;
+        GString *tasks_builder = NULL;
+        GList *l;
+        char *task_ids;
+        gboolean first = TRUE;
+        char *statement;
+        char *err = NULL;
+
+        priv = MILK_CACHE_PRIVATE (cache);
+
+        tasks_builder = g_string_new ("");
+        for (l = tasks; l; l = l->next) {
+                const char *format;
+
+                format = first ? "%s" : ",%s";
+
+                g_string_append_printf (tasks_builder, format,
+                                rtm_task_get_id (l->data));
+                first = FALSE;
+        }
+        task_ids = g_string_free (tasks_builder, FALSE);
+
+        statement = g_strdup_printf ("UPDATE tasks "
+                        "SET local_changes=0 "
+                        "WHERE task_id IN (%s);",
+                        task_ids);
+
+        if (sqlite3_exec (priv->db, statement, NULL, NULL, &err)) {
+                g_warning ("failed to acknowledge local changes were pushed "
+                                "to the server: %s\n",
+                                sqlite3_errmsg (priv->db));
+                success = FALSE;
+        }
+
+        g_free (statement);
+        sqlite3_free (err);
+
+        g_free (task_ids);
+}
+
+static gboolean
+cache_send_changed_tasks (MilkCache *cache,
+                          char      *timeline)
+{
+        MilkCachePrivate *priv;
+        gboolean success = TRUE;
+        GList *tasks_to_change;
+        GList *tasks_sent;
+
+        priv = MILK_CACHE_PRIVATE (cache);
+
+        tasks_to_change = db_get_tasks_to_change (cache);
+
+        if (!tasks_to_change)
+                return;
+
+        tasks_sent = milk_auth_tasks_send_changes (priv->auth,
+                        timeline, tasks_to_change);
+
+        /* as above, if we miss any of these, the worst case is just resending
+         * them later (vs. data loss or false caching) -- it's still not great
+         * if you're on a flakey network, though */
+        success &= (g_list_length (tasks_sent) ==
+                        g_list_length (tasks_to_change));
+
+        success &= db_tasks_mark_as_synced (cache, tasks_sent);
+
+        /* FIXME: cut this */
+        g_debug ("successfully updated all the tasks: %d", success);
+
+        g_list_foreach (tasks_to_change, (GFunc) g_object_unref, NULL);
+        g_list_free (tasks_to_change);
+        g_list_free (tasks_sent);
+
+        return success;
+}
+
+/* FIXME: make this async */
+static gboolean
+cache_send_changes (MilkCache *cache)
+{
+        MilkCachePrivate *priv;
+        gboolean success = TRUE;
+        char *timeline;
+
+        priv = MILK_CACHE_PRIVATE (cache);
+
+        /* FIXME: this makes the send_changes_hint fairly pointless, though we
+         * may want to rename this function similarly */
+        if (milk_auth_get_state (priv->auth) != MILK_AUTH_STATE_CONNECTED) {
+                g_warning ("trying to send changes before auth has connected - we shouldn't be doing this (at least not until we've connected once)");
+
+                return;
+        }
+
+        /* FIXME: cut this */
+        g_debug (G_STRLOC ": sending new (and updated tasks, once implemented) ");
+
+        timeline = milk_auth_timeline_create (priv->auth, NULL);
+
+        success &= cache_send_new_tasks (cache, timeline);
+        success &= cache_send_changed_tasks (cache, timeline);
+
+        /* FIXME: also get all the deleted tasks and delete them, and all the
+         * completed tasks (complete_date IS NOT NULL && local_changes=1), and
+         * apply those on the server */
+
+        /* FIXME: cut this */
+        g_debug ("looks like we successfully added the pending tasks");
+
+        /* XXX: if we use the same timeline for creating these new tasks as
+         * updating their attributes, we won't need to have this insane 2-step
+         * process, right? (and thus updating the local_changes above shouldn't
+         * be necessary) */
+
+        g_free (timeline);
+
+        return success;
+}
+
+/* FIXME: make this async */
+static gboolean
+cache_receive_changes (MilkCache *cache)
+{
+        MilkCachePrivate *priv = MILK_CACHE_PRIVATE (cache);
+        GList *rtm_tasks;
+        GList *l;
+        GList *added = NULL, *changed = NULL, *finished = NULL;
+        GTimeVal current_time;
+        char *new_sync;
+        GError *error = NULL;
+
+        if (milk_auth_get_state (priv->auth) != MILK_AUTH_STATE_CONNECTED) {
+                return TRUE;
+        }
+
+        g_get_current_time (&current_time);
+        new_sync = g_time_val_to_iso8601 (&current_time);
+
+        rtm_tasks = milk_auth_get_tasks (priv->auth, priv->last_sync, &error);
+        if (error) {
+                g_error (G_STRLOC ": failed to retrieve latest tasks: %s",
+                                error->message);
+                g_clear_error (&error);
+                goto cache_receive_changes_ERROR;
+        }
+
+        /* We don't wrap this in a begin...commit...rollback because it's better
+         * to let the individual items get committed automatically. If one of
+         * them fails, then we won't update the "last_sync" date. So the next
+         * time we sync, we'll start from where we did last time, and so on,
+         * until they all succeed at once. Any tasks that are already in the DB
+         * will be "updated", whether their content actually changed or not
+         * (likely not) */
+        for (l = rtm_tasks; l; l = g_list_delete_link (l, l)) {
+                GtkTreeIter iter;
+                RtmTask *task;
+                gboolean task_existed;
+
+                task = RTM_TASK (l->data);
+
+                if (!db_insert_or_update_task (priv->db, task, FALSE,
+                                               &task_existed))
+                        goto cache_receive_changes_ERROR;
+
+                        /* FIXME: read the task back out of the DB for any sort
+                         * of normalization to happen transparently, and for
+                         * more commonality in the code reading out of the DB
+                         */
+
+                /* Task is deleted or completed */
+                if (task_is_finished (task)) {
+                        finished = g_list_prepend (finished, task);
+
+                /* Task has been changed */
+                } else if (task_existed) {
+                        changed = g_list_prepend (changed, task);
+
+                /* Task is new */
+                } else {
+                        added = g_list_prepend (added, task);
+                }
+        }
+
+        cache_tasks_notify (cache, added, signals[TASK_ADDED]);
+        cache_tasks_notify (cache, changed, signals[TASK_CHANGED]);
+        cache_tasks_notify (cache, finished, signals[TASK_FINISHED]);
+
+        g_list_free (added);
+        g_list_free (changed);
+        g_list_free (finished);
+
+        if (db_set_last_sync (priv->db, new_sync)) {
+                g_free (priv->last_sync);
+                priv->last_sync = new_sync;
+        } else {
+                g_warning ("failed to set new sync timestamp to %d", new_sync);
+                goto cache_receive_changes_ERROR;
+        }
+
+        return TRUE;
+
+cache_receive_changes_ERROR:
+        g_free (new_sync);
+
+        return FALSE;
+}
+
+static gboolean
+cache_send_receive_changes (MilkCache *cache)
+{
+        return cache_send_changes (cache) && cache_receive_changes (cache);
+}
+
+static void
+restart_send_receive_poll (MilkCache *cache,
+                           gboolean   poll_first)
+{
+        MilkCachePrivate *priv;
+
+        priv = MILK_CACHE_PRIVATE (cache);
+
+        /* FIXME: cut this */
+        g_debug ("restarting the send/receive poll");
+
+        if (priv->update_id)
+                g_source_remove (priv->update_id);
+
+        if (poll_first)
+                cache_send_receive_changes (cache);
+
+        priv->update_id = g_timeout_add (CACHE_UPDATE_PERIOD,
+                        (GSourceFunc) cache_send_receive_changes, cache);
+}
+
+task_is_finished (RtmTask *task)
+{
+        return (rtm_task_get_completed_date (task) ||
+                rtm_task_get_deleted_date (task));
+}
+
+static void
+auth_notify_cb (MilkAuth   *auth,
+                GParamSpec *spec,
+                MilkCache  *cache)
+{
+        if (milk_auth_get_state (auth) == MILK_AUTH_STATE_CONNECTED) {
+                cache_send_receive_changes (cache);
+        }
+}
+
+void
+milk_cache_set_auth (MilkCache *cache,
+                     MilkAuth  *auth)
+{
+        MilkCachePrivate *priv;
+
+        g_return_if_fail (MILK_IS_CACHE (cache));
+        g_return_if_fail (MILK_IS_AUTH (auth));
+
+        priv = MILK_CACHE_PRIVATE (cache);
+
+        if (priv->auth) {
+                g_object_unref (priv->auth);
+        }
+        priv->auth = g_object_ref (auth);
+
+        restart_send_receive_poll (cache, FALSE);
+
+        g_signal_emit (cache, signals[CLEARED], 0);
+
+        g_signal_connect (priv->auth, "notify::state",
+                          G_CALLBACK (auth_notify_cb), cache);
+        auth_notify_cb (priv->auth, NULL, cache);
+}
+
+static void
+milk_cache_get_property (GObject    *object,
+                         guint       property_id,
+                         GValue     *value,
+                         GParamSpec *pspec)
+{
+        MilkCachePrivate *priv = MILK_CACHE_PRIVATE (object);
+
+        switch (property_id)
+        {
+                case PROP_AUTH:
+                        g_value_set_object (value, priv->auth);
+                break;
+
+                default:
+                        G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id,
+                                        pspec);
+        }
+}
+
+static void
+milk_cache_set_property (GObject      *object,
+                         guint         property_id,
+                         const GValue *value,
+                         GParamSpec   *pspec)
+{
+        MilkCachePrivate *priv;
+        MilkCache *cache;
+
+        cache = MILK_CACHE (object);
+        priv = MILK_CACHE_PRIVATE (cache);
+
+        switch (property_id)
+        {
+                case PROP_AUTH:
+                        milk_cache_set_auth (cache, g_value_get_object (value));
+                break;
+
+                default:
+                        G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id,
+                                        pspec);
+        }
+}
+
+GList*
+milk_cache_get_active_tasks (MilkCache *cache)
+{
+        MilkCachePrivate *priv;
+        GList *tasks;
+
+        g_return_val_if_fail (MILK_IS_CACHE (cache), NULL);
+
+        priv = MILK_CACHE_PRIVATE (cache);
+
+        tasks = db_get_tasks_active (priv->db);
+
+        return tasks;
+}
+
+char*
+milk_cache_timeline_create (MilkCache  *cache,
+                            GError    **error)
+{
+        MilkCachePrivate *priv;
+
+        g_return_val_if_fail (MILK_IS_CACHE (cache), NULL);
+
+        priv = MILK_CACHE_PRIVATE (cache);
+
+        return milk_auth_timeline_create (priv->auth, error);
+}
+
+/* FIXME: is the timeline argument even useful here? We should be able to just
+ * assume it's being added right now, right? */
+
+/* FIXME: either fill in error appropriately or cut it as an argument, so the
+ * caller doesn't assume it will be meaningful if we return NULL */
+RtmTask*
+milk_cache_task_add (MilkCache   *cache,
+                     char        *timeline,
+                     const char  *name,
+                     GError     **error)
+{
+        MilkCachePrivate *priv;
+        RtmTask *task;
+
+        g_return_val_if_fail (MILK_IS_CACHE (cache), NULL);
+
+        priv = MILK_CACHE_PRIVATE (cache);
+
+        task = db_add_local_only_task (priv->db, name);
+        if (task) {
+                GList *tasks;
+
+                tasks = g_list_prepend (NULL, task);
+                cache_tasks_notify (cache, tasks, signals[TASK_ADDED]);
+                restart_send_receive_poll (cache, TRUE);
+
+                g_list_free (tasks);
+        }
+
+        return task;
+}
+
+char*
+milk_cache_task_complete (MilkCache  *cache,
+                          char       *timeline,
+                          RtmTask    *task,
+                          GError    **error)
+{
+        MilkCachePrivate *priv;
+
+        g_return_val_if_fail (MILK_IS_CACHE (cache), NULL);
+
+        priv = MILK_CACHE_PRIVATE (cache);
+
+        /* FIXME: mark the task as "to-complete" and set up the periodic task
+         * that pushes the changes to the server; then immediately emit the
+         * "task-finished" signal, so the model will immediately reflect that */
+
+        return milk_auth_task_complete (priv->auth, timeline, task, error);
+}
+
+char*
+milk_cache_task_delete (MilkCache  *cache,
+                        char       *timeline,
+                        RtmTask    *task,
+                        GError    **error)
+{
+        MilkCachePrivate *priv;
+
+        g_return_val_if_fail (MILK_IS_CACHE (cache), NULL);
+
+        priv = MILK_CACHE_PRIVATE (cache);
+
+        /* FIXME: mark the task as "to-delete" and set up the periodic task that
+         * pushes the changes to the server; then immediately emit the
+         * "task-finished" signal, so the model will immediately reflect that */
+
+        return milk_auth_task_delete (priv->auth, timeline, task, error);
+}
+
+/* XXX: this won't be necessary when the auth handles this transparently; or at
+ * least this will merely be a signal to the auth that we're ready to
+ * authenticate when it is */
+void
+milk_cache_authenticate (MilkCache *cache)
+{
+        MilkCachePrivate *priv;
+
+        g_return_if_fail (MILK_IS_CACHE (cache));
+
+        priv = MILK_CACHE_PRIVATE (cache);
+
+        milk_auth_log_in (priv->auth);
+}
+
+static void
+milk_cache_dispose (GObject *object)
+{
+        MilkCachePrivate *priv = MILK_CACHE_PRIVATE (object);
+
+        if (priv->auth) {
+                g_object_unref (priv->auth);
+                priv->auth = NULL;
+        }
+
+        if (priv->update_id) {
+                g_source_remove (priv->update_id);
+                priv->update_id = 0;
+        }
+}
+
+static void
+milk_cache_finalize (GObject *object)
+{
+        MilkCachePrivate *priv = MILK_CACHE_PRIVATE (object);
+
+        g_free (priv->last_sync);
+
+        /* FIXME: should we do this at atexit() instead, for better safety? */
+        sqlite3_close (priv->db);
+}
+
+static void
+milk_cache_class_init (MilkCacheClass *klass)
+{
+        GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+        g_type_class_add_private (klass, sizeof (MilkCachePrivate));
+
+        object_class->get_property = milk_cache_get_property;
+        object_class->set_property = milk_cache_set_property;
+        object_class->dispose = milk_cache_dispose;
+        object_class->finalize = milk_cache_finalize;
+
+        g_object_class_install_property
+                (object_class,
+                 PROP_AUTH,
+                 g_param_spec_object
+                         ("auth",
+                          "Authentication proxy",
+                          "Remember The Milk authentication proxy.",
+                          MILK_TYPE_AUTH,
+                          G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY |
+                          G_PARAM_STATIC_STRINGS));
+
+        signals[CLEARED] = g_signal_new
+                ("cleared",
+                MILK_TYPE_CACHE, G_SIGNAL_RUN_LAST, 0, NULL, NULL,
+                g_cclosure_marshal_VOID__VOID, G_TYPE_NONE, 0);
+
+        signals[TASK_ADDED] = g_signal_new
+                ("task-added",
+                 MILK_TYPE_CACHE, G_SIGNAL_RUN_LAST, 0, NULL, NULL,
+                 g_cclosure_marshal_VOID__OBJECT, G_TYPE_NONE, 1,
+                 RTM_TYPE_TASK);
+
+        signals[TASK_FINISHED] = g_signal_new
+                ("task-finished",
+                MILK_TYPE_CACHE, G_SIGNAL_RUN_LAST, 0, NULL, NULL,
+                g_cclosure_marshal_VOID__OBJECT, G_TYPE_NONE, 1, RTM_TYPE_TASK);
+
+        signals[TASK_CHANGED] = g_signal_new
+                ("task-changed",
+                 MILK_TYPE_CACHE, G_SIGNAL_RUN_LAST, 0, NULL, NULL,
+                 g_cclosure_marshal_VOID__OBJECT, G_TYPE_NONE, 1,
+                 RTM_TYPE_TASK);
+}
+
+static void
+milk_cache_init (MilkCache *self)
+{
+        MilkCachePrivate *priv;
+
+        self->priv = priv = G_TYPE_INSTANCE_GET_PRIVATE (
+                        self, MILK_TYPE_CACHE, MilkCachePrivate);
+
+        /* open the DB, creating a new one if necessary */
+        priv->db = db_open ();
+        db_update_schema (priv->db);
+        priv->last_sync = db_get_last_sync (priv->db);
+}
+
+MilkCache*
+milk_cache_get_default ()
+{
+        if (!default_cache) {
+                default_cache = g_object_new (MILK_TYPE_CACHE,
+                                "auth", milk_auth_get_default (),
+                                NULL);
+        }
+
+        return default_cache;
+}
diff --git a/src/milk-cache.h b/src/milk-cache.h
new file mode 100644 (file)
index 0000000..d774fe0
--- /dev/null
@@ -0,0 +1,89 @@
+/*
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation; either version 2 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public
+ * License along with this program; if not, write to the
+ * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
+ * Boston, MA  02110-1301  USA
+ *
+ * Authors: Travis Reitter <treitter@gmail.com>
+ */
+
+#ifndef _MILK_CACHE_H
+#define _MILK_CACHE_H
+
+G_BEGIN_DECLS
+
+#define MILK_TYPE_CACHE milk_cache_get_type()
+
+#define MILK_CACHE(obj) \
+                (G_TYPE_CHECK_INSTANCE_CAST ((obj), \
+                MILK_TYPE_CACHE, MilkCache))
+
+#define MILK_CACHE_CLASS(klass) \
+                (G_TYPE_CHECK_CLASS_CAST ((klass), \
+                MILK_TYPE_CACHE, MilkCacheClass))
+
+#define MILK_IS_CACHE(obj) \
+                (G_TYPE_CHECK_INSTANCE_TYPE ((obj), \
+                MILK_TYPE_CACHE))
+
+#define MILK_IS_CACHE_CLASS(klass) \
+                (G_TYPE_CHECK_CLASS_TYPE ((klass), \
+                MILK_TYPE_CACHE))
+
+#define MILK_CACHE_GET_CLASS(obj) \
+                (G_TYPE_INSTANCE_GET_CLASS ((obj), \
+                MILK_TYPE_CACHE, MilkCacheClass))
+
+typedef struct _MilkCache MilkCache;
+typedef struct _MilkCacheClass MilkCacheClass;
+typedef struct _MilkCachePrivate MilkCachePrivate;
+
+struct _MilkCache
+{
+        GObject parent;
+        MilkCachePrivate *priv;
+};
+
+struct _MilkCacheClass
+{
+        GObjectClass parent_class;
+};
+
+GType milk_cache_get_type (void);
+
+
+MilkCache*      milk_cache_get_default (void);
+
+void            milk_cache_authenticate (MilkCache *cache);
+
+GList*          milk_cache_get_active_tasks (MilkCache *cache);
+
+char*           milk_cache_timeline_create (MilkCache  *cache,
+                                            GError    **error);
+
+RtmTask*        milk_cache_task_add (MilkCache   *cache,
+                                     char        *timeline,
+                                     const char  *name,
+                                     GError     **error);
+
+char*           milk_cache_task_complete (MilkCache  *cache,
+                                          char       *timeline,
+                                          RtmTask    *task,
+                                          GError    **error);
+
+char*           milk_cache_task_delete (MilkCache  *cache,
+                                        char       *timeline,
+                                        RtmTask    *task,
+                                        GError    **error);
+
+#endif /* _MILK_CACHE_H */
index e159b2b..abf9ec7 100644 (file)
@@ -26,7 +26,7 @@
 #include <rtm-glib/rtm-glib.h>
 
 #include "milk-main-window.h"
-#include "milk-auth.h"
+#include "milk-cache.h"
 #include "milk-task-model.h"
 
 G_DEFINE_TYPE (MilkMainWindow, milk_main_window, HILDON_TYPE_WINDOW)
@@ -40,7 +40,7 @@ static GtkWidget *default_window = NULL;
 
 struct _MilkMainWindowPrivate
 {
-        MilkAuth *auth;
+        MilkCache *cache;
 
         GtkWidget *app_menu;
 
@@ -103,19 +103,13 @@ new_task_clicked_cb (GtkButton      *button,
         g_debug ("FIXME: implement 'new task' action");
 }
 
-/* XXX: The latency between clicking "complete" and actually removing the task
- * from the view after polling the server is very long, so there's an obvious
- * lag -- it will be completely transparent (and look very fast) as soon as
- * we've got a cache in place */
-static void
-complete_clicked_cb (GtkButton      *button,
-                     MilkMainWindow *window)
+static GList*
+get_selected_tasks (MilkMainWindow *window)
 {
         MilkMainWindowPrivate *priv;
         GList *rows;
         GtkTreeModel *model;
-        char *timeline;
-        GError *error = NULL;
+        GList *tasks = NULL;
 
         priv = MILK_MAIN_WINDOW_PRIVATE (window);
 
@@ -126,85 +120,72 @@ complete_clicked_cb (GtkButton      *button,
                         HILDON_TOUCH_SELECTOR (priv->task_view),
                         TASK_VIEW_COLUMN_TITLE);
 
-        timeline = milk_auth_timeline_create (priv->auth, &error);
+        while (rows) {
+                GtkTreeIter iter;
+                RtmTask *task;
+
+                gtk_tree_model_get_iter (model, &iter, rows->data);
+                gtk_tree_model_get (model, &iter,
+                                MILK_TASK_MODEL_COLUMN_TASK, &task,
+                                -1);
+
+                tasks = g_list_prepend (tasks, task);
+                rows = g_list_delete_link (rows, rows);
+        }
+
+        return tasks;
+}
+
+static void
+complete_clicked_cb (GtkButton      *button,
+                     MilkMainWindow *window)
+{
+        MilkMainWindowPrivate *priv;
+        GList *tasks;
+        char *timeline;
+        GError *error = NULL;
+
+        priv = MILK_MAIN_WINDOW_PRIVATE (window);
+
+        tasks = get_selected_tasks (window);
+        timeline = milk_cache_timeline_create (priv->cache, &error);
 
         if (error) {
                 g_warning (G_STRLOC ": failed to create a timeline: %s",
                            error->message);
                 g_clear_error (&error);
         } else {
-                while (rows) {
-                        GtkTreeIter iter;
-                        RtmTask *task;
-
-                        gtk_tree_model_get_iter (model, &iter, rows->data);
-                        gtk_tree_model_get (model, &iter,
-                                        MILK_TASK_MODEL_COLUMN_TASK, &task,
-                                        -1);
-
-                        milk_auth_task_complete (priv->auth, timeline, task,
-                                        &error);
-                        if (error != NULL) {
-                                g_warning (G_STRLOC ": failed to complete task "
-                                                "%s: %s",
-                                                rtm_task_get_id (task),
-                                                error->message);
-                                g_clear_error (&error);
-                        }
-
-                        rows = g_list_delete_link (rows, rows);
+                while (tasks) {
+                        milk_cache_task_complete (priv->cache, timeline,
+                                        tasks->data, &error);
+                        tasks = g_list_delete_link (tasks, tasks);
                 }
         }
 }
 
-/* XXX: high latency until we have a cache; see the note for
- * complete_clicked_cb() */
 static void
 delete_clicked_cb (GtkButton      *button,
                    MilkMainWindow *window)
 {
         MilkMainWindowPrivate *priv;
-        GList *rows;
-        GtkTreeModel *model;
+        GList *tasks;
         char *timeline;
         GError *error = NULL;
 
         priv = MILK_MAIN_WINDOW_PRIVATE (window);
 
-        rows = hildon_touch_selector_get_selected_rows (
-                        HILDON_TOUCH_SELECTOR (priv->task_view),
-                        TASK_VIEW_COLUMN_TITLE);
-        model = hildon_touch_selector_get_model (
-                        HILDON_TOUCH_SELECTOR (priv->task_view),
-                        TASK_VIEW_COLUMN_TITLE);
-
-        timeline = milk_auth_timeline_create (priv->auth, &error);
+        tasks = get_selected_tasks (window);
+        timeline = milk_cache_timeline_create (priv->cache, &error);
 
         if (error) {
                 g_warning (G_STRLOC ": failed to create a timeline: %s",
                            error->message);
                 g_clear_error (&error);
         } else {
-                while (rows) {
-                        GtkTreeIter iter;
-                        RtmTask *task;
-
-                        gtk_tree_model_get_iter (model, &iter, rows->data);
-                        gtk_tree_model_get (model, &iter,
-                                        MILK_TASK_MODEL_COLUMN_TASK, &task,
-                                        -1);
-
-                        milk_auth_task_delete (priv->auth, timeline, task,
-                                        &error);
-                        if (error != NULL) {
-                                g_warning (G_STRLOC ": failed to delete task "
-                                                "%s: %s",
-                                                rtm_task_get_id (task),
-                                                error->message);
-                                g_clear_error (&error);
-                        }
-
-                        rows = g_list_delete_link (rows, rows);
+                while (tasks) {
+                        milk_cache_task_delete (priv->cache, timeline,
+                                        tasks->data, &error);
+                        tasks = g_list_delete_link (tasks, tasks);
                 }
         }
 }
@@ -259,7 +240,7 @@ new_task_entry_activated_cb (GtkEntry       *entry,
                 char *timeline;
                 GError *error = NULL;
 
-                timeline = milk_auth_timeline_create (priv->auth, &error);
+                timeline = milk_cache_timeline_create (priv->cache, &error);
 
                 if (error) {
                         g_warning (G_STRLOC ": failed to create a timeline: %s",
@@ -268,13 +249,17 @@ new_task_entry_activated_cb (GtkEntry       *entry,
                 } else {
                         RtmTask *task;
 
-                        task = milk_auth_task_add (priv->auth, timeline, name,
+                        task = milk_cache_task_add (priv->cache, timeline, name,
                                         &error);
                         if (task) {
                                 /* empty out the entry and show its placeholder
                                  * text */
                                 gtk_entry_set_text (entry, "");
                                 gtk_widget_grab_focus (priv->task_view);
+
+                                /* FIXME: we should probably scroll to this new
+                                 * task in the model view, if it's not currently
+                                 * visible (and highlight only it in any case */
                         } else {
                                 g_warning (G_STRLOC ": failed to add task: %s",
                                                 error->message);
@@ -400,13 +385,13 @@ contact_column_render_func (GtkCellLayout   *cell_layout,
 }
 
 static gboolean
-begin_auth_idle (MilkMainWindow *window)
+begin_cache_idle (MilkMainWindow *window)
 {
         MilkMainWindowPrivate *priv;
 
         priv = MILK_MAIN_WINDOW_PRIVATE (window);
 
-        milk_auth_log_in (priv->auth);
+        milk_cache_authenticate (priv->cache);
 
         return FALSE;
 }
@@ -444,11 +429,9 @@ milk_main_window_constructed (GObject* object)
         /*
          * Task List
          */
-        priv->auth = milk_auth_get_default ();
-        model = GTK_TREE_MODEL (milk_task_model_new (priv->auth));
+        model = GTK_TREE_MODEL (milk_task_model_new ());
         w = hildon_touch_selector_new ();
 
-
         renderer = gtk_cell_renderer_text_new ();
         g_object_set (renderer,
                         "ellipsize", PANGO_ELLIPSIZE_END,
@@ -484,9 +467,12 @@ milk_main_window_constructed (GObject* object)
         hildon_window_set_app_menu (
                         HILDON_WINDOW (self), HILDON_APP_MENU (priv->app_menu));
 
+        /* set up the cache */
+        priv->cache = milk_cache_get_default ();
+
         /* break a cyclical dependency by doing this after the window is
          * constructed */
-        g_idle_add ((GSourceFunc) begin_auth_idle, self);
+        g_idle_add ((GSourceFunc) begin_cache_idle, self);
 }
 
 static void
index a1d6594..cb24974 100644 (file)
@@ -27,6 +27,7 @@
 
 #include "milk-task-model.h"
 #include "milk-auth.h"
+#include "milk-cache.h"
 
 static void
 milk_task_model_tree_model_init (GtkTreeModelIface *iface);
@@ -41,30 +42,18 @@ G_DEFINE_TYPE_EXTENDED (MilkTaskModel,
 /* less expensive than G_TYPE_INSTANCE_GET_PRIVATE */
 #define MILK_TASK_MODEL_PRIVATE(o) ((MILK_TASK_MODEL ((o)))->priv)
 
-/* FIXME: make this configurable at runtime, pref. as a gconf value */
-/* time between syncing with the server, in ms */
-#define MODEL_UPDATE_PERIOD 60000
-
 struct _MilkTaskModelPrivate
 {
         GHashTable *tasks;
         GtkListStore *store;
-        MilkAuth *auth;
-        guint update_id;
-        char *last_sync;
+        MilkCache *cache;
 };
 
 enum {
-        PROP_AUTH = 1,
+        PROP_0,
+        PROP_CACHE,
 };
 
-static gboolean
-task_is_finished (RtmTask *task)
-{
-        return (rtm_task_get_completed_date (task) ||
-                rtm_task_get_deleted_date (task));
-}
-
 static GtkTreeModelFlags
 milk_task_model_get_flags (GtkTreeModel *model)
 {
@@ -243,10 +232,13 @@ milk_task_model_iter_parent (GtkTreeModel *model,
                         GTK_TREE_MODEL (priv->store), iter, child);
 }
 
+typedef gchar* (*RtmTaskAttrFunc) (RtmTask*);
+
 static gboolean
-model_store_find_task (MilkTaskModel *model,
-                       RtmTask       *task_in,
-                       GtkTreeIter   *iter_in)
+model_store_find_task_by_attr (MilkTaskModel   *model,
+                               RtmTask         *task_in,
+                               RtmTaskAttrFunc  attr_func,
+                               GtkTreeIter     *iter_in)
 {
         MilkTaskModelPrivate *priv = MILK_TASK_MODEL_PRIVATE (model);
         gboolean valid;
@@ -262,8 +254,8 @@ model_store_find_task (MilkTaskModel *model,
                                         MILK_TASK_MODEL_COLUMN_TASK, &task,
                                         -1);
 
-                if (!g_strcmp0 (rtm_task_get_id (task_in),
-                                rtm_task_get_id (task))) {
+                if (!g_strcmp0 (attr_func (task_in),
+                                attr_func (task))) {
                         *iter_in = iter;
                         found = TRUE;
                 }
@@ -278,99 +270,21 @@ model_store_find_task (MilkTaskModel *model,
 }
 
 static gboolean
-update_model (MilkTaskModel *model)
+model_store_find_task (MilkTaskModel *model,
+                       RtmTask       *task_in,
+                       GtkTreeIter   *iter_in)
 {
-        MilkTaskModelPrivate *priv = MILK_TASK_MODEL_PRIVATE (model);
-        GList *rtm_tasks;
-        GList *l;
-        GTimeVal current_time;
-        char *new_sync;
-        GError *error = NULL;
-
-        if (milk_auth_get_state (priv->auth) != MILK_AUTH_STATE_CONNECTED) {
-                return TRUE;
-        }
-
-        g_get_current_time (&current_time);
-        new_sync = g_time_val_to_iso8601 (&current_time);
-        rtm_tasks = milk_auth_get_tasks (priv->auth, priv->last_sync, &error);
-
-        if (error) {
-                g_error (G_STRLOC ": failed to retrieve latest tasks: %s",
-                                error->message);
-                g_clear_error (&error);
-        } else {
-                g_free (priv->last_sync);
-                priv->last_sync = new_sync;
-        }
-
-        /* Populate model */
-        for (l = rtm_tasks; l; l = g_list_delete_link (l, l)) {
-                GtkTreeIter iter;
-                RtmTask *rtm_task;
-                const char *id;
-                gboolean task_in_store;
-
-                rtm_task = RTM_TASK (l->data);
-
-                id = rtm_task_get_id (rtm_task);
-                g_hash_table_insert (priv->tasks, g_strdup (id),
-                                g_object_ref (rtm_task));
-
-                task_in_store = model_store_find_task (model, rtm_task, &iter);
-
-                /* Task is deleted or completed */
-                if (task_is_finished (rtm_task)) {
-                        if (task_in_store) {
-                                gtk_list_store_remove (priv->store, &iter);
-                        }
-                /* Task has been changed */
-                } else if (task_in_store) {
-                        RtmTask *old_task;
-                        GtkTreePath *path;
-
-                        /* rtm-glib doesn't re-use task structs when they're
-                         * updated, so we have to replace the changed */
-                        gtk_tree_model_get (
-                                        GTK_TREE_MODEL (priv->store), &iter,
-                                        MILK_TASK_MODEL_COLUMN_TASK, &old_task,
-                                        -1);
-
-                        gtk_list_store_set (
-                                        priv->store, &iter,
-                                        MILK_TASK_MODEL_COLUMN_TASK, rtm_task,
-                                        -1);
-
-                        path = gtk_tree_model_get_path (
-                                        GTK_TREE_MODEL (priv->store), &iter);
-                        gtk_tree_model_row_changed (
-                                        GTK_TREE_MODEL (priv->store),
-                                        path, &iter);
-                        gtk_tree_path_free (path);
-
-                        g_object_unref (old_task);
-
-                /* Task is new */
-                } else {
-                        gtk_list_store_append (priv->store, &iter);
-                        gtk_list_store_set (
-                                        priv->store, &iter,
-                                        MILK_TASK_MODEL_COLUMN_TASK, rtm_task,
-                                        -1);
-                }
-        }
-
-        return TRUE;
+        return model_store_find_task_by_attr (model, task_in, rtm_task_get_id,
+                        iter_in);
 }
 
-static void
-auth_notify_cb (MilkAuth      *auth,
-                GParamSpec    *spec,
-                MilkTaskModel *model)
+static gboolean
+model_store_find_local_only_task (MilkTaskModel *model,
+                                  RtmTask       *task_in,
+                                  GtkTreeIter   *iter_in)
 {
-        if (milk_auth_get_state (auth) == MILK_AUTH_STATE_CONNECTED) {
-                update_model (model);
-        }
+        return model_store_find_task_by_attr (model, task_in,rtm_task_get_name, 
+                        iter_in);
 }
 
 static void
@@ -410,35 +324,151 @@ rows_reordered_cb (GtkTreeModel  *model,
                         new_order);
 }
 
-void
-milk_task_model_set_auth (MilkTaskModel *model,
-                          MilkAuth      *auth)
+static void
+cache_cleared_cb (MilkCache     *cache,
+                  MilkTaskModel *model)
 {
         MilkTaskModelPrivate *priv;
+        priv = MILK_TASK_MODEL_PRIVATE (model);
 
-        g_return_if_fail (model);
-        g_return_if_fail (MILK_IS_TASK_MODEL (model));
-        g_return_if_fail (auth);
-        g_return_if_fail (MILK_IS_AUTH (auth));
+        gtk_list_store_clear (priv->store);
+}
+
+static void
+cache_task_added_cb (MilkCache     *cache,
+                     RtmTask       *task,
+                     MilkTaskModel *model)
+{
+        MilkTaskModelPrivate *priv;
+        GtkTreeIter iter;
+        const char *id;
+        gboolean task_in_store;
+
+        priv = MILK_TASK_MODEL_PRIVATE (model);
+
+        /* local-only tasks don't have a set task ID */
+        id = rtm_task_get_id (task);
+        if (id) {
+                /* clear out any entries for the task created before we knew its
+                 * server-side ID */
+                g_hash_table_remove (priv->tasks, rtm_task_get_name (task));
+        } else {
+                id = rtm_task_get_name (task);
+        }
+        g_return_if_fail (id);
+
+        g_hash_table_insert (priv->tasks, g_strdup (id),
+                        g_object_ref (task));
+
+        task_in_store = model_store_find_task (model, task, &iter);
+
+        gtk_list_store_append (priv->store, &iter);
+        gtk_list_store_set (priv->store, &iter,
+                        MILK_TASK_MODEL_COLUMN_TASK, task, -1);
+}
+
+static void
+cache_task_changed_cb (MilkCache     *cache,
+                       RtmTask       *task,
+                       MilkTaskModel *model)
+{
+        MilkTaskModelPrivate *priv;
+        GtkTreeIter iter;
+        const char *id;
+        gboolean task_in_store;
+        RtmTask *old_task;
+        GtkTreePath *path;
 
         priv = MILK_TASK_MODEL_PRIVATE (model);
 
-        if (priv->auth) {
-                g_object_unref (priv->auth);
+        id = rtm_task_get_id (task);
+        g_hash_table_insert (priv->tasks, g_strdup (id),
+                        g_object_ref (task));
+
+        /* try to find a local-only version of this task first, to upgrade it to
+         * remote status */
+        task_in_store = model_store_find_local_only_task (model, task, &iter);
+        if (!task_in_store) {
+                /* FIXME: cut this */
+                g_debug ("task (supposedly) was already known remotely");
+
+                task_in_store = model_store_find_task (model, task, &iter);
+        } else {
+                /* FIXME: cut this */
+                g_debug ("task was *NOT* known remotely");
         }
-        priv->auth = g_object_ref (auth);
 
-        if (priv->update_id) {
-                g_source_remove (priv->update_id);
+        /* rtm-glib doesn't re-use task structs when they're updated, so we have
+         * to replace the changed */
+        gtk_tree_model_get (GTK_TREE_MODEL (priv->store), &iter,
+                        MILK_TASK_MODEL_COLUMN_TASK, &old_task, -1);
+
+        gtk_list_store_set (priv->store, &iter,
+                        MILK_TASK_MODEL_COLUMN_TASK, task, -1);
+
+        path = gtk_tree_model_get_path (GTK_TREE_MODEL (priv->store), &iter);
+        gtk_tree_model_row_changed (GTK_TREE_MODEL (priv->store), path, &iter);
+        gtk_tree_path_free (path);
+
+        g_object_unref (old_task);
+}
+
+static void
+cache_task_finished_cb (MilkCache     *cache,
+                        RtmTask       *task,
+                        MilkTaskModel *model)
+{
+        MilkTaskModelPrivate *priv;
+        GtkTreeIter iter;
+        const char *id;
+        gboolean task_in_store;
+
+        priv = MILK_TASK_MODEL_PRIVATE (model);
+
+        id = rtm_task_get_id (task);
+        g_hash_table_insert (priv->tasks, g_strdup (id),
+                        g_object_ref (task));
+
+        task_in_store = model_store_find_task (model, task, &iter);
+
+        if (task_in_store)
+                gtk_list_store_remove (priv->store, &iter);
+}
+
+static void
+set_cache (MilkTaskModel *model,
+           MilkCache     *cache)
+{
+        MilkTaskModelPrivate *priv;
+        GList *tasks, *l;
+
+        g_return_if_fail (MILK_IS_TASK_MODEL (model));
+        g_return_if_fail (MILK_IS_CACHE (cache));
+
+        priv = MILK_TASK_MODEL_PRIVATE (model);
+
+        if (priv->cache) {
+                g_object_unref (priv->cache);
         }
-        priv->update_id = g_timeout_add (MODEL_UPDATE_PERIOD,
-                        (GSourceFunc) update_model, model);
+        priv->cache = g_object_ref (cache);
 
         gtk_list_store_clear (priv->store);
 
-        g_signal_connect (priv->auth, "notify::state",
-                          G_CALLBACK (auth_notify_cb), model);
-        auth_notify_cb (priv->auth, NULL, model);
+        g_signal_connect (cache, "cleared", G_CALLBACK (cache_cleared_cb),
+                        model);
+        g_signal_connect (cache, "task-added", G_CALLBACK (cache_task_added_cb),
+                        model);
+        g_signal_connect (cache, "task-changed",
+                        G_CALLBACK (cache_task_changed_cb), model);
+        g_signal_connect (cache, "task-finished",
+                        G_CALLBACK (cache_task_finished_cb), model);
+
+        /* do the initial fill from the cache */
+        tasks = milk_cache_get_active_tasks (cache);
+        for (l = tasks; l; l = l->next) {
+                cache_task_added_cb (cache, l->data, model);
+        }
+        g_list_free (tasks);
 }
 
 static void
@@ -451,8 +481,8 @@ milk_task_model_get_property (GObject    *object,
 
         switch (property_id)
         {
-                case PROP_AUTH:
-                        g_value_set_object (value, priv->auth);
+                case PROP_CACHE:
+                        g_value_set_object (value, priv->cache);
                 break;
 
                 default:
@@ -469,8 +499,8 @@ milk_task_model_set_property (GObject      *object,
 {
         switch (property_id)
         {
-                case PROP_AUTH:
-                        milk_task_model_set_auth (MILK_TASK_MODEL (object),
+                case PROP_CACHE:
+                        set_cache (MILK_TASK_MODEL (object),
                                         g_value_get_object (value));
                 break;
 
@@ -485,9 +515,18 @@ milk_task_model_dispose (GObject *object)
 {
         MilkTaskModelPrivate *priv = MILK_TASK_MODEL_PRIVATE (object);
 
-        if (priv->auth) {
-                g_object_unref (priv->auth);
-                priv->auth = NULL;
+        g_signal_handlers_disconnect_by_func (priv->cache, cache_cleared_cb,
+                        object);
+        g_signal_handlers_disconnect_by_func (priv->cache, cache_task_added_cb,
+                        object);
+        g_signal_handlers_disconnect_by_func (priv->cache,
+                        cache_task_changed_cb, object);
+        g_signal_handlers_disconnect_by_func (priv->cache,
+                        cache_task_finished_cb, object);
+
+        if (priv->cache) {
+                g_object_unref (priv->cache);
+                priv->cache = NULL;
         }
 
         g_signal_handlers_disconnect_by_func (priv->store, row_changed_cb,
@@ -504,11 +543,6 @@ milk_task_model_dispose (GObject *object)
                 priv->store = NULL;
         }
 
-        if (priv->update_id) {
-                g_source_remove (priv->update_id);
-                priv->update_id = 0;
-        }
-
         if (priv->tasks) {
                 g_hash_table_destroy (priv->tasks);
                 priv->tasks = NULL;
@@ -518,16 +552,6 @@ milk_task_model_dispose (GObject *object)
 }
 
 static void
-milk_task_model_finalize (GObject *object)
-{
-        MilkTaskModelPrivate *priv = MILK_TASK_MODEL_PRIVATE (object);
-
-        g_free (priv->last_sync);
-
-        G_OBJECT_CLASS (milk_task_model_parent_class)->finalize (object);
-}
-
-static void
 milk_task_model_class_init (MilkTaskModelClass *klass)
 {
         GObjectClass *object_class = G_OBJECT_CLASS (klass);
@@ -537,16 +561,15 @@ milk_task_model_class_init (MilkTaskModelClass *klass)
         object_class->get_property = milk_task_model_get_property;
         object_class->set_property = milk_task_model_set_property;
         object_class->dispose = milk_task_model_dispose;
-        object_class->finalize = milk_task_model_finalize;
 
         g_object_class_install_property
                 (object_class,
-                 PROP_AUTH,
+                 PROP_CACHE,
                  g_param_spec_object
-                         ("auth",
-                          "Authentication proxy",
-                          "Remember The Milk authentication proxy.",
-                          MILK_TYPE_AUTH,
+                         ("cache",
+                          "Cache of tasks",
+                          "Remember The Milk tasks cache.",
+                          MILK_TYPE_CACHE,
                           G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY |
                           G_PARAM_STATIC_STRINGS));
 }
@@ -559,8 +582,6 @@ milk_task_model_init (MilkTaskModel *self)
         self->priv = priv = G_TYPE_INSTANCE_GET_PRIVATE (
                         self, MILK_TYPE_TASK_MODEL, MilkTaskModelPrivate);
 
-        priv->last_sync = NULL;
-
         priv->tasks = g_hash_table_new_full (g_str_hash, g_str_equal,
                         g_free, g_object_unref);
 
@@ -598,7 +619,8 @@ milk_task_model_tree_model_init (GtkTreeModelIface *iface)
 }
 
 MilkTaskModel*
-milk_task_model_new (MilkAuth *auth)
+milk_task_model_new ()
 {
-        return g_object_new (MILK_TYPE_TASK_MODEL, "auth", auth, NULL);
+        return g_object_new (MILK_TYPE_TASK_MODEL,
+                        "cache", milk_cache_get_default (), NULL);
 }
index e48b75e..5dee531 100644 (file)
@@ -69,7 +69,7 @@ struct _MilkTaskModelClass
 GType milk_task_model_get_type (void);
 
 
-MilkTaskModel* milk_task_model_new (MilkAuth *auth);
+MilkTaskModel* milk_task_model_new (void);
 void milk_task_model_set_auth (MilkTaskModel *model, MilkAuth *auth);