From 8fabc2435ce0ddadd336ebe80e2a7419ba8d6b0f Mon Sep 17 00:00:00 2001 From: Tim Su Date: Wed, 4 Aug 2010 20:28:42 -0700 Subject: [PATCH] Now showing producteev notes. Made PDV more resistant to errors by retrying once, and fixed some bugs with labels and notes and such --- .../com/todoroo/astrid/api/TaskContainer.java | 15 +++ astrid/astrid.launch | 2 +- .../todoroo/astrid/common/SyncProvider.java | 11 +- .../producteev/ProducteevDetailExposer.java | 79 +++++++++++++ .../producteev/ProducteevUtilities.java | 8 +- .../producteev/api/ProducteevInvoker.java | 110 ++++++++++++------ .../sync/ProducteevDataService.java | 2 + .../sync/ProducteevSyncProvider.java | 48 ++++---- .../todoroo/astrid/adapter/TaskAdapter.java | 2 + .../astrid/service/StartupService.java | 2 +- 10 files changed, 212 insertions(+), 67 deletions(-) create mode 100644 astrid/plugin-src/com/todoroo/astrid/producteev/ProducteevDetailExposer.java diff --git a/astrid/api-src/com/todoroo/astrid/api/TaskContainer.java b/astrid/api-src/com/todoroo/astrid/api/TaskContainer.java index fbefced39..f8ed4ef8b 100644 --- a/astrid/api-src/com/todoroo/astrid/api/TaskContainer.java +++ b/astrid/api-src/com/todoroo/astrid/api/TaskContainer.java @@ -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; + + /** + * 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; + } + } \ No newline at end of file diff --git a/astrid/astrid.launch b/astrid/astrid.launch index f5769e42d..7ce8fc659 100644 --- a/astrid/astrid.launch +++ b/astrid/astrid.launch @@ -4,7 +4,7 @@ - + diff --git a/astrid/plugin-src/com/todoroo/astrid/common/SyncProvider.java b/astrid/plugin-src/com/todoroo/astrid/common/SyncProvider.java index a0785ec7e..184073bcd 100644 --- a/astrid/plugin-src/com/todoroo/astrid/common/SyncProvider.java +++ b/astrid/plugin-src/com/todoroo/astrid/common/SyncProvider.java @@ -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 { 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 { } 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 { } 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 { handleException("sync-remote-updated", e, false); //$NON-NLS-1$ } } + + Flags.set(Flags.REFRESH); } // --- helper classes diff --git a/astrid/plugin-src/com/todoroo/astrid/producteev/ProducteevDetailExposer.java b/astrid/plugin-src/com/todoroo/astrid/producteev/ProducteevDetailExposer.java new file mode 100644 index 000000000..cfe458c93 --- /dev/null +++ b/astrid/plugin-src/com/todoroo/astrid/producteev/ProducteevDetailExposer.java @@ -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 + * + */ +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 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; + } + +} diff --git a/astrid/plugin-src/com/todoroo/astrid/producteev/ProducteevUtilities.java b/astrid/plugin-src/com/todoroo/astrid/producteev/ProducteevUtilities.java index f317cb393..e1578bbfd 100644 --- a/astrid/plugin-src/com/todoroo/astrid/producteev/ProducteevUtilities.java +++ b/astrid/plugin-src/com/todoroo/astrid/producteev/ProducteevUtilities.java @@ -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() { + // + } } \ No newline at end of file diff --git a/astrid/plugin-src/com/todoroo/astrid/producteev/api/ProducteevInvoker.java b/astrid/plugin-src/com/todoroo/astrid/producteev/api/ProducteevInvoker.java index 09dec1884..c68d03b19 100644 --- a/astrid/plugin-src/com/todoroo/astrid/producteev/api/ProducteevInvoker.java +++ b/astrid/plugin-src/com/todoroo/astrid/producteev/api/ProducteevInvoker.java @@ -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); diff --git a/astrid/plugin-src/com/todoroo/astrid/producteev/sync/ProducteevDataService.java b/astrid/plugin-src/com/todoroo/astrid/producteev/sync/ProducteevDataService.java index 61a1a3dfc..6112694d4 100644 --- a/astrid/plugin-src/com/todoroo/astrid/producteev/sync/ProducteevDataService.java +++ b/astrid/plugin-src/com/todoroo/astrid/producteev/sync/ProducteevDataService.java @@ -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()) { diff --git a/astrid/plugin-src/com/todoroo/astrid/producteev/sync/ProducteevSyncProvider.java b/astrid/plugin-src/com/todoroo/astrid/producteev/sync/ProducteevSyncProvider.java index 4cbff384b..2e378f991 100644 --- a/astrid/plugin-src/com/todoroo/astrid/producteev/sync/ProducteevSyncProvider.java +++ b/astrid/plugin-src/com/todoroo/astrid/producteev/sync/ProducteevSyncProvider.java @@ -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 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 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); } diff --git a/astrid/src/com/todoroo/astrid/adapter/TaskAdapter.java b/astrid/src/com/todoroo/astrid/adapter/TaskAdapter.java index 2542eac1c..fc125cd51 100644 --- a/astrid/src/com/todoroo/astrid/adapter/TaskAdapter.java +++ b/astrid/src/com/todoroo/astrid/adapter/TaskAdapter.java @@ -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; diff --git a/astrid/src/com/todoroo/astrid/service/StartupService.java b/astrid/src/com/todoroo/astrid/service/StartupService.java index efe397b2f..f3531b20f 100644 --- a/astrid/src/com/todoroo/astrid/service/StartupService.java +++ b/astrid/src/com/todoroo/astrid/service/StartupService.java @@ -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)