com.nononsenseapps.notepad.sync.SyncAdapter.java Source code

Java tutorial

Introduction

Here is the source code for com.nononsenseapps.notepad.sync.SyncAdapter.java

Source

/*
 * Copyright (C) 2012 Jonas Kalderstam
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.nononsenseapps.notepad.sync;

import org.apache.http.client.ClientProtocolException;
import org.json.JSONException;

import com.nononsenseapps.notepad.prefs.SyncPrefs;
import com.nononsenseapps.notepad.sync.googleapi.GoogleAPITalker;
import com.nononsenseapps.notepad.sync.googleapi.GoogleDBTalker;
import com.nononsenseapps.notepad.sync.googleapi.GoogleTask;
import com.nononsenseapps.notepad.sync.googleapi.GoogleTaskList;

import android.accounts.Account;
import android.accounts.AccountManager;
import android.content.AbstractThreadedSyncAdapter;
import android.content.ContentProviderClient;
import android.content.ContentProviderResult;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.OperationApplicationException;
import android.content.SharedPreferences;
import android.content.SyncResult;
import android.os.Bundle;
import android.os.RemoteException;
import android.preference.PreferenceManager;
import android.util.Log;
import android.widget.Toast;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;

/**
 * This adapter syncs with GoogleTasks API. Each sync is an incremental sync
 * from our last sync. This is accomplished with a combinatinon of etags and
 * last updated time stamp. The API returns a "global" e-tag (hash-value of all
 * content). If this is the same as the etag we have, then nothing has changed
 * on server. Hence, we can know that there is nothing to download. If the etag
 * has changed, the adapter requests, for all lists, all tasks which have been
 * updated since the latest synced task in the database. Possible conflicts with
 * locally modified tasks is resolved by always choosing the latests modified
 * task as the winner.
 * 
 * Before any changes are committed either way, we should have two DISJOINT
 * sets:
 * 
 * TasksFromServer and TasksToServer.
 * 
 * Due to the conflict resolution, no task should exist in both sets. We then
 * upload TasksToServer. For each upload the server will return the current
 * state of the task with some fields updated. These changes we want to save of
 * course, so we add them to TasksFromServer. Which means that after uploading
 * we have a single set:
 * 
 * TasksFromServer
 * 
 * Which now contains all tasks that were modified either locally or remotely.
 * In other words, this set is now the union of the two initially disjoint sets,
 * with some fields updated by the server.
 * 
 * These tasks are then committed to the database in a single transaction.
 */
public class SyncAdapter extends AbstractThreadedSyncAdapter {

    private static final String TAG = "SyncAdapter";
    public final static boolean SYNC_DEBUG_PRINTS = false;

    // public static final String AUTH_TOKEN_TYPE =
    // "oauth2:https://www.googleapis.com/auth/tasks";
    public static final String AUTH_TOKEN_TYPE = "Manage your tasks"; // Alias
    // for
    // above
    public static final boolean NOTIFY_AUTH_FAILURE = true;
    public static final String SYNC_STARTED = "com.nononsenseapps.notepad.sync.SYNC_STARTED";
    public static final String SYNC_FINISHED = "com.nononsenseapps.notepad.sync.SYNC_FINISHED";

    public static final String SYNC_RESULT = "com.nononsenseapps.notepad.sync.SYNC_RESULT";
    public static final int SUCCESS = 0;
    public static final int LOGIN_FAIL = 1;
    public static final int ERROR = 2;

    private static final String PREFS_LAST_SYNC_ETAG = "lastserveretag";
    private static final String PREFS_LAST_SYNC_DATE = "lastsyncdate";

    private final AccountManager accountManager;

    private final Context mContext;

    public SyncAdapter(Context context, boolean autoInitialize) {
        super(context, autoInitialize);
        mContext = context;
        // mAccountManager = AccountManager.get(context);
        accountManager = AccountManager.get(context);
    }

    @SuppressWarnings("unchecked")
    @Override
    public void onPerformSync(Account account, Bundle extras, String authority, ContentProviderClient provider,
            SyncResult syncResult) {
        final SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(mContext);

        // Only sync if it has been enabled by the user, and account is selected
        // Issue on reinstall where account approval is remembered by system
        if (settings.getBoolean(SyncPrefs.KEY_SYNC_ENABLE, false)
                && !settings.getString(SyncPrefs.KEY_ACCOUNT, "").isEmpty()
                && account.name.equals(settings.getString(SyncPrefs.KEY_ACCOUNT, ""))) {

            if (SYNC_DEBUG_PRINTS)
                Log.d(TAG, "onPerformSync");
            Intent i = new Intent(SYNC_STARTED);
            mContext.sendBroadcast(i);
            // For later
            Intent doneIntent = new Intent(SYNC_FINISHED);
            doneIntent.putExtra(SYNC_RESULT, ERROR);

            // Initialize necessary stuff
            GoogleDBTalker dbTalker = new GoogleDBTalker(account.name, provider);
            GoogleAPITalker apiTalker = new GoogleAPITalker();

            try {
                boolean connected = apiTalker.initialize(accountManager, account, AUTH_TOKEN_TYPE,
                        NOTIFY_AUTH_FAILURE);

                if (connected) {
                    if (SYNC_DEBUG_PRINTS)
                        Log.d(TAG, "We got an authToken atleast");

                    try {
                        // FIrst of all, we need the latest updated time later.
                        // So
                        // save
                        // that for now.
                        // This is the latest time we synced
                        String lastUpdate = dbTalker.getLastUpdated(account.name);
                        //String lastUpdate = settings.getString(PREFS_LAST_SYNC_DATE, null);
                        // Get the latest hash value we saw on the server
                        String localEtag = settings.getString(PREFS_LAST_SYNC_ETAG, "");

                        // Prepare lists for items
                        ArrayList<GoogleTaskList> listsToSaveToDB = new ArrayList<GoogleTaskList>();
                        HashMap<GoogleTaskList, ArrayList<GoogleTask>> tasksInListToSaveToDB = new HashMap<GoogleTaskList, ArrayList<GoogleTask>>();

                        HashMap<Long, ArrayList<GoogleTask>> tasksInListToUpload = new HashMap<Long, ArrayList<GoogleTask>>();
                        HashMap<Long, ArrayList<GoogleTask>> allTasksInList = new HashMap<Long, ArrayList<GoogleTask>>();

                        // gets all tasks in one query
                        ArrayList<GoogleTask> allTasks = dbTalker.getAllTasks(allTasksInList, tasksInListToUpload);

                        ArrayList<GoogleTaskList> listsToUpload = new ArrayList<GoogleTaskList>();
                        ArrayList<GoogleTaskList> allLocalLists = new ArrayList<GoogleTaskList>();

                        // gets all lists in one query
                        dbTalker.getAllLists(allLocalLists, listsToUpload);

                        // Get the current hash value on the server and all
                        // remote
                        // lists if upload is not true

                        String serverEtag = apiTalker.getModifiedLists(localEtag, allLocalLists, listsToSaveToDB);

                        // IF the tags match, then nothing has changed on
                        // server.
                        if (localEtag.equals(serverEtag)) {
                            if (SYNC_DEBUG_PRINTS)
                                Log.d(TAG, "Etags match, nothing to download");
                        } else {
                            if (SYNC_DEBUG_PRINTS)
                                Log.d(TAG, "Etags dont match, downloading new tasks");
                            // Download tasks which have been updated since last
                            // time
                            for (GoogleTaskList list : listsToSaveToDB) {
                                if (list.id != null && !list.id.isEmpty()) {
                                    if (SYNC_DEBUG_PRINTS)
                                        Log.d(TAG, "Saving remote modified tasks for: " + list.id);
                                    tasksInListToSaveToDB.put(list,
                                            list.downloadModifiedTasks(apiTalker, allTasks, lastUpdate));
                                }
                            }
                        }

                        if (SYNC_DEBUG_PRINTS)
                            Log.d(TAG, "Getting stuff we want to upload");
                        // Get stuff we would like to upload to server
                        // In case of lists, locally modified versions always
                        // wins
                        // in
                        // conflict, so nothing more to do

                        for (GoogleTaskList list : allLocalLists) {
                            ArrayList<GoogleTask> moddedTasks = tasksInListToUpload.get(list.dbId);
                            if (moddedTasks != null && !moddedTasks.isEmpty()) {
                                // There are some tasks here which we want to
                                // upload
                                if (SYNC_DEBUG_PRINTS)
                                    Log.d(TAG, "List id " + list.dbId + ", Locally modified tasks found: "
                                            + moddedTasks.size());

                                // Now we need to handle possible conflicts in
                                // the
                                // tasks. But this has already been sorted when
                                // we
                                // downloaded them
                                // For any task which exists in stuffToSaveToDB,
                                // we
                                // should not upload it
                                // Iterate over a clone to avoid concurrency
                                // problems since we will be modifying
                                // the list during iteration
                                for (GoogleTask moddedTask : (ArrayList<GoogleTask>) moddedTasks.clone()) {
                                    ArrayList<GoogleTask> tasksToBeSaved = tasksInListToSaveToDB.get(list);
                                    if (tasksToBeSaved != null && tasksToBeSaved.contains(moddedTask)) {
                                        if (SYNC_DEBUG_PRINTS)
                                            Log.d(TAG,
                                                    "This modified task was newer on server, removing from upload list: "
                                                            + moddedTask.title);
                                        moddedTasks.remove(moddedTask);
                                    }
                                    // In the case that a task has been deleted
                                    // before it was synced the first time
                                    // We should definitely not sync it. Only
                                    // delete
                                    // it later
                                    if (moddedTask.deleted == 1
                                            && (moddedTask.id == null || moddedTask.id.isEmpty())) {
                                        moddedTasks.remove(moddedTask);
                                    }
                                }
                            }
                        }

                        if (SYNC_DEBUG_PRINTS)
                            Log.d(TAG, "Uploading lists");
                        // First thing we want to do is upload stuff, because
                        // some
                        // values are updated then
                        boolean uploadedStuff = false;
                        // Start with lists
                        for (GoogleTaskList list : listsToUpload) {
                            GoogleTaskList result = apiTalker.uploadList(list);
                            uploadedStuff = true;
                            if (result != null) {
                                // Make sure that local version is the same as
                                // server's
                                for (GoogleTaskList localList : allLocalLists) {
                                    if (result.equals(localList)) {
                                        localList.title = result.title;
                                        localList.id = result.id;
                                        result.dbId = localList.dbId;
                                        break;
                                    }
                                }
                                listsToSaveToDB.add(result);
                            }
                        }

                        if (SYNC_DEBUG_PRINTS)
                            Log.d(TAG, "Uploading tasks");
                        // Right, now upload tasks
                        for (GoogleTaskList list : allLocalLists) {
                            ArrayList<GoogleTask> tasksToUpload = tasksInListToUpload.get(list.dbId);
                            if (tasksToUpload != null) {
                                for (GoogleTask task : tasksToUpload) {
                                    GoogleTask result = apiTalker.uploadTask(task, list);
                                    uploadedStuff = true;
                                    // Task now has relevant fields set. Add to
                                    // DB-list
                                    if (tasksInListToSaveToDB.get(list) == null)
                                        tasksInListToSaveToDB.put(list, new ArrayList<GoogleTask>());
                                    tasksInListToSaveToDB.get(list).add(result);
                                }
                            }
                        }

                        // Finally, get the updated etag from the server and
                        // save.
                        // Only worth doing if we actually uploaded anything
                        // Also, only do this if we are doing a full sync
                        String currentEtag = serverEtag;
                        if (uploadedStuff) {
                            currentEtag = apiTalker.getEtag();
                            //lastUpdate = dbTalker.getLastUpdated(account.name);
                        }

                        settings.edit().putString(PREFS_LAST_SYNC_ETAG, currentEtag)
                                //.putString(PREFS_LAST_SYNC_DATE, lastUpdate)
                                .commit();

                        // Now, set sorting values.
                        for (GoogleTaskList list : tasksInListToSaveToDB.keySet()) {
                            if (SYNC_DEBUG_PRINTS)
                                Log.d(TAG, "Setting position values in: " + list.id);
                            ArrayList<GoogleTask> tasks = tasksInListToSaveToDB.get(list);
                            if (tasks != null) {
                                if (SYNC_DEBUG_PRINTS)
                                    Log.d(TAG, "Setting position values for #tasks: " + tasks.size());
                                ArrayList<GoogleTask> allListTasks = allTasksInList.get(list.dbId);
                                list.setSortingValues(tasks, allListTasks);
                            }
                        }

                        // Save to database in a single transaction
                        if (SYNC_DEBUG_PRINTS)
                            Log.d(TAG, "Save stuff to DB");
                        dbTalker.SaveToDatabase(listsToSaveToDB, tasksInListToSaveToDB);
                        // Commit it
                        ContentProviderResult[] result = dbTalker.apply();

                        if (SYNC_DEBUG_PRINTS)
                            Log.d(TAG, "Sync Complete!");
                        doneIntent.putExtra(SYNC_RESULT, SUCCESS);

                    } catch (ClientProtocolException e) {
                        if (SYNC_DEBUG_PRINTS)
                            Log.d(TAG, "ClientProtocolException: " + e.getLocalizedMessage());
                    } catch (JSONException e) {
                        if (SYNC_DEBUG_PRINTS)
                            Log.d(TAG, "JSONException: " + e.getLocalizedMessage());
                    } catch (IOException e) {
                        syncResult.stats.numIoExceptions++;
                        if (SYNC_DEBUG_PRINTS)
                            Log.d(TAG, "IOException: " + e.getLocalizedMessage());
                    } catch (RemoteException e) {
                        if (SYNC_DEBUG_PRINTS)
                            Log.d(TAG, "RemoteException: " + e.getLocalizedMessage());
                    } catch (OperationApplicationException e) {
                        Log.d(TAG, "Joined operation failed: " + e.getLocalizedMessage());
                    } catch (ClassCastException e) {
                        // GetListofLists will cast this if it returns a string. It should not return a string
                        // but it did...
                        Log.d(TAG, "ClassCastException: " + e.getLocalizedMessage());
                    }

                } else {
                    // return real failure
                    if (SYNC_DEBUG_PRINTS)
                        Log.d(TAG, "Could not get authToken. Reporting authException");
                    syncResult.stats.numAuthExceptions++;
                    doneIntent.putExtra(SYNC_RESULT, LOGIN_FAIL);
                }

            } finally {
                // This must always be called or we will leak resources
                if (apiTalker != null) {
                    apiTalker.closeClient();
                }

                mContext.sendBroadcast(doneIntent);

                if (SYNC_DEBUG_PRINTS)
                    Log.d(TAG, "SyncResult: " + syncResult.toDebugString());
            }
        }
    }
}