diff --git a/astrid/.classpath b/astrid/.classpath
index ef4e117f9..0025f06fe 100644
--- a/astrid/.classpath
+++ b/astrid/.classpath
@@ -6,6 +6,7 @@
+
diff --git a/astrid/AndroidManifest.xml b/astrid/AndroidManifest.xml
index 78ec5bf91..a4d8f0dca 100644
--- a/astrid/AndroidManifest.xml
+++ b/astrid/AndroidManifest.xml
@@ -1,8 +1,8 @@
+ android:versionName="3.3.0-rc1"
+ android:versionCode="154">
diff --git a/astrid/res/layout/rmilk_login_activity.xml b/astrid/res/layout/rmilk_login_activity.xml
new file mode 100644
index 000000000..e91492e54
--- /dev/null
+++ b/astrid/res/layout/rmilk_login_activity.xml
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/astrid/res/values/keys-rmilk.xml b/astrid/res/values/keys-rmilk.xml
new file mode 100644
index 000000000..9db71825a
--- /dev/null
+++ b/astrid/res/values/keys-rmilk.xml
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+ - 0
+ - 900
+ - 1800
+ - 3600
+ - 10800
+ - 21600
+ - 43200
+ - 86400
+ - 259200
+ - 604800
+
+
+
+ sync_status
+
+ sync_bgwifi
+
+ sync_sync
+
+ sync_forget
+
+
+
+
+ sync_freq
+
+
diff --git a/astrid/res/values/strings-rmilk.xml b/astrid/res/values/strings-rmilk.xml
new file mode 100644
index 000000000..1bf04dc4e
--- /dev/null
+++ b/astrid/res/values/strings-rmilk.xml
@@ -0,0 +1,69 @@
+
+
+
+
+
+ Astrid Remember the Milk Plugin
+
+
+
+
+ Remember the Milk Settings
+
+
+ RTM Repeating Task
+
+
+ Needs synchronization with RTM
+
+
+ Remember the Milk
+
+
+ Lists
+
+
+ RTM List \'%s\'
+
+
+
+
+ Remember the Milk
+
+
+ RTM List:
+
+
+ RTM Repeat Status:
+
+
+ i.e. every week, after 14 days
+
+
+
+
+ Remember the Milk
+
+
+
+
+ Please Log In and Authorize Astrid:
+
+
+
+Sorry, there was an error verifying your login. Please try again.
+\n\n
+Error Message: %s
+
+
+
+
+
+ Astrid: Remember the Milk
+
+
+ Connection Error! Check your Internet connection,
+ or maybe RTM servers (status.rememberthemilk.com), for possible solutions.
+
+
+
diff --git a/astrid/res/xml/preferences_rmilk.xml b/astrid/res/xml/preferences_rmilk.xml
new file mode 100644
index 000000000..537f4e168
--- /dev/null
+++ b/astrid/res/xml/preferences_rmilk.xml
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/astrid/rmilk-src/.gitignore b/astrid/rmilk-src/.gitignore
new file mode 100644
index 000000000..e69de29bb
diff --git a/astrid/rmilk-src/org/weloveastrid/rmilk/MilkBackgroundService.java b/astrid/rmilk-src/org/weloveastrid/rmilk/MilkBackgroundService.java
new file mode 100644
index 000000000..060c8871d
--- /dev/null
+++ b/astrid/rmilk-src/org/weloveastrid/rmilk/MilkBackgroundService.java
@@ -0,0 +1,130 @@
+package org.weloveastrid.rmilk;
+
+import org.weloveastrid.rmilk.sync.MilkSyncProvider;
+
+import android.app.AlarmManager;
+import android.app.PendingIntent;
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.os.IBinder;
+import android.util.Log;
+
+import com.todoroo.andlib.service.ContextManager;
+import com.todoroo.andlib.utility.DateUtilities;
+
+/**
+ * SynchronizationService is the service that performs Astrid's background
+ * synchronization with online task managers. Starting this service
+ * schedules a repeating alarm which handles the synchronization
+ *
+ * @author Tim Su
+ *
+ */
+public class MilkBackgroundService extends Service {
+
+ /** Minimum time before an auto-sync */
+ private static final long AUTO_SYNC_MIN_OFFSET = 5*60*1000L;
+
+ /** alarm identifier */
+ public static final String SYNC_ACTION = "sync"; //$NON-NLS-1$
+
+ // --- BroadcastReceiver abstract methods
+
+ /** start the synchronization service. sits in the background */
+ @Override
+ public void onStart(Intent intent, int startId) {
+ try {
+ if(intent != null && SYNC_ACTION.equals(intent.getAction()))
+ startSynchronization(this);
+ } catch (Exception e) {
+ MilkUtilities.INSTANCE.setLastError(e.toString());
+ }
+ }
+
+ /** Start the actual synchronization */
+ private void startSynchronization(Context context) {
+ if(context == null || context.getResources() == null)
+ return;
+
+ ContextManager.setContext(context);
+
+ if(MilkUtilities.INSTANCE.isOngoing())
+ return;
+
+ new MilkSyncProvider().synchronize(context);
+ }
+
+ // --- alarm management
+
+ /**
+ * Schedules repeating alarm for auto-synchronization
+ */
+ public static void scheduleService() {
+ Context context = ContextManager.getContext();
+ int syncFrequencySeconds = MilkUtilities.INSTANCE.getSyncAutoSyncFrequency();
+ if(syncFrequencySeconds <= 0) {
+ unscheduleService(context);
+ return;
+ }
+
+ // figure out synchronization frequency
+ long interval = 1000L * syncFrequencySeconds;
+ long offset = computeNextSyncOffset(interval);
+
+ // give a little padding
+ offset = Math.max(offset, AUTO_SYNC_MIN_OFFSET);
+
+ AlarmManager am = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
+ PendingIntent pendingIntent = PendingIntent.getService(context, 0,
+ createAlarmIntent(context), PendingIntent.FLAG_UPDATE_CURRENT);
+
+ Log.i("Astrid", "Autosync set for " + offset / 1000 //$NON-NLS-1$ //$NON-NLS-2$
+ + " seconds repeating every " + syncFrequencySeconds); //$NON-NLS-1$
+
+ // cancel all existing
+ am.cancel(pendingIntent);
+
+ // schedule new
+ am.setRepeating(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + offset,
+ interval, pendingIntent);
+ }
+
+
+ /**
+ * Removes repeating alarm for auto-synchronization
+ */
+ private static void unscheduleService(Context context) {
+ AlarmManager am = (AlarmManager)context.getSystemService(Context.ALARM_SERVICE);
+ PendingIntent pendingIntent = PendingIntent.getService(context, 0,
+ createAlarmIntent(context), PendingIntent.FLAG_UPDATE_CURRENT);
+ am.cancel(pendingIntent);
+ }
+
+ /** Create the alarm intent */
+ private static Intent createAlarmIntent(Context context) {
+ Intent intent = new Intent(context, MilkBackgroundService.class);
+ intent.setAction(SYNC_ACTION);
+ return intent;
+ }
+
+ // --- utility methods
+
+
+ private static long computeNextSyncOffset(long interval) {
+ // figure out last synchronize time
+ long lastSyncDate = MilkUtilities.INSTANCE.getLastSyncDate();
+
+ // if user never synchronized, give them a full offset period before bg sync
+ if(lastSyncDate != 0)
+ return Math.max(0, lastSyncDate + interval - DateUtilities.now());
+ else
+ return interval;
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return null;
+ }
+
+}
diff --git a/astrid/rmilk-src/org/weloveastrid/rmilk/MilkDetailExposer.java b/astrid/rmilk-src/org/weloveastrid/rmilk/MilkDetailExposer.java
new file mode 100644
index 000000000..c3880a8d7
--- /dev/null
+++ b/astrid/rmilk-src/org/weloveastrid/rmilk/MilkDetailExposer.java
@@ -0,0 +1,98 @@
+/**
+ * See the file "LICENSE" for the full license governing this code.
+ */
+package org.weloveastrid.rmilk;
+
+import org.weloveastrid.rmilk.data.MilkDataService;
+import org.weloveastrid.rmilk.data.MilkNoteFields;
+import org.weloveastrid.rmilk.data.MilkTaskFields;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+
+import com.timsu.astrid.R;
+import com.todoroo.andlib.data.TodorooCursor;
+import com.todoroo.andlib.service.ContextManager;
+import com.todoroo.astrid.api.AstridApiConstants;
+import com.todoroo.astrid.data.Metadata;
+
+/**
+ * Exposes Task Details for Remember the Milk:
+ * - RTM list
+ * - RTM repeat information
+ * - RTM notes
+ *
+ * @author Tim Su
+ *
+ */
+public class MilkDetailExposer extends BroadcastReceiver {
+
+ public static final String DETAIL_SEPARATOR = " | "; //$NON-NLS-1$
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ ContextManager.setContext(context);
+
+ // if we aren't logged in, don't expose features
+ if(!MilkUtilities.INSTANCE.isLoggedIn())
+ return;
+
+ long taskId = intent.getLongExtra(AstridApiConstants.EXTRAS_TASK_ID, -1);
+ if(taskId == -1)
+ return;
+
+ boolean extended = intent.getBooleanExtra(AstridApiConstants.EXTRAS_EXTENDED, false);
+ String taskDetail = getTaskDetails(context, taskId, extended);
+ if(taskDetail == null)
+ return;
+
+ Intent broadcastIntent = new Intent(AstridApiConstants.BROADCAST_SEND_DETAILS);
+ broadcastIntent.putExtra(AstridApiConstants.EXTRAS_ADDON, MilkUtilities.IDENTIFIER);
+ broadcastIntent.putExtra(AstridApiConstants.EXTRAS_TASK_ID, taskId);
+ broadcastIntent.putExtra(AstridApiConstants.EXTRAS_EXTENDED, extended);
+ broadcastIntent.putExtra(AstridApiConstants.EXTRAS_RESPONSE, taskDetail);
+ context.sendBroadcast(broadcastIntent, AstridApiConstants.PERMISSION_READ);
+ }
+
+ public String getTaskDetails(Context context, long id, boolean extended) {
+ Metadata metadata = MilkDataService.getInstance(context).getTaskMetadata(id);
+ if(metadata == null)
+ return null;
+
+ StringBuilder builder = new StringBuilder();
+
+ if(!extended) {
+ long listId = metadata.getValue(MilkTaskFields.LIST_ID);
+ String listName = MilkDataService.getInstance(context).getListName(listId);
+ // RTM list is out of date. don't display RTM stuff
+ if(listName == null)
+ return null;
+
+ if(listId > 0 && !"Inbox".equals(listName)) { //$NON-NLS-1$
+ builder.append("
").append(listName).append(DETAIL_SEPARATOR); //$NON-NLS-1$
+ }
+
+ int repeat = metadata.getValue(MilkTaskFields.REPEATING);
+ if(repeat != 0) {
+ builder.append(context.getString(R.string.rmilk_TLA_repeat)).append(DETAIL_SEPARATOR);
+ }
+ } else {
+ TodorooCursor notesCursor = MilkDataService.getInstance(context).getTaskNotesCursor(id);
+ try {
+ for(notesCursor.moveToFirst(); !notesCursor.isAfterLast(); notesCursor.moveToNext()) {
+ metadata.readFromCursor(notesCursor);
+ builder.append(MilkNoteFields.toTaskDetail(metadata)).append(DETAIL_SEPARATOR);
+ }
+ } finally {
+ notesCursor.close();
+ }
+ }
+
+ if(builder.length() == 0)
+ return null;
+ String result = builder.toString();
+ return result.substring(0, result.length() - DETAIL_SEPARATOR.length());
+ }
+
+}
diff --git a/astrid/rmilk-src/org/weloveastrid/rmilk/MilkFilterExposer.java b/astrid/rmilk-src/org/weloveastrid/rmilk/MilkFilterExposer.java
new file mode 100644
index 000000000..422cedbc4
--- /dev/null
+++ b/astrid/rmilk-src/org/weloveastrid/rmilk/MilkFilterExposer.java
@@ -0,0 +1,91 @@
+/**
+ * See the file "LICENSE" for the full license governing this code.
+ */
+package org.weloveastrid.rmilk;
+
+import org.weloveastrid.rmilk.data.MilkDataService;
+import org.weloveastrid.rmilk.data.MilkListFields;
+import org.weloveastrid.rmilk.data.MilkTaskFields;
+
+import android.content.BroadcastReceiver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.Intent;
+
+import com.timsu.astrid.R;
+import com.todoroo.andlib.service.ContextManager;
+import com.todoroo.andlib.sql.Criterion;
+import com.todoroo.andlib.sql.Join;
+import com.todoroo.andlib.sql.QueryTemplate;
+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.data.Metadata;
+import com.todoroo.astrid.data.StoreObject;
+import com.todoroo.astrid.data.Task;
+import com.todoroo.astrid.data.MetadataApiDao.MetadataCriteria;
+import com.todoroo.astrid.data.TaskApiDao.TaskCriteria;
+
+/**
+ * Exposes filters based on RTM lists
+ *
+ * @author Tim Su
+ *
+ */
+public class MilkFilterExposer extends BroadcastReceiver {
+
+ private Filter filterFromList(Context context, StoreObject list) {
+ String listName = list.getValue(MilkListFields.NAME);
+ String title = context.getString(R.string.rmilk_FEx_list_title,
+ listName);
+ ContentValues values = new ContentValues();
+ values.put(Metadata.KEY.name, MilkTaskFields.METADATA_KEY);
+ values.put(MilkTaskFields.LIST_ID.name, list.getValue(MilkListFields.REMOTE_ID));
+ values.put(MilkTaskFields.TASK_SERIES_ID.name, 0);
+ values.put(MilkTaskFields.TASK_ID.name, 0);
+ values.put(MilkTaskFields.REPEATING.name, 0);
+ Filter filter = new Filter(listName, title, new QueryTemplate().join(
+ Join.left(Metadata.TABLE, Task.ID.eq(Metadata.TASK))).where(Criterion.and(
+ MetadataCriteria.withKey(MilkTaskFields.METADATA_KEY),
+ TaskCriteria.activeAndVisible(),
+ MilkTaskFields.LIST_ID.eq(list.getValue(MilkListFields.REMOTE_ID)))),
+ values);
+
+ return filter;
+ }
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ ContextManager.setContext(context);
+
+ // if we aren't logged in, don't expose features
+ if(!MilkUtilities.INSTANCE.isLoggedIn())
+ return;
+
+ StoreObject[] lists = MilkDataService.getInstance(context).getLists();
+
+ // 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_ADDON, MilkUtilities.IDENTIFIER);
+ broadcastIntent.putExtra(AstridApiConstants.EXTRAS_RESPONSE, list);
+ context.sendBroadcast(broadcastIntent, AstridApiConstants.PERMISSION_READ);
+ }
+
+}
diff --git a/astrid/rmilk-src/org/weloveastrid/rmilk/MilkLoginActivity.java b/astrid/rmilk-src/org/weloveastrid/rmilk/MilkLoginActivity.java
new file mode 100644
index 000000000..11602eb1d
--- /dev/null
+++ b/astrid/rmilk-src/org/weloveastrid/rmilk/MilkLoginActivity.java
@@ -0,0 +1,133 @@
+/*
+ * 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 org.weloveastrid.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.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 {
+
+ // --- 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
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.rmilk_login_activity);
+ setTitle(R.string.rmilk_MPr_header);
+
+ final 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, re-load url
+ handler.post(new Runnable() {
+ public void run() {
+ DialogUtilities.okDialog(MilkLoginActivity.this,
+ result, null);
+ webView.loadUrl(urlParam);
+ }
+ });
+ }
+ }
+ }).start();
+ }
+ });
+
+ cancel.setOnClickListener(new OnClickListener() {
+ public void onClick(View v) {
+ setResult(RESULT_CANCELED);
+ finish();
+ }
+ });
+ }
+}
diff --git a/astrid/rmilk-src/org/weloveastrid/rmilk/MilkPreferences.java b/astrid/rmilk-src/org/weloveastrid/rmilk/MilkPreferences.java
new file mode 100644
index 000000000..613486a18
--- /dev/null
+++ b/astrid/rmilk-src/org/weloveastrid/rmilk/MilkPreferences.java
@@ -0,0 +1,38 @@
+package org.weloveastrid.rmilk;
+
+import org.weloveastrid.rmilk.sync.MilkSyncProvider;
+
+import com.timsu.astrid.R;
+import com.todoroo.astrid.sync.SyncProviderPreferences;
+import com.todoroo.astrid.sync.SyncProviderUtilities;
+
+/**
+ * Displays synchronization preferences and an action panel so users can
+ * initiate actions from the menu.
+ *
+ * @author Tim Su
+ *
+ */
+public class MilkPreferences extends SyncProviderPreferences {
+
+ @Override
+ public int getPreferenceResource() {
+ return R.xml.preferences_rmilk;
+ }
+
+ @Override
+ public void startSync() {
+ new MilkSyncProvider().synchronize(this);
+ }
+
+ @Override
+ public void logOut() {
+ new MilkSyncProvider().signOut(this);
+ }
+
+ @Override
+ public SyncProviderUtilities getUtilities() {
+ return MilkUtilities.INSTANCE;
+ }
+
+}
\ No newline at end of file
diff --git a/astrid/rmilk-src/org/weloveastrid/rmilk/MilkStartupReceiver.java b/astrid/rmilk-src/org/weloveastrid/rmilk/MilkStartupReceiver.java
new file mode 100644
index 000000000..0353b709b
--- /dev/null
+++ b/astrid/rmilk-src/org/weloveastrid/rmilk/MilkStartupReceiver.java
@@ -0,0 +1,23 @@
+/**
+ * See the file "LICENSE" for the full license governing this code.
+ */
+package org.weloveastrid.rmilk;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+
+import com.todoroo.andlib.service.ContextManager;
+
+public class MilkStartupReceiver extends BroadcastReceiver {
+
+ @Override
+ /** Called when device is restarted */
+ public void onReceive(final Context context, Intent intent) {
+ ContextManager.setContext(context);
+
+ MilkBackgroundService.scheduleService();
+ MilkUtilities.INSTANCE.stopOngoing();
+ }
+
+}
diff --git a/astrid/rmilk-src/org/weloveastrid/rmilk/MilkSyncActionExposer.java b/astrid/rmilk-src/org/weloveastrid/rmilk/MilkSyncActionExposer.java
new file mode 100644
index 000000000..666e3bbe9
--- /dev/null
+++ b/astrid/rmilk-src/org/weloveastrid/rmilk/MilkSyncActionExposer.java
@@ -0,0 +1,42 @@
+/**
+ * See the file "LICENSE" for the full license governing this code.
+ */
+package org.weloveastrid.rmilk;
+
+import android.app.PendingIntent;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+
+import com.timsu.astrid.R;
+import com.todoroo.andlib.service.ContextManager;
+import com.todoroo.astrid.api.AstridApiConstants;
+import com.todoroo.astrid.api.SyncAction;
+
+/**
+ * Exposes sync action
+ *
+ */
+public class MilkSyncActionExposer extends BroadcastReceiver {
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ ContextManager.setContext(context);
+
+ // if we aren't logged in, don't expose sync action
+ if(!MilkUtilities.INSTANCE.isLoggedIn())
+ return;
+
+ Intent syncIntent = new Intent(MilkBackgroundService.SYNC_ACTION, null,
+ context, MilkBackgroundService.class);
+ PendingIntent pendingIntent = PendingIntent.getService(context, 0, syncIntent, PendingIntent.FLAG_UPDATE_CURRENT);
+ SyncAction syncAction = new SyncAction(context.getString(R.string.rmilk_MPr_header),
+ pendingIntent);
+
+ Intent broadcastIntent = new Intent(AstridApiConstants.BROADCAST_SEND_SYNC_ACTIONS);
+ broadcastIntent.putExtra(AstridApiConstants.EXTRAS_ADDON, MilkUtilities.IDENTIFIER);
+ broadcastIntent.putExtra(AstridApiConstants.EXTRAS_RESPONSE, syncAction);
+ context.sendBroadcast(broadcastIntent, AstridApiConstants.PERMISSION_READ);
+ }
+
+}
diff --git a/astrid/rmilk-src/org/weloveastrid/rmilk/MilkUtilities.java b/astrid/rmilk-src/org/weloveastrid/rmilk/MilkUtilities.java
new file mode 100644
index 000000000..a1018a939
--- /dev/null
+++ b/astrid/rmilk-src/org/weloveastrid/rmilk/MilkUtilities.java
@@ -0,0 +1,63 @@
+/**
+ * See the file "LICENSE" for the full license governing this code.
+ */
+package org.weloveastrid.rmilk;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.content.SharedPreferences.Editor;
+
+import com.timsu.astrid.R;
+import com.todoroo.andlib.service.ContextManager;
+import com.todoroo.astrid.sync.SyncProviderUtilities;
+
+/**
+ * Constants and preferences for RTM plugin
+ *
+ * @author Tim Su
+ *
+ */
+@SuppressWarnings("nls")
+public class MilkUtilities extends SyncProviderUtilities {
+
+ // --- constants
+
+ /** add-on identifier */
+ public static final String IDENTIFIER = "rmilk";
+
+ public static final MilkUtilities INSTANCE = new MilkUtilities();
+
+ // --- utilities boilerplate
+
+ private MilkUtilities() {
+ // if no token is set, see if astrid has exported one
+ if(getToken() == null) {
+ try {
+ Context astridContext = ContextManager.getContext().createPackageContext("com.timsu.astrid", 0);
+ SharedPreferences sharedPreferences = astridContext.getSharedPreferences("rtm", 0);
+ if(sharedPreferences != null) {
+ String token = sharedPreferences.getString("rmilk_token", null);
+ long lastSyncDate = sharedPreferences.getLong("rmilk_last_sync", 0);
+
+ Editor editor = getPrefs().edit();
+ editor.putString(getIdentifier() + PREF_TOKEN, token);
+ editor.putLong(getIdentifier() + PREF_LAST_SYNC, lastSyncDate);
+ editor.commit();
+ }
+ } catch (Exception e) {
+ // too bad
+ }
+ }
+ }
+
+; @Override
+ public String getIdentifier() {
+ return IDENTIFIER;
+ }
+
+ @Override
+ public int getSyncIntervalKey() {
+ return R.string.rmilk_MPr_interval_key;
+ }
+
+}
diff --git a/astrid/rmilk-src/org/weloveastrid/rmilk/api/ApplicationInfo.java b/astrid/rmilk-src/org/weloveastrid/rmilk/api/ApplicationInfo.java
new file mode 100644
index 000000000..40b664ecb
--- /dev/null
+++ b/astrid/rmilk-src/org/weloveastrid/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 org.weloveastrid.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/rmilk-src/org/weloveastrid/rmilk/api/Invoker.java b/astrid/rmilk-src/org/weloveastrid/rmilk/api/Invoker.java
new file mode 100644
index 000000000..ebbc1518a
--- /dev/null
+++ b/astrid/rmilk-src/org/weloveastrid/rmilk/api/Invoker.java
@@ -0,0 +1,299 @@
+/*
+ * 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 org.weloveastrid.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 SERVICE_UNAVAILABLE_CODE = "105";
+
+ 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 final String serviceRelativeUri;
+
+ private final 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 {
+ if (SERVICE_UNAVAILABLE_CODE.equals(((Element) errElt)
+ .getAttribute("code")) && !repeat) {
+ try {
+ Thread.sleep(1500);
+ } catch (InterruptedException e) {
+ // ignore
+ }
+ return invoke(true, params);
+ }
+
+ 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/rmilk-src/org/weloveastrid/rmilk/api/Param.java b/astrid/rmilk-src/org/weloveastrid/rmilk/api/Param.java
new file mode 100644
index 000000000..b322154f7
--- /dev/null
+++ b/astrid/rmilk-src/org/weloveastrid/rmilk/api/Param.java
@@ -0,0 +1,59 @@
+/*
+ * 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 org.weloveastrid.rmilk.api;
+
+import java.util.Date;
+
+import org.weloveastrid.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/rmilk-src/org/weloveastrid/rmilk/api/Prefs.java b/astrid/rmilk-src/org/weloveastrid/rmilk/api/Prefs.java
new file mode 100644
index 000000000..8c6c5ab71
--- /dev/null
+++ b/astrid/rmilk-src/org/weloveastrid/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 org.weloveastrid.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/rmilk-src/org/weloveastrid/rmilk/api/ServiceException.java b/astrid/rmilk-src/org/weloveastrid/rmilk/api/ServiceException.java
new file mode 100644
index 000000000..ce14e85ff
--- /dev/null
+++ b/astrid/rmilk-src/org/weloveastrid/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 org.weloveastrid.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/rmilk-src/org/weloveastrid/rmilk/api/ServiceImpl.java b/astrid/rmilk-src/org/weloveastrid/rmilk/api/ServiceImpl.java
new file mode 100644
index 000000000..fd960aba4
--- /dev/null
+++ b/astrid/rmilk-src/org/weloveastrid/rmilk/api/ServiceImpl.java
@@ -0,0 +1,617 @@
+/*
+ * 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 org.weloveastrid.rmilk.api;
+
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map.Entry;
+import java.util.Set;
+
+import org.w3c.dom.Element;
+import org.weloveastrid.rmilk.api.data.RtmAuth;
+import org.weloveastrid.rmilk.api.data.RtmData;
+import org.weloveastrid.rmilk.api.data.RtmFrob;
+import org.weloveastrid.rmilk.api.data.RtmList;
+import org.weloveastrid.rmilk.api.data.RtmLists;
+import org.weloveastrid.rmilk.api.data.RtmLocation;
+import org.weloveastrid.rmilk.api.data.RtmTask;
+import org.weloveastrid.rmilk.api.data.RtmTask.Priority;
+import org.weloveastrid.rmilk.api.data.RtmTaskList;
+import org.weloveastrid.rmilk.api.data.RtmTaskNote;
+import org.weloveastrid.rmilk.api.data.RtmTaskSeries;
+import org.weloveastrid.rmilk.api.data.RtmTasks;
+import org.weloveastrid.rmilk.api.data.RtmTimeline;
+
+
+/**
+ * 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
+ {
+ if(fromListId.equals(toListId))
+ return null;
+ 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/rmilk-src/org/weloveastrid/rmilk/api/ServiceInternalException.java b/astrid/rmilk-src/org/weloveastrid/rmilk/api/ServiceInternalException.java
new file mode 100644
index 000000000..604d1abb3
--- /dev/null
+++ b/astrid/rmilk-src/org/weloveastrid/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 org.weloveastrid.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/rmilk-src/org/weloveastrid/rmilk/api/data/RtmAuth.java b/astrid/rmilk-src/org/weloveastrid/rmilk/api/data/RtmAuth.java
new file mode 100644
index 000000000..519a4ac00
--- /dev/null
+++ b/astrid/rmilk-src/org/weloveastrid/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 org.weloveastrid.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/rmilk-src/org/weloveastrid/rmilk/api/data/RtmData.java b/astrid/rmilk-src/org/weloveastrid/rmilk/api/data/RtmData.java
new file mode 100644
index 000000000..698060f1e
--- /dev/null
+++ b/astrid/rmilk-src/org/weloveastrid/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 org.weloveastrid.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/rmilk-src/org/weloveastrid/rmilk/api/data/RtmFrob.java b/astrid/rmilk-src/org/weloveastrid/rmilk/api/data/RtmFrob.java
new file mode 100644
index 000000000..0b2b52c38
--- /dev/null
+++ b/astrid/rmilk-src/org/weloveastrid/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 org.weloveastrid.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/rmilk-src/org/weloveastrid/rmilk/api/data/RtmList.java b/astrid/rmilk-src/org/weloveastrid/rmilk/api/data/RtmList.java
new file mode 100644
index 000000000..e4d5b996e
--- /dev/null
+++ b/astrid/rmilk-src/org/weloveastrid/rmilk/api/data/RtmList.java
@@ -0,0 +1,72 @@
+/*
+ * 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 org.weloveastrid.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 boolean archived;
+ private final int position;
+ private final String name;
+
+ public RtmList(String id, String name, boolean smart, boolean archived, int position) {
+ this.id = id;
+ this.name = name;
+ this.smart = smart;
+ this.archived = archived;
+ this.position = position;
+ }
+
+ public RtmList(Element elt) {
+ id = elt.getAttribute("id");
+ name = elt.getAttribute("name");
+ smart = elt.getAttribute("smart").equals("1");
+ archived = elt.getAttribute("archived").equals("1");
+ position = Integer.parseInt(elt.getAttribute("position"));
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public boolean isSmart() {
+ return smart;
+ }
+
+ public boolean isArchived() {
+ return archived;
+ }
+
+ public int getPosition() {
+ return position;
+ }
+
+ public boolean isInbox() {
+ return position == -1;
+ }
+}
diff --git a/astrid/rmilk-src/org/weloveastrid/rmilk/api/data/RtmLists.java b/astrid/rmilk-src/org/weloveastrid/rmilk/api/data/RtmLists.java
new file mode 100644
index 000000000..5c7c1aa15
--- /dev/null
+++ b/astrid/rmilk-src/org/weloveastrid/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 org.weloveastrid.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/rmilk-src/org/weloveastrid/rmilk/api/data/RtmLocation.java b/astrid/rmilk-src/org/weloveastrid/rmilk/api/data/RtmLocation.java
new file mode 100644
index 000000000..2cf2e53e8
--- /dev/null
+++ b/astrid/rmilk-src/org/weloveastrid/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 org.weloveastrid.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/rmilk-src/org/weloveastrid/rmilk/api/data/RtmTask.java b/astrid/rmilk-src/org/weloveastrid/rmilk/api/data/RtmTask.java
new file mode 100644
index 000000000..147ead68d
--- /dev/null
+++ b/astrid/rmilk-src/org/weloveastrid/rmilk/api/data/RtmTask.java
@@ -0,0 +1,199 @@
+/*
+ * 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 org.weloveastrid.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 Priority values(Integer value) {
+ value = Math.max(values().length - 1, value);
+ return values()[value];
+ }
+ }
+
+ 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/rmilk-src/org/weloveastrid/rmilk/api/data/RtmTaskList.java b/astrid/rmilk-src/org/weloveastrid/rmilk/api/data/RtmTaskList.java
new file mode 100644
index 000000000..240a1ca0c
--- /dev/null
+++ b/astrid/rmilk-src/org/weloveastrid/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 org.weloveastrid.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/rmilk-src/org/weloveastrid/rmilk/api/data/RtmTaskNote.java b/astrid/rmilk-src/org/weloveastrid/rmilk/api/data/RtmTaskNote.java
new file mode 100644
index 000000000..73e110877
--- /dev/null
+++ b/astrid/rmilk-src/org/weloveastrid/rmilk/api/data/RtmTaskNote.java
@@ -0,0 +1,106 @@
+/*
+ * 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 org.weloveastrid.rmilk.api.data;
+
+import java.util.Date;
+
+import org.w3c.dom.Element;
+import org.w3c.dom.EntityReference;
+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 final String id;
+
+ private final Date created;
+
+ private final Date modified;
+
+ private final 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 EntityReference) // this node is empty
+ continue;
+
+ 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/rmilk-src/org/weloveastrid/rmilk/api/data/RtmTaskNotes.java b/astrid/rmilk-src/org/weloveastrid/rmilk/api/data/RtmTaskNotes.java
new file mode 100644
index 000000000..3526dcec5
--- /dev/null
+++ b/astrid/rmilk-src/org/weloveastrid/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 org.weloveastrid.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/rmilk-src/org/weloveastrid/rmilk/api/data/RtmTaskSeries.java b/astrid/rmilk-src/org/weloveastrid/rmilk/api/data/RtmTaskSeries.java
new file mode 100644
index 000000000..e10f97f36
--- /dev/null
+++ b/astrid/rmilk-src/org/weloveastrid/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 org.weloveastrid.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/rmilk-src/org/weloveastrid/rmilk/api/data/RtmTasks.java b/astrid/rmilk-src/org/weloveastrid/rmilk/api/data/RtmTasks.java
new file mode 100644
index 000000000..9cd90c308
--- /dev/null
+++ b/astrid/rmilk-src/org/weloveastrid/rmilk/api/data/RtmTasks.java
@@ -0,0 +1,51 @@
+/*
+ * 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 org.weloveastrid.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/rmilk-src/org/weloveastrid/rmilk/api/data/RtmTimeline.java b/astrid/rmilk-src/org/weloveastrid/rmilk/api/data/RtmTimeline.java
new file mode 100644
index 000000000..b128ac9f2
--- /dev/null
+++ b/astrid/rmilk-src/org/weloveastrid/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 org.weloveastrid.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/rmilk-src/org/weloveastrid/rmilk/api/data/RtmUser.java b/astrid/rmilk-src/org/weloveastrid/rmilk/api/data/RtmUser.java
new file mode 100644
index 000000000..b6288a834
--- /dev/null
+++ b/astrid/rmilk-src/org/weloveastrid/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 org.weloveastrid.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/rmilk-src/org/weloveastrid/rmilk/data/MilkDataService.java b/astrid/rmilk-src/org/weloveastrid/rmilk/data/MilkDataService.java
new file mode 100644
index 000000000..25ff6c09a
--- /dev/null
+++ b/astrid/rmilk-src/org/weloveastrid/rmilk/data/MilkDataService.java
@@ -0,0 +1,324 @@
+/**
+ * See the file "LICENSE" for the full license governing this code.
+ */
+package org.weloveastrid.rmilk.data;
+
+import java.util.ArrayList;
+import java.util.Map;
+import java.util.Random;
+
+import org.weloveastrid.rmilk.MilkUtilities;
+import org.weloveastrid.rmilk.api.data.RtmList;
+import org.weloveastrid.rmilk.api.data.RtmLists;
+import org.weloveastrid.rmilk.sync.MilkTaskContainer;
+
+import android.content.Context;
+import android.database.CursorJoiner;
+
+import com.todoroo.andlib.data.Property;
+import com.todoroo.andlib.data.TodorooCursor;
+import com.todoroo.andlib.sql.Criterion;
+import com.todoroo.andlib.sql.Order;
+import com.todoroo.andlib.sql.Query;
+import com.todoroo.astrid.data.Metadata;
+import com.todoroo.astrid.data.MetadataApiDao;
+import com.todoroo.astrid.data.MetadataApiDao.MetadataCriteria;
+import com.todoroo.astrid.data.StoreObject;
+import com.todoroo.astrid.data.StoreObjectApiDao;
+import com.todoroo.astrid.data.StoreObjectApiDao.StoreObjectCriteria;
+import com.todoroo.astrid.data.Task;
+import com.todoroo.astrid.data.TaskApiDao;
+import com.todoroo.astrid.data.TaskApiDao.TaskCriteria;
+
+public final class MilkDataService {
+
+ /** metadata key of tag addon */
+ public static final String TAG_KEY = "tags-tag"; //$NON-NLS-1$
+
+ // --- singleton
+
+ private static MilkDataService instance = null;
+
+ public static synchronized MilkDataService getInstance(Context context) {
+ if(instance == null)
+ instance = new MilkDataService(context);
+ return instance;
+ }
+
+ // --- instance variables
+
+ private final TaskApiDao taskDao;
+ private final MetadataApiDao metadataDao;
+ private final StoreObjectApiDao storeObjectDao;
+
+ static final Random random = new Random();
+
+ private MilkDataService(Context context) {
+ // prevent instantiation
+ taskDao = new TaskApiDao(context);
+ storeObjectDao = new StoreObjectApiDao(context);
+ metadataDao = new MetadataApiDao(context);
+ }
+
+ // --- task and metadata methods
+
+ /**
+ * Clears RTM metadata information. Used when user logs out of RTM
+ */
+ public void clearMetadata() {
+ metadataDao.deleteWhere(Metadata.KEY.eq(MilkTaskFields.METADATA_KEY));
+ }
+
+ /**
+ * Gets tasks that were modified since last sync. Used internally to
+ * support the other methods.
+ *
+ * @param properties
+ * @return cursor
+ */
+ private TodorooCursor getLocallyModified(Criterion criterion, Property>... properties) {
+ long lastSyncDate = MilkUtilities.INSTANCE.getLastSyncDate();
+ if(lastSyncDate == 0)
+ return taskDao.query(Query.select(Task.ID).where(criterion).orderBy(Order.asc(Task.ID)));
+ return
+ taskDao.query(Query.select(properties).where(Criterion.and(criterion,
+ Task.MODIFICATION_DATE.gt(lastSyncDate))).orderBy(Order.asc(Task.ID)));
+ }
+
+ /**
+ * Gets milk task metadata for joining
+ *
+ * @return cursor
+ */
+ private TodorooCursor getMilkTaskMetadata() {
+ return metadataDao.query(Query.select(Metadata.TASK).where(
+ MetadataCriteria.withKey(MilkTaskFields.METADATA_KEY)).orderBy(Order.asc(Metadata.TASK)));
+ }
+
+ /**
+ * Gets tasks that were created since last sync
+ * @param properties
+ * @return
+ */
+ public TodorooCursor getLocallyCreated(Property>[] properties) {
+ TodorooCursor tasks = getLocallyModified(TaskCriteria.isActive(), Task.ID);
+ TodorooCursor metadata = getMilkTaskMetadata();
+
+ ArrayList matchingRows = new ArrayList();
+
+ CursorJoiner joiner = new CursorJoiner(tasks, new String[] { Task.ID.name },
+ metadata, new String[] { Metadata.TASK.name });
+ for (CursorJoiner.Result joinerResult : joiner) {
+ // only pick up tasks without metadata
+ if(joinerResult == CursorJoiner.Result.LEFT) {
+ matchingRows.add(tasks.getLong(0));
+ }
+ }
+
+ return
+ taskDao.query(Query.select(properties).where(Task.ID.in(matchingRows.toArray(new Long[matchingRows.size()]))));
+ }
+
+ /**
+ * Gets tasks that were modified since last sync
+ * @param properties
+ * @return null if never sync'd
+ */
+ public TodorooCursor getLocallyUpdated(Property>[] properties) {
+ TodorooCursor tasks = getLocallyModified(TaskCriteria.isActive(), Task.ID);
+ TodorooCursor metadata = getMilkTaskMetadata();
+
+ ArrayList matchingRows = new ArrayList();
+
+ CursorJoiner joiner = new CursorJoiner(tasks, new String[] { Task.ID.name },
+ metadata, new String[] { Metadata.TASK.name });
+ for (CursorJoiner.Result joinerResult : joiner) {
+ // only pick up tasks with metadata
+ if(joinerResult == CursorJoiner.Result.BOTH) {
+ matchingRows.add(tasks.getLong(0));
+ }
+ }
+
+ return
+ taskDao.query(Query.select(properties).where(Task.ID.in(matchingRows.toArray(new Long[matchingRows.size()]))));
+ }
+
+ /**
+ * Searches for a local task with same remote id, updates this task's id
+ * @param remoteTask
+ */
+ public void findLocalMatch(MilkTaskContainer remoteTask) {
+ if(remoteTask.task.getId() != Task.NO_ID)
+ return;
+ TodorooCursor cursor = metadataDao.query(Query.select(Metadata.TASK).
+ where(Criterion.and(MetadataCriteria.withKey(MilkTaskFields.METADATA_KEY),
+ MilkTaskFields.TASK_SERIES_ID.eq(remoteTask.taskSeriesId),
+ MilkTaskFields.TASK_ID.eq(remoteTask.taskId))));
+ try {
+ if(cursor.getCount() == 0)
+ return;
+ cursor.moveToFirst();
+ remoteTask.task.setId(cursor.get(Metadata.TASK));
+ } finally {
+ cursor.close();
+ }
+ }
+
+ /**
+ * Saves a task and its metadata
+ * @param task
+ */
+ public void saveTaskAndMetadata(MilkTaskContainer task) {
+ taskDao.save(task.task);
+
+ task.metadata.add(MilkTaskFields.create(task));
+ metadataDao.synchronizeMetadata(task.task.getId(), task.metadata,
+ Criterion.or(MetadataCriteria.withKey(TAG_KEY),
+ MetadataCriteria.withKey(MilkTaskFields.METADATA_KEY),
+ MetadataCriteria.withKey(MilkNoteFields.METADATA_KEY)));
+ }
+
+ /**
+ * Reads a task and its metadata
+ * @param task
+ * @return
+ */
+ public MilkTaskContainer readTaskAndMetadata(TodorooCursor taskCursor) {
+ Task task = new Task(taskCursor);
+
+ // read tags, notes, etc
+ ArrayList metadata = new ArrayList();
+ TodorooCursor metadataCursor = metadataDao.query(Query.select(Metadata.PROPERTIES).
+ where(Criterion.and(MetadataCriteria.byTask(task.getId()),
+ Criterion.or(MetadataCriteria.withKey(TAG_KEY),
+ MetadataCriteria.withKey(MilkTaskFields.METADATA_KEY),
+ MetadataCriteria.withKey(MilkNoteFields.METADATA_KEY)))));
+ try {
+ for(metadataCursor.moveToFirst(); !metadataCursor.isAfterLast(); metadataCursor.moveToNext()) {
+ metadata.add(new Metadata(metadataCursor));
+ }
+ } finally {
+ metadataCursor.close();
+ }
+
+ return new MilkTaskContainer(task, metadata);
+ }
+
+ /**
+ * Reads metadata out of a task
+ * @return null if no metadata found
+ */
+ public Metadata getTaskMetadata(long taskId) {
+ TodorooCursor cursor = metadataDao.query(Query.select(
+ MilkTaskFields.LIST_ID, MilkTaskFields.TASK_SERIES_ID, MilkTaskFields.TASK_ID, MilkTaskFields.REPEATING).where(
+ MetadataCriteria.byTaskAndwithKey(taskId, MilkTaskFields.METADATA_KEY)));
+ try {
+ if(cursor.getCount() == 0)
+ return null;
+ cursor.moveToFirst();
+ return new Metadata(cursor);
+ } finally {
+ cursor.close();
+ }
+ }
+
+ /**
+ * Reads task notes out of a task
+ */
+ public TodorooCursor getTaskNotesCursor(long taskId) {
+ TodorooCursor cursor = metadataDao.query(Query.select(Metadata.PROPERTIES).
+ where(MetadataCriteria.byTaskAndwithKey(taskId, MilkNoteFields.METADATA_KEY)));
+ return cursor;
+ }
+
+ // --- list methods
+
+ private StoreObject[] lists = null;
+
+ /**
+ * Reads dashboards
+ */
+ private void readLists() {
+ if(lists != null)
+ return;
+
+ TodorooCursor cursor = storeObjectDao.query(Query.select(StoreObject.PROPERTIES).
+ where(StoreObjectCriteria.byType(MilkListFields.TYPE)).orderBy(Order.asc(MilkListFields.POSITION)));
+ try {
+ lists = new StoreObject[cursor.getCount()];
+ for(int i = 0; i < lists.length; i++) {
+ cursor.moveToNext();
+ StoreObject list = new StoreObject(cursor);
+ lists[i] = list;
+ }
+ } finally {
+ cursor.close();
+ }
+ }
+
+ /**
+ * @return a list of lists
+ */
+ public StoreObject[] getLists() {
+ readLists();
+ return lists;
+ }
+
+ /**
+ * Clears current cache of RTM lists and loads data from RTM into
+ * database. Returns the inbox list.
+ *
+ * @param remoteLists
+ * @return list with the name "inbox"
+ */
+ public StoreObject setLists(RtmLists remoteLists) {
+ readLists();
+
+ StoreObject inbox = null;
+ for(Map.Entry remote : remoteLists.getLists().entrySet()) {
+ if(remote.getValue().isSmart() || "All Tasks".equals(remote.getValue().getName())) //$NON-NLS-1$
+ continue;
+
+ long id = Long.parseLong(remote.getValue().getId());
+ StoreObject local = null;
+ for(StoreObject list : lists) {
+ if(list.getValue(MilkListFields.REMOTE_ID).equals(id)) {
+ local = list;
+ break;
+ }
+ }
+
+ if(local == null)
+ local = new StoreObject();
+
+ local.setValue(StoreObject.TYPE, MilkListFields.TYPE);
+ local.setValue(MilkListFields.REMOTE_ID, id);
+ local.setValue(MilkListFields.NAME, remote.getValue().getName());
+ local.setValue(MilkListFields.POSITION, remote.getValue().getPosition());
+ local.setValue(MilkListFields.ARCHIVED, remote.getValue().isArchived() ? 1 : 0);
+ storeObjectDao.save(local);
+
+ if(remote.getValue().isInbox()) {
+ inbox = local;
+ }
+ }
+
+ // clear dashboard cache
+ lists = null;
+ return inbox;
+ }
+
+ /**
+ * Get list name by list id
+ * @param listId
+ * @return null if no list by this id exists, otherwise list name
+ */
+ public String getListName(long listId) {
+ readLists();
+ for(StoreObject list : lists)
+ if(list.getValue(MilkListFields.REMOTE_ID).equals(listId))
+ return list.getValue(MilkListFields.NAME);
+ return null;
+ }
+
+}
diff --git a/astrid/rmilk-src/org/weloveastrid/rmilk/data/MilkListFields.java b/astrid/rmilk-src/org/weloveastrid/rmilk/data/MilkListFields.java
new file mode 100644
index 000000000..bd5926125
--- /dev/null
+++ b/astrid/rmilk-src/org/weloveastrid/rmilk/data/MilkListFields.java
@@ -0,0 +1,40 @@
+/**
+ * See the file "LICENSE" for the full license governing this code.
+ */
+package org.weloveastrid.rmilk.data;
+
+
+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.data.StoreObject;
+
+/**
+ * Data Model which represents a list in RTM
+ *
+ * @author Tim Su
+ *
+ */
+public class MilkListFields {
+
+ /** type*/
+ public static final String TYPE = "rmilk-list"; //$NON-NLS-1$
+
+ // --- properties
+
+ /** Remote ID */
+ public static final LongProperty REMOTE_ID = new LongProperty(
+ StoreObject.TABLE, StoreObject.ITEM.name);
+
+ /** Name */
+ public static final StringProperty NAME = StoreObject.VALUE1;
+
+ /** Position */
+ public static final IntegerProperty POSITION = new IntegerProperty(
+ StoreObject.TABLE, StoreObject.VALUE2.name);
+
+ /** Archived (0 or 1) */
+ public static final IntegerProperty ARCHIVED = new IntegerProperty(
+ StoreObject.TABLE, StoreObject.VALUE3.name);
+
+}
diff --git a/astrid/rmilk-src/org/weloveastrid/rmilk/data/MilkNoteFields.java b/astrid/rmilk-src/org/weloveastrid/rmilk/data/MilkNoteFields.java
new file mode 100644
index 000000000..fad9599e4
--- /dev/null
+++ b/astrid/rmilk-src/org/weloveastrid/rmilk/data/MilkNoteFields.java
@@ -0,0 +1,109 @@
+package org.weloveastrid.rmilk.data;
+
+import org.weloveastrid.rmilk.api.data.RtmTaskNote;
+
+import android.text.TextUtils;
+
+import com.todoroo.andlib.data.Property.LongProperty;
+import com.todoroo.andlib.data.Property.StringProperty;
+import com.todoroo.astrid.data.Metadata;
+
+/**
+ * Metadata entries for a Remember the Milk note. The first RMilk note becomes
+ * Astrid's note field, subsequent notes are stored in metadata in this
+ * format.
+ *
+ * @author Tim Su
+ *
+ */
+public class MilkNoteFields {
+
+ /** metadata key */
+ public static final String METADATA_KEY = "rmilk-note"; //$NON-NLS-1$
+
+ /** note id */
+ public static final StringProperty ID = Metadata.VALUE1;
+
+ /** note title */
+ public static final StringProperty TITLE = Metadata.VALUE2;
+
+ /** note text */
+ public static final StringProperty TEXT = Metadata.VALUE3;
+
+ /** note creation date */
+ public static final LongProperty CREATED = new LongProperty(Metadata.TABLE,
+ Metadata.VALUE4.name);
+
+ public static Metadata create(RtmTaskNote note) {
+ Metadata metadata = new Metadata();
+ metadata.setValue(Metadata.KEY, METADATA_KEY);
+ metadata.setValue(ID, note.getId());
+ metadata.setValue(TITLE, note.getTitle());
+ metadata.setValue(TEXT, note.getText());
+ metadata.setValue(CREATED, note.getCreated().getTime());
+ return metadata;
+ }
+
+ /**
+ * Turn a note's title and text into a string
+ * @param title
+ * @param text
+ * @return
+ */
+ @SuppressWarnings("nls")
+ public static String toNoteField(RtmTaskNote note) {
+ String title = note.getTitle();
+ String text = note.getText();
+ if(TextUtils.isEmpty(text) && TextUtils.isEmpty(title))
+ return "";
+ StringBuilder result = new StringBuilder();
+ if(!TextUtils.isEmpty(title)) {
+ result.append(title);
+ if(!TextUtils.isEmpty(text))
+ result.append("\n");
+ }
+ if(!TextUtils.isEmpty(text)) {
+ result.append(text);
+ }
+ return result.toString();
+ }
+
+ /**
+ * Turn a string into a note's title and text
+ * @param value
+ * @return
+ */
+ @SuppressWarnings("nls")
+ public static String[] fromNoteField(String value) {
+ String[] result = new String[2];
+ int firstLineBreak = value.indexOf('\n');
+ if(firstLineBreak > -1 && firstLineBreak + 1 < value.length()) {
+ result[0] = value.substring(0, firstLineBreak);
+ result[1] = value.substring(firstLineBreak + 1, value.length());
+ } else {
+ result[0] = "";
+ result[1] = value;
+ }
+ return result;
+ }
+
+ /**
+ * Turn a note's title and text into an HTML string for notes
+ * @param metadata
+ * @return
+ */
+ @SuppressWarnings("nls")
+ public static String toTaskDetail(Metadata metadata) {
+ String title = metadata.getValue(TITLE);
+ String text = metadata.getValue(TEXT);
+
+ String result;
+ if(!TextUtils.isEmpty(title))
+ result = "" + title + " " + text;
+ else
+ result = text;
+
+ return result;
+ }
+
+}
diff --git a/astrid/rmilk-src/org/weloveastrid/rmilk/data/MilkTaskFields.java b/astrid/rmilk-src/org/weloveastrid/rmilk/data/MilkTaskFields.java
new file mode 100644
index 000000000..5d943a966
--- /dev/null
+++ b/astrid/rmilk-src/org/weloveastrid/rmilk/data/MilkTaskFields.java
@@ -0,0 +1,51 @@
+package org.weloveastrid.rmilk.data;
+
+import org.weloveastrid.rmilk.sync.MilkTaskContainer;
+
+import com.todoroo.andlib.data.Property.IntegerProperty;
+import com.todoroo.andlib.data.Property.LongProperty;
+import com.todoroo.astrid.data.Metadata;
+
+/**
+ * Metadata entries for a Remember The Milk Task
+ * @author Tim Su
+ *
+ */
+public class MilkTaskFields {
+
+ /** metadata key */
+ public static final String METADATA_KEY = "rmilk"; //$NON-NLS-1$
+
+ /** {@link MilkListFields} id */
+ public static final LongProperty LIST_ID = new LongProperty(Metadata.TABLE,
+ Metadata.VALUE1.name);
+
+ /** RTM Task Series Id */
+ public static final LongProperty TASK_SERIES_ID = new LongProperty(Metadata.TABLE,
+ Metadata.VALUE2.name);
+
+ /** RTM Task Id */
+ public static final LongProperty TASK_ID = new LongProperty(Metadata.TABLE,
+ Metadata.VALUE3.name);
+
+ /** Whether task repeats in RTM (1 or 0) */
+ public static final IntegerProperty REPEATING = new IntegerProperty(Metadata.TABLE,
+ Metadata.VALUE4.name);
+
+ /**
+ * Creates a piece of metadata from a remote task
+ * @param rtmTaskSeries
+ * @return
+ */
+ public static Metadata create(MilkTaskContainer container) {
+ Metadata metadata = new Metadata();
+ metadata.setValue(Metadata.KEY, METADATA_KEY);
+ metadata.setValue(MilkTaskFields.LIST_ID, container.listId);
+ metadata.setValue(MilkTaskFields.TASK_SERIES_ID, container.taskSeriesId);
+ metadata.setValue(MilkTaskFields.TASK_ID, container.taskId);
+ metadata.setValue(MilkTaskFields.REPEATING, container.repeating ? 1 : 0);
+
+ return metadata;
+ }
+
+}
diff --git a/astrid/rmilk-src/org/weloveastrid/rmilk/sync/MilkSyncProvider.java b/astrid/rmilk-src/org/weloveastrid/rmilk/sync/MilkSyncProvider.java
new file mode 100644
index 000000000..cf84df443
--- /dev/null
+++ b/astrid/rmilk-src/org/weloveastrid/rmilk/sync/MilkSyncProvider.java
@@ -0,0 +1,610 @@
+/**
+ * See the file "LICENSE" for the full license governing this code.
+ */
+package org.weloveastrid.rmilk.sync;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashSet;
+import java.util.List;
+
+import org.weloveastrid.rmilk.MilkBackgroundService;
+import org.weloveastrid.rmilk.MilkLoginActivity;
+import org.weloveastrid.rmilk.MilkPreferences;
+import org.weloveastrid.rmilk.MilkUtilities;
+import org.weloveastrid.rmilk.MilkLoginActivity.SyncLoginCallback;
+import org.weloveastrid.rmilk.api.ApplicationInfo;
+import org.weloveastrid.rmilk.api.ServiceImpl;
+import org.weloveastrid.rmilk.api.ServiceInternalException;
+import org.weloveastrid.rmilk.api.data.RtmList;
+import org.weloveastrid.rmilk.api.data.RtmLists;
+import org.weloveastrid.rmilk.api.data.RtmTask;
+import org.weloveastrid.rmilk.api.data.RtmTaskList;
+import org.weloveastrid.rmilk.api.data.RtmTaskNote;
+import org.weloveastrid.rmilk.api.data.RtmTaskSeries;
+import org.weloveastrid.rmilk.api.data.RtmTasks;
+import org.weloveastrid.rmilk.api.data.RtmAuth.Perms;
+import org.weloveastrid.rmilk.api.data.RtmTask.Priority;
+import org.weloveastrid.rmilk.data.MilkDataService;
+import org.weloveastrid.rmilk.data.MilkNoteFields;
+
+import android.app.Activity;
+import android.app.Notification;
+import android.app.PendingIntent;
+import android.app.Service;
+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.timsu.astrid.R;
+import com.todoroo.andlib.data.Property;
+import com.todoroo.andlib.data.TodorooCursor;
+import com.todoroo.andlib.service.ContextManager;
+import com.todoroo.andlib.utility.AndroidUtilities;
+import com.todoroo.andlib.utility.DateUtilities;
+import com.todoroo.andlib.utility.DialogUtilities;
+import com.todoroo.astrid.api.AstridApiConstants;
+import com.todoroo.astrid.data.Metadata;
+import com.todoroo.astrid.data.Task;
+import com.todoroo.astrid.sync.SyncProvider;
+
+public class MilkSyncProvider extends SyncProvider {
+
+ private ServiceImpl rtmService = null;
+ private String timeline = null;
+ private MilkDataService dataService = null;
+
+ // ----------------------------------------------------------------------
+ // ------------------------------------------------------- public methods
+ // ----------------------------------------------------------------------
+
+ /**
+ * Sign out of RTM, deleting all synchronization metadata
+ */
+ public void signOut(Context context) {
+ MilkUtilities.INSTANCE.setToken(null);
+ MilkUtilities.INSTANCE.clearLastSyncDate();
+
+ dataService = MilkDataService.getInstance(context);
+ dataService.clearMetadata();
+ }
+
+ /**
+ * 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 showError
+ * whether to display a dialog
+ */
+ @Override
+ protected void handleException(String tag, Exception e, boolean showError) {
+ final Context context = ContextManager.getContext();
+ MilkUtilities.INSTANCE.setLastError(e.toString());
+
+ String message = null;
+
+ // occurs when application was closed
+ if(e instanceof IllegalStateException) {
+ Log.e(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();
+ message = context.getString(R.string.rmilk_ioerror);
+ Log.e(tag, "ioexception", enclosedException); //$NON-NLS-1$
+ } else {
+ if(e instanceof ServiceInternalException)
+ e = ((ServiceInternalException)e).getEnclosedException();
+ if(e != null)
+ message = e.toString();
+ Log.e(tag, "unhandled", e); //$NON-NLS-1$
+ }
+
+ if(showError && context instanceof Activity && message != null) {
+ DialogUtilities.okDialog((Activity)context, message, null);
+ }
+ }
+
+ // ----------------------------------------------------------------------
+ // ------------------------------------------------------ initiating sync
+ // ----------------------------------------------------------------------
+
+ /**
+ * set up service
+ */
+ @SuppressWarnings("nls")
+ private void initializeService(String authToken) throws ServiceInternalException {
+ String appName = null;
+ String z = stripslashes(0,"q9883o3384n21snq17501qn38oo1r689", "b");
+ String v = stripslashes(16,"19o2n020345219os","a");
+
+ if(authToken == null)
+ rtmService = new ServiceImpl(new ApplicationInfo(
+ z, v, appName));
+ else
+ rtmService = new ServiceImpl(new ApplicationInfo(
+ z, v, appName, authToken));
+ }
+
+ /**
+ * initiate sync in background
+ */
+ @Override
+ @SuppressWarnings("nls")
+ protected void initiateBackground(Service service) {
+ dataService = MilkDataService.getInstance(service);
+
+ try {
+ String authToken = MilkUtilities.INSTANCE.getToken();
+
+ // check if we have a token & it works
+ if(authToken != null) {
+ initializeService(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();
+ MilkUtilities.INSTANCE.setToken(token);
+ performSync();
+
+ return;
+ } catch (Exception e) {
+ // didn't work. do the process again.
+ }
+ }
+
+ // can't do anything, user not logged in
+
+ } else {
+ performSync();
+ }
+ } catch (IllegalStateException e) {
+ // occurs when application was closed
+ } catch (Exception e) {
+ handleException("rtm-authenticate", e, true);
+ } finally {
+ MilkUtilities.INSTANCE.stopOngoing();
+ }
+ }
+
+ /**
+ * If user isn't already signed in, show sign in dialog. Else perform sync.
+ */
+ @SuppressWarnings("nls")
+ @Override
+ protected void initiateManual(final Activity activity) {
+ final Resources r = activity.getResources();
+ String authToken = MilkUtilities.INSTANCE.getToken();
+ MilkUtilities.INSTANCE.stopOngoing();
+
+ // check if we have a token & it works
+ if(authToken == null) {
+ // open up a dialog and have the user go to browser
+ final String url;
+ try {
+ initializeService(null);
+ url = rtmService.beginAuthorization(Perms.delete);
+ } catch (Exception e) {
+ handleException("rmilk-auth", e, true);
+ return;
+ }
+
+ Intent intent = new Intent(activity, MilkLoginActivity.class);
+ MilkLoginActivity.setCallback(new SyncLoginCallback() {
+ public String verifyLogin(final Handler syncLoginHandler) {
+ if(rtmService == null) {
+ return null;
+ }
+ try {
+ String token = rtmService.completeAuthorization();
+ MilkUtilities.INSTANCE.setToken(token);
+ synchronize(activity);
+ return null;
+ } catch (Exception e) {
+ // didn't work
+ handleException("rtm-verify-login", e, false);
+ 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);
+ activity.startActivityForResult(intent, 0);
+ } else {
+ activity.startService(new Intent(MilkBackgroundService.SYNC_ACTION, null,
+ activity, MilkBackgroundService.class));
+ activity.finish();
+ }
+ }
+
+ // ----------------------------------------------------------------------
+ // ----------------------------------------------------- synchronization!
+ // ----------------------------------------------------------------------
+
+ protected void performSync() {
+ 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 = new Date(MilkUtilities.INSTANCE.getLastSyncDate());
+ boolean shouldSyncIndividualLists = false;
+ String filter = null;
+ if(lastSyncDate.getTime() == 0)
+ 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) {
+ handleException("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) {
+ handleException("rtm-indiv-sync", e, true); //$NON-NLS-1$
+ continue;
+ }
+ }
+ }
+
+ SyncData syncData = populateSyncData(remoteChanges);
+ try {
+ synchronizeTasks(syncData);
+ } finally {
+ syncData.localCreated.close();
+ syncData.localUpdated.close();
+ }
+
+ MilkUtilities.INSTANCE.recordSuccessfulSync();
+ Intent broadcastIntent = new Intent(AstridApiConstants.BROADCAST_EVENT_REFRESH);
+ ContextManager.getContext().sendBroadcast(broadcastIntent, AstridApiConstants.PERMISSION_READ);
+
+ } catch (IllegalStateException e) {
+ // occurs when application was closed
+ } catch (Exception e) {
+ handleException("rtm-sync", e, true); //$NON-NLS-1$
+ }
+ }
+
+ // ----------------------------------------------------------------------
+ // ------------------------------------------------------------ sync data
+ // ----------------------------------------------------------------------
+
+ // all synchronized properties
+ private static final Property>[] PROPERTIES = new Property>[] {
+ Task.ID,
+ Task.TITLE,
+ Task.IMPORTANCE,
+ Task.DUE_DATE,
+ Task.CREATION_DATE,
+ Task.COMPLETION_DATE,
+ Task.DELETION_DATE,
+ Task.NOTES,
+ };
+
+ /**
+ * Populate SyncData data structure
+ */
+ private SyncData populateSyncData(ArrayList remoteTasks) {
+ // fetch locally created tasks
+ TodorooCursor localCreated = dataService.getLocallyCreated(PROPERTIES);
+
+ // fetch locally updated tasks
+ TodorooCursor localUpdated = dataService.getLocallyUpdated(PROPERTIES);
+
+ return new SyncData(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()) {
+ MilkTaskContainer remote = parseRemoteTask(taskSeries);
+
+ // update reminder flags for incoming remote tasks to prevent annoying
+ if(remote.task.hasDueDate() && remote.task.getValue(Task.DUE_DATE) < DateUtilities.now())
+ remote.task.setFlag(Task.REMINDER_FLAGS, Task.NOTIFY_AFTER_DEADLINE, false);
+
+ dataService.findLocalMatch(remote);
+ list.add(remote);
+ }
+ }
+ }
+
+ // ----------------------------------------------------------------------
+ // ------------------------------------------------- create / push / pull
+ // ----------------------------------------------------------------------
+
+ @Override
+ protected MilkTaskContainer create(MilkTaskContainer task) throws IOException {
+ String listId = null;
+ if(task.listId > 0)
+ listId = Long.toString(task.listId);
+ RtmTaskSeries rtmTask = rtmService.tasks_add(timeline, listId,
+ task.task.getValue(Task.TITLE));
+ MilkTaskContainer newRemoteTask = parseRemoteTask(rtmTask);
+ transferIdentifiers(newRemoteTask, task);
+ push(task, newRemoteTask);
+ return newRemoteTask;
+ }
+
+ /**
+ * 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(MilkTaskContainer task, Property> property, MilkTaskContainer remoteTask) {
+ if(!task.task.containsValue(property))
+ return false;
+
+ if(remoteTask == null)
+ return true;
+ if(!remoteTask.task.containsValue(property))
+ return true;
+
+ // special cases - match if they're zero or nonzero
+ if(property == Task.COMPLETION_DATE ||
+ property == Task.DELETION_DATE)
+ return !AndroidUtilities.equals((Long)task.task.getValue(property) == 0,
+ (Long)remoteTask.task.getValue(property) == 0);
+
+ return !AndroidUtilities.equals(task.task.getValue(property),
+ remoteTask.task.getValue(property));
+ }
+
+ /**
+ * Send changes for the given Task across the wire. If a remoteTask is
+ * supplied, we attempt to intelligently only transmit the values that
+ * have changed.
+ */
+ @Override
+ protected void push(MilkTaskContainer local, MilkTaskContainer remote) throws IOException {
+ boolean remerge = false;
+
+ // fetch remote task for comparison
+ if(remote == null)
+ remote = pull(local);
+
+ String listId = Long.toString(local.listId);
+ String taskSeriesId = Long.toString(local.taskSeriesId);
+ String taskId = Long.toString(local.taskId);
+
+ if(remote != null && !AndroidUtilities.equals(local.listId, remote.listId))
+ rtmService.tasks_moveTo(timeline, Long.toString(remote.listId),
+ listId, taskSeriesId, taskId);
+
+ // either delete or re-create if necessary
+ if(shouldTransmit(local, Task.DELETION_DATE, remote)) {
+ if(local.task.getValue(Task.DELETION_DATE) > 0)
+ rtmService.tasks_delete(timeline, listId, taskSeriesId, taskId);
+ else if(remote == null) {
+ RtmTaskSeries rtmTask = rtmService.tasks_add(timeline, listId,
+ local.task.getValue(Task.TITLE));
+ remote = parseRemoteTask(rtmTask);
+ transferIdentifiers(remote, local);
+ }
+ }
+
+ if(shouldTransmit(local, Task.TITLE, remote))
+ rtmService.tasks_setName(timeline, listId, taskSeriesId,
+ taskId, local.task.getValue(Task.TITLE));
+ if(shouldTransmit(local, Task.IMPORTANCE, remote))
+ rtmService.tasks_setPriority(timeline, listId, taskSeriesId,
+ taskId, Priority.values(local.task.getValue(Task.IMPORTANCE)));
+ if(shouldTransmit(local, Task.DUE_DATE, remote))
+ rtmService.tasks_setDueDate(timeline, listId, taskSeriesId,
+ taskId, DateUtilities.unixtimeToDate(local.task.getValue(Task.DUE_DATE)),
+ local.task.hasDueTime());
+ if(shouldTransmit(local, Task.COMPLETION_DATE, remote)) {
+ if(local.task.getValue(Task.COMPLETION_DATE) == 0)
+ rtmService.tasks_uncomplete(timeline, listId, taskSeriesId,
+ taskId);
+ else {
+ rtmService.tasks_complete(timeline, listId, taskSeriesId,
+ taskId);
+ // if repeating, pull and merge
+ if(local.repeating)
+ remerge = true;
+ }
+ }
+
+ // tags
+ HashSet localTags = new HashSet();
+ HashSet remoteTags = new HashSet();
+ for(Metadata item : local.metadata)
+ if(MilkDataService.TAG_KEY.equals(item.getValue(Metadata.KEY)))
+ localTags.add(item.getValue(Metadata.VALUE1));
+ if(remote != null && remote.metadata != null) {
+ for(Metadata item : remote.metadata)
+ if(MilkDataService.TAG_KEY.equals(item.getValue(Metadata.KEY)))
+ remoteTags.add(item.getValue(Metadata.VALUE1));
+ }
+ if(!localTags.equals(remoteTags)) {
+ String[] tags = localTags.toArray(new String[localTags.size()]);
+ rtmService.tasks_setTags(timeline, listId, taskSeriesId,
+ taskId, tags);
+ }
+
+ // notes
+ if(shouldTransmit(local, Task.NOTES, remote)) {
+ String[] titleAndText = MilkNoteFields.fromNoteField(local.task.getValue(Task.NOTES));
+ List notes = null;
+ if(remote != null && remote.remote.getNotes() != null)
+ notes = remote.remote.getNotes().getNotes();
+ if(notes != null && notes.size() > 0) {
+ String remoteNoteId = notes.get(0).getId();
+ rtmService.tasks_notes_edit(timeline, remoteNoteId, titleAndText[0],
+ titleAndText[1]);
+ } else {
+ rtmService.tasks_notes_add(timeline, listId, taskSeriesId,
+ taskId, titleAndText[0], titleAndText[1]);
+ }
+ }
+
+ if(remerge) {
+ remote = pull(local);
+ remote.task.setId(local.task.getId());
+ write(remote);
+ }
+ }
+
+ /** Create a task container for the given RtmTaskSeries */
+ private MilkTaskContainer parseRemoteTask(RtmTaskSeries rtmTaskSeries) {
+ Task task = new Task();
+ RtmTask rtmTask = rtmTaskSeries.getTask();
+ ArrayList metadata = new ArrayList();
+
+ task.setValue(Task.TITLE, rtmTaskSeries.getName());
+ 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,
+ task.createDueDate(rtmTask.getHasDueTime() ? Task.URGENCY_SPECIFIC_DAY_TIME :
+ Task.URGENCY_SPECIFIC_DAY, DateUtilities.dateToUnixtime(rtmTask.getDue())));
+ } else {
+ task.setValue(Task.DUE_DATE, 0L);
+ }
+ task.setValue(Task.IMPORTANCE, rtmTask.getPriority().ordinal());
+
+ if(rtmTaskSeries.getTags() != null) {
+ for(String tag : rtmTaskSeries.getTags()) {
+ Metadata tagData = new Metadata();
+ tagData.setValue(Metadata.KEY, MilkDataService.TAG_KEY);
+ tagData.setValue(Metadata.VALUE1, tag);
+ metadata.add(tagData);
+ }
+ }
+
+ task.setValue(Task.NOTES, ""); //$NON-NLS-1$
+ if(rtmTaskSeries.getNotes() != null && rtmTaskSeries.getNotes().getNotes().size() > 0) {
+ boolean firstNote = true;
+ Collections.reverse(rtmTaskSeries.getNotes().getNotes()); // reverse so oldest is first
+ for(RtmTaskNote note : rtmTaskSeries.getNotes().getNotes()) {
+ if(firstNote) {
+ firstNote = false;
+ task.setValue(Task.NOTES, MilkNoteFields.toNoteField(note));
+ } else
+ metadata.add(MilkNoteFields.create(note));
+ }
+ }
+
+ MilkTaskContainer container = new MilkTaskContainer(task, metadata, rtmTaskSeries);
+
+ return container;
+ }
+
+ @Override
+ protected MilkTaskContainer pull(MilkTaskContainer task) throws IOException {
+ if(task.taskSeriesId == 0)
+ throw new ServiceInternalException("Tried to read an invalid task"); //$NON-NLS-1$
+ RtmTaskSeries rtmTask = rtmService.tasks_getTask(Long.toString(task.taskSeriesId),
+ task.task.getValue(Task.TITLE));
+ if(rtmTask != null)
+ return parseRemoteTask(rtmTask);
+ return null;
+ }
+
+ // ----------------------------------------------------------------------
+ // --------------------------------------------------------- read / write
+ // ----------------------------------------------------------------------
+
+ @Override
+ protected MilkTaskContainer read(TodorooCursor cursor) throws IOException {
+ return dataService.readTaskAndMetadata(cursor);
+ }
+
+ @Override
+ protected void write(MilkTaskContainer task) throws IOException {
+ dataService.saveTaskAndMetadata(task);
+ }
+
+ // ----------------------------------------------------------------------
+ // --------------------------------------------------------- misc helpers
+ // ----------------------------------------------------------------------
+
+ @Override
+ protected int matchTask(ArrayList tasks, MilkTaskContainer target) {
+ int length = tasks.size();
+ for(int i = 0; i < length; i++) {
+ MilkTaskContainer task = tasks.get(i);
+ if(AndroidUtilities.equals(task.listId, target.listId) &&
+ AndroidUtilities.equals(task.taskSeriesId, target.taskSeriesId) &&
+ AndroidUtilities.equals(task.taskId, target.taskId))
+ return i;
+ }
+ return -1;
+ }
+
+ @Override
+ protected int updateNotification(Context context, Notification notification) {
+ String notificationTitle = context.getString(R.string.rmilk_notification_title);
+ Intent intent = new Intent(context, MilkPreferences.class);
+ PendingIntent notificationIntent = PendingIntent.getActivity(context, 0,
+ intent, 0);
+ notification.setLatestEventInfo(context,
+ notificationTitle, context.getString(R.string.SyP_progress),
+ notificationIntent);
+ return 0;
+ }
+
+ @Override
+ protected void transferIdentifiers(MilkTaskContainer source,
+ MilkTaskContainer destination) {
+ destination.listId = source.listId;
+ destination.taskSeriesId = source.taskSeriesId;
+ destination.taskId = source.taskId;
+ }
+
+ // ----------------------------------------------------------------------
+ // ------------------------------------------------------- helper classes
+ // ----------------------------------------------------------------------
+
+ 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/rmilk-src/org/weloveastrid/rmilk/sync/MilkTaskContainer.java b/astrid/rmilk-src/org/weloveastrid/rmilk/sync/MilkTaskContainer.java
new file mode 100644
index 000000000..98d02a471
--- /dev/null
+++ b/astrid/rmilk-src/org/weloveastrid/rmilk/sync/MilkTaskContainer.java
@@ -0,0 +1,63 @@
+package org.weloveastrid.rmilk.sync;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+
+import org.weloveastrid.rmilk.api.data.RtmTaskSeries;
+import org.weloveastrid.rmilk.data.MilkTaskFields;
+
+import com.todoroo.astrid.data.Metadata;
+import com.todoroo.astrid.data.Task;
+import com.todoroo.astrid.sync.SyncContainer;
+
+/**
+ * RTM Task Container
+ *
+ * @author Tim Su
+ *
+ */
+public class MilkTaskContainer extends SyncContainer {
+ public long listId, taskSeriesId, taskId;
+ public boolean repeating;
+ public RtmTaskSeries remote;
+
+ public MilkTaskContainer(Task task, ArrayList metadata,
+ long listId, long taskSeriesId, long taskId, boolean repeating,
+ RtmTaskSeries remote) {
+ this.task = task;
+ this.metadata = metadata;
+ this.listId = listId;
+ this.taskSeriesId = taskSeriesId;
+ this.taskId = taskId;
+ this.repeating = repeating;
+ this.remote = remote;
+ }
+
+ public MilkTaskContainer(Task task, ArrayList metadata,
+ RtmTaskSeries rtmTaskSeries) {
+ this(task, metadata, Long.parseLong(rtmTaskSeries.getList().getId()),
+ Long.parseLong(rtmTaskSeries.getId()), Long.parseLong(rtmTaskSeries.getTask().getId()),
+ rtmTaskSeries.hasRecurrence(), rtmTaskSeries);
+ }
+
+ public MilkTaskContainer(Task task, ArrayList metadata) {
+ this(task, metadata, 0, 0, 0, false, null);
+ for(Iterator iterator = metadata.iterator(); iterator.hasNext(); ) {
+ Metadata item = iterator.next();
+ if(MilkTaskFields.METADATA_KEY.equals(item.getValue(Metadata.KEY))) {
+ if(item.containsNonNullValue(MilkTaskFields.LIST_ID))
+ listId = item.getValue(MilkTaskFields.LIST_ID);
+ if(item.containsNonNullValue(MilkTaskFields.TASK_SERIES_ID))
+ taskSeriesId = item.getValue(MilkTaskFields.TASK_SERIES_ID);
+ if(item.containsNonNullValue(MilkTaskFields.TASK_ID))
+ taskId = item.getValue(MilkTaskFields.TASK_ID);
+ if(item.containsNonNullValue(MilkTaskFields.REPEATING))
+ repeating = item.getValue(MilkTaskFields.REPEATING) == 1;
+ iterator.remove();
+ break;
+ }
+ }
+ }
+
+
+}
\ No newline at end of file