Fixed sync issues:

- no more double-synchronizing, which gives "Service invocation failed" error
  - better logging
  - notes are now pushed to RTM if they don't already exist. this is a small hack
  - auto synchronization option
  - hacky screen gesture animations. they're really terrible, but kinda cool
  - completed tasks are marked as done on RTM, not deleted
  - tag-to-list conversion is now case insensitive

  Also, restored full tabs, and added animation xml's.
pull/14/head
Tim Su 16 years ago
parent f649568c14
commit 01af108e96

@ -1,13 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.timsu.astrid"
android:versionCode="57"
android:versionName="2.0-beta">
android:versionCode="58"
android:versionName="2.0.1-beta">
<meta-data android:name="com.a0soft.gphone.aTrackDog.webURL"
android:value="http://www.weloveastrid.com" />
<meta-data android:name="com.a0soft.gphone.aTrackDog.testVersion"
android:value="57" />
android:value="58" />
<uses-permission android:name="android.permission.VIBRATE"/>
<uses-permission android:name="android.permission.INTERNET"/>

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2008 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate android:fromXDelta="100%p" android:toXDelta="0" android:duration="400"/>
</set>

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2008 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate android:fromXDelta="0" android:toXDelta="-100%p" android:duration="400"/>
</set>

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2008 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate android:fromXDelta="-100%p" android:toXDelta="0" android:duration="400"/>
</set>

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2008 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate android:fromXDelta="0" android:toXDelta="100%p" android:duration="400"/>
</set>

@ -57,16 +57,6 @@
android:layout_width="fill_parent"
android:layout_height="wrap_content">
</LinearLayout>
<TextView android:id="@+id/estimatedDuration_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/estimatedDuration_label"
style="@style/TextAppearance.EditEvent_Label"/>
<Button android:id="@+id/estimatedDuration"
android:layout_width="fill_parent"
android:layout_height="wrap_content"/>
<TextView android:id="@+id/tags_label"
android:layout_width="wrap_content"
@ -268,6 +258,17 @@
android:background="@android:drawable/divider_horizontal_dark"
/>
<TextView android:id="@+id/estimatedDuration_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/estimatedDuration_label"
style="@style/TextAppearance.EditEvent_Label"/>
<Button android:id="@+id/estimatedDuration"
android:layout_width="fill_parent"
android:layout_height="wrap_content"/>
<TextView android:id="@+id/elapsedDuration_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"

@ -199,10 +199,14 @@ If you don\'t want to see the new task right after you complete the old one, you
<!-- Synchronization -->
<skip />
<string name="p_sync_rtm">sync_rtm</string>
<string name="p_sync_every">sync_every</string>
<string name="sync_pref_group">Synchronization Services</string>
<string name="sync_pref_group_actions">Actions</string>
<string name="sync_pref_group_options">Options</string>
<string name="sync_rtm_title">Remember The Milk</string>
<string name="sync_rtm_desc">http://www.rememberthemilk.com</string>
<string name="sync_every_title">Synchronize Frequency</string>
<string name="sync_every_desc">If set, sync every # hours when Astrid starts</string>
<string name="sync_error">Sync Error! Sorry for the inconvenience! Error:</string>
<string name="sync_auth_request">
In order to synchronize, please log in to your %s account and authorize Astrid to read your data.
@ -210,17 +214,17 @@ In order to synchronize, please log in to your %s account and authorize Astrid t
When finished, restart Astrid and come back here.
</string>
<string name="sync_rtm_notes">
Astrid's RTM sync is not complete!
Welcome to Astrid\'s RTM sync!
\n\n
- RTM tags are not read. Instead, RTM's lists are mapped to Astrid\'s tags.\n
- Task notes are read from RTM, but changes are not written back.\n
- Task notes are read from RTM, but edits are not written back.\n
- Notifications and repeats are not synchronized.\n
- Moving tasks in RTM to another list and then editing the task name causes duplication.\n
- Synchronization is SLOW! Be patient.\n
\n\n
- Moving tasks in RTM to another list and then renaming them causes dupes.\n
- Deleting tasks in RTM is not detected.\n
- Synchronization is SLOW!\n
Wish me luck!\n
</string>
<string name="sync_now">Synchronize!</string>
<string name="sync_now">Synchronize Now!</string>
<string name="sync_forget">Clear Personal Data</string>
<string name="sync_forget_confirm">Clear data for selected services?</string>

@ -12,4 +12,14 @@
</PreferenceCategory>
<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" />
</PreferenceCategory>
</PreferenceScreen>

@ -9,6 +9,7 @@ import android.widget.Button;
import com.timsu.astrid.R;
import com.timsu.astrid.sync.Synchronizer;
import com.timsu.astrid.sync.Synchronizer.SynchronizerListener;
import com.timsu.astrid.utilities.DialogUtilities;
public class SyncPreferences extends PreferenceActivity {
@ -21,10 +22,23 @@ public class SyncPreferences extends PreferenceActivity {
getListView().addFooterView(getLayoutInflater().inflate(
R.layout.sync_footer, getListView(), false));
((Button)findViewById(R.id.sync)).setOnClickListener(new View.OnClickListener() {
final Button syncButton = ((Button)findViewById(R.id.sync));
syncButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Synchronizer.synchronize(SyncPreferences.this);
syncButton.setEnabled(false);
Synchronizer.synchronize(SyncPreferences.this, new SynchronizerListener() {
@Override
public void onSynchronizerFinished(int numServicesSynced) {
syncButton.setEnabled(true);
if(numServicesSynced == 0) {
DialogUtilities.okDialog(SyncPreferences.this,
"Nothing to do!", null);
} else {
finish();
}
}
});
}
});

@ -100,6 +100,11 @@ public class TagList extends Activity {
gestureDetector = new GestureDetector(new TagListGestureDetector());
}
@Override
public void finish() {
super.finish();
}
class TagListGestureDetector extends GestureDetector.SimpleOnGestureListener {
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {

@ -48,11 +48,9 @@ import android.widget.CompoundButton;
import android.widget.EditText;
import android.widget.ImageButton;
import android.widget.LinearLayout;
import android.widget.RelativeLayout;
import android.widget.SimpleCursorAdapter;
import android.widget.Spinner;
import android.widget.TabHost;
import android.widget.TabWidget;
import android.widget.TextView;
import android.widget.ToggleButton;
import android.widget.CompoundButton.OnCheckedChangeListener;
@ -137,22 +135,22 @@ public class TaskEdit extends TaskModificationTabbedActivity<TaskModelForEdit> {
tabHost.getTabContentView(), true);
tabHost.addTab(tabHost.newTabSpec(TAB_BASIC)
.setIndicator("",
.setIndicator("Basic",
r.getDrawable(R.drawable.ic_dialog_info_c))
.setContent(R.id.tab_basic));
tabHost.addTab(tabHost.newTabSpec(TAB_DATES)
.setIndicator("",
.setIndicator("Dates",
r.getDrawable(R.drawable.ic_dialog_time_c))
.setContent(R.id.tab_dates));
tabHost.addTab(tabHost.newTabSpec(TAB_ALERTS)
.setIndicator("",
.setIndicator("Alerts",
r.getDrawable(R.drawable.ic_dialog_alert_c))
.setContent(R.id.tab_notification));
TabWidget tabWidget = tabHost.getTabWidget();
for(int i = 0; i < tabWidget.getChildCount(); i++) {
RelativeLayout tab = (RelativeLayout)tabWidget.getChildAt(i);
tab.getLayoutParams().height = 46;
}
// TabWidget tabWidget = tabHost.getTabWidget();
// for(int i = 0; i < tabWidget.getChildCount(); i++) {
// RelativeLayout tab = (RelativeLayout)tabWidget.getChildAt(i);
// tab.getLayoutParams().height = 46;
// }
setUpUIComponents();
setUpListeners();

@ -58,7 +58,9 @@ 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.Synchronizer;
import com.timsu.astrid.utilities.Constants;
import com.timsu.astrid.utilities.Preferences;
import com.timsu.astrid.utilities.StartupReceiver;
@ -143,6 +145,22 @@ public class TaskList extends Activity {
tagController = new TagController(this);
tagController.open();
setupUIComponents();
fillData();
// auto sync
Integer autoSyncHours = Preferences.autoSyncFrequency(this);
if(autoSyncHours != null) {
Date lastSync = Preferences.getSyncLastSync(this);
if(lastSync == null || lastSync.getTime() +
1000L*3600*autoSyncHours < System.currentTimeMillis()) {
Synchronizer.synchronize(this, null);
}
}
}
public void setupUIComponents() {
listView = (ListView)findViewById(R.id.tasklist);
addButton = (Button)findViewById(R.id.addtask);
addButton.setOnClickListener(new
@ -165,8 +183,6 @@ public class TaskList extends Activity {
}
});
fillData();
gestureDetector = new GestureDetector(new TaskListGestureDetector());
gestureTouchListener = new View.OnTouchListener() {
public boolean onTouch(View v, MotionEvent event) {

@ -144,13 +144,18 @@ public class TagController extends AbstractController {
TagModelForView.FIELD_LIST,
KEY_ROWID + "=" + id, null, null, null, null, null);
if (cursor != null) {
cursor.moveToFirst();
TagModelForView model = new TagModelForView(cursor);
return model;
try {
if (cursor != null) {
cursor.moveToFirst();
TagModelForView model = new TagModelForView(cursor);
return model;
}
throw new SQLException("Returned empty set!");
} finally {
if(cursor != null)
cursor.close();
}
throw new SQLException("Returned empty set!");
}
/** Deletes the tag and removes tag/task mappings */

@ -55,16 +55,18 @@ public class TaskController extends AbstractController {
AbstractTaskModel.NOTIFICATIONS,
AbstractTaskModel.NOTIFICATION_FLAGS), null, null, null, null, null);
if(cursor.getCount() == 0)
return list;
do {
cursor.moveToNext();
list.add(new TaskModelForNotify(cursor));
} while(!cursor.isLast());
try {
if(cursor.getCount() == 0)
return list;
do {
cursor.moveToNext();
list.add(new TaskModelForNotify(cursor));
} while(!cursor.isLast());
cursor.close();
return list;
return list;
} finally {
cursor.close();
}
}
/** Return a list of all active tasks with deadlines */
@ -77,16 +79,19 @@ public class TaskController extends AbstractController {
AbstractTaskModel.DEFINITE_DUE_DATE,
AbstractTaskModel.PREFERRED_DUE_DATE), null, null, null, null, null);
if(cursor.getCount() == 0)
return list;
try {
if(cursor.getCount() == 0)
return list;
do {
cursor.moveToNext();
list.add(new TaskModelForNotify(cursor));
} while(!cursor.isLast());
do {
cursor.moveToNext();
list.add(new TaskModelForNotify(cursor));
} while(!cursor.isLast());
cursor.close();
return list;
return list;
} finally {
cursor.close();
}
}
/** Return a list of all of the tasks with progress < COMPLETE_PERCENTAGE */
@ -119,21 +124,48 @@ public class TaskController extends AbstractController {
}
/** Get identifiers for all tasks */
public HashSet<TaskIdentifier> getAllTaskIdentifiers() {
HashSet<TaskIdentifier> list = new HashSet<TaskIdentifier>();
Cursor cursor = database.query(TASK_TABLE_NAME, new String[] { KEY_ROWID },
null, null, null, null, null, null);
try {
if(cursor.getCount() == 0)
return list;
do {
cursor.moveToNext();
list.add(new TaskIdentifier(cursor.getInt(
cursor.getColumnIndexOrThrow(KEY_ROWID))));
} while(!cursor.isLast());
return list;
} finally {
cursor.close();
}
}
/** Get identifiers for all non-completed tasks */
public HashSet<TaskIdentifier> getActiveTaskIdentifiers() {
HashSet<TaskIdentifier> list = new HashSet<TaskIdentifier>();
Cursor cursor = database.query(TASK_TABLE_NAME, new String[] { KEY_ROWID },
AbstractTaskModel.PROGRESS_PERCENTAGE + " < " +
AbstractTaskModel.COMPLETE_PERCENTAGE, null, null, null, null, null);
if(cursor.getCount() == 0)
return list;
do {
cursor.moveToNext();
list.add(new TaskIdentifier(cursor.getInt(
cursor.getColumnIndexOrThrow(KEY_ROWID))));
} while(!cursor.isLast());
try {
if(cursor.getCount() == 0)
return list;
return list;
do {
cursor.moveToNext();
list.add(new TaskIdentifier(cursor.getInt(
cursor.getColumnIndexOrThrow(KEY_ROWID))));
} while(!cursor.isLast());
return list;
} finally {
cursor.close();
}
}
/** Create a weighted list of tasks from the db cursor given */
@ -280,7 +312,10 @@ public class TaskController extends AbstractController {
/** Returns a TaskModelForView by name */
public TaskModelForSync searchForTaskForSync(String name) throws SQLException {
Cursor cursor = database.query(true, TASK_TABLE_NAME, TaskModelForSync.FIELD_LIST,
AbstractTaskModel.NAME + " = ?", new String[] { name }, null, null, null, null);
AbstractTaskModel.NAME + " = ? AND " +
AbstractTaskModel.PROGRESS_PERCENTAGE + " < "+
AbstractTaskModel.COMPLETE_PERCENTAGE,
new String[] { name }, null, null, null, null);
if (cursor == null || cursor.getCount() == 0)
return null;
cursor.moveToFirst();

@ -25,11 +25,12 @@ import android.database.Cursor;
import com.timsu.astrid.data.AbstractController;
import com.timsu.astrid.data.enums.Importance;
import com.timsu.astrid.utilities.Notifications.Notifiable;
/** Fields that you would want to synchronize in the TaskModel */
public class TaskModelForSync extends AbstractTaskModel {
public class TaskModelForSync extends AbstractTaskModel implements Notifiable {
static String[] FIELD_LIST = new String[] {
AbstractController.KEY_ROWID,
@ -46,6 +47,9 @@ public class TaskModelForSync extends AbstractTaskModel {
COMPLETION_DATE,
NOTES,
REPEAT,
LAST_NOTIFIED,
NOTIFICATIONS,
NOTIFICATION_FLAGS,
};
// --- constructors
@ -132,6 +136,23 @@ public class TaskModelForSync extends AbstractTaskModel {
return super.getRepeat();
}
@Override
public Integer getNotificationIntervalSeconds() {
return super.getNotificationIntervalSeconds();
}
@Override
public int getNotificationFlags() {
return super.getNotificationFlags();
}
@Override
public Date getLastNotificationDate() {
return super.getLastNotificationDate();
}
// --- setters
@Override
public void setDefiniteDueDate(Date definiteDueDate) {
super.setDefiniteDueDate(definiteDueDate);
@ -197,6 +218,9 @@ public class TaskModelForSync extends AbstractTaskModel {
super.setProgressPercentage(progressPercentage);
}
@Override
public void setNotificationIntervalSeconds(Integer intervalInSeconds) {
super.setNotificationIntervalSeconds(intervalInSeconds);
}
}

@ -134,7 +134,6 @@ public class RTMSyncService extends SynchronizationService {
@Override
public void run() {
performSyncInNewThread(activity);
Synchronizer.closeControllers();
}
}).start();
}
@ -157,7 +156,7 @@ public class RTMSyncService extends SynchronizationService {
// load RTM lists
RtmLists lists = rtmService.lists_getList();
for(RtmList list : lists.getLists().values()) {
listNameToIdMap.put(list.getName(), list.getId());
listNameToIdMap.put(list.getName().toLowerCase(), list.getId());
listIdToNameMap.put(list.getId(), list.getName());
// read the name of the inbox with the correct case
@ -184,16 +183,16 @@ public class RTMSyncService extends SynchronizationService {
public String createTask(String listName) throws IOException {
if(listName == null)
listName = INBOX_LIST_NAME;
if(!listNameToIdMap.containsKey(listName)) {
if(!listNameToIdMap.containsKey(listName.toLowerCase())) {
try {
String listId =
rtmService.lists_add(timeline, listName).getId();
listNameToIdMap.put(listName, listId);
listNameToIdMap.put(listName.toLowerCase(), listId);
} catch (Exception e) {
listName = INBOX_LIST_NAME;
}
}
String listId = listNameToIdMap.get(listName);
String listId = listNameToIdMap.get(listName.toLowerCase());
RtmTaskSeries s = rtmService.tasks_add(timeline,
listId, "tmp");
return new RtmId(listId, s).toString();
@ -219,7 +218,9 @@ public class RTMSyncService extends SynchronizationService {
}
});
Preferences.setSyncRTMLastSync(activity, new Date());
// 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);
} catch (Exception e) {
showError(activity, e);

@ -7,13 +7,16 @@ import java.util.LinkedList;
import java.util.List;
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;
@ -23,6 +26,8 @@ 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
*
@ -32,35 +37,26 @@ import com.timsu.astrid.utilities.DialogUtilities;
public abstract class SynchronizationService {
private int id;
protected ProgressDialog progressDialog;
protected Handler syncHandler = new Handler();
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.post(new Runnable() {
@Override
public void run() {
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.show();
}
});
syncHandler = new Handler();
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);
syncHandler.post(new Runnable() {
@Override
public void run() {
progressDialog.dismiss();
}
});
}
/** Synchronize with the service */
@ -142,14 +138,27 @@ public abstract class SynchronizationService {
protected void synchronizeTasks(final Activity activity, List<TaskProxy> remoteTasks,
SynchronizeHelper helper) throws IOException {
final SyncStats stats = new SyncStats();
final StringBuilder log = new StringBuilder();
syncHandler.post(new Runnable() {
@Override
public void run() {
if(!progressDialog.isShowing())
progressDialog.show();
}
});
SyncDataController syncController = Synchronizer.getSyncController(activity);
TaskController taskController = Synchronizer.getTaskController(activity);
TagController tagController = Synchronizer.getTagController(activity);
AlertController alertController = Synchronizer.getAlertController(activity);
// get data out of the database (note we get non-completed tasks only)
// get data out of the database
HashSet<SyncMapping> mappings = syncController.getSyncMapping(getId());
HashSet<TaskIdentifier> localTasks = taskController.getActiveTaskIdentifiers();
HashSet<TaskIdentifier> activeTasks = taskController.
getActiveTaskIdentifiers();
HashSet<TaskIdentifier> allTasks = taskController.
getAllTaskIdentifiers();
HashMap<TagIdentifier, TagModelForView> tags =
tagController.getAllTagsAsMap(activity);
@ -179,6 +188,7 @@ public abstract class SynchronizationService {
}
// grab tasks without a sync mapping and create them remotely
log.append(">> on remote server:\n");
syncHandler.post(new Runnable() {
@Override
public void run() {
@ -186,8 +196,7 @@ public abstract class SynchronizationService {
progressDialog.setProgress(0);
}
});
HashSet<TaskIdentifier> newlyCreatedTasks = new HashSet<TaskIdentifier>(
localTasks);
HashSet<TaskIdentifier> newlyCreatedTasks = new HashSet<TaskIdentifier>(activeTasks);
newlyCreatedTasks.removeAll(mappedTasks);
for(TaskIdentifier taskId : newlyCreatedTasks) {
LinkedList<TagIdentifier> taskTags =
@ -210,6 +219,7 @@ public abstract class SynchronizationService {
helper.pushTask(localTask, mapping);
// update stats
log.append("add " + task.getName() + "\n");
stats.remoteCreatedTasks++;
syncHandler.post(new ProgressUpdater(stats.remoteCreatedTasks,
newlyCreatedTasks.size()));
@ -223,8 +233,9 @@ public abstract class SynchronizationService {
progressDialog.setProgress(0);
}
});
HashSet<TaskIdentifier> deletedTasks = new HashSet<TaskIdentifier>(mappedTasks);
deletedTasks.removeAll(localTasks);
HashSet<TaskIdentifier> deletedTasks = new HashSet<TaskIdentifier>(
mappedTasks);
deletedTasks.removeAll(allTasks);
for(TaskIdentifier taskId : deletedTasks) {
SyncMapping mapping = localIdToSyncMapping.get(taskId);
syncController.deleteSyncMapping(mapping);
@ -236,6 +247,7 @@ public abstract class SynchronizationService {
remoteChangeMap.remove(taskId);
// update stats
log.append("del #" + taskId.getId() + "\n");
stats.remoteDeletedTasks++;
syncHandler.post(new ProgressUpdater(stats.remoteDeletedTasks,
deletedTasks.size()));
@ -262,6 +274,9 @@ public abstract class SynchronizationService {
remoteConflict = remoteChangeMap.get(mapping.getTask());
localTask.mergeWithOther(remoteConflict);
stats.mergedTasks++;
log.append("mrg " + task.getName() + "\n");
} else {
log.append("upd " + task.getName() + "\n");
}
try {
@ -276,13 +291,21 @@ public abstract class SynchronizationService {
TaskProxy newTask = helper.refetchTask(remoteConflict);
remoteTasks.remove(remoteConflict);
remoteTasks.add(newTask);
}
stats.remoteUpdatedTasks++;
} else
stats.remoteUpdatedTasks++;
syncHandler.post(new ProgressUpdater(stats.remoteUpdatedTasks,
localChanges.size()));
}
// load remote information
log.append(">> on astrid:\n");
syncHandler.post(new Runnable() {
@Override
public void run() {
progressDialog.setMessage("Updating local tasks");
progressDialog.setProgress(0);
}
});
for(TaskProxy remoteTask : remoteTasks) {
SyncMapping mapping = null;
TaskModelForSync task = null;
@ -297,18 +320,23 @@ public abstract class SynchronizationService {
task = taskController.searchForTaskForSync(remoteTask.name);
if(task == null) {
task = new TaskModelForSync();
setupTaskDefaults(activity, task);
log.append("add " + remoteTask.name + "\n");
} else {
mapping = localIdToSyncMapping.get(task.getTaskIdentifier());
log.append("mov " + remoteTask.name + "\n");
}
} else {
mapping = remoteIdToSyncMapping.get(remoteTask.getRemoteId());
if(remoteTask.isDeleted()) {
taskController.deleteTask(mapping.getTask());
syncController.deleteSyncMapping(mapping);
log.append("del " + remoteTask.name + "\n");
stats.localDeletedTasks++;
continue;
}
log.append("upd " + remoteTask.name + "\n");
task = taskController.fetchTaskForSync(
mapping.getTask());
}
@ -346,6 +374,9 @@ public abstract class SynchronizationService {
syncController.saveSyncMapping(mapping);
stats.localCreatedTasks++;
}
Notifications.updateAlarm(activity, taskController, alertController,
task);
}
stats.localUpdatedTasks -= stats.localCreatedTasks;
@ -353,11 +384,18 @@ public abstract class SynchronizationService {
syncHandler.post(new Runnable() {
@Override
public void run() {
stats.showDialog(activity);
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
protected class SyncStats {
@ -372,17 +410,29 @@ public abstract class SynchronizationService {
int remoteDeletedTasks = 0;
/** Display a dialog with statistics */
public void showDialog(Context context) {
progressDialog.dismiss();
public void showDialog(final Activity activity, String log) {
progressDialog.hide();
Dialog.OnClickListener finishListener = new Dialog.OnClickListener() {
@Override
public void onClick(DialogInterface dialog,
int which) {
Synchronizer.continueSynchronization(activity);
}
};
if(equals(new SyncStats())) { // i.e. no change
DialogUtilities.okDialog(context, "Nothing to do!", null);
// nothing updated
if(localCreatedTasks + localUpdatedTasks + localDeletedTasks +
mergedTasks + remoteCreatedTasks + remoteDeletedTasks +
remoteUpdatedTasks == 0) {
DialogUtilities.okDialog(activity, "Sync: Up to date!", finishListener);
return;
}
StringBuilder sb = new StringBuilder();
sb.append(getName()).append(" Sync Results:"); // TODO i18n
sb.append("\n\n--- Astrid Tasks ---");
sb.append("\n\n");
sb.append(log);
sb.append("\n--- Summary: Astrid Tasks ---");
if(localCreatedTasks > 0)
sb.append("\nCreated: " + localCreatedTasks);
if(localUpdatedTasks > 0)
@ -393,7 +443,7 @@ public abstract class SynchronizationService {
if(mergedTasks > 0)
sb.append("\n\nMerged: " + localCreatedTasks);
sb.append("\n\n--- Remote Tasks ---");
sb.append("\n\n--- Summary: Remote Server ---");
if(remoteCreatedTasks > 0)
sb.append("\nCreated: " + remoteCreatedTasks);
if(remoteUpdatedTasks > 0)
@ -403,7 +453,7 @@ public abstract class SynchronizationService {
sb.append("\n");
DialogUtilities.okDialog(context, sb.toString(), null);
DialogUtilities.okDialog(activity, sb.toString(), finishListener);
}
}

@ -1,10 +1,11 @@
package com.timsu.astrid.sync;
import java.util.HashMap;
import java.util.Map;
import java.util.Date;
import android.app.Activity;
import android.content.Context;
import com.timsu.astrid.data.alerts.AlertController;
import com.timsu.astrid.data.sync.SyncDataController;
import com.timsu.astrid.data.tag.TagController;
import com.timsu.astrid.data.task.TaskController;
@ -12,37 +13,114 @@ import com.timsu.astrid.utilities.Preferences;
public class Synchronizer {
// Synchronization Service ID's
private static final int SYNC_ID_RTM = 1;
// --- public interface
/** Synchronize all activated sync services */
public static void synchronize(final Activity activity) {
// RTM sync
if(Preferences.shouldSyncRTM(activity)) {
services.get(SYNC_ID_RTM).synchronizeService(activity);
}
public interface SynchronizerListener {
void onSynchronizerFinished(int numServicesSynced);
}
/** Synchronize all activated sync services */
public static void synchronize(Activity activity, SynchronizerListener listener) {
currentStep = ServiceWrapper._FIRST_SERVICE.ordinal();
servicesSynced = 0;
callback = listener;
continueSynchronization(activity);
}
/** Clears tokens if services are disabled */
public static void clearUserData(Activity activity) {
if(Preferences.shouldSyncRTM(activity)) {
services.get(SYNC_ID_RTM).clearPersonalData(activity);
for(ServiceWrapper serviceWrapper : ServiceWrapper.values()) {
if(serviceWrapper.isActivated(activity)) {
serviceWrapper.service.clearPersonalData(activity);
}
}
}
// --- package helpers
// --- internal synchronization logic
/** Synchronization Services enumeration
* note that id must be kept constant!
* @author timsu
*
*/
private enum ServiceWrapper {
_FIRST_SERVICE(null) { // must be first entry
@Override
boolean isActivated(Context arg0) {
return false;
}
},
RTM(new RTMSyncService(SYNC_ID_RTM)) {
@Override
boolean isActivated(Context context) {
return Preferences.shouldSyncRTM(context);
}
},
_LAST_SERVICE(null) { // must be last entry
@Override
boolean isActivated(Context arg0) {
return false;
}
};
private SynchronizationService service;
private ServiceWrapper(SynchronizationService service) {
this.service = service;
}
abstract boolean isActivated(Context context);
}
/** Service map */
private static Map<Integer, SynchronizationService> services =
new HashMap<Integer, SynchronizationService>();
static {
services.put(SYNC_ID_RTM, new RTMSyncService(SYNC_ID_RTM));
// Internal state for the synchronization process
/** Current step in the sync process */
private static int currentStep;
/** # of services synchronized */
private static int servicesSynced;
/** On finished callback */
private static SynchronizerListener callback;
/** Called to do the next step of synchronization. Run me on the UI thread! */
static void continueSynchronization(Activity activity) {
ServiceWrapper serviceWrapper =
ServiceWrapper.values()[currentStep];
currentStep++;
switch(serviceWrapper) {
case _FIRST_SERVICE:
continueSynchronization(activity);
break;
case RTM:
if(Preferences.shouldSyncRTM(activity)) {
servicesSynced++;
serviceWrapper.service.synchronizeService(activity);
} else {
continueSynchronization(activity);
}
break;
case _LAST_SERVICE:
finishSynchronization(activity);
}
}
/** Called at the end of sync. */
private static void finishSynchronization(final Activity activity) {
closeControllers();
Preferences.setSyncLastSync(activity, new Date());
if(callback != null)
callback.onSynchronizerFinished(servicesSynced);
}
// --- package helpers
static SyncDataController getSyncController(Activity activity) {
if(syncController == null) {
syncController = new SyncDataController(activity);
@ -67,12 +145,21 @@ public class Synchronizer {
return tagController;
}
static AlertController getAlertController(Activity activity) {
if(alertController == null) {
alertController = new AlertController(activity);
alertController.open();
}
return alertController;
}
// --- controller stuff
private static SyncDataController syncController = null;
private static TaskController taskController = null;
private static TagController tagController = null;
private static AlertController alertController = null;
static void closeControllers() {
private static void closeControllers() {
if(syncController != null) {
syncController.close();
syncController = null;
@ -87,5 +174,10 @@ public class Synchronizer {
tagController.close();
tagController = null;
}
if(alertController != null) {
alertController.close();
alertController = null;
}
}
}

@ -18,6 +18,7 @@ public class Preferences {
private static final String P_SHOW_REPEAT_HELP = "repeathelp";
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";
// default values
private static final boolean DEFAULT_PERSISTENCE_MODE = true;
@ -162,6 +163,31 @@ public class Preferences {
R.string.p_sync_rtm), false);
}
/** returns the font size user wants on the front page */
public static Integer autoSyncFrequency(Context context) {
return getIntegerValue(context, R.string.p_sync_every);
}
/** Last Auto-Sync Date, or null */
public static Date getSyncLastSync(Context context) {
Long value = getPrefs(context).getLong(P_SYNC_LAST_SYNC, 0);
if(value == 0)
return null;
return new Date(value);
}
/** Set Last Auto-Sync Date */
public static void setSyncLastSync(Context context, Date date) {
if(date == null) {
clearPref(context, P_SYNC_LAST_SYNC);
return;
}
Editor editor = getPrefs(context).edit();
editor.putLong(P_SYNC_LAST_SYNC, date.getTime());
editor.commit();
}
// --- helper methods
@SuppressWarnings("unused")

Loading…
Cancel
Save