com.jdom.get.stuff.done.android.AndroidSyncStrategy.java Source code

Java tutorial

Introduction

Here is the source code for com.jdom.get.stuff.done.android.AndroidSyncStrategy.java

Source

/** 
 *  Copyright (C) 2012  Just Do One More
 *  
 *  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.jdom.get.stuff.done.android;

import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TimeZone;
import java.util.concurrent.atomic.AtomicBoolean;

import org.apache.commons.lang3.StringUtils;

import android.accounts.Account;
import android.accounts.AccountManager;
import android.accounts.AccountManagerFuture;
import android.app.Activity;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.os.Bundle;
import android.util.Log;

import com.google.api.client.googleapis.auth.oauth2.draft10.GoogleAccessProtectedResource;
import com.google.api.client.googleapis.extensions.android2.auth.GoogleAccountManager;
import com.google.api.client.http.HttpTransport;
import com.google.api.client.http.apache.ApacheHttpTransport;
import com.google.api.client.http.json.JsonHttpRequest;
import com.google.api.client.http.json.JsonHttpRequestInitializer;
import com.google.api.client.json.jackson.JacksonFactory;
import com.google.api.client.util.DateTime;
import com.google.api.services.tasks.Tasks;
import com.google.api.services.tasks.Tasks.Builder;
import com.google.api.services.tasks.TasksRequest;
import com.google.api.services.tasks.model.TaskList;
import com.google.api.services.tasks.model.TaskLists;
import com.jdom.get.stuff.done.domain.Constants;
import com.jdom.get.stuff.done.domain.Task;
import com.jdom.get.stuff.done.model.dao.ApplicationDao;
import com.jdom.get.stuff.done.presenter.SyncOption;
import com.jdom.get.stuff.done.sync.GoogleSynchronizeStrategy;
import com.jdom.get.stuff.done.sync.SynchronizeStrategy;

public class AndroidSyncStrategy extends GoogleSynchronizeStrategy implements SynchronizeStrategy {

    private static final String CLASS_NAME = AndroidSyncStrategy.class.getName();

    private static AtomicBoolean running = new AtomicBoolean(false);

    private final Activity activity;

    private final AndroidApplicationContextFactory contextFactory;

    public AndroidSyncStrategy(Activity activity,
            AndroidApplicationContextFactory androidApplicationContextFactory) {
        super(androidApplicationContextFactory.getDaoFactory().getApplicationDao());
        this.activity = activity;
        this.contextFactory = androidApplicationContextFactory;
    }

    public void synchronize() {
        synchronize(false, null);
    }

    public void synchronizeSynchronously(Runnable afterCompletionAction) {
        synchronize(true, afterCompletionAction);
    }

    public void synchronizeIfScheduledTimeHasElapsed() {
        long lastSyncTime = contextFactory.getDaoFactory().getApplicationDao().getLastSyncTime();
        long timeSinceLastSync = System.currentTimeMillis() - lastSyncTime;
        boolean syncRequired = Constants.SYNC_FREQUENCY < timeSinceLastSync;
        Log.d(CLASS_NAME, "Sync required: " + syncRequired + ".  Last synced [" + timeSinceLastSync + "] ms ago.");
        if (syncRequired) {
            synchronizeIfEnabled(SyncOption.PERIODICALLY);
        }
    }

    private void synchronize(boolean synchronously, final Runnable afterCompletionAction) {
        // Shortcut if there is no account configured for syncing
        if (contextFactory.getDaoFactory().getApplicationDao().getSyncAccount() == null) {
            return;
        }

        if (!running.compareAndSet(false, true)) {
            return;
        }

        Thread t = new Thread() {
            @Override
            public void run() {
                try {
                    synchronizeWithRemote();
                } finally {
                    if (afterCompletionAction != null) {
                        afterCompletionAction.run();
                    }
                }
            }
        };
        t.start();
    }

    @Override
    public void synchronizeWithRemote() {
        long start = System.currentTimeMillis();
        Log.i(CLASS_NAME, "Started syncing with remote server at " + new SimpleDateFormat().format(new Date()));
        try {
            Tasks service = getTasksService();
            ApplicationDao dao = contextFactory.getDaoFactory().getApplicationDao();

            Map<String, List<com.google.api.services.tasks.model.Task>> listNameToTasks = new HashMap<String, List<com.google.api.services.tasks.model.Task>>();
            Map<String, String> listNameToRemoteIdentifier = new HashMap<String, String>();
            Map<String, TaskList> listNameToTaskList = new HashMap<String, TaskList>();
            Map<String, String> taskIdToListId = new HashMap<String, String>();
            Map<String, String> taskNameToRemoteIdentifier = new HashMap<String, String>();

            long portionStart = System.currentTimeMillis();
            TaskLists taskLists = service.tasklists().list().execute();
            AndroidSyncStrategy.logTimeTaken("Retrieving task lists", portionStart);
            TaskList defaultTaskList = service.tasklists().get(Constants.GOOGLE_DEFAULT_LIST_REMOTE_ID).execute();
            Set<com.jdom.get.stuff.done.domain.TaskList> localTaskLists = dao.getTaskLists();
            Set<Task> localTasks = dao.getTasks();
            List<TaskList> googleTaskLists = taskLists.getItems();

            for (TaskList googleTaskList : googleTaskLists) {
                String googleListTitle = googleTaskList.getTitle();
                String remoteId = googleTaskList.getId();
                if (StringUtils.isEmpty(googleListTitle)) {
                    continue;
                } else if (googleListTitle.equals(defaultTaskList.getTitle())) {
                    remoteId = Constants.GOOGLE_DEFAULT_LIST_REMOTE_ID;
                }
                listNameToRemoteIdentifier.put(googleListTitle, remoteId);
                listNameToTaskList.put(googleListTitle, googleTaskList);
                List<com.google.api.services.tasks.model.Task> googleTasksForList = new ArrayList<com.google.api.services.tasks.model.Task>();

                portionStart = System.currentTimeMillis();
                List<com.google.api.services.tasks.model.Task> googleTasks = service.tasks().list(remoteId)
                        .execute().getItems();
                AndroidSyncStrategy.logTimeTaken("Retrieving tasks for list " + googleListTitle, portionStart);
                if (googleTasks == null || googleTasks.isEmpty()) {
                    continue;
                }

                for (com.google.api.services.tasks.model.Task googleTask : googleTasks) {
                    String taskTitle = googleTask.getTitle();
                    if (StringUtils.isEmpty(taskTitle)) {
                        continue;
                    }
                    taskNameToRemoteIdentifier.put(taskTitle, googleTask.getId());
                    taskIdToListId.put(googleTask.getId(), remoteId);
                    googleTasksForList.add(googleTask);
                }
                listNameToTasks.put(googleListTitle, googleTasksForList);
            }

            // Check for any lists that were renamed remotely
            for (String listName : listNameToRemoteIdentifier.keySet()) {
                String remoteId = listNameToRemoteIdentifier.get(listName);

                com.jdom.get.stuff.done.domain.TaskList localVersion = findLocalTaskListWithRemoteId(remoteId,
                        localTaskLists);
                // No local version, create one
                if (localVersion == null) {
                    long localStart = System.currentTimeMillis();
                    com.jdom.get.stuff.done.domain.TaskList localTaskList = new com.jdom.get.stuff.done.domain.TaskList(
                            listName);
                    localTaskList.setRemoteIdentifier(remoteId);
                    localTaskLists.add(localTaskList);
                    dao.addTaskList(localTaskList);
                    AndroidSyncStrategy.logTimeTaken("Create local list from remote", localStart);
                } else if (!Constants.DEFAULT_LIST.equals(localVersion.getName())
                        && !localVersion.getName().equals(listName)) {
                    long lastSyncTime = dao.getLastSyncTime();
                    // If hasn't been updated since last sync, then was renamed
                    // remotely
                    if (localVersion.getLastUpdatedTime() <= lastSyncTime) {
                        long localStart = System.currentTimeMillis();
                        com.jdom.get.stuff.done.domain.TaskList newVersion = localVersion.clone();
                        newVersion.setName(listName);
                        dao.updateTaskList(localVersion, newVersion);
                        AndroidSyncStrategy.logTimeTaken("Update local list from remote", localStart);
                    } else {
                        long localStart = System.currentTimeMillis();
                        // If has been updated since last sync, then was renamed
                        // locally
                        TaskList googleTaskList = listNameToTaskList.get(listName);
                        googleTaskList.setTitle(localVersion.getName());
                        service.tasklists().update(remoteId, googleTaskList).execute();
                        AndroidSyncStrategy.logTimeTaken("Update remote list from local", localStart);
                    }
                }
            }

            Iterator<com.jdom.get.stuff.done.domain.TaskList> iter = localTaskLists.iterator();
            List<com.jdom.get.stuff.done.domain.TaskList> updatedList = new ArrayList<com.jdom.get.stuff.done.domain.TaskList>();
            while (iter.hasNext()) {
                com.jdom.get.stuff.done.domain.TaskList localTaskList = iter.next();
                String remoteId = localTaskList.getRemoteIdentifier();
                // Any locally created lists that need a remote version created
                if (remoteId == null) {
                    TaskList remote = new TaskList();
                    remote.setTitle(localTaskList.getName());
                    TaskList created = service.tasklists().insert(remote).execute();
                    com.jdom.get.stuff.done.domain.TaskList updated = localTaskList.clone();
                    updated.setRemoteIdentifier(created.getId());
                    dao.updateTaskList(localTaskList, updated);
                    updatedList.add(updated);
                    iter.remove();
                }
                // Any local task list with remote identifier that doesn't have
                // remote version were deleted remotely
                else if (!listNameToRemoteIdentifier.containsValue(remoteId)) {
                    long localStart = System.currentTimeMillis();
                    dao.deleteTaskList(localTaskList);
                    iter.remove();
                    AndroidSyncStrategy.logTimeTaken("Delete local task list because remote was", localStart);
                }
                // Flagged for deletion, remove remote version and local
                else if (localTaskList.isDeleted()) {
                    long localStart = System.currentTimeMillis();
                    service.tasklists().delete(remoteId).execute();
                    dao.deleteTaskList(localTaskList);
                    iter.remove();
                    AndroidSyncStrategy.logTimeTaken("Delete remote task list because local was", localStart);
                }
            }

            localTaskLists.addAll(updatedList);

            // Now check for tasks
            for (String listName : listNameToTasks.keySet()) {
                List<com.google.api.services.tasks.model.Task> googleTasksForList = listNameToTasks.get(listName);
                long lastSyncTime = dao.getLastSyncTime();

                for (com.google.api.services.tasks.model.Task googleTask : googleTasksForList) {
                    String remoteId = googleTask.getId();

                    com.jdom.get.stuff.done.domain.Task localVersion = findLocalTaskWithRemoteId(remoteId,
                            localTasks);
                    // No local version, create one
                    if (localVersion == null) {
                        long localStart = System.currentTimeMillis();
                        localVersion = createLocalTask(googleTask, listName);
                        localTasks.add(localVersion);
                        dao.addTask(localVersion);
                        AndroidSyncStrategy.logTimeTaken("Create local task from remote", localStart);
                    } else if (localVersion.getLastUpdatedTime() > lastSyncTime) {
                        long localStart = System.currentTimeMillis();
                        // If has been updated since last sync, then was
                        // changed locally
                        String originalRemoteId = localVersion.getRemoteIdentifier();
                        Task updated = createRemoteTask(service, dao, localVersion, listNameToRemoteIdentifier);
                        localVersion.setRemoteIdentifier(updated.getRemoteIdentifier());
                        taskNameToRemoteIdentifier.put(localVersion.getName(), updated.getRemoteIdentifier());

                        service.tasks().delete(listNameToRemoteIdentifier.get(listName), originalRemoteId)
                                .execute();
                        AndroidSyncStrategy.logTimeTaken("Update remote task from local", localStart);
                    }
                    // If hasn't been updated since last sync, then may
                    // have been changed remotely
                    else if (!localVersion.equals(googleTask, listName)) {
                        long localStart = System.currentTimeMillis();
                        com.jdom.get.stuff.done.domain.Task newVersion = createLocalTask(googleTask, listName);
                        dao.updateTask(localVersion, newVersion);
                        AndroidSyncStrategy.logTimeTaken("Update local task from remote", localStart);
                    }
                }
            }

            Iterator<com.jdom.get.stuff.done.domain.Task> taskIter = dao.getTasks().iterator();
            List<com.jdom.get.stuff.done.domain.Task> taskUpdatedList = new ArrayList<com.jdom.get.stuff.done.domain.Task>();
            while (taskIter.hasNext()) {
                com.jdom.get.stuff.done.domain.Task localTask = taskIter.next();
                String remoteId = localTask.getRemoteIdentifier();
                // If remoteId is null, then this is a local task that needs a
                // remote created
                if (remoteId == null) {
                    long localStart = System.currentTimeMillis();
                    Task updated = createRemoteTask(service, dao, localTask, listNameToRemoteIdentifier);

                    taskUpdatedList.add(updated);
                    taskIter.remove();
                    AndroidSyncStrategy.logTimeTaken("Create remote task from local", localStart);
                }
                // Any local task with remote identifier that doesn't have
                // remote version were deleted remotely
                else if (!taskNameToRemoteIdentifier.containsValue(remoteId)) {
                    long localStart = System.currentTimeMillis();
                    dao.deleteTask(localTask);
                    taskIter.remove();
                    AndroidSyncStrategy.logTimeTaken("Delete local task because remote was", localStart);
                }
                // Flagged for deletion, remove remote version and local
                else if (localTask.isDeleted()) {
                    long localStart = System.currentTimeMillis();
                    service.tasks().delete(taskIdToListId.get(remoteId), remoteId).execute();
                    dao.deleteTask(localTask);
                    taskIter.remove();
                    AndroidSyncStrategy.logTimeTaken("Delete remote task because local was", localStart);
                }
            }

            localTasks.addAll(taskUpdatedList);

            // Update the last sync time
            dao.setLastSyncTime(System.currentTimeMillis());
        } catch (Exception e) {
            handleException(e);
        } finally {
            AndroidSyncStrategy.logTimeTaken("Finished syncing with remote server", start);
            running.set(false);
        }
    }

    private static void logTimeTaken(String action, long portionStart) {
        Log.d(CLASS_NAME, action + " [" + (System.currentTimeMillis() - portionStart) + "] ms.");
    }

    private Task createLocalTask(com.google.api.services.tasks.model.Task googleTask, String listName) {
        Boolean deleted = Boolean.TRUE.equals(googleTask.getDeleted());

        Task localVersion = new com.jdom.get.stuff.done.domain.Task(googleTask.getTitle(), googleTask.getNotes());
        localVersion.setDeleted(deleted);
        localVersion.setListName(listName);
        localVersion.setCompleted(Constants.GOOGLE_COMPLETED_STRING.equals(googleTask.getStatus()));
        localVersion.setDescription(googleTask.getNotes());
        DateTime due = googleTask.getDue();
        if (due != null) {
            long utcTime = due.getValue();
            utcTime -= TimeZone.getDefault().getOffset(utcTime);
            localVersion.setDueDate(new Date(utcTime));
        }
        localVersion.setRemoteIdentifier(googleTask.getId());

        return localVersion;
    }

    private Task createRemoteTask(Tasks service, ApplicationDao dao, Task localTask,
            Map<String, String> listNameToRemoteIdentifier) throws IOException {
        com.google.api.services.tasks.model.Task newGoogleTask = new com.google.api.services.tasks.model.Task();
        newGoogleTask.setTitle(localTask.getName());
        newGoogleTask.setDeleted(localTask.isDeleted());
        newGoogleTask.setStatus(
                localTask.isCompleted() ? Constants.GOOGLE_COMPLETED_STRING : Constants.GOOGLE_INCOMPLETE_STRING);
        newGoogleTask.setNotes(localTask.getDescription());
        long time = localTask.getDueDate().getTime();
        time -= TimeZone.getDefault().getOffset(time);
        newGoogleTask.setDue(new DateTime(false, time, 0));

        String listName = localTask.getListName();
        String remoteId = listNameToRemoteIdentifier.get(listName);
        if (Constants.DEFAULT_LIST.equals(listName)) {
            remoteId = Constants.GOOGLE_DEFAULT_LIST_REMOTE_ID;
        }
        com.google.api.services.tasks.model.Task created = service.tasks().insert(remoteId, newGoogleTask)
                .execute();
        com.jdom.get.stuff.done.domain.Task updated = localTask.clone();
        localTask.setRemoteIdentifier(created.getId());
        updated.setRemoteIdentifier(created.getId());
        dao.updateTask(localTask, updated);

        return updated;
    }

    private Task findLocalTaskWithRemoteId(String remoteId, Set<Task> localTasks) {
        for (Task task : localTasks) {
            if (remoteId.equals(task.getRemoteIdentifier())) {
                return task;
            }
        }
        return null;
    }

    private com.jdom.get.stuff.done.domain.TaskList findLocalTaskListWithRemoteId(String remoteId,
            Set<com.jdom.get.stuff.done.domain.TaskList> localTaskLists) {
        for (com.jdom.get.stuff.done.domain.TaskList taskList : localTaskLists) {
            if (remoteId.equals(taskList.getRemoteIdentifier())) {
                return taskList;
            }
        }
        return null;
    }

    @Override
    protected List<TaskList> getRemoteTaskListsInternal() throws IOException {
        TaskLists taskLists = getTasksService().tasklists().list().execute();
        return taskLists.getItems();
    }

    @Override
    protected Tasks getTasksService() {
        HttpTransport transport = new ApacheHttpTransport();

        GoogleTasksInitializer initializer = new GoogleTasksInitializer(getToken());

        Builder builder = Tasks.builder(transport, new JacksonFactory());
        builder.setApplicationName(Constants.APP_NAME);
        builder.setHttpRequestInitializer(initializer);
        builder.setJsonHttpRequestInitializer(initializer);

        return builder.build();
    }

    private Account getAccount() {
        Account[] accounts = AccountManager.get(activity).getAccountsByType(Constants.GOOGLE_ACCOUNT_PREFIX);
        for (Account account : accounts) {
            if (account.name.equals(contextFactory.getDaoFactory().getApplicationDao().getSyncAccount())) {
                return account;
            }
        }
        return null;
    }

    private String getToken() {
        long start = System.currentTimeMillis();
        SharedPreferences prefs = activity.getSharedPreferences(Constants.AUTH_TOKEN_TYPE, Activity.MODE_PRIVATE);
        String token = prefs.getString(Constants.AUTH_TOKEN_TYPE, null);
        long lastTokenRetrievalTime = prefs.getLong(Constants.TOKEN_RETRIEVAL_TIME, 0);

        long timeSinceRetrievedLastToken = System.currentTimeMillis() - lastTokenRetrievalTime;
        long timeStillValid = Constants.TOKEN_VALID_DURATION - timeSinceRetrievedLastToken;
        if (token == null || timeStillValid < 1) {
            long portionStart = System.currentTimeMillis();
            Log.d(CLASS_NAME, "Token is no longer valid.");
            GoogleAccountManager accountManager = new GoogleAccountManager(activity);
            Account account = getAccount();

            accountManager.manager.invalidateAuthToken(account.type, token);
            AccountManagerFuture<Bundle> future = accountManager.manager.getAuthToken(account,
                    Constants.AUTH_TOKEN_TYPE, null, activity, null, null);
            try {
                Bundle result = future.getResult();
                token = result.getString(AccountManager.KEY_AUTHTOKEN);

                Editor editor = prefs.edit();
                editor.putString(Constants.AUTH_TOKEN_TYPE, token);
                editor.putLong(Constants.TOKEN_RETRIEVAL_TIME, System.currentTimeMillis());
                editor.commit();

            } catch (Exception e) {
                handleException(e);
            }
            AndroidSyncStrategy.logTimeTaken("Retrieving new token", portionStart);
        } else {
            Log.d(CLASS_NAME, "Token is still valid for [" + timeStillValid + "] ms.");
        }

        AndroidSyncStrategy.logTimeTaken("Retrieving token", start);
        return token;
    }

    @Override
    protected void handleException(Exception e) {
        Log.e(CLASS_NAME, "Caught exception while syncing!", e);
    }

    private static final class GoogleTasksInitializer extends GoogleAccessProtectedResource
            implements JsonHttpRequestInitializer {
        public GoogleTasksInitializer(String accessToken) {
            super(accessToken);
        }

        public void initialize(JsonHttpRequest request) throws IOException {
            TasksRequest tasksRequest = (TasksRequest) request;
            tasksRequest.setPrettyPrint(true);
            tasksRequest.setKey(Constants.API_KEY);
        }
    }

    public void synchronizeIfEnabled(SyncOption syncOption) {
        Integer syncOptionConfiguration = contextFactory.getDaoFactory().getApplicationDao()
                .getSyncOptionsConfiguration();
        if (syncOption.isEnabled(syncOptionConfiguration)) {
            Log.d(CLASS_NAME, "Synchronizing because [" + syncOption + "] is enabled.");
            synchronize();
        } else {
            Log.d(CLASS_NAME, "Not synchronizing because [" + syncOption + "] is disabled.");
        }
    }
}