/** * See the file "LICENSE" for the full license governing this code. */ package com.todoroo.astrid.sync; import java.util.ArrayList; import android.content.Context; import com.todoroo.andlib.data.Property; import com.todoroo.andlib.data.TodorooCursor; import com.todoroo.andlib.sql.Criterion; import com.todoroo.andlib.sql.Order; import com.todoroo.andlib.sql.Query; import com.todoroo.astrid.data.Metadata; import com.todoroo.astrid.data.MetadataApiDao; import com.todoroo.astrid.data.MetadataApiDao.MetadataCriteria; import com.todoroo.astrid.data.Task; import com.todoroo.astrid.data.TaskApiDao; import com.todoroo.astrid.data.TaskApiDao.TaskCriteria; abstract public class SyncMetadataService { /** metadata key of tag add-on */ public static final String TAG_KEY = "tags-tag"; //$NON-NLS-1$ // --- instance variables protected final TaskApiDao taskDao; protected final MetadataApiDao metadataDao; // --- abstract methods /** @return metadata key identifying this sync provider's metadata */ abstract public String getMetadataKey(); /** @return sync provider utilities */ abstract public SyncProviderUtilities getUtilities(); /** create a task container based on the given data */ abstract public TYPE createContainerFromLocalTask(Task task, ArrayList metadata); /** @return criterion for matching all metadata keys that your provider synchronizes */ abstract public Criterion getMetadataCriteria(); /** @return criterion for finding local matches of sync container in task database */ abstract public Criterion getLocalMatchCriteria(TYPE remoteTask); /** @return criterion for matching metadata that indicate remote task exists */ abstract public Criterion getMetadataWithRemoteId(); // --- implementation public SyncMetadataService(Context context) { taskDao = new TaskApiDao(context); metadataDao = new MetadataApiDao(context); } /** * Clears metadata information. Used when user logs out of sync provider */ public void clearMetadata() { metadataDao.deleteWhere(Metadata.KEY.eq(getMetadataKey())); } /** * Gets cursor across all task metadata for joining * * @return cursor */ private TodorooCursor getRemoteTaskMetadata() { return metadataDao.query(Query.select(Metadata.TASK).where( Criterion.and(MetadataCriteria.withKey(getMetadataKey()), getMetadataWithRemoteId())).orderBy(Order.asc(Metadata.TASK))); } /** * Gets tasks that were created since last sync * @param properties * @return */ public TodorooCursor getLocallyCreated(Property... properties) { TodorooCursor tasks = taskDao.query(Query.select(Task.ID).where( TaskCriteria.isActive()).orderBy(Order.asc(Task.ID))); return joinWithMetadata(tasks, false, properties); } /** * Gets tasks that were modified since last sync * @param properties * @return null if never sync'd */ public TodorooCursor getLocallyUpdated(Property... properties) { TodorooCursor tasks; long lastSyncDate = getUtilities().getLastSyncDate(); if(lastSyncDate == 0) tasks = taskDao.query(Query.select(Task.ID).where(Criterion.none)); else tasks = taskDao.query(Query.select(Task.ID).where(Criterion.and( Task.MODIFICATION_DATE.gt(lastSyncDate), Task.LAST_SYNC.lte(lastSyncDate))).orderBy(Order.asc(Task.ID))); return joinWithMetadata(tasks, true, properties); } private TodorooCursor joinWithMetadata(TodorooCursor tasks, boolean both, Property... properties) { try { TodorooCursor metadata = getRemoteTaskMetadata(); try { ArrayList matchingRows = new ArrayList(); joinRows(tasks, metadata, matchingRows, both); return taskDao.query(Query.select(properties).where(Task.ID.in( matchingRows.toArray(new Long[matchingRows.size()])))); } finally { metadata.close(); } } finally { tasks.close(); } } /** * Join rows from two cursors on the first column, assuming its an id column * @param left * @param right * @param matchingRows * @param both - if false, returns rows no right row exists, if true, * returns rows where both exist */ private static void joinRows(TodorooCursor left, TodorooCursor right, ArrayList matchingRows, boolean both) { left.moveToPosition(-1); right.moveToFirst(); while(true) { left.moveToNext(); if(left.isAfterLast()) break; long leftValue = left.getLong(0); // advance right until it is equal or bigger while(!right.isAfterLast() && right.getLong(0) < leftValue) { right.moveToNext(); } if(right.isAfterLast()) { if(!both) matchingRows.add(leftValue); continue; } if((right.getLong(0) == leftValue) == both) matchingRows.add(leftValue); } } /** * Searches for a local task with same remote id, updates this task's id * @param remoteTask */ public void findLocalMatch(TYPE remoteTask) { if(remoteTask.task.getId() != Task.NO_ID) return; TodorooCursor cursor = metadataDao.query(Query.select(Metadata.TASK). where(Criterion.and(MetadataCriteria.withKey(getMetadataKey()), getLocalMatchCriteria(remoteTask)))); try { if(cursor.getCount() == 0) return; cursor.moveToFirst(); remoteTask.task.setId(cursor.get(Metadata.TASK)); } finally { cursor.close(); } } /** * Saves a task and its metadata * @param task */ public void saveTaskAndMetadata(TYPE task) { task.prepareForSaving(); taskDao.save(task.task); metadataDao.synchronizeMetadata(task.task.getId(), task.metadata, getMetadataCriteria()); } /** * Reads a task and its metadata * @param task * @return */ public TYPE readTaskAndMetadata(TodorooCursor taskCursor) { Task task = new Task(taskCursor); ArrayList metadata = new ArrayList(); TodorooCursor metadataCursor = metadataDao.query(Query.select(Metadata.PROPERTIES). where(Criterion.and(MetadataCriteria.byTask(task.getId()), getMetadataCriteria()))); try { for(metadataCursor.moveToFirst(); !metadataCursor.isAfterLast(); metadataCursor.moveToNext()) { metadata.add(new Metadata(metadataCursor)); } } finally { metadataCursor.close(); } return createContainerFromLocalTask(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(Metadata.PROPERTIES).where( MetadataCriteria.byTaskAndwithKey(taskId, getMetadataKey()))); try { if(cursor.getCount() == 0) return null; cursor.moveToFirst(); return new Metadata(cursor); } finally { cursor.close(); } } }