From d1ccce4e6861a0279e5bec35f3b178406a9e2a1a Mon Sep 17 00:00:00 2001 From: Arne Jans Date: Mon, 9 Aug 2010 15:59:53 +0200 Subject: [PATCH] done reading/display-side of producteev-dashboard in the TaskDetailExposer at least the reading/displaying part is ready for testing, the part where dashboard-infos are written into metadata-db and dataservice-cache is missing and should be done by tim, i think. --- .../producteev/ProducteevDetailExposer.java | 174 +-- .../producteev/sync/ProducteevDashboard.java | 86 ++ .../sync/ProducteevDataService.java | 431 +++--- .../producteev/sync/ProducteevDatabase.java | 76 ++ .../sync/ProducteevSyncProvider.java | 1188 ++++++++--------- .../producteev/sync/ProducteevTask.java | 48 +- astrid/res/values/strings-producteev.xml | 55 +- 7 files changed, 1140 insertions(+), 918 deletions(-) create mode 100644 astrid/plugin-src/com/todoroo/astrid/producteev/sync/ProducteevDashboard.java create mode 100644 astrid/plugin-src/com/todoroo/astrid/producteev/sync/ProducteevDatabase.java diff --git a/astrid/plugin-src/com/todoroo/astrid/producteev/ProducteevDetailExposer.java b/astrid/plugin-src/com/todoroo/astrid/producteev/ProducteevDetailExposer.java index cfe458c93..222a9de67 100644 --- a/astrid/plugin-src/com/todoroo/astrid/producteev/ProducteevDetailExposer.java +++ b/astrid/plugin-src/com/todoroo/astrid/producteev/ProducteevDetailExposer.java @@ -1,79 +1,95 @@ -/** - * See the file "LICENSE" for the full license governing this code. - */ -package com.todoroo.astrid.producteev; - -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; - -import com.todoroo.andlib.data.TodorooCursor; -import com.todoroo.astrid.adapter.TaskAdapter; -import com.todoroo.astrid.api.AstridApiConstants; -import com.todoroo.astrid.api.DetailExposer; -import com.todoroo.astrid.model.Metadata; -import com.todoroo.astrid.producteev.sync.ProducteevDataService; -import com.todoroo.astrid.producteev.sync.ProducteevNote; - -/** - * Exposes Task Details for Producteev: - * - notes - * - * @author Tim Su - * - */ -public class ProducteevDetailExposer extends BroadcastReceiver implements DetailExposer{ - - @Override - public void onReceive(Context context, Intent intent) { - // if we aren't logged in, don't expose features - if(!ProducteevUtilities.INSTANCE.isLoggedIn()) - return; - - long taskId = intent.getLongExtra(AstridApiConstants.EXTRAS_TASK_ID, -1); - if(taskId == -1) - return; - - boolean extended = intent.getBooleanExtra(AstridApiConstants.EXTRAS_EXTENDED, false); - String taskDetail = getTaskDetails(context, taskId, extended); - if(taskDetail == null) - return; - - Intent broadcastIntent = new Intent(AstridApiConstants.BROADCAST_SEND_DETAILS); - broadcastIntent.putExtra(AstridApiConstants.EXTRAS_ADDON, ProducteevUtilities.IDENTIFIER); - broadcastIntent.putExtra(AstridApiConstants.EXTRAS_TASK_ID, taskId); - broadcastIntent.putExtra(AstridApiConstants.EXTRAS_EXTENDED, extended); - broadcastIntent.putExtra(AstridApiConstants.EXTRAS_RESPONSE, taskDetail); - context.sendBroadcast(broadcastIntent, AstridApiConstants.PERMISSION_READ); - } - - @Override - public String getTaskDetails(Context context, long id, boolean extended) { - - if(!extended) - return null; - - StringBuilder builder = new StringBuilder(); - TodorooCursor notesCursor = ProducteevDataService.getInstance().getTaskNotesCursor(id); - try { - Metadata metadata = new Metadata(); - for(notesCursor.moveToFirst(); !notesCursor.isAfterLast(); notesCursor.moveToNext()) { - metadata.readFromCursor(notesCursor); - builder.append(metadata.getValue(ProducteevNote.MESSAGE)).append(TaskAdapter.DETAIL_SEPARATOR); - } - } finally { - notesCursor.close(); - } - - if(builder.length() == 0) - return null; - String result = builder.toString(); - return result.substring(0, result.length() - TaskAdapter.DETAIL_SEPARATOR.length()); - } - - @Override - public String getPluginIdentifier() { - return ProducteevUtilities.IDENTIFIER; - } - -} +/** + * 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; + } + +} diff --git a/astrid/plugin-src/com/todoroo/astrid/producteev/sync/ProducteevDashboard.java b/astrid/plugin-src/com/todoroo/astrid/producteev/sync/ProducteevDashboard.java new file mode 100644 index 000000000..4fe57074c --- /dev/null +++ b/astrid/plugin-src/com/todoroo/astrid/producteev/sync/ProducteevDashboard.java @@ -0,0 +1,86 @@ +/** + * 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; + } +} 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 949eca637..a77a7273e 100644 --- a/astrid/plugin-src/com/todoroo/astrid/producteev/sync/ProducteevDataService.java +++ b/astrid/plugin-src/com/todoroo/astrid/producteev/sync/ProducteevDataService.java @@ -1,196 +1,235 @@ -/** - * See the file "LICENSE" for the full license governing this code. - */ -package com.todoroo.astrid.producteev.sync; - -import java.util.ArrayList; -import java.util.Random; - -import android.content.Context; - -import com.todoroo.andlib.data.Property; -import com.todoroo.andlib.data.TodorooCursor; -import com.todoroo.andlib.service.Autowired; -import com.todoroo.andlib.service.ContextManager; -import com.todoroo.andlib.service.DependencyInjectionService; -import com.todoroo.andlib.sql.Criterion; -import com.todoroo.andlib.sql.Join; -import com.todoroo.andlib.sql.Query; -import com.todoroo.astrid.dao.MetadataDao; -import com.todoroo.astrid.dao.MetadataDao.MetadataCriteria; -import com.todoroo.astrid.dao.TaskDao; -import com.todoroo.astrid.dao.TaskDao.TaskCriteria; -import com.todoroo.astrid.model.Metadata; -import com.todoroo.astrid.model.Task; -import com.todoroo.astrid.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; - - 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), - 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; - } - -} +/** + * 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(); + } + } +} diff --git a/astrid/plugin-src/com/todoroo/astrid/producteev/sync/ProducteevDatabase.java b/astrid/plugin-src/com/todoroo/astrid/producteev/sync/ProducteevDatabase.java new file mode 100644 index 000000000..bcddb43bd --- /dev/null +++ b/astrid/plugin-src/com/todoroo/astrid/producteev/sync/ProducteevDatabase.java @@ -0,0 +1,76 @@ +/* + * 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 1812499e0..0a9b52ddd 100644 --- a/astrid/plugin-src/com/todoroo/astrid/producteev/sync/ProducteevSyncProvider.java +++ b/astrid/plugin-src/com/todoroo/astrid/producteev/sync/ProducteevSyncProvider.java @@ -1,594 +1,594 @@ -/** - * 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.os.Handler; -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.ProducteevLoginActivity.SyncLoginCallback; -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.api.ServiceInternalException; -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.rmilk_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(); - - String z = stripslashes(0, "71o3346pr40o5o4nt4n7t6n287t4op28","2"); - String v = stripslashes(2, "9641n76n9s1736q1578q1o1337q19233","4ae"); - invoker = new ProducteevInvoker(z, v); - - String email = Preferences.getStringValue(R.string.producteev_PPr_email); - String password = Preferences.getStringValue(R.string.producteev_PPr_password); - - // 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); - ProducteevLoginActivity.setCallback(new SyncLoginCallback() { - public String verifyLogin(final Handler syncLoginHandler, String em, String pass) { - try { - invoker.authenticate(em, pass); - preferences.setToken(invoker.getToken()); - Preferences.setString(R.string.producteev_PPr_email, em); - Preferences.setString(R.string.producteev_PPr_password, pass); - performSync(); - return null; - } catch (Exception e) { - // didn't work - exceptionService.reportError("producteev-verify-login", e); - if(e instanceof ServiceInternalException) - e = ((ServiceInternalException)e).getEnclosedException(); - return context.getString(R.string.producteev_ioerror, e.getMessage()); - } - } - }); - 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(); - } - } - - // ---------------------------------------------------------------------- - // ----------------------------------------------------- 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.os.Handler; +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.ProducteevLoginActivity.SyncLoginCallback; +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.api.ServiceInternalException; +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(); + + String z = stripslashes(0, "71o3346pr40o5o4nt4n7t6n287t4op28","2"); + String v = stripslashes(2, "9641n76n9s1736q1578q1o1337q19233","4ae"); + invoker = new ProducteevInvoker(z, v); + + String email = Preferences.getStringValue(R.string.producteev_PPr_email); + String password = Preferences.getStringValue(R.string.producteev_PPr_password); + + // 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); + ProducteevLoginActivity.setCallback(new SyncLoginCallback() { + public String verifyLogin(final Handler syncLoginHandler, String em, String pass) { + try { + invoker.authenticate(em, pass); + preferences.setToken(invoker.getToken()); + Preferences.setString(R.string.producteev_PPr_email, em); + Preferences.setString(R.string.producteev_PPr_password, pass); + performSync(); + return null; + } catch (Exception e) { + // didn't work + exceptionService.reportError("producteev-verify-login", e); + if(e instanceof ServiceInternalException) + e = ((ServiceInternalException)e).getEnclosedException(); + return context.getString(R.string.producteev_ioerror, e.getMessage()); + } + } + }); + 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(); + } + } + + // ---------------------------------------------------------------------- + // ----------------------------------------------------- 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)_))); + } + + +} 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 2406bce48..058770195 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,24 @@ -package com.todoroo.astrid.producteev.sync; - -import com.todoroo.andlib.data.Property.LongProperty; -import com.todoroo.astrid.model.Metadata; - -/** - * Metadata entries for a Remember The Milk 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); + +} diff --git a/astrid/res/values/strings-producteev.xml b/astrid/res/values/strings-producteev.xml index fed7a3f02..740ce6faa 100644 --- a/astrid/res/values/strings-producteev.xml +++ b/astrid/res/values/strings-producteev.xml @@ -1,25 +1,30 @@ - - - - - - - - Producteev - - - - - Astrid: Producteev - - - Connection Error! Check your Internet connection. - - - E-Mail was not specified! - - - Password was not specified! - - - + + + + + + + + Producteev dashboard: %s + + + + + Producteev + + + + + Astrid: Producteev + + + Connection Error! Check your Internet connection. + + + E-Mail was not specified! + + + Password was not specified! + + +