mirror of https://github.com/tasks/tasks
Added Producteev code, but it doesn't work yet
parent
375bc26423
commit
62d55bf50e
@ -0,0 +1,193 @@
|
||||
package com.todoroo.astrid.producteev;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.content.DialogInterface.OnClickListener;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.Color;
|
||||
import android.os.Bundle;
|
||||
import android.preference.Preference;
|
||||
import android.preference.Preference.OnPreferenceClickListener;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup.OnHierarchyChangeListener;
|
||||
|
||||
import com.timsu.astrid.R;
|
||||
import com.todoroo.andlib.service.Autowired;
|
||||
import com.todoroo.andlib.utility.AndroidUtilities;
|
||||
import com.todoroo.andlib.utility.DateUtilities;
|
||||
import com.todoroo.andlib.utility.DialogUtilities;
|
||||
import com.todoroo.andlib.widget.TodorooPreferences;
|
||||
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 ProducteevPreferences extends TodorooPreferences {
|
||||
|
||||
@Autowired
|
||||
private DialogUtilities dialogUtilities;
|
||||
|
||||
private int statusColor = Color.BLACK;
|
||||
|
||||
@Override
|
||||
public int getPreferenceResource() {
|
||||
return R.xml.preferences_rmilk;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
getListView().setOnHierarchyChangeListener(new OnHierarchyChangeListener() {
|
||||
|
||||
@Override
|
||||
public void onChildViewRemoved(View parent, View child) {
|
||||
//
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChildViewAdded(View parent, View child) {
|
||||
View view = findViewById(R.id.status);
|
||||
if(view != null)
|
||||
view.setBackgroundColor(statusColor);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPause() {
|
||||
super.onPause();
|
||||
MilkBackgroundService.scheduleService();
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param resource
|
||||
* if null, updates all resources
|
||||
*/
|
||||
@Override
|
||||
public void updatePreferences(Preference preference, Object value) {
|
||||
final 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]));
|
||||
}
|
||||
|
||||
// status
|
||||
else if (r.getString(R.string.rmilk_MPr_status_key).equals(preference.getKey())) {
|
||||
boolean loggedIn = ProducteevUtilities.isLoggedIn();
|
||||
String status;
|
||||
String subtitle = ""; //$NON-NLS-1$
|
||||
|
||||
// ! logged in - display message, click -> sync
|
||||
if(!loggedIn) {
|
||||
status = r.getString(R.string.rmilk_status_loggedout);
|
||||
statusColor = Color.RED;
|
||||
preference.setOnPreferenceClickListener(new OnPreferenceClickListener() {
|
||||
public boolean onPreferenceClick(Preference p) {
|
||||
startService(new Intent(ProducteevPreferences.this, MilkBackgroundService.class));
|
||||
finish();
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
// sync is occurring
|
||||
else if(ProducteevUtilities.isOngoing()) {
|
||||
status = r.getString(R.string.rmilk_status_ongoing);
|
||||
statusColor = Color.rgb(0, 0, 100);
|
||||
}
|
||||
// last sync was error
|
||||
else if(ProducteevUtilities.getLastAttemptedSyncDate() != 0) {
|
||||
status = r.getString(R.string.rmilk_status_failed,
|
||||
DateUtilities.getDateWithTimeFormat(ProducteevPreferences.this).
|
||||
format(new Date(ProducteevUtilities.getLastAttemptedSyncDate())));
|
||||
if(ProducteevUtilities.getLastSyncDate() > 0) {
|
||||
subtitle = r.getString(R.string.rmilk_status_failed_subtitle,
|
||||
DateUtilities.getDateWithTimeFormat(ProducteevPreferences.this).
|
||||
format(new Date(ProducteevUtilities.getLastSyncDate())));
|
||||
}
|
||||
statusColor = Color.rgb(100, 0, 0);
|
||||
preference.setOnPreferenceClickListener(new OnPreferenceClickListener() {
|
||||
public boolean onPreferenceClick(Preference p) {
|
||||
String error = ProducteevUtilities.getLastError();
|
||||
if(error != null)
|
||||
dialogUtilities.okDialog(ProducteevPreferences.this, error, null);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
} else if(ProducteevUtilities.getLastSyncDate() > 0) {
|
||||
status = r.getString(R.string.rmilk_status_success,
|
||||
DateUtilities.getDateWithTimeFormat(ProducteevPreferences.this).
|
||||
format(new Date(ProducteevUtilities.getLastSyncDate())));
|
||||
statusColor = Color.rgb(0, 100, 0);
|
||||
} else {
|
||||
status = r.getString(R.string.rmilk_status_never);
|
||||
statusColor = Color.rgb(0, 0, 100);
|
||||
preference.setOnPreferenceClickListener(new OnPreferenceClickListener() {
|
||||
public boolean onPreferenceClick(Preference p) {
|
||||
startService(new Intent(ProducteevPreferences.this, MilkBackgroundService.class));
|
||||
finish();
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
preference.setTitle(status);
|
||||
preference.setSummary(subtitle);
|
||||
|
||||
View view = findViewById(R.id.status);
|
||||
if(view != null)
|
||||
view.setBackgroundColor(statusColor);
|
||||
}
|
||||
|
||||
// sync button
|
||||
else if (r.getString(R.string.rmilk_MPr_sync_key).equals(preference.getKey())) {
|
||||
boolean loggedIn = ProducteevUtilities.isLoggedIn();
|
||||
preference.setOnPreferenceClickListener(new OnPreferenceClickListener() {
|
||||
public boolean onPreferenceClick(Preference p) {
|
||||
new RTMSyncProvider().synchronize(ProducteevPreferences.this);
|
||||
finish();
|
||||
return true;
|
||||
}
|
||||
});
|
||||
if(!loggedIn)
|
||||
preference.setTitle(R.string.rmilk_MPr_sync_log_in);
|
||||
}
|
||||
|
||||
// log out button
|
||||
else if (r.getString(R.string.rmilk_MPr_forget_key).equals(preference.getKey())) {
|
||||
boolean loggedIn = ProducteevUtilities.isLoggedIn();
|
||||
preference.setOnPreferenceClickListener(new OnPreferenceClickListener() {
|
||||
public boolean onPreferenceClick(Preference p) {
|
||||
dialogUtilities.okCancelDialog(ProducteevPreferences.this,
|
||||
r.getString(R.string.rmilk_forget_confirm), new OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog,
|
||||
int which) {
|
||||
new RTMSyncProvider().signOut();
|
||||
initializePreference(getPreferenceScreen());
|
||||
}
|
||||
}, null);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
if(!loggedIn)
|
||||
preference.setEnabled(false);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,39 @@
|
||||
package com.todoroo.astrid.producteev;
|
||||
|
||||
import com.todoroo.andlib.data.Property.LongProperty;
|
||||
import com.todoroo.astrid.model.Metadata;
|
||||
import com.todoroo.astrid.producteev.sync.ProducteevTaskContainer;
|
||||
import com.todoroo.astrid.rmilk.data.MilkList;
|
||||
|
||||
/**
|
||||
* Metadata entries for a Remember The Milk Task
|
||||
* @author Tim Su <tim@todoroo.com>
|
||||
*
|
||||
*/
|
||||
public class ProducteevTask {
|
||||
|
||||
/** metadata key */
|
||||
public static final String METADATA_KEY = "producteev"; //$NON-NLS-1$
|
||||
|
||||
/** {@link MilkList} id */
|
||||
public static final LongProperty ID = new LongProperty(Metadata.TABLE,
|
||||
Metadata.VALUE1.name);
|
||||
|
||||
/** RTM Task Series Id */
|
||||
public static final LongProperty DASHBOARD_ID = new LongProperty(Metadata.TABLE,
|
||||
Metadata.VALUE2.name);
|
||||
|
||||
/**
|
||||
* Creates a piece of metadata from a remote task
|
||||
* @param rtmTaskSeries
|
||||
* @return
|
||||
*/
|
||||
public static Metadata create(ProducteevTaskContainer container) {
|
||||
Metadata metadata = new Metadata();
|
||||
metadata.setValue(Metadata.KEY, METADATA_KEY);
|
||||
metadata.setValue(ProducteevTask.ID, container.id);
|
||||
|
||||
return metadata;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,150 @@
|
||||
/**
|
||||
* See the file "LICENSE" for the full license governing this code.
|
||||
*/
|
||||
package com.todoroo.astrid.producteev;
|
||||
|
||||
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.andlib.utility.DateUtilities;
|
||||
|
||||
/**
|
||||
* Constants and preferences for rtm plugin
|
||||
*
|
||||
* @author Tim Su <tim@todoroo.com>
|
||||
*
|
||||
*/
|
||||
@SuppressWarnings("nls")
|
||||
public class ProducteevUtilities {
|
||||
|
||||
// --- constants
|
||||
|
||||
/** add-on identifier */
|
||||
public static final String IDENTIFIER = "producteev";
|
||||
|
||||
// --- Preference Keys
|
||||
|
||||
private static final String PREF_TOKEN = "pdv_token";
|
||||
|
||||
private static final String PREF_LAST_SYNC = "pdv_last_sync";
|
||||
|
||||
private static final String PREF_LAST_SYNC_SERVER = "pdv_last_sync_server";
|
||||
|
||||
private static final String PREF_LAST_ATTEMPTED_SYNC = "pdv_last_attempted";
|
||||
|
||||
private static final String PREF_LAST_ERROR = "pdv_last_error";
|
||||
|
||||
private static final String PREF_ONGOING = "pdv_ongoing";
|
||||
|
||||
// --- 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;
|
||||
}
|
||||
|
||||
/** authentication token, or null if doesn't exist */
|
||||
public static String getToken() {
|
||||
return getPrefs().getString(PREF_TOKEN, null);
|
||||
}
|
||||
|
||||
/** Sets the authentication token. Set to null to clear. */
|
||||
public static void setToken(String setting) {
|
||||
Editor editor = getPrefs().edit();
|
||||
editor.putString(PREF_TOKEN, setting);
|
||||
editor.commit();
|
||||
}
|
||||
|
||||
/** @return Last Successful Sync Date, or 0 */
|
||||
public static long getLastSyncDate() {
|
||||
return getPrefs().getLong(PREF_LAST_SYNC, 0);
|
||||
}
|
||||
|
||||
/** @return Last Attempted Sync Date, or 0 if it was successful */
|
||||
public static long getLastAttemptedSyncDate() {
|
||||
return getPrefs().getLong(PREF_LAST_ATTEMPTED_SYNC, 0);
|
||||
}
|
||||
|
||||
/** @return Last Error, or null if no last error */
|
||||
public static String getLastError() {
|
||||
return getPrefs().getString(PREF_LAST_ERROR, null);
|
||||
}
|
||||
|
||||
/** @return Last sync time according to producteev, or null */
|
||||
public static String getLastServerSyncTime() {
|
||||
return getPrefs().getString(PREF_LAST_SYNC_SERVER, null);
|
||||
}
|
||||
|
||||
/** @return Last Error, or null if no last error */
|
||||
public static boolean isOngoing() {
|
||||
return getPrefs().getBoolean(PREF_ONGOING, false);
|
||||
}
|
||||
|
||||
/** Deletes Last Successful Sync Date */
|
||||
public static void clearLastSyncDate() {
|
||||
Editor editor = getPrefs().edit();
|
||||
editor.remove(PREF_LAST_SYNC);
|
||||
editor.commit();
|
||||
}
|
||||
|
||||
/** Set Last Successful Sync Date */
|
||||
public static void setLastError(String error) {
|
||||
Editor editor = getPrefs().edit();
|
||||
editor.putString(PREF_LAST_ERROR, error);
|
||||
editor.commit();
|
||||
}
|
||||
|
||||
/** Set Ongoing */
|
||||
public static void stopOngoing() {
|
||||
Editor editor = getPrefs().edit();
|
||||
editor.putBoolean(PREF_ONGOING, false);
|
||||
editor.commit();
|
||||
}
|
||||
|
||||
/** Set Last Successful Sync Date */
|
||||
public static void recordSuccessfulSync(String producteevTime) {
|
||||
Editor editor = getPrefs().edit();
|
||||
editor.putLong(PREF_LAST_SYNC, DateUtilities.now());
|
||||
editor.putLong(PREF_LAST_ATTEMPTED_SYNC, 0);
|
||||
editor.putString(PREF_LAST_SYNC_SERVER, producteevTime);
|
||||
editor.commit();
|
||||
}
|
||||
|
||||
/** Set Last Attempted Sync Date */
|
||||
public static void recordSyncStart() {
|
||||
Editor editor = getPrefs().edit();
|
||||
editor.putLong(PREF_LAST_ATTEMPTED_SYNC, DateUtilities.now());
|
||||
editor.putString(PREF_LAST_ERROR, null);
|
||||
editor.putBoolean(PREF_ONGOING, true);
|
||||
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,19 @@
|
||||
package com.todoroo.astrid.producteev.api;
|
||||
|
||||
|
||||
/**
|
||||
* Exception that wraps an exception encountered during API invocation or
|
||||
* processing.
|
||||
*
|
||||
* @author timsu
|
||||
*
|
||||
*/
|
||||
public class ApiResponseParseException extends ApiServiceException {
|
||||
|
||||
private static final long serialVersionUID = 5421855785088364483L;
|
||||
|
||||
public ApiResponseParseException(Throwable throwable) {
|
||||
super("Exception reading API response: " + throwable.getMessage()); //$NON-NLS-1$
|
||||
initCause(throwable);
|
||||
}
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
package com.todoroo.astrid.producteev.api;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
|
||||
/**
|
||||
* Exception that wraps an exception encountered during API invocation or
|
||||
* processing.
|
||||
*
|
||||
* @author timsu
|
||||
*
|
||||
*/
|
||||
public class ApiServiceException extends IOException {
|
||||
|
||||
private static final long serialVersionUID = 8805573304840404684L;
|
||||
|
||||
public ApiServiceException(String detailMessage) {
|
||||
super(detailMessage);
|
||||
}
|
||||
|
||||
public ApiServiceException(Throwable throwable) {
|
||||
super(throwable.getMessage());
|
||||
initCause(throwable);
|
||||
}
|
||||
|
||||
public ApiServiceException() {
|
||||
super();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return getClass().getSimpleName() + ": " + getMessage(); //$NON-NLS-1$
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,55 @@
|
||||
package com.todoroo.astrid.producteev.api;
|
||||
|
||||
import java.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Calendar;
|
||||
import java.util.SimpleTimeZone;
|
||||
|
||||
import com.todoroo.andlib.utility.DateUtilities;
|
||||
|
||||
/**
|
||||
* Utilities for working with API responses and JSON objects
|
||||
*
|
||||
* @author timsu
|
||||
*
|
||||
*/
|
||||
public final class ApiUtilities {
|
||||
|
||||
private static final SimpleDateFormat timeParser = new SimpleDateFormat(
|
||||
"EEE, dd MMM yyyy HH:mm:ss Z"); //$NON-NLS-1$
|
||||
|
||||
static {
|
||||
// read and write dates in UTC
|
||||
Calendar cal = Calendar.getInstance(new SimpleTimeZone(0, "UTC")); //$NON-NLS-1$
|
||||
timeParser.setCalendar(cal);
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility method to convert PDV time to unix time
|
||||
*
|
||||
* @param date
|
||||
* @param defaultValue
|
||||
* @return
|
||||
*/
|
||||
public static long producteevToUnixTime(String value, long defaultValue) {
|
||||
synchronized(timeParser) {
|
||||
try {
|
||||
return DateUtilities.dateToUnixtime(timeParser.parse(value));
|
||||
} catch (ParseException e) {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility method to convert unix time to PDV time
|
||||
* @param time
|
||||
* @return
|
||||
*/
|
||||
public static String unixTimeToProducteev(long time) {
|
||||
synchronized(timeParser) {
|
||||
return timeParser.format(DateUtilities.unixtimeToDate(time));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,312 @@
|
||||
package com.todoroo.astrid.producteev.api;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.math.BigInteger;
|
||||
import java.net.URLEncoder;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Map;
|
||||
import java.util.TreeMap;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import com.todoroo.andlib.service.RestClient;
|
||||
|
||||
@SuppressWarnings("nls")
|
||||
public class ProducteevInvoker {
|
||||
|
||||
private final String URL = "https://api.producteev.com/";
|
||||
|
||||
private final String apiKey;
|
||||
private final String apiSecret;
|
||||
private String token = null;
|
||||
|
||||
/**
|
||||
* Create new producteev service
|
||||
* @param apiKey
|
||||
* @param apiSecret
|
||||
*/
|
||||
public ProducteevInvoker(String apiKey, String apiSecret) {
|
||||
this.apiKey = apiKey;
|
||||
this.apiSecret = apiSecret;
|
||||
}
|
||||
|
||||
// --- authentication
|
||||
|
||||
/**
|
||||
* Authenticate the given user
|
||||
*/
|
||||
public void authenticate(String email, String password) throws IOException, ApiServiceException {
|
||||
JSONObject response = invokeGet("users/login.json",
|
||||
"email", email, "password", password);
|
||||
try {
|
||||
token = response.getJSONObject("login").getString("token");
|
||||
} catch (JSONException e) {
|
||||
throw new ApiServiceException(e);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public boolean hasToken() {
|
||||
return token != null;
|
||||
}
|
||||
|
||||
public void setToken(String token) {
|
||||
this.token = token;
|
||||
}
|
||||
|
||||
// --- tasks
|
||||
|
||||
/**
|
||||
* create a task
|
||||
*
|
||||
* @param title
|
||||
* @param idResponsible (optional)
|
||||
* @param idDashboard (optional);
|
||||
* @param deadline (optional)
|
||||
* @param reminder (optional) 0 = "None", 1 = "5 minutes before", 2 = "15 minutes before", 3 = "30 minutes before", 4 = "1 hour before", 5 = "2 hours before", 6 = "1 day before", 7 = "2 days before", 8 = "on date of event"
|
||||
* @param status (optional) (1 = UNDONE, 2 = DONE)
|
||||
* @param star (optional) (0 to 5 stars)
|
||||
*
|
||||
* @return array tasks/view
|
||||
*/
|
||||
public JSONObject tasksCreate(String title, Integer idResponsible, Integer idDashboard,
|
||||
String deadline, Integer reminder, Integer status, Integer star) throws ApiServiceException, IOException {
|
||||
return invokeGet("tasks/create.json",
|
||||
"token", token,
|
||||
"title", title,
|
||||
"id_responsible", idResponsible,
|
||||
"id_dashboard", idDashboard,
|
||||
"deadline", deadline,
|
||||
"reminder", reminder,
|
||||
"status", status,
|
||||
"star", star);
|
||||
}
|
||||
|
||||
/**
|
||||
* show list
|
||||
*
|
||||
* @param idResponsible (optional) if null return every task for current user
|
||||
* @param since (optional) if not null, the function only returns tasks modified or created since this date
|
||||
*
|
||||
* @return array tasks/view
|
||||
*/
|
||||
public JSONObject tasksShowList(Integer idDashboard, String since) throws ApiServiceException, IOException {
|
||||
return invokeGet("tasks/show_list.json",
|
||||
"token", token,
|
||||
"id_dashboard", idDashboard,
|
||||
"since", since);
|
||||
}
|
||||
|
||||
/**
|
||||
* change title of a task
|
||||
*
|
||||
* @param idTask
|
||||
* @param title
|
||||
*
|
||||
* @return array tasks/view
|
||||
*/
|
||||
public JSONObject tasksSetTitle(int idTask, String title) throws ApiServiceException, IOException {
|
||||
return invokeGet("tasks/set_title.json",
|
||||
"token", token,
|
||||
"id_task", idTask,
|
||||
"title", title);
|
||||
}
|
||||
|
||||
/**
|
||||
* set star status of a task
|
||||
*
|
||||
* @param idTask
|
||||
* @param star (0 to 5 stars)
|
||||
*
|
||||
* @return array tasks/view
|
||||
*/
|
||||
public JSONObject tasksSetStar(int idTask, boolean star) throws ApiServiceException, IOException {
|
||||
return invokeGet("tasks/set_star.json",
|
||||
"token", token,
|
||||
"id_task", idTask,
|
||||
"star", star);
|
||||
}
|
||||
|
||||
/**
|
||||
* set a deadline
|
||||
*
|
||||
* @param idTask
|
||||
* @param deadline
|
||||
*
|
||||
* @return array tasks/view
|
||||
*/
|
||||
public JSONObject tasksSetDeadline(int idTask, String deadline) throws ApiServiceException, IOException {
|
||||
return invokeGet("tasks/set_deadline.json",
|
||||
"token", token,
|
||||
"id_task", idTask,
|
||||
"deadline", deadline);
|
||||
}
|
||||
|
||||
/**
|
||||
* delete a task
|
||||
*
|
||||
* @param idTask
|
||||
*
|
||||
* @return array with the result = (Array("stats" => Array("result" => "TRUE|FALSE"))
|
||||
*/
|
||||
public JSONObject tasksDelete(int idTask) throws ApiServiceException, IOException {
|
||||
return invokeGet("tasks/delete.json",
|
||||
"token", token,
|
||||
"id_task", idTask);
|
||||
}
|
||||
|
||||
/**
|
||||
* get labels assigned to a task
|
||||
*
|
||||
* @param idTask
|
||||
*
|
||||
* @return array: list of labels/view
|
||||
*/
|
||||
public JSONObject tasksLabels(int idTask) throws ApiServiceException, IOException {
|
||||
return invokeGet("tasks/labels.json",
|
||||
"token", token,
|
||||
"id_task", idTask);
|
||||
}
|
||||
|
||||
/**
|
||||
* set a labels to a task
|
||||
*
|
||||
* @param idTask
|
||||
* @param idLabel
|
||||
*
|
||||
* @return array: tasks/view
|
||||
*/
|
||||
public JSONObject tasksSetLabel(int idTask, int idLabel) throws ApiServiceException, IOException {
|
||||
return invokeGet("tasks/set_label.json",
|
||||
"token", token,
|
||||
"id_task", idTask,
|
||||
"id_label", idLabel);
|
||||
}
|
||||
|
||||
/**
|
||||
* set a labels to a task
|
||||
*
|
||||
* @param idTask
|
||||
* @param idLabel
|
||||
*
|
||||
* @return array: tasks/view
|
||||
*/
|
||||
public JSONObject tasksUnsetLabel(int idTask, int idLabel) throws ApiServiceException, IOException {
|
||||
return invokeGet("tasks/unset_label.json",
|
||||
"token", token,
|
||||
"id_task", idTask,
|
||||
"id_label", idLabel);
|
||||
}
|
||||
|
||||
// --- labels
|
||||
|
||||
/**
|
||||
* get every label for a given dashboard
|
||||
*
|
||||
* @param idTask
|
||||
* @param idLabel
|
||||
*
|
||||
* @return array: labels/view
|
||||
*/
|
||||
public JSONObject labelsShowList(int idDashboard, String since) throws ApiServiceException, IOException {
|
||||
return invokeGet("labels/show_list.json",
|
||||
"token", token,
|
||||
"id_dashboard", idDashboard,
|
||||
"since", since);
|
||||
}
|
||||
|
||||
/**
|
||||
* create a task
|
||||
*
|
||||
* @param idDashboard
|
||||
* @param title
|
||||
*
|
||||
* @return array: labels/view
|
||||
*/
|
||||
public JSONObject labelsCreate(int idDashboard, String title) throws ApiServiceException, IOException {
|
||||
return invokeGet("labels/create.json",
|
||||
"token", token,
|
||||
"id_dashboard", idDashboard,
|
||||
"title", title);
|
||||
}
|
||||
|
||||
// --- users
|
||||
|
||||
/**
|
||||
* get a user
|
||||
*
|
||||
* @param idColleague
|
||||
*
|
||||
* @return array information about the user
|
||||
*/
|
||||
public JSONObject usersView(Integer idColleague) throws ApiServiceException, IOException {
|
||||
return invokeGet("users/view.json",
|
||||
"token", token,
|
||||
"id_colleague", idColleague);
|
||||
}
|
||||
|
||||
// --- invocation
|
||||
|
||||
private final RestClient restClient = new ProducteevRestClient();
|
||||
|
||||
/**
|
||||
* Invokes API method using HTTP GET
|
||||
*
|
||||
* @param method
|
||||
* API method to invoke
|
||||
* @param getParameters
|
||||
* Name/Value pairs. Values will be URL encoded.
|
||||
* @return response object
|
||||
*/
|
||||
private JSONObject invokeGet(String method, Object... getParameters)
|
||||
throws IOException, ApiServiceException {
|
||||
try {
|
||||
String request = createFetchUrl(method, getParameters);
|
||||
String response = restClient.get(request);
|
||||
return new JSONObject(response);
|
||||
} catch (JSONException e) {
|
||||
throw new ApiResponseParseException(e);
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a URL for invoking an HTTP GET/POST on the given method
|
||||
* @param method
|
||||
* @param getParameters
|
||||
* @return
|
||||
* @throws UnsupportedEncodingException
|
||||
* @throws NoSuchAlgorithmException
|
||||
*/
|
||||
public String createFetchUrl(String method, Object... getParameters) throws UnsupportedEncodingException, NoSuchAlgorithmException {
|
||||
TreeMap<String, Object> treeMap = new TreeMap<String, Object>();
|
||||
for(int i = 0; i < getParameters.length; i += 2)
|
||||
treeMap.put(getParameters[i].toString(), getParameters[i+1]);
|
||||
treeMap.put("api_key", apiKey);
|
||||
|
||||
StringBuilder requestBuilder = new StringBuilder(URL).append(method).append('?');
|
||||
StringBuilder sigBuilder = new StringBuilder();
|
||||
for(Map.Entry<String, Object> entry : treeMap.entrySet()) {
|
||||
if(entry.getValue() == null)
|
||||
continue;
|
||||
|
||||
String key = entry.getKey();
|
||||
String value = entry.getValue().toString();
|
||||
String encoded = URLEncoder.encode(value, "UTF-8");
|
||||
|
||||
requestBuilder.append(key).append('=').append(encoded).append('&');
|
||||
sigBuilder.append(key).append(value);
|
||||
}
|
||||
|
||||
sigBuilder.append(apiSecret);
|
||||
byte[] digest = MessageDigest.getInstance("MD5").digest(sigBuilder.toString().getBytes("UTF-8"));
|
||||
String signature = new BigInteger(1, digest).toString(16);
|
||||
requestBuilder.append("api_sig").append('=').append(signature);
|
||||
return requestBuilder.toString();
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,160 @@
|
||||
package com.todoroo.astrid.producteev.api;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.lang.ref.WeakReference;
|
||||
|
||||
import org.apache.http.HttpEntity;
|
||||
import org.apache.http.HttpResponse;
|
||||
import org.apache.http.client.HttpClient;
|
||||
import org.apache.http.client.methods.HttpGet;
|
||||
import org.apache.http.client.methods.HttpPost;
|
||||
import org.apache.http.entity.StringEntity;
|
||||
import org.apache.http.impl.client.DefaultHttpClient;
|
||||
import org.apache.http.params.BasicHttpParams;
|
||||
import org.apache.http.params.HttpConnectionParams;
|
||||
import org.apache.http.params.HttpParams;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import com.todoroo.andlib.service.RestClient;
|
||||
|
||||
/**
|
||||
* RestClient allows Android to consume web requests.
|
||||
* <p>
|
||||
* Portions by Praeda:
|
||||
* http://senior.ceng.metu.edu.tr/2009/praeda/2009/01/11/a-simple
|
||||
* -restful-client-at-android/
|
||||
*
|
||||
* @author Tim Su <tim@todoroo.com>
|
||||
*
|
||||
*/
|
||||
public class ProducteevRestClient implements RestClient {
|
||||
|
||||
private static final int HTTP_OK = 200;
|
||||
|
||||
private static final int TIMEOUT_MILLIS = 30000;
|
||||
|
||||
private static WeakReference<HttpClient> httpClient = null;
|
||||
|
||||
private static String convertStreamToString(InputStream is) {
|
||||
/*
|
||||
* To convert the InputStream to String we use the
|
||||
* BufferedReader.readLine() method. We iterate until the BufferedReader
|
||||
* return null which means there's no more data to read. Each line will
|
||||
* appended to a StringBuilder and returned as String.
|
||||
*/
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(is), 16384);
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
String line = null;
|
||||
try {
|
||||
while ((line = reader.readLine()) != null) {
|
||||
sb.append(line + "\n"); //$NON-NLS-1$
|
||||
}
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
} finally {
|
||||
try {
|
||||
is.close();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private synchronized static void initializeHttpClient() {
|
||||
if (httpClient == null || httpClient.get() == null) {
|
||||
HttpParams params = new BasicHttpParams();
|
||||
HttpConnectionParams.setConnectionTimeout(params, TIMEOUT_MILLIS);
|
||||
HttpConnectionParams.setSoTimeout(params, TIMEOUT_MILLIS);
|
||||
httpClient = new WeakReference<HttpClient>(new DefaultHttpClient(params));
|
||||
}
|
||||
}
|
||||
|
||||
private String processHttpResponse(HttpResponse response) throws IOException, ApiServiceException {
|
||||
HttpEntity entity = response.getEntity();
|
||||
String body = null;
|
||||
if (entity != null) {
|
||||
InputStream contentStream = entity.getContent();
|
||||
try {
|
||||
body = convertStreamToString(contentStream);
|
||||
} finally {
|
||||
contentStream.close();
|
||||
}
|
||||
}
|
||||
|
||||
int statusCode = response.getStatusLine().getStatusCode();
|
||||
if(statusCode != HTTP_OK) {
|
||||
try {
|
||||
JSONObject error = new JSONObject(body);
|
||||
String errorMessage = error.getJSONObject("error").getString("message"); //$NON-NLS-1$ //$NON-NLS-2$
|
||||
throw new ApiServiceException(errorMessage);
|
||||
} catch (Exception e) {
|
||||
throw new ApiServiceException(response.getStatusLine() +
|
||||
"\n" + body); //$NON-NLS-1$
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return body;
|
||||
}
|
||||
|
||||
/**
|
||||
* Issue an HTTP GET for the given URL, return the response
|
||||
*
|
||||
* @param url url with url-encoded params
|
||||
* @return response, or null if there was no response
|
||||
* @throws IOException
|
||||
*/
|
||||
public synchronized String get(String url) throws IOException {
|
||||
initializeHttpClient();
|
||||
|
||||
System.err.println("GET: " + url); //$NON-NLS-1$ // (debug)
|
||||
|
||||
try {
|
||||
HttpGet httpGet = new HttpGet(url);
|
||||
HttpResponse response = httpClient.get().execute(httpGet);
|
||||
|
||||
return processHttpResponse(response);
|
||||
} catch (IOException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
IOException ioException = new IOException(e.getMessage());
|
||||
ioException.initCause(e);
|
||||
throw ioException;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Issue an HTTP POST for the given URL, return the response
|
||||
*
|
||||
* @param url
|
||||
* @param data
|
||||
* url-encoded data
|
||||
* @throws IOException
|
||||
*/
|
||||
public synchronized String post(String url, String data) throws IOException {
|
||||
initializeHttpClient();
|
||||
|
||||
System.err.println("POST: " + url); //$NON-NLS-1$ // (debug)
|
||||
|
||||
try {
|
||||
HttpPost httpPost = new HttpPost(url);
|
||||
httpPost.setEntity(new StringEntity(data));
|
||||
HttpResponse response = httpClient.get().execute(httpPost);
|
||||
|
||||
return processHttpResponse(response);
|
||||
} catch (IOException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
IOException ioException = new IOException(e.getMessage());
|
||||
ioException.initCause(e);
|
||||
throw ioException;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,512 @@
|
||||
/**
|
||||
* See the file "LICENSE" for the full license governing this code.
|
||||
*/
|
||||
package com.todoroo.astrid.producteev.sync;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
|
||||
import org.json.JSONObject;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.res.Resources;
|
||||
import android.text.TextUtils;
|
||||
|
||||
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.andlib.utility.DialogUtilities;
|
||||
import com.todoroo.astrid.api.SynchronizationProvider;
|
||||
import com.todoroo.astrid.api.TaskContainer;
|
||||
import com.todoroo.astrid.model.Metadata;
|
||||
import com.todoroo.astrid.model.Task;
|
||||
import com.todoroo.astrid.producteev.ProducteevUtilities;
|
||||
import com.todoroo.astrid.producteev.api.ProducteevInvoker;
|
||||
import com.todoroo.astrid.rmilk.MilkPreferences;
|
||||
import com.todoroo.astrid.rmilk.MilkUtilities;
|
||||
import com.todoroo.astrid.rmilk.api.ServiceInternalException;
|
||||
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.RtmTask.Priority;
|
||||
import com.todoroo.astrid.rmilk.data.MilkDataService;
|
||||
import com.todoroo.astrid.rmilk.data.MilkNote;
|
||||
import com.todoroo.astrid.service.AstridDependencyInjector;
|
||||
import com.todoroo.astrid.tags.TagService;
|
||||
import com.todoroo.astrid.utility.Preferences;
|
||||
|
||||
public class ProducteevSyncProvider extends SynchronizationProvider<ProducteevTaskContainer> {
|
||||
|
||||
private MilkDataService dataService = null;
|
||||
private ProducteevInvoker invoker = null;
|
||||
private int defaultDashboard;
|
||||
|
||||
static {
|
||||
AstridDependencyInjector.initialize();
|
||||
}
|
||||
|
||||
@Autowired
|
||||
protected ExceptionService exceptionService;
|
||||
|
||||
@Autowired
|
||||
protected DialogUtilities dialogUtilities;
|
||||
|
||||
public ProducteevSyncProvider() {
|
||||
super();
|
||||
DependencyInjectionService.getInstance().inject(this);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
// ------------------------------------------------------- public methods
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Sign out of RTM, deleting all synchronization metadata
|
||||
*/
|
||||
public void signOut() {
|
||||
ProducteevUtilities.setToken(null);
|
||||
ProducteevUtilities.clearLastSyncDate();
|
||||
|
||||
dataService.clearMetadata(); // TODO clear metadata
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
// ------------------------------------------------------- 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 showError
|
||||
* whether to display a dialog
|
||||
*/
|
||||
@Override
|
||||
protected void handleException(String tag, Exception e, boolean showError) {
|
||||
ProducteevUtilities.setLastError(e.toString());
|
||||
|
||||
// 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(showError) {
|
||||
Context context = ContextManager.getContext();
|
||||
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(showError) {
|
||||
Context context = ContextManager.getContext();
|
||||
showError(context, e, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initiate(Context context) {
|
||||
dataService = MilkDataService.getInstance();
|
||||
|
||||
// authenticate the user. this will automatically call the next step
|
||||
authenticate(context);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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("producteev-started");
|
||||
|
||||
ProducteevUtilities.recordSyncStart();
|
||||
|
||||
try {
|
||||
String authToken = ProducteevUtilities.getToken();
|
||||
|
||||
String z = stripslashes(0, "71o3346pr40o5o4nt4n7t6n287t4op28","2");
|
||||
String v = stripslashes(2, "9641n76n9s1736q1578q1o1337q19233","4ae");
|
||||
invoker = new ProducteevInvoker(z, v);
|
||||
|
||||
// check if we have a token & it works
|
||||
if(authToken != null) {
|
||||
invoker.setToken(authToken);
|
||||
}
|
||||
|
||||
if(authToken == null) {
|
||||
String email = Preferences.getStringValue(R.string.producteev_PPr_email);
|
||||
String password = Preferences.getStringValue(R.string.producteev_PPr_password);
|
||||
|
||||
invoker.authenticate(email, password);
|
||||
}
|
||||
|
||||
performSync();
|
||||
} catch (IllegalStateException e) {
|
||||
// occurs when application was closed
|
||||
} catch (Exception e) {
|
||||
handleException("rtm-authenticate", e, true);
|
||||
} finally {
|
||||
ProducteevUtilities.stopOngoing();
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
// ----------------------------------------------------- synchronization!
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
@SuppressWarnings("nls")
|
||||
protected void performSync() {
|
||||
try {
|
||||
// load user information
|
||||
JSONObject user = invoker.usersView(null);
|
||||
defaultDashboard = user.getJSONObject("user").getInt("default_dashboard");
|
||||
|
||||
// read all tasks
|
||||
JSONObject tasks = invoker.tasksShowList(defaultDashboard,
|
||||
ProducteevUtilities.getLastServerSyncTime());
|
||||
|
||||
SyncData<ProducteevTaskContainer> syncData = populateSyncData(tasks);
|
||||
try {
|
||||
synchronizeTasks(syncData);
|
||||
} finally {
|
||||
syncData.localCreated.close();
|
||||
syncData.localUpdated.close();
|
||||
}
|
||||
|
||||
MilkUtilities.recordSuccessfulSync();
|
||||
|
||||
FlurryAgent.onEvent("rtm-sync-finished"); //$NON-NLS-1$
|
||||
} catch (IllegalStateException e) {
|
||||
// occurs when application was closed
|
||||
} catch (Exception e) {
|
||||
handleException("rtm-sync", e, true); //$NON-NLS-1$
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
// ------------------------------------------------------------ sync data
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
// all synchronized properties
|
||||
private static final Property<?>[] PROPERTIES = new Property<?>[] {
|
||||
Task.ID,
|
||||
Task.TITLE,
|
||||
Task.IMPORTANCE,
|
||||
Task.DUE_DATE,
|
||||
Task.CREATION_DATE,
|
||||
Task.COMPLETION_DATE,
|
||||
Task.DELETION_DATE,
|
||||
Task.NOTES,
|
||||
};
|
||||
|
||||
/**
|
||||
* Populate SyncData data structure
|
||||
*/
|
||||
private SyncData<ProducteevTaskContainer> populateSyncData(ArrayList<ProducteevTaskContainer> remoteTasks) {
|
||||
// fetch locally created tasks
|
||||
TodorooCursor<Task> localCreated = dataService.getLocallyCreated(PROPERTIES);
|
||||
|
||||
// fetch locally updated tasks
|
||||
TodorooCursor<Task> localUpdated = dataService.getLocallyUpdated(PROPERTIES);
|
||||
|
||||
return new SyncData<ProducteevTaskContainer>(remoteTasks, localCreated, localUpdated);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the tasks read from RTM to the given list
|
||||
*/
|
||||
private void addTasksToList(RtmTasks tasks, ArrayList<ProducteevTaskContainer> list) {
|
||||
for (RtmTaskList taskList : tasks.getLists()) {
|
||||
for (RtmTaskSeries taskSeries : taskList.getSeries()) {
|
||||
ProducteevTaskContainer remoteTask = parseRemoteTask(taskSeries);
|
||||
dataService.findLocalMatch(remoteTask);
|
||||
list.add(remoteTask);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
// ------------------------------------------------- create / push / pull
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
@Override
|
||||
protected void create(ProducteevTaskContainer task) throws IOException {
|
||||
String listId = null;
|
||||
if(task.dashboard > 0)
|
||||
listId = Long.toString(task.listId);
|
||||
RtmTaskSeries rtmTask = rtmService.tasks_add(timeline, listId,
|
||||
task.task.getValue(Task.TITLE));
|
||||
ProducteevTaskContainer newRemoteTask = parseRemoteTask(rtmTask);
|
||||
transferIdentifiers(newRemoteTask, task);
|
||||
push(task, newRemoteTask);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether this task's property should be transmitted
|
||||
* @param task task to consider
|
||||
* @param property property to consider
|
||||
* @param remoteTask remote task proxy
|
||||
* @return
|
||||
*/
|
||||
private boolean shouldTransmit(TaskContainer task, Property<?> property, TaskContainer remoteTask) {
|
||||
if(!task.task.containsValue(property))
|
||||
return false;
|
||||
|
||||
if(remoteTask == null)
|
||||
return true;
|
||||
if(!remoteTask.task.containsValue(property))
|
||||
return true;
|
||||
|
||||
// special cases - match if they're zero or nonzero
|
||||
if(property == Task.COMPLETION_DATE ||
|
||||
property == Task.DELETION_DATE)
|
||||
return !AndroidUtilities.equals((Long)task.task.getValue(property) == 0,
|
||||
(Long)remoteTask.task.getValue(property) == 0);
|
||||
|
||||
return !AndroidUtilities.equals(task.task.getValue(property),
|
||||
remoteTask.task.getValue(property));
|
||||
}
|
||||
|
||||
/**
|
||||
* Send changes for the given Task across the wire. If a remoteTask is
|
||||
* supplied, we attempt to intelligently only transmit the values that
|
||||
* have changed.
|
||||
*/
|
||||
@Override
|
||||
protected void push(ProducteevTaskContainer local, ProducteevTaskContainer remote) throws IOException {
|
||||
boolean remerge = false;
|
||||
|
||||
// fetch remote task for comparison
|
||||
if(remote == null)
|
||||
remote = pull(local);
|
||||
|
||||
String listId = Long.toString(local.listId);
|
||||
String taskSeriesId = Long.toString(local.taskSeriesId);
|
||||
String taskId = Long.toString(local.taskId);
|
||||
|
||||
if(remote != null && !AndroidUtilities.equals(local.listId, remote.listId))
|
||||
rtmService.tasks_moveTo(timeline, Long.toString(remote.listId),
|
||||
listId, taskSeriesId, taskId);
|
||||
|
||||
// either delete or re-create if necessary
|
||||
if(shouldTransmit(local, Task.DELETION_DATE, remote)) {
|
||||
if(local.task.getValue(Task.DELETION_DATE) > 0)
|
||||
rtmService.tasks_delete(timeline, listId, taskSeriesId, taskId);
|
||||
else if(remote == null) {
|
||||
RtmTaskSeries rtmTask = rtmService.tasks_add(timeline, listId,
|
||||
local.task.getValue(Task.TITLE));
|
||||
remote = parseRemoteTask(rtmTask);
|
||||
transferIdentifiers(remote, local);
|
||||
}
|
||||
}
|
||||
|
||||
if(shouldTransmit(local, Task.TITLE, remote))
|
||||
rtmService.tasks_setName(timeline, listId, taskSeriesId,
|
||||
taskId, local.task.getValue(Task.TITLE));
|
||||
if(shouldTransmit(local, Task.IMPORTANCE, remote))
|
||||
rtmService.tasks_setPriority(timeline, listId, taskSeriesId,
|
||||
taskId, Priority.values()[local.task.getValue(Task.IMPORTANCE)]);
|
||||
if(shouldTransmit(local, Task.DUE_DATE, remote))
|
||||
rtmService.tasks_setDueDate(timeline, listId, taskSeriesId,
|
||||
taskId, DateUtilities.unixtimeToDate(local.task.getValue(Task.DUE_DATE)),
|
||||
local.task.hasDueTime());
|
||||
if(shouldTransmit(local, Task.COMPLETION_DATE, remote)) {
|
||||
if(local.task.getValue(Task.COMPLETION_DATE) == 0)
|
||||
rtmService.tasks_uncomplete(timeline, listId, taskSeriesId,
|
||||
taskId);
|
||||
else {
|
||||
rtmService.tasks_complete(timeline, listId, taskSeriesId,
|
||||
taskId);
|
||||
// if repeating, pull and merge
|
||||
if(local.repeating)
|
||||
remerge = true;
|
||||
}
|
||||
}
|
||||
|
||||
// tags
|
||||
HashSet<String> localTags = new HashSet<String>();
|
||||
HashSet<String> remoteTags = new HashSet<String>();
|
||||
for(Metadata item : local.metadata)
|
||||
if(TagService.KEY.equals(item.getValue(Metadata.KEY)))
|
||||
localTags.add(item.getValue(TagService.TAG));
|
||||
if(remote != null && remote.metadata != null) {
|
||||
for(Metadata item : remote.metadata)
|
||||
if(TagService.KEY.equals(item.getValue(Metadata.KEY)))
|
||||
remoteTags.add(item.getValue(TagService.TAG));
|
||||
}
|
||||
if(!localTags.equals(remoteTags)) {
|
||||
String[] tags = localTags.toArray(new String[localTags.size()]);
|
||||
rtmService.tasks_setTags(timeline, listId, taskSeriesId,
|
||||
taskId, tags);
|
||||
}
|
||||
|
||||
// notes
|
||||
if(shouldTransmit(local, Task.NOTES, remote)) {
|
||||
String[] titleAndText = MilkNote.fromNoteField(local.task.getValue(Task.NOTES));
|
||||
List<RtmTaskNote> notes = null;
|
||||
if(remote != null && remote.remote.getNotes() != null)
|
||||
notes = remote.remote.getNotes().getNotes();
|
||||
if(notes != null && notes.size() > 0) {
|
||||
String remoteNoteId = notes.get(0).getId();
|
||||
rtmService.tasks_notes_edit(timeline, remoteNoteId, titleAndText[0],
|
||||
titleAndText[1]);
|
||||
} else {
|
||||
rtmService.tasks_notes_add(timeline, listId, taskSeriesId,
|
||||
taskId, titleAndText[0], titleAndText[1]);
|
||||
}
|
||||
}
|
||||
|
||||
if(remerge) {
|
||||
remote = pull(local);
|
||||
remote.task.setId(local.task.getId());
|
||||
write(remote);
|
||||
}
|
||||
}
|
||||
|
||||
/** Create a task container for the given RtmTaskSeries */
|
||||
private ProducteevTaskContainer parseRemoteTask(RtmTaskSeries rtmTaskSeries) {
|
||||
Task task = new Task();
|
||||
RtmTask rtmTask = rtmTaskSeries.getTask();
|
||||
ArrayList<Metadata> metadata = new ArrayList<Metadata>();
|
||||
|
||||
task.setValue(Task.TITLE, rtmTaskSeries.getName());
|
||||
task.setValue(Task.CREATION_DATE, DateUtilities.dateToUnixtime(rtmTask.getAdded()));
|
||||
task.setValue(Task.COMPLETION_DATE, DateUtilities.dateToUnixtime(rtmTask.getCompleted()));
|
||||
task.setValue(Task.DELETION_DATE, DateUtilities.dateToUnixtime(rtmTask.getDeleted()));
|
||||
if(rtmTask.getDue() != null) {
|
||||
task.setValue(Task.DUE_DATE,
|
||||
task.createDueDate(rtmTask.getHasDueTime() ? Task.URGENCY_SPECIFIC_DAY_TIME :
|
||||
Task.URGENCY_SPECIFIC_DAY, DateUtilities.dateToUnixtime(rtmTask.getDue())));
|
||||
} else {
|
||||
task.setValue(Task.DUE_DATE, 0L);
|
||||
}
|
||||
task.setValue(Task.IMPORTANCE, rtmTask.getPriority().ordinal());
|
||||
|
||||
if(rtmTaskSeries.getTags() != null) {
|
||||
for(String tag : rtmTaskSeries.getTags()) {
|
||||
Metadata tagData = new Metadata();
|
||||
tagData.setValue(Metadata.KEY, TagService.KEY);
|
||||
tagData.setValue(TagService.TAG, tag);
|
||||
metadata.add(tagData);
|
||||
}
|
||||
}
|
||||
|
||||
task.setValue(Task.NOTES, ""); //$NON-NLS-1$
|
||||
if(rtmTaskSeries.getNotes() != null && rtmTaskSeries.getNotes().getNotes().size() > 0) {
|
||||
boolean firstNote = true;
|
||||
Collections.reverse(rtmTaskSeries.getNotes().getNotes()); // reverse so oldest is first
|
||||
for(RtmTaskNote note : rtmTaskSeries.getNotes().getNotes()) {
|
||||
if(firstNote) {
|
||||
firstNote = false;
|
||||
task.setValue(Task.NOTES, MilkNote.toNoteField(note));
|
||||
} else
|
||||
metadata.add(MilkNote.create(note));
|
||||
}
|
||||
}
|
||||
|
||||
ProducteevTaskContainer container = new ProducteevTaskContainer(task, metadata, rtmTaskSeries);
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ProducteevTaskContainer pull(ProducteevTaskContainer task) throws IOException {
|
||||
if(task.taskSeriesId == 0)
|
||||
throw new ServiceInternalException("Tried to read an invalid task"); //$NON-NLS-1$
|
||||
RtmTaskSeries rtmTask = rtmService.tasks_getTask(Long.toString(task.taskSeriesId),
|
||||
task.task.getValue(Task.TITLE));
|
||||
if(rtmTask != null)
|
||||
return parseRemoteTask(rtmTask);
|
||||
return null;
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
// --------------------------------------------------------- read / write
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
@Override
|
||||
protected ProducteevTaskContainer read(TodorooCursor<Task> cursor) throws IOException {
|
||||
return dataService.readTaskAndMetadata(cursor);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void write(ProducteevTaskContainer task) throws IOException {
|
||||
dataService.saveTaskAndMetadata(task);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
// --------------------------------------------------------- misc helpers
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
@Override
|
||||
protected int matchTask(ArrayList<ProducteevTaskContainer> tasks, ProducteevTaskContainer target) {
|
||||
int length = tasks.size();
|
||||
for(int i = 0; i < length; i++) {
|
||||
ProducteevTaskContainer task = tasks.get(i);
|
||||
if(AndroidUtilities.equals(task.listId, target.listId) &&
|
||||
AndroidUtilities.equals(task.taskSeriesId, target.taskSeriesId) &&
|
||||
AndroidUtilities.equals(task.taskId, target.taskId))
|
||||
return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateNotification(Context context, Notification notification) {
|
||||
String notificationTitle = context.getString(R.string.rmilk_notification_title);
|
||||
Intent intent = new Intent(context, MilkPreferences.class);
|
||||
PendingIntent notificationIntent = PendingIntent.getActivity(context, 0,
|
||||
intent, 0);
|
||||
notification.setLatestEventInfo(context,
|
||||
notificationTitle, context.getString(R.string.SyP_progress),
|
||||
notificationIntent);
|
||||
return ;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void transferIdentifiers(ProducteevTaskContainer source,
|
||||
ProducteevTaskContainer destination) {
|
||||
destination.listId = source.listId;
|
||||
destination.taskSeriesId = source.taskSeriesId;
|
||||
destination.taskId = source.taskId;
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
// ------------------------------------------------------- helper classes
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
private static final String stripslashes(int ____,String __,String ___) {
|
||||
int _=__.charAt(____/92);_=_==116?_-1:_;_=((_>=97)&&(_<=123)?
|
||||
((_-83)%27+97):_);return TextUtils.htmlEncode(____==31?___:
|
||||
stripslashes(____+1,__.substring(1),___+((char)_)));
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
package com.todoroo.astrid.producteev.sync;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
|
||||
import com.todoroo.astrid.api.TaskContainer;
|
||||
import com.todoroo.astrid.model.Metadata;
|
||||
import com.todoroo.astrid.model.Task;
|
||||
import com.todoroo.astrid.producteev.ProducteevTask;
|
||||
|
||||
/**
|
||||
* RTM Task Container
|
||||
*
|
||||
* @author Tim Su <tim@todoroo.com>
|
||||
*
|
||||
*/
|
||||
public class ProducteevTaskContainer extends TaskContainer {
|
||||
public long id;
|
||||
public long dashboard;
|
||||
|
||||
public ProducteevTaskContainer(Task task, ArrayList<Metadata> metadata, long id, long dashboard) {
|
||||
this.task = task;
|
||||
this.metadata = metadata;
|
||||
this.id = id;
|
||||
this.dashboard = dashboard;
|
||||
}
|
||||
|
||||
// public ProducteevTaskContainer(Task task, ArrayList<Metadata> metadata,
|
||||
// RtmTaskSeries rtmTaskSeries) {
|
||||
// this(task, metadata, );
|
||||
// }
|
||||
|
||||
public ProducteevTaskContainer(Task task, ArrayList<Metadata> metadata) {
|
||||
this(task, metadata, 0, 0);
|
||||
for(Iterator<Metadata> iterator = metadata.iterator(); iterator.hasNext(); ) {
|
||||
Metadata item = iterator.next();
|
||||
if(ProducteevTask.METADATA_KEY.equals(item.getValue(Metadata.KEY))) {
|
||||
if(item.containsNonNullValue(ProducteevTask.ID))
|
||||
id = item.getValue(ProducteevTask.ID);
|
||||
if(item.containsNonNullValue(ProducteevTask.DASHBOARD_ID))
|
||||
dashboard = item.getValue(ProducteevTask.DASHBOARD_ID);
|
||||
iterator.remove();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
Loading…
Reference in New Issue