diff --git a/astrid/plugin-src/com/todoroo/astrid/common/SyncProviderPreferences.java b/astrid/plugin-src/com/todoroo/astrid/common/SyncProviderPreferences.java index 1a15b1332..caba41517 100644 --- a/astrid/plugin-src/com/todoroo/astrid/common/SyncProviderPreferences.java +++ b/astrid/plugin-src/com/todoroo/astrid/common/SyncProviderPreferences.java @@ -73,7 +73,7 @@ abstract public class SyncProviderPreferences extends TodorooPreferences { private static final String PREF_ONGOING = "_ongoing"; //$NON-NLS-1$ /** Get preferences object from the context */ - private static SharedPreferences getPrefs() { + protected static SharedPreferences getPrefs() { return PreferenceManager.getDefaultSharedPreferences(ContextManager.getContext()); } diff --git a/astrid/plugin-src/com/todoroo/astrid/producteev/ProducteevPreferences.java b/astrid/plugin-src/com/todoroo/astrid/producteev/ProducteevPreferences.java index caae47158..83d362dd8 100644 --- a/astrid/plugin-src/com/todoroo/astrid/producteev/ProducteevPreferences.java +++ b/astrid/plugin-src/com/todoroo/astrid/producteev/ProducteevPreferences.java @@ -1,5 +1,7 @@ package com.todoroo.astrid.producteev; +import android.content.SharedPreferences.Editor; + import com.timsu.astrid.R; import com.todoroo.astrid.common.SyncProviderPreferences; import com.todoroo.astrid.producteev.sync.ProducteevSyncProvider; @@ -38,5 +40,21 @@ public class ProducteevPreferences extends SyncProviderPreferences { new ProducteevSyncProvider().signOut(); } + // --- producteev-specific preferences + + private static final String PREF_SERVER_LAST_SYNC = "_last_server"; //$NON-NLS-1$ + + /** @return last sync date, or null if no last */ + public String getLastServerSync() { + return getPrefs().getString(getIdentifier() + PREF_SERVER_LAST_SYNC, null); + } + + /** Deletes Last Successful Sync Date */ + public void setLastServerSync(String value) { + Editor editor = getPrefs().edit(); + editor.putString(getIdentifier() + PREF_SERVER_LAST_SYNC, value); + editor.commit(); + } + } \ No newline at end of file diff --git a/astrid/plugin-src/com/todoroo/astrid/producteev/api/ApiUtilities.java b/astrid/plugin-src/com/todoroo/astrid/producteev/api/ApiUtilities.java index 4b13a0b82..5ff8bbfd7 100644 --- a/astrid/plugin-src/com/todoroo/astrid/producteev/api/ApiUtilities.java +++ b/astrid/plugin-src/com/todoroo/astrid/producteev/api/ApiUtilities.java @@ -2,8 +2,6 @@ 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; @@ -18,11 +16,8 @@ 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); - } + private static final SimpleDateFormat dateParser = new SimpleDateFormat( + "EEE, dd MMM yyyy"); //$NON-NLS-1$ /** * Utility method to convert PDV time to unix time @@ -52,4 +47,15 @@ public final class ApiUtilities { } } + /** + * Utility method to convert unix date to PDV date + * @param time + * @return + */ + public static String unixDateToProducteev(long date) { + synchronized(dateParser) { + return dateParser.format(DateUtilities.unixtimeToDate(date)); + } + } + } 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 a616f22d6..1e4bf892c 100644 --- a/astrid/plugin-src/com/todoroo/astrid/producteev/api/ProducteevInvoker.java +++ b/astrid/plugin-src/com/todoroo/astrid/producteev/api/ProducteevInvoker.java @@ -9,6 +9,7 @@ import java.security.NoSuchAlgorithmException; import java.util.Map; import java.util.TreeMap; +import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; @@ -72,9 +73,9 @@ public class ProducteevInvoker { * * @return array tasks/view */ - public JSONObject tasksCreate(String title, Integer idResponsible, Integer idDashboard, + public JSONArray tasksCreate(String title, Long idResponsible, Long idDashboard, String deadline, Integer reminder, Integer status, Integer star) throws ApiServiceException, IOException { - return invokeGet("tasks/create.json", + return getResponse(invokeGet("tasks/create.json", "token", token, "title", title, "id_responsible", idResponsible, @@ -82,7 +83,7 @@ public class ProducteevInvoker { "deadline", deadline, "reminder", reminder, "status", status, - "star", star); + "star", star), "tasks"); } /** @@ -93,11 +94,24 @@ public class ProducteevInvoker { * * @return array tasks/view */ - public JSONObject tasksShowList(Integer idDashboard, String since) throws ApiServiceException, IOException { - return invokeGet("tasks/show_list.json", + public JSONArray tasksShowList(Long idDashboard, String since) throws ApiServiceException, IOException { + return getResponse(invokeGet("tasks/show_list.json", "token", token, "id_dashboard", idDashboard, - "since", since); + "since", since), "tasks"); + } + + /** + * get a task + * + * @param idTask + * + * @return array tasks/view + */ + public JSONArray tasksView(Long idTask) throws ApiServiceException, IOException { + return getResponse(invokeGet("tasks/view.json", + "token", token, + "id_task", idTask), "tasks"); } /** @@ -108,11 +122,26 @@ public class ProducteevInvoker { * * @return array tasks/view */ - public JSONObject tasksSetTitle(int idTask, String title) throws ApiServiceException, IOException { - return invokeGet("tasks/set_title.json", + public JSONArray tasksSetTitle(long idTask, String title) throws ApiServiceException, IOException { + return getResponse(invokeGet("tasks/set_title.json", "token", token, "id_task", idTask, - "title", title); + "title", title), "tasks"); + } + + /** + * set status of a task + * + * @param idTask + * @param status (1 = UNDONE, 2 = DONE) + * + * @return array tasks/view + */ + public JSONArray tasksSetStatus(long idTask, int status) throws ApiServiceException, IOException { + return getResponse(invokeGet("tasks/set_star.json", + "token", token, + "id_task", idTask, + "status", status), "tasks"); } /** @@ -123,11 +152,11 @@ public class ProducteevInvoker { * * @return array tasks/view */ - public JSONObject tasksSetStar(int idTask, boolean star) throws ApiServiceException, IOException { - return invokeGet("tasks/set_star.json", + public JSONArray tasksSetStar(long idTask, int star) throws ApiServiceException, IOException { + return getResponse(invokeGet("tasks/set_star.json", "token", token, "id_task", idTask, - "star", star); + "star", star), "tasks"); } /** @@ -138,11 +167,11 @@ public class ProducteevInvoker { * * @return array tasks/view */ - public JSONObject tasksSetDeadline(int idTask, String deadline) throws ApiServiceException, IOException { - return invokeGet("tasks/set_deadline.json", + public JSONArray tasksSetDeadline(long idTask, String deadline) throws ApiServiceException, IOException { + return getResponse(invokeGet("tasks/set_deadline.json", "token", token, "id_task", idTask, - "deadline", deadline); + "deadline", deadline), "tasks"); } /** @@ -152,7 +181,7 @@ public class ProducteevInvoker { * * @return array with the result = (Array("stats" => Array("result" => "TRUE|FALSE")) */ - public JSONObject tasksDelete(int idTask) throws ApiServiceException, IOException { + public JSONObject tasksDelete(long idTask) throws ApiServiceException, IOException { return invokeGet("tasks/delete.json", "token", token, "id_task", idTask); @@ -165,10 +194,10 @@ public class ProducteevInvoker { * * @return array: list of labels/view */ - public JSONObject tasksLabels(int idTask) throws ApiServiceException, IOException { - return invokeGet("tasks/labels.json", + public JSONArray tasksLabels(long idTask) throws ApiServiceException, IOException { + return getResponse(invokeGet("tasks/labels.json", "token", token, - "id_task", idTask); + "id_task", idTask), "labels"); } /** @@ -179,11 +208,11 @@ public class ProducteevInvoker { * * @return array: tasks/view */ - public JSONObject tasksSetLabel(int idTask, int idLabel) throws ApiServiceException, IOException { - return invokeGet("tasks/set_label.json", + public JSONArray tasksSetLabel(long idTask, long idLabel) throws ApiServiceException, IOException { + return getResponse(invokeGet("tasks/set_label.json", "token", token, "id_task", idTask, - "id_label", idLabel); + "id_label", idLabel), "tasks"); } /** @@ -194,11 +223,11 @@ public class ProducteevInvoker { * * @return array: tasks/view */ - public JSONObject tasksUnsetLabel(int idTask, int idLabel) throws ApiServiceException, IOException { - return invokeGet("tasks/unset_label.json", + public JSONArray tasksUnsetLabel(long idTask, long idLabel) throws ApiServiceException, IOException { + return getResponse(invokeGet("tasks/unset_label.json", "token", token, "id_task", idTask, - "id_label", idLabel); + "id_label", idLabel), "tasks"); } // --- labels @@ -211,11 +240,11 @@ public class ProducteevInvoker { * * @return array: labels/view */ - public JSONObject labelsShowList(int idDashboard, String since) throws ApiServiceException, IOException { - return invokeGet("labels/show_list.json", + public JSONArray labelsShowList(long idDashboard, String since) throws ApiServiceException, IOException { + return getResponse(invokeGet("labels/show_list.json", "token", token, "id_dashboard", idDashboard, - "since", since); + "since", since), "labels"); } /** @@ -226,11 +255,11 @@ public class ProducteevInvoker { * * @return array: labels/view */ - public JSONObject labelsCreate(int idDashboard, String title) throws ApiServiceException, IOException { - return invokeGet("labels/create.json", + public JSONArray labelsCreate(long idDashboard, String title) throws ApiServiceException, IOException { + return getResponse(invokeGet("labels/create.json", "token", token, "id_dashboard", idDashboard, - "title", title); + "title", title), "labels"); } // --- users @@ -242,7 +271,7 @@ public class ProducteevInvoker { * * @return array information about the user */ - public JSONObject usersView(Integer idColleague) throws ApiServiceException, IOException { + public JSONObject usersView(Long idColleague) throws ApiServiceException, IOException { return invokeGet("users/view.json", "token", token, "id_colleague", idColleague); @@ -309,4 +338,19 @@ public class ProducteevInvoker { return requestBuilder.toString(); } + /** + * Helper method to get a field out or throw an api exception + * @param response + * @param field + * @return + * @throws ApiResponseParseException + */ + private JSONArray getResponse(JSONObject response, String field) throws ApiResponseParseException { + try { + return response.getJSONArray(field); + } catch (JSONException e) { + throw new ApiResponseParseException(e); + } + } + } diff --git a/astrid/plugin-src/com/todoroo/astrid/producteev/sync/ProducteevDataService.java b/astrid/plugin-src/com/todoroo/astrid/producteev/sync/ProducteevDataService.java new file mode 100644 index 000000000..1547159fc --- /dev/null +++ b/astrid/plugin-src/com/todoroo/astrid/producteev/sync/ProducteevDataService.java @@ -0,0 +1,191 @@ +/** + * See the file "LICENSE" for the full license governing this code. + */ +package com.todoroo.astrid.producteev.sync; + +import java.util.ArrayList; +import java.util.Random; + +import android.content.Context; + +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.sql.Criterion; +import com.todoroo.andlib.sql.Join; +import com.todoroo.andlib.sql.Query; +import com.todoroo.astrid.dao.MetadataDao; +import com.todoroo.astrid.dao.MetadataDao.MetadataCriteria; +import com.todoroo.astrid.dao.TaskDao; +import com.todoroo.astrid.dao.TaskDao.TaskCriteria; +import com.todoroo.astrid.model.Metadata; +import com.todoroo.astrid.model.Task; +import com.todoroo.astrid.rmilk.sync.RTMTaskContainer; +import com.todoroo.astrid.tags.TagService; + +public final class ProducteevDataService { + + // --- constants + + /** Utility for joining tasks with metadata */ + public static final Join METADATA_JOIN = Join.left(Metadata.TABLE, Task.ID.eq(Metadata.TASK)); + + // --- singleton + + private static ProducteevDataService instance = null; + + public static synchronized ProducteevDataService getInstance() { + if(instance == null) + instance = new ProducteevDataService(ContextManager.getContext()); + return instance; + } + + // --- instance variables + + protected final Context context; + + @Autowired + private TaskDao taskDao; + + @Autowired + private MetadataDao metadataDao; + + static final Random random = new Random(); + + private ProducteevDataService(Context context) { + this.context = context; + DependencyInjectionService.getInstance().inject(this); + } + + // --- task and metadata methods + + /** + * Clears RTM metadata information. Used when user logs out of RTM + */ + public void clearMetadata() { + metadataDao.deleteWhere(Metadata.KEY.eq(ProducteevTask.METADATA_KEY)); + } + + /** + * Gets tasks that were created since last sync + * @param properties + * @return + */ + public TodorooCursor getLocallyCreated(Property[] properties) { + return + taskDao.query(Query.select(properties).join(ProducteevDataService.METADATA_JOIN).where(Criterion.and( + Criterion.not(Task.ID.in(Query.select(Metadata.TASK).from(Metadata.TABLE). + where(Criterion.and(MetadataCriteria.withKey(ProducteevTask.METADATA_KEY), ProducteevTask.TASK_SERIES_ID.gt(0))))), + TaskCriteria.isActive()))); + } + + /** + * Gets tasks that were modified since last sync + * @param properties + * @return null if never sync'd + */ + public TodorooCursor getLocallyUpdated(Property[] properties) { + long lastSyncDate = ProducteevUtilities.getLastSyncDate(); + if(lastSyncDate == 0) + return taskDao.query(Query.select(Task.ID).where(Criterion.none)); + return + taskDao.query(Query.select(properties).join(ProducteevDataService.METADATA_JOIN). + where(Criterion.and(MetadataCriteria.withKey(ProducteevTask.METADATA_KEY), + Task.MODIFICATION_DATE.gt(lastSyncDate)))); + } + + /** + * Searches for a local task with same remote id, updates this task's id + * @param remoteTask + */ + public void findLocalMatch(RTMTaskContainer remoteTask) { + if(remoteTask.task.getId() != Task.NO_ID) + return; + TodorooCursor cursor = taskDao.query(Query.select(Task.ID). + join(ProducteevDataService.METADATA_JOIN).where(Criterion.and(MetadataCriteria.withKey(ProducteevTask.METADATA_KEY), + ProducteevTask.TASK_SERIES_ID.eq(remoteTask.taskSeriesId), + ProducteevTask.TASK_ID.eq(remoteTask.taskId)))); + try { + if(cursor.getCount() == 0) + return; + cursor.moveToFirst(); + remoteTask.task.setId(cursor.get(Task.ID)); + } finally { + cursor.close(); + } + } + + /** + * Saves a task and its metadata + * @param task + */ + public void saveTaskAndMetadata(ProducteevTaskContainer task) { + taskDao.save(task.task, true); + + metadataDao.deleteWhere(Criterion.and(MetadataCriteria.byTask(task.task.getId()), + Criterion.or(MetadataCriteria.withKey(ProducteevTask.METADATA_KEY), + MetadataCriteria.withKey(ProducteevNote.METADATA_KEY), + MetadataCriteria.withKey(TagService.KEY)))); + task.metadata.add(ProducteevTask.create(task)); + for(Metadata metadata : task.metadata) { + metadata.setValue(Metadata.TASK, task.task.getId()); + metadataDao.createNew(metadata); + } + } + + /** + * Reads a task and its metadata + * @param task + * @return + */ + public ProducteevTaskContainer readTaskAndMetadata(TodorooCursor taskCursor) { + Task task = new Task(taskCursor); + + // read tags, notes, etc + ArrayList metadata = new ArrayList(); + TodorooCursor metadataCursor = metadataDao.query(Query.select(Metadata.PROPERTIES). + where(Criterion.and(MetadataCriteria.byTask(task.getId()), + Criterion.or(MetadataCriteria.withKey(TagService.KEY), + MetadataCriteria.withKey(ProducteevTask.METADATA_KEY), + MetadataCriteria.withKey(ProducteevNote.METADATA_KEY))))); + try { + for(metadataCursor.moveToFirst(); !metadataCursor.isAfterLast(); metadataCursor.moveToNext()) { + metadata.add(new Metadata(metadataCursor)); + } + } finally { + metadataCursor.close(); + } + + return new RTMTaskContainer(task, metadata); + } + + /** + * Reads metadata out of a task + * @return null if no metadata found + */ + public Metadata getTaskMetadata(long taskId) { + TodorooCursor cursor = metadataDao.query(Query.select( + ProducteevTask.LIST_ID, ProducteevTask.TASK_SERIES_ID, ProducteevTask.TASK_ID, ProducteevTask.REPEATING).where( + MetadataCriteria.byTaskAndwithKey(taskId, ProducteevTask.METADATA_KEY))); + try { + if(cursor.getCount() == 0) + return null; + cursor.moveToFirst(); + return new Metadata(cursor); + } finally { + cursor.close(); + } + } + + /** + * Reads task notes out of a task + */ + public TodorooCursor getTaskNotesCursor(long taskId) { + TodorooCursor cursor = metadataDao.query(Query.select(Metadata.PROPERTIES). + where(MetadataCriteria.byTaskAndwithKey(taskId, ProducteevNote.METADATA_KEY))); + return cursor; + } + +} diff --git a/astrid/plugin-src/com/todoroo/astrid/producteev/sync/ProducteevNote.java b/astrid/plugin-src/com/todoroo/astrid/producteev/sync/ProducteevNote.java new file mode 100644 index 000000000..4b05ab634 --- /dev/null +++ b/astrid/plugin-src/com/todoroo/astrid/producteev/sync/ProducteevNote.java @@ -0,0 +1,45 @@ +package com.todoroo.astrid.producteev.sync; + +import org.json.JSONObject; + +import com.todoroo.andlib.data.Property.LongProperty; +import com.todoroo.andlib.data.Property.StringProperty; +import com.todoroo.astrid.model.Metadata; +import com.todoroo.astrid.producteev.api.ApiUtilities; + +/** + * Metadata entries for a Producteev note. The first Producteev note becomes + * Astrid's note field, subsequent notes are stored in metadata in this + * format. + * + * @author Tim Su + * + */ +public class ProducteevNote { + + /** metadata key */ + public static final String METADATA_KEY = "producteev-note"; //$NON-NLS-1$ + + /** note id */ + public static final LongProperty ID = new LongProperty(Metadata.TABLE, + Metadata.VALUE1.name); + + /** note message */ + public static final StringProperty MESSAGE = Metadata.VALUE2; + + /** note creation date */ + public static final LongProperty CREATED = new LongProperty(Metadata.TABLE, + Metadata.VALUE3.name); + + @SuppressWarnings("nls") + public static Metadata create(JSONObject note) { + Metadata metadata = new Metadata(); + metadata.setValue(Metadata.KEY, METADATA_KEY); + metadata.setValue(ID, note.optLong("id_note")); + metadata.setValue(MESSAGE, note.optString("message")); + metadata.setValue(CREATED, ApiUtilities.producteevToUnixTime( + note.optString("time_create"), 0)); + return metadata; + } + +} 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 b2bd3e60a..796463be5 100644 --- a/astrid/plugin-src/com/todoroo/astrid/producteev/sync/ProducteevSyncProvider.java +++ b/astrid/plugin-src/com/todoroo/astrid/producteev/sync/ProducteevSyncProvider.java @@ -5,14 +5,19 @@ package com.todoroo.astrid.producteev.sync; import java.io.IOException; import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import org.json.JSONArray; +import org.json.JSONException; import org.json.JSONObject; import android.app.Notification; +import android.app.PendingIntent; import android.content.Context; -import android.content.res.Resources; +import android.content.Intent; import android.text.TextUtils; -import android.widget.Toast; import com.flurry.android.FlurryAgent; import com.timsu.astrid.R; @@ -22,24 +27,37 @@ 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.ProducteevPreferences; +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.MilkUtilities; import com.todoroo.astrid.rmilk.api.ServiceInternalException; -import com.todoroo.astrid.rmilk.data.MilkDataService; +import com.todoroo.astrid.rmilk.api.data.RtmTaskNote; +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; +@SuppressWarnings("nls") public class ProducteevSyncProvider extends SynchronizationProvider { - private MilkDataService dataService = null; + private ProducteevDataService dataService = null; private ProducteevInvoker invoker = null; - private int defaultDashboard; + private long defaultDashboard; private final ProducteevPreferences preferences = new ProducteevPreferences(); + /** map of producteev labels to id's */ + private final HashMap labelMap = new HashMap(); + static { AstridDependencyInjector.initialize(); } @@ -60,13 +78,13 @@ public class ProducteevSyncProvider extends SynchronizationProvider syncData = populateSyncData(tasks); try { @@ -210,77 +227,300 @@ public class ProducteevSyncProvider extends SynchronizationProvider populateSyncData(JSONObject tasks) { + @SuppressWarnings("nls") + private SyncData populateSyncData(JSONArray tasks) throws JSONException { // fetch locally created tasks TodorooCursor localCreated = dataService.getLocallyCreated(PROPERTIES); // fetch locally updated tasks TodorooCursor localUpdated = dataService.getLocallyUpdated(PROPERTIES); - // return new SyncData(tasks, localCreated, localUpdated); - return null; + // read json response + ArrayList remoteTasks = new ArrayList(tasks.length()); + for(int i = 0; i < tasks.length(); i++) + remoteTasks.add(parseRemoteTask(tasks.getJSONObject(i))); + + return new SyncData(remoteTasks, localCreated, localUpdated); } // ---------------------------------------------------------------------- // ------------------------------------------------- create / push / pull // ---------------------------------------------------------------------- + @Override + protected void create(ProducteevTaskContainer task) throws IOException { + Task local = task.task; + Long dashboard = null; + if(task.pdvTask.containsNonNullValue(ProducteevTask.DASHBOARD_ID)) + dashboard = task.pdvTask.getValue(ProducteevTask.DASHBOARD_ID); + JSONArray response = invoker.tasksCreate(local.getValue(Task.TITLE), + null, dashboard, createDeadline(local), createReminder(local), + local.isCompleted() ? 2 : 1, createStars(local)); + if(response.length() != 1) + throw new ApiServiceException("Unexpected # of tasks created: " + response.length()); + ProducteevTaskContainer newRemoteTask; + try { + newRemoteTask = parseRemoteTask(response.getJSONObject(0)); + } catch (JSONException e) { + throw new ApiResponseParseException(e); + } + transferIdentifiers(newRemoteTask, task); + } + /** Create a task container for the given RtmTaskSeries + * @throws JSONException */ + private ProducteevTaskContainer parseRemoteTask(JSONObject remoteTask) throws JSONException { + Task task = new Task(); + ArrayList metadata = new ArrayList(); + + task.setValue(Task.TITLE, remoteTask.getString("title")); + task.setValue(Task.CREATION_DATE, ApiUtilities.producteevToUnixTime(remoteTask.getString("time_created"), 0)); + task.setValue(Task.COMPLETION_DATE, remoteTask.getInt("status") == 2 ? DateUtilities.now() : 0); + task.setValue(Task.DELETION_DATE, remoteTask.getInt("deleted") == 1 ? DateUtilities.now() : 0); + + long dueDate = ApiUtilities.producteevToUnixTime(remoteTask.getString("deadline"), 0); + task.setValue(Task.DUE_DATE, task.createDueDate(Task.URGENCY_SPECIFIC_DAY_TIME, dueDate)); + task.setValue(Task.IMPORTANCE, 5 - remoteTask.getInt("star")); + + JSONArray labels = remoteTask.getJSONArray("labels"); + for(int i = 0; i < labels.length(); i++) { + JSONObject label = labels.getJSONObject(i).getJSONObject("label"); + if(label.getInt("deleted") != 0) + continue; + + Metadata tagData = new Metadata(); + tagData.setValue(Metadata.KEY, TagService.KEY); + tagData.setValue(TagService.TAG, label.getString("title")); + metadata.add(tagData); + } - // ---------------------------------------------------------------------- - // ------------------------------------------------------- helper classes - // ---------------------------------------------------------------------- + JSONArray notes = remoteTask.getJSONArray("notes"); + for(int i = notes.length() - 1; i >= 0; i--) { + JSONObject note = notes.getJSONObject(i).getJSONObject("note"); + if(i == notes.length() - 1) + task.setValue(Task.NOTES, note.getString("message")); + else + metadata.add(ProducteevNote.create(note)); + } - 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)_))); + ProducteevTaskContainer container = new ProducteevTaskContainer(task, metadata, remoteTask); + + return container; } @Override - protected void updateNotification(Context context, Notification n) { + protected ProducteevTaskContainer pull(ProducteevTaskContainer task) throws IOException { + if(!task.pdvTask.containsNonNullValue(ProducteevTask.ID)) + throw new ApiServiceException("Tried to read an invalid task"); //$NON-NLS-1$ + + JSONArray tasks = invoker.tasksView(task.pdvTask.getValue(ProducteevTask.ID)); + if(tasks.length() == 0) + return null; + try { + return parseRemoteTask(tasks.getJSONObject(0)); + } catch (JSONException e) { + throw new ApiResponseParseException(e); + } } + /** + * 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 create(ProducteevTaskContainer task) throws IOException { + protected void push(ProducteevTaskContainer local, ProducteevTaskContainer remote) throws IOException { + boolean remerge = false; + + // fetch remote task for comparison + if(remote == null) + remote = pull(local); + + long idTask = local.pdvTask.getValue(ProducteevTask.ID); + + // TODO handle task workspace switching + + // either delete or re-create if necessary + if(shouldTransmit(local, Task.DELETION_DATE, remote)) { + if(local.task.getValue(Task.DELETION_DATE) > 0) + invoker.tasksDelete(idTask); + else + create(local); + } + + if(shouldTransmit(local, Task.TITLE, remote)) + invoker.tasksSetTitle(idTask, local.task.getValue(Task.TITLE)); + if(shouldTransmit(local, Task.IMPORTANCE, remote)) + invoker.tasksSetStar(idTask, createStars(local.task)); + if(shouldTransmit(local, Task.DUE_DATE, remote)) + invoker.tasksSetDeadline(idTask, createDeadline(local.task)); + if(shouldTransmit(local, Task.COMPLETION_DATE, remote)) + invoker.tasksSetStatus(idTask, local.task.isCompleted() ? 2 : 1); + + // tags + HashSet localTags = new HashSet(); + HashSet remoteTags = new HashSet(); + 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 notes = null; + if(remote != null && remote.pdvTask.getNotes() != null) + notes = remote.pdvTask.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); + } } + + // ---------------------------------------------------------------------- + // --------------------------------------------------------- read / write + // ---------------------------------------------------------------------- + @Override - protected void push(ProducteevTaskContainer task, - ProducteevTaskContainer remote) throws IOException { + protected ProducteevTaskContainer read(TodorooCursor cursor) throws IOException { + return dataService.readTaskAndMetadata(cursor); } @Override - protected ProducteevTaskContainer pull(ProducteevTaskContainer task) - throws IOException { - return null; + protected void write(ProducteevTaskContainer task) throws IOException { + dataService.saveTaskAndMetadata(task); } + // ---------------------------------------------------------------------- + // --------------------------------------------------------- misc helpers + // ---------------------------------------------------------------------- + @Override - protected ProducteevTaskContainer read(TodorooCursor task) - throws IOException { + protected int matchTask(ArrayList tasks, ProducteevTaskContainer target) { + int length = tasks.size(); + for(int i = 0; i < length; i++) { + ProducteevTaskContainer task = tasks.get(i); + if(AndroidUtilities.equals(task.pdvTask, target.pdvTask)) + return i; + } + return -1; + } + + /** + * get stars in producteev format + * @param local + * @return + */ + private Integer createStars(Task local) { + return 5 - local.getValue(Task.IMPORTANCE); + } + + /** + * get reminder in producteev format + * @param local + * @return + */ + private Integer createReminder(Task local) { + if(local.getFlag(Task.REMINDER_FLAGS, Task.NOTIFY_AT_DEADLINE)) + return 8; return null; } - @Override - protected void write(ProducteevTaskContainer task) throws IOException { + /** + * get deadline in producteev format + * @param task + * @return + */ + private String createDeadline(Task task) { + if(!task.hasDueDate()) + return null; + if(!task.hasDueTime()) + return ApiUtilities.unixDateToProducteev(task.getValue(Task.DUE_DATE)); + return ApiUtilities.unixTimeToProducteev(task.getValue(Task.DUE_DATE)); + } + + /** + * 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)); } @Override - protected int matchTask(ArrayList tasks, - ProducteevTaskContainer target) { - return 0; + protected void updateNotification(Context context, Notification notification) { + String notificationTitle = context.getString(R.string.producteev_notification_title); + Intent intent = new Intent(context, ProducteevPreferences.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.pdvTask = source.pdvTask; + } + + // ---------------------------------------------------------------------- + // ------------------------------------------------------- helper methods + // ---------------------------------------------------------------------- + + 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)_))); } + } diff --git a/astrid/plugin-src/com/todoroo/astrid/producteev/ProducteevTask.java b/astrid/plugin-src/com/todoroo/astrid/producteev/sync/ProducteevTask.java similarity index 56% rename from astrid/plugin-src/com/todoroo/astrid/producteev/ProducteevTask.java rename to astrid/plugin-src/com/todoroo/astrid/producteev/sync/ProducteevTask.java index 90e62b2f6..2406bce48 100644 --- a/astrid/plugin-src/com/todoroo/astrid/producteev/ProducteevTask.java +++ b/astrid/plugin-src/com/todoroo/astrid/producteev/sync/ProducteevTask.java @@ -1,8 +1,7 @@ -package com.todoroo.astrid.producteev; +package com.todoroo.astrid.producteev.sync; import com.todoroo.andlib.data.Property.LongProperty; import com.todoroo.astrid.model.Metadata; -import com.todoroo.astrid.producteev.sync.ProducteevTaskContainer; /** * Metadata entries for a Remember The Milk Task @@ -22,17 +21,4 @@ public class ProducteevTask { 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; - } - } diff --git a/astrid/plugin-src/com/todoroo/astrid/producteev/sync/ProducteevTaskContainer.java b/astrid/plugin-src/com/todoroo/astrid/producteev/sync/ProducteevTaskContainer.java index 6aa76ae0b..1af0f2bd9 100644 --- a/astrid/plugin-src/com/todoroo/astrid/producteev/sync/ProducteevTaskContainer.java +++ b/astrid/plugin-src/com/todoroo/astrid/producteev/sync/ProducteevTaskContainer.java @@ -3,10 +3,11 @@ package com.todoroo.astrid.producteev.sync; import java.util.ArrayList; import java.util.Iterator; +import org.json.JSONObject; + 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 @@ -15,34 +16,38 @@ import com.todoroo.astrid.producteev.ProducteevTask; * */ public class ProducteevTaskContainer extends TaskContainer { - public long id; - public long dashboard; - public ProducteevTaskContainer(Task task, ArrayList metadata, long id, long dashboard) { + public Metadata pdvTask; + + public ProducteevTaskContainer(Task task, ArrayList metadata, Metadata remote) { this.task = task; this.metadata = metadata; - this.id = id; - this.dashboard = dashboard; + this.pdvTask = remote; + if(this.pdvTask == null) + this.pdvTask = new Metadata(); } -// public ProducteevTaskContainer(Task task, ArrayList metadata, -// RtmTaskSeries rtmTaskSeries) { -// this(task, metadata, ); -// } + @SuppressWarnings("nls") + public ProducteevTaskContainer(Task task, ArrayList metadata, JSONObject remoteTask) { + this(task, metadata, new Metadata()); + pdvTask.setValue(ProducteevTask.ID, remoteTask.optLong("id_task")); + pdvTask.setValue(ProducteevTask.DASHBOARD_ID, remoteTask.optLong("id_dashboard")); + } public ProducteevTaskContainer(Task task, ArrayList metadata) { - this(task, metadata, 0, 0); + this.task = task; + this.metadata = metadata; + for(Iterator 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); + pdvTask = item; iterator.remove(); break; } } + if(this.pdvTask == null) + this.pdvTask = new Metadata(); } diff --git a/astrid/res/values/strings-producteev.xml b/astrid/res/values/strings-producteev.xml index f81b22c64..b9b955d0a 100644 --- a/astrid/res/values/strings-producteev.xml +++ b/astrid/res/values/strings-producteev.xml @@ -6,6 +6,12 @@ Producteev + + + + + Astrid: Producteev +