Remember The Milk Background Synchronization.

- the synchronization process can either happen in the foreground with UI dialogs or in the background with logging to LogCat
  - UI and dialogs for setting up auto-sync
  - rename "SynchronizationService" => "SynchronizationProvider", created a service called "SynchronizationService"
pull/14/head
Tim Su 17 years ago
parent 60af08f0b1
commit 655c839550

@ -28,8 +28,20 @@
android:layout_height="wrap_content">
<TextView
android:id="@+id/last_sync_label"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="@string/sync_pref_group_actions"/>
<TextView
android:id="@+id/last_auto_sync_label"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="@string/sync_pref_group_actions"/>
<TextView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:paddingTop="10dip"
android:paddingBottom="10dip"
android:textAppearance="?android:attr/textAppearanceMedium"
android:gravity="center_horizontal"

@ -93,4 +93,25 @@
<item>2</item>
</string-array>
<!-- Synchronization Intervals -->
<string-array name="sync_interval_entries">
<item>disable</item>
<item>twice an hour</item>
<item>hourly</item>
<item>twice a day</item>
<item>daily</item>
<item>twice a week</item>
<item>weekly</item>
</string-array>
<!-- interval in seconds -->
<string-array name="sync_interval_values">
<item>0</item>
<item>1800</item>
<item>3600</item>
<item>43200</item>
<item>86400</item>
<item>302400</item>
<item>604800</item>
</string-array>
</resources>

@ -248,11 +248,12 @@ If you don\'t want to see the new task right after you complete the old one, you
<string name="sync_pref_group_actions">Actions</string>
<string name="sync_pref_group_options">Options</string>
<string name="p_sync_rtm">sync_rtm</string>
<string name="sync_rtm_title">Remember The Milk</string><!-- Proper noun - don't translate ? -->
<string name="sync_rtm_title">Remember The Milk</string><!-- Proper noun - don't translate -->
<string name="sync_rtm_desc">http://www.rememberthemilk.com</string>
<string name="p_sync_every">sync_every</string>
<string name="sync_every_title">Synchronize Frequency</string>
<string name="sync_every_desc">If set, perform sync every # hours</string>
<string name="p_sync_every_old">sync_every</string> <!-- old sync key -->
<string name="p_sync_interval">sync_freq</string>
<string name="sync_interval_title">Auto-Synchronize</string>
<string name="sync_interval_desc">If set, synchronization occurs automatically given interval</string>
<string name="p_sync_button">sync_button</string>
<string name="sync_button_title">Main Menu Shortcut</string>
<string name="sync_button_desc">Show \"Synchronize\" in Astrid\'s menu</string>
@ -281,6 +282,16 @@ Wish me luck!\n
<string name="sync_forget">Clear Personal Data</string>
<string name="sync_forget_confirm">Clear data for selected services?</string>
<string name="sync_no_synchronizers">No Synchronizers Enabled!</string>
<string name="sync_last_sync">Last Sync Date: %s</string>
<string name="sync_last_auto_sync">Last AutoSync Attempt: %s</string>
<string name="sync_date_never">never</string>
<string name="sync_result_title">%s Results</string>
<string name="sync_result_local">Summary - Astrid Tasks:</string>
<string name="sync_result_remote">Summary - Remote Server:</string>
<string name="sync_result_created">Created: %d</string>
<string name="sync_result_updated">Updated: %d</string>
<string name="sync_result_deleted">Deleted: %d</string>
<string name="sync_result_merged">Merged: %d</string>
<!-- Dialog Boxes -->
<skip />

@ -15,10 +15,12 @@
<PreferenceCategory
android:title="@string/sync_pref_group_options">
<EditTextPreference
android:key="@string/p_sync_every"
android:title="@string/sync_every_title"
android:summary="@string/sync_every_desc" />
<ListPreference
android:key="@string/p_sync_interval"
android:entries="@array/sync_interval_entries"
android:entryValues="@array/sync_interval_values"
android:title="@string/sync_interval_title"
android:summary="@string/sync_interval_desc" />
<CheckBoxPreference
android:key="@string/p_sync_button"

@ -19,17 +19,24 @@
*/
package com.timsu.astrid.activities;
import java.text.SimpleDateFormat;
import java.util.Date;
import android.app.Dialog;
import android.content.DialogInterface;
import android.content.res.Resources;
import android.os.Bundle;
import android.preference.PreferenceActivity;
import android.view.KeyEvent;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import com.timsu.astrid.R;
import com.timsu.astrid.sync.Synchronizer;
import com.timsu.astrid.utilities.Constants;
import com.timsu.astrid.utilities.DialogUtilities;
import com.timsu.astrid.utilities.Preferences;
/**
* Displays synchronization preferences and an action panel so users can
@ -40,9 +47,14 @@ import com.timsu.astrid.utilities.DialogUtilities;
*/
public class SyncPreferences extends PreferenceActivity {
private boolean rtmSyncPreference;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Resources r = getResources();
rtmSyncPreference = Preferences.shouldSyncRTM(this);
addPreferencesFromResource(R.xml.sync_preferences);
@ -69,5 +81,38 @@ public class SyncPreferences extends PreferenceActivity {
}, null);
}
});
TextView lastSyncLabel = (TextView)findViewById(R.id.last_sync_label);
SimpleDateFormat formatter = new SimpleDateFormat("MM/dd HH:mm");
String syncDate = r.getString(R.string.sync_date_never);
Date lastSyncDate = Preferences.getSyncLastSync(this);
if(lastSyncDate != null)
syncDate = formatter.format(lastSyncDate);
lastSyncLabel.setText(r.getString(R.string.sync_last_sync, syncDate));
syncDate = null;
TextView lastAutoSyncLabel = (TextView)findViewById(R.id.last_auto_sync_label);
Date lastAutoSyncDate = Preferences.getSyncLastSyncAttempt(this);
if(lastAutoSyncDate != null && (lastSyncDate == null ||
(lastAutoSyncDate.getTime() - lastSyncDate.getTime() < 3600000L)))
syncDate = formatter.format(lastAutoSyncDate);
if(syncDate != null)
lastAutoSyncLabel.setText(r.getString(R.string.sync_last_auto_sync, syncDate));
else
lastAutoSyncLabel.setVisibility(View.GONE);
}
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
if(keyCode == KeyEvent.KEYCODE_BACK) {
boolean newRtmSyncPreference = Preferences.shouldSyncRTM(this);
if(newRtmSyncPreference != rtmSyncPreference && newRtmSyncPreference) {
setResult(Constants.RESULT_SYNCHRONIZE);
}
finish();
return true;
}
return false;
}
}

@ -133,7 +133,7 @@ public class TagListSubActivity extends SubActivity {
tagToTaskCount = new HashMap<TagModelForView, Integer>();
for(TagModelForView tag : tagArray) {
LinkedList<TaskIdentifier> 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

@ -216,9 +216,9 @@ public class TaskEdit extends TaskModificationTabbedActivity<TaskModelForEdit> {
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<TagIdentifier, TagModelForView> tagsMap =
new HashMap<TagIdentifier, TagModelForView>();

@ -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,18 +125,9 @@ 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

@ -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<TagIdentifier, TagModelForView> 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<TaskIdentifier> tasks = getTagController().getTaggedTasks(getParent(),
LinkedList<TaskIdentifier> 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<TaskModelForList, String>();
StringBuilder tagBuilder = new StringBuilder();
context.tasksById = new HashMap<Long, TaskModelForList>();
@ -515,7 +519,7 @@ public class TaskListSubActivity extends SubActivity {
}
// get list of tags
LinkedList<TagIdentifier> tagIds = getTagController().getTaskTags(getParent(),
LinkedList<TagIdentifier> tagIds = getTagController().getTaskTags(
task.getTaskIdentifier());
tagBuilder.delete(0, tagBuilder.length());
for(Iterator<TagIdentifier> 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;

@ -41,19 +41,22 @@ public class TagController extends AbstractController {
// --- tag batch operations
/** Get a list of all tags */
public LinkedList<TagModelForView> getAllTags(Activity activity)
public LinkedList<TagModelForView> getAllTags()
throws SQLException {
LinkedList<TagModelForView> list = new LinkedList<TagModelForView>();
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<TagIdentifier, TagModelForView> getAllTagsAsMap(Activity activity) throws SQLException {
public HashMap<TagIdentifier, TagModelForView> getAllTagsAsMap() throws SQLException {
HashMap<TagIdentifier, TagModelForView> map = new HashMap<TagIdentifier, TagModelForView>();
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<TagIdentifier> getTaskTags(Activity activity, TaskIdentifier
public LinkedList<TagIdentifier> getTaskTags(TaskIdentifier
taskId) throws SQLException {
LinkedList<TagIdentifier> list = new LinkedList<TagIdentifier>();
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<TaskIdentifier> getTaggedTasks(Activity activity, TagIdentifier
public LinkedList<TaskIdentifier> getTaggedTasks(TagIdentifier
tagId) throws SQLException {
LinkedList<TaskIdentifier> list = new LinkedList<TaskIdentifier>();
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;
}

@ -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<String, String> listNameToIdMap = new HashMap<String, String>();
Map<String, String> listIdToNameMap = new HashMap<String, String>();
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<TaskProxy> remoteChanges = new LinkedList<TaskProxy>();
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<String, String> 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)

@ -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<TaskProxy>
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<TagIdentifier> taskTags = tagController.getTaskTags(task.getTaskIdentifier());
HashSet<TagIdentifier> tagsToAdd = new HashSet<TagIdentifier>();
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<TagIdentifier> tagsToDelete = new HashSet<TagIdentifier>(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<SyncMapping> mappings;
HashSet<TaskIdentifier> activeTasks;
HashSet<TaskIdentifier> allTasks;
HashMap<String, SyncMapping> remoteIdToSyncMapping;
HashMap<TaskIdentifier, SyncMapping> localIdToSyncMapping;
HashSet<SyncMapping> localChanges;
HashSet<TaskIdentifier> mappedTasks;
HashMap<TaskIdentifier, TaskProxy> remoteChangeMap;
HashMap<String, TaskProxy> newRemoteTasks;
HashMap<TagIdentifier, TagModelForView> tags;
HashMap<String, TagIdentifier> tagsByLCName;
HashSet<TaskIdentifier> newlyCreatedTasks;
HashSet<TaskIdentifier> deletedTasks;
public SyncData(Context context, LinkedList<TaskProxy> 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<String, SyncMapping>();
localIdToSyncMapping = new HashMap<TaskIdentifier, SyncMapping>();
localChanges = new HashSet<SyncMapping>();
mappedTasks = new HashSet<TaskIdentifier>();
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<String, TagIdentifier>();
for(TagModelForView tag : tags.values())
tagsByLCName.put(tag.getName().toLowerCase(), tag.getTagIdentifier());
// 3. build map of remote tasks
remoteChangeMap = new HashMap<TaskIdentifier, TaskProxy>();
newRemoteTasks = new HashMap<String, TaskProxy>();
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<TaskIdentifier>(activeTasks);
newlyCreatedTasks.removeAll(mappedTasks);
deletedTasks = new HashSet<TaskIdentifier>(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);
}
}
}
}

@ -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 java.util.Date;
import java.util.Timer;
import java.util.TimerTask;
import android.app.Activity;
import android.app.Dialog;
import android.app.ProgressDialog;
import android.app.Service;
import android.content.Context;
import android.content.DialogInterface;
import android.content.res.Resources;
import android.os.Handler;
import android.content.Intent;
import android.os.IBinder;
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 {
public class SynchronizationService extends Service {
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();
/** Service timer */
private Timer timer = new Timer();
synchronize(activity);
}
/** Service activity */
private static Context context;
/** Synchronize with the service */
protected abstract void synchronize(Activity activity);
/** Set the activity for this service */
public static void setContext(Context context) {
SynchronizationService.context = context;
}
/** Called when user requests a data clear */
abstract void clearPersonalData(Activity activity);
/** Get this service's id */
public int getId() {
return id;
@Override
public IBinder onBind(Intent arg0) {
return null; // unused
}
/** Gets this service's name */
abstract String getName();
@Override
public void onCreate() {
super.onCreate();
// --- utilities
// init the service here
startService();
/** 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
@Override
public void onDestroy() {
super.onDestroy();
/** 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;
shutdownService();
}
/** 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<TaskProxy>
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<TagIdentifier> taskTags = tagController.getTaskTags(activity, task.getTaskIdentifier());
HashSet<TagIdentifier> tagsToAdd = new HashSet<TagIdentifier>();
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<TagIdentifier> tagsToDelete = new HashSet<TagIdentifier>(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);
/** 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();
}
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());
}
});
}, offset, interval);
Log.i("astrid", "Synchronization Service Started, Offset: " + offset/1000 +
"s Interval: " + interval/1000);
}
/** 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);
/** Stop the timer that runs the service */
private void shutdownService() {
if (timer != null)
timer.cancel();
Log.i("astrid", "Synchronization Service Stopped");
}
// --- helper classes
/** data structure builder */
class SyncData {
HashSet<SyncMapping> mappings;
HashSet<TaskIdentifier> activeTasks;
HashSet<TaskIdentifier> allTasks;
HashMap<String, SyncMapping> remoteIdToSyncMapping;
HashMap<TaskIdentifier, SyncMapping> localIdToSyncMapping;
HashSet<SyncMapping> localChanges;
HashSet<TaskIdentifier> mappedTasks;
HashMap<TaskIdentifier, TaskProxy> remoteChangeMap;
HashMap<String, TaskProxy> newRemoteTasks;
HashMap<TagIdentifier, TagModelForView> tags;
HashMap<String, TagIdentifier> tagsByLCName;
HashSet<TaskIdentifier> newlyCreatedTasks;
HashSet<TaskIdentifier> deletedTasks;
public SyncData(Activity activity, LinkedList<TaskProxy> 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<String, SyncMapping>();
localIdToSyncMapping = new HashMap<TaskIdentifier, SyncMapping>();
localChanges = new HashSet<SyncMapping>();
mappedTasks = new HashSet<TaskIdentifier>();
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<String, TagIdentifier>();
for(TagModelForView tag : tags.values())
tagsByLCName.put(tag.getName().toLowerCase(), tag.getTagIdentifier());
// 3. build map of remote tasks
remoteChangeMap = new HashMap<TaskIdentifier, TaskProxy>();
newRemoteTasks = new HashMap<String, TaskProxy>();
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<TaskIdentifier>(activeTasks);
newlyCreatedTasks.removeAll(mappedTasks);
deletedTasks = new HashSet<TaskIdentifier>(mappedTasks);
deletedTasks.removeAll(allTasks);
}
}
/** 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());
/** 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);
}
Synchronizer.synchronize(context, true, null);
}
}

@ -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<TYPE extends AbstractController> {
@ -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> alertController =
new ControllerWrapper<AlertController>(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) {

@ -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;
@ -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<TagIdentifier, TagModelForView>
tagList) {
LinkedList<TagIdentifier> tagIds = tagController.getTaskTags(activity,
taskId);
LinkedList<TagIdentifier> tagIds = tagController.getTaskTags(taskId);
tags = new LinkedList<String>();
for(TagIdentifier tagId : tagIds) {
tags.add(tagList.get(tagId).getName());

@ -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 */

@ -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;
}
}

Loading…
Cancel
Save