--- /dev/null
+/*
+ * 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 (¤t_time);
+ new_sync = g_time_val_to_iso8601 (¤t_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;
+}
#include "milk-task-model.h"
#include "milk-auth.h"
+#include "milk-cache.h"
static void
milk_task_model_tree_model_init (GtkTreeModelIface *iface);
/* 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)
{
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;
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;
}
}
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 (¤t_time);
- new_sync = g_time_val_to_iso8601 (¤t_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
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
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:
{
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;
{
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,
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;
}
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);
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));
}
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);
}
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);
}