From aa2fe7db1bd2457c53a6bfbb3afecf46a0e8a948 Mon Sep 17 00:00:00 2001 From: Tim Su Date: Tue, 10 Aug 2010 16:08:50 -0700 Subject: [PATCH] Producteev Shared Desktop initial work. what's here: synchronizing multiple workspaces, displaying this information, displaying task responsiblity info, switching things into multiple workspaces --- .../producteev/ProducteevDetailExposer.java | 213 +-- .../producteev/ProducteevPreferences.java | 22 +- .../producteev/ProducteevUtilities.java | 36 +- .../producteev/api/ProducteevInvoker.java | 31 + .../producteev/sync/ProducteevDashboard.java | 116 +- .../sync/ProducteevDataService.java | 519 +++---- .../producteev/sync/ProducteevDatabase.java | 76 -- .../sync/ProducteevSyncProvider.java | 1214 +++++++++-------- .../producteev/sync/ProducteevTask.java | 56 +- .../sync/ProducteevTaskContainer.java | 2 + astrid/res/layout/addon_adapter_row.xml | 176 +-- astrid/res/values/strings-producteev.xml | 171 +-- .../astrid/activity/TaskEditActivity.java | 8 +- .../astrid/activity/TaskListActivity.java | 4 +- .../src/com/todoroo/astrid/dao/Database.java | 37 +- .../todoroo/astrid/dao/StoreObjectDao.java | 52 + .../com/todoroo/astrid/model/Metadata.java | 12 +- .../com/todoroo/astrid/model/StoreObject.java | 105 ++ .../service/AstridDependencyInjector.java | 2 + 19 files changed, 1555 insertions(+), 1297 deletions(-) delete mode 100644 astrid/plugin-src/com/todoroo/astrid/producteev/sync/ProducteevDatabase.java create mode 100644 astrid/src/com/todoroo/astrid/dao/StoreObjectDao.java create mode 100644 astrid/src/com/todoroo/astrid/model/StoreObject.java diff --git a/astrid/plugin-src/com/todoroo/astrid/producteev/ProducteevDetailExposer.java b/astrid/plugin-src/com/todoroo/astrid/producteev/ProducteevDetailExposer.java index 222a9de67..355e6f483 100644 --- a/astrid/plugin-src/com/todoroo/astrid/producteev/ProducteevDetailExposer.java +++ b/astrid/plugin-src/com/todoroo/astrid/producteev/ProducteevDetailExposer.java @@ -1,95 +1,118 @@ -/** - * 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.timsu.astrid.R; -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; -import com.todoroo.astrid.producteev.sync.ProducteevTask; - -/** - * 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) { - Metadata metadata = ProducteevDataService.getInstance().getTaskMetadata(id); - if(metadata == null) - return null; - - StringBuilder builder = new StringBuilder(); - - if(!extended) { - long dashboardId = metadata.getValue(ProducteevTask.DASHBOARD_ID); - String dashboardName = ProducteevDataService.getInstance().getDashboardName(dashboardId); - // Prod dashboard is out of date. don't display Producteev stuff - if(dashboardName == null) - return null; - - if(dashboardId > 0) { - builder.append(context.getString(R.string.producteev_TLA_dashboard, - dashboardId)).append(TaskAdapter.DETAIL_SEPARATOR); - } - - } else { - TodorooCursor notesCursor = ProducteevDataService.getInstance().getTaskNotesCursor(id); - try { - 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; - } - -} +/** + * 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.timsu.astrid.R; +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.model.StoreObject; +import com.todoroo.astrid.producteev.sync.ProducteevDashboard; +import com.todoroo.astrid.producteev.sync.ProducteevDataService; +import com.todoroo.astrid.producteev.sync.ProducteevNote; +import com.todoroo.astrid.producteev.sync.ProducteevTask; +import com.todoroo.astrid.utility.Preferences; + +/** + * 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) { + Metadata metadata = ProducteevDataService.getInstance().getTaskMetadata(id); + if(metadata == null) + return null; + + StringBuilder builder = new StringBuilder(); + + if(!extended) { + long dashboardId = metadata.getValue(ProducteevTask.DASHBOARD_ID); + long responsibleId = metadata.getValue(ProducteevTask.RESPONSIBLE_ID); + + // display dashboard if not "no sync" or "default" + StoreObject ownerDashboard = null; + for(StoreObject dashboard : ProducteevDataService.getInstance().getDashboards()) { + if(dashboard.getValue(ProducteevDashboard.REMOTE_ID) == dashboardId) { + ownerDashboard = dashboard; + break; + } + } + if(dashboardId != ProducteevUtilities.DASHBOARD_NO_SYNC && dashboardId + != Preferences.getLong(ProducteevUtilities.PREF_DEFAULT_DASHBOARD, 0L) && + ownerDashboard != null) { + String dashboardName = ownerDashboard.getValue(ProducteevDashboard.NAME); + builder.append(context.getString(R.string.producteev_TLA_dashboard, + dashboardName)).append(TaskAdapter.DETAIL_SEPARATOR); + } + + // display responsible user if not current one + if(responsibleId > 0 && ownerDashboard != null && responsibleId != + Preferences.getLong(ProducteevUtilities.PREF_USER_ID, 0L)) { + String users = ownerDashboard.getValue(ProducteevDashboard.USERS); + int index = users.indexOf(";" + responsibleId + ","); //$NON-NLS-1$ //$NON-NLS-2$ + if(index > -1) { + String user = users.substring(users.indexOf(',', index), + users.indexOf(';', index + 1)); + builder.append(context.getString(R.string.producteev_TLA_responsible, + user)).append(TaskAdapter.DETAIL_SEPARATOR); + } + } + } else { + TodorooCursor notesCursor = ProducteevDataService.getInstance().getTaskNotesCursor(id); + try { + 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/ProducteevPreferences.java b/astrid/plugin-src/com/todoroo/astrid/producteev/ProducteevPreferences.java index e2d135a6c..81b8e73aa 100644 --- a/astrid/plugin-src/com/todoroo/astrid/producteev/ProducteevPreferences.java +++ b/astrid/plugin-src/com/todoroo/astrid/producteev/ProducteevPreferences.java @@ -10,6 +10,9 @@ import com.timsu.astrid.R; import com.todoroo.andlib.utility.AndroidUtilities; import com.todoroo.astrid.common.SyncProviderPreferences; import com.todoroo.astrid.common.SyncProviderUtilities; +import com.todoroo.astrid.model.StoreObject; +import com.todoroo.astrid.producteev.sync.ProducteevDashboard; +import com.todoroo.astrid.producteev.sync.ProducteevDataService; import com.todoroo.astrid.producteev.sync.ProducteevSyncProvider; /** @@ -47,16 +50,23 @@ public class ProducteevPreferences extends SyncProviderPreferences { super.onCreate(savedInstanceState); ListPreference defaultDash = (ListPreference)findPreference(getString(R.string.producteev_PPr_defaultdash_key)); + String[] entries, entryValues; if(ProducteevUtilities.INSTANCE.isLoggedIn()) { - // + StoreObject[] dashboards = ProducteevDataService.getInstance().getDashboards(); + entries = new String[dashboards.length + 1]; + entryValues = new String[dashboards.length + 1]; + for(int i = 0; i < dashboards.length; i++) { + entries[i + 1] = dashboards[i].getValue(ProducteevDashboard.NAME); + entryValues[i + 1] = Long.toString(dashboards[i].getValue(ProducteevDashboard.REMOTE_ID)); + } + } else { + entries = new String[2]; + entries[1] = getString(R.string.producteev_default_dashboard); + entryValues = new String[2]; + entryValues[1] = Integer.toString(ProducteevUtilities.DASHBOARD_DEFAULT); } - String[] entries = new String[2]; entries[0] = getString(R.string.producteev_no_dashboard); - entries[1] = getString(R.string.producteev_default_dashboard); - - String[] entryValues = new String[2]; entryValues[0] = Integer.toString(ProducteevUtilities.DASHBOARD_NO_SYNC); - entryValues[1] = Integer.toString(ProducteevUtilities.DASHBOARD_DEFAULT); defaultDash.setEntries(entries); defaultDash.setEntryValues(entryValues); } diff --git a/astrid/plugin-src/com/todoroo/astrid/producteev/ProducteevUtilities.java b/astrid/plugin-src/com/todoroo/astrid/producteev/ProducteevUtilities.java index fb6dc7d2d..796387c43 100644 --- a/astrid/plugin-src/com/todoroo/astrid/producteev/ProducteevUtilities.java +++ b/astrid/plugin-src/com/todoroo/astrid/producteev/ProducteevUtilities.java @@ -1,9 +1,8 @@ package com.todoroo.astrid.producteev; -import android.content.SharedPreferences.Editor; - import com.timsu.astrid.R; import com.todoroo.astrid.common.SyncProviderUtilities; +import com.todoroo.astrid.utility.Preferences; /** * Displays synchronization preferences and an action panel so users can @@ -37,22 +36,33 @@ public class ProducteevUtilities extends SyncProviderUtilities { // --- producteev-specific preferences - private static final String PREF_SERVER_LAST_SYNC = "_last_server"; //$NON-NLS-1$ + public static final String PREF_SERVER_LAST_SYNC = IDENTIFIER + "_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); - } + public static final String PREF_EMAIL = IDENTIFIER + "_email"; //$NON-NLS-1$ + + public static final String PREF_PASSWORD = IDENTIFIER + "_password"; //$NON-NLS-1$ + + public static final String PREF_DEFAULT_DASHBOARD = IDENTIFIER + "_defaultdash"; //$NON-NLS-1$ + + public static final String PREF_USER_ID = IDENTIFIER + "_userid"; //$NON-NLS-1$ - /** Deletes Last Successful Sync Date */ - public void setLastServerSync(String value) { - Editor editor = getPrefs().edit(); - editor.putString(getIdentifier() + PREF_SERVER_LAST_SYNC, value); - editor.commit(); + /** + * Gets default dashboard from setting + * @return DASHBOARD_NO_SYNC if should not sync, otherwise remote id + */ + public long getDefaultDashboard() { + int defaultDashboard = Preferences.getIntegerFromString(R.string.producteev_PPr_defaultdash_key, + DASHBOARD_DEFAULT); + if(defaultDashboard == DASHBOARD_NO_SYNC) + return DASHBOARD_NO_SYNC; + else if(defaultDashboard == DASHBOARD_DEFAULT) + return Preferences.getLong(PREF_DEFAULT_DASHBOARD, 0); + else + return (long) defaultDashboard; } private ProducteevUtilities() { - // + // prevent instantiation } } \ 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 01d621d42..9ddfe1a98 100644 --- a/astrid/plugin-src/com/todoroo/astrid/producteev/api/ProducteevInvoker.java +++ b/astrid/plugin-src/com/todoroo/astrid/producteev/api/ProducteevInvoker.java @@ -96,6 +96,22 @@ public class ProducteevInvoker { "fbuid", fbUid); } + // --- dashboards + + /** + * 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 JSONArray dashboardsShowList(String since) throws ApiServiceException, IOException { + return getResponse(callAuthenticated("dashboards/show_list.json", + "token", token, + "since", since), "dashboards"); + } + // --- tasks /** @@ -212,6 +228,21 @@ public class ProducteevInvoker { "deadline", deadline); } + /** + * set a workspace + * + * @param idTask + * @param id_dashboard + * + * @return array tasks/view + */ + public JSONObject tasksSetWorkspace(long idTask, long idDashboard) throws ApiServiceException, IOException { + return callAuthenticated("tasks/set_workspace.json", + "token", token, + "id_task", idTask, + "id_dashboard", idDashboard); + } + /** * delete a task * diff --git a/astrid/plugin-src/com/todoroo/astrid/producteev/sync/ProducteevDashboard.java b/astrid/plugin-src/com/todoroo/astrid/producteev/sync/ProducteevDashboard.java index 4fe57074c..c44089f6b 100644 --- a/astrid/plugin-src/com/todoroo/astrid/producteev/sync/ProducteevDashboard.java +++ b/astrid/plugin-src/com/todoroo/astrid/producteev/sync/ProducteevDashboard.java @@ -1,86 +1,30 @@ -/** - * See the file "LICENSE" for the full license governing this code. - */ -package com.todoroo.astrid.producteev.sync; - - -import android.content.ContentValues; - -import com.todoroo.andlib.data.AbstractModel; -import com.todoroo.andlib.data.Property; -import com.todoroo.andlib.data.Table; -import com.todoroo.andlib.data.TodorooCursor; -import com.todoroo.andlib.data.Property.LongProperty; -import com.todoroo.andlib.data.Property.StringProperty; -import com.todoroo.astrid.model.Task; - -/** - * Data Model which represents a dashboard in Producteev - * - * @author Tim Su - * - */ -@SuppressWarnings("nls") -public class ProducteevDashboard extends AbstractModel { - - // --- table - - public static final Table TABLE = new Table("dashboards", ProducteevDashboard.class); - - // --- properties - - /** ID (corresponds to RTM ID) */ - public static final LongProperty ID = new LongProperty( - TABLE, ID_PROPERTY_NAME); - - /** Name */ - public static final StringProperty NAME = new StringProperty( - TABLE, "name"); - - /** List of all properties for this model */ - public static final Property[] PROPERTIES = generateProperties(ProducteevDashboard.class); - - // --- defaults - - /** Default values container */ - private static final ContentValues defaultValues = new ContentValues(); - -// static { -// defaultValues.put(POSITION.name, 0); -// defaultValues.put(ARCHIVED.name, 0); -// } - - @Override - public ContentValues getDefaultValues() { - return defaultValues; - } - - // --- data access boilerplate - - public ProducteevDashboard() { - super(); - } - - public ProducteevDashboard(TodorooCursor cursor) { - this(); - readPropertiesFromCursor(cursor); - } - - public void readFromCursor(TodorooCursor cursor) { - super.readPropertiesFromCursor(cursor); - } - - @Override - public long getId() { - return getIdHelper(ID); - }; - - // --- parcelable helpers - - private static final Creator CREATOR = new ModelCreator(Task.class); - - @Override - protected Creator getCreator() { - return CREATOR; - } -} +package com.todoroo.astrid.producteev.sync; + +import com.todoroo.andlib.data.Property.LongProperty; +import com.todoroo.andlib.data.Property.StringProperty; +import com.todoroo.astrid.model.StoreObject; + +/** + * {@link StoreObject} entries for a Producteev Dashboard + * + * @author Tim Su + * + */ +public class ProducteevDashboard { + + /** type*/ + public static final String TYPE = "pdv-dash"; //$NON-NLS-1$ + + /** dashboard id in producteev */ + public static final LongProperty REMOTE_ID = new LongProperty(StoreObject.TABLE, + StoreObject.ITEM.name); + + /** dashboard name */ + public static final StringProperty NAME = new StringProperty(StoreObject.TABLE, + StoreObject.VALUE1.name); + + /** users (list in the format "id_user,name;id_user,name;") */ + public static final StringProperty USERS = new StringProperty(StoreObject.TABLE, + StoreObject.VALUE2.name); + +} 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 a77a7273e..3665283e9 100644 --- a/astrid/plugin-src/com/todoroo/astrid/producteev/sync/ProducteevDataService.java +++ b/astrid/plugin-src/com/todoroo/astrid/producteev/sync/ProducteevDataService.java @@ -1,235 +1,284 @@ -/** - * See the file "LICENSE" for the full license governing this code. - */ -package com.todoroo.astrid.producteev.sync; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.Map; -import java.util.Random; - -import android.content.Context; - -import com.todoroo.andlib.data.GenericDao; -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.andlib.utility.SoftHashMap; -import com.todoroo.astrid.dao.MetadataDao; -import com.todoroo.astrid.dao.TaskDao; -import com.todoroo.astrid.dao.MetadataDao.MetadataCriteria; -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 { - - // --- 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; - - private final ProducteevDatabase prodDatabase = new ProducteevDatabase(); - - private final GenericDao prodDashboardDao; - - @Autowired - private TaskDao taskDao; - - @Autowired - private MetadataDao metadataDao; - - private final ProducteevUtilities preferences = ProducteevUtilities.INSTANCE; - - static final Random random = new Random(); - - private ProducteevDataService(Context context) { - this.context = context; - DependencyInjectionService.getInstance().inject(this); - prodDashboardDao = new GenericDao(ProducteevDashboard.class, prodDatabase); - } - - // --- 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)); - metadataDao.deleteWhere(Metadata.KEY.eq(ProducteevNote.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.ID.gt(0))))), - TaskCriteria.isActive())).groupBy(Task.ID)); - } - - /** - * Gets tasks that were modified since last sync - * @param properties - * @return null if never sync'd - */ - public TodorooCursor getLocallyUpdated(Property[] properties) { - long lastSyncDate = preferences.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))).groupBy(Task.ID)); - } - - /** - * Searches for a local task with same remote id, updates this task's id - * @param remoteTask - */ - public void findLocalMatch(ProducteevTaskContainer 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.ID.eq(remoteTask.pdvTask.getValue(ProducteevTask.ID))))); - 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(task.pdvTask); - task.pdvTask.setValue(Metadata.KEY, ProducteevTask.METADATA_KEY); - 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), - // FIXME: Constant from other plugin shouldnt be used - MetadataCriteria.withKey(MilkNote.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 ProducteevTaskContainer(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.ID, ProducteevTask.DASHBOARD_ID).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; - } - - // --- list methods - - private final Map dashboardCache = - Collections.synchronizedMap(new SoftHashMap()); - - /** - * Get dashboard name by dashboard id - * @param dashboardId - * @return null if no dashboard by this id exists, otherwise dashboard name - */ - public String getDashboardName(long dashboardId) { - if(dashboardCache.containsKey(dashboardId)) - return dashboardCache.get(dashboardId); - - TodorooCursor cursor = prodDashboardDao.query(Query.select( - ProducteevDashboard.NAME).where(ProducteevDashboard.ID.eq(dashboardId))); - try { - if(cursor.getCount() == 0) { - dashboardCache.put(dashboardId, null); - return null; - } - cursor.moveToFirst(); - String name = cursor.get(ProducteevDashboard.NAME); - dashboardCache.put(dashboardId, name); - return name; - } finally { - cursor.close(); - } - } -} +/** + * 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 org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +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.StoreObjectDao; +import com.todoroo.astrid.dao.StoreObjectDao.StoreObjectCriteria; +import com.todoroo.astrid.dao.TaskDao; +import com.todoroo.astrid.dao.TaskDao.TaskCriteria; +import com.todoroo.astrid.model.Metadata; +import com.todoroo.astrid.model.StoreObject; +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 { + + // --- 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; + + @Autowired + private StoreObjectDao storeObjectDao; + + private final ProducteevUtilities preferences = ProducteevUtilities.INSTANCE; + + 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)); + metadataDao.deleteWhere(Metadata.KEY.eq(ProducteevNote.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.ID.gt(0))))), + TaskCriteria.isActive())).groupBy(Task.ID)); + } + + /** + * Gets tasks that were modified since last sync + * @param properties + * @return null if never sync'd + */ + public TodorooCursor getLocallyUpdated(Property[] properties) { + long lastSyncDate = preferences.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))).groupBy(Task.ID)); + } + + /** + * Searches for a local task with same remote id, updates this task's id + * @param remoteTask + */ + public void findLocalMatch(ProducteevTaskContainer 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.ID.eq(remoteTask.pdvTask.getValue(ProducteevTask.ID))))); + 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(task.pdvTask); + task.pdvTask.setValue(Metadata.KEY, ProducteevTask.METADATA_KEY); + 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(MilkNote.METADATA_KEY), // to sync rmilk notes + MetadataCriteria.withKey(ProducteevNote.METADATA_KEY))))); + try { + for(metadataCursor.moveToFirst(); !metadataCursor.isAfterLast(); metadataCursor.moveToNext()) { + metadata.add(new Metadata(metadataCursor)); + } + } finally { + metadataCursor.close(); + } + + return new ProducteevTaskContainer(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.ID, ProducteevTask.DASHBOARD_ID).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; + } + + // --- dashboard methods + + private StoreObject[] dashboards = null; + + /** + * Reads dashboards + */ + private void readDashboards() { + if(dashboards != null) + return; + + TodorooCursor cursor = storeObjectDao.query(Query.select(StoreObject.PROPERTIES). + where(StoreObjectCriteria.byType(ProducteevDashboard.TYPE))); + try { + dashboards = new StoreObject[cursor.getCount()]; + for(int i = 0; i < dashboards.length; i++) { + cursor.moveToNext(); + StoreObject dashboard = new StoreObject(cursor); + dashboards[i] = dashboard; + } + } finally { + cursor.close(); + } + } + + /** + * @return a list of dashboards + */ + public StoreObject[] getDashboards() { + readDashboards(); + return dashboards; + } + + /** + * Reads dashboards + * @throws JSONException + */ + @SuppressWarnings("nls") + public void updateDashboards(JSONArray changedDashboards) throws JSONException { + readDashboards(); + for(int i = 0; i < changedDashboards.length(); i++) { + JSONObject remote = changedDashboards.getJSONObject(i).getJSONObject("dashboard"); + long id = remote.getLong("id_dashboard"); + StoreObject local = null; + for(StoreObject dashboard : dashboards) { + if(dashboard.getValue(ProducteevDashboard.REMOTE_ID).equals(id)) { + local = dashboard; + break; + } + } + + if(remote.getInt("deleted") != 0) { + if(local != null) + storeObjectDao.delete(local.getId()); + continue; + } + + if(local == null) + local = new StoreObject(); + local.setValue(StoreObject.TYPE, ProducteevDashboard.TYPE); + local.setValue(ProducteevDashboard.REMOTE_ID, id); + local.setValue(ProducteevDashboard.NAME, remote.getString("title")); + + StringBuilder users = new StringBuilder(); + JSONArray accessList = remote.getJSONArray("accesslist"); + for(int j = 0; j < accessList.length(); j++) { + JSONObject user = accessList.getJSONObject(j).getJSONObject("user"); + users.append(user.getLong("id_user")).append(','). + append(user.getString("firstName")).append(' '). + append(user.getString("lastName")).append(';'); + } + local.setValue(ProducteevDashboard.USERS, users.toString()); + storeObjectDao.persist(local); + } + + // clear dashboard cache + dashboards = null; + } +} diff --git a/astrid/plugin-src/com/todoroo/astrid/producteev/sync/ProducteevDatabase.java b/astrid/plugin-src/com/todoroo/astrid/producteev/sync/ProducteevDatabase.java deleted file mode 100644 index bcddb43bd..000000000 --- a/astrid/plugin-src/com/todoroo/astrid/producteev/sync/ProducteevDatabase.java +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright (c) 2009, Todoroo Inc - * All Rights Reserved - * http://www.todoroo.com - */ -package com.todoroo.astrid.producteev.sync; - -import com.todoroo.andlib.data.AbstractDatabase; -import com.todoroo.andlib.data.GenericDao; -import com.todoroo.andlib.data.Table; - -/** - * Database wrapper - * - * @author Tim Su - * - */ -@SuppressWarnings("nls") -public class ProducteevDatabase extends AbstractDatabase { - - // --- constants - - /** - * Database version number. This variable must be updated when database - * tables are updated, as it determines whether a database needs updating. - */ - public static final int VERSION = 1; - - /** - * Database name (must be unique) - */ - private static final String NAME = "producteev"; - - /** - * List of table/ If you're adding a new table, add it to this list and - * also make sure that our SQLite helper does the right thing. - */ - public static final Table[] TABLES = new Table[] { - ProducteevDashboard.TABLE, - }; - - // --- implementation - - private final GenericDao dao = new GenericDao(ProducteevDashboard.class, this); - - @Override - protected String getName() { - return NAME; - } - - @Override - protected int getVersion() { - return VERSION; - } - - @Override - public Table[] getTables() { - return TABLES; - } - - public GenericDao getDao() { - return dao; - } - - @Override - protected void onCreateTables() { - // do nothing - } - - @Override - protected boolean onUpgrade(int oldVersion, int newVersion) { - return false; - } - -} - 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 7b909229a..9c9a637a7 100644 --- a/astrid/plugin-src/com/todoroo/astrid/producteev/sync/ProducteevSyncProvider.java +++ b/astrid/plugin-src/com/todoroo/astrid/producteev/sync/ProducteevSyncProvider.java @@ -1,576 +1,638 @@ -/** - * 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.HashMap; -import java.util.HashSet; - -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - -import android.app.Activity; -import android.app.Notification; -import android.app.PendingIntent; -import android.content.Context; -import android.content.Intent; -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.TaskContainer; -import com.todoroo.astrid.common.SyncProvider; -import com.todoroo.astrid.model.Metadata; -import com.todoroo.astrid.model.Task; -import com.todoroo.astrid.producteev.ProducteevLoginActivity; -import com.todoroo.astrid.producteev.ProducteevPreferences; -import com.todoroo.astrid.producteev.ProducteevUtilities; -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; - -@SuppressWarnings("nls") -public class ProducteevSyncProvider extends SyncProvider { - - private ProducteevDataService dataService = null; - private ProducteevInvoker invoker = null; - private long defaultDashboard; - private final ProducteevUtilities preferences = ProducteevUtilities.INSTANCE; - - /** map of producteev labels to id's */ - private final HashMap labelMap = new HashMap(); - - static { - AstridDependencyInjector.initialize(); - } - - @Autowired - protected ExceptionService exceptionService; - - @Autowired - protected DialogUtilities dialogUtilities; - - public ProducteevSyncProvider() { - super(); - DependencyInjectionService.getInstance().inject(this); - } - - // ---------------------------------------------------------------------- - // ------------------------------------------------------- public methods - // ---------------------------------------------------------------------- - - /** - * Sign out of service, deleting all synchronization metadata - */ - public void signOut() { - preferences.setToken(null); - preferences.clearLastSyncDate(); - - dataService = ProducteevDataService.getInstance(); - dataService.clearMetadata(); - } - - // ---------------------------------------------------------------------- - // ------------------------------------------------------- authentication - // ---------------------------------------------------------------------- - - /** - * Deal with a synchronization exception. If requested, will show an error - * to the user (unless synchronization is happening in background) - * - * @param context - * @param tag - * error tag - * @param e - * exception - * @param showError - * whether to display a dialog - */ - @Override - protected void handleException(String tag, Exception e, boolean showError) { - preferences.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 ApiServiceException) && e instanceof IOException) { - exceptionService.reportError(tag + "-ioexception", e); //$NON-NLS-1$ - if(showError) { - Context context = ContextManager.getContext(); - showError(context, e, context.getString(R.string.producteev_ioerror)); - } - } else { - 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 = ProducteevDataService.getInstance(); - - // authenticate the user. this will automatically call the next step - authenticate(); - } - - /** - * Perform authentication with RTM. Will open the SyncBrowser if necessary - */ - private void authenticate() { - FlurryAgent.onEvent("producteev-started"); - - preferences.recordSyncStart(); - - try { - String authToken = preferences.getToken(); - invoker = getInvoker(); - - String email = Preferences.getStringValue(R.string.producteev_PPr_email); - String password = Preferences.getStringValue(R.string.producteev_PPr_password); - - // check if we have a token & it works - if(authToken != null) { - invoker.setCredentials(authToken, email, password); - performSync(); - } else { - if (email == null && password == null) { - // display login-activity - final Context context = ContextManager.getContext(); - Intent intent = new Intent(context, ProducteevLoginActivity.class); - if(context instanceof Activity) - ((Activity)context).startActivityForResult(intent, 0); - else { - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - context.startActivity(intent); - } - } else { - invoker.authenticate(email, password); - preferences.setToken(invoker.getToken()); - performSync(); - } - } - } catch (IllegalStateException e) { - // occurs when application was closed - } catch (Exception e) { - handleException("pdv-authenticate", e, true); - } finally { - preferences.stopOngoing(); - } - } - - public static ProducteevInvoker getInvoker() { - String z = stripslashes(0, "71o3346pr40o5o4nt4n7t6n287t4op28","2"); - String v = stripslashes(2, "9641n76n9s1736q1578q1o1337q19233","4ae"); - return new ProducteevInvoker(z, v); - } - - // ---------------------------------------------------------------------- - // ----------------------------------------------------- synchronization! - // ---------------------------------------------------------------------- - - protected void performSync() { - try { - // load user information - JSONObject user = invoker.usersView(null); - defaultDashboard = user.getJSONObject("user").getLong("default_dashboard"); - - // get labels - JSONArray labels = invoker.labelsShowList(defaultDashboard, null); - readLabels(labels); - - // read all tasks - String lastServerSync = preferences.getLastServerSync(); - if(lastServerSync != null) - lastServerSync = lastServerSync.substring(0, lastServerSync.lastIndexOf(' ')); - JSONArray tasks = invoker.tasksShowList(defaultDashboard, lastServerSync); - - SyncData syncData = populateSyncData(tasks); - try { - synchronizeTasks(syncData); - } finally { - syncData.localCreated.close(); - syncData.localUpdated.close(); - } - - preferences.setLastServerSync(invoker.time()); - preferences.recordSuccessfulSync(); - - FlurryAgent.onEvent("pdv-sync-finished"); //$NON-NLS-1$ - } catch (IllegalStateException e) { - // occurs when application was closed - } catch (Exception e) { - handleException("pdv-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.REMINDER_FLAGS, - Task.NOTES, - }; - - /** - * Populate SyncData data structure - * @throws JSONException - */ - 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); - - // read json response - ArrayList remoteTasks = new ArrayList(tasks.length()); - for(int i = 0; i < tasks.length(); i++) { - ProducteevTaskContainer remote = parseRemoteTask(tasks.getJSONObject(i)); - dataService.findLocalMatch(remote); - remoteTasks.add(remote); - } - - return new SyncData(remoteTasks, localCreated, localUpdated); - } - - // ---------------------------------------------------------------------- - // ------------------------------------------------- create / push / pull - // ---------------------------------------------------------------------- - - @Override - protected void create(ProducteevTaskContainer local) throws IOException { - Task localTask = local.task; - long dashboard = defaultDashboard; - if(local.pdvTask.containsNonNullValue(ProducteevTask.DASHBOARD_ID)) - dashboard = local.pdvTask.getValue(ProducteevTask.DASHBOARD_ID); - JSONObject response = invoker.tasksCreate(localTask.getValue(Task.TITLE), - null, dashboard, createDeadline(localTask), createReminder(localTask), - localTask.isCompleted() ? 2 : 1, createStars(localTask)); - ProducteevTaskContainer newRemoteTask; - try { - newRemoteTask = parseRemoteTask(response); - } catch (JSONException e) { - throw new ApiResponseParseException(e); - } - transferIdentifiers(newRemoteTask, local); - push(local, newRemoteTask); - } - - /** 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(); - - if(remoteTask.has("task")) - remoteTask = remoteTask.getJSONObject("task"); - - task.setValue(Task.TITLE, ApiUtilities.decode(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, ApiUtilities.decode(label.getString("title"))); - metadata.add(tagData); - } - - JSONArray notes = remoteTask.getJSONArray("notes"); - for(int i = notes.length() - 1; i >= 0; i--) { - JSONObject note = notes.getJSONObject(i).getJSONObject("note"); - metadata.add(ProducteevNote.create(note)); - } - - ProducteevTaskContainer container = new ProducteevTaskContainer(task, metadata, remoteTask); - - return container; - } - - @Override - 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$ - - JSONObject remote = invoker.tasksView(task.pdvTask.getValue(ProducteevTask.ID)); - try { - return parseRemoteTask(remote); - } 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 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); - - // 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)); - } - - try { - if(!localTags.equals(remoteTags)) { - HashSet toAdd = new HashSet(localTags); - toAdd.removeAll(remoteTags); - HashSet toRemove = remoteTags; - toRemove.removeAll(localTags); - - if(toAdd.size() > 0) { - for(String label : toAdd) { - if(!labelMap.containsKey(label)) { - JSONObject result = invoker.labelsCreate(defaultDashboard, label).getJSONObject("label"); - labelMap.put(ApiUtilities.decode(result.getString("title")), result.getLong("id_label")); - } - invoker.tasksSetLabel(idTask, labelMap.get(label)); - } - } - - if(toRemove.size() > 0) { - for(String label : toRemove) { - if(!labelMap.containsKey(label)) - continue; - invoker.tasksUnsetLabel(idTask, labelMap.get(label)); - } - } - } - - // notes - if(!TextUtils.isEmpty(local.task.getValue(Task.NOTES))) { - String note = local.task.getValue(Task.NOTES); - JSONObject result = invoker.tasksNoteCreate(idTask, note); - 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); - } - - if(remerge) { - remote = pull(local); - remote.task.setId(local.task.getId()); - write(remote); - } - } - - - // ---------------------------------------------------------------------- - // --------------------------------------------------------- read / write - // ---------------------------------------------------------------------- - - @Override - protected ProducteevTaskContainer read(TodorooCursor 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 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; - } - - /** - * 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)); - String time = ApiUtilities.unixTimeToProducteev(task.getValue(Task.DUE_DATE)); - return time.substring(0, time.lastIndexOf(' ')); - } - - /** - * 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 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; - } - - - /** - * Read labels into label map - * @throws JSONException - * @throws ApiServiceException - * @throws IOException - */ - private void readLabels(JSONArray labels) throws JSONException, ApiServiceException, IOException { - for(int i = 0; i < labels.length(); i++) { - JSONObject label = labels.getJSONObject(i).getJSONObject("label"); - labelMap.put(ApiUtilities.decode(label.getString("title")), label.getLong("id_label")); - } - } - - // ---------------------------------------------------------------------- - // ------------------------------------------------------- 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)_))); - } - - -} +/** + * 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.HashMap; +import java.util.HashSet; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import android.app.Activity; +import android.app.Notification; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +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.TaskContainer; +import com.todoroo.astrid.common.SyncProvider; +import com.todoroo.astrid.model.Metadata; +import com.todoroo.astrid.model.StoreObject; +import com.todoroo.astrid.model.Task; +import com.todoroo.astrid.producteev.ProducteevLoginActivity; +import com.todoroo.astrid.producteev.ProducteevPreferences; +import com.todoroo.astrid.producteev.ProducteevUtilities; +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; + +@SuppressWarnings("nls") +public class ProducteevSyncProvider extends SyncProvider { + + private ProducteevDataService dataService = null; + private ProducteevInvoker invoker = null; + private final ProducteevUtilities preferences = ProducteevUtilities.INSTANCE; + + /** map of producteev labels to id's */ + private final HashMap labelMap = new HashMap(); + + static { + AstridDependencyInjector.initialize(); + } + + @Autowired + protected ExceptionService exceptionService; + + @Autowired + protected DialogUtilities dialogUtilities; + + public ProducteevSyncProvider() { + super(); + DependencyInjectionService.getInstance().inject(this); + } + + // ---------------------------------------------------------------------- + // ------------------------------------------------------- public methods + // ---------------------------------------------------------------------- + + /** + * Sign out of service, deleting all synchronization metadata + */ + public void signOut() { + preferences.setToken(null); + preferences.clearLastSyncDate(); + + dataService = ProducteevDataService.getInstance(); + dataService.clearMetadata(); + } + + // ---------------------------------------------------------------------- + // ------------------------------------------------------- authentication + // ---------------------------------------------------------------------- + + /** + * Deal with a synchronization exception. If requested, will show an error + * to the user (unless synchronization is happening in background) + * + * @param context + * @param tag + * error tag + * @param e + * exception + * @param showError + * whether to display a dialog + */ + @Override + protected void handleException(String tag, Exception e, boolean showError) { + preferences.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 ApiServiceException) && e instanceof IOException) { + exceptionService.reportError(tag + "-ioexception", e); //$NON-NLS-1$ + if(showError) { + Context context = ContextManager.getContext(); + showError(context, e, context.getString(R.string.producteev_ioerror)); + } + } else { + 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 = ProducteevDataService.getInstance(); + + // authenticate the user. this will automatically call the next step + authenticate(); + } + + /** + * Perform authentication with RTM. Will open the SyncBrowser if necessary + */ + private void authenticate() { + FlurryAgent.onEvent("producteev-started"); + + preferences.recordSyncStart(); + + try { + String authToken = preferences.getToken(); + invoker = getInvoker(); + + String email = Preferences.getStringValue(R.string.producteev_PPr_email); + String password = Preferences.getStringValue(R.string.producteev_PPr_password); + + // check if we have a token & it works + if(authToken != null) { + invoker.setCredentials(authToken, email, password); + performSync(); + } else { + if (email == null && password == null) { + // display login-activity + final Context context = ContextManager.getContext(); + Intent intent = new Intent(context, ProducteevLoginActivity.class); + if(context instanceof Activity) + ((Activity)context).startActivityForResult(intent, 0); + else { + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + context.startActivity(intent); + } + } else { + invoker.authenticate(email, password); + preferences.setToken(invoker.getToken()); + performSync(); + } + } + } catch (IllegalStateException e) { + // occurs when application was closed + } catch (Exception e) { + handleException("pdv-authenticate", e, true); + } finally { + preferences.stopOngoing(); + } + } + + public static ProducteevInvoker getInvoker() { + String z = stripslashes(0, "71o3346pr40o5o4nt4n7t6n287t4op28","2"); + String v = stripslashes(2, "9641n76n9s1736q1578q1o1337q19233","4ae"); + return new ProducteevInvoker(z, v); + } + + // ---------------------------------------------------------------------- + // ----------------------------------------------------- synchronization! + // ---------------------------------------------------------------------- + + protected void performSync() { + try { + // load user information + JSONObject user = invoker.usersView(null).getJSONObject("user"); + saveUserData(user); + + String lastServerSync = Preferences.getStringValue(ProducteevUtilities.PREF_SERVER_LAST_SYNC); + if(lastServerSync != null) + lastServerSync = lastServerSync.substring(0, lastServerSync.lastIndexOf(' ')); + + // read dashboards + JSONArray dashboards = invoker.dashboardsShowList(lastServerSync); + dataService.updateDashboards(dashboards); + + // read labels and tasks for each dashboard + ArrayList remoteTasks = new ArrayList(); + for(StoreObject dashboard : dataService.getDashboards()) { + long dashboardId = dashboard.getValue(ProducteevDashboard.REMOTE_ID); + JSONArray labels = invoker.labelsShowList(dashboardId, null); + readLabels(labels); + + JSONArray tasks = invoker.tasksShowList(dashboardId, lastServerSync); + for(int i = 0; i < tasks.length(); i++) { + ProducteevTaskContainer remote = parseRemoteTask(tasks.getJSONObject(i)); + dataService.findLocalMatch(remote); + remoteTasks.add(remote); + } + } + + SyncData syncData = populateSyncData(remoteTasks); + try { + synchronizeTasks(syncData); + } finally { + syncData.localCreated.close(); + syncData.localUpdated.close(); + } + + Preferences.setString(ProducteevUtilities.PREF_SERVER_LAST_SYNC, invoker.time()); + preferences.recordSuccessfulSync(); + + FlurryAgent.onEvent("pdv-sync-finished"); //$NON-NLS-1$ + } catch (IllegalStateException e) { + // occurs when application was closed + } catch (Exception e) { + handleException("pdv-sync", e, true); //$NON-NLS-1$ + } + } + + // ---------------------------------------------------------------------- + // ------------------------------------------------------------ sync data + // ---------------------------------------------------------------------- + + private void saveUserData(JSONObject user) throws JSONException { + long defaultDashboard = user.getLong("default_dashboard"); + long userId = user.getLong("id_user"); + Preferences.setLong(ProducteevUtilities.PREF_DEFAULT_DASHBOARD, defaultDashboard); + Preferences.setLong(ProducteevUtilities.PREF_USER_ID, userId); + } + + // 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.REMINDER_FLAGS, + Task.NOTES, + }; + + /** + * Populate SyncData data structure + * @throws JSONException + */ + private SyncData populateSyncData(ArrayList remoteTasks) throws JSONException { + // fetch locally created tasks + TodorooCursor localCreated = dataService.getLocallyCreated(PROPERTIES); + + // fetch locally updated tasks + TodorooCursor localUpdated = dataService.getLocallyUpdated(PROPERTIES); + + return new SyncData(remoteTasks, localCreated, localUpdated); + } + + // ---------------------------------------------------------------------- + // ------------------------------------------------- create / push / pull + // ---------------------------------------------------------------------- + + @Override + protected void create(ProducteevTaskContainer local) throws IOException { + Task localTask = local.task; + long dashboard = ProducteevUtilities.INSTANCE.getDefaultDashboard(); + if(local.pdvTask.containsNonNullValue(ProducteevTask.DASHBOARD_ID)) + dashboard = local.pdvTask.getValue(ProducteevTask.DASHBOARD_ID); + + if(dashboard == ProducteevUtilities.DASHBOARD_NO_SYNC) { + // set a bogus task id, then return without creating + local.pdvTask.setValue(ProducteevTask.ID, 1L); + return; + } + + JSONObject response = invoker.tasksCreate(localTask.getValue(Task.TITLE), + null, dashboard, createDeadline(localTask), createReminder(localTask), + localTask.isCompleted() ? 2 : 1, createStars(localTask)); + ProducteevTaskContainer newRemoteTask; + try { + newRemoteTask = parseRemoteTask(response); + } catch (JSONException e) { + throw new ApiResponseParseException(e); + } + transferIdentifiers(newRemoteTask, local); + push(local, newRemoteTask); + } + + /** 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(); + + if(remoteTask.has("task")) + remoteTask = remoteTask.getJSONObject("task"); + + task.setValue(Task.TITLE, ApiUtilities.decode(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, ApiUtilities.decode(label.getString("title"))); + metadata.add(tagData); + } + + JSONArray notes = remoteTask.getJSONArray("notes"); + for(int i = notes.length() - 1; i >= 0; i--) { + JSONObject note = notes.getJSONObject(i).getJSONObject("note"); + metadata.add(ProducteevNote.create(note)); + } + + ProducteevTaskContainer container = new ProducteevTaskContainer(task, metadata, remoteTask); + + return container; + } + + @Override + 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$ + + JSONObject remote = invoker.tasksView(task.pdvTask.getValue(ProducteevTask.ID)); + try { + return parseRemoteTask(remote); + } 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 push(ProducteevTaskContainer local, ProducteevTaskContainer remote) throws IOException { + long idTask = local.pdvTask.getValue(ProducteevTask.ID); + long idDashboard = local.pdvTask.getValue(ProducteevTask.DASHBOARD_ID); + + // if local is marked do not sync, handle accordingly + if(idDashboard == ProducteevUtilities.DASHBOARD_NO_SYNC) { + if(idTask != 1) + invoker.tasksDelete(idTask); + return; + } + + // fetch remote task for comparison + if(remote == null) + remote = pull(local); + + // 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); + } + + // dashboard + if(remote != null && idDashboard != remote.pdvTask.getValue(ProducteevTask.DASHBOARD_ID)) { + invoker.tasksSetWorkspace(idTask, idDashboard); + remote = pull(local); + } + + // core properties + 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); + + + try { + // tags + transmitTags(local, remote, idTask, idDashboard); + + // notes + if(!TextUtils.isEmpty(local.task.getValue(Task.NOTES))) { + String note = local.task.getValue(Task.NOTES); + JSONObject result = invoker.tasksNoteCreate(idTask, note); + 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); + } + } + + /** + * Transmit tags + * + * @param local + * @param remote + * @param idTask + * @param idDashboard + * @throws ApiServiceException + * @throws JSONException + * @throws IOException + */ + private void transmitTags(ProducteevTaskContainer local, + ProducteevTaskContainer remote, long idTask, long idDashboard) throws ApiServiceException, JSONException, IOException { + 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)) { + HashSet toAdd = new HashSet(localTags); + toAdd.removeAll(remoteTags); + HashSet toRemove = remoteTags; + toRemove.removeAll(localTags); + + if(toAdd.size() > 0) { + for(String label : toAdd) { + if(!labelMap.containsKey(label)) { + JSONObject result = invoker.labelsCreate(idDashboard, label).getJSONObject("label"); + putLabelIntoCache(result); + } + invoker.tasksSetLabel(idTask, labelMap.get(label)); + } + } + + if(toRemove.size() > 0) { + for(String label : toRemove) { + if(!labelMap.containsKey(label)) + continue; + invoker.tasksUnsetLabel(idTask, labelMap.get(label)); + } + } + } + } + + // ---------------------------------------------------------------------- + // --------------------------------------------------------- read / write + // ---------------------------------------------------------------------- + + @Override + protected ProducteevTaskContainer read(TodorooCursor 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 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; + } + + /** + * 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)); + String time = ApiUtilities.unixTimeToProducteev(task.getValue(Task.DUE_DATE)); + return time.substring(0, time.lastIndexOf(' ')); + } + + /** + * 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 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; + } + + public class ProducteevLabel { + public String name; + public long dashboard; + } + + /** + * Read labels into label map + * @param dashboardId + * @throws JSONException + * @throws ApiServiceException + * @throws IOException + */ + private void readLabels(JSONArray labels) throws JSONException, ApiServiceException, IOException { + for(int i = 0; i < labels.length(); i++) { + JSONObject label = labels.getJSONObject(i).getJSONObject("label"); + putLabelIntoCache(label); + } + } + + /** + * Puts a single label into the cache + * @param dashboardId + * @param label + * @throws JSONException + */ + private void putLabelIntoCache(JSONObject label) + throws JSONException { + ProducteevLabel labelContainer = new ProducteevLabel(); + labelContainer.name = ApiUtilities.decode(label.getString("title")); + labelContainer.dashboard = label.getLong("id_dashboard"); + labelMap.put(labelContainer, label.getLong("id_label")); + } + + // ---------------------------------------------------------------------- + // ------------------------------------------------------- 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/sync/ProducteevTask.java b/astrid/plugin-src/com/todoroo/astrid/producteev/sync/ProducteevTask.java index 058770195..525fc5e8a 100644 --- a/astrid/plugin-src/com/todoroo/astrid/producteev/sync/ProducteevTask.java +++ b/astrid/plugin-src/com/todoroo/astrid/producteev/sync/ProducteevTask.java @@ -1,24 +1,32 @@ -package com.todoroo.astrid.producteev.sync; - -import com.todoroo.andlib.data.Property.LongProperty; -import com.todoroo.astrid.model.Metadata; - -/** - * Metadata entries for a Producteev Task - * @author Tim Su - * - */ -public class ProducteevTask { - - /** metadata key */ - public static final String METADATA_KEY = "producteev"; //$NON-NLS-1$ - - /** task id in producteev */ - public static final LongProperty ID = new LongProperty(Metadata.TABLE, - Metadata.VALUE1.name); - - /** dashboard id */ - public static final LongProperty DASHBOARD_ID = new LongProperty(Metadata.TABLE, - Metadata.VALUE2.name); - -} +package com.todoroo.astrid.producteev.sync; + +import com.todoroo.andlib.data.Property.LongProperty; +import com.todoroo.astrid.model.Metadata; + +/** + * Metadata entries for a Producteev Task + * @author Tim Su + * + */ +public class ProducteevTask { + + /** metadata key */ + public static final String METADATA_KEY = "producteev"; //$NON-NLS-1$ + + /** task id in producteev */ + public static final LongProperty ID = new LongProperty(Metadata.TABLE, + Metadata.VALUE1.name); + + /** dashboard id */ + public static final LongProperty DASHBOARD_ID = new LongProperty(Metadata.TABLE, + Metadata.VALUE2.name); + + /** creator id */ + public static final LongProperty CREATOR_ID = new LongProperty(Metadata.TABLE, + Metadata.VALUE3.name); + + /** responsible id */ + public static final LongProperty RESPONSIBLE_ID = new LongProperty(Metadata.TABLE, + Metadata.VALUE4.name); + +} 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 9fb03b990..12f15af68 100644 --- a/astrid/plugin-src/com/todoroo/astrid/producteev/sync/ProducteevTaskContainer.java +++ b/astrid/plugin-src/com/todoroo/astrid/producteev/sync/ProducteevTaskContainer.java @@ -35,6 +35,8 @@ public class ProducteevTaskContainer extends TaskContainer { pdvTask.setValue(Metadata.KEY, ProducteevTask.METADATA_KEY); pdvTask.setValue(ProducteevTask.ID, remoteTask.optLong("id_task")); pdvTask.setValue(ProducteevTask.DASHBOARD_ID, remoteTask.optLong("id_dashboard")); + pdvTask.setValue(ProducteevTask.RESPONSIBLE_ID, remoteTask.optLong("id_responsible")); + pdvTask.setValue(ProducteevTask.CREATOR_ID, remoteTask.optLong("id_creator")); } public ProducteevTaskContainer(Task task, ArrayList metadata) { diff --git a/astrid/res/layout/addon_adapter_row.xml b/astrid/res/layout/addon_adapter_row.xml index b3f3cc6c0..c99ea3dc0 100644 --- a/astrid/res/layout/addon_adapter_row.xml +++ b/astrid/res/layout/addon_adapter_row.xml @@ -1,88 +1,88 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/astrid/res/values/strings-producteev.xml b/astrid/res/values/strings-producteev.xml index 77faecaec..7554f104a 100644 --- a/astrid/res/values/strings-producteev.xml +++ b/astrid/res/values/strings-producteev.xml @@ -1,84 +1,87 @@ - - - - - - - - Workspace: %s - - - - - Producteev - - - Default Workspace - - - Do Not Synchronize - - - Default Workspace - - - New tasks will be added to: %s - - - New tasks will not be synchronized by default - - - - - Log In to Producteev - - - Sign in with your existing - Producteev account, or create a new account! - - - Terms & Conditions - - - Sign In - - - Create New User - - - E-mail - - - Password - - - Confirm Password - - - First Name - - - Last Name - - - Error: fill out all fields! - - - Error: passwords don\'t match! - - - - - Astrid: Producteev - - - Connection Error! Check your Internet connection. - - - E-Mail was not specified! - - - Password was not specified! - - - + + + + + + + + Workspace: %s + + + Responsible: %s + + + + + Producteev + + + Default Workspace + + + Do Not Synchronize + + + Default Workspace + + + New tasks will be added to: %s + + + New tasks will not be synchronized by default + + + + + Log In to Producteev + + + Sign in with your existing + Producteev account, or create a new account! + + + Terms & Conditions + + + Sign In + + + Create New User + + + E-mail + + + Password + + + Confirm Password + + + First Name + + + Last Name + + + Error: fill out all fields! + + + Error: passwords don\'t match! + + + + + Astrid: Producteev + + + Connection Error! Check your Internet connection. + + + E-Mail was not specified! + + + Password was not specified! + + + diff --git a/astrid/src/com/todoroo/astrid/activity/TaskEditActivity.java b/astrid/src/com/todoroo/astrid/activity/TaskEditActivity.java index bec91debe..ac5c45f99 100644 --- a/astrid/src/com/todoroo/astrid/activity/TaskEditActivity.java +++ b/astrid/src/com/todoroo/astrid/activity/TaskEditActivity.java @@ -19,8 +19,8 @@ */ package com.todoroo.astrid.activity; -import java.text.SimpleDateFormat; import java.util.ArrayList; +import java.util.Calendar; import java.util.Date; import java.util.LinkedList; import java.util.List; @@ -37,6 +37,7 @@ import android.content.Intent; import android.content.IntentFilter; import android.content.res.Resources; import android.os.Bundle; +import android.text.format.DateUtils; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; @@ -735,8 +736,9 @@ public final class TaskEditActivity extends TabActivity { Task.URGENCY_TODAY); urgencyValues[2] = new UrgencyValue(labels[2], Task.URGENCY_TOMORROW); - String dayAfterTomorrow = new SimpleDateFormat("EEEE").format( //$NON-NLS-1$ - new Date(DateUtilities.now() + 2 * DateUtilities.ONE_DAY)); + String dayAfterTomorrow = DateUtils.getDayOfWeekString( + new Date(DateUtilities.now() + 2 * DateUtilities.ONE_DAY).getDay() + + Calendar.SUNDAY, 0); urgencyValues[3] = new UrgencyValue(dayAfterTomorrow, Task.URGENCY_DAY_AFTER); urgencyValues[4] = new UrgencyValue(labels[4], diff --git a/astrid/src/com/todoroo/astrid/activity/TaskListActivity.java b/astrid/src/com/todoroo/astrid/activity/TaskListActivity.java index 0b6baa8fd..8977788ba 100644 --- a/astrid/src/com/todoroo/astrid/activity/TaskListActivity.java +++ b/astrid/src/com/todoroo/astrid/activity/TaskListActivity.java @@ -354,13 +354,13 @@ public class TaskListActivity extends ListActivity implements OnScrollListener, ServiceConnection refreshConnection = new ServiceConnection() { @Override public void onServiceConnected(ComponentName name, IBinder service) { - System.err.println("connected to service " + name); + System.err.println("connected to service " + name); //$NON-NLS-1$ // } @Override public void onServiceDisconnected(ComponentName name) { // service disconnected, let's refresh - System.err.println("your junk was done, refreshing"); + System.err.println("your junk was done, refreshing"); //$NON-NLS-1$ loadTaskListContent(true); } }; diff --git a/astrid/src/com/todoroo/astrid/dao/Database.java b/astrid/src/com/todoroo/astrid/dao/Database.java index 64ab86ae9..ca057cb39 100644 --- a/astrid/src/com/todoroo/astrid/dao/Database.java +++ b/astrid/src/com/todoroo/astrid/dao/Database.java @@ -6,9 +6,11 @@ package com.todoroo.astrid.dao; import com.todoroo.andlib.data.AbstractDatabase; +import com.todoroo.andlib.data.AbstractModel; import com.todoroo.andlib.data.Property; import com.todoroo.andlib.data.Table; import com.todoroo.astrid.model.Metadata; +import com.todoroo.astrid.model.StoreObject; import com.todoroo.astrid.model.Task; /** @@ -26,7 +28,7 @@ public class Database extends AbstractDatabase { * Database version number. This variable must be updated when database * tables are updated, as it determines whether a database needs updating. */ - public static final int VERSION = 3; + public static final int VERSION = 4; /** * Database name (must be unique) @@ -40,6 +42,7 @@ public class Database extends AbstractDatabase { public static final Table[] TABLES = new Table[] { Task.TABLE, Metadata.TABLE, + StoreObject.TABLE, }; // --- implementation @@ -70,24 +73,52 @@ public class Database extends AbstractDatabase { append(Metadata.TASK.name). append(')'); database.execSQL(sql.toString()); + sql.setLength(0); + + sql.append("CREATE INDEX IF NOT EXISTS so_id ON "). + append(StoreObject.TABLE).append('('). + append(StoreObject.TYPE).append(','). + append(StoreObject.ITEM). + append(')'); + database.execSQL(sql.toString()); + sql.setLength(0); } @Override @edu.umd.cs.findbugs.annotations.SuppressWarnings(value="SF_SWITCH_FALLTHROUGH") protected synchronized boolean onUpgrade(int oldVersion, int newVersion) { + SqlConstructorVisitor visitor = new SqlConstructorVisitor(); switch(oldVersion) { case 1: { - SqlConstructorVisitor visitor = new SqlConstructorVisitor(); database.execSQL("ALTER TABLE " + Task.TABLE.name + " ADD " + Task.RECURRENCE.accept(visitor, null)); } case 2: { - SqlConstructorVisitor visitor = new SqlConstructorVisitor(); for(Property property : new Property[] { Metadata.VALUE2, Metadata.VALUE3, Metadata.VALUE4, Metadata.VALUE5 }) database.execSQL("ALTER TABLE " + Metadata.TABLE.name + " ADD " + property.accept(visitor, null)); } + case 3: { + StringBuilder sql = new StringBuilder(); + sql.append("CREATE TABLE IF NOT EXISTS ").append(StoreObject.TABLE.name).append('('). + append(AbstractModel.ID_PROPERTY).append(" INTEGER PRIMARY KEY AUTOINCREMENT"); + for(Property property : StoreObject.PROPERTIES) { + if(AbstractModel.ID_PROPERTY.name.equals(property.name)) + continue; + sql.append(',').append(property.accept(visitor, null)); + } + sql.append(')'); + database.execSQL(sql.toString()); + sql.setLength(0); + + sql.append("CREATE INDEX IF NOT EXISTS so_id ON "). + append(StoreObject.TABLE).append('('). + append(StoreObject.TYPE).append(','). + append(StoreObject.ITEM). + append(')'); + database.execSQL(sql.toString()); + } return true; } diff --git a/astrid/src/com/todoroo/astrid/dao/StoreObjectDao.java b/astrid/src/com/todoroo/astrid/dao/StoreObjectDao.java new file mode 100644 index 000000000..cffb6c76a --- /dev/null +++ b/astrid/src/com/todoroo/astrid/dao/StoreObjectDao.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2009, Todoroo Inc + * All Rights Reserved + * http://www.todoroo.com + */ +package com.todoroo.astrid.dao; + +import com.todoroo.andlib.data.GenericDao; +import com.todoroo.andlib.service.Autowired; +import com.todoroo.andlib.service.DependencyInjectionService; +import com.todoroo.andlib.sql.Criterion; +import com.todoroo.astrid.model.StoreObject; + +/** + * Data Access layer for {@link StoreObject}-related operations. + * + * @author Tim Su + * + */ +public class StoreObjectDao extends GenericDao { + + @Autowired + private Database database; + + @edu.umd.cs.findbugs.annotations.SuppressWarnings(value="UR_UNINIT_READ") + public StoreObjectDao() { + super(StoreObject.class); + DependencyInjectionService.getInstance().inject(this); + setDatabase(database); + } + + // --- SQL clause generators + + /** + * Generates SQL clauses + */ + public static class StoreObjectCriteria { + + /** Returns all store objects with given type */ + public static Criterion byType(String type) { + return StoreObject.TYPE.eq(type); + } + + /** Returns store object with type and key */ + public static Criterion byTypeAndItem(String type, String item) { + return Criterion.and(byType(type), StoreObject.ITEM.eq(item)); + } + + } + +} + diff --git a/astrid/src/com/todoroo/astrid/model/Metadata.java b/astrid/src/com/todoroo/astrid/model/Metadata.java index 84406a3b2..f1c71abe6 100644 --- a/astrid/src/com/todoroo/astrid/model/Metadata.java +++ b/astrid/src/com/todoroo/astrid/model/Metadata.java @@ -8,10 +8,10 @@ import android.content.ContentValues; import com.todoroo.andlib.data.AbstractModel; import com.todoroo.andlib.data.Property; -import com.todoroo.andlib.data.Table; -import com.todoroo.andlib.data.TodorooCursor; import com.todoroo.andlib.data.Property.LongProperty; import com.todoroo.andlib.data.Property.StringProperty; +import com.todoroo.andlib.data.Table; +import com.todoroo.andlib.data.TodorooCursor; /** * Data Model which represents a piece of metadata associated with a task @@ -48,15 +48,15 @@ public class Metadata extends AbstractModel { public static final StringProperty VALUE2 = new StringProperty( TABLE, "value2"); - /** Metadata Text Value Column 1 */ + /** Metadata Text Value Column 3 */ public static final StringProperty VALUE3 = new StringProperty( TABLE, "value3"); - /** Metadata Text Value Column 1 */ + /** Metadata Text Value Column 4 */ public static final StringProperty VALUE4 = new StringProperty( TABLE, "value4"); - /** Metadata Text Value Column 1 */ + /** Metadata Text Value Column 5 */ public static final StringProperty VALUE5 = new StringProperty( TABLE, "value5"); @@ -95,7 +95,7 @@ public class Metadata extends AbstractModel { // --- parcelable helpers - private static final Creator CREATOR = new ModelCreator(Task.class); + private static final Creator CREATOR = new ModelCreator(Metadata.class); @Override protected Creator getCreator() { diff --git a/astrid/src/com/todoroo/astrid/model/StoreObject.java b/astrid/src/com/todoroo/astrid/model/StoreObject.java new file mode 100644 index 000000000..9d1294cdf --- /dev/null +++ b/astrid/src/com/todoroo/astrid/model/StoreObject.java @@ -0,0 +1,105 @@ +/** + * See the file "LICENSE" for the full license governing this code. + */ +package com.todoroo.astrid.model; + + +import android.content.ContentValues; + +import com.todoroo.andlib.data.AbstractModel; +import com.todoroo.andlib.data.Property; +import com.todoroo.andlib.data.Property.LongProperty; +import com.todoroo.andlib.data.Property.StringProperty; +import com.todoroo.andlib.data.Table; +import com.todoroo.andlib.data.TodorooCursor; + +/** + * Data Model which represents a piece of data unrelated to a task + * + * @author Tim Su + * + */ +@SuppressWarnings("nls") +public class StoreObject extends AbstractModel { + + // --- table + + public static final Table TABLE = new Table("store", StoreObject.class); + + // --- properties + + /** ID */ + public static final LongProperty ID = new LongProperty( + TABLE, ID_PROPERTY_NAME); + + /** Store Type Key */ + public static final StringProperty TYPE = new StringProperty( + TABLE, "type"); + + /** Store Item Key */ + public static final StringProperty ITEM= new StringProperty( + TABLE, "item"); + + /** Store Value Column 1 */ + public static final StringProperty VALUE1 = new StringProperty( + TABLE, "value"); + + /** Store Value Column 2 */ + public static final StringProperty VALUE2 = new StringProperty( + TABLE, "value2"); + + /** Store Value Column 3 */ + public static final StringProperty VALUE3 = new StringProperty( + TABLE, "value3"); + + /** Store Value Column 4 */ + public static final StringProperty VALUE4 = new StringProperty( + TABLE, "value4"); + + /** Store Value Column 5 */ + public static final StringProperty VALUE5 = new StringProperty( + TABLE, "value5"); + + /** List of all properties for this model */ + public static final Property[] PROPERTIES = generateProperties(StoreObject.class); + + // --- defaults + + /** Default values container */ + private static final ContentValues defaultValues = new ContentValues(); + + @Override + public ContentValues getDefaultValues() { + return defaultValues; + } + + // --- data access boilerplate + + public StoreObject() { + super(); + } + + public StoreObject(TodorooCursor cursor) { + this(); + readPropertiesFromCursor(cursor); + } + + public void readFromCursor(TodorooCursor cursor) { + super.readPropertiesFromCursor(cursor); + } + + @Override + public long getId() { + return getIdHelper(ID); + }; + + // --- parcelable helpers + + private static final Creator CREATOR = new ModelCreator(StoreObject.class); + + @Override + protected Creator getCreator() { + return CREATOR; + } + +} diff --git a/astrid/src/com/todoroo/astrid/service/AstridDependencyInjector.java b/astrid/src/com/todoroo/astrid/service/AstridDependencyInjector.java index ea9ec1e4b..4d14d2dc0 100644 --- a/astrid/src/com/todoroo/astrid/service/AstridDependencyInjector.java +++ b/astrid/src/com/todoroo/astrid/service/AstridDependencyInjector.java @@ -17,6 +17,7 @@ import com.todoroo.andlib.utility.DateUtilities; import com.todoroo.andlib.utility.DialogUtilities; import com.todoroo.astrid.dao.Database; import com.todoroo.astrid.dao.MetadataDao; +import com.todoroo.astrid.dao.StoreObjectDao; import com.todoroo.astrid.dao.TaskDao; /** @@ -97,6 +98,7 @@ public class AstridDependencyInjector implements AbstractDependencyInjector { injectables.put("database", Database.class); injectables.put("taskDao", TaskDao.class); injectables.put("metadataDao", MetadataDao.class); + injectables.put("storeObjectDao", StoreObjectDao.class); // com.todoroo.astrid.service injectables.put("taskService", TaskService.class);