Merge upstream/4.0
After Width: | Height: | Size: 5.2 KiB |
After Width: | Height: | Size: 5.6 KiB |
@ -1,686 +0,0 @@
|
||||
/**
|
||||
* See the file "LICENSE" for the full license governing this code.
|
||||
*/
|
||||
package com.todoroo.astrid.gtasks.sync;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Semaphore;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import org.json.JSONException;
|
||||
|
||||
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 android.util.Log;
|
||||
|
||||
import com.google.api.services.tasks.model.TaskList;
|
||||
import com.google.api.services.tasks.model.TaskLists;
|
||||
import com.timsu.astrid.R;
|
||||
import com.todoroo.andlib.data.AbstractModel;
|
||||
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.utility.AndroidUtilities;
|
||||
import com.todoroo.andlib.utility.DateUtilities;
|
||||
import com.todoroo.andlib.utility.Preferences;
|
||||
import com.todoroo.astrid.api.AstridApiConstants;
|
||||
import com.todoroo.astrid.core.PluginServices;
|
||||
import com.todoroo.astrid.dao.TaskDao;
|
||||
import com.todoroo.astrid.data.Metadata;
|
||||
import com.todoroo.astrid.data.StoreObject;
|
||||
import com.todoroo.astrid.data.Task;
|
||||
import com.todoroo.astrid.gtasks.GtasksBackgroundService;
|
||||
import com.todoroo.astrid.gtasks.GtasksList;
|
||||
import com.todoroo.astrid.gtasks.GtasksListService;
|
||||
import com.todoroo.astrid.gtasks.GtasksMetadata;
|
||||
import com.todoroo.astrid.gtasks.GtasksMetadataService;
|
||||
import com.todoroo.astrid.gtasks.GtasksPreferenceService;
|
||||
import com.todoroo.astrid.gtasks.GtasksPreferences;
|
||||
import com.todoroo.astrid.gtasks.GtasksTaskListUpdater;
|
||||
import com.todoroo.astrid.gtasks.api.CreateRequest;
|
||||
import com.todoroo.astrid.gtasks.api.GoogleTasksException;
|
||||
import com.todoroo.astrid.gtasks.api.GtasksApiUtilities;
|
||||
import com.todoroo.astrid.gtasks.api.GtasksInvoker;
|
||||
import com.todoroo.astrid.gtasks.api.MoveListRequest;
|
||||
import com.todoroo.astrid.gtasks.api.MoveRequest;
|
||||
import com.todoroo.astrid.gtasks.api.PushRequest;
|
||||
import com.todoroo.astrid.gtasks.api.UpdateRequest;
|
||||
import com.todoroo.astrid.gtasks.auth.GtasksLoginActivity;
|
||||
import com.todoroo.astrid.gtasks.auth.GtasksTokenValidator;
|
||||
import com.todoroo.astrid.service.AstridDependencyInjector;
|
||||
import com.todoroo.astrid.service.StatisticsConstants;
|
||||
import com.todoroo.astrid.service.StatisticsService;
|
||||
import com.todoroo.astrid.sync.SyncContainer;
|
||||
import com.todoroo.astrid.sync.SyncProvider;
|
||||
import com.todoroo.astrid.sync.SyncProviderUtilities;
|
||||
import com.todoroo.astrid.utility.Constants;
|
||||
|
||||
@SuppressWarnings("nls")
|
||||
public class GtasksSyncProvider extends SyncProvider<GtasksTaskContainer> {
|
||||
|
||||
@Autowired private GtasksListService gtasksListService;
|
||||
@Autowired private GtasksMetadataService gtasksMetadataService;
|
||||
@Autowired private GtasksPreferenceService gtasksPreferenceService;
|
||||
@Autowired private GtasksTaskListUpdater gtasksTaskListUpdater;
|
||||
|
||||
/** google task service fields */
|
||||
private GtasksInvoker taskService = null;
|
||||
|
||||
public GtasksInvoker getGtasksService() {
|
||||
return taskService;
|
||||
}
|
||||
|
||||
/** tasks to read id for */
|
||||
ArrayList<GtasksTaskContainer> createdWithoutId;
|
||||
ArrayList<GtasksTaskContainer> createdWithoutOrder;
|
||||
Semaphore pushedTaskSemaphore = new Semaphore(0);
|
||||
AtomicInteger pushedTaskCount = new AtomicInteger(0);
|
||||
|
||||
static {
|
||||
AstridDependencyInjector.initialize();
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
// ------------------------------------------------------ utility methods
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
@Override
|
||||
protected SyncProviderUtilities getUtilities() {
|
||||
return gtasksPreferenceService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sign out of service, deleting all synchronization metadata
|
||||
*/
|
||||
public void signOut() {
|
||||
gtasksPreferenceService.clearLastSyncDate();
|
||||
gtasksPreferenceService.setToken(null);
|
||||
Preferences.setString(GtasksPreferenceService.PREF_USER_NAME, null);
|
||||
gtasksMetadataService.clearMetadata();
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
// ------------------------------------------------------ initiating sync
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* initiate sync in background
|
||||
*/
|
||||
@Override
|
||||
protected void initiateBackground() {
|
||||
try {
|
||||
String authToken = gtasksPreferenceService.getToken();
|
||||
authToken = GtasksTokenValidator.validateAuthToken(ContextManager.getContext(), authToken);
|
||||
gtasksPreferenceService.setToken(authToken);
|
||||
|
||||
taskService = new GtasksInvoker(authToken);
|
||||
performSync();
|
||||
} catch (IllegalStateException e) {
|
||||
// occurs when application was closed
|
||||
} catch (Exception e) {
|
||||
handleException("gtasks-authenticate", e, true);
|
||||
} finally {
|
||||
gtasksPreferenceService.stopOngoing();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If user isn't already signed in, show sign in dialog. Else perform sync.
|
||||
*/
|
||||
@Override
|
||||
protected void initiateManual(final Activity activity) {
|
||||
String authToken = gtasksPreferenceService.getToken();
|
||||
gtasksPreferenceService.stopOngoing();
|
||||
|
||||
// check if we have a token & it works
|
||||
if(authToken == null) {
|
||||
Intent intent = new Intent(activity, GtasksLoginActivity.class);
|
||||
activity.startActivityForResult(intent, 0);
|
||||
} else {
|
||||
activity.startService(new Intent(null, null,
|
||||
activity, GtasksBackgroundService.class));
|
||||
activity.finish();
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
// ----------------------------------------------------- synchronization!
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
protected void performSync() {
|
||||
String syncSuccess = "failed";
|
||||
gtasksPreferenceService.recordSyncStart();
|
||||
if(Constants.DEBUG)
|
||||
Log.e("gtasks-debug", "- -------- SYNC STARTED");
|
||||
createdWithoutId = new ArrayList<GtasksTaskContainer>();
|
||||
createdWithoutOrder = new ArrayList<GtasksTaskContainer>();
|
||||
try {
|
||||
TaskLists allTaskLists = taskService.allGtaskLists();
|
||||
|
||||
new GtasksLegacyMigrator(taskService, gtasksListService,
|
||||
allTaskLists).checkAndMigrateLegacy();
|
||||
|
||||
getActiveList(allTaskLists);
|
||||
|
||||
gtasksListService.updateLists(allTaskLists);
|
||||
|
||||
gtasksTaskListUpdater.createParentSiblingMaps();
|
||||
|
||||
// read non-deleted tasks for each list
|
||||
SyncData<GtasksTaskContainer> syncData = populateSyncData();
|
||||
try {
|
||||
synchronizeTasks(syncData);
|
||||
AndroidUtilities.sleepDeep(3000L); // Wait for changes to be saved (i.e. for repeating tasks to be cloned)
|
||||
checkForCreatedDuringSync();
|
||||
} finally {
|
||||
syncData.localCreated.close();
|
||||
syncData.localUpdated.close();
|
||||
}
|
||||
|
||||
gtasksTaskListUpdater.updateAllMetadata();
|
||||
|
||||
gtasksPreferenceService.recordSuccessfulSync();
|
||||
|
||||
Intent broadcastIntent = new Intent(AstridApiConstants.BROADCAST_EVENT_REFRESH);
|
||||
ContextManager.getContext().sendBroadcast(broadcastIntent, AstridApiConstants.PERMISSION_READ);
|
||||
if(Constants.DEBUG)
|
||||
Log.e("gtasks-debug", "- ------ SYNC FINISHED");
|
||||
|
||||
syncSuccess = getFinalSyncStatus();
|
||||
} catch (IllegalStateException e) {
|
||||
// occurs when application was closed
|
||||
} catch (IOException e) {
|
||||
handleException("gtasks-sync", e, true); //$NON-NLS-1$
|
||||
} finally {
|
||||
StatisticsService.reportEvent(StatisticsConstants.GTASKS_SYNC_FINISHED,
|
||||
"success", syncSuccess); //$NON-NLS-1$
|
||||
}
|
||||
}
|
||||
|
||||
private void checkForCreatedDuringSync() {
|
||||
TodorooCursor<Task> localCreated = gtasksMetadataService.getLocallyCreated(PROPERTIES);
|
||||
try {
|
||||
SyncData<GtasksTaskContainer> localCreatedData = new SyncData<GtasksTaskContainer>(null, localCreated, null);
|
||||
sendLocallyCreated(localCreatedData, new HashMap<String, Integer>());
|
||||
} catch (IOException e) {
|
||||
handleException("gtasks-sync", e, true);
|
||||
} finally {
|
||||
localCreated.close();
|
||||
}
|
||||
}
|
||||
|
||||
private void getActiveList(TaskLists taskView) throws IOException {
|
||||
String listId;
|
||||
if(taskView.getItems().size() == 0) {
|
||||
if(Constants.DEBUG)
|
||||
Log.e("gtasks-debug", "ACTION: createList(4)");
|
||||
TaskList newList = taskService.createGtaskList(ContextManager.getString(R.string.app_name));
|
||||
listId = newList.getId();
|
||||
} else if (Preferences.getStringValue(GtasksPreferenceService.PREF_DEFAULT_LIST) != null) {
|
||||
listId = Preferences.getStringValue(GtasksPreferenceService.PREF_DEFAULT_LIST);
|
||||
} else {
|
||||
listId = taskService.getGtaskList("@default").getId();
|
||||
}
|
||||
|
||||
Preferences.setString(GtasksPreferenceService.PREF_DEFAULT_LIST, listId);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void readRemotelyUpdated(SyncData<GtasksTaskContainer> data)
|
||||
throws IOException {
|
||||
|
||||
// wait for pushed threads
|
||||
try {
|
||||
pushedTaskSemaphore.acquire(pushedTaskCount.get());
|
||||
pushedTaskCount.set(0);
|
||||
} catch (InterruptedException e) {
|
||||
return;
|
||||
}
|
||||
|
||||
// match remote tasks to locally created tasks
|
||||
HashMap<String, GtasksTaskContainer> locals = new HashMap<String, GtasksTaskContainer>();
|
||||
for(GtasksTaskContainer task : createdWithoutId) {
|
||||
locals.put(task.gtaskMetadata.getValue(GtasksMetadata.ID), task);
|
||||
}
|
||||
|
||||
verifyCreatedOrder();
|
||||
|
||||
// first, pull all tasks. then we can write them
|
||||
// include deleted tasks so we can delete them in astrid
|
||||
data.remoteUpdated = readAllRemoteTasks(true, true);
|
||||
|
||||
for(GtasksTaskContainer remote : data.remoteUpdated) {
|
||||
if(remote.task.getId() < 1) {
|
||||
GtasksTaskContainer local = locals.get(remote.gtaskMetadata.getValue(GtasksMetadata.ID));
|
||||
if(local != null) {
|
||||
if(Constants.DEBUG)
|
||||
Log.e("gtasks-debug", "FOUND LOCAL - " + remote.task.getId());
|
||||
remote.task.setId(local.task.getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
super.readRemotelyUpdated(data);
|
||||
}
|
||||
|
||||
private void verifyCreatedOrder() throws IOException {
|
||||
Collections.sort(createdWithoutOrder, new Comparator<GtasksTaskContainer>() {
|
||||
@Override
|
||||
public int compare(GtasksTaskContainer arg0, GtasksTaskContainer arg1) {
|
||||
long order0 = arg0.gtaskMetadata.getValue(GtasksMetadata.ORDER);
|
||||
long order1 = arg1.gtaskMetadata.getValue(GtasksMetadata.ORDER);
|
||||
|
||||
if (order0 == order1) return 0;
|
||||
if (order0 < order1) return -1;
|
||||
else return 1;
|
||||
}
|
||||
|
||||
});
|
||||
for (GtasksTaskContainer t : createdWithoutOrder) {
|
||||
String toMove = t.gtaskMetadata.getValue(GtasksMetadata.ID);
|
||||
String listId = t.gtaskMetadata.getValue(GtasksMetadata.LIST_ID);
|
||||
String remoteParent = gtasksMetadataService.getRemoteParentId(t.gtaskMetadata);
|
||||
String remoteSibling = gtasksMetadataService.getRemoteSiblingId(listId, t.gtaskMetadata);
|
||||
|
||||
MoveRequest move = new MoveRequest(taskService, toMove, listId, remoteParent, remoteSibling);
|
||||
move.push();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
// ------------------------------------------------------------ sync data
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
// all synchronized properties
|
||||
private static final Property<?>[] PROPERTIES = Task.PROPERTIES;
|
||||
|
||||
/**
|
||||
* Populate SyncData data structure
|
||||
* @throws JSONException
|
||||
*/
|
||||
private SyncData<GtasksTaskContainer> populateSyncData() throws IOException {
|
||||
|
||||
// fetch remote tasks
|
||||
ArrayList<GtasksTaskContainer> remoteTasks = readAllRemoteTasks(false, false);
|
||||
|
||||
// fetch locally created tasks
|
||||
TodorooCursor<Task> localCreated = gtasksMetadataService.getLocallyCreated(PROPERTIES);
|
||||
|
||||
// fetch locally updated tasks
|
||||
TodorooCursor<Task> localUpdated = gtasksMetadataService.getLocallyUpdated(PROPERTIES);
|
||||
|
||||
return new SyncData<GtasksTaskContainer>(remoteTasks, localCreated, localUpdated);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
// ------------------------------------------------- create / push / pull
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
private ArrayList<GtasksTaskContainer> readAllRemoteTasks(final boolean includeDeleted, final boolean includeHidden) {
|
||||
final ArrayList<GtasksTaskContainer> list = new ArrayList<GtasksTaskContainer>();
|
||||
final Semaphore listsFinished = new Semaphore(0);
|
||||
|
||||
// launch threads
|
||||
StoreObject[] lists = gtasksListService.getLists();
|
||||
for(final StoreObject dashboard : lists) {
|
||||
new Thread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
String listId = dashboard.getValue(GtasksList.REMOTE_ID);
|
||||
if(Constants.DEBUG)
|
||||
Log.e("gtasks-debug", "ACTION: getTasks, " + listId);
|
||||
List<com.google.api.services.tasks.model.Task> remoteTasks = taskService.getAllGtasksFromListId(listId, includeDeleted, includeHidden, 0).getItems();
|
||||
addRemoteTasksToList(remoteTasks, listId, list);
|
||||
} catch (Exception e) {
|
||||
handleException("read-remotes", e, false);
|
||||
} finally {
|
||||
listsFinished.release();
|
||||
}
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
try {
|
||||
listsFinished.acquire(lists.length);
|
||||
} catch (InterruptedException e) {
|
||||
handleException("wait-for-remotes", e, false);
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
private void addRemoteTasksToList(List<com.google.api.services.tasks.model.Task> remoteTasks, String listId, ArrayList<GtasksTaskContainer> list) {
|
||||
|
||||
if (remoteTasks != null) {
|
||||
long order = 0;
|
||||
HashMap<String, com.google.api.services.tasks.model.Task> idsToTasks = new HashMap<String, com.google.api.services.tasks.model.Task>();
|
||||
HashMap<String, Integer> indentation = new HashMap<String, Integer>();
|
||||
HashMap<String, String> parentToPriorSiblingMap = new HashMap<String, String>();
|
||||
|
||||
|
||||
//Build map of String ids to task objects
|
||||
for (com.google.api.services.tasks.model.Task task : remoteTasks) {
|
||||
String id = task.getId();
|
||||
idsToTasks.put(id, task);
|
||||
}
|
||||
|
||||
for(com.google.api.services.tasks.model.Task remoteTask : remoteTasks) {
|
||||
if(TextUtils.isEmpty(remoteTask.getTitle()))
|
||||
continue;
|
||||
|
||||
GtasksTaskContainer container = parseRemoteTask(remoteTask, listId);
|
||||
String id = remoteTask.getId();
|
||||
|
||||
// update parents, prior sibling
|
||||
String parent = remoteTask.getParent(); // can be null, which means top level task
|
||||
container.parentId = parent;
|
||||
if(parentToPriorSiblingMap.containsKey(parent))
|
||||
container.priorSiblingId = parentToPriorSiblingMap.get(parent);
|
||||
parentToPriorSiblingMap.put(parent, id);
|
||||
|
||||
// update order, indent
|
||||
container.gtaskMetadata.setValue(GtasksMetadata.ORDER, order++);
|
||||
int indent = findIndentation(idsToTasks, indentation, remoteTask);
|
||||
indentation.put(id, indent);
|
||||
container.gtaskMetadata.setValue(GtasksMetadata.INDENT, indent);
|
||||
|
||||
// update reminder flags for incoming remote tasks to prevent annoying
|
||||
if(container.task.hasDueDate() && container.task.getValue(Task.DUE_DATE) < DateUtilities.now())
|
||||
container.task.setFlag(Task.REMINDER_FLAGS, Task.NOTIFY_AFTER_DEADLINE, false);
|
||||
|
||||
gtasksMetadataService.findLocalMatch(container);
|
||||
synchronized(list) {
|
||||
list.add(container);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private int findIndentation(HashMap<String, com.google.api.services.tasks.model.Task> idsToTasks,
|
||||
HashMap<String, Integer> indentation, com.google.api.services.tasks.model.Task task) {
|
||||
if (task == null) return 0; //TODO: Not sure why this is happening...
|
||||
if(indentation.containsKey(task.getId()))
|
||||
return indentation.get(task.getId());
|
||||
|
||||
if(TextUtils.isEmpty(task.getParent()))
|
||||
return 0;
|
||||
|
||||
return findIndentation(idsToTasks, indentation, idsToTasks.get(task.getParent())) + 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected GtasksTaskContainer create(GtasksTaskContainer local) throws IOException {
|
||||
String listId = Preferences.getStringValue(GtasksPreferenceService.PREF_DEFAULT_LIST);
|
||||
if(local.gtaskMetadata.containsNonNullValue(GtasksMetadata.LIST_ID))
|
||||
listId = local.gtaskMetadata.getValue(GtasksMetadata.LIST_ID);
|
||||
gtasksTaskListUpdater.updateParentAndSibling(local);
|
||||
local.gtaskMetadata.setValue(GtasksMetadata.ID, null);
|
||||
local.gtaskMetadata.setValue(GtasksMetadata.LIST_ID, listId);
|
||||
|
||||
com.google.api.services.tasks.model.Task createdTask = new com.google.api.services.tasks.model.Task();
|
||||
|
||||
CreateRequest createRequest = new CreateRequest(taskService, listId, createdTask, local.parentId, local.priorSiblingId);
|
||||
//updateTaskHelper(local, null, createRequest);
|
||||
localPropertiesToModel(local, null, createRequest.getToPush());
|
||||
com.google.api.services.tasks.model.Task createResult = createRequest.push();
|
||||
createdWithoutId.add(local);
|
||||
createdWithoutOrder.add(local);
|
||||
String newIdTask = createResult.getId();
|
||||
local.gtaskMetadata.setValue(GtasksMetadata.ID, newIdTask);
|
||||
|
||||
return local;
|
||||
}//*/
|
||||
|
||||
private void localPropertiesToModel(GtasksTaskContainer local, GtasksTaskContainer remote,
|
||||
com.google.api.services.tasks.model.Task model) {
|
||||
if(shouldTransmit(local, Task.TITLE, remote))
|
||||
model.setTitle(local.task.getValue(Task.TITLE));
|
||||
if(shouldTransmit(local, Task.DUE_DATE, remote)) {
|
||||
model.setDue(GtasksApiUtilities.unixTimeToGtasksDueDate(local.task.getValue(Task.DUE_DATE)));
|
||||
}
|
||||
if(shouldTransmit(local, Task.COMPLETION_DATE, remote)) {
|
||||
model.setCompleted(GtasksApiUtilities.unixTimeToGtasksCompletionTime(local.task.getValue(Task.COMPLETION_DATE)));
|
||||
model.setStatus((local.task.isCompleted() ? "completed" : "needsAction"));
|
||||
}
|
||||
if(shouldTransmit(local, Task.DELETION_DATE, remote))
|
||||
model.setDeleted(local.task.isDeleted());
|
||||
if(shouldTransmit(local, Task.NOTES, remote))
|
||||
model.setNotes(local.task.getValue(Task.NOTES));
|
||||
}
|
||||
|
||||
private void updateTaskHelper(final GtasksTaskContainer local,
|
||||
final GtasksTaskContainer remote, final PushRequest request) throws IOException {
|
||||
|
||||
final String idTask = local.gtaskMetadata.getValue(GtasksMetadata.ID);
|
||||
final String idList = local.gtaskMetadata.getValue(GtasksMetadata.LIST_ID);
|
||||
|
||||
try {
|
||||
// set properties
|
||||
localPropertiesToModel(local, null, request.getToPush());
|
||||
|
||||
// write task (and perform move action if requested)
|
||||
if(request instanceof UpdateRequest) {
|
||||
if(Constants.DEBUG)
|
||||
Log.e("gtasks-debug", "ACTION: task edit (6), " + idTask);
|
||||
} else if(request instanceof CreateRequest) {
|
||||
if(Constants.DEBUG)
|
||||
Log.e("gtasks-debug", "ACTION: task create (7), " + local.task.getValue(Task.TITLE));
|
||||
} else
|
||||
throw new GoogleTasksException("Unknown request type " + request.getClass());
|
||||
|
||||
pushedTaskCount.incrementAndGet();
|
||||
new Thread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
String newIdTask = idTask;
|
||||
try {
|
||||
/*if (request instanceof CreateRequest) { //This is commented out until bugs with multithreaded task creation are resolved
|
||||
com.google.api.services.tasks.v1.model.Task createResult = request.executePush();
|
||||
newIdTask = createResult.id;
|
||||
System.err.println("Created " + createResult.title + " successfully, remote id: " + createResult.id);
|
||||
local.gtaskMetadata.setValue(GtasksMetadata.ID, newIdTask);
|
||||
}//*/
|
||||
if(!TextUtils.isEmpty(newIdTask) && remote != null && (local.parentId != remote.parentId ||
|
||||
local.priorSiblingId != remote.priorSiblingId)) {
|
||||
if(Constants.DEBUG)
|
||||
Log.e("gtasks-debug", "ACTION: move(1) - " + newIdTask + ", " + local.parentId + ", " + local.priorSiblingId);
|
||||
//This case basically defaults to whatever local settings are. Future versions could try and merge better
|
||||
MoveRequest moveRequest = new MoveRequest(taskService, newIdTask, idList, local.parentId, local.priorSiblingId);
|
||||
moveRequest.push();
|
||||
|
||||
}
|
||||
if (request instanceof UpdateRequest) {
|
||||
request.push();
|
||||
}
|
||||
|
||||
//Strategy--delete, migrate properties, recreate, update local AND remote ids; happens in MoveListRequest
|
||||
if(remote != null && !idList.equals(remote.gtaskMetadata.getValue(
|
||||
GtasksMetadata.LIST_ID))) {
|
||||
if(Constants.DEBUG)
|
||||
Log.e("gtasks-debug", "ACTION: moveTask(5), " + newIdTask + ", " + idList + " to " +
|
||||
remote.gtaskMetadata.getValue(GtasksMetadata.LIST_ID));
|
||||
MoveListRequest moveList = new MoveListRequest(taskService, newIdTask, remote.gtaskMetadata.getValue(GtasksMetadata.LIST_ID), idList, null);
|
||||
com.google.api.services.tasks.model.Task result = moveList.push();
|
||||
local.gtaskMetadata.setValue(GtasksMetadata.ID, result.getId());
|
||||
remote.gtaskMetadata.setValue(GtasksMetadata.ID, result.getId());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
handleException("update-task", e, false);
|
||||
} finally {
|
||||
pushedTaskSemaphore.release();
|
||||
}
|
||||
}
|
||||
}).start();
|
||||
|
||||
} catch (Exception e) {
|
||||
throw new GoogleTasksException(e);
|
||||
}
|
||||
}//*/
|
||||
|
||||
/** Create a task container for the given remote task
|
||||
* @throws JSONException */
|
||||
private GtasksTaskContainer parseRemoteTask(com.google.api.services.tasks.model.Task remoteTask, String listId) {
|
||||
Task task = new Task();
|
||||
|
||||
ArrayList<Metadata> metadata = new ArrayList<Metadata>();
|
||||
|
||||
task.setValue(Task.TITLE, remoteTask.getTitle());
|
||||
task.setValue(Task.CREATION_DATE, DateUtilities.now());
|
||||
task.setValue(Task.COMPLETION_DATE, GtasksApiUtilities.gtasksCompletedTimeToUnixTime(remoteTask.getCompleted(), 0));
|
||||
if (remoteTask.getDeleted() == null || !remoteTask.getDeleted().booleanValue())
|
||||
task.setValue(Task.DELETION_DATE, 0L);
|
||||
else if (remoteTask.getDeleted().booleanValue())
|
||||
task.setValue(Task.DELETION_DATE, DateUtilities.now());
|
||||
if (remoteTask.getHidden() != null && remoteTask.getHidden().booleanValue())
|
||||
task.setValue(Task.DELETION_DATE, DateUtilities.now());
|
||||
|
||||
long dueDate = GtasksApiUtilities.gtasksDueTimeToUnixTime(remoteTask.getDue(), 0);
|
||||
long createdDate = Task.createDueDate(Task.URGENCY_SPECIFIC_DAY, dueDate);
|
||||
task.setValue(Task.DUE_DATE, createdDate);
|
||||
task.setValue(Task.NOTES, remoteTask.getNotes());
|
||||
|
||||
Metadata gtasksMetadata = GtasksMetadata.createEmptyMetadata(AbstractModel.NO_ID);
|
||||
gtasksMetadata.setValue(GtasksMetadata.ID, remoteTask.getId());
|
||||
gtasksMetadata.setValue(GtasksMetadata.LIST_ID, listId);
|
||||
|
||||
GtasksTaskContainer container = new GtasksTaskContainer(task, metadata,
|
||||
gtasksMetadata);
|
||||
return container;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected GtasksTaskContainer pull(GtasksTaskContainer task) throws IOException {
|
||||
// we pull all tasks at the end, so here we just
|
||||
// return the task that was requested
|
||||
return task;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 GtasksTaskContainer push(GtasksTaskContainer local, GtasksTaskContainer remote) throws IOException {
|
||||
gtasksTaskListUpdater.updateParentAndSibling(local);
|
||||
|
||||
String id = local.gtaskMetadata.getValue(GtasksMetadata.ID);
|
||||
if(Constants.DEBUG)
|
||||
Log.e("gtasks-debug", "ACTION: modifyTask(3) - " + id);
|
||||
|
||||
com.google.api.services.tasks.model.Task toUpdate = taskService.getGtask(local.gtaskMetadata.getValue(GtasksMetadata.LIST_ID), id);
|
||||
UpdateRequest modifyTask = new UpdateRequest(taskService, local.gtaskMetadata.getValue(GtasksMetadata.LIST_ID), toUpdate);
|
||||
updateTaskHelper(local, remote, modifyTask);
|
||||
|
||||
return pull(remote);
|
||||
}//*/
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
// --------------------------------------------------------- read / write
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
@Override
|
||||
protected GtasksTaskContainer read(TodorooCursor<Task> cursor) throws IOException {
|
||||
return gtasksMetadataService.readTaskAndMetadata(cursor);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void write(GtasksTaskContainer task) throws IOException {
|
||||
// merge astrid dates with google dates
|
||||
if(task.task.isSaved()) {
|
||||
Task local = PluginServices.getTaskService().fetchById(task.task.getId(), Task.DUE_DATE, Task.COMPLETION_DATE);
|
||||
mergeDates(task.task, local);
|
||||
if(task.task.isCompleted() && !local.isCompleted())
|
||||
StatisticsService.reportEvent(StatisticsConstants.GTASKS_TASK_COMPLETED);
|
||||
} else { // Set default reminders for remotely created tasks
|
||||
TaskDao.setDefaultReminders(task.task);
|
||||
}
|
||||
gtasksMetadataService.saveTaskAndMetadata(task);
|
||||
}
|
||||
|
||||
/** pick up due time from local task */
|
||||
private void mergeDates(Task remote, Task local) {
|
||||
if(remote.hasDueDate() && local.hasDueTime()) {
|
||||
Date newDate = new Date(remote.getValue(Task.DUE_DATE));
|
||||
Date oldDate = new Date(local.getValue(Task.DUE_DATE));
|
||||
newDate.setHours(oldDate.getHours());
|
||||
newDate.setMinutes(oldDate.getMinutes());
|
||||
newDate.setSeconds(oldDate.getSeconds());
|
||||
long setDate = Task.createDueDate(Task.URGENCY_SPECIFIC_DAY_TIME,
|
||||
newDate.getTime());
|
||||
remote.setValue(Task.DUE_DATE, setDate);
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
// --------------------------------------------------------- misc helpers
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
@Override
|
||||
protected int matchTask(ArrayList<GtasksTaskContainer> tasks, GtasksTaskContainer target) {
|
||||
int length = tasks.size();
|
||||
for(int i = 0; i < length; i++) {
|
||||
GtasksTaskContainer task = tasks.get(i);
|
||||
if(AndroidUtilities.equals(task.gtaskMetadata.getValue(GtasksMetadata.ID),
|
||||
target.gtaskMetadata.getValue(GtasksMetadata.ID)))
|
||||
return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(SyncContainer task, Property<?> property, SyncContainer remoteTask) {
|
||||
if(!task.task.containsValue(property))
|
||||
return false;
|
||||
|
||||
if(remoteTask == null)
|
||||
return true;
|
||||
if(!remoteTask.task.containsValue(property))
|
||||
return true;
|
||||
|
||||
// special cases - match if they're zero or nonzero
|
||||
if(property == Task.COMPLETION_DATE ||
|
||||
property == Task.DELETION_DATE)
|
||||
return !AndroidUtilities.equals((Long)task.task.getValue(property) == 0,
|
||||
(Long)remoteTask.task.getValue(property) == 0);
|
||||
|
||||
return !AndroidUtilities.equals(task.task.getValue(property),
|
||||
remoteTask.task.getValue(property));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int updateNotification(Context context, Notification notification) {
|
||||
String notificationTitle = context.getString(R.string.gtasks_notification_title);
|
||||
Intent intent = new Intent(context, GtasksPreferences.class);
|
||||
PendingIntent notificationIntent = PendingIntent.getActivity(context, 0,
|
||||
intent, 0);
|
||||
notification.setLatestEventInfo(context,
|
||||
notificationTitle, context.getString(R.string.SyP_progress),
|
||||
notificationIntent);
|
||||
return Constants.NOTIFICATION_SYNC;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void transferIdentifiers(GtasksTaskContainer source,
|
||||
GtasksTaskContainer destination) {
|
||||
destination.gtaskMetadata = source.gtaskMetadata;
|
||||
}
|
||||
}
|
@ -0,0 +1,330 @@
|
||||
package com.todoroo.astrid.subtasks;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.text.TextUtils;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.util.Log;
|
||||
import android.util.TypedValue;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.ViewGroup.MarginLayoutParams;
|
||||
import android.widget.ListView;
|
||||
|
||||
import com.commonsware.cwac.tlv.TouchListView.DropListener;
|
||||
import com.commonsware.cwac.tlv.TouchListView.GrabberClickListener;
|
||||
import com.commonsware.cwac.tlv.TouchListView.SwipeListener;
|
||||
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.DependencyInjectionService;
|
||||
import com.todoroo.andlib.utility.DateUtilities;
|
||||
import com.todoroo.andlib.utility.DialogUtilities;
|
||||
import com.todoroo.andlib.utility.Preferences;
|
||||
import com.todoroo.astrid.activity.TaskListFragment;
|
||||
import com.todoroo.astrid.adapter.TaskAdapter;
|
||||
import com.todoroo.astrid.adapter.TaskAdapter.OnCompletedTaskListener;
|
||||
import com.todoroo.astrid.api.Filter;
|
||||
import com.todoroo.astrid.data.Metadata;
|
||||
import com.todoroo.astrid.data.Task;
|
||||
import com.todoroo.astrid.service.MetadataService;
|
||||
import com.todoroo.astrid.service.TaskService;
|
||||
import com.todoroo.astrid.service.ThemeService;
|
||||
import com.todoroo.astrid.subtasks.OrderedListUpdater.Node;
|
||||
import com.todoroo.astrid.subtasks.OrderedListUpdater.OrderedListNodeVisitor;
|
||||
import com.todoroo.astrid.ui.DraggableListView;
|
||||
import com.todoroo.astrid.utility.AstridPreferences;
|
||||
|
||||
public class OrderedListFragmentHelper<LIST> {
|
||||
|
||||
private final DisplayMetrics metrics = new DisplayMetrics();
|
||||
private final OrderedListUpdater<LIST> updater;
|
||||
private final TaskListFragment fragment;
|
||||
|
||||
@Autowired TaskService taskService;
|
||||
@Autowired MetadataService metadataService;
|
||||
|
||||
private DraggableTaskAdapter taskAdapter;
|
||||
|
||||
private LIST list;
|
||||
|
||||
public OrderedListFragmentHelper(TaskListFragment fragment, OrderedListUpdater<LIST> updater) {
|
||||
DependencyInjectionService.getInstance().inject(this);
|
||||
this.fragment = fragment;
|
||||
this.updater = updater;
|
||||
}
|
||||
|
||||
// --- ui component setup
|
||||
|
||||
private Activity getActivity() {
|
||||
return fragment.getActivity();
|
||||
}
|
||||
|
||||
private ListView getListView() {
|
||||
return fragment.getListView();
|
||||
}
|
||||
|
||||
private Filter getFilter() {
|
||||
return fragment.getFilter();
|
||||
}
|
||||
|
||||
public DraggableListView getTouchListView() {
|
||||
DraggableListView tlv = (DraggableListView) fragment.getListView();
|
||||
return tlv;
|
||||
}
|
||||
|
||||
public void setUpUiComponents() {
|
||||
TypedValue tv = new TypedValue();
|
||||
getActivity().getTheme().resolveAttribute(R.attr.asThemeTextColor, tv, false);
|
||||
getTouchListView().setDragndropBackgroundColor(tv.data);
|
||||
getTouchListView().setDropListener(dropListener);
|
||||
getTouchListView().setClickListener(rowClickListener);
|
||||
getTouchListView().setSwipeListener(swipeListener);
|
||||
getActivity().getWindowManager().getDefaultDisplay().getMetrics(metrics);
|
||||
|
||||
if(Preferences.getInt(AstridPreferences.P_SUBTASKS_HELP, 0) == 0)
|
||||
showSubtasksHelp();
|
||||
}
|
||||
|
||||
@SuppressWarnings("nls")
|
||||
private void showSubtasksHelp() {
|
||||
String body = String.format("<h3>%s</h3><img src='%s'>" +
|
||||
"<br>%s<br><br><br><img src='%s'><br>%s",
|
||||
getActivity().getString(R.string.subtasks_help_1),
|
||||
"subtasks_vertical.png",
|
||||
getActivity().getString(R.string.subtasks_help_2),
|
||||
"subtasks_horizontal.png",
|
||||
getActivity().getString(R.string.subtasks_help_3));
|
||||
|
||||
String color = ThemeService.getDialogTextColor();
|
||||
String html = String.format("<html><body style='text-align:center;color:%s'>%s</body></html>",
|
||||
color, body);
|
||||
|
||||
DialogUtilities.htmlDialog(getActivity(), html, R.string.subtasks_help_title);
|
||||
Preferences.setInt(AstridPreferences.P_SUBTASKS_HELP, 1);
|
||||
}
|
||||
|
||||
public void beforeSetUpTaskList(Filter filter) {
|
||||
updater.initialize(list, filter);
|
||||
}
|
||||
|
||||
public Property<?>[] taskProperties() {
|
||||
ArrayList<Property<?>> properties = new ArrayList<Property<?>>(Arrays.asList(TaskAdapter.PROPERTIES));
|
||||
properties.add(updater.indentProperty());
|
||||
properties.add(updater.orderProperty());
|
||||
return properties.toArray(new Property<?>[properties.size()]);
|
||||
}
|
||||
|
||||
|
||||
private final DropListener dropListener = new DropListener() {
|
||||
@Override
|
||||
public void drop(int from, int to) {
|
||||
long targetTaskId = taskAdapter.getItemId(from);
|
||||
if (targetTaskId <= 0) return; // This can happen with gestures on empty parts of the list (e.g. extra space below tasks)
|
||||
long destinationTaskId = taskAdapter.getItemId(to);
|
||||
|
||||
try {
|
||||
if(to >= getListView().getCount())
|
||||
updater.moveTo(getFilter(), list, targetTaskId, -1);
|
||||
else
|
||||
updater.moveTo(getFilter(), list, targetTaskId, destinationTaskId);
|
||||
} catch (Exception e) {
|
||||
Log.e("drag", "Drag Error", e); //$NON-NLS-1$ //$NON-NLS-2$
|
||||
}
|
||||
|
||||
fragment.loadTaskListContent(true);
|
||||
onMetadataChanged(targetTaskId);
|
||||
}
|
||||
};
|
||||
|
||||
private final SwipeListener swipeListener = new SwipeListener() {
|
||||
@Override
|
||||
public void swipeRight(int which) {
|
||||
indent(which, 1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void swipeLeft(int which) {
|
||||
indent(which, -1);
|
||||
}
|
||||
|
||||
protected void indent(int which, int delta) {
|
||||
long targetTaskId = taskAdapter.getItemId(which);
|
||||
if (targetTaskId <= 0) return; // This can happen with gestures on empty parts of the list (e.g. extra space below tasks)
|
||||
try {
|
||||
updater.indent(getFilter(), list, targetTaskId, delta);
|
||||
} catch (Exception e) {
|
||||
Log.e("drag", "Indent Error", e); //$NON-NLS-1$ //$NON-NLS-2$
|
||||
}
|
||||
fragment.loadTaskListContent(true);
|
||||
onMetadataChanged(targetTaskId);
|
||||
}
|
||||
};
|
||||
|
||||
private final GrabberClickListener rowClickListener = new GrabberClickListener() {
|
||||
@Override
|
||||
public void onLongClick(final View v) {
|
||||
if(v == null)
|
||||
return;
|
||||
|
||||
fragment.registerForContextMenu(getListView());
|
||||
getListView().showContextMenuForChild(v);
|
||||
fragment.unregisterForContextMenu(getListView());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
if(v == null)
|
||||
return;
|
||||
((DraggableTaskAdapter) taskAdapter).getListener().onClick(v);
|
||||
}
|
||||
};
|
||||
|
||||
public TaskAdapter createTaskAdapter(TodorooCursor<Task> cursor,
|
||||
AtomicReference<String> sqlQueryTemplate) {
|
||||
taskAdapter = new DraggableTaskAdapter(fragment, R.layout.task_adapter_row,
|
||||
cursor, sqlQueryTemplate, false, null);
|
||||
|
||||
taskAdapter.addOnCompletedTaskListener(new OnCompletedTaskListener() {
|
||||
@Override
|
||||
public void onCompletedTask(Task item, boolean newState) {
|
||||
setCompletedForItemAndSubtasks(item, newState);
|
||||
}
|
||||
});
|
||||
|
||||
return taskAdapter;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param targetTaskId
|
||||
*/
|
||||
protected void onMetadataChanged(long targetTaskId) {
|
||||
// hook
|
||||
}
|
||||
|
||||
private final class DraggableTaskAdapter extends TaskAdapter {
|
||||
|
||||
private DraggableTaskAdapter(TaskListFragment activity, int resource,
|
||||
Cursor c, AtomicReference<String> query, boolean autoRequery,
|
||||
OnCompletedTaskListener onCompletedTaskListener) {
|
||||
super(activity, resource, c, query, autoRequery,
|
||||
onCompletedTaskListener);
|
||||
|
||||
applyListeners = APPLY_LISTENERS_NONE;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ViewHolder getTagFromCheckBox(View v) {
|
||||
return (ViewHolder)((View)v.getParent()).getTag();
|
||||
}
|
||||
|
||||
@Override
|
||||
public View newView(Context context, Cursor cursor, ViewGroup parent) {
|
||||
View view = super.newView(context, cursor, parent);
|
||||
view.getLayoutParams().height = Math.round(45 * metrics.density);
|
||||
|
||||
ViewHolder vh = (ViewHolder) view.getTag();
|
||||
|
||||
MarginLayoutParams rowParams = (MarginLayoutParams) vh.rowBody.getLayoutParams();
|
||||
rowParams.topMargin = rowParams.bottomMargin = 0;
|
||||
|
||||
ViewGroup.LayoutParams pictureParams = vh.picture.getLayoutParams();
|
||||
pictureParams.width = pictureParams.height = Math.round(38 * metrics.density);
|
||||
|
||||
pictureParams = vh.pictureBorder.getLayoutParams();
|
||||
pictureParams.width = pictureParams.height = Math.round(38 * metrics.density);
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void setFieldContentsAndVisibility(View view) {
|
||||
super.setFieldContentsAndVisibility(view);
|
||||
|
||||
ViewHolder vh = (ViewHolder) view.getTag();
|
||||
int indent = vh.task.getValue(updater.indentProperty());
|
||||
vh.rowBody.setPadding(Math.round(indent * 20 * metrics.density), 0, 0, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void addListeners(View container) {
|
||||
super.addListeners(container);
|
||||
}
|
||||
|
||||
public TaskRowListener getListener() {
|
||||
return listener;
|
||||
}
|
||||
}
|
||||
|
||||
private final Map<Long, ArrayList<Long>> chainedCompletions =
|
||||
Collections.synchronizedMap(new HashMap<Long, ArrayList<Long>>());
|
||||
|
||||
private void setCompletedForItemAndSubtasks(final Task item, final boolean completedState) {
|
||||
final long itemId = item.getId();
|
||||
|
||||
final Task model = new Task();
|
||||
final long completionDate = completedState ? DateUtilities.now() : 0;
|
||||
|
||||
if(completedState == false) {
|
||||
ArrayList<Long> chained = chainedCompletions.get(itemId);
|
||||
if(chained != null) {
|
||||
for(Long taskId : chained) {
|
||||
model.setId(taskId);
|
||||
model.setValue(Task.COMPLETION_DATE, completionDate);
|
||||
taskService.save(model);
|
||||
model.clear();
|
||||
|
||||
taskAdapter.getCompletedItems().put(taskId, false);
|
||||
}
|
||||
taskAdapter.notifyDataSetInvalidated();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
final ArrayList<Long> chained = new ArrayList<Long>();
|
||||
final int parentIndent = item.getValue(updater.indentProperty());
|
||||
updater.applyToChildren(getFilter(), list, itemId, new OrderedListNodeVisitor() {
|
||||
@Override
|
||||
public void visitNode(Node node) {
|
||||
Task childTask = taskService.fetchById(node.taskId, Task.RECURRENCE);
|
||||
if(!TextUtils.isEmpty(childTask.getValue(Task.RECURRENCE))) {
|
||||
Metadata metadata = updater.getTaskMetadata(list, node.taskId);
|
||||
metadata.setValue(updater.indentProperty(), parentIndent);
|
||||
metadataService.save(metadata);
|
||||
}
|
||||
|
||||
model.setId(node.taskId);
|
||||
model.setValue(Task.COMPLETION_DATE, completionDate);
|
||||
taskService.save(model);
|
||||
model.clear();
|
||||
|
||||
taskAdapter.getCompletedItems().put(node.taskId, true);
|
||||
chained.add(node.taskId);
|
||||
}
|
||||
});
|
||||
|
||||
if(chained.size() > 0) {
|
||||
chainedCompletions.put(itemId, chained);
|
||||
taskAdapter.notifyDataSetInvalidated();
|
||||
}
|
||||
}
|
||||
|
||||
public void setList(LIST list) {
|
||||
this.list = list;
|
||||
}
|
||||
|
||||
public void onDeleteTask(Task task) {
|
||||
updater.onDeleteTask(getFilter(), list, task.getId());
|
||||
taskAdapter.notifyDataSetInvalidated();
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,370 @@
|
||||
package com.todoroo.astrid.subtasks;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import com.todoroo.andlib.data.Property.IntegerProperty;
|
||||
import com.todoroo.andlib.data.Property.LongProperty;
|
||||
import com.todoroo.andlib.service.DependencyInjectionService;
|
||||
import com.todoroo.andlib.utility.DateUtilities;
|
||||
import com.todoroo.astrid.api.Filter;
|
||||
import com.todoroo.astrid.core.PluginServices;
|
||||
import com.todoroo.astrid.data.Metadata;
|
||||
import com.todoroo.astrid.data.Task;
|
||||
|
||||
abstract public class OrderedListUpdater<LIST> {
|
||||
|
||||
public OrderedListUpdater() {
|
||||
DependencyInjectionService.getInstance().inject(this);
|
||||
}
|
||||
|
||||
public interface OrderedListIterator {
|
||||
public void processTask(long taskId, Metadata metadata);
|
||||
}
|
||||
|
||||
// --- abstract and empty
|
||||
|
||||
abstract protected Metadata getTaskMetadata(LIST list, long taskId);
|
||||
|
||||
abstract protected IntegerProperty indentProperty();
|
||||
|
||||
abstract protected LongProperty orderProperty();
|
||||
|
||||
abstract protected LongProperty parentProperty();
|
||||
|
||||
abstract protected void iterateThroughList(Filter filter, LIST list, OrderedListIterator iterator);
|
||||
|
||||
abstract protected Metadata createEmptyMetadata(LIST list, long taskId);
|
||||
|
||||
/**
|
||||
* @param list
|
||||
* @param filter
|
||||
*/
|
||||
protected void initialize(LIST list, Filter filter) {
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list
|
||||
*/
|
||||
protected void beforeIndent(LIST list) {
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list
|
||||
* @param taskId
|
||||
* @param metadata
|
||||
* @param indent
|
||||
* @param order
|
||||
*/
|
||||
protected void beforeSaveIndent(LIST list, long taskId, Metadata metadata, int indent, int order) {
|
||||
//
|
||||
}
|
||||
|
||||
// --- task indenting
|
||||
|
||||
/**
|
||||
* Indent a task and all its children
|
||||
*/
|
||||
public void indent(final Filter filter, final LIST list, final long targetTaskId, final int delta) {
|
||||
if(list == null)
|
||||
return;
|
||||
|
||||
beforeIndent(list);
|
||||
|
||||
final AtomicInteger targetTaskIndent = new AtomicInteger(-1);
|
||||
final AtomicInteger previousIndent = new AtomicInteger(-1);
|
||||
final AtomicLong previousTask = new AtomicLong(-1);
|
||||
final AtomicLong globalOrder = new AtomicLong(-1);
|
||||
|
||||
iterateThroughList(filter, list, new OrderedListIterator() {
|
||||
@Override
|
||||
public void processTask(long taskId, Metadata metadata) {
|
||||
if(!metadata.isSaved())
|
||||
metadata = createEmptyMetadata(list, taskId);
|
||||
int indent = metadata.containsNonNullValue(indentProperty()) ?
|
||||
metadata.getValue(indentProperty()) : 0;
|
||||
|
||||
long order = globalOrder.incrementAndGet();
|
||||
metadata.setValue(orderProperty(), order);
|
||||
|
||||
if(targetTaskId == taskId) {
|
||||
// if indenting is warranted, indent me and my children
|
||||
if(indent + delta <= previousIndent.get() + 1 && indent + delta >= 0) {
|
||||
targetTaskIndent.set(indent);
|
||||
metadata.setValue(indentProperty(), indent + delta);
|
||||
|
||||
if(parentProperty() != null) {
|
||||
long newParent = computeNewParent(filter, list,
|
||||
taskId, indent + delta - 1);
|
||||
if (newParent == taskId)
|
||||
metadata.setValue(parentProperty(), Task.NO_ID);
|
||||
else
|
||||
metadata.setValue(parentProperty(), newParent);
|
||||
}
|
||||
saveAndUpdateModifiedDate(metadata, taskId);
|
||||
}
|
||||
} else if(targetTaskIndent.get() > -1) {
|
||||
// found first task that is not beneath target
|
||||
if(indent <= targetTaskIndent.get())
|
||||
targetTaskIndent.set(-1);
|
||||
else {
|
||||
metadata.setValue(indentProperty(), indent + delta);
|
||||
saveAndUpdateModifiedDate(metadata, taskId);
|
||||
}
|
||||
} else {
|
||||
previousIndent.set(indent);
|
||||
previousTask.set(taskId);
|
||||
}
|
||||
|
||||
if(!metadata.isSaved())
|
||||
saveAndUpdateModifiedDate(metadata, taskId);
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to iterate through a list and compute a new parent for the target task
|
||||
* based on the target parent's indent
|
||||
* @param list
|
||||
* @param targetTaskId
|
||||
* @param newIndent
|
||||
* @return
|
||||
*/
|
||||
private long computeNewParent(Filter filter, LIST list, long targetTaskId, int targetParentIndent) {
|
||||
final AtomicInteger desiredParentIndent = new AtomicInteger(targetParentIndent);
|
||||
final AtomicLong targetTask = new AtomicLong(targetTaskId);
|
||||
final AtomicLong lastPotentialParent = new AtomicLong(-1);
|
||||
final AtomicBoolean computedParent = new AtomicBoolean(false);
|
||||
|
||||
iterateThroughList(filter, list, new OrderedListIterator() {
|
||||
@Override
|
||||
public void processTask(long taskId, Metadata metadata) {
|
||||
if (targetTask.get() == taskId) {
|
||||
computedParent.set(true);
|
||||
}
|
||||
|
||||
int indent = metadata.getValue(indentProperty());
|
||||
if (!computedParent.get() && indent == desiredParentIndent.get()) {
|
||||
lastPotentialParent.set(taskId);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (lastPotentialParent.get() == -1) return Task.NO_ID;
|
||||
return lastPotentialParent.get();
|
||||
}
|
||||
|
||||
// --- task moving
|
||||
|
||||
/**
|
||||
* Move a task and all its children to the position right above
|
||||
* taskIdToMoveto. Will change the indent level to match taskIdToMoveTo.
|
||||
*
|
||||
* @param newTaskId task we will move above. if -1, moves to end of list
|
||||
*/
|
||||
public void moveTo(Filter filter, LIST list, final long targetTaskId,
|
||||
final long moveBeforeTaskId) {
|
||||
if(list == null)
|
||||
return;
|
||||
|
||||
Node root = buildTreeModel(filter, list);
|
||||
Node target = findNode(root, targetTaskId);
|
||||
|
||||
if(target != null && target.parent != null) {
|
||||
if(moveBeforeTaskId == -1) {
|
||||
target.parent.children.remove(target);
|
||||
root.children.add(target);
|
||||
target.parent = root;
|
||||
} else {
|
||||
Node sibling = findNode(root, moveBeforeTaskId);
|
||||
if(sibling != null && !ancestorOf(target, sibling)) {
|
||||
int index = sibling.parent.children.indexOf(sibling);
|
||||
|
||||
if(target.parent == sibling.parent &&
|
||||
target.parent.children.indexOf(target) < index)
|
||||
index--;
|
||||
|
||||
target.parent.children.remove(target);
|
||||
sibling.parent.children.add(index, target);
|
||||
target.parent = sibling.parent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
traverseTreeAndWriteValues(list, root, new AtomicLong(0), -1);
|
||||
}
|
||||
|
||||
private boolean ancestorOf(Node ancestor, Node descendant) {
|
||||
if(descendant.parent == ancestor)
|
||||
return true;
|
||||
if(descendant.parent == null)
|
||||
return false;
|
||||
return ancestorOf(ancestor, descendant.parent);
|
||||
}
|
||||
|
||||
protected static class Node {
|
||||
public final long taskId;
|
||||
public Node parent;
|
||||
public final ArrayList<Node> children = new ArrayList<Node>();
|
||||
|
||||
public Node(long taskId, Node parent) {
|
||||
this.taskId = taskId;
|
||||
this.parent = parent;
|
||||
}
|
||||
}
|
||||
|
||||
protected void traverseTreeAndWriteValues(LIST list, Node node, AtomicLong order, int indent) {
|
||||
if(node.taskId != -1) {
|
||||
Metadata metadata = getTaskMetadata(list, node.taskId);
|
||||
if(metadata == null)
|
||||
metadata = createEmptyMetadata(list, node.taskId);
|
||||
metadata.setValue(orderProperty(), order.getAndIncrement());
|
||||
metadata.setValue(indentProperty(), indent);
|
||||
if(parentProperty() != null)
|
||||
metadata.setValue(parentProperty(), node.parent.taskId);
|
||||
saveAndUpdateModifiedDate(metadata, node.taskId);
|
||||
}
|
||||
|
||||
for(Node child : node.children) {
|
||||
traverseTreeAndWriteValues(list, child, order, indent + 1);
|
||||
}
|
||||
}
|
||||
|
||||
protected Node findNode(Node node, long taskId) {
|
||||
if(node.taskId == taskId)
|
||||
return node;
|
||||
for(Node child : node.children) {
|
||||
Node found = findNode(child, taskId);
|
||||
if(found != null)
|
||||
return found;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
protected Node buildTreeModel(Filter filter, LIST list) {
|
||||
final Node root = new Node(-1, null);
|
||||
final AtomicInteger previoustIndent = new AtomicInteger(-1);
|
||||
final AtomicReference<Node> currentNode = new AtomicReference<Node>(root);
|
||||
|
||||
iterateThroughList(filter, list, new OrderedListIterator() {
|
||||
@Override
|
||||
public void processTask(long taskId, Metadata metadata) {
|
||||
int indent = metadata.getValue(indentProperty());
|
||||
|
||||
int previousIndentValue = previoustIndent.get();
|
||||
if(indent == previousIndentValue) { // sibling
|
||||
Node parent = currentNode.get().parent;
|
||||
currentNode.set(new Node(taskId, parent));
|
||||
parent.children.add(currentNode.get());
|
||||
} else if(indent > previousIndentValue) { // child
|
||||
Node parent = currentNode.get();
|
||||
currentNode.set(new Node(taskId, parent));
|
||||
parent.children.add(currentNode.get());
|
||||
} else { // in a different tree
|
||||
Node node = currentNode.get().parent;
|
||||
for(int i = indent; i < previousIndentValue; i++)
|
||||
node = node.parent;
|
||||
if(node == null)
|
||||
node = root;
|
||||
currentNode.set(new Node(taskId, node));
|
||||
node.children.add(currentNode.get());
|
||||
}
|
||||
|
||||
previoustIndent.set(indent);
|
||||
}
|
||||
});
|
||||
return root;
|
||||
}
|
||||
|
||||
protected final Task taskContainer = new Task();
|
||||
|
||||
protected void saveAndUpdateModifiedDate(Metadata metadata, long taskId) {
|
||||
if(metadata.getSetValues().size() == 0)
|
||||
return;
|
||||
PluginServices.getMetadataService().save(metadata);
|
||||
taskContainer.setId(taskId);
|
||||
taskContainer.setValue(Task.MODIFICATION_DATE, DateUtilities.now());
|
||||
taskContainer.setValue(Task.DETAILS_DATE, DateUtilities.now());
|
||||
PluginServices.getTaskService().save(taskContainer);
|
||||
}
|
||||
|
||||
// --- task cascading operations
|
||||
|
||||
public interface OrderedListNodeVisitor {
|
||||
public void visitNode(Node node);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply an operation only to the children of the task
|
||||
*/
|
||||
public void applyToChildren(Filter filter, LIST list, long targetTaskId,
|
||||
OrderedListNodeVisitor visitor) {
|
||||
|
||||
Node root = buildTreeModel(filter, list);
|
||||
Node target = findNode(root, targetTaskId);
|
||||
|
||||
if(target != null)
|
||||
for(Node child : target.children)
|
||||
applyVisitor(child, visitor);
|
||||
}
|
||||
|
||||
private void applyVisitor(Node node, OrderedListNodeVisitor visitor) {
|
||||
visitor.visitNode(node);
|
||||
for(Node child : node.children)
|
||||
applyVisitor(child, visitor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a task from the order hierarchy and un-indent children
|
||||
* @param filter
|
||||
* @param list
|
||||
* @param targetTaskId
|
||||
*/
|
||||
public void onDeleteTask(Filter filter, LIST list, final long targetTaskId) {
|
||||
if(list == null)
|
||||
return;
|
||||
|
||||
Node root = buildTreeModel(filter, list);
|
||||
Node target = findNode(root, targetTaskId);
|
||||
|
||||
if(target != null && target.parent != null) {
|
||||
int targetIndex = target.parent.children.indexOf(target);
|
||||
target.parent.children.remove(targetIndex);
|
||||
for(Node node : target.children)
|
||||
target.parent.children.add(targetIndex++, node);
|
||||
}
|
||||
|
||||
traverseTreeAndWriteValues(list, root, new AtomicLong(0), -1);
|
||||
}
|
||||
|
||||
// --- utility
|
||||
|
||||
public void debugPrint(Filter filter, LIST list) {
|
||||
iterateThroughList(filter, list, new OrderedListIterator() {
|
||||
public void processTask(long taskId, Metadata metadata) {
|
||||
System.err.format("id %d: order %d, indent:%d, parent:%d\n", taskId, //$NON-NLS-1$
|
||||
metadata.getValue(orderProperty()),
|
||||
metadata.getValue(indentProperty()),
|
||||
parentProperty() == null ? -1 : metadata.getValue(parentProperty()));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@SuppressWarnings("nls")
|
||||
public void debugPrint(Node root, int depth) {
|
||||
for(int i = 0; i < depth; i++) System.err.print(" + ");
|
||||
System.err.format("%03d", root.taskId);
|
||||
System.err.print("\n");
|
||||
|
||||
for(int i = 0; i < root.children.size(); i++)
|
||||
debugPrint(root.children.get(i), depth + 1);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,79 @@
|
||||
package com.todoroo.astrid.subtasks;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import com.timsu.astrid.R;
|
||||
import com.todoroo.andlib.data.Property;
|
||||
import com.todoroo.andlib.data.TodorooCursor;
|
||||
import com.todoroo.astrid.activity.TaskListFragment;
|
||||
import com.todoroo.astrid.adapter.TaskAdapter;
|
||||
import com.todoroo.astrid.data.Task;
|
||||
|
||||
/**
|
||||
* Fragment for subtasks
|
||||
*
|
||||
* @author Tim Su <tim@astrid.com>
|
||||
*
|
||||
*/
|
||||
public class SubtasksListFragment extends TaskListFragment {
|
||||
|
||||
protected OrderedListFragmentHelper<?> helper;
|
||||
|
||||
@Override
|
||||
public void onActivityCreated(Bundle savedInstanceState) {
|
||||
helper = createFragmentHelper();
|
||||
super.onActivityCreated(savedInstanceState);
|
||||
}
|
||||
|
||||
protected OrderedListFragmentHelper<?> createFragmentHelper() {
|
||||
OrderedListFragmentHelper<String> olfh =
|
||||
new OrderedListFragmentHelper<String>(this, new SubtasksUpdater());
|
||||
olfh.setList(SubtasksMetadata.LIST_ACTIVE_TASKS);
|
||||
return olfh;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected View getListBody(ViewGroup root) {
|
||||
return getActivity().getLayoutInflater().inflate(R.layout.task_list_body_subtasks, root, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setUpUiComponents() {
|
||||
super.setUpUiComponents();
|
||||
|
||||
helper.setUpUiComponents();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setUpTaskList() {
|
||||
helper.beforeSetUpTaskList(filter);
|
||||
|
||||
super.setUpTaskList();
|
||||
|
||||
unregisterForContextMenu(getListView());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Property<?>[] taskProperties() {
|
||||
return helper.taskProperties();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected boolean isDraggable() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onTaskDelete(Task task) {
|
||||
helper.onDeleteTask(task);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected TaskAdapter createTaskAdapter(TodorooCursor<Task> cursor) {
|
||||
return helper.createTaskAdapter(cursor, sqlQueryTemplate);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
package com.todoroo.astrid.subtasks;
|
||||
|
||||
import com.todoroo.andlib.data.Property.IntegerProperty;
|
||||
import com.todoroo.andlib.data.Property.LongProperty;
|
||||
import com.todoroo.andlib.data.Property.StringProperty;
|
||||
import com.todoroo.astrid.data.Metadata;
|
||||
|
||||
/**
|
||||
* Metadata entries for a Subtask list
|
||||
*
|
||||
* @author Tim Su <tim@todoroo.com>
|
||||
*
|
||||
*/
|
||||
public class SubtasksMetadata {
|
||||
|
||||
public static final String LIST_ACTIVE_TASKS = "[AT]"; //$NON-NLS-1$
|
||||
|
||||
/** metadata key */
|
||||
public static final String METADATA_KEY = "subtasks"; //$NON-NLS-1$
|
||||
|
||||
/** tag name */
|
||||
public static final StringProperty TAG = new StringProperty(Metadata.TABLE,
|
||||
Metadata.VALUE1.name);
|
||||
|
||||
public static final IntegerProperty INDENT = new IntegerProperty(Metadata.TABLE,
|
||||
Metadata.VALUE2.name);
|
||||
|
||||
public static final LongProperty ORDER = new LongProperty(Metadata.TABLE,
|
||||
Metadata.VALUE3.name);
|
||||
|
||||
}
|
@ -0,0 +1,77 @@
|
||||
package com.todoroo.astrid.subtasks;
|
||||
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import com.timsu.astrid.R;
|
||||
import com.todoroo.andlib.data.Property;
|
||||
import com.todoroo.andlib.data.TodorooCursor;
|
||||
import com.todoroo.astrid.actfm.TagViewFragment;
|
||||
import com.todoroo.astrid.adapter.TaskAdapter;
|
||||
import com.todoroo.astrid.data.Task;
|
||||
|
||||
public class SubtasksTagListFragment extends TagViewFragment {
|
||||
|
||||
private final OrderedListFragmentHelper<String> helper;
|
||||
|
||||
public SubtasksTagListFragment() {
|
||||
super();
|
||||
helper = new OrderedListFragmentHelper<String>(this, new SubtasksUpdater());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void postLoadTagData() {
|
||||
String list = "td:" + tagData.getId(); //$NON-NLS-1$
|
||||
helper.setList(list);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected View getListBody(ViewGroup root) {
|
||||
ViewGroup parent = (ViewGroup) getActivity().getLayoutInflater().inflate(
|
||||
R.layout.task_list_body_tag, root, false);
|
||||
|
||||
taskListView =
|
||||
getActivity().getLayoutInflater().inflate(R.layout.task_list_body_subtasks, root, false);
|
||||
parent.addView(taskListView);
|
||||
|
||||
return parent;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setUpUiComponents() {
|
||||
super.setUpUiComponents();
|
||||
|
||||
helper.setUpUiComponents();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setUpTaskList() {
|
||||
helper.beforeSetUpTaskList(filter);
|
||||
|
||||
super.setUpTaskList();
|
||||
|
||||
unregisterForContextMenu(getListView());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Property<?>[] taskProperties() {
|
||||
return helper.taskProperties();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected boolean isDraggable() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onTaskDelete(Task task) {
|
||||
helper.onDeleteTask(task);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected TaskAdapter createTaskAdapter(TodorooCursor<Task> cursor) {
|
||||
return helper.createTaskAdapter(cursor, sqlQueryTemplate);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,153 @@
|
||||
package com.todoroo.astrid.subtasks;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
import com.todoroo.andlib.data.Property.IntegerProperty;
|
||||
import com.todoroo.andlib.data.Property.LongProperty;
|
||||
import com.todoroo.andlib.data.TodorooCursor;
|
||||
import com.todoroo.andlib.service.Autowired;
|
||||
import com.todoroo.andlib.sql.Criterion;
|
||||
import com.todoroo.andlib.sql.Query;
|
||||
import com.todoroo.astrid.api.Filter;
|
||||
import com.todoroo.astrid.dao.TaskDao.TaskCriteria;
|
||||
import com.todoroo.astrid.data.Metadata;
|
||||
import com.todoroo.astrid.data.Task;
|
||||
import com.todoroo.astrid.service.MetadataService;
|
||||
import com.todoroo.astrid.service.TaskService;
|
||||
|
||||
public class SubtasksUpdater extends OrderedListUpdater<String> {
|
||||
|
||||
private static final String METADATA_ID = "mdi"; //$NON-NLS-1$
|
||||
|
||||
@Autowired MetadataService metadataService;
|
||||
@Autowired TaskService taskService;
|
||||
|
||||
@Override
|
||||
protected IntegerProperty indentProperty() {
|
||||
return SubtasksMetadata.INDENT;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected LongProperty orderProperty() {
|
||||
return SubtasksMetadata.ORDER;
|
||||
}
|
||||
@Override
|
||||
protected LongProperty parentProperty() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initialize(String list, Filter filter) {
|
||||
applySubtasksToFilter(filter, list);
|
||||
sanitizeTaskList(filter, list);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Metadata getTaskMetadata(String list, long taskId) {
|
||||
TodorooCursor<Metadata> cursor = metadataService.query(Query.select(Metadata.PROPERTIES).where(
|
||||
Criterion.and(
|
||||
Metadata.TASK.eq(taskId),
|
||||
Metadata.KEY.eq(SubtasksMetadata.METADATA_KEY),
|
||||
SubtasksMetadata.TAG.eq(list))));
|
||||
try {
|
||||
cursor.moveToFirst();
|
||||
if(cursor.isAfterLast())
|
||||
return null;
|
||||
return new Metadata(cursor);
|
||||
} finally {
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Metadata createEmptyMetadata(String list, long taskId) {
|
||||
Metadata m = new Metadata();
|
||||
m.setValue(Metadata.TASK, taskId);
|
||||
m.setValue(Metadata.KEY, SubtasksMetadata.METADATA_KEY);
|
||||
m.setValue(SubtasksMetadata.TAG, list);
|
||||
return m;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void iterateThroughList(Filter filter, String list, OrderedListIterator iterator) {
|
||||
TodorooCursor<Task> cursor = taskService.query(Query.select(Task.ID,
|
||||
Metadata.ID.as(METADATA_ID), Metadata.TASK, Metadata.KEY, SubtasksMetadata.INDENT,
|
||||
SubtasksMetadata.ORDER).withQueryTemplate(filter.sqlQuery));
|
||||
TodorooCursor<Metadata> metadataCursor = new TodorooCursor<Metadata>(cursor.getCursor(),
|
||||
cursor.getProperties());
|
||||
Metadata metadata = new Metadata();
|
||||
try {
|
||||
for(cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) {
|
||||
metadata.readFromCursor(metadataCursor);
|
||||
metadata.setId(cursor.getLong(cursor.getColumnIndex(METADATA_ID)));
|
||||
iterator.processTask(cursor.get(Task.ID), metadata);
|
||||
}
|
||||
} finally {
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("nls")
|
||||
public void applySubtasksToFilter(Filter filter, String tagName) {
|
||||
String query = filter.sqlQuery;
|
||||
|
||||
if(tagName == null)
|
||||
tagName = SubtasksMetadata.LIST_ACTIVE_TASKS;
|
||||
String subtaskJoin = String.format("LEFT JOIN %s ON (%s = %s AND %s = '%s' AND %s = '%s') ",
|
||||
Metadata.TABLE, Task.ID, Metadata.TASK,
|
||||
Metadata.KEY, SubtasksMetadata.METADATA_KEY,
|
||||
SubtasksMetadata.TAG, tagName);
|
||||
|
||||
if(!query.contains(subtaskJoin)) {
|
||||
query = subtaskJoin + query;
|
||||
query = query.replaceAll("ORDER BY .*", "");
|
||||
query = query + String.format(" ORDER BY %s, %s, IFNULL(CAST(%s AS LONG), %s)",
|
||||
Task.DELETION_DATE, Task.COMPLETION_DATE,
|
||||
SubtasksMetadata.ORDER, Task.CREATION_DATE);
|
||||
query = query.replace(TaskCriteria.isVisible().toString(),
|
||||
Criterion.all.toString());
|
||||
|
||||
filter.sqlQuery = query;
|
||||
}
|
||||
}
|
||||
|
||||
public void sanitizeTaskList(Filter filter, String list) {
|
||||
final AtomicInteger previousIndent = new AtomicInteger(-1);
|
||||
final AtomicLong previousOrder = new AtomicLong(-1);
|
||||
final HashSet<Long> taskIds = new HashSet<Long>();
|
||||
|
||||
iterateThroughList(filter, list, new OrderedListIterator() {
|
||||
@Override
|
||||
public void processTask(long taskId, Metadata metadata) {
|
||||
if(!metadata.isSaved())
|
||||
return;
|
||||
|
||||
if(taskIds.contains(taskId)) {
|
||||
metadataService.delete(metadata);
|
||||
return;
|
||||
}
|
||||
|
||||
long order = metadata.getValue(SubtasksMetadata.ORDER);
|
||||
if(order <= previousOrder.get()) // bad
|
||||
order = previousOrder.get() + 1;
|
||||
|
||||
int indent = metadata.getValue(SubtasksMetadata.INDENT);
|
||||
if(indent < 0 || indent > previousIndent.get() + 1) // bad
|
||||
indent = Math.max(0, previousIndent.get());
|
||||
|
||||
metadata.setValue(SubtasksMetadata.ORDER, order);
|
||||
metadata.setValue(SubtasksMetadata.INDENT, indent);
|
||||
saveAndUpdateModifiedDate(metadata, taskId);
|
||||
|
||||
previousIndent.set(indent);
|
||||
previousOrder.set(order);
|
||||
taskIds.add(taskId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 955 B |
Before Width: | Height: | Size: 930 B |
Before Width: | Height: | Size: 435 B |
Before Width: | Height: | Size: 974 B |
Before Width: | Height: | Size: 3.9 KiB |
Before Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 4.7 KiB |
Before Width: | Height: | Size: 2.2 KiB |
Before Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 2.0 KiB |
Before Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 905 B |
Before Width: | Height: | Size: 505 B |
Before Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 623 B |
Before Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 3.4 KiB |
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 3.1 KiB |
Before Width: | Height: | Size: 237 B |
Before Width: | Height: | Size: 214 B |
Before Width: | Height: | Size: 202 B |
Before Width: | Height: | Size: 200 B |
Before Width: | Height: | Size: 9.4 KiB After Width: | Height: | Size: 5.9 KiB |
After Width: | Height: | Size: 2.0 KiB |
After Width: | Height: | Size: 2.8 KiB |
@ -1,6 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<solid
|
||||
android:color="#60000000" />
|
||||
</shape>
|
@ -1,9 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<gradient
|
||||
android:startColor="#ffffffff"
|
||||
android:centerColor="#ff000000"
|
||||
android:endColor="#ffffffff"
|
||||
android:angle="0" />
|
||||
</shape>
|
Before Width: | Height: | Size: 1.0 KiB |
Before Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 983 B |
Before Width: | Height: | Size: 655 B |
Before Width: | Height: | Size: 676 B |
@ -1,20 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright (C) 2008 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item
|
||||
android:drawable="@drawable/none" />
|
||||
</selector>
|
Before Width: | Height: | Size: 4.7 KiB |
Before Width: | Height: | Size: 4.7 KiB |
Before Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 2.8 KiB |
Before Width: | Height: | Size: 3.3 KiB |
Before Width: | Height: | Size: 832 B |
@ -1,7 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<gradient
|
||||
android:startColor="#008d43"
|
||||
android:endColor="#00522b"/>
|
||||
</shape>
|