From c38890a8854d1688052a54b1700ab71aee35ac2b Mon Sep 17 00:00:00 2001 From: Tim Su Date: Tue, 13 Jul 2010 18:56:00 -0700 Subject: [PATCH] Remember the milk initial commit. stuff don't compile yet... --- astrid/.classpath | 2 +- .../astrid/api/SynchronizationProvider.java | 277 ++++++++ .../todoroo/astrid/rmilk/DetailExposer.java | 65 ++ .../astrid/rmilk/EditOperationExposer.java | 48 ++ .../todoroo/astrid/rmilk/FilterExposer.java | 70 ++ .../astrid/rmilk/MilkEditActivity.java | 86 +++ .../astrid/rmilk/MilkLoginActivity.java | 141 ++++ .../todoroo/astrid/rmilk/MilkPreferences.java | 127 ++++ .../todoroo/astrid/rmilk/StartupReceiver.java | 40 ++ .../com/todoroo/astrid/rmilk/Utilities.java | 135 ++++ .../astrid/rmilk/api/ApplicationInfo.java | 74 +++ .../com/todoroo/astrid/rmilk/api/Invoker.java | 287 ++++++++ .../com/todoroo/astrid/rmilk/api/Param.java | 64 ++ .../com/todoroo/astrid/rmilk/api/Prefs.java | 47 ++ .../astrid/rmilk/api/ServiceException.java | 50 ++ .../todoroo/astrid/rmilk/api/ServiceImpl.java | 615 ++++++++++++++++++ .../rmilk/api/ServiceInternalException.java | 53 ++ .../astrid/rmilk/api/data/RtmAuth.java | 66 ++ .../astrid/rmilk/api/data/RtmData.java | 120 ++++ .../astrid/rmilk/api/data/RtmFrob.java | 44 ++ .../astrid/rmilk/api/data/RtmList.java | 54 ++ .../astrid/rmilk/api/data/RtmLists.java | 52 ++ .../astrid/rmilk/api/data/RtmLocation.java | 60 ++ .../astrid/rmilk/api/data/RtmTask.java | 194 ++++++ .../astrid/rmilk/api/data/RtmTaskList.java | 61 ++ .../astrid/rmilk/api/data/RtmTaskNote.java | 101 +++ .../astrid/rmilk/api/data/RtmTaskNotes.java | 54 ++ .../astrid/rmilk/api/data/RtmTaskSeries.java | 170 +++++ .../astrid/rmilk/api/data/RtmTasks.java | 50 ++ .../astrid/rmilk/api/data/RtmTimeline.java | 39 ++ .../astrid/rmilk/api/data/RtmUser.java | 63 ++ .../astrid/rmilk/data/MilkDataService.java | 282 ++++++++ .../astrid/rmilk/data/MilkDatabase.java | 76 +++ .../todoroo/astrid/rmilk/data/MilkList.java | 103 +++ .../astrid/rmilk/sync/RTMSyncProvider.java | 465 +++++++++++++ astrid/res/layout/rmilk_edit_activity.xml | 40 ++ astrid/res/layout/rmilk_login_activity.xml | 41 ++ astrid/res/values/rmilk_strings.xml | 170 +++++ astrid/res/xml/preferences_rmilk.xml | 44 ++ .../astrid/utilities/DialogUtilities.java | 1 + 40 files changed, 4530 insertions(+), 1 deletion(-) create mode 100644 astrid/api-src/com/todoroo/astrid/api/SynchronizationProvider.java create mode 100644 astrid/plugin-src/com/todoroo/astrid/rmilk/DetailExposer.java create mode 100644 astrid/plugin-src/com/todoroo/astrid/rmilk/EditOperationExposer.java create mode 100644 astrid/plugin-src/com/todoroo/astrid/rmilk/FilterExposer.java create mode 100644 astrid/plugin-src/com/todoroo/astrid/rmilk/MilkEditActivity.java create mode 100644 astrid/plugin-src/com/todoroo/astrid/rmilk/MilkLoginActivity.java create mode 100644 astrid/plugin-src/com/todoroo/astrid/rmilk/MilkPreferences.java create mode 100644 astrid/plugin-src/com/todoroo/astrid/rmilk/StartupReceiver.java create mode 100644 astrid/plugin-src/com/todoroo/astrid/rmilk/Utilities.java create mode 100644 astrid/plugin-src/com/todoroo/astrid/rmilk/api/ApplicationInfo.java create mode 100644 astrid/plugin-src/com/todoroo/astrid/rmilk/api/Invoker.java create mode 100644 astrid/plugin-src/com/todoroo/astrid/rmilk/api/Param.java create mode 100644 astrid/plugin-src/com/todoroo/astrid/rmilk/api/Prefs.java create mode 100644 astrid/plugin-src/com/todoroo/astrid/rmilk/api/ServiceException.java create mode 100644 astrid/plugin-src/com/todoroo/astrid/rmilk/api/ServiceImpl.java create mode 100644 astrid/plugin-src/com/todoroo/astrid/rmilk/api/ServiceInternalException.java create mode 100644 astrid/plugin-src/com/todoroo/astrid/rmilk/api/data/RtmAuth.java create mode 100644 astrid/plugin-src/com/todoroo/astrid/rmilk/api/data/RtmData.java create mode 100644 astrid/plugin-src/com/todoroo/astrid/rmilk/api/data/RtmFrob.java create mode 100644 astrid/plugin-src/com/todoroo/astrid/rmilk/api/data/RtmList.java create mode 100644 astrid/plugin-src/com/todoroo/astrid/rmilk/api/data/RtmLists.java create mode 100644 astrid/plugin-src/com/todoroo/astrid/rmilk/api/data/RtmLocation.java create mode 100644 astrid/plugin-src/com/todoroo/astrid/rmilk/api/data/RtmTask.java create mode 100644 astrid/plugin-src/com/todoroo/astrid/rmilk/api/data/RtmTaskList.java create mode 100644 astrid/plugin-src/com/todoroo/astrid/rmilk/api/data/RtmTaskNote.java create mode 100644 astrid/plugin-src/com/todoroo/astrid/rmilk/api/data/RtmTaskNotes.java create mode 100644 astrid/plugin-src/com/todoroo/astrid/rmilk/api/data/RtmTaskSeries.java create mode 100644 astrid/plugin-src/com/todoroo/astrid/rmilk/api/data/RtmTasks.java create mode 100644 astrid/plugin-src/com/todoroo/astrid/rmilk/api/data/RtmTimeline.java create mode 100644 astrid/plugin-src/com/todoroo/astrid/rmilk/api/data/RtmUser.java create mode 100644 astrid/plugin-src/com/todoroo/astrid/rmilk/data/MilkDataService.java create mode 100644 astrid/plugin-src/com/todoroo/astrid/rmilk/data/MilkDatabase.java create mode 100644 astrid/plugin-src/com/todoroo/astrid/rmilk/data/MilkList.java create mode 100644 astrid/plugin-src/com/todoroo/astrid/rmilk/sync/RTMSyncProvider.java create mode 100644 astrid/res/layout/rmilk_edit_activity.xml create mode 100644 astrid/res/layout/rmilk_login_activity.xml create mode 100644 astrid/res/values/rmilk_strings.xml create mode 100644 astrid/res/xml/preferences_rmilk.xml diff --git a/astrid/.classpath b/astrid/.classpath index a68b469ef..a02fb551c 100644 --- a/astrid/.classpath +++ b/astrid/.classpath @@ -4,7 +4,7 @@ - + diff --git a/astrid/api-src/com/todoroo/astrid/api/SynchronizationProvider.java b/astrid/api-src/com/todoroo/astrid/api/SynchronizationProvider.java new file mode 100644 index 000000000..1cae40f09 --- /dev/null +++ b/astrid/api-src/com/todoroo/astrid/api/SynchronizationProvider.java @@ -0,0 +1,277 @@ +/** + * See the file "LICENSE" for the full license governing this code. + */ +package com.todoroo.astrid.api; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; + +import android.app.Notification; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.content.res.Resources; + +import com.timsu.astrid.R; +import com.todoroo.andlib.data.Property; +import com.todoroo.andlib.data.TodorooCursor; +import com.todoroo.andlib.data.Property.LongProperty; +import com.todoroo.andlib.service.Autowired; +import com.todoroo.andlib.service.ContextManager; +import com.todoroo.andlib.service.DependencyInjectionService; +import com.todoroo.andlib.service.ExceptionService; +import com.todoroo.andlib.service.NotificationManager; +import com.todoroo.astrid.model.Task; +import com.todoroo.astrid.rmilk.MilkPreferences; +import com.todoroo.astrid.service.TaskService; + +/** + * A helper class for writing synchronization services for Astrid. This class + * contains logic for merging incoming changes and writing outgoing changes. + *

+ * Use {@link synchronize} as the entry point for your synchronization service, + * which should handle authentication and then call {@link synchronizeTasks} to + * initiate synchronization. + * + * @author timsu + * + */ +public abstract class SynchronizationProvider { + + /** Notification Manager id for RMilk notifications */ + private static final int RMILK_NOTIFICATION_ID = -1; + + // --- abstract methods - your services should implement these + + /** + * Synchronize with the service + */ + abstract public void synchronize(); + + /** + * Push variables from given task to the remote server. + * + * @param task + * task proxy to push + * @param remoteTask + * remote task that we merged with. may be null + */ + abstract protected void push(Task task, Task remote) throws IOException; + + /** + * Create a task on the remote server. + * + * @return task to create + */ + abstract protected void create(Task task) throws IOException; + + /** + * Fetch remote task. Used to re-read merged tasks + * + * @param task + * task with id's to re-read + * @return new Task + */ + abstract protected Task read(Task task) throws IOException; + + /** + * Finds a task in the list with the same remote identifier(s) as + * the task passed in + * + * @return task from list if matches, null otherwise + */ + abstract protected Task matchTask(ArrayList tasks, Task target); + + /** + * Transfer remote identifier(s) from one task to another + */ + abstract protected void transferIdentifiers(Task source, Task destination); + + // --- implementation + + @Autowired + private TaskService taskService; + + @Autowired + private ExceptionService exceptionService; + + private final Notification notification; + private PendingIntent notificationIntent; + + public SynchronizationProvider() { + DependencyInjectionService.getInstance().inject(this); + + // initialize notification + int icon = android.R.drawable.stat_notify_sync; + long when = System.currentTimeMillis(); + notification = new Notification(icon, null, when); + } + + // --- utilities + + /** + * Utility method for showing synchronization errors. If message is null, + * the contents of the throwable is displayed. It is assumed that the error + * was logged separately. + */ + protected void showError(final Context context, Throwable e, String message) { + exceptionService.displayAndReportError(context, message, e); + } + + /** + * 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(Context context, String string) { + notification.setLatestEventInfo(context, + context.getString(R.string.rmilk_notification_title), string, notificationIntent); + } + + // --- synchronization logic + + /** + * 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 data synchronization data structure + */ + protected void synchronizeTasks(SyncData data) throws IOException { + + int length; + Task task = new Task(); + Context context = ContextManager.getContext(); + Resources r = context.getResources(); + + // create notification + notificationIntent = PendingIntent.getActivity(context, 0, new Intent(context, MilkPreferences.class), 0); + postUpdate(context, r.getString(R.string.rmilk_progress_starting)); + NotificationManager nm = new NotificationManager.AndroidNotificationManager(context); + nm.notify(RMILK_NOTIFICATION_ID, notification); + + // create internal data structures + HashMap remoteNewTaskNameMap = new HashMap(); + length = data.remoteUpdated.size(); + for(int i = 0; i < length; i++) { + Task remote = data.remoteUpdated.get(i); + if(remote.getId() != Task.NO_ID) + continue; + remoteNewTaskNameMap.put(remote.getValue(Task.TITLE), remote); + } + + // 1. CREATE: grab newly created tasks and create them remotely + length = data.localCreated.getCount(); + for(int i = 0; i < length; i++) { + data.localCreated.moveToNext(); + task.readFromCursor(data.localCreated); + + String taskTitle = task.getValue(Task.TITLE); + postUpdate(context, r.getString(R.string.rmilk_progress_localtx, + taskTitle)); + + /* 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, + * because user could have synchronized like this before. Instead, + * we create a mapping and do an update. + */ + if (remoteNewTaskNameMap.containsKey(taskTitle)) { + Task remote = remoteNewTaskNameMap.remove(taskTitle); + remote.setId(task.getId()); + + transferIdentifiers(remote, task); + push(task, remote); + read(remote); + } else { + create(task); + } + } + + // 2. UPDATE: for each updated local task + length = data.localUpdated.getCount(); + for(int i = 0; i < length; i++) { + data.localUpdated.moveToNext(); + task.readFromCursor(data.localUpdated); + postUpdate(context, r.getString(R.string.rmilk_progress_localtx, + task.getValue(Task.TITLE))); + + // if there is a conflict, merge + Task remote = matchTask(data.remoteUpdated, task); + if(remote != null) { + push(task, remote); + read(remote); + } else { + push(task, null); + } + } + + // 3. REMOTE: load remote information + + // Rearrange remoteTasks so completed tasks get synchronized first. + // This prevents bugs where a repeated task has two copies come down + // the wire, the new version and the completed old version. The new + // version would get merged, then completed, if done in the wrong order. + + Collections.sort(data.remoteUpdated, new Comparator() { + private static final int SENTINEL = -2; + private final int check(Task o1, Task o2, LongProperty property) { + long o1Property = o1.getValue(property); + long o2Property = o2.getValue(property); + if(o1Property != 0 && o2Property != 0) + return 0; + else if(o1Property != 0) + return -1; + else if(o2Property != 0) + return 1; + return SENTINEL; + } + public int compare(Task o1, Task o2) { + int comparison = check(o1, o2, Task.DELETION_DATE); + if(comparison != SENTINEL) + return comparison; + comparison = check(o1, o2, Task.COMPLETION_DATE); + if(comparison != SENTINEL) + return comparison; + return 0; + } + }); + + length = data.remoteUpdated.size(); + for(int i = 0; i < length; i++) { + task = data.remoteUpdated.get(i); + postUpdate(context, r.getString(R.string.rmilk_progress_remotetx, + task.getValue(Task.TITLE))); + + // save the data (TODO: save metadata) + taskService.save(task, true); + } + } + + // --- helper classes + + /** data structure builder */ + protected static class SyncData { + + public final Property[] properties; + public final ArrayList remoteUpdated; + + public final TodorooCursor localCreated; + public final TodorooCursor localUpdated; + + public SyncData(Property[] properties, + ArrayList remoteUpdated, + TodorooCursor localCreated, + TodorooCursor localUpdated) { + super(); + this.properties = properties; + this.remoteUpdated = remoteUpdated; + this.localCreated = localCreated; + this.localUpdated = localUpdated; + } + + } +} diff --git a/astrid/plugin-src/com/todoroo/astrid/rmilk/DetailExposer.java b/astrid/plugin-src/com/todoroo/astrid/rmilk/DetailExposer.java new file mode 100644 index 000000000..62cc801f9 --- /dev/null +++ b/astrid/plugin-src/com/todoroo/astrid/rmilk/DetailExposer.java @@ -0,0 +1,65 @@ +/** + * See the file "LICENSE" for the full license governing this code. + */ +package com.todoroo.astrid.rmilk; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; + +import com.todoroo.astrid.R; +import com.todoroo.astrid.api.AstridApiConstants; +import com.todoroo.astrid.api.TaskDetail; +import com.todoroo.astrid.model.Task; +import com.todoroo.astrid.rmilk.data.MilkDataService; + +/** + * Exposes {@link TaskDetail}s for Remember the Milk: + * - RTM list + * - RTM repeat information + * - whether task has been changed + * + * @author Tim Su + * + */ +public class DetailExposer extends BroadcastReceiver { + + @Override + public void onReceive(Context context, Intent intent) { + // if we aren't logged in, don't expose features + if(!Utilities.isLoggedIn()) + return; + + long taskId = intent.getLongExtra(AstridApiConstants.EXTRAS_TASK_ID, -1); + if(taskId == -1) + return; + + MilkDataService service = new MilkDataService(context); + Task task = service.readTask(taskId); + + if(task == null) + return; + + TaskDetail[] details = new TaskDetail[2]; + + String listId = task.getValue(MilkDataService.LIST_ID); + if(listId != null && listId.length() > 0) + details[0] = new TaskDetail(context.getString(R.string.rmilk_TLA_list, + service.getList(listId))); + else + details[0] = null; + + int repeat = task.getValue(MilkDataService.REPEAT); + if(repeat != 0) + details[1] = new TaskDetail(context.getString(R.string.rmilk_TLA_repeat)); + else + details[1] = null; + + // transmit + Intent broadcastIntent = new Intent(AstridApiConstants.BROADCAST_SEND_DETAILS); + broadcastIntent.putExtra(AstridApiConstants.EXTRAS_ITEMS, details); + broadcastIntent.putExtra(AstridApiConstants.EXTRAS_TASK_ID, taskId); + context.sendBroadcast(broadcastIntent, AstridApiConstants.PERMISSION_READ); + } + +} diff --git a/astrid/plugin-src/com/todoroo/astrid/rmilk/EditOperationExposer.java b/astrid/plugin-src/com/todoroo/astrid/rmilk/EditOperationExposer.java new file mode 100644 index 000000000..e6d696235 --- /dev/null +++ b/astrid/plugin-src/com/todoroo/astrid/rmilk/EditOperationExposer.java @@ -0,0 +1,48 @@ +/** + * See the file "LICENSE" for the full license governing this code. + */ +package com.todoroo.astrid.rmilk; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; + +import com.todoroo.astrid.R; +import com.todoroo.astrid.api.AstridApiConstants; +import com.todoroo.astrid.api.EditOperation; + +/** + * Exposes {@link EditOperation} for Remember the Milk + * + * @author Tim Su + * + */ +public class EditOperationExposer extends BroadcastReceiver { + + @Override + public void onReceive(Context context, Intent intent) { + // if we aren't logged in, don't expose features + if(!Utilities.isLoggedIn()) + return; + + long taskId = intent + .getLongExtra(AstridApiConstants.EXTRAS_TASK_ID, -1); + if (taskId == -1) + return; + + EditOperation taskEditOperation; + Intent editIntent = new Intent(context, MilkEditActivity.class); + taskEditOperation = new EditOperation(context.getString( + R.string.rmilk_EOE_button), editIntent); + + // transmit + EditOperation[] operations = new EditOperation[1]; + operations[0] = taskEditOperation; + Intent broadcastIntent = new Intent( + AstridApiConstants.BROADCAST_SEND_EDIT_OPERATIONS); + broadcastIntent.putExtra(AstridApiConstants.EXTRAS_ITEMS, operations); + context.sendBroadcast(broadcastIntent, + AstridApiConstants.PERMISSION_READ); + } + +} diff --git a/astrid/plugin-src/com/todoroo/astrid/rmilk/FilterExposer.java b/astrid/plugin-src/com/todoroo/astrid/rmilk/FilterExposer.java new file mode 100644 index 000000000..3cc33097e --- /dev/null +++ b/astrid/plugin-src/com/todoroo/astrid/rmilk/FilterExposer.java @@ -0,0 +1,70 @@ +/** + * See the file "LICENSE" for the full license governing this code. + */ +package com.todoroo.astrid.rmilk; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; + +import com.todoroo.astrid.R; +import com.todoroo.astrid.api.AstridApiConstants; +import com.todoroo.astrid.api.Filter; +import com.todoroo.astrid.api.FilterCategory; +import com.todoroo.astrid.api.FilterListHeader; +import com.todoroo.astrid.api.FilterListItem; +import com.todoroo.astrid.rmilk.Utilities.ListContainer; +import com.todoroo.astrid.rmilk.data.MilkDataService; + +/** + * Exposes filters based on RTM lists + * + * @author Tim Su + * + */ +public class FilterExposer extends BroadcastReceiver { + + + @SuppressWarnings("nls") + private Filter filterFromList(Context context, ListContainer list) { + String listTitle = context.getString(R.string.rmilk_FEx_list_item). + replace("$N", list.name).replace("$C", Integer.toString(list.count)); + String title = context.getString(R.string.rmilk_FEx_list_title, list.name); + Filter filter = new Filter(listTitle, title, + "TODO", + "TODO"); + + return filter; + } + + @Override + public void onReceive(Context context, Intent intent) { + // if we aren't logged in, don't expose features + if(!Utilities.isLoggedIn()) + return; + + MilkDataService service = new MilkDataService(context); + ListContainer[] lists = service.getListsWithCounts(); + + // If user does not have any tags, don't show this section at all + if(lists.length == 0) + return; + + Filter[] listFilters = new Filter[lists.length]; + for(int i = 0; i < lists.length; i++) + listFilters[i] = filterFromList(context, lists[i]); + + FilterListHeader rtmHeader = new FilterListHeader(context.getString(R.string.rmilk_FEx_header)); + FilterCategory rtmLists = new FilterCategory( + context.getString(R.string.rmilk_FEx_list), listFilters); + + // transmit filter list + FilterListItem[] list = new FilterListItem[2]; + list[0] = rtmHeader; + list[1] = rtmLists; + Intent broadcastIntent = new Intent(AstridApiConstants.BROADCAST_SEND_FILTERS); + broadcastIntent.putExtra(AstridApiConstants.EXTRAS_ITEMS, list); + context.sendBroadcast(broadcastIntent, AstridApiConstants.PERMISSION_READ); + } + +} diff --git a/astrid/plugin-src/com/todoroo/astrid/rmilk/MilkEditActivity.java b/astrid/plugin-src/com/todoroo/astrid/rmilk/MilkEditActivity.java new file mode 100644 index 000000000..3f132b574 --- /dev/null +++ b/astrid/plugin-src/com/todoroo/astrid/rmilk/MilkEditActivity.java @@ -0,0 +1,86 @@ +/** + * See the file "LICENSE" for the full license governing this code. + */ +package com.todoroo.astrid.rmilk; + +import android.app.Activity; +import android.os.Bundle; +import android.view.View; +import android.view.View.OnClickListener; +import android.widget.ArrayAdapter; +import android.widget.Button; +import android.widget.EditText; +import android.widget.Spinner; + +import com.todoroo.astrid.R; +import com.todoroo.astrid.api.AstridApiConstants; +import com.todoroo.astrid.model.Task; +import com.todoroo.astrid.rmilk.Utilities.ListContainer; +import com.todoroo.astrid.rmilk.data.MilkDataService; + +/** + * Displays a dialog box for users to edit their RTM stuff + * + * @author Tim Su + * + */ +public class MilkEditActivity extends Activity { + + long taskId; + MilkDataService service; + Task model; + + Spinner list; + EditText repeat; + + /** Called when loading up the activity */ + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + taskId = getIntent().getLongExtra(AstridApiConstants.EXTRAS_TASK_ID, -1); + if(taskId == -1) + return; + + setContentView(R.layout.rmilk_edit_activity); + setTitle(R.string.rmilk_MEA_title); + + ((Button)findViewById(R.id.ok)).setOnClickListener(new OnClickListener() { + public void onClick(View v) { + saveAndQuit(); + } + }); + ((Button)findViewById(R.id.cancel)).setOnClickListener(new OnClickListener() { + public void onClick(View v) { + setResult(RESULT_CANCELED); + finish(); + } + }); + + // load all lists + service = new MilkDataService(this); + ListContainer[] lists = service.getLists(); + + list = (Spinner) findViewById(R.id.rmilk_list); + ArrayAdapter listAdapter = new ArrayAdapter( + this, android.R.layout.simple_spinner_item, + lists); + listAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + list.setAdapter(listAdapter); + + // load model + model = service.readTask(taskId); + repeat.setText(model.getValue(MilkDataService.REPEAT)); + list.setSelection(0); // TODO + } + + /** + * Save tags to task and then quit + */ + protected void saveAndQuit() { + // model.setValue(DataService.LIST_ID, list.getSelectedItem()); TODO + + setResult(RESULT_OK); + finish(); + } + +} \ No newline at end of file diff --git a/astrid/plugin-src/com/todoroo/astrid/rmilk/MilkLoginActivity.java b/astrid/plugin-src/com/todoroo/astrid/rmilk/MilkLoginActivity.java new file mode 100644 index 000000000..a28da10b5 --- /dev/null +++ b/astrid/plugin-src/com/todoroo/astrid/rmilk/MilkLoginActivity.java @@ -0,0 +1,141 @@ +/* + * 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.todoroo.astrid.rmilk; + +import android.app.Activity; +import android.os.Bundle; +import android.os.Handler; +import android.view.View; +import android.view.View.OnClickListener; +import android.webkit.WebView; +import android.webkit.WebViewClient; +import android.widget.Button; + +import com.timsu.astrid.R; +import com.todoroo.andlib.service.Autowired; +import com.todoroo.andlib.service.DependencyInjectionService; +import com.todoroo.andlib.utility.DialogUtilities; + +/** + * This activity displays a WebView that allows users to log in to the + * synchronization provider requested. A callback method determines whether + * their login was successful and therefore whether to dismiss the dialog. + * + * @author timsu + * + */ +public class MilkLoginActivity extends Activity { + + @Autowired + DialogUtilities dialogUtilities; + + // --- bundle arguments + + /** + * URL to display + */ + public static final String URL_TOKEN = "u"; //$NON-NLS-1$ + + // --- callback + + /** Callback interface */ + public interface SyncLoginCallback { + /** + * Verifies whether the user's login attempt was successful. Will be + * called off of the UI thread, use the handler to post messages. + * + * @return error string, or null if sync was successful + */ + public String verifyLogin(Handler handler); + } + + protected static SyncLoginCallback callback = null; + + /** Sets callback method */ + public static void setCallback(SyncLoginCallback newCallback) { + callback = newCallback; + } + + // --- ui initialization + + public MilkLoginActivity() { + super(); + DependencyInjectionService.getInstance().inject(this); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.rmilk_login_activity); + + String urlParam = getIntent().getStringExtra(URL_TOKEN); + + final WebView webView = (WebView)findViewById(R.id.browser); + Button done = (Button)findViewById(R.id.done); + Button cancel = (Button)findViewById(R.id.cancel); + + webView.setWebViewClient(new WebViewClient() { + @Override + public boolean shouldOverrideUrlLoading(WebView view, String url) { + view.loadUrl(url); + return true; + } + }); + webView.getSettings().setJavaScriptEnabled(true); + webView.getSettings().setSavePassword(false); + webView.getSettings().setSupportZoom(true); + webView.loadUrl(urlParam); + + done.setOnClickListener(new OnClickListener() { + public void onClick(View v) { + final Handler handler = new Handler(); + + if(callback == null) { + finish(); + return; + } + + new Thread(new Runnable() { + public void run() { + final String result = callback.verifyLogin(handler); + if(result == null) { + finish(); + } else { + // display the error + handler.post(new Runnable() { + public void run() { + dialogUtilities.okDialog(MilkLoginActivity.this, + result, null); + } + }); + } + } + }).start(); + } + }); + + cancel.setOnClickListener(new OnClickListener() { + public void onClick(View v) { + setResult(RESULT_CANCELED); + finish(); + } + }); + } +} \ No newline at end of file diff --git a/astrid/plugin-src/com/todoroo/astrid/rmilk/MilkPreferences.java b/astrid/plugin-src/com/todoroo/astrid/rmilk/MilkPreferences.java new file mode 100644 index 000000000..c47ad29db --- /dev/null +++ b/astrid/plugin-src/com/todoroo/astrid/rmilk/MilkPreferences.java @@ -0,0 +1,127 @@ +package com.todoroo.astrid.rmilk; + +import android.content.res.Resources; +import android.graphics.Color; +import android.os.Bundle; +import android.preference.CheckBoxPreference; +import android.preference.EditTextPreference; +import android.preference.ListPreference; +import android.preference.Preference; +import android.preference.PreferenceActivity; +import android.preference.PreferenceGroup; +import android.preference.PreferenceScreen; +import android.preference.Preference.OnPreferenceChangeListener; +import android.preference.Preference.OnPreferenceClickListener; +import android.view.View; +import android.view.ViewGroup.OnHierarchyChangeListener; +import android.widget.ListView; + +import com.timsu.astrid.R; +import com.todoroo.andlib.utility.AndroidUtilities; +import com.todoroo.astrid.rmilk.sync.RTMSyncProvider; + +/** + * Displays synchronization preferences and an action panel so users can + * initiate actions from the menu. + * + * @author timsu + * + */ +public class MilkPreferences extends PreferenceActivity { + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + addPreferencesFromResource(R.xml.preferences_rmilk); + + PreferenceScreen screen = getPreferenceScreen(); + initializePreference(screen); + + // status + final String status = "Please Log In To RTM!"; //$NON-NLS-1$ + final int statusColor = Color.RED; + + Resources r = getResources(); + Preference preference = screen.findPreference(r.getString(R.string.rmilk_MPr_status_key)); + preference.setTitle(status); + + getListView().setOnHierarchyChangeListener(new OnHierarchyChangeListener() { + public void onChildViewRemoved(View arg0, View arg1) { + // + } + public void onChildViewAdded(View parent, View child) { + if(((ListView)parent).getChildCount() == 2) { + child.setBackgroundColor(statusColor); + } + } + }); + + // action buttons + Preference syncAction = screen.getPreferenceManager().findPreference( + getString(R.string.rmilk_MPr_sync_key)); + syncAction.setOnPreferenceClickListener(new OnPreferenceClickListener() { + public boolean onPreferenceClick(Preference p) { + new RTMSyncProvider().synchronize(MilkPreferences.this); + return true; + } + }); + /*Preference clearDataPreference = screen.getPreferenceManager().findPreference( + getString(R.string.rmilk_MPr_forget_key));*/ + } + + private void initializePreference(Preference preference) { + if(preference instanceof PreferenceGroup) { + PreferenceGroup group = (PreferenceGroup)preference; + for(int i = 0; i < group.getPreferenceCount(); i++) { + initializePreference(group.getPreference(i)); + } + } else { + Object value = null; + if(preference instanceof ListPreference) + value = ((ListPreference)preference).getValue(); + else if(preference instanceof CheckBoxPreference) + value = ((CheckBoxPreference)preference).isChecked(); + else if(preference instanceof EditTextPreference) + value = ((EditTextPreference)preference).getText(); + else + return; + + updatePreferences(preference, value); + + preference.setOnPreferenceChangeListener(new OnPreferenceChangeListener() { + public boolean onPreferenceChange(Preference myPreference, Object newValue) { + return updatePreferences(myPreference, newValue); + } + }); + } + } + + /** + * + * @param resource if null, updates all resources + */ + protected boolean updatePreferences(Preference preference, Object value) { + Resources r = getResources(); + + // interval + if(r.getString(R.string.rmilk_MPr_interval_key).equals(preference.getKey())) { + int index = AndroidUtilities.indexOf(r.getStringArray(R.array.rmilk_MPr_interval_values), (String)value); + if(index <= 0) + preference.setSummary(R.string.rmilk_MPr_interval_desc_disabled); + else + preference.setSummary(r.getString(R.string.rmilk_MPr_interval_desc, + r.getStringArray(R.array.rmilk_MPr_interval_entries)[index])); + } + + // shortcut + else if(r.getString(R.string.rmilk_MPr_shortcut_key).equals(preference.getKey())) { + if((Boolean)value) { + preference.setSummary(R.string.rmilk_MPr_shortcut_desc_enabled); + } else { + preference.setSummary(R.string.rmilk_MPr_shortcut_desc_disabled); + } + } + + return true; + } +} \ No newline at end of file diff --git a/astrid/plugin-src/com/todoroo/astrid/rmilk/StartupReceiver.java b/astrid/plugin-src/com/todoroo/astrid/rmilk/StartupReceiver.java new file mode 100644 index 000000000..894dbcfea --- /dev/null +++ b/astrid/plugin-src/com/todoroo/astrid/rmilk/StartupReceiver.java @@ -0,0 +1,40 @@ +/** + * See the file "LICENSE" for the full license governing this code. + */ +package com.todoroo.astrid.rmilk; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.SharedPreferences.Editor; +import android.content.res.Resources; +import android.preference.PreferenceManager; + +import com.todoroo.astrid.R; + +public class StartupReceiver extends BroadcastReceiver { + + public static void setPreferenceDefaults(Context context) { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + Editor editor = prefs.edit(); + Resources r = context.getResources(); + + if(!prefs.contains(r.getString(R.string.rmilk_MPr_interval_key))) { + editor.putString(r.getString(R.string.rmilk_MPr_interval_key), + Integer.toString(0)); + } + if(!prefs.contains(r.getString(R.string.rmilk_MPr_shortcut_key))) { + editor.putBoolean(r.getString(R.string.rmilk_MPr_shortcut_key), true); + } + + editor.commit(); + } + + @Override + /** Called when this plug-in run for the first time (installed, upgrade, or device was rebooted */ + public void onReceive(final Context context, Intent intent) { + setPreferenceDefaults(context); + } + +} diff --git a/astrid/plugin-src/com/todoroo/astrid/rmilk/Utilities.java b/astrid/plugin-src/com/todoroo/astrid/rmilk/Utilities.java new file mode 100644 index 000000000..b5c722ce0 --- /dev/null +++ b/astrid/plugin-src/com/todoroo/astrid/rmilk/Utilities.java @@ -0,0 +1,135 @@ +/** + * See the file "LICENSE" for the full license governing this code. + */ +package com.todoroo.astrid.rmilk; + +import java.util.Date; + +import android.content.SharedPreferences; +import android.content.SharedPreferences.Editor; +import android.preference.PreferenceManager; + +import com.timsu.astrid.R; +import com.todoroo.andlib.service.ContextManager; +import com.todoroo.astrid.rmilk.data.MilkList; + +/** + * Utility constants + * + * @author Tim Su + * + */ +@SuppressWarnings("nls") +public class Utilities { + + // --- helper classes + + /** + * Helper class for describing RTM lists + * + * @author Tim Su + */ + public static class ListContainer { + public ListContainer(MilkList list) { + this(list.getValue(MilkList.ID), list.getValue(MilkList.NAME)); + } + + public ListContainer(long id, String name) { + this.id = id; + this.name = name; + this.count = -1; + } + + @Override + public String toString() { + return name; + } + + public long id; + public String name; + public int count; + } + + // --- Metadata keys + // NOTE: no sql escaping is provided for keys + + public static final String KEY_LIST_ID = "rmilk_listId"; + + public static final String KEY_TASK_SERIES_ID = "rmilk_taskSeriesId"; + + public static final String KEY_TASK_ID = "rmilk_taskId"; + + public static final String KEY_REPEAT = "rmilk_repeat"; + + public static final String KEY_UPDATED = "rmilk_updated"; + + // --- Preference Keys + + private static final String PREF_TOKEN = "rmilk_token"; + + private static final String PREF_LAST_SYNC = "rmilk_last_sync"; + + // --- Preference Utility Methods + + /** Get preferences object from the context */ + private static SharedPreferences getPrefs() { + return PreferenceManager.getDefaultSharedPreferences(ContextManager.getContext()); + } + + /** + * @return true if we have a token for this user, false otherwise + */ + public static boolean isLoggedIn() { + return getPrefs().getString(PREF_TOKEN, null) != null; + } + + /** RTM authentication token, or null if doesn't exist */ + public static String getToken() { + return getPrefs().getString(PREF_TOKEN, null); + } + + /** Sets the RTM authentication token. Set to null to clear. */ + public static void setToken(String setting) { + Editor editor = getPrefs().edit(); + editor.putString(PREF_TOKEN, setting); + editor.commit(); + } + + /** RTM Last Successful Sync Date, or null */ + public static Date getLastSyncDate() { + Long value = getPrefs().getLong(PREF_LAST_SYNC, 0); + if (value == 0) + return null; + return new Date(value); + } + + /** Set RTM Last Successful Sync Date */ + public static void setLastSyncDate(Date date) { + Editor editor = getPrefs().edit(); + if (date == null) { + editor.remove(PREF_LAST_SYNC); + } else { + editor.putLong(PREF_LAST_SYNC, date.getTime()); + } + editor.commit(); + } + + /** + * Reads the frequency, in seconds, auto-sync should occur. + * + * @return seconds duration, or 0 if not desired + */ + public static int getSyncAutoSyncFrequency() { + String value = getPrefs().getString( + ContextManager.getContext().getString( + R.string.rmilk_MPr_interval_key), null); + if (value == null) + return 0; + try { + return Integer.parseInt(value); + } catch (Exception e) { + return 0; + } + } + +} diff --git a/astrid/plugin-src/com/todoroo/astrid/rmilk/api/ApplicationInfo.java b/astrid/plugin-src/com/todoroo/astrid/rmilk/api/ApplicationInfo.java new file mode 100644 index 000000000..c7bb56a2d --- /dev/null +++ b/astrid/plugin-src/com/todoroo/astrid/rmilk/api/ApplicationInfo.java @@ -0,0 +1,74 @@ +/* + * Copyright 2007, MetaDimensional Technologies Inc. + * + * + * This file is part of the RememberTheMilk Java API. + * + * The RememberTheMilk Java API is free software; you can redistribute it + * and/or modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * The RememberTheMilk Java API 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 Lesser + * General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ +package com.todoroo.astrid.rmilk.api; + +/** + * Encapsulates information about an application that is a client of RememberTheMilk. Includes information required by RTM to connect: the API key and + * the shared secret. + * + * @author Will Ross Jun 22, 2007 + */ +public class ApplicationInfo +{ + + private final String apiKey; + + private final String sharedSecret; + + private final String name; + + private final String authToken; + + public ApplicationInfo(String apiKey, String sharedSecret, String name) + { + this(apiKey, sharedSecret, name, null); + } + + public ApplicationInfo(String apiKey, String sharedSecret, String name, + String authToken) + { + super(); + this.apiKey = apiKey; + this.sharedSecret = sharedSecret; + this.name = name; + this.authToken = authToken; + } + + public String getApiKey() + { + return apiKey; + } + + public String getSharedSecret() + { + return sharedSecret; + } + + public String getName() + { + return name; + } + + public String getAuthToken() + { + return authToken; + } + +} diff --git a/astrid/plugin-src/com/todoroo/astrid/rmilk/api/Invoker.java b/astrid/plugin-src/com/todoroo/astrid/rmilk/api/Invoker.java new file mode 100644 index 000000000..0a1b10515 --- /dev/null +++ b/astrid/plugin-src/com/todoroo/astrid/rmilk/api/Invoker.java @@ -0,0 +1,287 @@ +/* + * Copyright 2007, MetaDimensional Technologies Inc. + * + * + * This file is part of the RememberTheMilk Java API. + * + * The RememberTheMilk Java API is free software; you can redistribute it + * and/or modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * The RememberTheMilk Java API 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 Lesser + * General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ +package com.todoroo.astrid.rmilk.api; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; + +import org.apache.http.HttpResponse; +import org.apache.http.HttpStatus; +import org.apache.http.client.HttpClient; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.impl.client.DefaultHttpClient; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.xml.sax.SAXException; + +import android.util.Log; + +/** + * Handles the details of invoking a method on the RTM REST API. + * + * @author Will Ross Jun 21, 2007 + */ +@SuppressWarnings("nls") +public class Invoker { + + private static final String TAG = "rtm-invoker"; //$NON-NLS-1$ + + private static final DocumentBuilder builder; + static + { + // Done this way because the builder is marked "final" + DocumentBuilder aBuilder; + try + { + final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setNamespaceAware(false); + factory.setValidating(false); + aBuilder = factory.newDocumentBuilder(); + } + catch (Exception exception) + { + Log.e(TAG, "Unable to construct a document builder", exception); + aBuilder = null; + } + builder = aBuilder; + } + + private static final String ENCODING = "UTF-8"; //$NON-NLS-1$ + + private static final String API_SIG_PARAM = "api_sig"; //$NON-NLS-1$ + + private static final long INVOCATION_INTERVAL = 400; + + private long lastInvocation; + + private final ApplicationInfo applicationInfo; + + private final MessageDigest digest; + + private String serviceRelativeUri; + + private HttpClient httpClient; + + public Invoker(@SuppressWarnings("unused") String serverHostName, + @SuppressWarnings("unused") int serverPortNumber, + String serviceRelativeUri, ApplicationInfo applicationInfo) + throws ServiceInternalException { + + this.serviceRelativeUri = serviceRelativeUri; + + httpClient = new DefaultHttpClient(); + + lastInvocation = System.currentTimeMillis(); + this.applicationInfo = applicationInfo; + + try { + digest = MessageDigest.getInstance("md5"); //$NON-NLS-1$ + } catch (NoSuchAlgorithmException e) { + throw new ServiceInternalException( + "Could not create properly the MD5 digest", e); + } + } + + private StringBuffer computeRequestUri(Param... params) + throws ServiceInternalException { + final StringBuffer requestUri = new StringBuffer(serviceRelativeUri); + if (params.length > 0) { + requestUri.append("?"); + } + for (Param param : params) { + try { + requestUri.append(param.getName()).append("=").append( + URLEncoder.encode(param.getValue(), ENCODING)).append( + "&"); + } catch (Exception exception) { + final StringBuffer message = new StringBuffer( + "Cannot encode properly the HTTP GET request URI: cannot execute query"); + Log.e(TAG, message.toString(), exception); + throw new ServiceInternalException(message.toString()); + } + } + requestUri.append(API_SIG_PARAM).append("=").append(calcApiSig(params)); + return requestUri; + } + + /** Call invoke with a false repeat */ + public Element invoke(Param... params) throws ServiceException { + return invoke(false, params); + } + + public Element invoke(boolean repeat, Param... params) + throws ServiceException { + long timeSinceLastInvocation = System.currentTimeMillis() - + lastInvocation; + if (timeSinceLastInvocation < INVOCATION_INTERVAL) { + // In order not to invoke the RTM service too often + try { + Thread.sleep(INVOCATION_INTERVAL - timeSinceLastInvocation); + } catch (InterruptedException e) { + return null; + } + } + + // We compute the URI + final StringBuffer requestUri = computeRequestUri(params); + HttpResponse response = null; + + final HttpGet request = new HttpGet("http://" //$NON-NLS-1$ + + ServiceImpl.SERVER_HOST_NAME + requestUri.toString()); + final String methodUri = request.getRequestLine().getUri(); + + Element result; + try { + Log.i(TAG, "Executing the method:" + methodUri); //$NON-NLS-1$ + response = httpClient.execute(request); + lastInvocation = System.currentTimeMillis(); + + final int statusCode = response.getStatusLine().getStatusCode(); + if (statusCode != HttpStatus.SC_OK) { + Log.e(TAG, "Method failed: " + response.getStatusLine()); //$NON-NLS-1$ + + // Tim: HTTP error. Let's wait a little bit + if (!repeat) { + try { + Thread.sleep(1500); + } catch (InterruptedException e) { + // ignore + } + return invoke(true, params); + } + + throw new ServiceInternalException("method failed: " + + response.getStatusLine()); + } + + final Document responseDoc = builder.parse(response.getEntity() + .getContent()); + final Element wrapperElt = responseDoc.getDocumentElement(); + if (!wrapperElt.getNodeName().equals("rsp")) { + throw new ServiceInternalException( + "unexpected response returned by RTM service: " + + wrapperElt.getNodeName()); + } else { + String stat = wrapperElt.getAttribute("stat"); + if (stat.equals("fail")) { + Node errElt = wrapperElt.getFirstChild(); + while (errElt != null + && (errElt.getNodeType() != Node.ELEMENT_NODE || !errElt + .getNodeName().equals("err"))) { + errElt = errElt.getNextSibling(); + } + if (errElt == null) { + throw new ServiceInternalException( + "unexpected response returned by RTM service: " + + wrapperElt.getNodeValue()); + } else { + throw new ServiceException(Integer + .parseInt(((Element) errElt) + .getAttribute("code")), + ((Element) errElt).getAttribute("msg")); + } + } else { + Node dataElt = wrapperElt.getFirstChild(); + while (dataElt != null + && (dataElt.getNodeType() != Node.ELEMENT_NODE || dataElt + .getNodeName().equals("transaction") == true)) { + try { + Node nextSibling = dataElt.getNextSibling(); + if (nextSibling == null) { + break; + } else { + dataElt = nextSibling; + } + } catch (IndexOutOfBoundsException exception) { + // Some implementation may throw this exception, + // instead of returning a null sibling + break; + } + } + if (dataElt == null) { + throw new ServiceInternalException( + "unexpected response returned by RTM service: " + + wrapperElt.getNodeValue()); + } else { + result = (Element) dataElt; + } + } + } + } catch (IOException e) { + throw new ServiceInternalException("Error making connection: " + + e.getMessage(), e); + } catch (SAXException e) { + // repeat call if possible. + if(!repeat) + return invoke(true, params); + else + throw new ServiceInternalException("Error parsing response. " + + "Please try sync again!", e); + } finally { + httpClient.getConnectionManager().closeExpiredConnections(); + } + + return result; + } + + final String calcApiSig(Param... params) throws ServiceInternalException { + try { + digest.reset(); + digest.update(applicationInfo.getSharedSecret().getBytes(ENCODING)); + List sorted = Arrays.asList(params); + Collections.sort(sorted); + for (Param param : sorted) { + digest.update(param.getName().getBytes(ENCODING)); + digest.update(param.getValue().getBytes(ENCODING)); + } + return convertToHex(digest.digest()); + } catch (UnsupportedEncodingException e) { + throw new ServiceInternalException( + "cannot hahdle properly the encoding", e); + } + } + + private static String convertToHex(byte[] data) { + StringBuffer buf = new StringBuffer(); + for (int i = 0; i < data.length; i++) { + int halfbyte = (data[i] >>> 4) & 0x0F; + int two_halfs = 0; + do { + if ((0 <= halfbyte) && (halfbyte <= 9)) + buf.append((char) ('0' + halfbyte)); + else + buf.append((char) ('a' + (halfbyte - 10))); + halfbyte = data[i] & 0x0F; + } while (two_halfs++ < 1); + } + return buf.toString(); + } + +} diff --git a/astrid/plugin-src/com/todoroo/astrid/rmilk/api/Param.java b/astrid/plugin-src/com/todoroo/astrid/rmilk/api/Param.java new file mode 100644 index 000000000..f10332826 --- /dev/null +++ b/astrid/plugin-src/com/todoroo/astrid/rmilk/api/Param.java @@ -0,0 +1,64 @@ +/* + * Copyright 2007, MetaDimensional Technologies Inc. + * + * + * This file is part of the RememberTheMilk Java API. + * + * The RememberTheMilk Java API is free software; you can redistribute it + * and/or modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * The RememberTheMilk Java API 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 Lesser + * General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ +package com.todoroo.astrid.rmilk.api; + +import java.util.Date; + +import com.todoroo.astrid.rmilk.api.data.RtmData; + +/** + * + * @author Will Ross Jun 21, 2007 + */ +public class Param + implements Comparable +{ + + private final String name; + + private final String value; + + public Param(String name, String value) + { + this.name = name; + this.value = value; + } + + public Param(String name, Date value) + { + this.name = name; + this.value = RtmData.formatDate(value); + } + + public String getName() + { + return name; + } + + public String getValue() + { + return value; + } + + public int compareTo(Param p) + { + return name.compareTo(p.getName()); + } +} diff --git a/astrid/plugin-src/com/todoroo/astrid/rmilk/api/Prefs.java b/astrid/plugin-src/com/todoroo/astrid/rmilk/api/Prefs.java new file mode 100644 index 000000000..6b1738365 --- /dev/null +++ b/astrid/plugin-src/com/todoroo/astrid/rmilk/api/Prefs.java @@ -0,0 +1,47 @@ +/* + * Copyright 2007, MetaDimensional Technologies Inc. + * + * + * This file is part of the RememberTheMilk Java API. + * + * The RememberTheMilk Java API is free software; you can redistribute it + * and/or modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * The RememberTheMilk Java API 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 Lesser + * General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ +package com.todoroo.astrid.rmilk.api; + +import java.util.prefs.Preferences; + +/** + * + * @author Will Ross Jun 21, 2007 + */ +public class Prefs { + + Preferences preferences; + + public enum PrefKey { + AuthToken + } + + public Prefs() { + preferences = Preferences.userNodeForPackage(Prefs.class); + } + + public String getAuthToken() { + return preferences.get(PrefKey.AuthToken.toString(), null); + } + + public void setAuthToken(String authToken) { + preferences.put(PrefKey.AuthToken.toString(), authToken); + } +} diff --git a/astrid/plugin-src/com/todoroo/astrid/rmilk/api/ServiceException.java b/astrid/plugin-src/com/todoroo/astrid/rmilk/api/ServiceException.java new file mode 100644 index 000000000..8ba6a6b47 --- /dev/null +++ b/astrid/plugin-src/com/todoroo/astrid/rmilk/api/ServiceException.java @@ -0,0 +1,50 @@ +/* + * Copyright 2007, MetaDimensional Technologies Inc. + * + * + * This file is part of the RememberTheMilk Java API. + * + * The RememberTheMilk Java API is free software; you can redistribute it + * and/or modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * The RememberTheMilk Java API 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 Lesser + * General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ +package com.todoroo.astrid.rmilk.api; + +import java.io.IOException; + +/** + * + * @author Will Ross Jun 21, 2007 + */ +@SuppressWarnings("nls") +public class ServiceException extends IOException { + + private static final long serialVersionUID = -6711156026040643361L; + + int responseCode; + + String responseMessage; + + public ServiceException(int responseCode, String responseMessage) { + super("Service invocation failed. Code: " + responseCode + "; message: " + responseMessage); + this.responseCode = responseCode; + this.responseMessage = responseMessage; + } + + int getResponseCode() { + return responseCode; + } + + String getResponseMessage() { + return responseMessage; + } +} diff --git a/astrid/plugin-src/com/todoroo/astrid/rmilk/api/ServiceImpl.java b/astrid/plugin-src/com/todoroo/astrid/rmilk/api/ServiceImpl.java new file mode 100644 index 000000000..4662f3b31 --- /dev/null +++ b/astrid/plugin-src/com/todoroo/astrid/rmilk/api/ServiceImpl.java @@ -0,0 +1,615 @@ +/* + * Copyright 2007, MetaDimensional Technologies Inc. + * + * + * This file is part of the RememberTheMilk Java API. + * + * The RememberTheMilk Java API is free software; you can redistribute it + * and/or modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * The RememberTheMilk Java API 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 Lesser + * General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ +package com.todoroo.astrid.rmilk.api; + +import java.util.ArrayList; +import java.util.Date; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.Map.Entry; + +import org.w3c.dom.Element; + +import com.todoroo.astrid.rmilk.api.data.RtmAuth; +import com.todoroo.astrid.rmilk.api.data.RtmData; +import com.todoroo.astrid.rmilk.api.data.RtmFrob; +import com.todoroo.astrid.rmilk.api.data.RtmList; +import com.todoroo.astrid.rmilk.api.data.RtmLists; +import com.todoroo.astrid.rmilk.api.data.RtmLocation; +import com.todoroo.astrid.rmilk.api.data.RtmTask; +import com.todoroo.astrid.rmilk.api.data.RtmTaskList; +import com.todoroo.astrid.rmilk.api.data.RtmTaskNote; +import com.todoroo.astrid.rmilk.api.data.RtmTaskSeries; +import com.todoroo.astrid.rmilk.api.data.RtmTasks; +import com.todoroo.astrid.rmilk.api.data.RtmTimeline; +import com.todoroo.astrid.rmilk.api.data.RtmTask.Priority; + +/** + * A major part of the RTM API implementation is here. + * + * @author Will Ross Jun 21, 2007 + * @author Edouard Mercier, since 2008.04.15 + * @author timsu January 2009 + */ +@SuppressWarnings("nls") +public class ServiceImpl +{ + + public final static String SERVER_HOST_NAME = "api.rememberthemilk.com"; //$NON-NLS-1$ + + public final static int SERVER_PORT_NUMBER = 80; + + public final static String REST_SERVICE_URL_POSTFIX = "/services/rest/"; //$NON-NLS-1$ + + private final ApplicationInfo applicationInfo; + + private final Invoker invoker; + + private final Prefs prefs; + + private String currentAuthToken; + + RtmFrob tempFrob; + + public ServiceImpl(ApplicationInfo applicationInfo) + throws ServiceInternalException + { + invoker = new Invoker(SERVER_HOST_NAME, SERVER_PORT_NUMBER, REST_SERVICE_URL_POSTFIX, applicationInfo); + this.applicationInfo = applicationInfo; + prefs = new Prefs(); + if (applicationInfo.getAuthToken() != null) + { + currentAuthToken = applicationInfo.getAuthToken(); + } + else + { + currentAuthToken = prefs.getAuthToken(); + } + } + + public boolean isServiceAuthorized() + throws ServiceException + { + if (currentAuthToken == null) + return false; + + try + { + /* RtmAuth auth = */auth_checkToken(currentAuthToken); + return true; + } + catch (ServiceException e) + { + if (e.getResponseCode() != 98) + { + throw e; + } + else + { + // Bad token. + currentAuthToken = null; + return false; + } + } + } + + public String beginAuthorization(RtmAuth.Perms permissions) + throws ServiceException + { + // Instructions from the "User authentication for desktop applications" + // section at http://www.rememberthemilk.com/services/api/authentication.rtm + tempFrob = auth_getFrob(); + return beginAuthorization(tempFrob, permissions); + } + + public String beginAuthorization(RtmFrob frob, RtmAuth.Perms permissions) + throws ServiceException + { + String authBaseUrl = "http://" + SERVER_HOST_NAME + "/services/auth/"; + Param[] params = new Param[] { new Param("api_key", applicationInfo.getApiKey()), new Param("perms", permissions.toString()), + new Param("frob", frob.getValue()) }; + Param sig = new Param("api_sig", invoker.calcApiSig(params)); + StringBuilder authUrl = new StringBuilder(authBaseUrl); + authUrl.append("?"); + for (Param param : params) + { + authUrl.append(param.getName()).append("=").append(param.getValue()).append("&"); + } + authUrl.append(sig.getName()).append("=").append(sig.getValue()); + return authUrl.toString(); + } + + public String completeAuthorization() + throws ServiceException + { + return completeAuthorization(tempFrob); + } + + public String completeAuthorization(RtmFrob frob) + throws ServiceException + { + currentAuthToken = auth_getToken(frob.getValue()); + prefs.setAuthToken(currentAuthToken); + return currentAuthToken; + } + + public RtmAuth auth_checkToken(String authToken) + throws ServiceException + { + Element response = invoker.invoke(new Param("method", "rtm.auth.checkToken"), new Param("auth_token", authToken), + new Param("api_key", applicationInfo.getApiKey())); + return new RtmAuth(response); + } + + public RtmFrob auth_getFrob() + throws ServiceException + { + return new RtmFrob(invoker.invoke(new Param("method", "rtm.auth.getFrob"), new Param("api_key", applicationInfo.getApiKey()))); + } + + public String auth_getToken(String frob) + throws ServiceException + { + Element response = invoker.invoke(new Param("method", "rtm.auth.getToken"), new Param("frob", frob), new Param("api_key", applicationInfo.getApiKey())); + return new RtmAuth(response).getToken(); + } + + public void contacts_add() + { + throw new UnsupportedOperationException("Not supported yet."); + } + + public void contacts_delete() + { + throw new UnsupportedOperationException("Not supported yet."); + } + + public void contacts_getList() + { + throw new UnsupportedOperationException("Not supported yet."); + } + + public void groups_add() + { + throw new UnsupportedOperationException("Not supported yet."); + } + + public void groups_addContact() + { + throw new UnsupportedOperationException("Not supported yet."); + } + + public void groups_delete() + { + throw new UnsupportedOperationException("Not supported yet."); + } + + public void groups_getList() + { + throw new UnsupportedOperationException("Not supported yet."); + } + + public void groups_removeContact() + { + throw new UnsupportedOperationException("Not supported yet."); + } + + public RtmList lists_add(String timelineId, String listName) + throws ServiceException + { + Element response = invoker.invoke(new Param("method", "rtm.lists.add"), new Param("auth_token", currentAuthToken), + new Param("api_key", applicationInfo.getApiKey()), new Param("name", listName), new Param("timeline", timelineId)); + return new RtmList(response); + } + + public void lists_archive() + { + throw new UnsupportedOperationException("Not supported yet."); + } + + public void lists_delete(String timelineId, String listId) + throws ServiceException + { + invoker.invoke(new Param("method", "rtm.lists.delete"), new Param("auth_token", currentAuthToken), new Param("api_key", applicationInfo.getApiKey()), + new Param("timeline", timelineId), new Param("list_id", listId)); + } + + public RtmLists lists_getList() + throws ServiceException + { + Element response = invoker.invoke(new Param("method", "rtm.lists.getList"), new Param("auth_token", currentAuthToken), + new Param("api_key", applicationInfo.getApiKey())); + return new RtmLists(response); + } + + public RtmList lists_getList(String listName) + throws ServiceException + { + RtmLists fullList = lists_getList(); + for (Entry entry : fullList.getLists().entrySet()) + { + if (entry.getValue().getName().equals(listName)) + { + return entry.getValue(); + } + } + return null; + } + + public void lists_setDefaultList() + { + throw new UnsupportedOperationException("Not supported yet."); + } + + public RtmList lists_setName(String timelineId, String listId, String newName) + throws ServiceException + { + Element response = invoker.invoke(new Param("method", "rtm.lists.setName"), new Param("timeline", timelineId), new Param("list_id", listId), + new Param("name", newName), new Param("auth_token", currentAuthToken), new Param("api_key", applicationInfo.getApiKey())); + return new RtmList(response); + } + + public void lists_unarchive() + { + throw new UnsupportedOperationException("Not supported yet."); + } + + public void reflection_getMethodInfo() + { + throw new UnsupportedOperationException("Not supported yet."); + } + + public void reflection_getMethods() + { + throw new UnsupportedOperationException("Not supported yet."); + } + + public void settings_getList() + { + throw new UnsupportedOperationException("Not supported yet."); + } + + /** + * Adds a task, name, to the list specified by list_id. + * @param timelineId + * @param listId can be null to omit this parameter (assumes Inbox) + * @param name + * @return + * @throws ServiceException + */ + public RtmTaskSeries tasks_add(String timelineId, String listId, String name) + throws ServiceException + { + Element response; + if(listId != null) + response = invoker.invoke(new Param("method", "rtm.tasks.add"), new Param("timeline", timelineId), new Param("list_id", listId), + new Param("name", name), new Param("auth_token", currentAuthToken), new Param("api_key", applicationInfo.getApiKey())); + else + response = invoker.invoke(new Param("method", "rtm.tasks.add"), new Param("timeline", timelineId), + new Param("name", name), new Param("auth_token", currentAuthToken), new Param("api_key", applicationInfo.getApiKey())); + + RtmTaskList rtmTaskList = new RtmTaskList(response); + if (rtmTaskList.getSeries().size() == 1) + { + return rtmTaskList.getSeries().get(0); + } + else if (rtmTaskList.getSeries().size() > 1) + { + throw new ServiceInternalException("Internal error: more that one task (" + rtmTaskList.getSeries().size() + ") has been created"); + } + throw new ServiceInternalException("Internal error: no task has been created"); + } + + public void tasks_addTags() + { + throw new UnsupportedOperationException("Not supported yet."); + } + + public void tasks_complete(String timelineId, String listId, String taskSeriesId, String taskId) + throws ServiceException + { + invoker.invoke(new Param("method", "rtm.tasks.complete"), new Param("timeline", timelineId), new Param("list_id", listId), + new Param("taskseries_id", taskSeriesId), new Param("task_id", taskId), new Param("auth_token", currentAuthToken), + new Param("api_key", applicationInfo.getApiKey())); + } + + public void tasks_delete(String timelineId, String listId, String taskSeriesId, String taskId) + throws ServiceException + { + invoker.invoke(new Param("method", "rtm.tasks.delete"), new Param("timeline", timelineId), new Param("list_id", listId), + new Param("taskseries_id", taskSeriesId), new Param("task_id", taskId), new Param("auth_token", currentAuthToken), + new Param("api_key", applicationInfo.getApiKey())); + } + + public RtmTasks tasks_getList(String listId, String filter, Date lastSync) + throws ServiceException + { + Set params = new HashSet(); + params.add(new Param("method", "rtm.tasks.getList")); + if (listId != null) + { + params.add(new Param("list_id", listId)); + } + if (filter != null) + { + params.add(new Param("filter", filter)); + } + if (lastSync != null) + { + params.add(new Param("last_sync", lastSync)); + } + params.add(new Param("auth_token", currentAuthToken)); + params.add(new Param("api_key", applicationInfo.getApiKey())); + return new RtmTasks(invoker.invoke(params.toArray(new Param[params.size()]))); + } + + public RtmTaskSeries tasks_getTask(String taskName) + throws ServiceException + { + return tasks_getTask(null, taskName); + } + + public RtmTaskSeries tasks_getTask(String taskSeriesId, String taskName) + throws ServiceException + { + Set params = new HashSet(); + params.add(new Param("method", "rtm.tasks.getList")); + params.add(new Param("auth_token", currentAuthToken)); + params.add(new Param("api_key", applicationInfo.getApiKey())); + params.add(new Param("filter", "name:" + taskName)); + RtmTasks rtmTasks = new RtmTasks(invoker.invoke(params.toArray(new Param[params.size()]))); + return findTask(taskSeriesId, rtmTasks); + } + + private RtmTaskSeries findTask(String taskId, RtmTasks rtmTasks) + { + for (RtmTaskList list : rtmTasks.getLists()) + { + for (RtmTaskSeries series : list.getSeries()) + { + if (taskId != null) + { + if (series.getId().equals(taskId)) + { + return series; + } + } + else + { + return series; + } + } + } + return null; + } + + public void tasks_movePriority() + { + throw new UnsupportedOperationException("Not supported yet."); + } + + public RtmTaskSeries tasks_moveTo(String timelineId, String fromListId, String toListId, String taskSeriesId, String taskId) + throws ServiceException + { + Element elt = invoker.invoke(new Param("method", "rtm.tasks.moveTo"), new Param("timeline", timelineId), new Param("from_list_id", fromListId), + new Param("to_list_id", toListId), new Param("taskseries_id", taskSeriesId), new Param("task_id", taskId), new Param("auth_token", currentAuthToken), + new Param("api_key", applicationInfo.getApiKey())); + RtmTaskList rtmTaskList = new RtmTaskList(elt); + return findTask(taskSeriesId, taskId, rtmTaskList); + } + + public void tasks_postpone() + { + throw new UnsupportedOperationException("Not supported yet."); + } + + public void tasks_removeTags() + { + throw new UnsupportedOperationException("Not supported yet."); + } + + public void tasks_setDueDate(String timelineId, String listId, String taskSeriesId, String taskId, Date due, boolean hasDueTime) + throws ServiceException + { + final boolean setDueDate = (due != null); + if (setDueDate == true) + { + invoker.invoke(new Param("method", "rtm.tasks.setDueDate"), new Param("timeline", timelineId), new Param("list_id", listId), + new Param("taskseries_id", taskSeriesId), new Param("task_id", taskId), new Param("due", due), new Param("has_due_time", hasDueTime ? "1" : "0"), + new Param("auth_token", currentAuthToken), new Param("api_key", applicationInfo.getApiKey())); + } + else + { + invoker.invoke(new Param("method", "rtm.tasks.setDueDate"), new Param("timeline", timelineId), new Param("list_id", listId), + new Param("taskseries_id", taskSeriesId), new Param("task_id", taskId), new Param("auth_token", currentAuthToken), + new Param("api_key", applicationInfo.getApiKey())); + } + } + + public void tasks_setEstimate(String timelineId, String listId, String taskSeriesId, String taskId, String newEstimate) + throws ServiceException + { + invoker.invoke(new Param("method", "rtm.tasks.setEstimate"), new Param("timeline", timelineId), new Param("list_id", listId), + new Param("taskseries_id", taskSeriesId), new Param("task_id", taskId), new Param("estimate", newEstimate), new Param("auth_token", currentAuthToken), + new Param("api_key", applicationInfo.getApiKey())); + } + + public void tasks_setName(String timelineId, String listId, String taskSeriesId, String taskId, String newName) + throws ServiceException + { + invoker.invoke(new Param("method", "rtm.tasks.setName"), new Param("timeline", timelineId), new Param("list_id", listId), + new Param("taskseries_id", taskSeriesId), new Param("task_id", taskId), new Param("name", newName), new Param("auth_token", currentAuthToken), + new Param("api_key", applicationInfo.getApiKey())); + } + + private RtmTaskSeries findTask(String taskSeriesId, String taskId, RtmTaskList rtmTaskList) + { + for (RtmTaskSeries series : rtmTaskList.getSeries()) + { + if (series.getId().equals(taskSeriesId) && series.getTask().getId().equals(taskId)) + { + return series; + } + } + return null; + } + + public void tasks_setPriority(String timelineId, String listId, String taskSeriesId, String taskId, Priority priority) + throws ServiceException + { + invoker.invoke(new Param("method", "rtm.tasks.setPriority"), new Param("timeline", timelineId), new Param("list_id", listId), + new Param("taskseries_id", taskSeriesId), new Param("task_id", taskId), new Param("priority", RtmTask.convertPriority(priority)), + new Param("auth_token", currentAuthToken), new Param("api_key", applicationInfo.getApiKey())); + } + + public void tasks_setRecurrence(String timelineId, String listId, String taskSeriesId, String taskId, String repeat) + throws ServiceException + { + invoker.invoke(new Param("method", "rtm.tasks.setRecurrence"), new Param("timeline", timelineId), new Param("list_id", listId), + new Param("taskseries_id", taskSeriesId), new Param("task_id", taskId), new Param("repeat", repeat), + new Param("auth_token", currentAuthToken), new Param("api_key", applicationInfo.getApiKey())); + } + + public void tasks_setTags(String timelineId, String listId, + String taskSeriesId, String taskId, String[] tags) throws ServiceException + { + StringBuilder tagString = new StringBuilder(); + if(tags != null) { + for(int i = 0; i < tags.length; i++) { + tagString.append(tags[i].replace(" ", "_")); + if(i < tags.length - 1) + tagString.append(","); + } + } + invoker.invoke(new Param("method", "rtm.tasks.setTags"), new Param("timeline", timelineId), new Param("list_id", listId), + new Param("taskseries_id", taskSeriesId), new Param("task_id", taskId), new Param("tags", tagString.toString()), new Param("auth_token", currentAuthToken), + new Param("api_key", applicationInfo.getApiKey())); + } + + public void tasks_setURL() + { + throw new UnsupportedOperationException("Not supported yet."); + } + + public void tasks_uncomplete(String timelineId, String listId, String taskSeriesId, String taskId) + throws ServiceException + { + invoker.invoke(new Param("method", "rtm.tasks.uncomplete"), new Param("timeline", timelineId), new Param("list_id", listId), + new Param("taskseries_id", taskSeriesId), new Param("task_id", taskId), new Param("auth_token", currentAuthToken), + new Param("api_key", applicationInfo.getApiKey())); + } + + public RtmTaskNote tasks_notes_add(String timelineId, String listId, String taskSeriesId, String taskId, String title, String text) + throws ServiceException + { + Element elt = invoker.invoke(new Param("method", "rtm.tasks.notes.add"), new Param("timeline", timelineId), new Param("list_id", listId), + new Param("taskseries_id", taskSeriesId), new Param("task_id", taskId), new Param("note_title", title), new Param("note_text", text), + new Param("auth_token", currentAuthToken), new Param("api_key", applicationInfo.getApiKey())); + return new RtmTaskNote(elt); + } + + public void tasks_notes_delete(String timelineId, String noteId) + throws ServiceException + { + invoker.invoke(new Param("method", "rtm.tasks.notes.delete"), new Param("timeline", timelineId), new Param("note_id", noteId), + new Param("auth_token", currentAuthToken), new Param("api_key", applicationInfo.getApiKey())); + } + + public RtmTaskNote tasks_notes_edit(String timelineId, String noteId, String title, String text) + throws ServiceException + { + Element elt = invoker.invoke(new Param("method", "rtm.tasks.notes.edit"), new Param("timeline", timelineId), new Param("note_id", noteId), + new Param("note_title", title), new Param("note_text", text), new Param("auth_token", currentAuthToken), + new Param("api_key", applicationInfo.getApiKey())); + return new RtmTaskNote(elt); + } + + public RtmTaskSeries tasks_setLocation(String timelineId, String listId, String taskSeriesId, String taskId, String locationId) + throws ServiceException + { + Element response = invoker.invoke(new Param("method", "rtm.tasks.setLocation"), new Param("timeline", timelineId), new Param("list_id", listId), + new Param("taskseries_id", taskSeriesId), new Param("task_id", taskId), new Param("location_id", locationId), + new Param("auth_token", currentAuthToken), new Param("api_key", applicationInfo.getApiKey())); + RtmTaskList rtmTaskList = new RtmTaskList(response); + return findTask(taskSeriesId, taskId, rtmTaskList); + } + + public RtmTaskSeries tasks_setURL(String timelineId, String listId, String taskSeriesId, String taskId, String url) + throws ServiceException + { + Element response = invoker.invoke(new Param("method", "rtm.tasks.setURL"), new Param("timeline", timelineId), new Param("list_id", listId), + new Param("taskseries_id", taskSeriesId), new Param("task_id", taskId), new Param("url", url), new Param("auth_token", currentAuthToken), + new Param("api_key", applicationInfo.getApiKey())); + RtmTaskList rtmTaskList = new RtmTaskList(response); + return findTask(taskSeriesId, taskId, rtmTaskList); + } + + public void test_echo() + { + throw new UnsupportedOperationException("Not supported yet."); + } + + public void test_login() + { + throw new UnsupportedOperationException("Not supported yet."); + } + + public void time_convert() + { + throw new UnsupportedOperationException("Not supported yet."); + } + + public void time_parse() + { + throw new UnsupportedOperationException("Not supported yet."); + } + + public String timelines_create() + throws ServiceException + { + return new RtmTimeline(invoker.invoke(new Param("method", "rtm.timelines.create"), new Param("auth_token", currentAuthToken), + new Param("api_key", applicationInfo.getApiKey()))).getId(); + } + + public void timezones_getList() + { + throw new UnsupportedOperationException("Not supported yet."); + } + + public void transactions_undo() + { + throw new UnsupportedOperationException("Not supported yet."); + } + + public List locations_getList() + throws ServiceException + { + Element result = invoker.invoke(new Param("method", "rtm.locations.getList"), new Param("auth_token", currentAuthToken), + new Param("api_key", applicationInfo.getApiKey())); + List locations = new ArrayList(); + for (Element child : RtmData.children(result, "location")) + { + locations.add(new RtmLocation(child)); + } + return locations; + } + +} diff --git a/astrid/plugin-src/com/todoroo/astrid/rmilk/api/ServiceInternalException.java b/astrid/plugin-src/com/todoroo/astrid/rmilk/api/ServiceInternalException.java new file mode 100644 index 000000000..c69eae884 --- /dev/null +++ b/astrid/plugin-src/com/todoroo/astrid/rmilk/api/ServiceInternalException.java @@ -0,0 +1,53 @@ +/* + * Copyright 2007, MetaDimensional Technologies Inc. + * + * + * This file is part of the RememberTheMilk Java API. + * + * The RememberTheMilk Java API is free software; you can redistribute it + * and/or modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * The RememberTheMilk Java API 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 Lesser + * General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ +package com.todoroo.astrid.rmilk.api; + + +/** + * Introduced in order to get rid of the {@link RuntimeException}, and have only one time of regular exception to cope with, from the API end-user + * point of view. + * + * @author Edouard Mercier + * @since 2008.04.23 + */ +public class ServiceInternalException + extends ServiceException +{ + private static final long serialVersionUID = -423838945284984432L; + + private final Exception enclosedException; + + public ServiceInternalException(String message) + { + this(message, null); + } + + public ServiceInternalException(String message, Exception exception) + { + super(-1, message); + this.enclosedException = exception; + } + + public Exception getEnclosedException() + { + return enclosedException; + } + +} diff --git a/astrid/plugin-src/com/todoroo/astrid/rmilk/api/data/RtmAuth.java b/astrid/plugin-src/com/todoroo/astrid/rmilk/api/data/RtmAuth.java new file mode 100644 index 000000000..b5e8b9a22 --- /dev/null +++ b/astrid/plugin-src/com/todoroo/astrid/rmilk/api/data/RtmAuth.java @@ -0,0 +1,66 @@ +/* + * Copyright 2007, MetaDimensional Technologies Inc. + * + * + * This file is part of the RememberTheMilk Java API. + * + * The RememberTheMilk Java API is free software; you can redistribute it + * and/or modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * The RememberTheMilk Java API 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 Lesser + * General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ +package com.todoroo.astrid.rmilk.api.data; + +import org.w3c.dom.Element; + +/** + * + * @author Will Ross Jun 21, 2007 + */ +@SuppressWarnings("nls") +public class RtmAuth extends RtmData { + + public enum Perms { + read, write, delete + } + + private final String token; + + private final Perms perms; + + private final RtmUser user; + + public RtmAuth(String token, Perms perms, RtmUser user) { + this.token = token; + this.perms = perms; + this.user = user; + } + + public RtmAuth(Element elt) { + if (!elt.getNodeName().equals("auth")) { throw new IllegalArgumentException("Element " + elt.getNodeName() + " does not represent an Auth object."); } + + this.token = text(child(elt, "token")); + this.perms = Enum.valueOf(Perms.class, text(child(elt, "perms"))); + this.user = new RtmUser(child(elt, "user")); + } + + public String getToken() { + return token; + } + + public Perms getPerms() { + return perms; + } + + public RtmUser getUser() { + return user; + } +} diff --git a/astrid/plugin-src/com/todoroo/astrid/rmilk/api/data/RtmData.java b/astrid/plugin-src/com/todoroo/astrid/rmilk/api/data/RtmData.java new file mode 100644 index 000000000..104460e16 --- /dev/null +++ b/astrid/plugin-src/com/todoroo/astrid/rmilk/api/data/RtmData.java @@ -0,0 +1,120 @@ +/* + * Copyright 2007, MetaDimensional Technologies Inc. + * + * + * This file is part of the RememberTheMilk Java API. + * + * The RememberTheMilk Java API is free software; you can redistribute it + * and/or modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * The RememberTheMilk Java API 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 Lesser + * General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ +package com.todoroo.astrid.rmilk.api.data; + +import java.text.DateFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.TimeZone; + +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +/** + * + * @author Will Ross Jun 21, 2007 + */ +@SuppressWarnings("nls") +public abstract class RtmData +{ + + private static final DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss"); + + public RtmData() { + // + } + + /** + * The method is not optimized at most, but circumvents a bug in Android runtime. + */ + public static Element child(Element elt, String nodeName) + { + NodeList childNodes = elt.getChildNodes(); + for (int index = 0; index < childNodes.getLength(); index++) + { + Node child = childNodes.item(index); + if (child.getNodeType() == Node.ELEMENT_NODE && child.getNodeName().equals(nodeName)) + { + return (Element) child; + } + } + return null; + } + + /** + * The method is not optimized at most, but circumvents a bug in Android runtime. + */ + public static List children(Element elt, String nodeName) + { + List result = new ArrayList(); + NodeList childNodes = elt.getChildNodes(); + for (int index = 0; index < childNodes.getLength(); index++) + { + Node child = childNodes.item(index); + if (child.getNodeType() == Node.ELEMENT_NODE && child.getNodeName().equals(nodeName)) + { + result.add((Element) child); + } + } + return result; + } + + protected String text(Element elt) + { + StringBuilder result = new StringBuilder(); + Node child = elt.getFirstChild(); + while (child != null) + { + switch (child.getNodeType()) + { + case Node.TEXT_NODE: + case Node.CDATA_SECTION_NODE: + result.append(child.getNodeValue()); + break; + default: + break; + } + child = child.getNextSibling(); + } + return result.toString(); + } + + public synchronized static Date parseDate(String s) + { + try + { + Date d = DATE_FORMAT.parse(s); + return new Date(d.getTime() + TimeZone.getDefault().getOffset(d.getTime())); + } + catch (ParseException e) + { + throw new RuntimeException(e); + } + } + + public synchronized static String formatDate(Date d) + { + return DATE_FORMAT.format(new Date(d.getTime() - TimeZone.getDefault().getOffset(d.getTime()))) + "Z"; + } +} diff --git a/astrid/plugin-src/com/todoroo/astrid/rmilk/api/data/RtmFrob.java b/astrid/plugin-src/com/todoroo/astrid/rmilk/api/data/RtmFrob.java new file mode 100644 index 000000000..29365b364 --- /dev/null +++ b/astrid/plugin-src/com/todoroo/astrid/rmilk/api/data/RtmFrob.java @@ -0,0 +1,44 @@ +/* + * Copyright 2007, MetaDimensional Technologies Inc. + * + * + * This file is part of the RememberTheMilk Java API. + * + * The RememberTheMilk Java API is free software; you can redistribute it + * and/or modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * The RememberTheMilk Java API 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 Lesser + * General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ +package com.todoroo.astrid.rmilk.api.data; + +import org.w3c.dom.Element; + +/** + * + * @author Will Ross Jun 21, 2007 + */ +public class RtmFrob extends RtmData { + + private final String value; + + public RtmFrob(String value) { + this.value = value; + } + + public RtmFrob(Element elt) { + this.value = text(elt); + } + + public String getValue() { + return value; + } + +} diff --git a/astrid/plugin-src/com/todoroo/astrid/rmilk/api/data/RtmList.java b/astrid/plugin-src/com/todoroo/astrid/rmilk/api/data/RtmList.java new file mode 100644 index 000000000..09593d306 --- /dev/null +++ b/astrid/plugin-src/com/todoroo/astrid/rmilk/api/data/RtmList.java @@ -0,0 +1,54 @@ +/* + * Copyright 2007, MetaDimensional Technologies Inc. + * + * + * This file is part of the RememberTheMilk Java API. + * + * The RememberTheMilk Java API is free software; you can redistribute it + * and/or modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * The RememberTheMilk Java API 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 Lesser + * General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ +package com.todoroo.astrid.rmilk.api.data; + +import org.w3c.dom.Element; + +@SuppressWarnings("nls") +public class RtmList extends RtmData { + + private final String id; + private final boolean smart; + private final String name; + + public RtmList(String id, String name, boolean smart) { + this.id = id; + this.name = name; + this.smart = smart; + } + + public RtmList(Element elt) { + id = elt.getAttribute("id"); + name = elt.getAttribute("name"); + smart = elt.getAttribute("smart") == "1"; + } + + public String getId() { + return id; + } + + public String getName() { + return name; + } + + public boolean isSmart() { + return smart; + } +} diff --git a/astrid/plugin-src/com/todoroo/astrid/rmilk/api/data/RtmLists.java b/astrid/plugin-src/com/todoroo/astrid/rmilk/api/data/RtmLists.java new file mode 100644 index 000000000..d7fe2254f --- /dev/null +++ b/astrid/plugin-src/com/todoroo/astrid/rmilk/api/data/RtmLists.java @@ -0,0 +1,52 @@ +/* + * Copyright 2007, MetaDimensional Technologies Inc. + * + * + * This file is part of the RememberTheMilk Java API. + * + * The RememberTheMilk Java API is free software; you can redistribute it + * and/or modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * The RememberTheMilk Java API 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 Lesser + * General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ +package com.todoroo.astrid.rmilk.api.data; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.w3c.dom.Element; + +@SuppressWarnings("nls") +public class RtmLists extends RtmData { + + private final Map lists; + + public RtmLists() { + this.lists = new HashMap(); + } + + public RtmLists(Element elt) { + this.lists = new HashMap(); + for (Element listElt : children(elt, "list")) { + RtmList list = new RtmList(listElt); + lists.put(list.getId(), list); + } + } + + public RtmList getList(String id) { + return lists.get(id); + } + + public Map getLists() { + return Collections.unmodifiableMap(lists); + } +} diff --git a/astrid/plugin-src/com/todoroo/astrid/rmilk/api/data/RtmLocation.java b/astrid/plugin-src/com/todoroo/astrid/rmilk/api/data/RtmLocation.java new file mode 100644 index 000000000..b54c51766 --- /dev/null +++ b/astrid/plugin-src/com/todoroo/astrid/rmilk/api/data/RtmLocation.java @@ -0,0 +1,60 @@ +/* + * Copyright 2007, MetaDimensional Technologies Inc. + * + * + * This file is part of the RememberTheMilk Java API. + * + * The RememberTheMilk Java API is free software; you can redistribute it + * and/or modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * The RememberTheMilk Java API 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 Lesser + * General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ +package com.todoroo.astrid.rmilk.api.data; + +import org.w3c.dom.Element; + +/** + * Represents a location. + * + * @author Edouard Mercier + * @since 2008.05.22 + */ +@SuppressWarnings("nls") +public class RtmLocation + extends RtmData +{ + + public final String id; + + public final String name; + + public final float longitude; + + public final float latitude; + + public final String address; + + public final boolean viewable; + + public int zoom; + + public RtmLocation(Element element) + { + id = element.getAttribute("id"); + name = element.getAttribute("name"); + longitude = Float.parseFloat(element.getAttribute("longitude")); + latitude = Float.parseFloat(element.getAttribute("latitude")); + address = element.getAttribute("address"); + zoom = Integer.parseInt(element.getAttribute("zoom")); + viewable = element.getAttribute("viewable").equals("1") ? true : false; + } + +} diff --git a/astrid/plugin-src/com/todoroo/astrid/rmilk/api/data/RtmTask.java b/astrid/plugin-src/com/todoroo/astrid/rmilk/api/data/RtmTask.java new file mode 100644 index 000000000..48553c5b6 --- /dev/null +++ b/astrid/plugin-src/com/todoroo/astrid/rmilk/api/data/RtmTask.java @@ -0,0 +1,194 @@ +/* + * Copyright 2007, MetaDimensional Technologies Inc. + * + * + * This file is part of the RememberTheMilk Java API. + * + * The RememberTheMilk Java API is free software; you can redistribute it + * and/or modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * The RememberTheMilk Java API 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 Lesser + * General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ +package com.todoroo.astrid.rmilk.api.data; + +import java.util.Date; + +import org.w3c.dom.Element; + +import android.util.Log; + +/** + * + * @author Will Ross Jun 21, 2007 + */ +@SuppressWarnings("nls") +public class RtmTask + extends RtmData +{ + + private static final String TAG = "rtm-task"; + + private final String id; + + private final Date due; + + private final boolean hasDueTime; + + private final Date added; + + private final Date completed; + + private final Date deleted; + + private final Priority priority; + + private final int postponed; + + private final String estimate; + + public enum Priority + { + High, Medium, Low, None + } + + public static String convertPriority(Priority priority) + { + switch (priority) + { + case None: + return new String(new char[] { 'n' }); + case Low: + return new String(new char[] { '3' }); + case Medium: + return new String(new char[] { '2' }); + case High: + return new String(new char[] { '1' }); + default: + Log.e(TAG, "Unrecognized RTM task priority: '" + priority + "'"); + return new String(new char[] { 'n' }); + } + } + + public RtmTask(String id, Date due, boolean hasDueTime, Date added, Date completed, Date deleted, Priority priority, int postponed, String estimate) + { + this.id = id; + this.due = due; + this.hasDueTime = hasDueTime; + this.added = added; + this.completed = completed; + this.deleted = deleted; + this.priority = priority; + this.postponed = postponed; + this.estimate = estimate; + } + + public RtmTask(Element elt) + { + id = elt.getAttribute("id"); + String dueStr = elt.getAttribute("due"); + due = (dueStr == null || dueStr.length() == 0) ? null : parseDate(dueStr); + hasDueTime = Integer.parseInt(elt.getAttribute("has_due_time")) != 0; + String addedStr = elt.getAttribute("added"); + added = (addedStr == null || addedStr.length() == 0) ? null : parseDate(addedStr); + String completedStr = elt.getAttribute("completed"); + completed = (completedStr == null || completedStr.length() == 0) ? null : parseDate(completedStr); + String deletedStr = elt.getAttribute("deleted"); + deleted = (deletedStr == null || deletedStr.length() == 0) ? null : parseDate(deletedStr); + String priorityStr = elt.getAttribute("priority"); + if (priorityStr.length() > 0) + { + switch (priorityStr.charAt(0)) + { + case 'N': + case 'n': + priority = Priority.None; + break; + case '3': + priority = Priority.Low; + break; + case '2': + priority = Priority.Medium; + break; + case '1': + priority = Priority.High; + break; + default: + System.err.println("Unrecognized RTM task priority: '" + priorityStr + "'"); + priority = Priority.Medium; + } + } + else + { + priority = Priority.None; + } + if (elt.hasAttribute("postponed") == true && elt.getAttribute("postponed").length() > 0) + { + postponed = Integer.parseInt(elt.getAttribute("postponed")); + } + else + { + postponed = 0; + } + estimate = elt.getAttribute("estimate"); + } + + public String getId() + { + return id; + } + + public Date getDue() + { + return due; + } + + public boolean getHasDueTime() + { + return hasDueTime; + } + + public Date getAdded() + { + return added; + } + + public Date getCompleted() + { + return completed; + } + + public Date getDeleted() + { + return deleted; + } + + public Priority getPriority() + { + return priority; + } + + public int getPostponed() + { + return postponed; + } + + public String getEstimate() + { + return estimate; + } + + @Override + public String toString() + { + return "Task<" + id + ">"; + } + +} diff --git a/astrid/plugin-src/com/todoroo/astrid/rmilk/api/data/RtmTaskList.java b/astrid/plugin-src/com/todoroo/astrid/rmilk/api/data/RtmTaskList.java new file mode 100644 index 000000000..40bfe43d4 --- /dev/null +++ b/astrid/plugin-src/com/todoroo/astrid/rmilk/api/data/RtmTaskList.java @@ -0,0 +1,61 @@ +/* + * Copyright 2007, MetaDimensional Technologies Inc. + * + * + * This file is part of the RememberTheMilk Java API. + * + * The RememberTheMilk Java API is free software; you can redistribute it + * and/or modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * The RememberTheMilk Java API 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 Lesser + * General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ +package com.todoroo.astrid.rmilk.api.data; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.w3c.dom.Element; + +/** + * + * @author Will Ross Jun 22, 2007 + */ +@SuppressWarnings("nls") +public class RtmTaskList extends RtmData { + + private final String id; + + private final List series; + + public RtmTaskList(String id) { + this.id = id; + this.series = new ArrayList(); + } + + public RtmTaskList(Element elt) { + id = elt.getAttribute("id"); + series = new ArrayList(); + for (Element seriesElt : children(elt, "taskseries")) { + series.add(new RtmTaskSeries(this, seriesElt)); + } + + if (id == null || id.length() == 0) { throw new RuntimeException("No id found in task list."); } + } + + public String getId() { + return id; + } + + public List getSeries() { + return Collections.unmodifiableList(series); + } +} diff --git a/astrid/plugin-src/com/todoroo/astrid/rmilk/api/data/RtmTaskNote.java b/astrid/plugin-src/com/todoroo/astrid/rmilk/api/data/RtmTaskNote.java new file mode 100644 index 000000000..5ad24f260 --- /dev/null +++ b/astrid/plugin-src/com/todoroo/astrid/rmilk/api/data/RtmTaskNote.java @@ -0,0 +1,101 @@ +/* + * Copyright 2007, MetaDimensional Technologies Inc. + * + * + * This file is part of the RememberTheMilk Java API. + * + * The RememberTheMilk Java API is free software; you can redistribute it + * and/or modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * The RememberTheMilk Java API 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 Lesser + * General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ +package com.todoroo.astrid.rmilk.api.data; + +import java.util.Date; + +import org.w3c.dom.Element; +import org.w3c.dom.Text; + +import android.util.Log; + +/** + * Represents a single task note. + * + * @author Edouard Mercier + * @since 2008.04.22 + */ +@SuppressWarnings("nls") +public class RtmTaskNote + extends RtmData +{ + + private String id; + + private Date created; + + private Date modified; + + private String title; + + private String text; + + public RtmTaskNote(Element element) + { + id = element.getAttribute("id"); + created = parseDate(element.getAttribute("created")); + modified = parseDate(element.getAttribute("modified")); + title = element.getAttribute("title"); + + // The note text itself might be split across multiple children of the + // note element, so get all of the children. + for (int i=0; i < element.getChildNodes().getLength(); i++) { + Object innerNote = element.getChildNodes().item(i); + if(!(innerNote instanceof Text)) { + Log.w("rtm-note", "Expected text type, got " + innerNote.getClass()); + continue; + } + + Text innerText = (Text) innerNote; + + if (text == null) + text = innerText.getData(); + else + text = text.concat(innerText.getData()); + + } + } + + public String getId() + { + return id; + } + + public Date getCreated() + { + return created; + } + + public Date getModified() + { + return modified; + } + + public String getTitle() + { + return title; + } + + public String getText() + { + return text; + } + +} diff --git a/astrid/plugin-src/com/todoroo/astrid/rmilk/api/data/RtmTaskNotes.java b/astrid/plugin-src/com/todoroo/astrid/rmilk/api/data/RtmTaskNotes.java new file mode 100644 index 000000000..9c3c3870f --- /dev/null +++ b/astrid/plugin-src/com/todoroo/astrid/rmilk/api/data/RtmTaskNotes.java @@ -0,0 +1,54 @@ +/* + * Copyright 2007, MetaDimensional Technologies Inc. + * + * + * This file is part of the RememberTheMilk Java API. + * + * The RememberTheMilk Java API is free software; you can redistribute it + * and/or modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * The RememberTheMilk Java API 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 Lesser + * General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ +package com.todoroo.astrid.rmilk.api.data; + +import java.util.ArrayList; +import java.util.List; + +import org.w3c.dom.Element; + +/** + * Represents the notes of a task. + * + * @author Edouard Mercier + * @since 2008.04.22 + */ +@SuppressWarnings("nls") +public class RtmTaskNotes + extends RtmData +{ + + private List notes; + + public RtmTaskNotes(Element element) + { + notes = new ArrayList(); + for (Element child : children(element, "note")) + { + notes.add(new RtmTaskNote(child)); + } + } + + public List getNotes() + { + return notes; + } + +} diff --git a/astrid/plugin-src/com/todoroo/astrid/rmilk/api/data/RtmTaskSeries.java b/astrid/plugin-src/com/todoroo/astrid/rmilk/api/data/RtmTaskSeries.java new file mode 100644 index 000000000..1c72c6e69 --- /dev/null +++ b/astrid/plugin-src/com/todoroo/astrid/rmilk/api/data/RtmTaskSeries.java @@ -0,0 +1,170 @@ +/* + * Copyright 2007, MetaDimensional Technologies Inc. + * + * + * This file is part of the RememberTheMilk Java API. + * + * The RememberTheMilk Java API is free software; you can redistribute it + * and/or modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * The RememberTheMilk Java API 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 Lesser + * General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ +package com.todoroo.astrid.rmilk.api.data; + +import java.util.Date; +import java.util.LinkedList; +import java.util.List; + +import org.w3c.dom.Element; + +/** + * + * @author Will Ross Jun 22, 2007 + */ +@SuppressWarnings("nls") +public class RtmTaskSeries extends RtmData { + + private final RtmTaskList list; + + private final String id; + + private final Date created; + + private final Date modified; + + private final String name; + + private final String source; + + private final RtmTask task; + + private final LinkedList tags; + + private final RtmTaskNotes notes; + + private final String locationId; + + private final String url; + + private final boolean hasRecurrence; + + public RtmTaskSeries(RtmTaskList list, String id, Date created, Date modified, String name, + String source, RtmTask task) { + this.list = list; + this.id = id; + this.created = created; + this.modified = modified; + this.name = name; + this.source = source; + this.task = task; + this.locationId = null; + notes = null; + url = null; + tags = null; + hasRecurrence = false; + } + + public RtmTaskSeries(RtmTaskList list, Element elt) { + this.list = list; + id = elt.getAttribute("id"); + created = parseDate(elt.getAttribute("created")); + modified = parseDate(elt.getAttribute("modified")); + name = elt.getAttribute("name"); + source = elt.getAttribute("source"); + List children = children(elt, "task"); + if (children.size() > 1) { + // assume it's a repeating task - pick the child with nearest + // but not expired due date + RtmTask selectedTask = new RtmTask(children.get(0)); + for(Element element : children) { + RtmTask childTask = new RtmTask(element); + if(childTask.getCompleted() == null) { + selectedTask = childTask; + break; + } + } + task = selectedTask; + } else { + task = new RtmTask(child(elt, "task")); + } + notes = new RtmTaskNotes(child(elt, "notes")); + locationId = elt.getAttribute("location_id"); + url = elt.getAttribute("url"); + hasRecurrence = children(elt, "rrule").size() > 0; + + Element elementTags = child(elt, "tags"); + if (elementTags.getChildNodes().getLength() > 0) { + List elementTagList = children(elementTags, "tag"); + tags = new LinkedList(); + for (Element elementTag : elementTagList) { + String tag = text(elementTag); + if (tag != null) + tags.add(tag); + } + } else { + tags = null; + } + } + + public String getId() { + return id; + } + + public Date getCreated() { + return created; + } + + public Date getModified() { + return modified; + } + + public String getName() { + return name; + } + + public String getSource() { + return source; + } + + public RtmTask getTask() { + return task; + } + + public LinkedList getTags() { + return tags; + } + + public RtmTaskNotes getNotes() { + return notes; + } + + public String getLocationId() { + return locationId; + } + + @Override + public String toString() { + return "TaskSeries<" + id + "," + name + ">"; + } + + public String getURL() { + return url; + } + + public boolean hasRecurrence() { + return hasRecurrence; + } + + public RtmTaskList getList() { + return list; + } + +} diff --git a/astrid/plugin-src/com/todoroo/astrid/rmilk/api/data/RtmTasks.java b/astrid/plugin-src/com/todoroo/astrid/rmilk/api/data/RtmTasks.java new file mode 100644 index 000000000..ee401f471 --- /dev/null +++ b/astrid/plugin-src/com/todoroo/astrid/rmilk/api/data/RtmTasks.java @@ -0,0 +1,50 @@ +/* + * Copyright 2007, MetaDimensional Technologies Inc. + * + * + * This file is part of the RememberTheMilk Java API. + * + * The RememberTheMilk Java API is free software; you can redistribute it + * and/or modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * The RememberTheMilk Java API 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 Lesser + * General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ +package com.todoroo.astrid.rmilk.api.data; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import org.w3c.dom.Element; + +/** + * + * @author Will Ross Jun 21, 2007 + */ +@SuppressWarnings("nls") +public class RtmTasks extends RtmData { + + private final List lists; + + public RtmTasks() { + this.lists = new ArrayList(); + } + + public RtmTasks(Element elt) { + this.lists = new ArrayList(); + for (Element listElt : children(elt, "list")) { + lists.add(new RtmTaskList(listElt)); + } + } + + public List getLists() { + return Collections.unmodifiableList(lists); + } +} diff --git a/astrid/plugin-src/com/todoroo/astrid/rmilk/api/data/RtmTimeline.java b/astrid/plugin-src/com/todoroo/astrid/rmilk/api/data/RtmTimeline.java new file mode 100644 index 000000000..c9187bfac --- /dev/null +++ b/astrid/plugin-src/com/todoroo/astrid/rmilk/api/data/RtmTimeline.java @@ -0,0 +1,39 @@ +/* + * Copyright 2007, MetaDimensional Technologies Inc. + * + * + * This file is part of the RememberTheMilk Java API. + * + * The RememberTheMilk Java API is free software; you can redistribute it + * and/or modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * The RememberTheMilk Java API 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 Lesser + * General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ +package com.todoroo.astrid.rmilk.api.data; + +import org.w3c.dom.Element; + +public class RtmTimeline extends RtmData { + + private final String id; + + public RtmTimeline(String id) { + this.id = id; + } + + public RtmTimeline(Element elt) { + id = text(elt); + } + + public String getId() { + return id; + } +} diff --git a/astrid/plugin-src/com/todoroo/astrid/rmilk/api/data/RtmUser.java b/astrid/plugin-src/com/todoroo/astrid/rmilk/api/data/RtmUser.java new file mode 100644 index 000000000..5e71e264c --- /dev/null +++ b/astrid/plugin-src/com/todoroo/astrid/rmilk/api/data/RtmUser.java @@ -0,0 +1,63 @@ +/* + * Copyright 2007, MetaDimensional Technologies Inc. + * + * + * This file is part of the RememberTheMilk Java API. + * + * The RememberTheMilk Java API is free software; you can redistribute it + * and/or modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * The RememberTheMilk Java API 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 Lesser + * General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ +package com.todoroo.astrid.rmilk.api.data; + +import org.w3c.dom.Element; + +/** + * + * @author Will Ross Jun 21, 2007 + */ +@SuppressWarnings("nls") +public class RtmUser extends RtmData { + + private final String id; + + private final String username; + + private final String fullname; + + public RtmUser(String id, String username, String fullname) { + this.id = id; + this.username = username; + this.fullname = fullname; + } + + public RtmUser(Element elt) { + if (!elt.getNodeName().equals("user")) { throw new IllegalArgumentException("Element " + elt.getNodeName() + " does not represent a User object."); } + + this.id = elt.getAttribute("id"); + this.username = elt.getAttribute("username"); + this.fullname = elt.getAttribute("fullname"); + } + + public String getId() { + return id; + } + + public String getUsername() { + return username; + } + + public String getFullname() { + return fullname; + } + +} diff --git a/astrid/plugin-src/com/todoroo/astrid/rmilk/data/MilkDataService.java b/astrid/plugin-src/com/todoroo/astrid/rmilk/data/MilkDataService.java new file mode 100644 index 000000000..9369f6950 --- /dev/null +++ b/astrid/plugin-src/com/todoroo/astrid/rmilk/data/MilkDataService.java @@ -0,0 +1,282 @@ +/** + * See the file "LICENSE" for the full license governing this code. + */ +package com.todoroo.astrid.rmilk.data; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Random; + +import android.content.Context; +import android.database.sqlite.SQLiteQueryBuilder; + +import com.todoroo.andlib.data.GenericDao; +import com.todoroo.andlib.data.Property; +import com.todoroo.andlib.data.TodorooCursor; +import com.todoroo.andlib.data.Property.IntegerProperty; +import com.todoroo.andlib.service.Autowired; +import com.todoroo.andlib.service.DependencyInjectionService; +import com.todoroo.astrid.dao.Database; +import com.todoroo.astrid.dao.TaskDao; +import com.todoroo.astrid.model.Metadata; +import com.todoroo.astrid.model.Task; +import com.todoroo.astrid.rmilk.Utilities; +import com.todoroo.astrid.rmilk.Utilities.ListContainer; +import com.todoroo.astrid.rmilk.api.data.RtmList; +import com.todoroo.astrid.rmilk.api.data.RtmLists; +import com.todoroo.astrid.service.MetadataService; + +@SuppressWarnings("nls") +public class MilkDataService { + + protected final Context context; + + private final MilkDatabase milkDatabase = new MilkDatabase(); + + @Autowired + private MetadataService metadataService; + + private final GenericDao listDao; + + @Autowired + private TaskDao taskDao; + + @Autowired + private Database database; + + static final Random random = new Random(); + + public MilkDataService(Context context) { + this.context = context; + DependencyInjectionService.getInstance().inject(this); + listDao = new GenericDao(MilkList.class, milkDatabase); + } + + // --- RTM properties + + /** RTM List id */ + public static final StringJoinProperty LIST_ID = + new StringJoinProperty(Utilities.KEY_LIST_ID); + + /** RTM Task Series id */ + public static final StringJoinProperty TASK_SERIES_ID = + new StringJoinProperty(Utilities.KEY_TASK_SERIES_ID); + + /** RTM Task id */ + public static final StringJoinProperty TASK_ID = + new StringJoinProperty(Utilities.KEY_TASK_ID); + + /** 1 if task repeats in RTM, 0 otherwise */ + public static final IntegerJoinProperty REPEAT = + new IntegerJoinProperty(Utilities.KEY_REPEAT); + + /** 1 if task was updated since last sync, 0 otherwise */ + public static final IntegerJoinProperty UPDATED = + new IntegerJoinProperty(Utilities.KEY_UPDATED); + + // --- non-RTM properties we synchronize + + public static final StringJoinProperty TAGS = + new StringJoinProperty(com.todoroo.astrid.tags.TagService.KEY); + + // --- task and metadata methods + + /** Properties to fetch when user wants to view / edit tasks */ + public static final Property[] RTM_PROPERTIES = new Property[] { + Task.ID, + LIST_ID, + TASK_SERIES_ID, + TASK_ID, + REPEAT + }; + + /** + * Read a single task by task id + * @param taskId + * @return item, or null if it doesn't exist + */ + public Task readTask(long taskId) { + SQLiteQueryBuilder builder = new SQLiteQueryBuilder(); + builder.setTables(createJoinClause(RTM_PROPERTIES)); + TodorooCursor cursor = + taskDao.query(database, RTM_PROPERTIES, builder, + Task.ID.qualifiedName()+ " = " + taskId, + null, null); + try { + if (cursor.getCount() == 0) + return null; + cursor.moveToFirst(); + Task task = new Task(cursor, RTM_PROPERTIES); + return task; + } finally { + cursor.close(); + } + } + + /** Helper method for building a join clause for task/metadata joins */ + public static String createJoinClause(Property[] properties) { + StringBuilder stringBuilder = new StringBuilder(Database.TASK_TABLE); + int joinTableCount = 0; + for(Property property : properties) { + if(property instanceof JoinProperty) { + JoinProperty jp = (JoinProperty)property; + stringBuilder.append(" LEFT JOIN (").append(jp.joinTable()). + append(") m").append(++joinTableCount). + append(" ON ").append(Task.ID_PROPERTY).append(" = m"). + append(joinTableCount).append('.').append(Metadata.TASK.name). + append(' '); + } + } + return stringBuilder.toString(); + } + + /** + * Clears RTM metadata information. Used when user logs out of RTM + */ + public void clearMetadata() { + metadataService.deleteWhere(String.format("%s = '%s' OR %s = '%s' " + + "OR %s = '%s OR %s = '%s'", + Metadata.KEY, LIST_ID, + Metadata.KEY, TASK_SERIES_ID, + Metadata.KEY, TASK_ID, + Metadata.KEY, REPEAT)); + } + + public TodorooCursor getLocallyCreated(Property[] properties) { + SQLiteQueryBuilder builder = new SQLiteQueryBuilder(); + builder.setTables(createJoinClause(properties)); + TodorooCursor cursor = + taskDao.query(database, properties, builder, + TASK_ID + " ISNULL", + null, null); + return cursor; + } + + public TodorooCursor getLocallyUpdated(Property[] properties) { + SQLiteQueryBuilder builder = new SQLiteQueryBuilder(); + builder.setTables(createJoinClause(properties)); + TodorooCursor cursor = + taskDao.query(database, properties, builder, + "NOT " + TASK_ID + " ISNULL", // TODO wrong! + null, null); + return cursor; + } + + // --- list methods + + /** + * Get list name by list id + * @param listId + * @return null if no list by this id exists, otherwise list name + */ + public String getList(String listId) { + pluginDatabase.open(context); + TodorooCursor cursor = listDao.fetch(pluginDatabase, + List.PROPERTIES, ListSql.withId(listId), null, "1"); //$NON-NLS-1$ + try { + if(cursor.getCount() == 0) + return null; + cursor.moveToFirst(); + return cursor.get(List.NAME); + } finally { + cursor.close(); + pluginDatabase.close(); + } + } + + /** + * Get RTM lists as container objects + * @return + */ + public ListContainer[] getListsWithCounts() { + // read all list titles + pluginDatabase.open(context); + TodorooCursor listCursor = listDao.fetch(pluginDatabase, + List.PROPERTIES, null); + HashMap listIdToContainerMap; + try { + int count = listCursor.getCount(); + if(count == 0) + return new ListContainer[0]; + + listIdToContainerMap = + new HashMap(count); + List list = new List(); + for(int i = 0; i < count; i++) { + listCursor.moveToNext(); + list.readFromCursor(listCursor, List.PROPERTIES); + ListContainer container = new ListContainer(list); + listIdToContainerMap.put(container.id, container); + } + } finally { + listCursor.close(); + } + + // read all list counts + IntegerProperty countProperty = Property.countProperty(); + TodorooCursor metadataCursor = metadataService.fetchWithCount( + MetadataSql.withKey(Utilities.KEY_LIST_ID), Metadata.VALUE + " ASC", false); //$NON-NLS-1$ + ListContainer[] containers = new ListContainer[metadataCursor.getCount()]; + try { + for(int i = 0; i < containers.length; i++) { + metadataCursor.moveToNext(); + String id = metadataCursor.get(Metadata.VALUE); + ListContainer container = listIdToContainerMap.get(id); + if(container == null) { + container = new ListContainer(id, "[unknown]"); //$NON-NLS-1$ + } + container.count = metadataCursor.get(countProperty); + containers[i] = container; + } + return containers; + } finally { + metadataCursor.close(); + pluginDatabase.close(); + } + } + + /** + * Get RTM lists as strings + * @return + */ + public ListContainer[] getLists() { + // read all list titles + pluginDatabase.open(context); + TodorooCursor cursor = listDao.fetch(pluginDatabase, + List.PROPERTIES, null, List.ID + " ASC"); //$NON-NLS-1$ + ListContainer[] containers = new ListContainer[cursor.getCount()]; + try { + List list = new List(); + for(int i = 0; i < containers.length; i++) { + cursor.moveToNext(); + list.readFromCursor(cursor, List.PROPERTIES); + ListContainer container = new ListContainer(list); + containers[i] = container; + } + return containers; + } finally { + cursor.close(); + pluginDatabase.close(); + } + } + + /** + * Clears current cache of RTM lists and re-populates + * @param lists + */ + public void setLists(RtmLists lists) { + pluginDatabase.open(context); + try { + List model = new List(); + for(Map.Entry list : lists.getLists().entrySet()) { + model.setValue(List.ID, list.getValue().getId()); + model.setValue(List.NAME, list.getValue().getName()); + listDao.save(pluginDatabase, model); + } + } finally { + pluginDatabase.close(); + } + } + +} diff --git a/astrid/plugin-src/com/todoroo/astrid/rmilk/data/MilkDatabase.java b/astrid/plugin-src/com/todoroo/astrid/rmilk/data/MilkDatabase.java new file mode 100644 index 000000000..179f7990a --- /dev/null +++ b/astrid/plugin-src/com/todoroo/astrid/rmilk/data/MilkDatabase.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2009, Todoroo Inc + * All Rights Reserved + * http://www.todoroo.com + */ +package com.todoroo.astrid.rmilk.data; + +import com.todoroo.andlib.data.AbstractDatabase; +import com.todoroo.andlib.data.GenericDao; +import com.todoroo.andlib.data.Table; + +/** + * Database wrapper + * + * @author Tim Su + * + */ +@SuppressWarnings("nls") +public class MilkDatabase extends AbstractDatabase { + + // --- constants + + /** + * Database version number. This variable must be updated when database + * tables are updated, as it determines whether a database needs updating. + */ + public static final int VERSION = 1; + + /** + * Database name (must be unique) + */ + private static final String NAME = "milk"; + + /** + * List of table/ If you're adding a new table, add it to this list and + * also make sure that our SQLite helper does the right thing. + */ + public static final Table[] TABLES = new Table[] { + MilkList.TABLE + }; + + // --- implementation + + private final GenericDao dao = new GenericDao(MilkList.class, this); + + @Override + protected String getName() { + return NAME; + } + + @Override + protected int getVersion() { + return VERSION; + } + + @Override + public Table[] getTables() { + return TABLES; + } + + public GenericDao getDao() { + return dao; + } + + @Override + protected void onCreateTables() { + // do nothing + } + + @Override + protected boolean onUpgrade(int oldVersion, int newVersion) { + return false; + } + +} + diff --git a/astrid/plugin-src/com/todoroo/astrid/rmilk/data/MilkList.java b/astrid/plugin-src/com/todoroo/astrid/rmilk/data/MilkList.java new file mode 100644 index 000000000..aa4f169b8 --- /dev/null +++ b/astrid/plugin-src/com/todoroo/astrid/rmilk/data/MilkList.java @@ -0,0 +1,103 @@ +/** + * See the file "LICENSE" for the full license governing this code. + */ +package com.todoroo.astrid.rmilk.data; + + +import android.content.ContentValues; + +import com.todoroo.andlib.data.AbstractModel; +import com.todoroo.andlib.data.Property; +import com.todoroo.andlib.data.Table; +import com.todoroo.andlib.data.TodorooCursor; +import com.todoroo.andlib.data.Property.IntegerProperty; +import com.todoroo.andlib.data.Property.LongProperty; +import com.todoroo.andlib.data.Property.StringProperty; +import com.todoroo.astrid.model.Task; + +/** + * Data Model which represents a list in RTM + * + * @author Tim Su + * + */ +@SuppressWarnings("nls") +public class MilkList extends AbstractModel { + + // --- table + + public static final Table TABLE = new Table("lists", MilkList.class); + + // --- properties + + /** ID (corresponds to RTM ID) */ + public static final LongProperty ID = new LongProperty( + TABLE, ID_PROPERTY_NAME); + + /** Name */ + public static final StringProperty NAME = new StringProperty( + TABLE, "name"); + + /** Position */ + public static final IntegerProperty POSITION = new IntegerProperty( + TABLE, "position"); + + /** Archived (0 or 1) */ + public static final IntegerProperty ARCHIVED = new IntegerProperty( + TABLE, "archived"); + + /** List of all properties for this model */ + public static final Property[] PROPERTIES = generateProperties(MilkList.class); + + // --- defaults + + /** Default values container */ + private static final ContentValues defaultValues = new ContentValues(); + + static { + defaultValues.put(POSITION.name, 0); + defaultValues.put(ARCHIVED.name, 0); + } + + @Override + public ContentValues getDefaultValues() { + return defaultValues; + } + + // --- data access boilerplate + + public MilkList() { + super(); + } + + public MilkList(TodorooCursor cursor) { + this(); + readPropertiesFromCursor(cursor); + } + + public void readFromCursor(TodorooCursor cursor) { + super.readPropertiesFromCursor(cursor); + } + + @Override + public long getId() { + return getIdHelper(ID); + }; + + /** + * @return whether this list is archived. requires {@link ARCHIVED} + */ + public boolean isArchived() { + return getValue(ARCHIVED) > 0; + } + + // --- parcelable helpers + + private static final Creator CREATOR = new ModelCreator(Task.class); + + @Override + protected Creator getCreator() { + return CREATOR; + } + +} diff --git a/astrid/plugin-src/com/todoroo/astrid/rmilk/sync/RTMSyncProvider.java b/astrid/plugin-src/com/todoroo/astrid/rmilk/sync/RTMSyncProvider.java new file mode 100644 index 000000000..e9a6a0447 --- /dev/null +++ b/astrid/plugin-src/com/todoroo/astrid/rmilk/sync/RTMSyncProvider.java @@ -0,0 +1,465 @@ +/** + * See the file "LICENSE" for the full license governing this code. + */ +package com.todoroo.astrid.rmilk.sync; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Date; + +import android.content.Context; +import android.content.Intent; +import android.content.res.Resources; +import android.os.Handler; +import android.text.TextUtils; +import android.util.Log; + +import com.flurry.android.FlurryAgent; +import com.timsu.astrid.R; +import com.todoroo.andlib.data.Property; +import com.todoroo.andlib.data.TodorooCursor; +import com.todoroo.andlib.service.Autowired; +import com.todoroo.andlib.service.ContextManager; +import com.todoroo.andlib.service.DependencyInjectionService; +import com.todoroo.andlib.service.ExceptionService; +import com.todoroo.andlib.utility.AndroidUtilities; +import com.todoroo.andlib.utility.DateUtilities; +import com.todoroo.astrid.api.SynchronizationProvider; +import com.todoroo.astrid.model.Task; +import com.todoroo.astrid.rmilk.MilkLoginActivity; +import com.todoroo.astrid.rmilk.Utilities; +import com.todoroo.astrid.rmilk.MilkLoginActivity.SyncLoginCallback; +import com.todoroo.astrid.rmilk.api.ApplicationInfo; +import com.todoroo.astrid.rmilk.api.ServiceImpl; +import com.todoroo.astrid.rmilk.api.ServiceInternalException; +import com.todoroo.astrid.rmilk.api.data.RtmList; +import com.todoroo.astrid.rmilk.api.data.RtmLists; +import com.todoroo.astrid.rmilk.api.data.RtmTask; +import com.todoroo.astrid.rmilk.api.data.RtmTaskList; +import com.todoroo.astrid.rmilk.api.data.RtmTaskSeries; +import com.todoroo.astrid.rmilk.api.data.RtmTasks; +import com.todoroo.astrid.rmilk.api.data.RtmAuth.Perms; +import com.todoroo.astrid.rmilk.api.data.RtmTask.Priority; +import com.todoroo.astrid.rmilk.data.MilkDataService; +import com.todoroo.astrid.service.AstridDependencyInjector; + +public class RTMSyncProvider extends SynchronizationProvider { + + protected ServiceImpl rtmService = null; + protected String timeline = null; + protected MilkDataService dataService = null; + + static { + AstridDependencyInjector.initialize(); + } + + @Autowired + protected ExceptionService exceptionService; + + public RTMSyncProvider() { + super(); + DependencyInjectionService.getInstance().inject(this); + } + + // ---------------------------------------------------------------------- + // ------------------------------------------------------- public methods + // ---------------------------------------------------------------------- + + @Override + public void synchronize() { + Context context = ContextManager.getContext(); + dataService = new MilkDataService(context); + + // authenticate the user. this will automatically call the next step + authenticate(context); + } + + /** + * Sign out of RTM, deleting all synchronization metadata + */ + public void signOut() { + Utilities.setToken(null); + Utilities.setLastSyncDate(null); + dataService.clearMetadata(); + } + + // ---------------------------------------------------------------------- + // ------------------------------------------------------- authentication + // ---------------------------------------------------------------------- + + /** + * Deal with a synchronization exception. If requested, will show an error + * to the user (unless synchronization is happening in background) + * + * @param context + * @param tag + * error tag + * @param e + * exception + * @param showErrorIfNeeded + * whether to display a dialog + */ + private void handleRtmException(Context context, String tag, Exception e, + boolean showErrorIfNeeded) { + // occurs when application was closed + if(e instanceof IllegalStateException) { + exceptionService.reportError(tag + "-caught", e); //$NON-NLS-1$ + + // occurs when network error + } else if(e instanceof ServiceInternalException && + ((ServiceInternalException)e).getEnclosedException() instanceof + IOException) { + Exception enclosedException = ((ServiceInternalException)e).getEnclosedException(); + exceptionService.reportError(tag + "-ioexception", enclosedException); //$NON-NLS-1$ + if(showErrorIfNeeded) { + showError(context, enclosedException, + context.getString(R.string.rmilk_ioerror)); + } + } else { + if(e instanceof ServiceInternalException) + e = ((ServiceInternalException)e).getEnclosedException(); + exceptionService.reportError(tag + "-unhandled", e); //$NON-NLS-1$ + if(showErrorIfNeeded) + showError(context, e, null); + } + } + + /** Perform authentication with RTM. Will open the SyncBrowser if necessary */ + @SuppressWarnings("nls") + private void authenticate(final Context context) { + final Resources r = context.getResources(); + FlurryAgent.onEvent("rtm-started"); + + try { + String appName = null; + String authToken = Utilities.getToken(); + String z = stripslashes(0,"q9883o3384n21snq17501qn38oo1r689", "b"); + String v = stripslashes(16,"19o2n020345219os","a"); + + // check if we have a token & it works + if(authToken != null) { + rtmService = new ServiceImpl(new ApplicationInfo( + z, v, appName, authToken)); + if(!rtmService.isServiceAuthorized()) // re-do login + authToken = null; + } + + if(authToken == null) { + // try completing the authorization if it was partial + if(rtmService != null) { + try { + String token = rtmService.completeAuthorization(); + Utilities.setToken(token); + performSync(context); + + return; + } catch (Exception e) { + // didn't work. do the process again. + } + } + + // open up a dialog and have the user go to browser + rtmService = new ServiceImpl(new ApplicationInfo( + z, v, appName)); + final String url = rtmService.beginAuthorization(Perms.delete); + + Intent intent = new Intent(context, MilkLoginActivity.class); + MilkLoginActivity.setCallback(new SyncLoginCallback() { + public String verifyLogin(final Handler syncLoginHandler) { + if(rtmService == null) { + return null; + } + + try { + String token = rtmService.completeAuthorization(); + Utilities.setToken(token); + return null; + } catch (Exception e) { + // didn't work + exceptionService.reportError("rtm-verify-login", e); + rtmService = null; + if(e instanceof ServiceInternalException) + e = ((ServiceInternalException)e).getEnclosedException(); + return r.getString(R.string.rmilk_MLA_error, e.getMessage()); + } + } + }); + intent.putExtra(MilkLoginActivity.URL_TOKEN, url); + context.startActivity(intent); + + } else { + performSync(context); + } + } catch (IllegalStateException e) { + // occurs when application was closed + } catch (Exception e) { + handleRtmException(context, "rtm-authenticate", e, true); + } + } + + // ---------------------------------------------------------------------- + // ----------------------------------------------------- synchronization! + // ---------------------------------------------------------------------- + + private void performSync(final Context context) { + new Thread(new Runnable() { + public void run() { + performSyncInNewThread(context); + } + }).start(); + } + + protected void performSyncInNewThread(final Context context) { + try { + // get RTM timeline + timeline = rtmService.timelines_create(); + + // load RTM lists + RtmLists lists = rtmService.lists_getList(); + dataService.setLists(lists); + + // read all tasks + ArrayList remoteChanges = new ArrayList(); + Date lastSyncDate = Utilities.getLastSyncDate(); + boolean shouldSyncIndividualLists = false; + String filter = null; + if(lastSyncDate == null) + filter = "status:incomplete"; //$NON-NLS-1$ // 1st time sync: get unfinished tasks + + // try the quick synchronization + try { + Thread.sleep(2000); // throttle + RtmTasks tasks = rtmService.tasks_getList(null, filter, lastSyncDate); + addTasksToList(tasks, remoteChanges); + } catch (Exception e) { + handleRtmException(context, "rtm-quick-sync", e, false); //$NON-NLS-1$ + remoteChanges.clear(); + shouldSyncIndividualLists = true; + } + + if(shouldSyncIndividualLists) { + for(RtmList list : lists.getLists().values()) { + if(list.isSmart()) + continue; + try { + Thread.sleep(1500); + RtmTasks tasks = rtmService.tasks_getList(list.getId(), + filter, lastSyncDate); + addTasksToList(tasks, remoteChanges); + } catch (Exception e) { + handleRtmException(context, "rtm-indiv-sync", e, true); //$NON-NLS-1$ + continue; + } + } + } + + SyncData syncData = populateSyncData(remoteChanges); + synchronizeTasks(syncData); + + Date syncTime = new Date(System.currentTimeMillis()); + Utilities.setLastSyncDate(syncTime); + + FlurryAgent.onEvent("rtm-sync-finished"); //$NON-NLS-1$ + } catch (IllegalStateException e) { + // occurs when application was closed + } catch (Exception e) { + handleRtmException(context, "rtm-sync", e, true); //$NON-NLS-1$ + } + } + + // ---------------------------------------------------------------------- + // ------------------------------------------------------- helper methods + // ---------------------------------------------------------------------- + + /** + * Populate SyncData data structure + */ + private SyncData populateSyncData(ArrayList remoteTasks) { + // all synchronized properties + Property[] properties = new Property[] { + Task.ID, + Task.TITLE, + Task.IMPORTANCE, + Task.DUE_DATE, + Task.CREATION_DATE, + Task.COMPLETION_DATE, + Task.DELETION_DATE, + MilkDataService.LIST_ID, + MilkDataService.TASK_SERIES_ID, + MilkDataService.TASK_ID, + MilkDataService.REPEAT, + MilkDataService.TAGS, + }; + + // fetch locally created tasks + TodorooCursor localCreated = dataService.getLocallyCreated(properties); + + // fetch locally updated tasks + TodorooCursor localUpdated = dataService.getLocallyUpdated(properties); + + return new SyncData(properties, remoteTasks, localCreated, localUpdated); + } + + /** + * Add the tasks read from RTM to the given list + */ + private void addTasksToList(RtmTasks tasks, ArrayList list) { + for (RtmTaskList taskList : tasks.getLists()) { + for (RtmTaskSeries taskSeries : taskList.getSeries()) { + Task remoteTask = parseRemoteTask(taskSeries); + list.add(remoteTask); + } + } + } + + /** + * Determine whether this task's property should be transmitted + * @param task task to consider + * @param property property to consider + * @param remoteTask remote task proxy + * @return + */ + private boolean shouldTransmit(Task task, Property property, Task remoteTask) { + if(!task.hasValue(property)) + return false; + + if(remoteTask == null) + return true; + if(!remoteTask.hasValue(property)) + return true; + return !AndroidUtilities.equals(task.getValue(property), remoteTask.getValue(property)); + } + + @Override + protected void create(Task task) throws IOException { + String listId = null; + if(task.hasValue(MilkDataService.LIST_ID)) + listId = task.getValue(MilkDataService.LIST_ID); + + RtmTaskSeries rtmTask = rtmService.tasks_add(timeline, listId, + task.getValue(Task.TITLE)); + push(task, parseRemoteTask(rtmTask)); + } + + /** Send changes for the given TaskProxy across the wire */ + @Override + protected void push(Task task, Task remoteTask) throws IOException { + RtmId id = new RtmId(task); + + // fetch remote task for comparison + if(remoteTask == null) { + remoteTask = read(task); + } + + if(shouldTransmit(task, Task.TITLE, remoteTask)) + rtmService.tasks_setName(timeline, id.listId, id.taskSeriesId, + id.taskId, task.getValue(Task.TITLE)); + if(shouldTransmit(task, Task.IMPORTANCE, remoteTask)) + rtmService.tasks_setPriority(timeline, id.listId, id.taskSeriesId, + id.taskId, Priority.values()[task.getValue(Task.IMPORTANCE)]); + if(shouldTransmit(task, Task.DUE_DATE, remoteTask)) + rtmService.tasks_setDueDate(timeline, id.listId, id.taskSeriesId, + id.taskId, DateUtilities.unixtimeToDate(task.getValue(Task.DUE_DATE)), + task.getValue(Task.URGENCY) == Task.URGENCY_SPECIFIC_DAY_TIME); + if(shouldTransmit(task, Task.COMPLETION_DATE, remoteTask)) { + if(task.getValue(Task.COMPLETION_DATE) == 0) + rtmService.tasks_uncomplete(timeline, id.listId, id.taskSeriesId, + id.taskId); + else + rtmService.tasks_complete(timeline, id.listId, id.taskSeriesId, + id.taskId); + } + if(shouldTransmit(task, Task.DELETION_DATE, remoteTask)) + rtmService.tasks_delete(timeline, id.listId, id.taskSeriesId, + id.taskId); + + if(shouldTransmit(task, MilkDataService.LIST_ID, remoteTask) && remoteTask != null) + rtmService.tasks_moveTo(timeline, remoteTask.getValue(MilkDataService.LIST_ID), + id.listId, id.taskSeriesId, id.taskId); + } + + /** Create a task proxy for the given RtmTaskSeries */ + private Task parseRemoteTask(RtmTaskSeries rtmTaskSeries) { + Task task = new Task(); + + task.setValue(MilkDataService.LIST_ID, rtmTaskSeries.getList().getId()); + task.setValue(MilkDataService.TASK_SERIES_ID, rtmTaskSeries.getId()); + task.setValue(Task.TITLE, rtmTaskSeries.getName()); + task.setValue(MilkDataService.REPEAT, rtmTaskSeries.hasRecurrence() ? 1 : 0); + + RtmTask rtmTask = rtmTaskSeries.getTask(); + if(rtmTask != null) { + task.setValue(Task.CREATION_DATE, DateUtilities.dateToUnixtime(rtmTask.getAdded())); + task.setValue(Task.COMPLETION_DATE, DateUtilities.dateToUnixtime(rtmTask.getCompleted())); + task.setValue(Task.DELETION_DATE, DateUtilities.dateToUnixtime(rtmTask.getDeleted())); + if(rtmTask.getDue() != null) { + task.setValue(Task.DUE_DATE, DateUtilities.dateToUnixtime(rtmTask.getDue())); + if(rtmTask.getHasDueTime()) + task.setValue(Task.URGENCY, Task.URGENCY_SPECIFIC_DAY_TIME); + else + task.setValue(Task.URGENCY, Task.URGENCY_SPECIFIC_DAY); + } else { + task.setValue(Task.URGENCY, Task.URGENCY_NONE); + } + task.setValue(Task.IMPORTANCE, rtmTask.getPriority().ordinal()); + } else { + // error in upstream code, try to handle gracefully + Log.e("rtmsync", "Got null task parsing remote task series", + new Throwable()); + } + + return task; + } + + @Override + protected Task matchTask(ArrayList tasks, Task target) { + int length = tasks.size(); + for(int i = 0; i < length; i++) { + Task task = tasks.get(i); + if(task.getValue(MilkDataService.LIST_ID).equals(target.getValue(MilkDataService.LIST_ID)) && + task.getValue(MilkDataService.TASK_SERIES_ID).equals(target.getValue(MilkDataService.TASK_SERIES_ID)) && + task.getValue(MilkDataService.TASK_ID).equals(target.getValue(MilkDataService.TASK_ID))) + return task; + } + return null; + } + + @Override + protected Task read(Task task) throws IOException { + RtmTaskSeries rtmTask = rtmService.tasks_getTask(task.getValue(MilkDataService.TASK_SERIES_ID), + task.getValue(Task.TITLE)); + if(rtmTask != null) + return parseRemoteTask(rtmTask); + return null; + } + + @Override + protected void transferIdentifiers(Task source, Task destination) { + destination.setValue(MilkDataService.LIST_ID, source.getValue(MilkDataService.LIST_ID)); + destination.setValue(MilkDataService.TASK_SERIES_ID, source.getValue(MilkDataService.TASK_SERIES_ID)); + destination.setValue(MilkDataService.TASK_ID, source.getValue(MilkDataService.TASK_ID)); + } + + // ---------------------------------------------------------------------- + // ------------------------------------------------------- helper classes + // ---------------------------------------------------------------------- + + /** Helper class for processing RTM id's into one field */ + private static class RtmId { + public String taskId; + public String taskSeriesId; + public String listId; + + public RtmId(Task task) { + taskId = task.getValue(MilkDataService.TASK_ID); + taskSeriesId = task.getValue(MilkDataService.TASK_SERIES_ID); + listId = task.getValue(MilkDataService.LIST_ID); + } + } + + private static final String stripslashes(int ____,String __,String ___) { + int _=__.charAt(____/92);_=_==115?_-1:_;_=((_>=97)&&(_<=123)?((_-83)%27+97):_);return + TextUtils.htmlEncode(____==31?___:stripslashes(____+1,__.substring(1),___+((char)_))); + } + +} diff --git a/astrid/res/layout/rmilk_edit_activity.xml b/astrid/res/layout/rmilk_edit_activity.xml new file mode 100644 index 000000000..d63ec219c --- /dev/null +++ b/astrid/res/layout/rmilk_edit_activity.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + +