/* * Copyright (c) 2015. Jonas Kalderstam * * 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 3 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, see <http://www.gnu.org/licenses/>. */ package com.nononsenseapps.notepad.data.remote.gtasks; import android.accounts.Account; import android.accounts.AccountManager; import android.content.ContentProviderClient; import android.content.Context; import android.content.SharedPreferences; import android.content.SyncResult; import android.database.Cursor; import android.os.Bundle; import android.preference.PreferenceManager; import android.util.Pair; import com.nononsenseapps.build.Config; import com.nononsenseapps.notepad.data.model.gtasks.GoogleTask; import com.nononsenseapps.notepad.data.model.gtasks.GoogleTaskList; import com.nononsenseapps.notepad.util.Log; import com.nononsenseapps.notepad.data.model.sql.Task; import com.nononsenseapps.notepad.data.model.sql.TaskList; import com.nononsenseapps.notepad.ui.settings.SyncPrefs; import com.nononsenseapps.notepad.util.PermissionsHelper; import com.nononsenseapps.notepad.util.SyncGtaskHelper; import com.nononsenseapps.notepad.util.RFC3339Date; import org.json.JSONException; import java.io.IOException; import java.util.ArrayList; import java.util.Calendar; import java.util.HashMap; import java.util.List; import retrofit.RetrofitError; public class GoogleTaskSync { static final String TAG = "nononsenseapps gtasksync"; public static final boolean NOTIFY_AUTH_FAILURE = true; public static final String PREFS_LAST_SYNC_ETAG = "lastserveretag"; public static final String PREFS_GTASK_LAST_SYNC_TIME = "gtasklastsync"; /** * Returns true if sync was successful, false otherwise */ public static boolean fullSync(final Context context, final Account account, final Bundle extras, final String authority, final ContentProviderClient provider, final SyncResult syncResult) { Log.d(TAG, "fullSync"); if (!PermissionsHelper.hasPermissions(context, PermissionsHelper.PERMISSIONS_GTASKS)) { Log.d(TAG, "Missing permissions, disabling sync"); SyncGtaskHelper.disableSync(context); return false; } // Is saved at a successful sync final long startTime = Calendar.getInstance().getTimeInMillis(); boolean success = false; // Initialize necessary stuff final AccountManager accountManager = AccountManager.get(context); try { GoogleTasksClient client = new GoogleTasksClient(GoogleTasksClient.getAuthToken (accountManager, account, NOTIFY_AUTH_FAILURE), Config .getGtasksApiKey(context), account.name); Log.d(TAG, "AuthToken acquired, we are connected..."); // IF full sync, download since start of all time // Temporary fix for delete all bug // if (PreferenceManager.getDefaultSharedPreferences(context) // .getBoolean(SyncPrefs.KEY_FULLSYNC, false)) { PreferenceManager.getDefaultSharedPreferences(context).edit().putBoolean(SyncPrefs .KEY_FULLSYNC, false).putLong(PREFS_GTASK_LAST_SYNC_TIME, 0).commit(); // } // Download lists from server Log.d(TAG, "download lists"); final List<GoogleTaskList> remoteLists = downloadLists(client); // merge with local complement Log.d(TAG, "merge lists"); mergeListsWithLocalDB(context, account.name, remoteLists); // Synchronize lists locally Log.d(TAG, "sync lists locally"); final List<Pair<TaskList, GoogleTaskList>> listPairs = synchronizeListsLocally (context, remoteLists); // Synchronize lists remotely Log.d(TAG, "sync lists remotely"); final List<Pair<TaskList, GoogleTaskList>> syncedPairs = synchronizeListsRemotely (context, listPairs, client); // For each list for (Pair<TaskList, GoogleTaskList> syncedPair : syncedPairs) { // Download tasks from server Log.d(TAG, "download tasks"); final List<GoogleTask> remoteTasks = downloadChangedTasks(context, client, syncedPair.second); // merge with local complement Log.d(TAG, "merge tasks"); mergeTasksWithLocalDB(context, account.name, remoteTasks, syncedPair.first._id); // Synchronize tasks locally Log.d(TAG, "sync tasks locally"); final List<Pair<Task, GoogleTask>> taskPairs = synchronizeTasksLocally(context, remoteTasks, syncedPair); // Synchronize tasks remotely Log.d(TAG, "sync tasks remotely"); synchronizeTasksRemotely(context, taskPairs, syncedPair.second, client); } Log.d(TAG, "Sync Complete!"); success = true; PreferenceManager.getDefaultSharedPreferences(context).edit().putLong (PREFS_GTASK_LAST_SYNC_TIME, startTime).commit(); } catch (RetrofitError e) { Log.d(TAG, "Retrofit: " + e); final int status; if (e.getResponse() != null) { Log.e(TAG, "" + e.getResponse().getStatus() + "; " + e.getResponse().getReason()); status = e.getResponse().getStatus(); } else { status = 999; } // An HTTP error was encountered. switch (status) { case 404: // No such item, should never happen, programming error case 415: // Not proper body, programming error case 400: // Didn't specify url, programming error //syncResult.databaseError = true; case 401: // Unauthorized, token could possibly just be stale // auth-exceptions are hard errors, and if the token is stale, // that's too harsh //syncResult.stats.numAuthExceptions++; // Instead, report ioerror, which is a soft error default: // Default is to consider it a networking/server issue syncResult.stats.numIoExceptions++; break; } } catch (Exception e) { // Something went wrong, don't punish the user Log.e(TAG, "Exception: " + e); syncResult.stats.numIoExceptions++; } finally { Log.d(TAG, "SyncResult: " + syncResult.toDebugString()); } return success; } /** * Loads the remote lists from the database and merges the two lists. If the * remote list contains all lists, then this method only adds local db-ids * to the items. If it does not contain all of them, this loads whatever * extra items are known in the db to the list also. * * Since all lists are expected to be downloaded, any non-existing entries * are assumed to be deleted and marked as such. */ public static void mergeListsWithLocalDB(final Context context, final String account, final List<GoogleTaskList> remoteLists) { Log.d(TAG, "mergeList starting with: " + remoteLists.size()); final HashMap<String, GoogleTaskList> localVersions = new HashMap<String, GoogleTaskList>(); final Cursor c = context.getContentResolver().query( GoogleTaskList.URI, GoogleTaskList.Columns.FIELDS, GoogleTaskList.Columns.ACCOUNT + " IS ? AND " + GoogleTaskList.Columns.SERVICE + " IS ?", new String[] { account, GoogleTaskList.SERVICENAME }, null); try { while (c != null && c.moveToNext()) { GoogleTaskList list = new GoogleTaskList(c); localVersions.put(list.remoteId, list); } } finally { if (c != null) c.close(); } for (final GoogleTaskList remotelist : remoteLists) { // Merge with hashmap if (localVersions.containsKey(remotelist.remoteId)) { remotelist._id = localVersions.get(remotelist.remoteId)._id; remotelist.dbid = localVersions.get(remotelist.remoteId).dbid; remotelist.setDeleted(localVersions.get(remotelist.remoteId) .isDeleted()); localVersions.remove(remotelist.remoteId); } } // Remaining ones for (final GoogleTaskList list : localVersions.values()) { list.remotelyDeleted = true; remoteLists.add(list); } Log.d(TAG, "mergeList finishing with: " + remoteLists.size()); } /** * Loads the remote tasks from the database and merges the two lists. If the * remote list contains all items, then this method only adds local db-ids * to the items. If it does not contain all of them, this loads whatever * extra items are known in the db to the list also. */ public static void mergeTasksWithLocalDB(final Context context, final String account, final List<GoogleTask> remoteTasks, long listDbId) { final HashMap<String, GoogleTask> localVersions = new HashMap<String, GoogleTask>(); final Cursor c = context.getContentResolver().query( GoogleTask.URI, GoogleTask.Columns.FIELDS, GoogleTask.Columns.LISTDBID + " IS ? AND " + GoogleTask.Columns.ACCOUNT + " IS ? AND " + GoogleTask.Columns.SERVICE + " IS ?", new String[] { Long.toString(listDbId), account, GoogleTaskList.SERVICENAME }, null); try { while (c != null && c.moveToNext()) { GoogleTask task = new GoogleTask(c); localVersions.put(task.remoteId, task); } } finally { if (c != null) c.close(); } for (final GoogleTask task : remoteTasks) { // Set list on remote objects task.listdbid = listDbId; // Merge with hashmap if (localVersions.containsKey(task.remoteId)) { task.dbid = localVersions.get(task.remoteId).dbid; task.setDeleted(localVersions.get(task.remoteId).isDeleted()); if (task.isDeleted()) { Log.d(TAG, "merge1: deleting " + task.title); } localVersions.remove(task.remoteId); } } // Remaining ones for (final GoogleTask task : localVersions.values()) { remoteTasks.add(task); if (task.isDeleted()) { Log.d(TAG, "merge2: was deleted " + task.title); } } } /** * Downloads all lists in GTasks and returns them * * @throws IOException * @throws ClientProtocolException * @throws JSONException * @param client */ static List<GoogleTaskList> downloadLists(final GoogleTasksClient client) throws IOException, RetrofitError { // Do the actual download final ArrayList<GoogleTaskList> remoteLists = new ArrayList<GoogleTaskList>(); client.listLists(remoteLists); // Return list of TaskLists return remoteLists; } /** * Given a list of remote GTaskLists, iterates through it and their versions * (if any) in the local database. If the remote version is newer, the local * version is updated. * * If local list has a remote id, but it does not exist in the list of * remote lists, then it has been deleted remotely and is deleted locally as * well. * * Returns a list of pairs (local, remote). */ public static List<Pair<TaskList, GoogleTaskList>> synchronizeListsLocally( final Context context, final List<GoogleTaskList> remoteLists) { final ArrayList<Pair<TaskList, GoogleTaskList>> listPairs = new ArrayList<Pair<TaskList, GoogleTaskList>>(); // For every list for (final GoogleTaskList remoteList : remoteLists) { // Compare with local Log.d(TAG, "Loading remote lists from db"); TaskList localList = loadRemoteListFromDB(context, remoteList); if (localList == null) { if (remoteList.remotelyDeleted) { Log.d(TAG, "List was remotely deleted1"); // Deleted locally AND on server remoteList.delete(context); } else if (remoteList.isDeleted()) { Log.d(TAG, "List was locally deleted"); // Was deleted locally } else { // is a new list Log.d(TAG, "Inserting new list: " + remoteList.title); localList = new TaskList(); localList.title = remoteList.title; localList.save(context, remoteList.updated); // Save id in remote also remoteList.dbid = localList._id; remoteList.save(context); } } else { // If local is newer, update remote object if (remoteList.remotelyDeleted) { Log.d(TAG, "Remote list was deleted2: " + remoteList.title); localList.delete(context); localList = null; remoteList.delete(context); } else if (localList.updated > remoteList.updated) { Log.d(TAG, "Local list newer"); remoteList.title = localList.title; // Updated is set by Google } else if (localList.updated.equals(remoteList.updated)) { // Nothing to do } else { Log.d(TAG, "Updating local list: " + remoteList.title); // If remote is newer, update local and save to db localList.title = remoteList.title; localList.save(context, remoteList.updated); } } if (!remoteList.remotelyDeleted) listPairs.add(new Pair<TaskList, GoogleTaskList>(localList, remoteList)); } // Add local lists without a remote version to pairs for (final TaskList tl : loadNewListsFromDB(context, remoteLists.get(0))) { Log.d(TAG, "loading new list db: " + tl.title); listPairs.add(new Pair<TaskList, GoogleTaskList>(tl, null)); } // return pairs return listPairs; } static List<Pair<TaskList, GoogleTaskList>> synchronizeListsRemotely( final Context context, final List<Pair<TaskList, GoogleTaskList>> listPairs, final GoogleTasksClient client) throws IOException, RetrofitError { final List<Pair<TaskList, GoogleTaskList>> syncedPairs = new ArrayList<Pair<TaskList, GoogleTaskList>>(); // For every list for (final Pair<TaskList, GoogleTaskList> pair : listPairs) { Pair<TaskList, GoogleTaskList> syncedPair = pair; if (pair.second == null) { // New list to create final GoogleTaskList newList = new GoogleTaskList(pair.first, client.accountName); client.insertList(newList); // Save to db also newList.save(context); pair.first.save(context, newList.updated); syncedPair = new Pair<TaskList, GoogleTaskList>(pair.first, newList); } else if (pair.second.isDeleted()) { Log.d(TAG, "remotesync: isDeletedLocally"); // Deleted locally, delete remotely also pair.second.remotelyDeleted = true; try { client.deleteList(pair.second); } catch (RetrofitError e) { if (e.getResponse() != null && e.getResponse().getStatus() == 400) { // Deleted the default list. Ignore error Log.d(TAG, "Error when deleting list. This is expected for the default list: " + e); } else { throw e; } } // and delete from db if it exists there pair.second.delete(context); syncedPair = null; } else if (pair.first.updated > pair.second.updated) { // If local update is different than remote, that means we // should update client.patchList(pair.second); // No need to save remote object pair.first.save(context, pair.second.updated); } // else remote has already been saved locally, nothing to upload if (syncedPair != null) { syncedPairs.add(syncedPair); } } // return (updated) pairs return syncedPairs; } static void synchronizeTasksRemotely(final Context context, final List<Pair<Task, GoogleTask>> taskPairs, final GoogleTaskList gTaskList, final GoogleTasksClient client) throws IOException, RetrofitError { for (final Pair<Task, GoogleTask> pair : taskPairs) { // if newly created locally if (pair.second == null) { Log.d(TAG, "Second was null"); final GoogleTask newTask = new GoogleTask(pair.first, client.accountName); client.insertTask(newTask, gTaskList); newTask.save(context); pair.first.save(context, newTask.updated); } // if deleted locally else if (pair.second.isDeleted()) { Log.d(TAG, "Second isDeleted"); // Delete remote also pair.second.remotelydeleted = true; client.deleteTask(pair.second, gTaskList); // Remove from db pair.second.delete(context); } // if local updated is different from remote, // should update remote else if (pair.first.updated > pair.second.updated) { Log.d(TAG, "First updated after second"); client.patchTask(pair.second, gTaskList); // No need to save remote object here pair.first.save(context, pair.second.updated); } } } static TaskList loadRemoteListFromDB(final Context context, final GoogleTaskList remoteList) { if (remoteList.dbid == null || remoteList.dbid < 1) return null; final Cursor c = context.getContentResolver().query( TaskList.getUri(remoteList.dbid), TaskList.Columns.FIELDS, null, null, null); TaskList tl = null; try { if (c != null && c.moveToFirst()) { tl = new TaskList(c); } } finally { if (c != null) c.close(); } return tl; } static List<TaskList> loadNewListsFromDB(final Context context, final GoogleTaskList remoteList) { final Cursor c = context.getContentResolver().query(TaskList.URI, TaskList.Columns.FIELDS, GoogleTaskList.getTaskListWithoutRemoteClause(), remoteList.getTaskListWithoutRemoteArgs(), null); final ArrayList<TaskList> lists = new ArrayList<TaskList>(); try { while (c != null && c.moveToNext()) { lists.add(new TaskList(c)); } } finally { if (c != null) c.close(); } return lists; } static List<Task> loadNewTasksFromDB(final Context context, final long listdbid, final String account) { final Cursor c = context.getContentResolver().query( Task.URI, Task.Columns.FIELDS, GoogleTask.getTaskWithoutRemoteClause(), GoogleTask.getTaskWithoutRemoteArgs(listdbid, account, GoogleTaskList.SERVICENAME), null); final ArrayList<Task> tasks = new ArrayList<Task>(); try { while (c != null && c.moveToNext()) { tasks.add(new Task(c)); } } finally { if (c != null) c.close(); } return tasks; } static List<GoogleTask> downloadChangedTasks(final Context context, final GoogleTasksClient client, final GoogleTaskList remoteList) throws IOException, RetrofitError { // final SharedPreferences settings = PreferenceManager // .getDefaultSharedPreferences(context); // RFC3339Date.asRFC3339(settings.getLong( // PREFS_GTASK_LAST_SYNC_TIME, 0)) return client.listTasks(remoteList); } static Task loadRemoteTaskFromDB(final Context context, final GoogleTask remoteTask) { final Cursor c = context.getContentResolver().query(Task.URI, Task.Columns.FIELDS, remoteTask.getTaskWithRemoteClause(), remoteTask.getTaskWithRemoteArgs(), null); Task t = null; try { if (c != null && c.moveToFirst()) { t = new Task(c); } } finally { if (c != null) c.close(); } return t; } public static List<Pair<Task, GoogleTask>> synchronizeTasksLocally( final Context context, final List<GoogleTask> remoteTasks, final Pair<TaskList, GoogleTaskList> listPair) { final SharedPreferences settings = PreferenceManager .getDefaultSharedPreferences(context); final ArrayList<Pair<Task, GoogleTask>> taskPairs = new ArrayList<Pair<Task, GoogleTask>>(); // For every list for (final GoogleTask remoteTask : remoteTasks) { // Compare with local Task localTask = loadRemoteTaskFromDB(context, remoteTask); // When no local version was found, either // a) it was deleted by the user or // b) it was created on the server if (localTask == null) { if (remoteTask.remotelydeleted) { Log.d(TAG, "slocal: task was remotely deleted1: " + remoteTask.title); // Nothing to do remoteTask.delete(context); } else if (remoteTask.isDeleted()) { Log.d(TAG, "slocal: task was locally deleted: " + remoteTask.remoteId); // upload change } else { //Log.d(TAG, "slocal: task was new: " + remoteTask.title); // If no local, and updated is higher than latestupdate, // create new localTask = new Task(); localTask.title = remoteTask.title; localTask.note = remoteTask.notes; localTask.dblist = remoteTask.listdbid; // Don't touch time if (remoteTask.dueDate != null && !remoteTask.dueDate.isEmpty()) { try { localTask.due = RFC3339Date.combineDateAndTime(remoteTask.dueDate, localTask.due); } catch (Exception ignored) { } } if (remoteTask.status != null && remoteTask.status.equals(GoogleTask.COMPLETED)) { localTask.completed = remoteTask.updated; } localTask.save(context, remoteTask.updated); // Save id in remote also remoteTask.dbid = localTask._id; remoteTask.save(context); } } else { // If local is newer, update remote object if (localTask.updated > remoteTask.updated) { remoteTask.fillFrom(localTask); // Updated is set by Google } // Remote is newer else if (remoteTask.remotelydeleted) { Log.d(TAG, "slocal: task was remotely deleted2: " + remoteTask.title); localTask.delete(context); localTask = null; remoteTask.delete(context); } else if (localTask.updated.equals(remoteTask.updated)) { // Nothing to do, we are already updated } else { //Log.d(TAG, "slocal: task was remotely updated: " + remoteTask.title); // If remote is newer, update local and save to db localTask.title = remoteTask.title; localTask.note = remoteTask.notes; localTask.dblist = remoteTask.listdbid; if (remoteTask.dueDate != null && !remoteTask.dueDate.isEmpty()) { try { // dont touch time localTask.due = RFC3339Date.combineDateAndTime(remoteTask.dueDate, localTask.due); } catch (Exception e) { localTask.due = null; } } else { localTask.due = null; } if (remoteTask.status != null && remoteTask.status.equals(GoogleTask.COMPLETED)) { // Only change this if it is not already completed if (localTask.completed == null) { localTask.completed = remoteTask.updated; } } else { localTask.completed = null; } localTask.save(context, remoteTask.updated); } } if (remoteTask.remotelydeleted) { // Dont Log.d(TAG, "skipping remotely deleted"); } else if (localTask != null && remoteTask != null && localTask.updated.equals(remoteTask.updated)) { Log.d(TAG, "skipping equal update"); // Dont } else { // if (localTask != null) { // Log.d("nononsenseapps gtasksync", "going to upload: " + localTask.title + ", l." + localTask.updated + " r." + remoteTask.updated); // } Log.d(TAG, "add to sync list: " + remoteTask.title); taskPairs .add(new Pair<Task, GoogleTask>(localTask, remoteTask)); } } // Add local lists without a remote version to pairs for (final Task t : loadNewTasksFromDB(context, listPair.first._id, listPair.second.account)) { //Log.d("nononsenseapps gtasksync", "adding local only: " + t.title); taskPairs.add(new Pair<Task, GoogleTask>(t, null)); } // return pairs return taskPairs; } }