Now showing producteev notes. Made PDV more resistant to errors by retrying once, and fixed some bugs with labels and notes and such

pull/14/head
Tim Su 14 years ago
parent 385db6235f
commit 8fabc2435c

@ -2,6 +2,7 @@ package com.todoroo.astrid.api;
import java.util.ArrayList;
import com.todoroo.andlib.utility.AndroidUtilities;
import com.todoroo.astrid.common.SyncProvider;
import com.todoroo.astrid.model.Metadata;
import com.todoroo.astrid.model.Task;
@ -17,4 +18,18 @@ import com.todoroo.astrid.model.Task;
public class TaskContainer {
public Task task;
public ArrayList<Metadata> metadata;
/**
* Check if the metadata contains anything with the given key
* @param key
* @return first match. or null
*/
public Metadata findMetadata(String key) {
for(Metadata item : metadata) {
if(AndroidUtilities.equals(key, item.getValue(Metadata.KEY)))
return item;
}
return null;
}
}

@ -4,7 +4,7 @@
<booleanAttribute key="ch.zork.quicklaunch" value="true"/>
<stringAttribute key="ch.zork.quicklaunch.icon" value="14.gif"/>
<intAttribute key="ch.zork.quicklaunch.index" value="0"/>
<stringAttribute key="ch.zork.quicklaunch.mode" value="debug"/>
<stringAttribute key="ch.zork.quicklaunch.mode" value="run"/>
<intAttribute key="com.android.ide.eclipse.adt.action" value="0"/>
<stringAttribute key="com.android.ide.eclipse.adt.commandline" value="-scale 0.7"/>
<intAttribute key="com.android.ide.eclipse.adt.delay" value="0"/>

@ -24,6 +24,7 @@ import com.todoroo.andlib.service.NotificationManager;
import com.todoroo.astrid.api.TaskContainer;
import com.todoroo.astrid.model.Task;
import com.todoroo.astrid.utility.Constants;
import com.todoroo.astrid.utility.Flags;
/**
* A helper class for writing synchronization services for Astrid. This class
@ -208,8 +209,8 @@ public abstract class SyncProvider<TYPE extends TaskContainer> {
length = data.localCreated.getCount();
for(int i = 0; i < length; i++) {
data.localCreated.moveToNext();
TYPE local = read(data.localCreated);
try {
TYPE local = read(data.localCreated);
String taskTitle = local.task.getValue(Task.TITLE);
@ -232,18 +233,18 @@ public abstract class SyncProvider<TYPE extends TaskContainer> {
} else {
create(local);
}
write(local);
} catch (Exception e) {
handleException("sync-local-created", e, false); //$NON-NLS-1$
}
write(local);
}
// 2. UPDATE: for each updated local task
length = data.localUpdated.getCount();
for(int i = 0; i < length; i++) {
data.localUpdated.moveToNext();
TYPE local = read(data.localUpdated);
try {
TYPE local = read(data.localUpdated);
if(local.task == null)
continue;
@ -260,10 +261,10 @@ public abstract class SyncProvider<TYPE extends TaskContainer> {
} else {
push(local, null);
}
write(local);
} catch (Exception e) {
handleException("sync-local-updated", e, false); //$NON-NLS-1$
}
write(local);
}
// 3. REMOTE: load remote information
@ -306,6 +307,8 @@ public abstract class SyncProvider<TYPE extends TaskContainer> {
handleException("sync-remote-updated", e, false); //$NON-NLS-1$
}
}
Flags.set(Flags.REFRESH);
}
// --- helper classes

@ -0,0 +1,79 @@
/**
* See the file "LICENSE" for the full license governing this code.
*/
package com.todoroo.astrid.producteev;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import com.todoroo.andlib.data.TodorooCursor;
import com.todoroo.astrid.adapter.TaskAdapter;
import com.todoroo.astrid.api.AstridApiConstants;
import com.todoroo.astrid.api.DetailExposer;
import com.todoroo.astrid.model.Metadata;
import com.todoroo.astrid.producteev.sync.ProducteevDataService;
import com.todoroo.astrid.producteev.sync.ProducteevNote;
/**
* Exposes Task Details for Producteev:
* - notes
*
* @author Tim Su <tim@todoroo.com>
*
*/
public class ProducteevDetailExposer extends BroadcastReceiver implements DetailExposer{
@Override
public void onReceive(Context context, Intent intent) {
// if we aren't logged in, don't expose features
if(!ProducteevUtilities.INSTANCE.isLoggedIn())
return;
long taskId = intent.getLongExtra(AstridApiConstants.EXTRAS_TASK_ID, -1);
if(taskId == -1)
return;
boolean extended = intent.getBooleanExtra(AstridApiConstants.EXTRAS_EXTENDED, false);
String taskDetail = getTaskDetails(context, taskId, extended);
if(taskDetail == null)
return;
Intent broadcastIntent = new Intent(AstridApiConstants.BROADCAST_SEND_DETAILS);
broadcastIntent.putExtra(AstridApiConstants.EXTRAS_ADDON, ProducteevUtilities.IDENTIFIER);
broadcastIntent.putExtra(AstridApiConstants.EXTRAS_TASK_ID, taskId);
broadcastIntent.putExtra(AstridApiConstants.EXTRAS_EXTENDED, extended);
broadcastIntent.putExtra(AstridApiConstants.EXTRAS_RESPONSE, taskDetail);
context.sendBroadcast(broadcastIntent, AstridApiConstants.PERMISSION_READ);
}
@Override
public String getTaskDetails(Context context, long id, boolean extended) {
if(!extended)
return null;
StringBuilder builder = new StringBuilder();
TodorooCursor<Metadata> notesCursor = ProducteevDataService.getInstance().getTaskNotesCursor(id);
try {
Metadata metadata = new Metadata();
for(notesCursor.moveToFirst(); !notesCursor.isAfterLast(); notesCursor.moveToNext()) {
metadata.readFromCursor(notesCursor);
builder.append(metadata.getValue(ProducteevNote.MESSAGE)).append(TaskAdapter.DETAIL_SEPARATOR);
}
} finally {
notesCursor.close();
}
if(builder.length() == 0)
return null;
String result = builder.toString();
return result.substring(0, result.length() - TaskAdapter.DETAIL_SEPARATOR.length());
}
@Override
public String getPluginIdentifier() {
return ProducteevUtilities.IDENTIFIER;
}
}

@ -14,11 +14,14 @@ import com.todoroo.astrid.common.SyncProviderUtilities;
*/
public class ProducteevUtilities extends SyncProviderUtilities {
/** add-on identifier */
public static final String IDENTIFIER = "pdv"; //$NON-NLS-1$
public static final ProducteevUtilities INSTANCE = new ProducteevUtilities();
@Override
public String getIdentifier() {
return "pdv"; //$NON-NLS-1$
return IDENTIFIER;
}
@Override
@ -42,5 +45,8 @@ public class ProducteevUtilities extends SyncProviderUtilities {
editor.commit();
}
private ProducteevUtilities() {
//
}
}

@ -22,6 +22,10 @@ public class ProducteevInvoker {
private final String apiKey;
private final String apiSecret;
/** saved credentials in case we need to re-log in */
private String retryEmail;
private String retryPassword;
private String token = null;
/**
@ -40,6 +44,8 @@ public class ProducteevInvoker {
* Authenticate the given user
*/
public void authenticate(String email, String password) throws IOException, ApiServiceException {
retryEmail = email;
retryPassword = password;
JSONObject response = invokeGet("users/login.json",
"email", email, "password", password);
try {
@ -54,8 +60,10 @@ public class ProducteevInvoker {
return token != null;
}
public void setToken(String token) {
public void setCredentials(String token, String email, String password) {
this.token = token;
retryEmail = email;
retryPassword = password;
}
public String getToken() {
@ -92,7 +100,7 @@ public class ProducteevInvoker {
*/
public JSONObject tasksCreate(String title, Long idResponsible, Long idDashboard,
String deadline, Integer reminder, Integer status, Integer star) throws ApiServiceException, IOException {
return invokeGet("tasks/create.json",
return callAuthenticated("tasks/create.json",
"token", token,
"title", title,
"id_responsible", idResponsible,
@ -112,7 +120,7 @@ public class ProducteevInvoker {
* @return array tasks/view
*/
public JSONArray tasksShowList(Long idDashboard, String since) throws ApiServiceException, IOException {
return getResponse(invokeGet("tasks/show_list.json",
return getResponse(callAuthenticated("tasks/show_list.json",
"token", token,
"id_dashboard", idDashboard,
"since", since), "tasks");
@ -126,7 +134,7 @@ public class ProducteevInvoker {
* @return array tasks/view
*/
public JSONObject tasksView(Long idTask) throws ApiServiceException, IOException {
return invokeGet("tasks/view.json",
return callAuthenticated("tasks/view.json",
"token", token,
"id_task", idTask);
}
@ -140,7 +148,7 @@ public class ProducteevInvoker {
* @return array tasks/view
*/
public JSONObject tasksSetTitle(long idTask, String title) throws ApiServiceException, IOException {
return invokeGet("tasks/set_title.json",
return callAuthenticated("tasks/set_title.json",
"token", token,
"id_task", idTask,
"title", title);
@ -155,7 +163,7 @@ public class ProducteevInvoker {
* @return array tasks/view
*/
public JSONObject tasksSetStatus(long idTask, int status) throws ApiServiceException, IOException {
return invokeGet("tasks/set_star.json",
return callAuthenticated("tasks/set_star.json",
"token", token,
"id_task", idTask,
"status", status);
@ -170,7 +178,7 @@ public class ProducteevInvoker {
* @return array tasks/view
*/
public JSONObject tasksSetStar(long idTask, int star) throws ApiServiceException, IOException {
return invokeGet("tasks/set_star.json",
return callAuthenticated("tasks/set_star.json",
"token", token,
"id_task", idTask,
"star", star);
@ -185,7 +193,7 @@ public class ProducteevInvoker {
* @return array tasks/view
*/
public JSONObject tasksSetDeadline(long idTask, String deadline) throws ApiServiceException, IOException {
return invokeGet("tasks/set_deadline.json",
return callAuthenticated("tasks/set_deadline.json",
"token", token,
"id_task", idTask,
"deadline", deadline);
@ -199,7 +207,7 @@ public class ProducteevInvoker {
* @return array with the result = (Array("stats" => Array("result" => "TRUE|FALSE"))
*/
public JSONObject tasksDelete(long idTask) throws ApiServiceException, IOException {
return invokeGet("tasks/delete.json",
return callAuthenticated("tasks/delete.json",
"token", token,
"id_task", idTask);
}
@ -212,7 +220,7 @@ public class ProducteevInvoker {
* @return array: list of labels/view
*/
public JSONArray tasksLabels(long idTask) throws ApiServiceException, IOException {
return getResponse(invokeGet("tasks/labels.json",
return getResponse(callAuthenticated("tasks/labels.json",
"token", token,
"id_task", idTask), "labels");
}
@ -225,16 +233,11 @@ public class ProducteevInvoker {
*
* @return array: tasks/view
*/
public JSONObject tasksSetLabels(long idTask, long... idLabels) throws ApiServiceException, IOException {
Object[] params = new Object[idLabels.length * 2 + 2];
params[0] = "token";
params[1] = token;
for(int i = 0; i < idLabels.length; i++) {
params[i*2 + 2] = "id_label[]";
params[i*2 + 3] = idLabels[i];
}
return invokeGet("tasks/set_label.json", params);
public JSONObject tasksSetLabel(long idTask, long idLabel) throws ApiServiceException, IOException {
return callAuthenticated("tasks/set_label.json",
"token", token,
"id_task", idTask,
"id_label", idLabel);
}
/**
@ -245,16 +248,11 @@ public class ProducteevInvoker {
*
* @return array: tasks/view
*/
public JSONObject tasksUnsetLabels(long idTask, long... idLabels) throws ApiServiceException, IOException {
Object[] params = new Object[idLabels.length * 2 + 2];
params[0] = "token";
params[1] = token;
for(int i = 0; i < idLabels.length; i++) {
params[i*2 + 2] = "id_label[]";
params[i*2 + 3] = idLabels[i];
}
return invokeGet("tasks/unset_label.json", params);
public JSONObject tasksUnsetLabel(long idTask, long idLabel) throws ApiServiceException, IOException {
return callAuthenticated("tasks/unset_label.json",
"token", token,
"id_task", idTask,
"id_label", idLabel);
}
/**
@ -266,7 +264,7 @@ public class ProducteevInvoker {
* @return array tasks::note_view
*/
public JSONObject tasksNoteCreate(long idTask, String message) throws ApiServiceException, IOException {
return invokeGet("tasks/note_create.json",
return callAuthenticated("tasks/note_create.json",
"token", token,
"id_task", idTask,
"message", message);
@ -283,7 +281,7 @@ public class ProducteevInvoker {
* @return array: labels/view
*/
public JSONArray labelsShowList(long idDashboard, String since) throws ApiServiceException, IOException {
return getResponse(invokeGet("labels/show_list.json",
return getResponse(callAuthenticated("labels/show_list.json",
"token", token,
"id_dashboard", idDashboard,
"since", since), "labels");
@ -297,11 +295,11 @@ public class ProducteevInvoker {
*
* @return array: labels/view
*/
public JSONArray labelsCreate(long idDashboard, String title) throws ApiServiceException, IOException {
return getResponse(invokeGet("labels/create.json",
public JSONObject labelsCreate(long idDashboard, String title) throws ApiServiceException, IOException {
return callAuthenticated("labels/create.json",
"token", token,
"id_dashboard", idDashboard,
"title", title), "labels");
"title", title);
}
// --- users
@ -314,7 +312,7 @@ public class ProducteevInvoker {
* @return array information about the user
*/
public JSONObject usersView(Long idColleague) throws ApiServiceException, IOException {
return invokeGet("users/view.json",
return callAuthenticated("users/view.json",
"token", token,
"id_colleague", idColleague);
}
@ -323,6 +321,45 @@ public class ProducteevInvoker {
private final RestClient restClient = new ProducteevRestClient();
/**
* Invokes authenticated method using HTTP GET. Will retry after re-authenticating if service exception encountered
*
* @param method
* API method to invoke
* @param getParameters
* Name/Value pairs. Values will be URL encoded.
* @return response object
*/
private JSONObject callAuthenticated(String method, Object... getParameters)
throws IOException, ApiServiceException {
try {
String request = createFetchUrl(method, getParameters);
String response;
try {
response = restClient.get(request);
} catch (ApiServiceException e) {
String oldToken = getToken();
System.err.println("PDV: retrying due to exception: " + e);
authenticate(retryEmail, retryPassword);
for(int i = 0; i < getParameters.length; i++)
if(oldToken.equals(getParameters[i]))
getParameters[i] = getToken();
request = createFetchUrl(method, getParameters);
response = restClient.get(request);
}
if(response.startsWith("DEBUG MESSAGE")) {
System.err.println(response);
return new JSONObject();
}
return new JSONObject(response);
} catch (JSONException e) {
throw new ApiResponseParseException(e);
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}
/**
* Invokes API method using HTTP GET
*
@ -376,7 +413,6 @@ public class ProducteevInvoker {
}
sigBuilder.append(apiSecret);
System.err.println("sigbuilder " + sigBuilder);
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);

@ -23,6 +23,7 @@ import com.todoroo.astrid.dao.TaskDao.TaskCriteria;
import com.todoroo.astrid.model.Metadata;
import com.todoroo.astrid.model.Task;
import com.todoroo.astrid.producteev.ProducteevUtilities;
import com.todoroo.astrid.rmilk.data.MilkNote;
import com.todoroo.astrid.tags.TagService;
public final class ProducteevDataService {
@ -151,6 +152,7 @@ public final class ProducteevDataService {
where(Criterion.and(MetadataCriteria.byTask(task.getId()),
Criterion.or(MetadataCriteria.withKey(TagService.KEY),
MetadataCriteria.withKey(ProducteevTask.METADATA_KEY),
MetadataCriteria.withKey(MilkNote.METADATA_KEY),
MetadataCriteria.withKey(ProducteevNote.METADATA_KEY)))));
try {
for(metadataCursor.moveToFirst(); !metadataCursor.isAfterLast(); metadataCursor.moveToNext()) {

@ -39,6 +39,7 @@ import com.todoroo.astrid.producteev.api.ApiResponseParseException;
import com.todoroo.astrid.producteev.api.ApiServiceException;
import com.todoroo.astrid.producteev.api.ApiUtilities;
import com.todoroo.astrid.producteev.api.ProducteevInvoker;
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;
@ -147,17 +148,15 @@ public class ProducteevSyncProvider extends SyncProvider<ProducteevTaskContainer
String v = stripslashes(2, "9641n76n9s1736q1578q1o1337q19233","4ae");
invoker = new ProducteevInvoker(z, v);
String email = Preferences.getStringValue(R.string.producteev_PPr_email);
String password = Preferences.getStringValue(R.string.producteev_PPr_password);
email = "astrid@todoroo.com"; // TODO
password = "astrid"; // TODO
// 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);
email = "astrid@todoroo.com";
password = "astrid";
invoker.setCredentials(authToken, email, password);
} else {
invoker.authenticate(email, password);
preferences.setToken(invoker.getToken());
}
@ -379,29 +378,21 @@ public class ProducteevSyncProvider extends SyncProvider<ProducteevTaskContainer
toRemove.removeAll(localTags);
if(toAdd.size() > 0) {
long[] toAddIds = new long[toAdd.size()];
int index = 0;
for(String label : toAdd) {
if(!labelMap.containsKey(label)) {
JSONArray result = invoker.labelsCreate(defaultDashboard, label);
readLabels(result);
JSONObject result = invoker.labelsCreate(defaultDashboard, label).getJSONObject("label");
labelMap.put(result.getString("title"), result.getLong("id_label"));
}
toAddIds[index++] = labelMap.get(label);
invoker.tasksSetLabel(idTask, labelMap.get(label));
}
invoker.tasksSetLabels(idTask, toAddIds);
}
if(toRemove.size() > 0) {
long[] toRemoveIds = new long[toRemove.size()];
int index = 0;
for(String label : toRemove) {
if(!labelMap.containsKey(label)) {
JSONArray result = invoker.labelsCreate(defaultDashboard, label);
readLabels(result);
}
toRemoveIds[index++] = labelMap.get(label);
if(!labelMap.containsKey(label))
continue;
invoker.tasksUnsetLabel(idTask, labelMap.get(label));
}
invoker.tasksUnsetLabels(idTask, toRemoveIds);
}
}
@ -412,6 +403,17 @@ public class ProducteevSyncProvider extends SyncProvider<ProducteevTaskContainer
local.metadata.add(ProducteevNote.create(result.getJSONObject("note")));
local.task.setValue(Task.NOTES, "");
}
// milk note => producteev note
if(local.findMetadata(MilkNote.METADATA_KEY) != null && (remote == null ||
(remote.findMetadata(ProducteevNote.METADATA_KEY) == null))) {
for(Metadata item : local.metadata)
if(MilkNote.METADATA_KEY.equals(item.getValue(Metadata.KEY))) {
String message = MilkNote.toTaskDetail(item);
JSONObject result = invoker.tasksNoteCreate(idTask, message);
local.metadata.add(ProducteevNote.create(result.getJSONObject("note")));
}
}
} catch (JSONException e) {
throw new ApiResponseParseException(e);
}

@ -46,6 +46,7 @@ import com.todoroo.astrid.api.TaskAction;
import com.todoroo.astrid.api.TaskDecoration;
import com.todoroo.astrid.model.Task;
import com.todoroo.astrid.notes.NoteDetailExposer;
import com.todoroo.astrid.producteev.ProducteevDetailExposer;
import com.todoroo.astrid.repeats.RepeatDetailExposer;
import com.todoroo.astrid.rmilk.MilkDetailExposer;
import com.todoroo.astrid.service.TaskService;
@ -85,6 +86,7 @@ public class TaskAdapter extends CursorAdapter implements Filterable {
new RepeatDetailExposer(),
new NoteDetailExposer(),
new MilkDetailExposer(),
new ProducteevDetailExposer(),
};
private static int[] IMPORTANCE_COLORS = null;

@ -115,7 +115,7 @@ public class StartupService {
// if sync ongoing flag was set, clear it
MilkUtilities.stopOngoing();
new ProducteevUtilities().stopOngoing();
ProducteevUtilities.INSTANCE.stopOngoing();
// check for task killers
if(!Constants.OEM)

Loading…
Cancel
Save