diff --git a/res/layout/sync_footer.xml b/res/layout/sync_footer.xml index ea94ebb44..fbff6c8ea 100644 --- a/res/layout/sync_footer.xml +++ b/res/layout/sync_footer.xml @@ -28,8 +28,20 @@ android:layout_height="wrap_content"> + + + 2 + + + disable + twice an hour + hourly + twice a day + daily + twice a week + weekly + + + + + 0 + 1800 + 3600 + 43200 + 86400 + 302400 + 604800 + diff --git a/res/values/strings.xml b/res/values/strings.xml index 8e3da932b..2e2fb0e28 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -248,11 +248,12 @@ If you don\'t want to see the new task right after you complete the old one, you Actions Options sync_rtm - Remember The Milk + Remember The Milk http://www.rememberthemilk.com - sync_every - Synchronize Frequency - If set, perform sync every # hours + sync_every + sync_freq + Auto-Synchronize + If set, synchronization occurs automatically given interval sync_button Main Menu Shortcut Show \"Synchronize\" in Astrid\'s menu @@ -281,6 +282,16 @@ Wish me luck!\n Clear Personal Data Clear data for selected services? No Synchronizers Enabled! + Last Sync Date: %s + Last AutoSync Attempt: %s + never + %s Results + Summary - Astrid Tasks: + Summary - Remote Server: + Created: %d + Updated: %d + Deleted: %d + Merged: %d diff --git a/res/xml/sync_preferences.xml b/res/xml/sync_preferences.xml index deb15db82..4417a3d4d 100644 --- a/res/xml/sync_preferences.xml +++ b/res/xml/sync_preferences.xml @@ -15,10 +15,12 @@ - + (); for(TagModelForView tag : tagArray) { LinkedList tasks = getTagController().getTaggedTasks( - getParent(), tag.getTagIdentifier()); + tag.getTagIdentifier()); int count = 0; for(TaskIdentifier task : tasks) if(activeTasks.contains(task)) @@ -156,7 +156,7 @@ public class TagListSubActivity extends SubActivity { /** Fill in the Tag List with our tags */ private synchronized void fillData() { try { - tagArray = getTagController().getAllTags(getParent()); + tagArray = getTagController().getAllTags(); sortTagArray(); } catch (StaleDataException e) { // happens when you rotate the screen while the thread is diff --git a/src/com/timsu/astrid/activities/TaskEdit.java b/src/com/timsu/astrid/activities/TaskEdit.java index c078636d7..1eb3e0131 100644 --- a/src/com/timsu/astrid/activities/TaskEdit.java +++ b/src/com/timsu/astrid/activities/TaskEdit.java @@ -216,9 +216,9 @@ public class TaskEdit extends TaskModificationTabbedActivity { addToCalendar.setText(r.getString(R.string.showCalendar_label)); // tags - tags = tagController.getAllTags(this); + tags = tagController.getAllTags(); if(model.getTaskIdentifier() != null) { - taskTags = tagController.getTaskTags(this, model.getTaskIdentifier()); + taskTags = tagController.getTaskTags(model.getTaskIdentifier()); if(taskTags.size() > 0) { Map tagsMap = new HashMap(); @@ -286,7 +286,7 @@ public class TaskEdit extends TaskModificationTabbedActivity { saveTags(); saveAlerts(); Notifications.updateAlarm(this, controller, alertController, model); - + Date dueDate = model.getPreferredDueDate(); if (dueDate == null) { dueDate = model.getDefiniteDueDate(); @@ -296,33 +296,33 @@ public class TaskEdit extends TaskModificationTabbedActivity { } else { showSaveToast(); } - + } catch (Exception e) { Log.e("astrid", "Error saving", e); } } /** - * Displays a Toast reporting that the selected task has been saved and is + * Displays a Toast reporting that the selected task has been saved and is * due in 'x' amount of time, to 2 time-units of precision (e.g. Days + Hours). - * @param dueDate the Date when the task is due + * @param dueDate the Date when the task is due */ private void showSaveToast(Date dueDate) { int timeInSeconds = (int)((dueDate.getTime() - System.currentTimeMillis())/1000L); String formattedDate = DateUtilities.getDurationString(getResources(), timeInSeconds, 2); - Toast.makeText(this, + Toast.makeText(this, getResources().getString(R.string.taskEdit_onTaskSave_Due, formattedDate), Toast.LENGTH_SHORT).show(); } - + /** * Displays a Toast reporting that the selected task has been saved. * Use this version when no due Date has been set. */ private void showSaveToast() { Toast.makeText(this, R.string.taskEdit_onTaskSave_notDue, Toast.LENGTH_SHORT).show(); - } - + } + /** Save task tags. Must be called after task already has an ID */ private void saveTags() { Set tagsToDelete; diff --git a/src/com/timsu/astrid/activities/TaskList.java b/src/com/timsu/astrid/activities/TaskList.java index b06aeeab8..6bd4c72dc 100644 --- a/src/com/timsu/astrid/activities/TaskList.java +++ b/src/com/timsu/astrid/activities/TaskList.java @@ -19,8 +19,6 @@ */ package com.timsu.astrid.activities; -import java.util.Date; - import android.app.Activity; import android.content.Intent; import android.os.Bundle; @@ -41,7 +39,6 @@ import com.timsu.astrid.data.tag.TagController; import com.timsu.astrid.data.task.TaskController; import com.timsu.astrid.sync.Synchronizer; import com.timsu.astrid.utilities.Constants; -import com.timsu.astrid.utilities.Preferences; import com.timsu.astrid.utilities.StartupReceiver; /** @@ -128,20 +125,11 @@ public class TaskList extends Activity { getCurrentSubActivity().onDisplay(variables); } - // auto sync if requested - Float autoSyncHours = Preferences.autoSyncFrequency(this); + // sync now if requested if(synchronizeNow) { - synchronizeNow = false; Synchronizer.synchronize(this, true, null); - } else if(autoSyncHours != null) { - final Date lastSync = Preferences.getSyncLastSync(this); - - if(lastSync == null || lastSync.getTime() + - 1000L*3600*autoSyncHours < System.currentTimeMillis()) { - Synchronizer.synchronize(this, true, null); - } } - + // if we have no filter tag, we're not on the last task if(getCurrentSubActivity() == taskListWTag && ((TaskListSubActivity)taskListWTag).getFilterTag() == null) { diff --git a/src/com/timsu/astrid/activities/TaskListSubActivity.java b/src/com/timsu/astrid/activities/TaskListSubActivity.java index 4621ad463..d5e9d1dc5 100644 --- a/src/com/timsu/astrid/activities/TaskListSubActivity.java +++ b/src/com/timsu/astrid/activities/TaskListSubActivity.java @@ -59,6 +59,7 @@ import com.timsu.astrid.data.tag.TagModelForView; import com.timsu.astrid.data.task.TaskController; import com.timsu.astrid.data.task.TaskIdentifier; import com.timsu.astrid.data.task.TaskModelForList; +import com.timsu.astrid.sync.SynchronizationService; import com.timsu.astrid.sync.Synchronizer; import com.timsu.astrid.sync.Synchronizer.SynchronizerListener; import com.timsu.astrid.utilities.Constants; @@ -132,6 +133,9 @@ public class TaskListSubActivity extends SubActivity { // in another activity) static boolean shouldRefreshTaskList = false; + // indicator flag set if synchronization window has been opened & closed + static boolean syncPreferencesOpened = false; + // other instance variables class TaskListContext { Map tagMap; @@ -205,7 +209,7 @@ public class TaskListSubActivity extends SubActivity { // process tag to filter, if any if(variables != null && variables.containsKey(TAG_TOKEN)) { TagIdentifier identifier = new TagIdentifier(variables.getLong(TAG_TOKEN)); - context.tagMap = getTagController().getAllTagsAsMap(getParent()); + context.tagMap = getTagController().getAllTagsAsMap(); if(context.tagMap.containsKey(identifier)) context.filterTag = context.tagMap.get(identifier); else @@ -480,7 +484,7 @@ public class TaskListSubActivity extends SubActivity { // get a cursor to the task list Cursor tasksCursor; if(context.filterTag != null) { - LinkedList tasks = getTagController().getTaggedTasks(getParent(), + LinkedList tasks = getTagController().getTaggedTasks( context.filterTag.getTagIdentifier()); tasksCursor = getTaskController().getTaskListCursorById(tasks); } else { @@ -493,7 +497,7 @@ public class TaskListSubActivity extends SubActivity { context.taskArray = getTaskController().createTaskListFromCursor(tasksCursor); // read tags and apply filters - context.tagMap = getTagController().getAllTagsAsMap(getParent()); + context.tagMap = getTagController().getAllTagsAsMap(); context.taskTags = new HashMap(); StringBuilder tagBuilder = new StringBuilder(); context.tasksById = new HashMap(); @@ -515,7 +519,7 @@ public class TaskListSubActivity extends SubActivity { } // get list of tags - LinkedList tagIds = getTagController().getTaskTags(getParent(), + LinkedList tagIds = getTagController().getTaskTags( task.getTaskIdentifier()); tagBuilder.delete(0, tagBuilder.length()); for(Iterator j = tagIds.iterator(); j.hasNext(); ) { @@ -798,7 +802,14 @@ public class TaskListSubActivity extends SubActivity { if (hasFocus) { if (shouldRefreshTaskList) reloadList(); - else if (context.taskArray != null && + else if(syncPreferencesOpened) { + syncPreferencesOpened = false; + + // stop & start synchronization service + Intent service = new Intent(getParent(), SynchronizationService.class); + getParent().stopService(service); + getParent().startService(service); + } else if (context.taskArray != null && context.taskArray.size() > 0 && context.taskArray.size() < AUTO_REFRESH_MAX_LIST_SIZE) { @@ -1013,7 +1024,7 @@ public class TaskListSubActivity extends SubActivity { showTagsView(); return true; case SYNC_ID: - onActivityResult(ACTIVITY_SYNCHRONIZE, Constants.RESULT_SYNCHRONIZE, null); + onActivityResult(ACTIVITY_SYNCHRONIZE, Constants.RESULT_SYNCHRONIZE, null); return true; case MORE_ID: layout.showContextMenu(); @@ -1021,6 +1032,7 @@ public class TaskListSubActivity extends SubActivity { // --- more options menu items case OPTIONS_SYNC_ID: + syncPreferencesOpened = true; launchActivity(new Intent(getParent(), SyncPreferences.class), ACTIVITY_SYNCHRONIZE); return true; diff --git a/src/com/timsu/astrid/data/tag/TagController.java b/src/com/timsu/astrid/data/tag/TagController.java index be3697615..46afe5d60 100644 --- a/src/com/timsu/astrid/data/tag/TagController.java +++ b/src/com/timsu/astrid/data/tag/TagController.java @@ -41,19 +41,22 @@ public class TagController extends AbstractController { // --- tag batch operations /** Get a list of all tags */ - public LinkedList getAllTags(Activity activity) + public LinkedList getAllTags() throws SQLException { LinkedList list = new LinkedList(); Cursor cursor = tagDatabase.query(TAG_TABLE_NAME, TagModelForView.FIELD_LIST, null, null, null, null, null, null); - activity.startManagingCursor(cursor); - if(cursor.getCount() == 0) - return list; - do { - cursor.moveToNext(); - list.add(new TagModelForView(cursor)); - } while(!cursor.isLast()); + try { + if(cursor.getCount() == 0) + return list; + do { + cursor.moveToNext(); + list.add(new TagModelForView(cursor)); + } while(!cursor.isLast()); + } finally { + cursor.close(); + } return list; } @@ -61,47 +64,53 @@ public class TagController extends AbstractController { // --- tag to task map batch operations /** Get a list of all tags as an id => tag map */ - public HashMap getAllTagsAsMap(Activity activity) throws SQLException { + public HashMap getAllTagsAsMap() throws SQLException { HashMap map = new HashMap(); - for(TagModelForView tag : getAllTags(activity)) + for(TagModelForView tag : getAllTags()) map.put(tag.getTagIdentifier(), tag); return map; } /** Get a list of tag identifiers for the given task */ - public LinkedList getTaskTags(Activity activity, TaskIdentifier + public LinkedList getTaskTags(TaskIdentifier taskId) throws SQLException { LinkedList list = new LinkedList(); Cursor cursor = tagToTaskMapDatabase.query(TAG_TASK_MAP_NAME, TagToTaskMapping.FIELD_LIST, TagToTaskMapping.TASK + " = ?", new String[] { taskId.idAsString() }, null, null, null); - activity.startManagingCursor(cursor); - if(cursor.getCount() == 0) - return list; - do { - cursor.moveToNext(); - list.add(new TagToTaskMapping(cursor).getTag()); - } while(!cursor.isLast()); + try { + if(cursor.getCount() == 0) + return list; + do { + cursor.moveToNext(); + list.add(new TagToTaskMapping(cursor).getTag()); + } while(!cursor.isLast()); + } finally { + cursor.close(); + } return list; } /** Get a list of task identifiers for the given tag */ - public LinkedList getTaggedTasks(Activity activity, TagIdentifier + public LinkedList getTaggedTasks(TagIdentifier tagId) throws SQLException { LinkedList list = new LinkedList(); Cursor cursor = tagToTaskMapDatabase.query(TAG_TASK_MAP_NAME, TagToTaskMapping.FIELD_LIST, TagToTaskMapping.TAG + " = ?", new String[] { tagId.idAsString() }, null, null, null); - activity.startManagingCursor(cursor); - - if(cursor.getCount() == 0) - return list; - do { - cursor.moveToNext(); - list.add(new TagToTaskMapping(cursor).getTask()); - } while(!cursor.isLast()); + + try { + if(cursor.getCount() == 0) + return list; + do { + cursor.moveToNext(); + list.add(new TagToTaskMapping(cursor).getTask()); + } while(!cursor.isLast()); + } finally { + cursor.close(); + } return list; } diff --git a/src/com/timsu/astrid/sync/RTMSyncService.java b/src/com/timsu/astrid/sync/RTMSyncProvider.java similarity index 84% rename from src/com/timsu/astrid/sync/RTMSyncService.java rename to src/com/timsu/astrid/sync/RTMSyncProvider.java index c82444f73..be9e49fb3 100644 --- a/src/com/timsu/astrid/sync/RTMSyncService.java +++ b/src/com/timsu/astrid/sync/RTMSyncProvider.java @@ -27,8 +27,7 @@ import java.util.Map; import java.util.StringTokenizer; import java.util.Map.Entry; -import android.app.Activity; -import android.app.Dialog; +import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.res.Resources; @@ -56,14 +55,14 @@ import com.timsu.astrid.data.task.TaskModelForSync; import com.timsu.astrid.utilities.DialogUtilities; import com.timsu.astrid.utilities.Preferences; -public class RTMSyncService extends SynchronizationService { +public class RTMSyncProvider extends SynchronizationProvider { private ServiceImpl rtmService = null; private String INBOX_LIST_NAME = "Inbox"; Map listNameToIdMap = new HashMap(); Map listIdToNameMap = new HashMap(); - public RTMSyncService(int id) { + public RTMSyncProvider(int id) { super(id); } @@ -75,41 +74,27 @@ public class RTMSyncService extends SynchronizationService { } @Override - protected void synchronize(final Activity activity) { - if(Preferences.shouldSyncRTM(activity) && rtmService == null && - Preferences.getSyncRTMToken(activity) == null) { - DialogUtilities.okCancelDialog(activity, - activity.getResources().getString(R.string.sync_rtm_notes), - new Dialog.OnClickListener() { - public void onClick(DialogInterface dialog, int which) { - authenticate(activity); - } - }, new Dialog.OnClickListener() { - public void onClick(DialogInterface dialog, int which) { - if(progressDialog != null) - progressDialog.dismiss(); - } - }); - } else - authenticate(activity); + protected void synchronize(final Context activity) { + // authenticate the user. this will automatically call the next step + authenticate(activity); } @Override - public void clearPersonalData(Activity activity) { - Preferences.setSyncRTMToken(activity, null); - Preferences.setSyncRTMLastSync(activity, null); - Synchronizer.getSyncController(activity).deleteAllMappings(getId()); + public void clearPersonalData(Context context) { + Preferences.setSyncRTMToken(context, null); + Preferences.setSyncRTMLastSync(context, null); + Synchronizer.getSyncController(context).deleteAllMappings(getId()); } // --- authentication /** Perform authentication with RTM. Will open the SyncBrowser if necessary */ - private void authenticate(final Activity activity) { + private void authenticate(final Context context) { try { String apiKey = "bd9883b3384a21ead17501da38bb1e68"; String sharedSecret = "a19b2a020345219b"; String appName = null; - String authToken = Preferences.getSyncRTMToken(activity); + String authToken = Preferences.getSyncRTMToken(context); // check if we have a token & it works if(authToken != null) { @@ -125,8 +110,8 @@ public class RTMSyncService extends SynchronizationService { try { String token = rtmService.completeAuthorization(); Log.w("astrid", "got RTM token: " + token); - Preferences.setSyncRTMToken(activity, token); - performSync(activity); + Preferences.setSyncRTMToken(context, token); + performSync(context); return; } catch (Exception e) { @@ -134,24 +119,28 @@ public class RTMSyncService extends SynchronizationService { } } + // open up a dialog and have the user go to browser + if(isBackgroundService()) + return; + rtmService = new ServiceImpl(new ApplicationInfo( apiKey, sharedSecret, appName)); final String url = rtmService.beginAuthorization(Perms.delete); progressDialog.dismiss(); - Resources r = activity.getResources(); - DialogUtilities.okCancelDialog(activity, + Resources r = context.getResources(); + DialogUtilities.okCancelDialog(context, r.getString(R.string.sync_auth_request, "RTM"), new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { TaskList.synchronizeNow = true; Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); - activity.startActivity(intent); + context.startActivity(intent); } }, null); } else { - performSync(activity); + performSync(context); } } catch (Exception e) { @@ -159,31 +148,31 @@ public class RTMSyncService extends SynchronizationService { if(e instanceof ServiceInternalException && ((ServiceInternalException)e).getEnclosedException() instanceof IOException) { - showError(activity, e, "Sync Connection Error! Check your " + + showError(context, e, "Sync Connection Error! Check your " + "Internet connection & try again..."); } else - showError(activity, e, null); + showError(context, e, null); } } // --- synchronization! - private void performSync(final Activity activity) { + private void performSync(final Context context) { new Thread(new Runnable() { public void run() { - performSyncInNewThread(activity); + performSyncInNewThread(context); } }).start(); } - private void performSyncInNewThread(final Activity activity) { + private void performSyncInNewThread(final Context context) { try { - syncHandler.post(new ProgressLabelUpdater("Reading remote data")); - syncHandler.post(new ProgressUpdater(0, 5)); + postUpdate(new ProgressLabelUpdater("Reading remote data")); + postUpdate(new ProgressUpdater(0, 5)); // get RTM timeline final String timeline = rtmService.timelines_create(); - syncHandler.post(new ProgressUpdater(1, 5)); + postUpdate(new ProgressUpdater(1, 5)); // load RTM lists RtmLists lists = rtmService.lists_getList(); @@ -195,11 +184,11 @@ public class RTMSyncService extends SynchronizationService { if(INBOX_LIST_NAME.equalsIgnoreCase(list.getName())) INBOX_LIST_NAME = list.getName(); } - syncHandler.post(new ProgressUpdater(2, 5)); + postUpdate(new ProgressUpdater(2, 5)); // read all tasks LinkedList remoteChanges = new LinkedList(); - Date lastSyncDate = Preferences.getSyncRTMLastSync(activity); + Date lastSyncDate = Preferences.getSyncRTMLastSync(context); boolean shouldSyncIndividualLists = false; String filter = null; if(lastSyncDate == null) @@ -208,9 +197,9 @@ public class RTMSyncService extends SynchronizationService { // try the quick synchronization try { Thread.sleep(2000); // throttle - syncHandler.post(new ProgressUpdater(3, 5)); + postUpdate(new ProgressUpdater(3, 5)); RtmTasks tasks = rtmService.tasks_getList(null, filter, lastSyncDate); - syncHandler.post(new ProgressUpdater(5, 5)); + postUpdate(new ProgressUpdater(5, 5)); addTasksToList(tasks, remoteChanges); } catch (Exception e) { remoteChanges.clear(); @@ -220,9 +209,9 @@ public class RTMSyncService extends SynchronizationService { if(shouldSyncIndividualLists) { int progress = 0; for(final Entry entry : listIdToNameMap.entrySet()) { - syncHandler.post(new ProgressLabelUpdater("Reading " + + postUpdate(new ProgressLabelUpdater("Reading " + " list: " + entry.getValue())); - syncHandler.post(new ProgressUpdater(progress++, + postUpdate(new ProgressUpdater(progress++, listIdToNameMap.size())); try { Thread.sleep(1500); @@ -230,9 +219,9 @@ public class RTMSyncService extends SynchronizationService { filter, lastSyncDate); addTasksToList(tasks, remoteChanges); } catch (Exception e) { - syncHandler.post(new Runnable() { + postUpdate(new Runnable() { public void run() { - DialogUtilities.okDialog(activity, + DialogUtilities.okDialog(context, "List '" + entry.getValue() + "' import failed (too big?)", null); } @@ -240,17 +229,17 @@ public class RTMSyncService extends SynchronizationService { continue; } } - syncHandler.post(new ProgressUpdater(1, 1)); + postUpdate(new ProgressUpdater(1, 1)); } - synchronizeTasks(activity, remoteChanges, new RtmSyncHelper(timeline)); + synchronizeTasks(context, remoteChanges, new RtmSyncHelper(timeline)); // add a bit of fudge time so we don't load tasks we just edited Date syncTime = new Date(System.currentTimeMillis() + 1000); - Preferences.setSyncRTMLastSync(activity, syncTime); + Preferences.setSyncRTMLastSync(context, syncTime); } catch (Exception e) { - showError(activity, e, null); + showError(context, e, null); } } @@ -335,8 +324,8 @@ public class RTMSyncService extends SynchronizationService { } // estimated time - if(task.estimatedSeconds != remoteTask.estimatedSeconds && - !task.estimatedSeconds.equals(remoteTask.estimatedSeconds)) { + if(task.estimatedSeconds == 0 && remoteTask.estimatedSeconds != null || + task.estimatedSeconds > 0 && remoteTask.estimatedSeconds == null) { String estimation; int estimatedSeconds = task.estimatedSeconds; if(estimatedSeconds == 0) diff --git a/src/com/timsu/astrid/sync/SynchronizationProvider.java b/src/com/timsu/astrid/sync/SynchronizationProvider.java new file mode 100644 index 000000000..4895bef20 --- /dev/null +++ b/src/com/timsu/astrid/sync/SynchronizationProvider.java @@ -0,0 +1,578 @@ +/* + * ASTRID: Android's Simple Task Recording Dashboard + * + * Copyright (c) 2009 Tim Su + * + * 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., + * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ +package com.timsu.astrid.sync; + +import java.io.IOException; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedList; + +import android.app.Dialog; +import android.app.ProgressDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.res.Resources; +import android.os.Handler; +import android.util.Log; + +import com.timsu.astrid.R; +import com.timsu.astrid.data.alerts.AlertController; +import com.timsu.astrid.data.sync.SyncDataController; +import com.timsu.astrid.data.sync.SyncMapping; +import com.timsu.astrid.data.tag.TagController; +import com.timsu.astrid.data.tag.TagIdentifier; +import com.timsu.astrid.data.tag.TagModelForView; +import com.timsu.astrid.data.task.TaskController; +import com.timsu.astrid.data.task.TaskIdentifier; +import com.timsu.astrid.data.task.TaskModelForSync; +import com.timsu.astrid.utilities.DialogUtilities; +import com.timsu.astrid.utilities.Notifications; +import com.timsu.astrid.utilities.Preferences; + +/** A service that synchronizes with Astrid + * + * @author timsu + * + */ +public abstract class SynchronizationProvider { + + private int id; + static ProgressDialog progressDialog; + private Handler syncHandler; + private boolean backgroundSync; + + public SynchronizationProvider(int id) { + this.id = id; + } + + // called off the UI thread. does some setup + void synchronizeService(final Context activity, boolean isBackgroundSync) { + this.backgroundSync = isBackgroundSync; + + if(!isBackgroundService()) { + syncHandler = new Handler(); + SynchronizationProvider.progressDialog = new ProgressDialog(activity); + progressDialog.setIcon(android.R.drawable.ic_dialog_alert); + progressDialog.setTitle("Synchronization"); + progressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL); + progressDialog.setMax(100); + progressDialog.setMessage("Checking Authorization..."); + progressDialog.setProgress(0); + progressDialog.setCancelable(false); + progressDialog.show(); + } + + synchronize(activity); + } + + /** Synchronize with the service */ + protected abstract void synchronize(Context activity); + + /** Called when user requests a data clear */ + abstract void clearPersonalData(Context activity); + + /** Get this service's id */ + public int getId() { + return id; + } + + /** Gets this service's name */ + abstract String getName(); + + // --- utilities + + /** Check whether this synchronization request is running in the background + * @return true if it's running as a background service + */ + protected boolean isBackgroundService() { + return backgroundSync; + } + + /** Utility method for showing synchronization errors. If message is null, + * the contents of the throwable is displayed. + */ + void showError(final Context context, Throwable e, String message) { + Log.e("astrid", "Synchronization Error", e); + + if(isBackgroundService()) + return; + + Resources r = context.getResources(); + final String messageToDisplay; + if(message == null) { + messageToDisplay = r.getString(R.string.sync_error) + " " + + e.toString() + " - " + e.getStackTrace()[1]; + } else { + messageToDisplay = message; + } + syncHandler.post(new Runnable() { + public void run() { + if(progressDialog != null) + progressDialog.dismiss(); + DialogUtilities.okDialog(context, messageToDisplay, null); + } + }); + } + + /** Utility method to update the UI if we're an active sync, or output + * to console if we're a background sync. + */ + protected void postUpdate(Runnable updater) { + if(isBackgroundService()) { + // only run jobs if they can actually be processed + if(updater instanceof ProgressLabelUpdater) + updater.run(); + } else { + syncHandler.post(updater); + } + } + + // --- synchronization logic + + /** interface to assist with synchronization */ + protected interface SynchronizeHelper { + /** Push the given task to the remote server. + * + * @param task task proxy to push + * @param remoteTask remote task that we merged with, or null + * @param mapping local/remote mapping. + */ + void pushTask(TaskProxy task, TaskProxy remoteTask, + SyncMapping mapping) throws IOException; + + /** Create a task on the remote server. This is followed by a call of + * pushTask on the id in question. + * + * @return task to create + * @return remote id + */ + String createTask(TaskModelForSync task) throws IOException; + + /** Fetch remote task. Used to re-read merged tasks + * + * @param task TaskProxy of the original task + * @return new TaskProxy + */ + TaskProxy refetchTask(TaskProxy task) throws IOException; + + /** Delete the task from the remote server + * + * @param mapping mapping to delete + */ + void deleteTask(SyncMapping mapping) throws IOException; + } + + /** Helper to synchronize remote tasks with our local database. + * + * This initiates the following process: + * 1. local changes are read + * 2. remote changes are read + * 3. local tasks are merged with remote changes and pushed across + * 4. remote changes are then read in + * + * @param remoteTasks remote tasks that have been updated + * @return local tasks that need to be pushed across + */ + protected void synchronizeTasks(final Context context, LinkedList + remoteTasks, SynchronizeHelper helper) throws IOException { + final SyncStats stats = new SyncStats(); + final StringBuilder log = new StringBuilder(); + + SyncDataController syncController = Synchronizer.getSyncController(context); + TaskController taskController = Synchronizer.getTaskController(context); + TagController tagController = Synchronizer.getTagController(context); + AlertController alertController = Synchronizer.getAlertController(context); + SyncData data = new SyncData(context, remoteTasks); + + // 1. CREATE: grab tasks without a sync mapping and create them remotely + log.append(">> on remote server:\n"); + for(TaskIdentifier taskId : data.newlyCreatedTasks) { + TaskModelForSync task = taskController.fetchTaskForSync(taskId); + postUpdate(new ProgressLabelUpdater("Sending local task: " + + task.getName())); + postUpdate(new ProgressUpdater(stats.remoteCreatedTasks, + data.newlyCreatedTasks.size())); + + /* If there exists an incoming remote task with the same name and + * no mapping, we don't want to create this on the remote server. + * Instead, we create a mapping and do an update. */ + if(data.newRemoteTasks.containsKey(task.getName())) { + TaskProxy remoteTask = data.newRemoteTasks.get(task.getName()); + SyncMapping mapping = new SyncMapping(taskId, getId(), + remoteTask.getRemoteId()); + syncController.saveSyncMapping(mapping); + data.localChanges.add(mapping); + data.remoteChangeMap.put(taskId, remoteTask); + data.localIdToSyncMapping.put(taskId, mapping); + continue; + } + + String remoteId = helper.createTask(task); + SyncMapping mapping = new SyncMapping(taskId, getId(), remoteId); + syncController.saveSyncMapping(mapping); + data.localIdToSyncMapping.put(taskId, mapping); + + TaskProxy localTask = new TaskProxy(getId(), remoteId, false); + localTask.readFromTaskModel(task); + localTask.readTagsFromController(taskId, tagController, data.tags); + helper.pushTask(localTask, null, mapping); + + // update stats + log.append("added '" + task.getName() + "'\n"); + stats.remoteCreatedTasks++; + } + + // 2. DELETE: find deleted tasks and remove them from the list + postUpdate(new ProgressLabelUpdater("Sending locally deleted tasks")); + for(TaskIdentifier taskId : data.deletedTasks) { + SyncMapping mapping = data.localIdToSyncMapping.get(taskId); + syncController.deleteSyncMapping(mapping); + helper.deleteTask(mapping); + + // remove it from data structures + data.localChanges.remove(mapping); + data.localIdToSyncMapping.remove(taskId); + data.remoteIdToSyncMapping.remove(mapping); + data.remoteChangeMap.remove(taskId); + + // update stats + log.append("deleted id #" + taskId.getId() + "\n"); + stats.remoteDeletedTasks++; + postUpdate(new ProgressUpdater(stats.remoteDeletedTasks, + data.deletedTasks.size())); + } + + // 3. UPDATE: for each updated local task + for(SyncMapping mapping : data.localChanges) { + TaskProxy localTask = new TaskProxy(getId(), mapping.getRemoteId(), + false); + TaskModelForSync task = taskController.fetchTaskForSync( + mapping.getTask()); + localTask.readFromTaskModel(task); + localTask.readTagsFromController(task.getTaskIdentifier(), + tagController, data.tags); + + postUpdate(new ProgressLabelUpdater("Sending local task: " + + task.getName())); + postUpdate(new ProgressUpdater(stats.remoteUpdatedTasks, + data.localChanges.size())); + + // if there is a conflict, merge + TaskProxy remoteConflict = null; + if(data.remoteChangeMap.containsKey(mapping.getTask())) { + remoteConflict = data.remoteChangeMap.get(mapping.getTask()); + localTask.mergeWithOther(remoteConflict); + stats.mergedTasks++; + } + + try { + helper.pushTask(localTask, remoteConflict, mapping); + if(remoteConflict != null) + log.append("merged '" + task.getName() + "'\n"); + else + log.append("updated '" + task.getName() + "'\n"); + } catch (Exception e) { + Log.e("astrid", "Exception pushing task", e); + log.append("error sending '" + task.getName() + "'\n"); + continue; + } + + // re-fetch remote task + if(remoteConflict != null) { + TaskProxy newTask = helper.refetchTask(remoteConflict); + remoteTasks.remove(remoteConflict); + remoteTasks.add(newTask); + } else + stats.remoteUpdatedTasks++; + } + + // 4. REMOTE SYNC load remote information + log.append("\n>> on astrid:\n"); + postUpdate(new ProgressUpdater(0, 1)); + for(TaskProxy remoteTask : remoteTasks) { + if(remoteTask.name != null) + postUpdate(new ProgressLabelUpdater("Updating local " + + "tasks: " + remoteTask.name)); + else + postUpdate(new ProgressLabelUpdater("Updating local tasks")); + SyncMapping mapping = null; + TaskModelForSync task = null; + + // if it's new, create a new task model + if(!data.remoteIdToSyncMapping.containsKey(remoteTask.getRemoteId())) { + // if it's new & deleted, forget about it + if(remoteTask.isDeleted()) { + continue; + } + + task = taskController.searchForTaskForSync(remoteTask.name); + if(task == null) { + task = new TaskModelForSync(); + setupTaskDefaults(context, task); + log.append("added " + remoteTask.name + "\n"); + } else { + mapping = data.localIdToSyncMapping.get(task.getTaskIdentifier()); + log.append("merged " + remoteTask.name + "\n"); + } + } else { + mapping = data.remoteIdToSyncMapping.get(remoteTask.getRemoteId()); + if(remoteTask.isDeleted()) { + taskController.deleteTask(mapping.getTask()); + syncController.deleteSyncMapping(mapping); + log.append("deleted " + remoteTask.name + "\n"); + stats.localDeletedTasks++; + continue; + } + + log.append("updated '" + remoteTask.name + "'\n"); + task = taskController.fetchTaskForSync( + mapping.getTask()); + } + + // save the data + remoteTask.writeToTaskModel(task); + taskController.saveTask(task); + + // save tags + if(remoteTask.tags != null) { + LinkedList taskTags = tagController.getTaskTags(task.getTaskIdentifier()); + HashSet tagsToAdd = new HashSet(); + for(String tag : remoteTask.tags) { + String tagLower = tag.toLowerCase(); + if(!data.tagsByLCName.containsKey(tagLower)) { + TagIdentifier tagId = tagController.createTag(tag); + data.tagsByLCName.put(tagLower, tagId); + tagsToAdd.add(tagId); + } else + tagsToAdd.add(data.tagsByLCName.get(tagLower)); + } + + HashSet tagsToDelete = new HashSet(taskTags); + tagsToDelete.removeAll(tagsToAdd); + tagsToAdd.removeAll(taskTags); + + for(TagIdentifier tagId : tagsToDelete) + tagController.removeTag(task.getTaskIdentifier(), tagId); + for(TagIdentifier tagId : tagsToAdd) + tagController.addTag(task.getTaskIdentifier(), tagId); + } + stats.localUpdatedTasks++; + + // try looking for this task if it doesn't already have a mapping + if(mapping == null) { + mapping = data.localIdToSyncMapping.get(task.getTaskIdentifier()); + if(mapping == null) { + try { + mapping = new SyncMapping(task.getTaskIdentifier(), remoteTask); + syncController.saveSyncMapping(mapping); + data.localIdToSyncMapping.put(task.getTaskIdentifier(), + mapping); + } catch (Exception e) { + // unique violation: ignore - it'll get merged later + Log.e("astrid-sync", "Exception creating mapping", e); + } + } + stats.localCreatedTasks++; + } + + Notifications.updateAlarm(context, taskController, alertController, + task); + postUpdate(new ProgressUpdater(stats.localUpdatedTasks, + remoteTasks.size())); + } + stats.localUpdatedTasks -= stats.localCreatedTasks; + + syncController.clearUpdatedTaskList(getId()); + postUpdate(new Runnable() { + public void run() { + stats.showDialog(context, log.toString()); + } + }); + } + + /** Set up defaults from preferences for this task */ + private void setupTaskDefaults(Context context, TaskModelForSync task) { + Integer reminder = Preferences.getDefaultReminder(context); + if(reminder != null) + task.setNotificationIntervalSeconds(24*3600*reminder); + } + + // --- helper classes + + /** data structure builder */ + class SyncData { + HashSet mappings; + HashSet activeTasks; + HashSet allTasks; + + HashMap remoteIdToSyncMapping; + HashMap localIdToSyncMapping; + + HashSet localChanges; + HashSet mappedTasks; + HashMap remoteChangeMap; + HashMap newRemoteTasks; + + HashMap tags; + HashMap tagsByLCName; + + HashSet newlyCreatedTasks; + HashSet deletedTasks; + + public SyncData(Context context, LinkedList remoteTasks) { + // 1. get data out of the database + mappings = Synchronizer.getSyncController(context).getSyncMapping(getId()); + activeTasks = Synchronizer.getTaskController(context).getActiveTaskIdentifiers(); + allTasks = Synchronizer.getTaskController(context).getAllTaskIdentifiers(); + tags = Synchronizer.getTagController(context).getAllTagsAsMap(); + + // 2. build helper data structures + remoteIdToSyncMapping = new HashMap(); + localIdToSyncMapping = new HashMap(); + localChanges = new HashSet(); + mappedTasks = new HashSet(); + for(SyncMapping mapping : mappings) { + if(mapping.isUpdated()) + localChanges.add(mapping); + remoteIdToSyncMapping.put(mapping.getRemoteId(), mapping); + localIdToSyncMapping.put(mapping.getTask(), mapping); + mappedTasks.add(mapping.getTask()); + } + tagsByLCName = new HashMap(); + for(TagModelForView tag : tags.values()) + tagsByLCName.put(tag.getName().toLowerCase(), tag.getTagIdentifier()); + + // 3. build map of remote tasks + remoteChangeMap = new HashMap(); + newRemoteTasks = new HashMap(); + for(TaskProxy remoteTask : remoteTasks) { + if(remoteIdToSyncMapping.containsKey(remoteTask.getRemoteId())) { + SyncMapping mapping = remoteIdToSyncMapping.get(remoteTask.getRemoteId()); + remoteChangeMap.put(mapping.getTask(), remoteTask); + } else if(remoteTask.name != null){ + newRemoteTasks.put(remoteTask.name, remoteTask); + } + } + + // 4. build data structures of things to do + newlyCreatedTasks = new HashSet(activeTasks); + newlyCreatedTasks.removeAll(mappedTasks); + deletedTasks = new HashSet(mappedTasks); + deletedTasks.removeAll(allTasks); + } + } + + + /** statistics tracking and displaying */ + protected class SyncStats { + int localCreatedTasks = 0; + int localUpdatedTasks = 0; + int localDeletedTasks = 0; + + int mergedTasks = 0; + + int remoteCreatedTasks = 0; + int remoteUpdatedTasks = 0; + int remoteDeletedTasks = 0; + + /** Display a dialog with statistics */ + public void showDialog(final Context context, String log) { + progressDialog.hide(); + Resources r = context.getResources(); + + if(Preferences.shouldSuppressSyncDialogs(context)) + return; + + Dialog.OnClickListener finishListener = new Dialog.OnClickListener() { + public void onClick(DialogInterface dialog, + int which) { + Synchronizer.continueSynchronization(context); + } + }; + + // nothing updated + if(localCreatedTasks + localUpdatedTasks + localDeletedTasks + + mergedTasks + remoteCreatedTasks + remoteDeletedTasks + + remoteUpdatedTasks == 0) { + if(!isBackgroundService()) + DialogUtilities.okDialog(context, "Sync: Up to date!", finishListener); + return; + } + + StringBuilder sb = new StringBuilder(); + sb.append(r.getString(R.string.sync_result_title, getName())); + sb.append("\n\n"); + sb.append(log).append("\n"); + if(localCreatedTasks + localUpdatedTasks + localDeletedTasks > 0) + sb.append(r.getString(R.string.sync_result_local)).append("\n"); + if(localCreatedTasks > 0) + sb.append(r.getString(R.string.sync_result_created, localCreatedTasks)).append("\n"); + if(localUpdatedTasks > 0) + sb.append(r.getString(R.string.sync_result_updated, localUpdatedTasks)).append("\n"); + if(localDeletedTasks > 0) + sb.append(r.getString(R.string.sync_result_deleted, localDeletedTasks)).append("\n"); + + if(mergedTasks > 0) + sb.append("\n").append(r.getString(R.string.sync_result_merged, mergedTasks)).append("\n"); + sb.append("\n"); + + if(remoteCreatedTasks + remoteDeletedTasks + remoteUpdatedTasks > 0) + sb.append(r.getString(R.string.sync_result_remote)).append("\n"); + if(remoteCreatedTasks > 0) + sb.append(r.getString(R.string.sync_result_created, remoteCreatedTasks)).append("\n"); + if(remoteUpdatedTasks > 0) + sb.append(r.getString(R.string.sync_result_updated, remoteUpdatedTasks)).append("\n"); + if(remoteDeletedTasks > 0) + sb.append(r.getString(R.string.sync_result_deleted, remoteDeletedTasks)).append("\n"); + + sb.append("\n"); + + DialogUtilities.okDialog(context, sb.toString(), finishListener); + } + } + + protected class ProgressUpdater implements Runnable { + int step, outOf; + public ProgressUpdater(int step, int outOf) { + this.step = step; + this.outOf = outOf; + } + public void run() { + if(!isBackgroundService()) + progressDialog.setProgress(100*step/outOf); + } + } + + protected class ProgressLabelUpdater implements Runnable { + String label; + public ProgressLabelUpdater(String label) { + this.label = label; + } + public void run() { + if(isBackgroundService()) { + Log.i("astrid-sync", label); + } else { + if(!progressDialog.isShowing()) + progressDialog.show(); + progressDialog.setMessage(label); + } + } + } +} diff --git a/src/com/timsu/astrid/sync/SynchronizationService.java b/src/com/timsu/astrid/sync/SynchronizationService.java index 962d405fd..18081b2c3 100644 --- a/src/com/timsu/astrid/sync/SynchronizationService.java +++ b/src/com/timsu/astrid/sync/SynchronizationService.java @@ -1,542 +1,99 @@ -/* - * ASTRID: Android's Simple Task Recording Dashboard - * - * Copyright (c) 2009 Tim Su - * - * 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., - * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA - */ -package com.timsu.astrid.sync; - -import java.io.IOException; -import java.util.HashMap; -import java.util.HashSet; -import java.util.LinkedList; - -import android.app.Activity; -import android.app.Dialog; -import android.app.ProgressDialog; -import android.content.Context; -import android.content.DialogInterface; -import android.content.res.Resources; -import android.os.Handler; -import android.util.Log; - -import com.timsu.astrid.R; -import com.timsu.astrid.data.alerts.AlertController; -import com.timsu.astrid.data.sync.SyncDataController; -import com.timsu.astrid.data.sync.SyncMapping; -import com.timsu.astrid.data.tag.TagController; -import com.timsu.astrid.data.tag.TagIdentifier; -import com.timsu.astrid.data.tag.TagModelForView; -import com.timsu.astrid.data.task.TaskController; -import com.timsu.astrid.data.task.TaskIdentifier; -import com.timsu.astrid.data.task.TaskModelForSync; -import com.timsu.astrid.utilities.DialogUtilities; -import com.timsu.astrid.utilities.Notifications; -import com.timsu.astrid.utilities.Preferences; - -/** A service that synchronizes with Astrid - * - * @author timsu - * - */ -public abstract class SynchronizationService { - - private int id; - static ProgressDialog progressDialog; - protected Handler syncHandler; - public SynchronizationService(int id) { - this.id = id; - } - - // called off the UI thread. does some setup - void synchronizeService(final Activity activity) { - syncHandler = new Handler(); - SynchronizationService.progressDialog = new ProgressDialog(activity); - progressDialog.setIcon(android.R.drawable.ic_dialog_alert); - progressDialog.setTitle("Synchronization"); - progressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL); - progressDialog.setMax(100); - progressDialog.setMessage("Checking Authorization..."); - progressDialog.setProgress(0); - progressDialog.setCancelable(false); - progressDialog.show(); - - synchronize(activity); - } - - /** Synchronize with the service */ - protected abstract void synchronize(Activity activity); - - /** Called when user requests a data clear */ - abstract void clearPersonalData(Activity activity); - - /** Get this service's id */ - public int getId() { - return id; - } - - /** Gets this service's name */ - abstract String getName(); - - // --- utilities - - /** Utility method for showing synchronization errors. If message is null, - * the contents of the throwable is displayed. - */ - void showError(final Context context, Throwable e, String message) { - Log.e("astrid", "Synchronization Error", e); - Resources r = context.getResources(); - final String messageToDisplay; - if(message == null) { - messageToDisplay = r.getString(R.string.sync_error) + " " + - e.toString() + " - " + e.getStackTrace()[1]; - } else { - messageToDisplay = message; - } - syncHandler.post(new Runnable() { - public void run() { - if(progressDialog != null) - progressDialog.dismiss(); - DialogUtilities.okDialog(context, messageToDisplay, null); - } - }); - } - - // --- synchronization logic - - /** interface to assist with synchronization */ - protected interface SynchronizeHelper { - /** Push the given task to the remote server. - * - * @param task task proxy to push - * @param remoteTask remote task that we merged with, or null - * @param mapping local/remote mapping. - */ - void pushTask(TaskProxy task, TaskProxy remoteTask, - SyncMapping mapping) throws IOException; - - /** Create a task on the remote server. This is followed by a call of - * pushTask on the id in question. - * - * @return task to create - * @return remote id - */ - String createTask(TaskModelForSync task) throws IOException; - - /** Fetch remote task. Used to re-read merged tasks - * - * @param task TaskProxy of the original task - * @return new TaskProxy - */ - TaskProxy refetchTask(TaskProxy task) throws IOException; - - /** Delete the task from the remote server - * - * @param mapping mapping to delete - */ - void deleteTask(SyncMapping mapping) throws IOException; - } - - /** Helper to synchronize remote tasks with our local database. - * - * This initiates the following process: - * 1. local changes are read - * 2. remote changes are read - * 3. local tasks are merged with remote changes and pushed across - * 4. remote changes are then read in - * - * @param remoteTasks remote tasks that have been updated - * @return local tasks that need to be pushed across - */ - protected void synchronizeTasks(final Activity activity, LinkedList - remoteTasks, SynchronizeHelper helper) throws IOException { - final SyncStats stats = new SyncStats(); - final StringBuilder log = new StringBuilder(); - - SyncDataController syncController = Synchronizer.getSyncController(activity); - TaskController taskController = Synchronizer.getTaskController(activity); - TagController tagController = Synchronizer.getTagController(activity); - AlertController alertController = Synchronizer.getAlertController(activity); - SyncData data = new SyncData(activity, remoteTasks); - - // 1. CREATE: grab tasks without a sync mapping and create them remotely - log.append(">> on remote server:\n"); - for(TaskIdentifier taskId : data.newlyCreatedTasks) { - TaskModelForSync task = taskController.fetchTaskForSync(taskId); - syncHandler.post(new ProgressLabelUpdater("Sending local task: " + - task.getName())); - syncHandler.post(new ProgressUpdater(stats.remoteCreatedTasks, - data.newlyCreatedTasks.size())); - - /* If there exists an incoming remote task with the same name and - * no mapping, we don't want to create this on the remote server. - * Instead, we create a mapping and do an update. */ - if(data.newRemoteTasks.containsKey(task.getName())) { - TaskProxy remoteTask = data.newRemoteTasks.get(task.getName()); - SyncMapping mapping = new SyncMapping(taskId, getId(), - remoteTask.getRemoteId()); - syncController.saveSyncMapping(mapping); - data.localChanges.add(mapping); - data.remoteChangeMap.put(taskId, remoteTask); - data.localIdToSyncMapping.put(taskId, mapping); - continue; - } - - String remoteId = helper.createTask(task); - SyncMapping mapping = new SyncMapping(taskId, getId(), remoteId); - syncController.saveSyncMapping(mapping); - data.localIdToSyncMapping.put(taskId, mapping); - - TaskProxy localTask = new TaskProxy(getId(), remoteId, false); - localTask.readFromTaskModel(task); - localTask.readTagsFromController(activity, taskId, tagController, data.tags); - helper.pushTask(localTask, null, mapping); - - // update stats - log.append("added '" + task.getName() + "'\n"); - stats.remoteCreatedTasks++; - } - - // 2. DELETE: find deleted tasks and remove them from the list - syncHandler.post(new ProgressLabelUpdater("Sending locally deleted tasks")); - for(TaskIdentifier taskId : data.deletedTasks) { - SyncMapping mapping = data.localIdToSyncMapping.get(taskId); - syncController.deleteSyncMapping(mapping); - helper.deleteTask(mapping); - - // remove it from data structures - data.localChanges.remove(mapping); - data.localIdToSyncMapping.remove(taskId); - data.remoteIdToSyncMapping.remove(mapping); - data.remoteChangeMap.remove(taskId); - - // update stats - log.append("deleted id #" + taskId.getId() + "\n"); - stats.remoteDeletedTasks++; - syncHandler.post(new ProgressUpdater(stats.remoteDeletedTasks, - data.deletedTasks.size())); - } - - // 3. UPDATE: for each updated local task - for(SyncMapping mapping : data.localChanges) { - TaskProxy localTask = new TaskProxy(getId(), mapping.getRemoteId(), - false); - TaskModelForSync task = taskController.fetchTaskForSync( - mapping.getTask()); - localTask.readFromTaskModel(task); - localTask.readTagsFromController(activity, task.getTaskIdentifier(), - tagController, data.tags); - - syncHandler.post(new ProgressLabelUpdater("Sending local task: " + - task.getName())); - syncHandler.post(new ProgressUpdater(stats.remoteUpdatedTasks, - data.localChanges.size())); - - // if there is a conflict, merge - TaskProxy remoteConflict = null; - if(data.remoteChangeMap.containsKey(mapping.getTask())) { - remoteConflict = data.remoteChangeMap.get(mapping.getTask()); - localTask.mergeWithOther(remoteConflict); - stats.mergedTasks++; - } - - try { - helper.pushTask(localTask, remoteConflict, mapping); - if(remoteConflict != null) - log.append("merged '" + task.getName() + "'\n"); - else - log.append("updated '" + task.getName() + "'\n"); - } catch (Exception e) { - Log.e("astrid", "Exception pushing task", e); - log.append("error sending '" + task.getName() + "'\n"); - continue; - } - - // re-fetch remote task - if(remoteConflict != null) { - TaskProxy newTask = helper.refetchTask(remoteConflict); - remoteTasks.remove(remoteConflict); - remoteTasks.add(newTask); - } else - stats.remoteUpdatedTasks++; - } - - // 4. REMOTE SYNC load remote information - log.append("\n>> on astrid:\n"); - syncHandler.post(new ProgressUpdater(0, 1)); - for(TaskProxy remoteTask : remoteTasks) { - if(remoteTask.name != null) - syncHandler.post(new ProgressLabelUpdater("Updating local " + - "tasks: " + remoteTask.name)); - else - syncHandler.post(new ProgressLabelUpdater("Updating local tasks")); - SyncMapping mapping = null; - TaskModelForSync task = null; - - // if it's new, create a new task model - if(!data.remoteIdToSyncMapping.containsKey(remoteTask.getRemoteId())) { - // if it's new & deleted, forget about it - if(remoteTask.isDeleted()) { - continue; - } - - task = taskController.searchForTaskForSync(remoteTask.name); - if(task == null) { - task = new TaskModelForSync(); - setupTaskDefaults(activity, task); - log.append("added " + remoteTask.name + "\n"); - } else { - mapping = data.localIdToSyncMapping.get(task.getTaskIdentifier()); - log.append("merged " + remoteTask.name + "\n"); - } - } else { - mapping = data.remoteIdToSyncMapping.get(remoteTask.getRemoteId()); - if(remoteTask.isDeleted()) { - taskController.deleteTask(mapping.getTask()); - syncController.deleteSyncMapping(mapping); - log.append("deleted " + remoteTask.name + "\n"); - stats.localDeletedTasks++; - continue; - } - - log.append("updated '" + remoteTask.name + "'\n"); - task = taskController.fetchTaskForSync( - mapping.getTask()); - } - - // save the data - remoteTask.writeToTaskModel(task); - taskController.saveTask(task); - - // save tags - if(remoteTask.tags != null) { - LinkedList taskTags = tagController.getTaskTags(activity, task.getTaskIdentifier()); - HashSet tagsToAdd = new HashSet(); - for(String tag : remoteTask.tags) { - String tagLower = tag.toLowerCase(); - if(!data.tagsByLCName.containsKey(tagLower)) { - TagIdentifier tagId = tagController.createTag(tag); - data.tagsByLCName.put(tagLower, tagId); - tagsToAdd.add(tagId); - } else - tagsToAdd.add(data.tagsByLCName.get(tagLower)); - } - - HashSet tagsToDelete = new HashSet(taskTags); - tagsToDelete.removeAll(tagsToAdd); - tagsToAdd.removeAll(taskTags); - - for(TagIdentifier tagId : tagsToDelete) - tagController.removeTag(task.getTaskIdentifier(), tagId); - for(TagIdentifier tagId : tagsToAdd) - tagController.addTag(task.getTaskIdentifier(), tagId); - } - stats.localUpdatedTasks++; - - // try looking for this task if it doesn't already have a mapping - if(mapping == null) { - mapping = data.localIdToSyncMapping.get(task.getTaskIdentifier()); - if(mapping == null) { - try { - mapping = new SyncMapping(task.getTaskIdentifier(), remoteTask); - syncController.saveSyncMapping(mapping); - data.localIdToSyncMapping.put(task.getTaskIdentifier(), - mapping); - } catch (Exception e) { - // unique violation: ignore - it'll get merged later - Log.e("astrid-sync", "Exception creating mapping", e); - } - } - stats.localCreatedTasks++; - } - - Notifications.updateAlarm(activity, taskController, alertController, - task); - syncHandler.post(new ProgressUpdater(stats.localUpdatedTasks, - remoteTasks.size())); - } - stats.localUpdatedTasks -= stats.localCreatedTasks; - - syncController.clearUpdatedTaskList(getId()); - syncHandler.post(new Runnable() { - public void run() { - stats.showDialog(activity, log.toString()); - } - }); - } - - /** Set up defaults from preferences for this task */ - private void setupTaskDefaults(Activity activity, TaskModelForSync task) { - Integer reminder = Preferences.getDefaultReminder(activity); - if(reminder != null) - task.setNotificationIntervalSeconds(24*3600*reminder); - } - - // --- helper classes - - /** data structure builder */ - class SyncData { - HashSet mappings; - HashSet activeTasks; - HashSet allTasks; - - HashMap remoteIdToSyncMapping; - HashMap localIdToSyncMapping; - - HashSet localChanges; - HashSet mappedTasks; - HashMap remoteChangeMap; - HashMap newRemoteTasks; - - HashMap tags; - HashMap tagsByLCName; - - HashSet newlyCreatedTasks; - HashSet deletedTasks; - - public SyncData(Activity activity, LinkedList remoteTasks) { - // 1. get data out of the database - mappings = Synchronizer.getSyncController(activity).getSyncMapping(getId()); - activeTasks = Synchronizer.getTaskController(activity).getActiveTaskIdentifiers(); - allTasks = Synchronizer.getTaskController(activity).getAllTaskIdentifiers(); - tags = Synchronizer.getTagController(activity).getAllTagsAsMap(activity); - - // 2. build helper data structures - remoteIdToSyncMapping = new HashMap(); - localIdToSyncMapping = new HashMap(); - localChanges = new HashSet(); - mappedTasks = new HashSet(); - for(SyncMapping mapping : mappings) { - if(mapping.isUpdated()) - localChanges.add(mapping); - remoteIdToSyncMapping.put(mapping.getRemoteId(), mapping); - localIdToSyncMapping.put(mapping.getTask(), mapping); - mappedTasks.add(mapping.getTask()); - } - tagsByLCName = new HashMap(); - for(TagModelForView tag : tags.values()) - tagsByLCName.put(tag.getName().toLowerCase(), tag.getTagIdentifier()); - - // 3. build map of remote tasks - remoteChangeMap = new HashMap(); - newRemoteTasks = new HashMap(); - for(TaskProxy remoteTask : remoteTasks) { - if(remoteIdToSyncMapping.containsKey(remoteTask.getRemoteId())) { - SyncMapping mapping = remoteIdToSyncMapping.get(remoteTask.getRemoteId()); - remoteChangeMap.put(mapping.getTask(), remoteTask); - } else if(remoteTask.name != null){ - newRemoteTasks.put(remoteTask.name, remoteTask); - } - } - - // 4. build data structures of things to do - newlyCreatedTasks = new HashSet(activeTasks); - newlyCreatedTasks.removeAll(mappedTasks); - deletedTasks = new HashSet(mappedTasks); - deletedTasks.removeAll(allTasks); - } - } - - - /** statistics tracking and displaying */ - protected class SyncStats { - int localCreatedTasks = 0; - int localUpdatedTasks = 0; - int localDeletedTasks = 0; - - int mergedTasks = 0; - - int remoteCreatedTasks = 0; - int remoteUpdatedTasks = 0; - int remoteDeletedTasks = 0; - - /** Display a dialog with statistics */ - public void showDialog(final Activity activity, String log) { - progressDialog.hide(); - - if(Preferences.shouldSuppressSyncDialogs(activity)) - return; - - Dialog.OnClickListener finishListener = new Dialog.OnClickListener() { - public void onClick(DialogInterface dialog, - int which) { - Synchronizer.continueSynchronization(activity); - } - }; - - // nothing updated - if(localCreatedTasks + localUpdatedTasks + localDeletedTasks + - mergedTasks + remoteCreatedTasks + remoteDeletedTasks + - remoteUpdatedTasks == 0) { - if(!Synchronizer.isAutoSync()) - DialogUtilities.okDialog(activity, "Sync: Up to date!", finishListener); - return; - } - - StringBuilder sb = new StringBuilder(); - sb.append(getName()).append(" Results:"); // TODO i18n - sb.append("\n\n"); - sb.append(log); - if(localCreatedTasks + localUpdatedTasks + localDeletedTasks > 0) - sb.append("\nSummary - Astrid Tasks:"); - if(localCreatedTasks > 0) - sb.append("\nCreated: " + localCreatedTasks); - if(localUpdatedTasks > 0) - sb.append("\nUpdated: " + localUpdatedTasks); - if(localDeletedTasks > 0) - sb.append("\nDeleted: " + localDeletedTasks); - - if(mergedTasks > 0) - sb.append("\n\nMerged: " + mergedTasks); - - if(remoteCreatedTasks + remoteDeletedTasks + remoteUpdatedTasks > 0) - sb.append("\n\nSummary - Remote Server:"); - if(remoteCreatedTasks > 0) - sb.append("\nCreated: " + remoteCreatedTasks); - if(remoteUpdatedTasks > 0) - sb.append("\nUpdated: " + remoteUpdatedTasks); - if(remoteDeletedTasks > 0) - sb.append("\nDeleted: " + remoteDeletedTasks); - - sb.append("\n"); - - DialogUtilities.okDialog(activity, sb.toString(), finishListener); - } - } - - protected static class ProgressUpdater implements Runnable { - int step, outOf; - public ProgressUpdater(int step, int outOf) { - this.step = step; - this.outOf = outOf; - } - public void run() { - progressDialog.setProgress(100*step/outOf); - } - } - - protected static class ProgressLabelUpdater implements Runnable { - String label; - public ProgressLabelUpdater(String label) { - this.label = label; - } - public void run() { - if(!progressDialog.isShowing()) - progressDialog.show(); - progressDialog.setMessage(label); - } - } -} +package com.timsu.astrid.sync; + +import java.util.Date; +import java.util.Timer; +import java.util.TimerTask; + +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.os.IBinder; +import android.util.Log; + +import com.timsu.astrid.utilities.Preferences; + +public class SynchronizationService extends Service { + + /** Service timer */ + private Timer timer = new Timer(); + + /** Service activity */ + private static Context context; + + /** Set the activity for this service */ + public static void setContext(Context context) { + SynchronizationService.context = context; + } + + @Override + public IBinder onBind(Intent arg0) { + return null; // unused + } + + @Override + public void onCreate() { + super.onCreate(); + + // init the service here + startService(); + + } + + @Override + public void onDestroy() { + super.onDestroy(); + + shutdownService(); + } + + /** Start the timer that runs the service */ + private void startService() { + // figure out synchronization frequency + Integer syncFrequencySeconds = Preferences.getSyncAutoSyncFrequency(context); + if(syncFrequencySeconds == null) { + shutdownService(); + return; + } + + long interval = 1000L * syncFrequencySeconds; + + // figure out last synchronize time + Date lastSyncDate = Preferences.getSyncLastSync(context); + Date lastAutoSyncDate = Preferences.getSyncLastSyncAttempt(context); + long latestSyncMillis = 0; + if(lastSyncDate != null) + latestSyncMillis = lastSyncDate.getTime(); + if(lastAutoSyncDate != null && lastAutoSyncDate.getTime() > latestSyncMillis) + latestSyncMillis = lastAutoSyncDate.getTime(); + long offset = 0; + if(latestSyncMillis != 0) + offset = Math.max(0, latestSyncMillis + interval - System.currentTimeMillis()); + + timer.scheduleAtFixedRate(new TimerTask() { + @Override + public void run() { + performSynchronization(); + } + }, offset, interval); + Log.i("astrid", "Synchronization Service Started, Offset: " + offset/1000 + + "s Interval: " + interval/1000); + } + + /** Stop the timer that runs the service */ + private void shutdownService() { + if (timer != null) + timer.cancel(); + Log.i("astrid", "Synchronization Service Stopped"); + } + + /** Perform the actual synchronization */ + private void performSynchronization() { + if(context == null || context.getResources() == null) + return; + + Log.i("astrid", "Automatic Synchronize Initiated."); + Preferences.setSyncLastSyncAttempt(context, new Date()); + + Synchronizer.synchronize(context, true, null); + } +} diff --git a/src/com/timsu/astrid/sync/Synchronizer.java b/src/com/timsu/astrid/sync/Synchronizer.java index dc4195525..c9a37f46e 100644 --- a/src/com/timsu/astrid/sync/Synchronizer.java +++ b/src/com/timsu/astrid/sync/Synchronizer.java @@ -44,13 +44,13 @@ public class Synchronizer { } /** Synchronize all activated sync services */ - public static void synchronize(Activity activity, boolean isAutoSync, + public synchronized static void synchronize(Context context, boolean isAutoSync, SynchronizerListener listener) { currentStep = ServiceWrapper._FIRST_SERVICE.ordinal(); servicesSynced = 0; autoSync = isAutoSync; callback = listener; - continueSynchronization(activity); + continueSynchronization(context); } @@ -78,7 +78,7 @@ public class Synchronizer { } }, - RTM(new RTMSyncService(SYNC_ID_RTM)) { + RTM(new RTMSyncProvider(SYNC_ID_RTM)) { @Override boolean isActivated(Context context) { return Preferences.shouldSyncRTM(context); @@ -92,9 +92,9 @@ public class Synchronizer { } }; - private SynchronizationService service; + private SynchronizationProvider service; - private ServiceWrapper(SynchronizationService service) { + private ServiceWrapper(SynchronizationProvider service) { this.service = service; } @@ -112,45 +112,38 @@ public class Synchronizer { /** On finished callback */ private static SynchronizerListener callback; - /** If this synchronization was automatically initiated */ private static boolean autoSync; - /** Called to do the next step of synchronization. Run me on the UI thread! */ - static void continueSynchronization(Activity activity) { + static void continueSynchronization(Context context) { ServiceWrapper serviceWrapper = ServiceWrapper.values()[currentStep]; currentStep++; switch(serviceWrapper) { case _FIRST_SERVICE: - continueSynchronization(activity); + continueSynchronization(context); break; case RTM: - if(Preferences.shouldSyncRTM(activity)) { + if(Preferences.shouldSyncRTM(context)) { servicesSynced++; - serviceWrapper.service.synchronizeService(activity); + serviceWrapper.service.synchronizeService(context, autoSync); } else { - continueSynchronization(activity); + continueSynchronization(context); } break; case _LAST_SERVICE: - finishSynchronization(activity); + finishSynchronization(context); } } /** Called at the end of sync. */ - private static void finishSynchronization(final Activity activity) { + private static void finishSynchronization(final Context context) { closeControllers(); - Preferences.setSyncLastSync(activity, new Date()); + Preferences.setSyncLastSync(context, new Date()); if(callback != null) callback.onSynchronizerFinished(servicesSynced); } - /** Was this sync automatically initiated? */ - static boolean isAutoSync() { - return autoSync; - } - // --- controller stuff private static class ControllerWrapper { @@ -165,11 +158,11 @@ public class Synchronizer { } @SuppressWarnings("unchecked") - public TYPE get(Activity activity) { + public TYPE get(Context context) { if(controller == null) { try { controller = (TYPE)typeClass.getConstructors()[0].newInstance( - activity); + context); } catch (IllegalArgumentException e) { Log.e(getClass().getSimpleName(), e.toString()); } catch (SecurityException e) { @@ -210,20 +203,20 @@ public class Synchronizer { private static ControllerWrapper alertController = new ControllerWrapper(AlertController.class); - static SyncDataController getSyncController(Activity activity) { - return syncController.get(activity); + static SyncDataController getSyncController(Context context) { + return syncController.get(context); } - static TaskController getTaskController(Activity activity) { - return taskController.get(activity); + static TaskController getTaskController(Context context) { + return taskController.get(context); } - static TagController getTagController(Activity activity) { - return tagController.get(activity); + static TagController getTagController(Context context) { + return tagController.get(context); } - static AlertController getAlertController(Activity activity) { - return alertController.get(activity); + static AlertController getAlertController(Context context) { + return alertController.get(context); } public static void setTaskController(TaskController taskController) { diff --git a/src/com/timsu/astrid/sync/TaskProxy.java b/src/com/timsu/astrid/sync/TaskProxy.java index 3c31c6432..f109e3cb9 100644 --- a/src/com/timsu/astrid/sync/TaskProxy.java +++ b/src/com/timsu/astrid/sync/TaskProxy.java @@ -23,8 +23,6 @@ import java.util.Date; import java.util.HashMap; import java.util.LinkedList; -import android.app.Activity; - import com.timsu.astrid.data.enums.Importance; import com.timsu.astrid.data.enums.RepeatInterval; import com.timsu.astrid.data.tag.TagController; @@ -141,7 +139,7 @@ public class TaskProxy { completionDate = task.getCompletionDate(); definiteDueDate = task.getDefiniteDueDate(); preferredDueDate = task.getPreferredDueDate(); - dueDate = definiteDueDate != null ? definiteDueDate : preferredDueDate; + dueDate = definiteDueDate != null ? definiteDueDate : preferredDueDate; hiddenUntil = task.getHiddenUntil(); estimatedSeconds = task.getEstimatedSeconds(); elapsedSeconds = task.getElapsedSeconds(); @@ -152,11 +150,10 @@ public class TaskProxy { } /** Read tags from the given tag controller */ - public void readTagsFromController(Activity activity, TaskIdentifier taskId, + public void readTagsFromController(TaskIdentifier taskId, TagController tagController, HashMap tagList) { - LinkedList tagIds = tagController.getTaskTags(activity, - taskId); + LinkedList tagIds = tagController.getTaskTags(taskId); tags = new LinkedList(); for(TagIdentifier tagId : tagIds) { tags.add(tagList.get(tagId).getName()); @@ -177,25 +174,25 @@ public class TaskProxy { task.setCreationDate(creationDate); if(completionDate != null) task.setCompletionDate(completionDate); - + // date handling: if sync service only supports one type of due date, // we have to figure out which field to write to based on what // already has data - + if(dueDate != null) { if(task.getDefiniteDueDate() != null) task.setDefiniteDueDate(dueDate); else if(task.getPreferredDueDate() != null) task.setPreferredDueDate(dueDate); else - task.setDefiniteDueDate(dueDate); + task.setDefiniteDueDate(dueDate); } else { if(definiteDueDate != null) task.setDefiniteDueDate(definiteDueDate); if(preferredDueDate != null) task.setPreferredDueDate(preferredDueDate); } - + if(hiddenUntil != null) task.setHiddenUntil(hiddenUntil); if(estimatedSeconds != null) diff --git a/src/com/timsu/astrid/utilities/Preferences.java b/src/com/timsu/astrid/utilities/Preferences.java index edeaad167..d9e9e1ee6 100644 --- a/src/com/timsu/astrid/utilities/Preferences.java +++ b/src/com/timsu/astrid/utilities/Preferences.java @@ -20,6 +20,7 @@ public class Preferences { private static final String P_SYNC_RTM_TOKEN = "rtmtoken"; private static final String P_SYNC_RTM_LAST_SYNC = "rtmlastsync"; private static final String P_SYNC_LAST_SYNC = "lastsync"; + private static final String P_SYNC_LAST_SYNC_ATTEMPT = "lastsyncattempt"; // pref values public static final int ICON_SET_PINK = 0; @@ -93,7 +94,7 @@ public class Preferences { } } - // --- sysetm preferences + // --- system preferences /** CurrentVersion: the currently installed version of Astrid */ public static int getCurrentVersion(Context context) { @@ -257,9 +258,26 @@ public class Preferences { R.string.p_sync_quiet), false); } - /** returns the font size user wants on the front page */ - public static Float autoSyncFrequency(Context context) { - return getFloatValue(context, R.string.p_sync_every); + /** Reads the frequency, in seconds, auto-sync should occur. + * @return seconds duration, or null if not desired */ + public static Integer getSyncAutoSyncFrequency(Context context) { + Integer time = getIntegerValue(context, R.string.p_sync_interval); + if(time != null && time == 0) + time = null; + return time; + } + + /** Reads the old auto */ + public static Float getSyncOldAutoSyncFrequency(Context context) { + return getFloatValue(context, R.string.p_sync_every_old); + } + + /** Sets the auto-sync frequency to the desired value */ + public static void setSyncAutoSyncFrequency(Context context, int value) { + Editor editor = getPrefs(context).edit(); + editor.putString(context.getResources().getString(R.string.p_sync_interval), + Integer.toString(value)); + editor.commit(); } /** Last Auto-Sync Date, or null */ @@ -270,7 +288,15 @@ public class Preferences { return new Date(value); } - /** Set Last Auto-Sync Date */ + /** Last Successful Auto-Sync Date, or null */ + public static Date getSyncLastSyncAttempt(Context context) { + Long value = getPrefs(context).getLong(P_SYNC_LAST_SYNC_ATTEMPT, 0); + if(value == 0) + return null; + return new Date(value); + } + + /** Set Last Sync Date */ public static void setSyncLastSync(Context context, Date date) { if(date == null) { clearPref(context, P_SYNC_LAST_SYNC); @@ -282,6 +308,13 @@ public class Preferences { editor.commit(); } + /** Set Last Auto-Sync Attempt Date */ + public static void setSyncLastSyncAttempt(Context context, Date date) { + Editor editor = getPrefs(context).edit(); + editor.putLong(P_SYNC_LAST_SYNC_ATTEMPT, date.getTime()); + editor.commit(); + } + // --- helper methods /** Clear the given preference */ diff --git a/src/com/timsu/astrid/utilities/StartupReceiver.java b/src/com/timsu/astrid/utilities/StartupReceiver.java index 015436a7f..b8f9f402a 100644 --- a/src/com/timsu/astrid/utilities/StartupReceiver.java +++ b/src/com/timsu/astrid/utilities/StartupReceiver.java @@ -1,5 +1,7 @@ package com.timsu.astrid.utilities; +import com.timsu.astrid.sync.SynchronizationService; + import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; @@ -36,6 +38,15 @@ public class StartupReceiver extends BroadcastReceiver { boolean justUpgraded = latestSetVersion != version; final int finalVersion = version; if(justUpgraded) { + // perform version-specific processing + if(latestSetVersion <= 99) { + if(Preferences.getSyncOldAutoSyncFrequency(context) != null) { + float value = Preferences.getSyncOldAutoSyncFrequency(context); + Preferences.setSyncAutoSyncFrequency(context, + Math.round(value * 3600)); + } + } + new Thread(new Runnable() { public void run() { Notifications.scheduleAllAlarms(context); @@ -49,6 +60,11 @@ public class StartupReceiver extends BroadcastReceiver { Preferences.setPreferenceDefaults(context); + // start synchronization service + SynchronizationService.setContext(context); + Intent service = new Intent(context, SynchronizationService.class); + context.startService(service); + hasStartedUp = true; } }