Remember the milk initial commit. stuff don't compile yet...

pull/14/head
Tim Su 14 years ago
parent a7536dcf4d
commit c38890a885

@ -4,7 +4,7 @@
<classpathentry kind="src" path="src-legacy"/>
<classpathentry kind="src" path="api-src"/>
<classpathentry kind="src" path="common-src"/>
<classpathentry kind="src" path="plugin-src"/>
<classpathentry excluding="com/todoroo/astrid/rmilk/DetailExposer.java|com/todoroo/astrid/rmilk/EditOperationExposer.java|com/todoroo/astrid/rmilk/FilterExposer.java|com/todoroo/astrid/rmilk/MilkEditActivity.java|com/todoroo/astrid/rmilk/StartupReceiver.java" kind="src" path="plugin-src"/>
<classpathentry kind="src" path="gen"/>
<classpathentry kind="con" path="com.android.ide.eclipse.adt.ANDROID_FRAMEWORK"/>
<classpathentry exported="true" kind="lib" path="lib/commons-codec-1.3.jar"/>

@ -0,0 +1,277 @@
/**
* See the file "LICENSE" for the full license governing this code.
*/
package com.todoroo.astrid.api;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import android.app.Notification;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
import com.timsu.astrid.R;
import com.todoroo.andlib.data.Property;
import com.todoroo.andlib.data.TodorooCursor;
import com.todoroo.andlib.data.Property.LongProperty;
import com.todoroo.andlib.service.Autowired;
import com.todoroo.andlib.service.ContextManager;
import com.todoroo.andlib.service.DependencyInjectionService;
import com.todoroo.andlib.service.ExceptionService;
import com.todoroo.andlib.service.NotificationManager;
import com.todoroo.astrid.model.Task;
import com.todoroo.astrid.rmilk.MilkPreferences;
import com.todoroo.astrid.service.TaskService;
/**
* A helper class for writing synchronization services for Astrid. This class
* contains logic for merging incoming changes and writing outgoing changes.
* <p>
* Use {@link synchronize} as the entry point for your synchronization service,
* which should handle authentication and then call {@link synchronizeTasks} to
* initiate synchronization.
*
* @author timsu
*
*/
public abstract class SynchronizationProvider {
/** Notification Manager id for RMilk notifications */
private static final int RMILK_NOTIFICATION_ID = -1;
// --- abstract methods - your services should implement these
/**
* Synchronize with the service
*/
abstract public void synchronize();
/**
* Push variables from given task to the remote server.
*
* @param task
* task proxy to push
* @param remoteTask
* remote task that we merged with. may be null
*/
abstract protected void push(Task task, Task remote) throws IOException;
/**
* Create a task on the remote server.
*
* @return task to create
*/
abstract protected void create(Task task) throws IOException;
/**
* Fetch remote task. Used to re-read merged tasks
*
* @param task
* task with id's to re-read
* @return new Task
*/
abstract protected Task read(Task task) throws IOException;
/**
* Finds a task in the list with the same remote identifier(s) as
* the task passed in
*
* @return task from list if matches, null otherwise
*/
abstract protected Task matchTask(ArrayList<Task> tasks, Task target);
/**
* Transfer remote identifier(s) from one task to another
*/
abstract protected void transferIdentifiers(Task source, Task destination);
// --- implementation
@Autowired
private TaskService taskService;
@Autowired
private ExceptionService exceptionService;
private final Notification notification;
private PendingIntent notificationIntent;
public SynchronizationProvider() {
DependencyInjectionService.getInstance().inject(this);
// initialize notification
int icon = android.R.drawable.stat_notify_sync;
long when = System.currentTimeMillis();
notification = new Notification(icon, null, when);
}
// --- utilities
/**
* Utility method for showing synchronization errors. If message is null,
* the contents of the throwable is displayed. It is assumed that the error
* was logged separately.
*/
protected void showError(final Context context, Throwable e, String message) {
exceptionService.displayAndReportError(context, message, e);
}
/**
* Utility method to update the UI if we're an active sync, or output to
* console if we're a background sync.
*/
protected void postUpdate(Context context, String string) {
notification.setLatestEventInfo(context,
context.getString(R.string.rmilk_notification_title), string, notificationIntent);
}
// --- synchronization logic
/**
* Helper to synchronize remote tasks with our local database.
*
* This initiates the following process: 1. local changes are read 2. remote
* changes are read 3. local tasks are merged with remote changes and pushed
* across 4. remote changes are then read in
*
* @param data synchronization data structure
*/
protected void synchronizeTasks(SyncData data) throws IOException {
int length;
Task task = new Task();
Context context = ContextManager.getContext();
Resources r = context.getResources();
// create notification
notificationIntent = PendingIntent.getActivity(context, 0, new Intent(context, MilkPreferences.class), 0);
postUpdate(context, r.getString(R.string.rmilk_progress_starting));
NotificationManager nm = new NotificationManager.AndroidNotificationManager(context);
nm.notify(RMILK_NOTIFICATION_ID, notification);
// create internal data structures
HashMap<String, Task> remoteNewTaskNameMap = new HashMap<String, Task>();
length = data.remoteUpdated.size();
for(int i = 0; i < length; i++) {
Task remote = data.remoteUpdated.get(i);
if(remote.getId() != Task.NO_ID)
continue;
remoteNewTaskNameMap.put(remote.getValue(Task.TITLE), remote);
}
// 1. CREATE: grab newly created tasks and create them remotely
length = data.localCreated.getCount();
for(int i = 0; i < length; i++) {
data.localCreated.moveToNext();
task.readFromCursor(data.localCreated);
String taskTitle = task.getValue(Task.TITLE);
postUpdate(context, r.getString(R.string.rmilk_progress_localtx,
taskTitle));
/* If there exists an incoming remote task with the same name and no
* mapping, we don't want to create this on the remote server,
* because user could have synchronized like this before. Instead,
* we create a mapping and do an update.
*/
if (remoteNewTaskNameMap.containsKey(taskTitle)) {
Task remote = remoteNewTaskNameMap.remove(taskTitle);
remote.setId(task.getId());
transferIdentifiers(remote, task);
push(task, remote);
read(remote);
} else {
create(task);
}
}
// 2. UPDATE: for each updated local task
length = data.localUpdated.getCount();
for(int i = 0; i < length; i++) {
data.localUpdated.moveToNext();
task.readFromCursor(data.localUpdated);
postUpdate(context, r.getString(R.string.rmilk_progress_localtx,
task.getValue(Task.TITLE)));
// if there is a conflict, merge
Task remote = matchTask(data.remoteUpdated, task);
if(remote != null) {
push(task, remote);
read(remote);
} else {
push(task, null);
}
}
// 3. REMOTE: load remote information
// Rearrange remoteTasks so completed tasks get synchronized first.
// This prevents bugs where a repeated task has two copies come down
// the wire, the new version and the completed old version. The new
// version would get merged, then completed, if done in the wrong order.
Collections.sort(data.remoteUpdated, new Comparator<Task>() {
private static final int SENTINEL = -2;
private final int check(Task o1, Task o2, LongProperty property) {
long o1Property = o1.getValue(property);
long o2Property = o2.getValue(property);
if(o1Property != 0 && o2Property != 0)
return 0;
else if(o1Property != 0)
return -1;
else if(o2Property != 0)
return 1;
return SENTINEL;
}
public int compare(Task o1, Task o2) {
int comparison = check(o1, o2, Task.DELETION_DATE);
if(comparison != SENTINEL)
return comparison;
comparison = check(o1, o2, Task.COMPLETION_DATE);
if(comparison != SENTINEL)
return comparison;
return 0;
}
});
length = data.remoteUpdated.size();
for(int i = 0; i < length; i++) {
task = data.remoteUpdated.get(i);
postUpdate(context, r.getString(R.string.rmilk_progress_remotetx,
task.getValue(Task.TITLE)));
// save the data (TODO: save metadata)
taskService.save(task, true);
}
}
// --- helper classes
/** data structure builder */
protected static class SyncData {
public final Property<?>[] properties;
public final ArrayList<Task> remoteUpdated;
public final TodorooCursor<Task> localCreated;
public final TodorooCursor<Task> localUpdated;
public SyncData(Property<?>[] properties,
ArrayList<Task> remoteUpdated,
TodorooCursor<Task> localCreated,
TodorooCursor<Task> localUpdated) {
super();
this.properties = properties;
this.remoteUpdated = remoteUpdated;
this.localCreated = localCreated;
this.localUpdated = localUpdated;
}
}
}

@ -0,0 +1,65 @@
/**
* See the file "LICENSE" for the full license governing this code.
*/
package com.todoroo.astrid.rmilk;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import com.todoroo.astrid.R;
import com.todoroo.astrid.api.AstridApiConstants;
import com.todoroo.astrid.api.TaskDetail;
import com.todoroo.astrid.model.Task;
import com.todoroo.astrid.rmilk.data.MilkDataService;
/**
* Exposes {@link TaskDetail}s for Remember the Milk:
* - RTM list
* - RTM repeat information
* - whether task has been changed
*
* @author Tim Su <tim@todoroo.com>
*
*/
public class DetailExposer extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
// if we aren't logged in, don't expose features
if(!Utilities.isLoggedIn())
return;
long taskId = intent.getLongExtra(AstridApiConstants.EXTRAS_TASK_ID, -1);
if(taskId == -1)
return;
MilkDataService service = new MilkDataService(context);
Task task = service.readTask(taskId);
if(task == null)
return;
TaskDetail[] details = new TaskDetail[2];
String listId = task.getValue(MilkDataService.LIST_ID);
if(listId != null && listId.length() > 0)
details[0] = new TaskDetail(context.getString(R.string.rmilk_TLA_list,
service.getList(listId)));
else
details[0] = null;
int repeat = task.getValue(MilkDataService.REPEAT);
if(repeat != 0)
details[1] = new TaskDetail(context.getString(R.string.rmilk_TLA_repeat));
else
details[1] = null;
// transmit
Intent broadcastIntent = new Intent(AstridApiConstants.BROADCAST_SEND_DETAILS);
broadcastIntent.putExtra(AstridApiConstants.EXTRAS_ITEMS, details);
broadcastIntent.putExtra(AstridApiConstants.EXTRAS_TASK_ID, taskId);
context.sendBroadcast(broadcastIntent, AstridApiConstants.PERMISSION_READ);
}
}

@ -0,0 +1,48 @@
/**
* See the file "LICENSE" for the full license governing this code.
*/
package com.todoroo.astrid.rmilk;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import com.todoroo.astrid.R;
import com.todoroo.astrid.api.AstridApiConstants;
import com.todoroo.astrid.api.EditOperation;
/**
* Exposes {@link EditOperation} for Remember the Milk
*
* @author Tim Su <tim@todoroo.com>
*
*/
public class EditOperationExposer extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
// if we aren't logged in, don't expose features
if(!Utilities.isLoggedIn())
return;
long taskId = intent
.getLongExtra(AstridApiConstants.EXTRAS_TASK_ID, -1);
if (taskId == -1)
return;
EditOperation taskEditOperation;
Intent editIntent = new Intent(context, MilkEditActivity.class);
taskEditOperation = new EditOperation(context.getString(
R.string.rmilk_EOE_button), editIntent);
// transmit
EditOperation[] operations = new EditOperation[1];
operations[0] = taskEditOperation;
Intent broadcastIntent = new Intent(
AstridApiConstants.BROADCAST_SEND_EDIT_OPERATIONS);
broadcastIntent.putExtra(AstridApiConstants.EXTRAS_ITEMS, operations);
context.sendBroadcast(broadcastIntent,
AstridApiConstants.PERMISSION_READ);
}
}

@ -0,0 +1,70 @@
/**
* See the file "LICENSE" for the full license governing this code.
*/
package com.todoroo.astrid.rmilk;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import com.todoroo.astrid.R;
import com.todoroo.astrid.api.AstridApiConstants;
import com.todoroo.astrid.api.Filter;
import com.todoroo.astrid.api.FilterCategory;
import com.todoroo.astrid.api.FilterListHeader;
import com.todoroo.astrid.api.FilterListItem;
import com.todoroo.astrid.rmilk.Utilities.ListContainer;
import com.todoroo.astrid.rmilk.data.MilkDataService;
/**
* Exposes filters based on RTM lists
*
* @author Tim Su <tim@todoroo.com>
*
*/
public class FilterExposer extends BroadcastReceiver {
@SuppressWarnings("nls")
private Filter filterFromList(Context context, ListContainer list) {
String listTitle = context.getString(R.string.rmilk_FEx_list_item).
replace("$N", list.name).replace("$C", Integer.toString(list.count));
String title = context.getString(R.string.rmilk_FEx_list_title, list.name);
Filter filter = new Filter(listTitle, title,
"TODO",
"TODO");
return filter;
}
@Override
public void onReceive(Context context, Intent intent) {
// if we aren't logged in, don't expose features
if(!Utilities.isLoggedIn())
return;
MilkDataService service = new MilkDataService(context);
ListContainer[] lists = service.getListsWithCounts();
// If user does not have any tags, don't show this section at all
if(lists.length == 0)
return;
Filter[] listFilters = new Filter[lists.length];
for(int i = 0; i < lists.length; i++)
listFilters[i] = filterFromList(context, lists[i]);
FilterListHeader rtmHeader = new FilterListHeader(context.getString(R.string.rmilk_FEx_header));
FilterCategory rtmLists = new FilterCategory(
context.getString(R.string.rmilk_FEx_list), listFilters);
// transmit filter list
FilterListItem[] list = new FilterListItem[2];
list[0] = rtmHeader;
list[1] = rtmLists;
Intent broadcastIntent = new Intent(AstridApiConstants.BROADCAST_SEND_FILTERS);
broadcastIntent.putExtra(AstridApiConstants.EXTRAS_ITEMS, list);
context.sendBroadcast(broadcastIntent, AstridApiConstants.PERMISSION_READ);
}
}

@ -0,0 +1,86 @@
/**
* See the file "LICENSE" for the full license governing this code.
*/
package com.todoroo.astrid.rmilk;
import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Spinner;
import com.todoroo.astrid.R;
import com.todoroo.astrid.api.AstridApiConstants;
import com.todoroo.astrid.model.Task;
import com.todoroo.astrid.rmilk.Utilities.ListContainer;
import com.todoroo.astrid.rmilk.data.MilkDataService;
/**
* Displays a dialog box for users to edit their RTM stuff
*
* @author Tim Su <tim@todoroo.com>
*
*/
public class MilkEditActivity extends Activity {
long taskId;
MilkDataService service;
Task model;
Spinner list;
EditText repeat;
/** Called when loading up the activity */
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
taskId = getIntent().getLongExtra(AstridApiConstants.EXTRAS_TASK_ID, -1);
if(taskId == -1)
return;
setContentView(R.layout.rmilk_edit_activity);
setTitle(R.string.rmilk_MEA_title);
((Button)findViewById(R.id.ok)).setOnClickListener(new OnClickListener() {
public void onClick(View v) {
saveAndQuit();
}
});
((Button)findViewById(R.id.cancel)).setOnClickListener(new OnClickListener() {
public void onClick(View v) {
setResult(RESULT_CANCELED);
finish();
}
});
// load all lists
service = new MilkDataService(this);
ListContainer[] lists = service.getLists();
list = (Spinner) findViewById(R.id.rmilk_list);
ArrayAdapter<ListContainer> listAdapter = new ArrayAdapter<ListContainer>(
this, android.R.layout.simple_spinner_item,
lists);
listAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
list.setAdapter(listAdapter);
// load model
model = service.readTask(taskId);
repeat.setText(model.getValue(MilkDataService.REPEAT));
list.setSelection(0); // TODO
}
/**
* Save tags to task and then quit
*/
protected void saveAndQuit() {
// model.setValue(DataService.LIST_ID, list.getSelectedItem()); TODO
setResult(RESULT_OK);
finish();
}
}

@ -0,0 +1,141 @@
/*
* ASTRID: Android's Simple Task Recording Dashboard
*
* Copyright (c) 2009 Tim Su
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package com.todoroo.astrid.rmilk;
import android.app.Activity;
import android.os.Bundle;
import android.os.Handler;
import android.view.View;
import android.view.View.OnClickListener;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.Button;
import com.timsu.astrid.R;
import com.todoroo.andlib.service.Autowired;
import com.todoroo.andlib.service.DependencyInjectionService;
import com.todoroo.andlib.utility.DialogUtilities;
/**
* This activity displays a <code>WebView</code> that allows users to log in to the
* synchronization provider requested. A callback method determines whether
* their login was successful and therefore whether to dismiss the dialog.
*
* @author timsu
*
*/
public class MilkLoginActivity extends Activity {
@Autowired
DialogUtilities dialogUtilities;
// --- bundle arguments
/**
* URL to display
*/
public static final String URL_TOKEN = "u"; //$NON-NLS-1$
// --- callback
/** Callback interface */
public interface SyncLoginCallback {
/**
* Verifies whether the user's login attempt was successful. Will be
* called off of the UI thread, use the handler to post messages.
*
* @return error string, or null if sync was successful
*/
public String verifyLogin(Handler handler);
}
protected static SyncLoginCallback callback = null;
/** Sets callback method */
public static void setCallback(SyncLoginCallback newCallback) {
callback = newCallback;
}
// --- ui initialization
public MilkLoginActivity() {
super();
DependencyInjectionService.getInstance().inject(this);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.rmilk_login_activity);
String urlParam = getIntent().getStringExtra(URL_TOKEN);
final WebView webView = (WebView)findViewById(R.id.browser);
Button done = (Button)findViewById(R.id.done);
Button cancel = (Button)findViewById(R.id.cancel);
webView.setWebViewClient(new WebViewClient() {
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
view.loadUrl(url);
return true;
}
});
webView.getSettings().setJavaScriptEnabled(true);
webView.getSettings().setSavePassword(false);
webView.getSettings().setSupportZoom(true);
webView.loadUrl(urlParam);
done.setOnClickListener(new OnClickListener() {
public void onClick(View v) {
final Handler handler = new Handler();
if(callback == null) {
finish();
return;
}
new Thread(new Runnable() {
public void run() {
final String result = callback.verifyLogin(handler);
if(result == null) {
finish();
} else {
// display the error
handler.post(new Runnable() {
public void run() {
dialogUtilities.okDialog(MilkLoginActivity.this,
result, null);
}
});
}
}
}).start();
}
});
cancel.setOnClickListener(new OnClickListener() {
public void onClick(View v) {
setResult(RESULT_CANCELED);
finish();
}
});
}
}

@ -0,0 +1,127 @@
package com.todoroo.astrid.rmilk;
import android.content.res.Resources;
import android.graphics.Color;
import android.os.Bundle;
import android.preference.CheckBoxPreference;
import android.preference.EditTextPreference;
import android.preference.ListPreference;
import android.preference.Preference;
import android.preference.PreferenceActivity;
import android.preference.PreferenceGroup;
import android.preference.PreferenceScreen;
import android.preference.Preference.OnPreferenceChangeListener;
import android.preference.Preference.OnPreferenceClickListener;
import android.view.View;
import android.view.ViewGroup.OnHierarchyChangeListener;
import android.widget.ListView;
import com.timsu.astrid.R;
import com.todoroo.andlib.utility.AndroidUtilities;
import com.todoroo.astrid.rmilk.sync.RTMSyncProvider;
/**
* Displays synchronization preferences and an action panel so users can
* initiate actions from the menu.
*
* @author timsu
*
*/
public class MilkPreferences extends PreferenceActivity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
addPreferencesFromResource(R.xml.preferences_rmilk);
PreferenceScreen screen = getPreferenceScreen();
initializePreference(screen);
// status
final String status = "Please Log In To RTM!"; //$NON-NLS-1$
final int statusColor = Color.RED;
Resources r = getResources();
Preference preference = screen.findPreference(r.getString(R.string.rmilk_MPr_status_key));
preference.setTitle(status);
getListView().setOnHierarchyChangeListener(new OnHierarchyChangeListener() {
public void onChildViewRemoved(View arg0, View arg1) {
//
}
public void onChildViewAdded(View parent, View child) {
if(((ListView)parent).getChildCount() == 2) {
child.setBackgroundColor(statusColor);
}
}
});
// action buttons
Preference syncAction = screen.getPreferenceManager().findPreference(
getString(R.string.rmilk_MPr_sync_key));
syncAction.setOnPreferenceClickListener(new OnPreferenceClickListener() {
public boolean onPreferenceClick(Preference p) {
new RTMSyncProvider().synchronize(MilkPreferences.this);
return true;
}
});
/*Preference clearDataPreference = screen.getPreferenceManager().findPreference(
getString(R.string.rmilk_MPr_forget_key));*/
}
private void initializePreference(Preference preference) {
if(preference instanceof PreferenceGroup) {
PreferenceGroup group = (PreferenceGroup)preference;
for(int i = 0; i < group.getPreferenceCount(); i++) {
initializePreference(group.getPreference(i));
}
} else {
Object value = null;
if(preference instanceof ListPreference)
value = ((ListPreference)preference).getValue();
else if(preference instanceof CheckBoxPreference)
value = ((CheckBoxPreference)preference).isChecked();
else if(preference instanceof EditTextPreference)
value = ((EditTextPreference)preference).getText();
else
return;
updatePreferences(preference, value);
preference.setOnPreferenceChangeListener(new OnPreferenceChangeListener() {
public boolean onPreferenceChange(Preference myPreference, Object newValue) {
return updatePreferences(myPreference, newValue);
}
});
}
}
/**
*
* @param resource if null, updates all resources
*/
protected boolean updatePreferences(Preference preference, Object value) {
Resources r = getResources();
// interval
if(r.getString(R.string.rmilk_MPr_interval_key).equals(preference.getKey())) {
int index = AndroidUtilities.indexOf(r.getStringArray(R.array.rmilk_MPr_interval_values), (String)value);
if(index <= 0)
preference.setSummary(R.string.rmilk_MPr_interval_desc_disabled);
else
preference.setSummary(r.getString(R.string.rmilk_MPr_interval_desc,
r.getStringArray(R.array.rmilk_MPr_interval_entries)[index]));
}
// shortcut
else if(r.getString(R.string.rmilk_MPr_shortcut_key).equals(preference.getKey())) {
if((Boolean)value) {
preference.setSummary(R.string.rmilk_MPr_shortcut_desc_enabled);
} else {
preference.setSummary(R.string.rmilk_MPr_shortcut_desc_disabled);
}
}
return true;
}
}

@ -0,0 +1,40 @@
/**
* See the file "LICENSE" for the full license governing this code.
*/
package com.todoroo.astrid.rmilk;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.content.res.Resources;
import android.preference.PreferenceManager;
import com.todoroo.astrid.R;
public class StartupReceiver extends BroadcastReceiver {
public static void setPreferenceDefaults(Context context) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
Editor editor = prefs.edit();
Resources r = context.getResources();
if(!prefs.contains(r.getString(R.string.rmilk_MPr_interval_key))) {
editor.putString(r.getString(R.string.rmilk_MPr_interval_key),
Integer.toString(0));
}
if(!prefs.contains(r.getString(R.string.rmilk_MPr_shortcut_key))) {
editor.putBoolean(r.getString(R.string.rmilk_MPr_shortcut_key), true);
}
editor.commit();
}
@Override
/** Called when this plug-in run for the first time (installed, upgrade, or device was rebooted */
public void onReceive(final Context context, Intent intent) {
setPreferenceDefaults(context);
}
}

@ -0,0 +1,135 @@
/**
* See the file "LICENSE" for the full license governing this code.
*/
package com.todoroo.astrid.rmilk;
import java.util.Date;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.preference.PreferenceManager;
import com.timsu.astrid.R;
import com.todoroo.andlib.service.ContextManager;
import com.todoroo.astrid.rmilk.data.MilkList;
/**
* Utility constants
*
* @author Tim Su <tim@todoroo.com>
*
*/
@SuppressWarnings("nls")
public class Utilities {
// --- helper classes
/**
* Helper class for describing RTM lists
*
* @author Tim Su <tim@todoroo.com>
*/
public static class ListContainer {
public ListContainer(MilkList list) {
this(list.getValue(MilkList.ID), list.getValue(MilkList.NAME));
}
public ListContainer(long id, String name) {
this.id = id;
this.name = name;
this.count = -1;
}
@Override
public String toString() {
return name;
}
public long id;
public String name;
public int count;
}
// --- Metadata keys
// NOTE: no sql escaping is provided for keys
public static final String KEY_LIST_ID = "rmilk_listId";
public static final String KEY_TASK_SERIES_ID = "rmilk_taskSeriesId";
public static final String KEY_TASK_ID = "rmilk_taskId";
public static final String KEY_REPEAT = "rmilk_repeat";
public static final String KEY_UPDATED = "rmilk_updated";
// --- Preference Keys
private static final String PREF_TOKEN = "rmilk_token";
private static final String PREF_LAST_SYNC = "rmilk_last_sync";
// --- Preference Utility Methods
/** Get preferences object from the context */
private static SharedPreferences getPrefs() {
return PreferenceManager.getDefaultSharedPreferences(ContextManager.getContext());
}
/**
* @return true if we have a token for this user, false otherwise
*/
public static boolean isLoggedIn() {
return getPrefs().getString(PREF_TOKEN, null) != null;
}
/** RTM authentication token, or null if doesn't exist */
public static String getToken() {
return getPrefs().getString(PREF_TOKEN, null);
}
/** Sets the RTM authentication token. Set to null to clear. */
public static void setToken(String setting) {
Editor editor = getPrefs().edit();
editor.putString(PREF_TOKEN, setting);
editor.commit();
}
/** RTM Last Successful Sync Date, or null */
public static Date getLastSyncDate() {
Long value = getPrefs().getLong(PREF_LAST_SYNC, 0);
if (value == 0)
return null;
return new Date(value);
}
/** Set RTM Last Successful Sync Date */
public static void setLastSyncDate(Date date) {
Editor editor = getPrefs().edit();
if (date == null) {
editor.remove(PREF_LAST_SYNC);
} else {
editor.putLong(PREF_LAST_SYNC, date.getTime());
}
editor.commit();
}
/**
* Reads the frequency, in seconds, auto-sync should occur.
*
* @return seconds duration, or 0 if not desired
*/
public static int getSyncAutoSyncFrequency() {
String value = getPrefs().getString(
ContextManager.getContext().getString(
R.string.rmilk_MPr_interval_key), null);
if (value == null)
return 0;
try {
return Integer.parseInt(value);
} catch (Exception e) {
return 0;
}
}
}

@ -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 <http://www.gnu.org/licenses/>.
*/
package com.todoroo.astrid.rmilk.api;
/**
* Encapsulates information about an application that is a client of RememberTheMilk. Includes information required by RTM to connect: the API key and
* the shared secret.
*
* @author Will Ross Jun 22, 2007
*/
public class ApplicationInfo
{
private final String apiKey;
private final String sharedSecret;
private final String name;
private final String authToken;
public ApplicationInfo(String apiKey, String sharedSecret, String name)
{
this(apiKey, sharedSecret, name, null);
}
public ApplicationInfo(String apiKey, String sharedSecret, String name,
String authToken)
{
super();
this.apiKey = apiKey;
this.sharedSecret = sharedSecret;
this.name = name;
this.authToken = authToken;
}
public String getApiKey()
{
return apiKey;
}
public String getSharedSecret()
{
return sharedSecret;
}
public String getName()
{
return name;
}
public String getAuthToken()
{
return authToken;
}
}

@ -0,0 +1,287 @@
/*
* Copyright 2007, MetaDimensional Technologies Inc.
*
*
* This file is part of the RememberTheMilk Java API.
*
* The RememberTheMilk Java API is free software; you can redistribute it
* and/or modify it under the terms of the GNU Lesser General Public License
* as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* The RememberTheMilk Java API is distributed in the hope that it will be
* useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser
* General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.todoroo.astrid.rmilk.api;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.DefaultHttpClient;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.xml.sax.SAXException;
import android.util.Log;
/**
* Handles the details of invoking a method on the RTM REST API.
*
* @author Will Ross Jun 21, 2007
*/
@SuppressWarnings("nls")
public class Invoker {
private static final String TAG = "rtm-invoker"; //$NON-NLS-1$
private static final DocumentBuilder builder;
static
{
// Done this way because the builder is marked "final"
DocumentBuilder aBuilder;
try
{
final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setNamespaceAware(false);
factory.setValidating(false);
aBuilder = factory.newDocumentBuilder();
}
catch (Exception exception)
{
Log.e(TAG, "Unable to construct a document builder", exception);
aBuilder = null;
}
builder = aBuilder;
}
private static final String ENCODING = "UTF-8"; //$NON-NLS-1$
private static final String API_SIG_PARAM = "api_sig"; //$NON-NLS-1$
private static final long INVOCATION_INTERVAL = 400;
private long lastInvocation;
private final ApplicationInfo applicationInfo;
private final MessageDigest digest;
private String serviceRelativeUri;
private HttpClient httpClient;
public Invoker(@SuppressWarnings("unused") String serverHostName,
@SuppressWarnings("unused") int serverPortNumber,
String serviceRelativeUri, ApplicationInfo applicationInfo)
throws ServiceInternalException {
this.serviceRelativeUri = serviceRelativeUri;
httpClient = new DefaultHttpClient();
lastInvocation = System.currentTimeMillis();
this.applicationInfo = applicationInfo;
try {
digest = MessageDigest.getInstance("md5"); //$NON-NLS-1$
} catch (NoSuchAlgorithmException e) {
throw new ServiceInternalException(
"Could not create properly the MD5 digest", e);
}
}
private StringBuffer computeRequestUri(Param... params)
throws ServiceInternalException {
final StringBuffer requestUri = new StringBuffer(serviceRelativeUri);
if (params.length > 0) {
requestUri.append("?");
}
for (Param param : params) {
try {
requestUri.append(param.getName()).append("=").append(
URLEncoder.encode(param.getValue(), ENCODING)).append(
"&");
} catch (Exception exception) {
final StringBuffer message = new StringBuffer(
"Cannot encode properly the HTTP GET request URI: cannot execute query");
Log.e(TAG, message.toString(), exception);
throw new ServiceInternalException(message.toString());
}
}
requestUri.append(API_SIG_PARAM).append("=").append(calcApiSig(params));
return requestUri;
}
/** Call invoke with a false repeat */
public Element invoke(Param... params) throws ServiceException {
return invoke(false, params);
}
public Element invoke(boolean repeat, Param... params)
throws ServiceException {
long timeSinceLastInvocation = System.currentTimeMillis() -
lastInvocation;
if (timeSinceLastInvocation < INVOCATION_INTERVAL) {
// In order not to invoke the RTM service too often
try {
Thread.sleep(INVOCATION_INTERVAL - timeSinceLastInvocation);
} catch (InterruptedException e) {
return null;
}
}
// We compute the URI
final StringBuffer requestUri = computeRequestUri(params);
HttpResponse response = null;
final HttpGet request = new HttpGet("http://" //$NON-NLS-1$
+ ServiceImpl.SERVER_HOST_NAME + requestUri.toString());
final String methodUri = request.getRequestLine().getUri();
Element result;
try {
Log.i(TAG, "Executing the method:" + methodUri); //$NON-NLS-1$
response = httpClient.execute(request);
lastInvocation = System.currentTimeMillis();
final int statusCode = response.getStatusLine().getStatusCode();
if (statusCode != HttpStatus.SC_OK) {
Log.e(TAG, "Method failed: " + response.getStatusLine()); //$NON-NLS-1$
// Tim: HTTP error. Let's wait a little bit
if (!repeat) {
try {
Thread.sleep(1500);
} catch (InterruptedException e) {
// ignore
}
return invoke(true, params);
}
throw new ServiceInternalException("method failed: "
+ response.getStatusLine());
}
final Document responseDoc = builder.parse(response.getEntity()
.getContent());
final Element wrapperElt = responseDoc.getDocumentElement();
if (!wrapperElt.getNodeName().equals("rsp")) {
throw new ServiceInternalException(
"unexpected response returned by RTM service: "
+ wrapperElt.getNodeName());
} else {
String stat = wrapperElt.getAttribute("stat");
if (stat.equals("fail")) {
Node errElt = wrapperElt.getFirstChild();
while (errElt != null
&& (errElt.getNodeType() != Node.ELEMENT_NODE || !errElt
.getNodeName().equals("err"))) {
errElt = errElt.getNextSibling();
}
if (errElt == null) {
throw new ServiceInternalException(
"unexpected response returned by RTM service: "
+ wrapperElt.getNodeValue());
} else {
throw new ServiceException(Integer
.parseInt(((Element) errElt)
.getAttribute("code")),
((Element) errElt).getAttribute("msg"));
}
} else {
Node dataElt = wrapperElt.getFirstChild();
while (dataElt != null
&& (dataElt.getNodeType() != Node.ELEMENT_NODE || dataElt
.getNodeName().equals("transaction") == true)) {
try {
Node nextSibling = dataElt.getNextSibling();
if (nextSibling == null) {
break;
} else {
dataElt = nextSibling;
}
} catch (IndexOutOfBoundsException exception) {
// Some implementation may throw this exception,
// instead of returning a null sibling
break;
}
}
if (dataElt == null) {
throw new ServiceInternalException(
"unexpected response returned by RTM service: "
+ wrapperElt.getNodeValue());
} else {
result = (Element) dataElt;
}
}
}
} catch (IOException e) {
throw new ServiceInternalException("Error making connection: " +
e.getMessage(), e);
} catch (SAXException e) {
// repeat call if possible.
if(!repeat)
return invoke(true, params);
else
throw new ServiceInternalException("Error parsing response. " +
"Please try sync again!", e);
} finally {
httpClient.getConnectionManager().closeExpiredConnections();
}
return result;
}
final String calcApiSig(Param... params) throws ServiceInternalException {
try {
digest.reset();
digest.update(applicationInfo.getSharedSecret().getBytes(ENCODING));
List<Param> 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();
}
}

@ -0,0 +1,64 @@
/*
* Copyright 2007, MetaDimensional Technologies Inc.
*
*
* This file is part of the RememberTheMilk Java API.
*
* The RememberTheMilk Java API is free software; you can redistribute it
* and/or modify it under the terms of the GNU Lesser General Public License
* as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* The RememberTheMilk Java API is distributed in the hope that it will be
* useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser
* General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.todoroo.astrid.rmilk.api;
import java.util.Date;
import com.todoroo.astrid.rmilk.api.data.RtmData;
/**
*
* @author Will Ross Jun 21, 2007
*/
public class Param
implements Comparable<Param>
{
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());
}
}

@ -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 <http://www.gnu.org/licenses/>.
*/
package com.todoroo.astrid.rmilk.api;
import java.util.prefs.Preferences;
/**
*
* @author Will Ross Jun 21, 2007
*/
public class Prefs {
Preferences preferences;
public enum PrefKey {
AuthToken
}
public Prefs() {
preferences = Preferences.userNodeForPackage(Prefs.class);
}
public String getAuthToken() {
return preferences.get(PrefKey.AuthToken.toString(), null);
}
public void setAuthToken(String authToken) {
preferences.put(PrefKey.AuthToken.toString(), authToken);
}
}

@ -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 <http://www.gnu.org/licenses/>.
*/
package com.todoroo.astrid.rmilk.api;
import java.io.IOException;
/**
*
* @author Will Ross Jun 21, 2007
*/
@SuppressWarnings("nls")
public class ServiceException extends IOException {
private static final long serialVersionUID = -6711156026040643361L;
int responseCode;
String responseMessage;
public ServiceException(int responseCode, String responseMessage) {
super("Service invocation failed. Code: " + responseCode + "; message: " + responseMessage);
this.responseCode = responseCode;
this.responseMessage = responseMessage;
}
int getResponseCode() {
return responseCode;
}
String getResponseMessage() {
return responseMessage;
}
}

@ -0,0 +1,615 @@
/*
* Copyright 2007, MetaDimensional Technologies Inc.
*
*
* This file is part of the RememberTheMilk Java API.
*
* The RememberTheMilk Java API is free software; you can redistribute it
* and/or modify it under the terms of the GNU Lesser General Public License
* as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* The RememberTheMilk Java API is distributed in the hope that it will be
* useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser
* General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.todoroo.astrid.rmilk.api;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.Map.Entry;
import org.w3c.dom.Element;
import com.todoroo.astrid.rmilk.api.data.RtmAuth;
import com.todoroo.astrid.rmilk.api.data.RtmData;
import com.todoroo.astrid.rmilk.api.data.RtmFrob;
import com.todoroo.astrid.rmilk.api.data.RtmList;
import com.todoroo.astrid.rmilk.api.data.RtmLists;
import com.todoroo.astrid.rmilk.api.data.RtmLocation;
import com.todoroo.astrid.rmilk.api.data.RtmTask;
import com.todoroo.astrid.rmilk.api.data.RtmTaskList;
import com.todoroo.astrid.rmilk.api.data.RtmTaskNote;
import com.todoroo.astrid.rmilk.api.data.RtmTaskSeries;
import com.todoroo.astrid.rmilk.api.data.RtmTasks;
import com.todoroo.astrid.rmilk.api.data.RtmTimeline;
import com.todoroo.astrid.rmilk.api.data.RtmTask.Priority;
/**
* A major part of the RTM API implementation is here.
*
* @author Will Ross Jun 21, 2007
* @author Edouard Mercier, since 2008.04.15
* @author timsu January 2009
*/
@SuppressWarnings("nls")
public class ServiceImpl
{
public final static String SERVER_HOST_NAME = "api.rememberthemilk.com"; //$NON-NLS-1$
public final static int SERVER_PORT_NUMBER = 80;
public final static String REST_SERVICE_URL_POSTFIX = "/services/rest/"; //$NON-NLS-1$
private final ApplicationInfo applicationInfo;
private final Invoker invoker;
private final Prefs prefs;
private String currentAuthToken;
RtmFrob tempFrob;
public ServiceImpl(ApplicationInfo applicationInfo)
throws ServiceInternalException
{
invoker = new Invoker(SERVER_HOST_NAME, SERVER_PORT_NUMBER, REST_SERVICE_URL_POSTFIX, applicationInfo);
this.applicationInfo = applicationInfo;
prefs = new Prefs();
if (applicationInfo.getAuthToken() != null)
{
currentAuthToken = applicationInfo.getAuthToken();
}
else
{
currentAuthToken = prefs.getAuthToken();
}
}
public boolean isServiceAuthorized()
throws ServiceException
{
if (currentAuthToken == null)
return false;
try
{
/* RtmAuth auth = */auth_checkToken(currentAuthToken);
return true;
}
catch (ServiceException e)
{
if (e.getResponseCode() != 98)
{
throw e;
}
else
{
// Bad token.
currentAuthToken = null;
return false;
}
}
}
public String beginAuthorization(RtmAuth.Perms permissions)
throws ServiceException
{
// Instructions from the "User authentication for desktop applications"
// section at http://www.rememberthemilk.com/services/api/authentication.rtm
tempFrob = auth_getFrob();
return beginAuthorization(tempFrob, permissions);
}
public String beginAuthorization(RtmFrob frob, RtmAuth.Perms permissions)
throws ServiceException
{
String authBaseUrl = "http://" + SERVER_HOST_NAME + "/services/auth/";
Param[] params = new Param[] { new Param("api_key", applicationInfo.getApiKey()), new Param("perms", permissions.toString()),
new Param("frob", frob.getValue()) };
Param sig = new Param("api_sig", invoker.calcApiSig(params));
StringBuilder authUrl = new StringBuilder(authBaseUrl);
authUrl.append("?");
for (Param param : params)
{
authUrl.append(param.getName()).append("=").append(param.getValue()).append("&");
}
authUrl.append(sig.getName()).append("=").append(sig.getValue());
return authUrl.toString();
}
public String completeAuthorization()
throws ServiceException
{
return completeAuthorization(tempFrob);
}
public String completeAuthorization(RtmFrob frob)
throws ServiceException
{
currentAuthToken = auth_getToken(frob.getValue());
prefs.setAuthToken(currentAuthToken);
return currentAuthToken;
}
public RtmAuth auth_checkToken(String authToken)
throws ServiceException
{
Element response = invoker.invoke(new Param("method", "rtm.auth.checkToken"), new Param("auth_token", authToken),
new Param("api_key", applicationInfo.getApiKey()));
return new RtmAuth(response);
}
public RtmFrob auth_getFrob()
throws ServiceException
{
return new RtmFrob(invoker.invoke(new Param("method", "rtm.auth.getFrob"), new Param("api_key", applicationInfo.getApiKey())));
}
public String auth_getToken(String frob)
throws ServiceException
{
Element response = invoker.invoke(new Param("method", "rtm.auth.getToken"), new Param("frob", frob), new Param("api_key", applicationInfo.getApiKey()));
return new RtmAuth(response).getToken();
}
public void contacts_add()
{
throw new UnsupportedOperationException("Not supported yet.");
}
public void contacts_delete()
{
throw new UnsupportedOperationException("Not supported yet.");
}
public void contacts_getList()
{
throw new UnsupportedOperationException("Not supported yet.");
}
public void groups_add()
{
throw new UnsupportedOperationException("Not supported yet.");
}
public void groups_addContact()
{
throw new UnsupportedOperationException("Not supported yet.");
}
public void groups_delete()
{
throw new UnsupportedOperationException("Not supported yet.");
}
public void groups_getList()
{
throw new UnsupportedOperationException("Not supported yet.");
}
public void groups_removeContact()
{
throw new UnsupportedOperationException("Not supported yet.");
}
public RtmList lists_add(String timelineId, String listName)
throws ServiceException
{
Element response = invoker.invoke(new Param("method", "rtm.lists.add"), new Param("auth_token", currentAuthToken),
new Param("api_key", applicationInfo.getApiKey()), new Param("name", listName), new Param("timeline", timelineId));
return new RtmList(response);
}
public void lists_archive()
{
throw new UnsupportedOperationException("Not supported yet.");
}
public void lists_delete(String timelineId, String listId)
throws ServiceException
{
invoker.invoke(new Param("method", "rtm.lists.delete"), new Param("auth_token", currentAuthToken), new Param("api_key", applicationInfo.getApiKey()),
new Param("timeline", timelineId), new Param("list_id", listId));
}
public RtmLists lists_getList()
throws ServiceException
{
Element response = invoker.invoke(new Param("method", "rtm.lists.getList"), new Param("auth_token", currentAuthToken),
new Param("api_key", applicationInfo.getApiKey()));
return new RtmLists(response);
}
public RtmList lists_getList(String listName)
throws ServiceException
{
RtmLists fullList = lists_getList();
for (Entry<String, RtmList> 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<Param> params = new HashSet<Param>();
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<Param> params = new HashSet<Param>();
params.add(new Param("method", "rtm.tasks.getList"));
params.add(new Param("auth_token", currentAuthToken));
params.add(new Param("api_key", applicationInfo.getApiKey()));
params.add(new Param("filter", "name:" + taskName));
RtmTasks rtmTasks = new RtmTasks(invoker.invoke(params.toArray(new Param[params.size()])));
return findTask(taskSeriesId, rtmTasks);
}
private RtmTaskSeries findTask(String taskId, RtmTasks rtmTasks)
{
for (RtmTaskList list : rtmTasks.getLists())
{
for (RtmTaskSeries series : list.getSeries())
{
if (taskId != null)
{
if (series.getId().equals(taskId))
{
return series;
}
}
else
{
return series;
}
}
}
return null;
}
public void tasks_movePriority()
{
throw new UnsupportedOperationException("Not supported yet.");
}
public RtmTaskSeries tasks_moveTo(String timelineId, String fromListId, String toListId, String taskSeriesId, String taskId)
throws ServiceException
{
Element elt = invoker.invoke(new Param("method", "rtm.tasks.moveTo"), new Param("timeline", timelineId), new Param("from_list_id", fromListId),
new Param("to_list_id", toListId), new Param("taskseries_id", taskSeriesId), new Param("task_id", taskId), new Param("auth_token", currentAuthToken),
new Param("api_key", applicationInfo.getApiKey()));
RtmTaskList rtmTaskList = new RtmTaskList(elt);
return findTask(taskSeriesId, taskId, rtmTaskList);
}
public void tasks_postpone()
{
throw new UnsupportedOperationException("Not supported yet.");
}
public void tasks_removeTags()
{
throw new UnsupportedOperationException("Not supported yet.");
}
public void tasks_setDueDate(String timelineId, String listId, String taskSeriesId, String taskId, Date due, boolean hasDueTime)
throws ServiceException
{
final boolean setDueDate = (due != null);
if (setDueDate == true)
{
invoker.invoke(new Param("method", "rtm.tasks.setDueDate"), new Param("timeline", timelineId), new Param("list_id", listId),
new Param("taskseries_id", taskSeriesId), new Param("task_id", taskId), new Param("due", due), new Param("has_due_time", hasDueTime ? "1" : "0"),
new Param("auth_token", currentAuthToken), new Param("api_key", applicationInfo.getApiKey()));
}
else
{
invoker.invoke(new Param("method", "rtm.tasks.setDueDate"), new Param("timeline", timelineId), new Param("list_id", listId),
new Param("taskseries_id", taskSeriesId), new Param("task_id", taskId), new Param("auth_token", currentAuthToken),
new Param("api_key", applicationInfo.getApiKey()));
}
}
public void tasks_setEstimate(String timelineId, String listId, String taskSeriesId, String taskId, String newEstimate)
throws ServiceException
{
invoker.invoke(new Param("method", "rtm.tasks.setEstimate"), new Param("timeline", timelineId), new Param("list_id", listId),
new Param("taskseries_id", taskSeriesId), new Param("task_id", taskId), new Param("estimate", newEstimate), new Param("auth_token", currentAuthToken),
new Param("api_key", applicationInfo.getApiKey()));
}
public void tasks_setName(String timelineId, String listId, String taskSeriesId, String taskId, String newName)
throws ServiceException
{
invoker.invoke(new Param("method", "rtm.tasks.setName"), new Param("timeline", timelineId), new Param("list_id", listId),
new Param("taskseries_id", taskSeriesId), new Param("task_id", taskId), new Param("name", newName), new Param("auth_token", currentAuthToken),
new Param("api_key", applicationInfo.getApiKey()));
}
private RtmTaskSeries findTask(String taskSeriesId, String taskId, RtmTaskList rtmTaskList)
{
for (RtmTaskSeries series : rtmTaskList.getSeries())
{
if (series.getId().equals(taskSeriesId) && series.getTask().getId().equals(taskId))
{
return series;
}
}
return null;
}
public void tasks_setPriority(String timelineId, String listId, String taskSeriesId, String taskId, Priority priority)
throws ServiceException
{
invoker.invoke(new Param("method", "rtm.tasks.setPriority"), new Param("timeline", timelineId), new Param("list_id", listId),
new Param("taskseries_id", taskSeriesId), new Param("task_id", taskId), new Param("priority", RtmTask.convertPriority(priority)),
new Param("auth_token", currentAuthToken), new Param("api_key", applicationInfo.getApiKey()));
}
public void tasks_setRecurrence(String timelineId, String listId, String taskSeriesId, String taskId, String repeat)
throws ServiceException
{
invoker.invoke(new Param("method", "rtm.tasks.setRecurrence"), new Param("timeline", timelineId), new Param("list_id", listId),
new Param("taskseries_id", taskSeriesId), new Param("task_id", taskId), new Param("repeat", repeat),
new Param("auth_token", currentAuthToken), new Param("api_key", applicationInfo.getApiKey()));
}
public void tasks_setTags(String timelineId, String listId,
String taskSeriesId, String taskId, String[] tags) throws ServiceException
{
StringBuilder tagString = new StringBuilder();
if(tags != null) {
for(int i = 0; i < tags.length; i++) {
tagString.append(tags[i].replace(" ", "_"));
if(i < tags.length - 1)
tagString.append(",");
}
}
invoker.invoke(new Param("method", "rtm.tasks.setTags"), new Param("timeline", timelineId), new Param("list_id", listId),
new Param("taskseries_id", taskSeriesId), new Param("task_id", taskId), new Param("tags", tagString.toString()), new Param("auth_token", currentAuthToken),
new Param("api_key", applicationInfo.getApiKey()));
}
public void tasks_setURL()
{
throw new UnsupportedOperationException("Not supported yet.");
}
public void tasks_uncomplete(String timelineId, String listId, String taskSeriesId, String taskId)
throws ServiceException
{
invoker.invoke(new Param("method", "rtm.tasks.uncomplete"), new Param("timeline", timelineId), new Param("list_id", listId),
new Param("taskseries_id", taskSeriesId), new Param("task_id", taskId), new Param("auth_token", currentAuthToken),
new Param("api_key", applicationInfo.getApiKey()));
}
public RtmTaskNote tasks_notes_add(String timelineId, String listId, String taskSeriesId, String taskId, String title, String text)
throws ServiceException
{
Element elt = invoker.invoke(new Param("method", "rtm.tasks.notes.add"), new Param("timeline", timelineId), new Param("list_id", listId),
new Param("taskseries_id", taskSeriesId), new Param("task_id", taskId), new Param("note_title", title), new Param("note_text", text),
new Param("auth_token", currentAuthToken), new Param("api_key", applicationInfo.getApiKey()));
return new RtmTaskNote(elt);
}
public void tasks_notes_delete(String timelineId, String noteId)
throws ServiceException
{
invoker.invoke(new Param("method", "rtm.tasks.notes.delete"), new Param("timeline", timelineId), new Param("note_id", noteId),
new Param("auth_token", currentAuthToken), new Param("api_key", applicationInfo.getApiKey()));
}
public RtmTaskNote tasks_notes_edit(String timelineId, String noteId, String title, String text)
throws ServiceException
{
Element elt = invoker.invoke(new Param("method", "rtm.tasks.notes.edit"), new Param("timeline", timelineId), new Param("note_id", noteId),
new Param("note_title", title), new Param("note_text", text), new Param("auth_token", currentAuthToken),
new Param("api_key", applicationInfo.getApiKey()));
return new RtmTaskNote(elt);
}
public RtmTaskSeries tasks_setLocation(String timelineId, String listId, String taskSeriesId, String taskId, String locationId)
throws ServiceException
{
Element response = invoker.invoke(new Param("method", "rtm.tasks.setLocation"), new Param("timeline", timelineId), new Param("list_id", listId),
new Param("taskseries_id", taskSeriesId), new Param("task_id", taskId), new Param("location_id", locationId),
new Param("auth_token", currentAuthToken), new Param("api_key", applicationInfo.getApiKey()));
RtmTaskList rtmTaskList = new RtmTaskList(response);
return findTask(taskSeriesId, taskId, rtmTaskList);
}
public RtmTaskSeries tasks_setURL(String timelineId, String listId, String taskSeriesId, String taskId, String url)
throws ServiceException
{
Element response = invoker.invoke(new Param("method", "rtm.tasks.setURL"), new Param("timeline", timelineId), new Param("list_id", listId),
new Param("taskseries_id", taskSeriesId), new Param("task_id", taskId), new Param("url", url), new Param("auth_token", currentAuthToken),
new Param("api_key", applicationInfo.getApiKey()));
RtmTaskList rtmTaskList = new RtmTaskList(response);
return findTask(taskSeriesId, taskId, rtmTaskList);
}
public void test_echo()
{
throw new UnsupportedOperationException("Not supported yet.");
}
public void test_login()
{
throw new UnsupportedOperationException("Not supported yet.");
}
public void time_convert()
{
throw new UnsupportedOperationException("Not supported yet.");
}
public void time_parse()
{
throw new UnsupportedOperationException("Not supported yet.");
}
public String timelines_create()
throws ServiceException
{
return new RtmTimeline(invoker.invoke(new Param("method", "rtm.timelines.create"), new Param("auth_token", currentAuthToken),
new Param("api_key", applicationInfo.getApiKey()))).getId();
}
public void timezones_getList()
{
throw new UnsupportedOperationException("Not supported yet.");
}
public void transactions_undo()
{
throw new UnsupportedOperationException("Not supported yet.");
}
public List<RtmLocation> 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<RtmLocation> locations = new ArrayList<RtmLocation>();
for (Element child : RtmData.children(result, "location"))
{
locations.add(new RtmLocation(child));
}
return locations;
}
}

@ -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 <http://www.gnu.org/licenses/>.
*/
package com.todoroo.astrid.rmilk.api;
/**
* Introduced in order to get rid of the {@link RuntimeException}, and have only one time of regular exception to cope with, from the API end-user
* point of view.
*
* @author Edouard Mercier
* @since 2008.04.23
*/
public class ServiceInternalException
extends ServiceException
{
private static final long serialVersionUID = -423838945284984432L;
private final Exception enclosedException;
public ServiceInternalException(String message)
{
this(message, null);
}
public ServiceInternalException(String message, Exception exception)
{
super(-1, message);
this.enclosedException = exception;
}
public Exception getEnclosedException()
{
return enclosedException;
}
}

@ -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 <http://www.gnu.org/licenses/>.
*/
package com.todoroo.astrid.rmilk.api.data;
import org.w3c.dom.Element;
/**
*
* @author Will Ross Jun 21, 2007
*/
@SuppressWarnings("nls")
public class RtmAuth extends RtmData {
public enum Perms {
read, write, delete
}
private final String token;
private final Perms perms;
private final RtmUser user;
public RtmAuth(String token, Perms perms, RtmUser user) {
this.token = token;
this.perms = perms;
this.user = user;
}
public RtmAuth(Element elt) {
if (!elt.getNodeName().equals("auth")) { throw new IllegalArgumentException("Element " + elt.getNodeName() + " does not represent an Auth object."); }
this.token = text(child(elt, "token"));
this.perms = Enum.valueOf(Perms.class, text(child(elt, "perms")));
this.user = new RtmUser(child(elt, "user"));
}
public String getToken() {
return token;
}
public Perms getPerms() {
return perms;
}
public RtmUser getUser() {
return user;
}
}

@ -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 <http://www.gnu.org/licenses/>.
*/
package com.todoroo.astrid.rmilk.api.data;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.TimeZone;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
/**
*
* @author Will Ross Jun 21, 2007
*/
@SuppressWarnings("nls")
public abstract class RtmData
{
private static final DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
public RtmData() {
//
}
/**
* The method is not optimized at most, but circumvents a bug in Android runtime.
*/
public static Element child(Element elt, String nodeName)
{
NodeList childNodes = elt.getChildNodes();
for (int index = 0; index < childNodes.getLength(); index++)
{
Node child = childNodes.item(index);
if (child.getNodeType() == Node.ELEMENT_NODE && child.getNodeName().equals(nodeName))
{
return (Element) child;
}
}
return null;
}
/**
* The method is not optimized at most, but circumvents a bug in Android runtime.
*/
public static List<Element> children(Element elt, String nodeName)
{
List<Element> result = new ArrayList<Element>();
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";
}
}

@ -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 <http://www.gnu.org/licenses/>.
*/
package com.todoroo.astrid.rmilk.api.data;
import org.w3c.dom.Element;
/**
*
* @author Will Ross Jun 21, 2007
*/
public class RtmFrob extends RtmData {
private final String value;
public RtmFrob(String value) {
this.value = value;
}
public RtmFrob(Element elt) {
this.value = text(elt);
}
public String getValue() {
return value;
}
}

@ -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 <http://www.gnu.org/licenses/>.
*/
package com.todoroo.astrid.rmilk.api.data;
import org.w3c.dom.Element;
@SuppressWarnings("nls")
public class RtmList extends RtmData {
private final String id;
private final boolean smart;
private final String name;
public RtmList(String id, String name, boolean smart) {
this.id = id;
this.name = name;
this.smart = smart;
}
public RtmList(Element elt) {
id = elt.getAttribute("id");
name = elt.getAttribute("name");
smart = elt.getAttribute("smart") == "1";
}
public String getId() {
return id;
}
public String getName() {
return name;
}
public boolean isSmart() {
return smart;
}
}

@ -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 <http://www.gnu.org/licenses/>.
*/
package com.todoroo.astrid.rmilk.api.data;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import org.w3c.dom.Element;
@SuppressWarnings("nls")
public class RtmLists extends RtmData {
private final Map<String, RtmList> lists;
public RtmLists() {
this.lists = new HashMap<String, RtmList>();
}
public RtmLists(Element elt) {
this.lists = new HashMap<String, RtmList>();
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<String, RtmList> getLists() {
return Collections.unmodifiableMap(lists);
}
}

@ -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 <http://www.gnu.org/licenses/>.
*/
package com.todoroo.astrid.rmilk.api.data;
import org.w3c.dom.Element;
/**
* Represents a location.
*
* @author Edouard Mercier
* @since 2008.05.22
*/
@SuppressWarnings("nls")
public class RtmLocation
extends RtmData
{
public final String id;
public final String name;
public final float longitude;
public final float latitude;
public final String address;
public final boolean viewable;
public int zoom;
public RtmLocation(Element element)
{
id = element.getAttribute("id");
name = element.getAttribute("name");
longitude = Float.parseFloat(element.getAttribute("longitude"));
latitude = Float.parseFloat(element.getAttribute("latitude"));
address = element.getAttribute("address");
zoom = Integer.parseInt(element.getAttribute("zoom"));
viewable = element.getAttribute("viewable").equals("1") ? true : false;
}
}

@ -0,0 +1,194 @@
/*
* Copyright 2007, MetaDimensional Technologies Inc.
*
*
* This file is part of the RememberTheMilk Java API.
*
* The RememberTheMilk Java API is free software; you can redistribute it
* and/or modify it under the terms of the GNU Lesser General Public License
* as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* The RememberTheMilk Java API is distributed in the hope that it will be
* useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser
* General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.todoroo.astrid.rmilk.api.data;
import java.util.Date;
import org.w3c.dom.Element;
import android.util.Log;
/**
*
* @author Will Ross Jun 21, 2007
*/
@SuppressWarnings("nls")
public class RtmTask
extends RtmData
{
private static final String TAG = "rtm-task";
private final String id;
private final Date due;
private final boolean hasDueTime;
private final Date added;
private final Date completed;
private final Date deleted;
private final Priority priority;
private final int postponed;
private final String estimate;
public enum Priority
{
High, Medium, Low, None
}
public static String convertPriority(Priority priority)
{
switch (priority)
{
case None:
return new String(new char[] { 'n' });
case Low:
return new String(new char[] { '3' });
case Medium:
return new String(new char[] { '2' });
case High:
return new String(new char[] { '1' });
default:
Log.e(TAG, "Unrecognized RTM task priority: '" + priority + "'");
return new String(new char[] { 'n' });
}
}
public RtmTask(String id, Date due, boolean hasDueTime, Date added, Date completed, Date deleted, Priority priority, int postponed, String estimate)
{
this.id = id;
this.due = due;
this.hasDueTime = hasDueTime;
this.added = added;
this.completed = completed;
this.deleted = deleted;
this.priority = priority;
this.postponed = postponed;
this.estimate = estimate;
}
public RtmTask(Element elt)
{
id = elt.getAttribute("id");
String dueStr = elt.getAttribute("due");
due = (dueStr == null || dueStr.length() == 0) ? null : parseDate(dueStr);
hasDueTime = Integer.parseInt(elt.getAttribute("has_due_time")) != 0;
String addedStr = elt.getAttribute("added");
added = (addedStr == null || addedStr.length() == 0) ? null : parseDate(addedStr);
String completedStr = elt.getAttribute("completed");
completed = (completedStr == null || completedStr.length() == 0) ? null : parseDate(completedStr);
String deletedStr = elt.getAttribute("deleted");
deleted = (deletedStr == null || deletedStr.length() == 0) ? null : parseDate(deletedStr);
String priorityStr = elt.getAttribute("priority");
if (priorityStr.length() > 0)
{
switch (priorityStr.charAt(0))
{
case 'N':
case 'n':
priority = Priority.None;
break;
case '3':
priority = Priority.Low;
break;
case '2':
priority = Priority.Medium;
break;
case '1':
priority = Priority.High;
break;
default:
System.err.println("Unrecognized RTM task priority: '" + priorityStr + "'");
priority = Priority.Medium;
}
}
else
{
priority = Priority.None;
}
if (elt.hasAttribute("postponed") == true && elt.getAttribute("postponed").length() > 0)
{
postponed = Integer.parseInt(elt.getAttribute("postponed"));
}
else
{
postponed = 0;
}
estimate = elt.getAttribute("estimate");
}
public String getId()
{
return id;
}
public Date getDue()
{
return due;
}
public boolean getHasDueTime()
{
return hasDueTime;
}
public Date getAdded()
{
return added;
}
public Date getCompleted()
{
return completed;
}
public Date getDeleted()
{
return deleted;
}
public Priority getPriority()
{
return priority;
}
public int getPostponed()
{
return postponed;
}
public String getEstimate()
{
return estimate;
}
@Override
public String toString()
{
return "Task<" + id + ">";
}
}

@ -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 <http://www.gnu.org/licenses/>.
*/
package com.todoroo.astrid.rmilk.api.data;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.w3c.dom.Element;
/**
*
* @author Will Ross Jun 22, 2007
*/
@SuppressWarnings("nls")
public class RtmTaskList extends RtmData {
private final String id;
private final List<RtmTaskSeries> series;
public RtmTaskList(String id) {
this.id = id;
this.series = new ArrayList<RtmTaskSeries>();
}
public RtmTaskList(Element elt) {
id = elt.getAttribute("id");
series = new ArrayList<RtmTaskSeries>();
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<RtmTaskSeries> getSeries() {
return Collections.unmodifiableList(series);
}
}

@ -0,0 +1,101 @@
/*
* Copyright 2007, MetaDimensional Technologies Inc.
*
*
* This file is part of the RememberTheMilk Java API.
*
* The RememberTheMilk Java API is free software; you can redistribute it
* and/or modify it under the terms of the GNU Lesser General Public License
* as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* The RememberTheMilk Java API is distributed in the hope that it will be
* useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser
* General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.todoroo.astrid.rmilk.api.data;
import java.util.Date;
import org.w3c.dom.Element;
import org.w3c.dom.Text;
import android.util.Log;
/**
* Represents a single task note.
*
* @author Edouard Mercier
* @since 2008.04.22
*/
@SuppressWarnings("nls")
public class RtmTaskNote
extends RtmData
{
private String id;
private Date created;
private Date modified;
private String title;
private String text;
public RtmTaskNote(Element element)
{
id = element.getAttribute("id");
created = parseDate(element.getAttribute("created"));
modified = parseDate(element.getAttribute("modified"));
title = element.getAttribute("title");
// The note text itself might be split across multiple children of the
// note element, so get all of the children.
for (int i=0; i < element.getChildNodes().getLength(); i++) {
Object innerNote = element.getChildNodes().item(i);
if(!(innerNote instanceof Text)) {
Log.w("rtm-note", "Expected text type, got " + innerNote.getClass());
continue;
}
Text innerText = (Text) innerNote;
if (text == null)
text = innerText.getData();
else
text = text.concat(innerText.getData());
}
}
public String getId()
{
return id;
}
public Date getCreated()
{
return created;
}
public Date getModified()
{
return modified;
}
public String getTitle()
{
return title;
}
public String getText()
{
return text;
}
}

@ -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 <http://www.gnu.org/licenses/>.
*/
package com.todoroo.astrid.rmilk.api.data;
import java.util.ArrayList;
import java.util.List;
import org.w3c.dom.Element;
/**
* Represents the notes of a task.
*
* @author Edouard Mercier
* @since 2008.04.22
*/
@SuppressWarnings("nls")
public class RtmTaskNotes
extends RtmData
{
private List<RtmTaskNote> notes;
public RtmTaskNotes(Element element)
{
notes = new ArrayList<RtmTaskNote>();
for (Element child : children(element, "note"))
{
notes.add(new RtmTaskNote(child));
}
}
public List<RtmTaskNote> getNotes()
{
return notes;
}
}

@ -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 <http://www.gnu.org/licenses/>.
*/
package com.todoroo.astrid.rmilk.api.data;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;
import org.w3c.dom.Element;
/**
*
* @author Will Ross Jun 22, 2007
*/
@SuppressWarnings("nls")
public class RtmTaskSeries extends RtmData {
private final RtmTaskList list;
private final String id;
private final Date created;
private final Date modified;
private final String name;
private final String source;
private final RtmTask task;
private final LinkedList<String> 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<Element> 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<Element> elementTagList = children(elementTags, "tag");
tags = new LinkedList<String>();
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<String> 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;
}
}

@ -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 <http://www.gnu.org/licenses/>.
*/
package com.todoroo.astrid.rmilk.api.data;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.w3c.dom.Element;
/**
*
* @author Will Ross Jun 21, 2007
*/
@SuppressWarnings("nls")
public class RtmTasks extends RtmData {
private final List<RtmTaskList> lists;
public RtmTasks() {
this.lists = new ArrayList<RtmTaskList>();
}
public RtmTasks(Element elt) {
this.lists = new ArrayList<RtmTaskList>();
for (Element listElt : children(elt, "list")) {
lists.add(new RtmTaskList(listElt));
}
}
public List<RtmTaskList> getLists() {
return Collections.unmodifiableList(lists);
}
}

@ -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 <http://www.gnu.org/licenses/>.
*/
package com.todoroo.astrid.rmilk.api.data;
import org.w3c.dom.Element;
public class RtmTimeline extends RtmData {
private final String id;
public RtmTimeline(String id) {
this.id = id;
}
public RtmTimeline(Element elt) {
id = text(elt);
}
public String getId() {
return id;
}
}

@ -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 <http://www.gnu.org/licenses/>.
*/
package com.todoroo.astrid.rmilk.api.data;
import org.w3c.dom.Element;
/**
*
* @author Will Ross Jun 21, 2007
*/
@SuppressWarnings("nls")
public class RtmUser extends RtmData {
private final String id;
private final String username;
private final String fullname;
public RtmUser(String id, String username, String fullname) {
this.id = id;
this.username = username;
this.fullname = fullname;
}
public RtmUser(Element elt) {
if (!elt.getNodeName().equals("user")) { throw new IllegalArgumentException("Element " + elt.getNodeName() + " does not represent a User object."); }
this.id = elt.getAttribute("id");
this.username = elt.getAttribute("username");
this.fullname = elt.getAttribute("fullname");
}
public String getId() {
return id;
}
public String getUsername() {
return username;
}
public String getFullname() {
return fullname;
}
}

@ -0,0 +1,282 @@
/**
* See the file "LICENSE" for the full license governing this code.
*/
package com.todoroo.astrid.rmilk.data;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import android.content.Context;
import android.database.sqlite.SQLiteQueryBuilder;
import com.todoroo.andlib.data.GenericDao;
import com.todoroo.andlib.data.Property;
import com.todoroo.andlib.data.TodorooCursor;
import com.todoroo.andlib.data.Property.IntegerProperty;
import com.todoroo.andlib.service.Autowired;
import com.todoroo.andlib.service.DependencyInjectionService;
import com.todoroo.astrid.dao.Database;
import com.todoroo.astrid.dao.TaskDao;
import com.todoroo.astrid.model.Metadata;
import com.todoroo.astrid.model.Task;
import com.todoroo.astrid.rmilk.Utilities;
import com.todoroo.astrid.rmilk.Utilities.ListContainer;
import com.todoroo.astrid.rmilk.api.data.RtmList;
import com.todoroo.astrid.rmilk.api.data.RtmLists;
import com.todoroo.astrid.service.MetadataService;
@SuppressWarnings("nls")
public class MilkDataService {
protected final Context context;
private final MilkDatabase milkDatabase = new MilkDatabase();
@Autowired
private MetadataService metadataService;
private final GenericDao<MilkList> listDao;
@Autowired
private TaskDao taskDao;
@Autowired
private Database database;
static final Random random = new Random();
public MilkDataService(Context context) {
this.context = context;
DependencyInjectionService.getInstance().inject(this);
listDao = new GenericDao<MilkList>(MilkList.class, milkDatabase);
}
// --- RTM properties
/** RTM List id */
public static final StringJoinProperty LIST_ID =
new StringJoinProperty(Utilities.KEY_LIST_ID);
/** RTM Task Series id */
public static final StringJoinProperty TASK_SERIES_ID =
new StringJoinProperty(Utilities.KEY_TASK_SERIES_ID);
/** RTM Task id */
public static final StringJoinProperty TASK_ID =
new StringJoinProperty(Utilities.KEY_TASK_ID);
/** 1 if task repeats in RTM, 0 otherwise */
public static final IntegerJoinProperty REPEAT =
new IntegerJoinProperty(Utilities.KEY_REPEAT);
/** 1 if task was updated since last sync, 0 otherwise */
public static final IntegerJoinProperty UPDATED =
new IntegerJoinProperty(Utilities.KEY_UPDATED);
// --- non-RTM properties we synchronize
public static final StringJoinProperty TAGS =
new StringJoinProperty(com.todoroo.astrid.tags.TagService.KEY);
// --- task and metadata methods
/** Properties to fetch when user wants to view / edit tasks */
public static final Property<?>[] RTM_PROPERTIES = new Property<?>[] {
Task.ID,
LIST_ID,
TASK_SERIES_ID,
TASK_ID,
REPEAT
};
/**
* Read a single task by task id
* @param taskId
* @return item, or null if it doesn't exist
*/
public Task readTask(long taskId) {
SQLiteQueryBuilder builder = new SQLiteQueryBuilder();
builder.setTables(createJoinClause(RTM_PROPERTIES));
TodorooCursor<Task> cursor =
taskDao.query(database, RTM_PROPERTIES, builder,
Task.ID.qualifiedName()+ " = " + taskId,
null, null);
try {
if (cursor.getCount() == 0)
return null;
cursor.moveToFirst();
Task task = new Task(cursor, RTM_PROPERTIES);
return task;
} finally {
cursor.close();
}
}
/** Helper method for building a join clause for task/metadata joins */
public static String createJoinClause(Property<?>[] properties) {
StringBuilder stringBuilder = new StringBuilder(Database.TASK_TABLE);
int joinTableCount = 0;
for(Property<?> property : properties) {
if(property instanceof JoinProperty) {
JoinProperty jp = (JoinProperty)property;
stringBuilder.append(" LEFT JOIN (").append(jp.joinTable()).
append(") m").append(++joinTableCount).
append(" ON ").append(Task.ID_PROPERTY).append(" = m").
append(joinTableCount).append('.').append(Metadata.TASK.name).
append(' ');
}
}
return stringBuilder.toString();
}
/**
* Clears RTM metadata information. Used when user logs out of RTM
*/
public void clearMetadata() {
metadataService.deleteWhere(String.format("%s = '%s' OR %s = '%s' " +
"OR %s = '%s OR %s = '%s'",
Metadata.KEY, LIST_ID,
Metadata.KEY, TASK_SERIES_ID,
Metadata.KEY, TASK_ID,
Metadata.KEY, REPEAT));
}
public TodorooCursor<Task> getLocallyCreated(Property<?>[] properties) {
SQLiteQueryBuilder builder = new SQLiteQueryBuilder();
builder.setTables(createJoinClause(properties));
TodorooCursor<Task> cursor =
taskDao.query(database, properties, builder,
TASK_ID + " ISNULL",
null, null);
return cursor;
}
public TodorooCursor<Task> getLocallyUpdated(Property<?>[] properties) {
SQLiteQueryBuilder builder = new SQLiteQueryBuilder();
builder.setTables(createJoinClause(properties));
TodorooCursor<Task> cursor =
taskDao.query(database, properties, builder,
"NOT " + TASK_ID + " ISNULL", // TODO wrong!
null, null);
return cursor;
}
// --- list methods
/**
* Get list name by list id
* @param listId
* @return null if no list by this id exists, otherwise list name
*/
public String getList(String listId) {
pluginDatabase.open(context);
TodorooCursor<List> cursor = listDao.fetch(pluginDatabase,
List.PROPERTIES, ListSql.withId(listId), null, "1"); //$NON-NLS-1$
try {
if(cursor.getCount() == 0)
return null;
cursor.moveToFirst();
return cursor.get(List.NAME);
} finally {
cursor.close();
pluginDatabase.close();
}
}
/**
* Get RTM lists as container objects
* @return
*/
public ListContainer[] getListsWithCounts() {
// read all list titles
pluginDatabase.open(context);
TodorooCursor<List> listCursor = listDao.fetch(pluginDatabase,
List.PROPERTIES, null);
HashMap<String, ListContainer> listIdToContainerMap;
try {
int count = listCursor.getCount();
if(count == 0)
return new ListContainer[0];
listIdToContainerMap =
new HashMap<String, ListContainer>(count);
List list = new List();
for(int i = 0; i < count; i++) {
listCursor.moveToNext();
list.readFromCursor(listCursor, List.PROPERTIES);
ListContainer container = new ListContainer(list);
listIdToContainerMap.put(container.id, container);
}
} finally {
listCursor.close();
}
// read all list counts
IntegerProperty countProperty = Property.countProperty();
TodorooCursor<Metadata> metadataCursor = metadataService.fetchWithCount(
MetadataSql.withKey(Utilities.KEY_LIST_ID), Metadata.VALUE + " ASC", false); //$NON-NLS-1$
ListContainer[] containers = new ListContainer[metadataCursor.getCount()];
try {
for(int i = 0; i < containers.length; i++) {
metadataCursor.moveToNext();
String id = metadataCursor.get(Metadata.VALUE);
ListContainer container = listIdToContainerMap.get(id);
if(container == null) {
container = new ListContainer(id, "[unknown]"); //$NON-NLS-1$
}
container.count = metadataCursor.get(countProperty);
containers[i] = container;
}
return containers;
} finally {
metadataCursor.close();
pluginDatabase.close();
}
}
/**
* Get RTM lists as strings
* @return
*/
public ListContainer[] getLists() {
// read all list titles
pluginDatabase.open(context);
TodorooCursor<List> cursor = listDao.fetch(pluginDatabase,
List.PROPERTIES, null, List.ID + " ASC"); //$NON-NLS-1$
ListContainer[] containers = new ListContainer[cursor.getCount()];
try {
List list = new List();
for(int i = 0; i < containers.length; i++) {
cursor.moveToNext();
list.readFromCursor(cursor, List.PROPERTIES);
ListContainer container = new ListContainer(list);
containers[i] = container;
}
return containers;
} finally {
cursor.close();
pluginDatabase.close();
}
}
/**
* Clears current cache of RTM lists and re-populates
* @param lists
*/
public void setLists(RtmLists lists) {
pluginDatabase.open(context);
try {
List model = new List();
for(Map.Entry<String, RtmList> list : lists.getLists().entrySet()) {
model.setValue(List.ID, list.getValue().getId());
model.setValue(List.NAME, list.getValue().getName());
listDao.save(pluginDatabase, model);
}
} finally {
pluginDatabase.close();
}
}
}

@ -0,0 +1,76 @@
/*
* Copyright (c) 2009, Todoroo Inc
* All Rights Reserved
* http://www.todoroo.com
*/
package com.todoroo.astrid.rmilk.data;
import com.todoroo.andlib.data.AbstractDatabase;
import com.todoroo.andlib.data.GenericDao;
import com.todoroo.andlib.data.Table;
/**
* Database wrapper
*
* @author Tim Su <tim@todoroo.com>
*
*/
@SuppressWarnings("nls")
public class MilkDatabase extends AbstractDatabase {
// --- constants
/**
* Database version number. This variable must be updated when database
* tables are updated, as it determines whether a database needs updating.
*/
public static final int VERSION = 1;
/**
* Database name (must be unique)
*/
private static final String NAME = "milk";
/**
* List of table/ If you're adding a new table, add it to this list and
* also make sure that our SQLite helper does the right thing.
*/
public static final Table[] TABLES = new Table[] {
MilkList.TABLE
};
// --- implementation
private final GenericDao<MilkList> dao = new GenericDao<MilkList>(MilkList.class, this);
@Override
protected String getName() {
return NAME;
}
@Override
protected int getVersion() {
return VERSION;
}
@Override
public Table[] getTables() {
return TABLES;
}
public GenericDao<MilkList> getDao() {
return dao;
}
@Override
protected void onCreateTables() {
// do nothing
}
@Override
protected boolean onUpgrade(int oldVersion, int newVersion) {
return false;
}
}

@ -0,0 +1,103 @@
/**
* See the file "LICENSE" for the full license governing this code.
*/
package com.todoroo.astrid.rmilk.data;
import android.content.ContentValues;
import com.todoroo.andlib.data.AbstractModel;
import com.todoroo.andlib.data.Property;
import com.todoroo.andlib.data.Table;
import com.todoroo.andlib.data.TodorooCursor;
import com.todoroo.andlib.data.Property.IntegerProperty;
import com.todoroo.andlib.data.Property.LongProperty;
import com.todoroo.andlib.data.Property.StringProperty;
import com.todoroo.astrid.model.Task;
/**
* Data Model which represents a list in RTM
*
* @author Tim Su <tim@todoroo.com>
*
*/
@SuppressWarnings("nls")
public class MilkList extends AbstractModel {
// --- table
public static final Table TABLE = new Table("lists", MilkList.class);
// --- properties
/** ID (corresponds to RTM ID) */
public static final LongProperty ID = new LongProperty(
TABLE, ID_PROPERTY_NAME);
/** Name */
public static final StringProperty NAME = new StringProperty(
TABLE, "name");
/** Position */
public static final IntegerProperty POSITION = new IntegerProperty(
TABLE, "position");
/** Archived (0 or 1) */
public static final IntegerProperty ARCHIVED = new IntegerProperty(
TABLE, "archived");
/** List of all properties for this model */
public static final Property<?>[] PROPERTIES = generateProperties(MilkList.class);
// --- defaults
/** Default values container */
private static final ContentValues defaultValues = new ContentValues();
static {
defaultValues.put(POSITION.name, 0);
defaultValues.put(ARCHIVED.name, 0);
}
@Override
public ContentValues getDefaultValues() {
return defaultValues;
}
// --- data access boilerplate
public MilkList() {
super();
}
public MilkList(TodorooCursor<MilkList> cursor) {
this();
readPropertiesFromCursor(cursor);
}
public void readFromCursor(TodorooCursor<MilkList> cursor) {
super.readPropertiesFromCursor(cursor);
}
@Override
public long getId() {
return getIdHelper(ID);
};
/**
* @return whether this list is archived. requires {@link ARCHIVED}
*/
public boolean isArchived() {
return getValue(ARCHIVED) > 0;
}
// --- parcelable helpers
private static final Creator<Task> CREATOR = new ModelCreator<Task>(Task.class);
@Override
protected Creator<? extends AbstractModel> getCreator() {
return CREATOR;
}
}

@ -0,0 +1,465 @@
/**
* See the file "LICENSE" for the full license governing this code.
*/
package com.todoroo.astrid.rmilk.sync;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Date;
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
import android.os.Handler;
import android.text.TextUtils;
import android.util.Log;
import com.flurry.android.FlurryAgent;
import com.timsu.astrid.R;
import com.todoroo.andlib.data.Property;
import com.todoroo.andlib.data.TodorooCursor;
import com.todoroo.andlib.service.Autowired;
import com.todoroo.andlib.service.ContextManager;
import com.todoroo.andlib.service.DependencyInjectionService;
import com.todoroo.andlib.service.ExceptionService;
import com.todoroo.andlib.utility.AndroidUtilities;
import com.todoroo.andlib.utility.DateUtilities;
import com.todoroo.astrid.api.SynchronizationProvider;
import com.todoroo.astrid.model.Task;
import com.todoroo.astrid.rmilk.MilkLoginActivity;
import com.todoroo.astrid.rmilk.Utilities;
import com.todoroo.astrid.rmilk.MilkLoginActivity.SyncLoginCallback;
import com.todoroo.astrid.rmilk.api.ApplicationInfo;
import com.todoroo.astrid.rmilk.api.ServiceImpl;
import com.todoroo.astrid.rmilk.api.ServiceInternalException;
import com.todoroo.astrid.rmilk.api.data.RtmList;
import com.todoroo.astrid.rmilk.api.data.RtmLists;
import com.todoroo.astrid.rmilk.api.data.RtmTask;
import com.todoroo.astrid.rmilk.api.data.RtmTaskList;
import com.todoroo.astrid.rmilk.api.data.RtmTaskSeries;
import com.todoroo.astrid.rmilk.api.data.RtmTasks;
import com.todoroo.astrid.rmilk.api.data.RtmAuth.Perms;
import com.todoroo.astrid.rmilk.api.data.RtmTask.Priority;
import com.todoroo.astrid.rmilk.data.MilkDataService;
import com.todoroo.astrid.service.AstridDependencyInjector;
public class RTMSyncProvider extends SynchronizationProvider {
protected ServiceImpl rtmService = null;
protected String timeline = null;
protected MilkDataService dataService = null;
static {
AstridDependencyInjector.initialize();
}
@Autowired
protected ExceptionService exceptionService;
public RTMSyncProvider() {
super();
DependencyInjectionService.getInstance().inject(this);
}
// ----------------------------------------------------------------------
// ------------------------------------------------------- public methods
// ----------------------------------------------------------------------
@Override
public void synchronize() {
Context context = ContextManager.getContext();
dataService = new MilkDataService(context);
// authenticate the user. this will automatically call the next step
authenticate(context);
}
/**
* Sign out of RTM, deleting all synchronization metadata
*/
public void signOut() {
Utilities.setToken(null);
Utilities.setLastSyncDate(null);
dataService.clearMetadata();
}
// ----------------------------------------------------------------------
// ------------------------------------------------------- authentication
// ----------------------------------------------------------------------
/**
* Deal with a synchronization exception. If requested, will show an error
* to the user (unless synchronization is happening in background)
*
* @param context
* @param tag
* error tag
* @param e
* exception
* @param showErrorIfNeeded
* whether to display a dialog
*/
private void handleRtmException(Context context, String tag, Exception e,
boolean showErrorIfNeeded) {
// occurs when application was closed
if(e instanceof IllegalStateException) {
exceptionService.reportError(tag + "-caught", e); //$NON-NLS-1$
// occurs when network error
} else if(e instanceof ServiceInternalException &&
((ServiceInternalException)e).getEnclosedException() instanceof
IOException) {
Exception enclosedException = ((ServiceInternalException)e).getEnclosedException();
exceptionService.reportError(tag + "-ioexception", enclosedException); //$NON-NLS-1$
if(showErrorIfNeeded) {
showError(context, enclosedException,
context.getString(R.string.rmilk_ioerror));
}
} else {
if(e instanceof ServiceInternalException)
e = ((ServiceInternalException)e).getEnclosedException();
exceptionService.reportError(tag + "-unhandled", e); //$NON-NLS-1$
if(showErrorIfNeeded)
showError(context, e, null);
}
}
/** Perform authentication with RTM. Will open the SyncBrowser if necessary */
@SuppressWarnings("nls")
private void authenticate(final Context context) {
final Resources r = context.getResources();
FlurryAgent.onEvent("rtm-started");
try {
String appName = null;
String authToken = Utilities.getToken();
String z = stripslashes(0,"q9883o3384n21snq17501qn38oo1r689", "b");
String v = stripslashes(16,"19o2n020345219os","a");
// check if we have a token & it works
if(authToken != null) {
rtmService = new ServiceImpl(new ApplicationInfo(
z, v, appName, authToken));
if(!rtmService.isServiceAuthorized()) // re-do login
authToken = null;
}
if(authToken == null) {
// try completing the authorization if it was partial
if(rtmService != null) {
try {
String token = rtmService.completeAuthorization();
Utilities.setToken(token);
performSync(context);
return;
} catch (Exception e) {
// didn't work. do the process again.
}
}
// open up a dialog and have the user go to browser
rtmService = new ServiceImpl(new ApplicationInfo(
z, v, appName));
final String url = rtmService.beginAuthorization(Perms.delete);
Intent intent = new Intent(context, MilkLoginActivity.class);
MilkLoginActivity.setCallback(new SyncLoginCallback() {
public String verifyLogin(final Handler syncLoginHandler) {
if(rtmService == null) {
return null;
}
try {
String token = rtmService.completeAuthorization();
Utilities.setToken(token);
return null;
} catch (Exception e) {
// didn't work
exceptionService.reportError("rtm-verify-login", e);
rtmService = null;
if(e instanceof ServiceInternalException)
e = ((ServiceInternalException)e).getEnclosedException();
return r.getString(R.string.rmilk_MLA_error, e.getMessage());
}
}
});
intent.putExtra(MilkLoginActivity.URL_TOKEN, url);
context.startActivity(intent);
} else {
performSync(context);
}
} catch (IllegalStateException e) {
// occurs when application was closed
} catch (Exception e) {
handleRtmException(context, "rtm-authenticate", e, true);
}
}
// ----------------------------------------------------------------------
// ----------------------------------------------------- synchronization!
// ----------------------------------------------------------------------
private void performSync(final Context context) {
new Thread(new Runnable() {
public void run() {
performSyncInNewThread(context);
}
}).start();
}
protected void performSyncInNewThread(final Context context) {
try {
// get RTM timeline
timeline = rtmService.timelines_create();
// load RTM lists
RtmLists lists = rtmService.lists_getList();
dataService.setLists(lists);
// read all tasks
ArrayList<Task> remoteChanges = new ArrayList<Task>();
Date lastSyncDate = Utilities.getLastSyncDate();
boolean shouldSyncIndividualLists = false;
String filter = null;
if(lastSyncDate == null)
filter = "status:incomplete"; //$NON-NLS-1$ // 1st time sync: get unfinished tasks
// try the quick synchronization
try {
Thread.sleep(2000); // throttle
RtmTasks tasks = rtmService.tasks_getList(null, filter, lastSyncDate);
addTasksToList(tasks, remoteChanges);
} catch (Exception e) {
handleRtmException(context, "rtm-quick-sync", e, false); //$NON-NLS-1$
remoteChanges.clear();
shouldSyncIndividualLists = true;
}
if(shouldSyncIndividualLists) {
for(RtmList list : lists.getLists().values()) {
if(list.isSmart())
continue;
try {
Thread.sleep(1500);
RtmTasks tasks = rtmService.tasks_getList(list.getId(),
filter, lastSyncDate);
addTasksToList(tasks, remoteChanges);
} catch (Exception e) {
handleRtmException(context, "rtm-indiv-sync", e, true); //$NON-NLS-1$
continue;
}
}
}
SyncData syncData = populateSyncData(remoteChanges);
synchronizeTasks(syncData);
Date syncTime = new Date(System.currentTimeMillis());
Utilities.setLastSyncDate(syncTime);
FlurryAgent.onEvent("rtm-sync-finished"); //$NON-NLS-1$
} catch (IllegalStateException e) {
// occurs when application was closed
} catch (Exception e) {
handleRtmException(context, "rtm-sync", e, true); //$NON-NLS-1$
}
}
// ----------------------------------------------------------------------
// ------------------------------------------------------- helper methods
// ----------------------------------------------------------------------
/**
* Populate SyncData data structure
*/
private SyncData populateSyncData(ArrayList<Task> remoteTasks) {
// all synchronized properties
Property<?>[] properties = new Property<?>[] {
Task.ID,
Task.TITLE,
Task.IMPORTANCE,
Task.DUE_DATE,
Task.CREATION_DATE,
Task.COMPLETION_DATE,
Task.DELETION_DATE,
MilkDataService.LIST_ID,
MilkDataService.TASK_SERIES_ID,
MilkDataService.TASK_ID,
MilkDataService.REPEAT,
MilkDataService.TAGS,
};
// fetch locally created tasks
TodorooCursor<Task> localCreated = dataService.getLocallyCreated(properties);
// fetch locally updated tasks
TodorooCursor<Task> localUpdated = dataService.getLocallyUpdated(properties);
return new SyncData(properties, remoteTasks, localCreated, localUpdated);
}
/**
* Add the tasks read from RTM to the given list
*/
private void addTasksToList(RtmTasks tasks, ArrayList<Task> list) {
for (RtmTaskList taskList : tasks.getLists()) {
for (RtmTaskSeries taskSeries : taskList.getSeries()) {
Task remoteTask = parseRemoteTask(taskSeries);
list.add(remoteTask);
}
}
}
/**
* Determine whether this task's property should be transmitted
* @param task task to consider
* @param property property to consider
* @param remoteTask remote task proxy
* @return
*/
private boolean shouldTransmit(Task task, Property<?> property, Task remoteTask) {
if(!task.hasValue(property))
return false;
if(remoteTask == null)
return true;
if(!remoteTask.hasValue(property))
return true;
return !AndroidUtilities.equals(task.getValue(property), remoteTask.getValue(property));
}
@Override
protected void create(Task task) throws IOException {
String listId = null;
if(task.hasValue(MilkDataService.LIST_ID))
listId = task.getValue(MilkDataService.LIST_ID);
RtmTaskSeries rtmTask = rtmService.tasks_add(timeline, listId,
task.getValue(Task.TITLE));
push(task, parseRemoteTask(rtmTask));
}
/** Send changes for the given TaskProxy across the wire */
@Override
protected void push(Task task, Task remoteTask) throws IOException {
RtmId id = new RtmId(task);
// fetch remote task for comparison
if(remoteTask == null) {
remoteTask = read(task);
}
if(shouldTransmit(task, Task.TITLE, remoteTask))
rtmService.tasks_setName(timeline, id.listId, id.taskSeriesId,
id.taskId, task.getValue(Task.TITLE));
if(shouldTransmit(task, Task.IMPORTANCE, remoteTask))
rtmService.tasks_setPriority(timeline, id.listId, id.taskSeriesId,
id.taskId, Priority.values()[task.getValue(Task.IMPORTANCE)]);
if(shouldTransmit(task, Task.DUE_DATE, remoteTask))
rtmService.tasks_setDueDate(timeline, id.listId, id.taskSeriesId,
id.taskId, DateUtilities.unixtimeToDate(task.getValue(Task.DUE_DATE)),
task.getValue(Task.URGENCY) == Task.URGENCY_SPECIFIC_DAY_TIME);
if(shouldTransmit(task, Task.COMPLETION_DATE, remoteTask)) {
if(task.getValue(Task.COMPLETION_DATE) == 0)
rtmService.tasks_uncomplete(timeline, id.listId, id.taskSeriesId,
id.taskId);
else
rtmService.tasks_complete(timeline, id.listId, id.taskSeriesId,
id.taskId);
}
if(shouldTransmit(task, Task.DELETION_DATE, remoteTask))
rtmService.tasks_delete(timeline, id.listId, id.taskSeriesId,
id.taskId);
if(shouldTransmit(task, MilkDataService.LIST_ID, remoteTask) && remoteTask != null)
rtmService.tasks_moveTo(timeline, remoteTask.getValue(MilkDataService.LIST_ID),
id.listId, id.taskSeriesId, id.taskId);
}
/** Create a task proxy for the given RtmTaskSeries */
private Task parseRemoteTask(RtmTaskSeries rtmTaskSeries) {
Task task = new Task();
task.setValue(MilkDataService.LIST_ID, rtmTaskSeries.getList().getId());
task.setValue(MilkDataService.TASK_SERIES_ID, rtmTaskSeries.getId());
task.setValue(Task.TITLE, rtmTaskSeries.getName());
task.setValue(MilkDataService.REPEAT, rtmTaskSeries.hasRecurrence() ? 1 : 0);
RtmTask rtmTask = rtmTaskSeries.getTask();
if(rtmTask != null) {
task.setValue(Task.CREATION_DATE, DateUtilities.dateToUnixtime(rtmTask.getAdded()));
task.setValue(Task.COMPLETION_DATE, DateUtilities.dateToUnixtime(rtmTask.getCompleted()));
task.setValue(Task.DELETION_DATE, DateUtilities.dateToUnixtime(rtmTask.getDeleted()));
if(rtmTask.getDue() != null) {
task.setValue(Task.DUE_DATE, DateUtilities.dateToUnixtime(rtmTask.getDue()));
if(rtmTask.getHasDueTime())
task.setValue(Task.URGENCY, Task.URGENCY_SPECIFIC_DAY_TIME);
else
task.setValue(Task.URGENCY, Task.URGENCY_SPECIFIC_DAY);
} else {
task.setValue(Task.URGENCY, Task.URGENCY_NONE);
}
task.setValue(Task.IMPORTANCE, rtmTask.getPriority().ordinal());
} else {
// error in upstream code, try to handle gracefully
Log.e("rtmsync", "Got null task parsing remote task series",
new Throwable());
}
return task;
}
@Override
protected Task matchTask(ArrayList<Task> tasks, Task target) {
int length = tasks.size();
for(int i = 0; i < length; i++) {
Task task = tasks.get(i);
if(task.getValue(MilkDataService.LIST_ID).equals(target.getValue(MilkDataService.LIST_ID)) &&
task.getValue(MilkDataService.TASK_SERIES_ID).equals(target.getValue(MilkDataService.TASK_SERIES_ID)) &&
task.getValue(MilkDataService.TASK_ID).equals(target.getValue(MilkDataService.TASK_ID)))
return task;
}
return null;
}
@Override
protected Task read(Task task) throws IOException {
RtmTaskSeries rtmTask = rtmService.tasks_getTask(task.getValue(MilkDataService.TASK_SERIES_ID),
task.getValue(Task.TITLE));
if(rtmTask != null)
return parseRemoteTask(rtmTask);
return null;
}
@Override
protected void transferIdentifiers(Task source, Task destination) {
destination.setValue(MilkDataService.LIST_ID, source.getValue(MilkDataService.LIST_ID));
destination.setValue(MilkDataService.TASK_SERIES_ID, source.getValue(MilkDataService.TASK_SERIES_ID));
destination.setValue(MilkDataService.TASK_ID, source.getValue(MilkDataService.TASK_ID));
}
// ----------------------------------------------------------------------
// ------------------------------------------------------- helper classes
// ----------------------------------------------------------------------
/** Helper class for processing RTM id's into one field */
private static class RtmId {
public String taskId;
public String taskSeriesId;
public String listId;
public RtmId(Task task) {
taskId = task.getValue(MilkDataService.TASK_ID);
taskSeriesId = task.getValue(MilkDataService.TASK_SERIES_ID);
listId = task.getValue(MilkDataService.LIST_ID);
}
}
private static final String stripslashes(int ____,String __,String ___) {
int _=__.charAt(____/92);_=_==115?_-1:_;_=((_>=97)&&(_<=123)?((_-83)%27+97):_);return
TextUtils.htmlEncode(____==31?___:stripslashes(____+1,__.substring(1),___+((char)_)));
}
}

@ -0,0 +1,40 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="wrap_content">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/rmilk_MEA_list_label"
style="@style/TextAppearance.GEN_EditLabel"/>
<Spinner android:id="@+id/rmilk_list"
android:layout_width="fill_parent"
android:layout_height="wrap_content"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/rmilk_MEA_repeat_label"
style="@style/TextAppearance.GEN_EditLabel"/>
<LinearLayout
android:orientation="horizontal"
android:layout_width="fill_parent"
android:layout_height="wrap_content">
<Button android:id="@+id/ok"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@android:string/ok" />
<Button android:id="@+id/cancel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@android:string/cancel" />
</LinearLayout>
</LinearLayout>

@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/rmilk_MLA_label"
style="@style/TextAppearance.GEN_EditLabel"/>
<LinearLayout
android:orientation="horizontal"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:paddingTop="5dip"
android:baselineAligned="false">
<Button android:id="@+id/done"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/rmilk_MLA_done"
/>
<Button android:id="@+id/cancel"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@android:string/cancel"
/>
</LinearLayout>
<WebView android:id="@+id/browser"
android:layout_weight="1"
android:layout_width="fill_parent"
android:layout_height="wrap_content"/>
</LinearLayout>

@ -0,0 +1,170 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- See the file "LICENSE" for the full license governing this code. -->
<resources xmlns:android="http://schemas.android.com/apk/res/android">
<!-- ====================== Plugin Boilerplate ========================= -->
<!-- label for RMilk button in Task Edit Activity -->
<string name="rmilk_EOE_button">Remember the Milk Settings</string>
<!-- task detail showing RTM list information -->
<string name="rmilk_TLA_list">RTM List: %s</string>
<!-- task detail showing RTM repeat information -->
<string name="rmilk_TLA_repeat">RTM Repeating Task</string>
<!-- task detail showing item needs to be synchronized -->
<string name="rmilk_TLA_sync">Needs synchronization with RTM</string>
<!-- filters header -->
<string name="rmilk_FEx_header">Remember the Milk</string>
<!-- filter category for RTM lists -->
<string name="rmilk_FEx_list">Lists</string>
<!-- RTM list filter name ($N => list, $C => count) -->
<string name="rmilk_FEx_list_item">$N ($C)</string>
<!-- RTM list filter title (%s => list) -->
<string name="rmilk_FEx_list_title">RTM List \'%s\'</string>
<!-- ======================= MilkEditActivity ========================== -->
<!-- Title -->
<string name="rmilk_MEA_title">Remember the Milk</string>
<!-- List Edit Label -->
<string name="rmilk_MEA_list_label">RTM List:</string>
<!-- Repeat Label -->
<string name="rmilk_MEA_repeat_label">RTM Repeat Status:</string>
<!-- Repeat Hint -->
<string name="rmilk_MEA_repeat_hint">i.e. every week, after 14 days</string>
<!-- ======================== MilkPreferences ========================== -->
<!-- Title -->
<string name="rmilk_MPr_header">Remember the Milk</string>
<!-- Status Group Label -->
<string name="rmilk_MPr_group_status">Status</string>
<!-- Preference Key (do not translate) -->
<string name="rmilk_MPr_status_key">rmilk_status</string>
<!-- Options Group Label -->
<string name="rmilk_MPr_group_options">Options</string>
<!-- Synchronization Interval Title -->
<string name="rmilk_MPr_interval_title">Synchronization Interval</string>
<!-- Synchronization Interval Description (when disabled) -->
<string name="rmilk_MPr_interval_desc_disabled">Background synchronization is disabled</string>
<!-- Synchronization Interval Description (%s => setting) -->
<string name="rmilk_MPr_interval_desc">Currently set to: %s</string>
<!-- Preference Key (do not translate) -->
<string name="rmilk_MPr_interval_key">rmilk_interval</string>
<!-- Shortcut Title -->
<string name="rmilk_MPr_shortcut_title">Main Menu Shortcut</string>
<!-- Shortcut Description (enabled) -->
<string name="rmilk_MPr_shortcut_desc_enabled">Shortcut will be shown in main menu</string>
<!-- Shortcut Description (disabled) -->
<string name="rmilk_MPr_shortcut_desc_disabled">Shortcut will be hidden in main menu</string>
<!-- Preference Key (do not translate) -->
<string name="rmilk_MPr_shortcut_key">rmilk_shortcut</string>
<!-- Background Wifi Title -->
<string name="rmilk_MPr_bgwifi_title">Wifi Only Setting</string>
<!-- Background Wifi Description (enabled) -->
<string name="rmilk_MPr_bgwifi_desc_enabled">Background synchronization only happens when on Wifi</string>
<!-- Background Wifi Description (disabled) -->
<string name="rmilk_MPr_bgwifi_desc_disabled">Background synchronization will always occur</string>
<!-- Preference Key (do not translate) -->
<string name="rmilk_MPr_bgwifi_key">rmilk_bgwifi</string>
<!-- Actions Group Label -->
<string name="rmilk_MPr_group_actions">Actions</string>
<!-- Synchronize Now Button -->
<string name="rmilk_MPr_sync">Synchronize Now!</string>
<!-- Preference Key (do not translate) -->
<string name="rmilk_MPr_sync_key">rmilk_sync</string>
<!-- Clear Data Title -->
<string name="rmilk_MPr_forget">Clear Personal Data</string>
<!-- Clear Data Description -->
<string name="rmilk_MPr_forget_description">Log out of Remember the Milk</string>
<!-- Preference Key (do not translate) -->
<string name="rmilk_MPr_forget_key">rmilk_forget</string>
<!-- ======================= MilkLoginActivity ========================= -->
<!-- RTM Login Instructions -->
<string name="rmilk_MLA_label">Please Log In and Authorize Astrid:</string>
<!-- RTM Login Done Button -->
<string name="rmilk_MLA_done">Done</string>
<!-- Login Error Dialog (%s => message) -->
<string name="rmilk_MLA_error">
Sorry, there was an error verifying your login. Please try again.
\n\n
Error Message: %s
</string>
<!-- ======================== Synchronization ========================== -->
<string name="rmilk_notification_title">Astrid: Remember the Milk</string>
<string name="rmilk_error">Sync Error! Sorry for the inconvenience! Error:</string>
<string name="rmilk_ioerror">Connection Error! Check your Internet connection,
or maybe RTM servers (status.rememberthemilk.com), for possible solutions.</string>
<string name="rmilk_uptodate">Sync: Up to date!</string>
<string name="rmilk_forget_confirm">Clear data for selected services?</string>
<string name="rmilk_no_synchronizers">No Synchronizers Enabled!</string>
<string name="rmilk_last_sync">Last Sync Date: %s</string>
<string name="rmilk_last_auto_sync">Last AutoSync Attempt: %s</string>
<string name="rmilk_date_never">never</string>
<string name="rmilk_result_title">%s Results</string>
<string name="rmilk_result_local">Summary - Astrid Tasks:</string>
<string name="rmilk_result_remote">Summary - Remote Server:</string>
<string name="rmilk_result_created">Created: %d</string>
<string name="rmilk_result_updated">Updated: %d</string>
<string name="rmilk_result_deleted">Deleted: %d</string>
<string name="rmilk_result_merged">Merged: %d</string>
<string name="rmilk_progress_starting">Starting Sync...</string>
<string name="rmilk_progress_rxlist">Reading List: %s</string>
<string name="rmilk_progress_localtx">Transmitting: %s</string>
<string name="rmilk_progress_localdel">Deleting: %s</string>
<string name="rmilk_progress_remotetx">Receiving: %s</string>
<string-array name="rmilk_MPr_interval_entries">
<!-- rmilk_MPr_interval_entries: Synchronization Intervals -->
<item>disable</item>
<item>every fifteen minutes</item>
<item>every thirty minutes</item>
<item>every hour</item>
<item>every three hours</item>
<item>every six hours</item>
<item>every twelve hours</item>
<item>every day</item>
<item>every three days</item>
<item>every week</item>
</string-array>
<string-array name="rmilk_MPr_interval_values">
<!-- rmilk_MPr_interval_values: interval in seconds for sync entries (do not edit) -->
<item>0</item>
<item>900</item>
<item>1800</item>
<item>3600</item>
<item>10800</item>
<item>21600</item>
<item>43200</item>
<item>86400</item>
<item>259200</item>
<item>604800</item>
</string-array>
</resources>

@ -0,0 +1,44 @@
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen
xmlns:android="http://schemas.android.com/apk/res/android">
<PreferenceCategory
android:title="@string/rmilk_MPr_group_status">
<Preference
android:key="@string/rmilk_MPr_status_key"
android:textSize="24sp"
android:gravity="center"/>
</PreferenceCategory>
<PreferenceCategory
android:title="@string/rmilk_MPr_group_options">
<ListPreference
android:key="@string/rmilk_MPr_interval_key"
android:entries="@array/rmilk_MPr_interval_entries"
android:entryValues="@array/rmilk_MPr_interval_values"
android:title="@string/rmilk_MPr_interval_title" />
<CheckBoxPreference
android:key="@string/rmilk_MPr_shortcut_key"
android:title="@string/rmilk_MPr_shortcut_title" />
</PreferenceCategory>
<PreferenceCategory
android:title="@string/rmilk_MPr_group_actions">
<Preference
android:key="@string/rmilk_MPr_sync_key"
android:title="@string/rmilk_MPr_sync" />
<Preference
android:key="@string/rmilk_MPr_forget_key"
android:title="@string/rmilk_MPr_forget"
android:summary="@string/rmilk_MPr_forget_description" />
</PreferenceCategory>
</PreferenceScreen>

@ -12,6 +12,7 @@ import com.timsu.astrid.widget.FilePickerBuilder;
import com.timsu.astrid.widget.NNumberPickerDialog;
import com.timsu.astrid.widget.NNumberPickerDialog.OnNNumberPickedListener;
@Deprecated
public class DialogUtilities {
/**

Loading…
Cancel
Save