Complete rewrite of Google Task manual ordering

pull/820/head
Alex Baker 5 years ago
parent bef060591a
commit 087aae0a7b

@ -0,0 +1,996 @@
{
"formatVersion": 1,
"database": {
"version": 63,
"identityHash": "6a0e3b6d4ed545092c478951e77574ba",
"entities": [
{
"tableName": "notification",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `task` INTEGER NOT NULL, `timestamp` INTEGER NOT NULL, `type` INTEGER NOT NULL, `location` INTEGER)",
"fields": [
{
"fieldPath": "uid",
"columnName": "uid",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "taskId",
"columnName": "task",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "timestamp",
"columnName": "timestamp",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "location",
"columnName": "location",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"uid"
],
"autoGenerate": true
},
"indices": [
{
"name": "index_notification_task",
"unique": true,
"columnNames": [
"task"
],
"createSql": "CREATE UNIQUE INDEX `index_notification_task` ON `${TABLE_NAME}` (`task`)"
}
],
"foreignKeys": []
},
{
"tableName": "tagdata",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `remoteId` TEXT, `name` TEXT, `color` INTEGER, `tagOrdering` TEXT)",
"fields": [
{
"fieldPath": "id",
"columnName": "_id",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "remoteId",
"columnName": "remoteId",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "color",
"columnName": "color",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "tagOrdering",
"columnName": "tagOrdering",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"_id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "userActivity",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `remoteId` TEXT, `message` TEXT, `picture` TEXT, `target_id` TEXT, `created_at` INTEGER)",
"fields": [
{
"fieldPath": "id",
"columnName": "_id",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "remoteId",
"columnName": "remoteId",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "message",
"columnName": "message",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "picture",
"columnName": "picture",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "targetId",
"columnName": "target_id",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "created",
"columnName": "created_at",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"_id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "task_attachments",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `remoteId` TEXT, `task_id` TEXT, `name` TEXT, `path` TEXT, `content_type` TEXT)",
"fields": [
{
"fieldPath": "id",
"columnName": "_id",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "remoteId",
"columnName": "remoteId",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "taskId",
"columnName": "task_id",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "uri",
"columnName": "path",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "contentType",
"columnName": "content_type",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"_id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "task_list_metadata",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `remoteId` TEXT, `tag_uuid` TEXT, `filter` TEXT, `task_ids` TEXT)",
"fields": [
{
"fieldPath": "id",
"columnName": "_id",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "remoteId",
"columnName": "remoteId",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "tagUuid",
"columnName": "tag_uuid",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "filter",
"columnName": "filter",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "taskIds",
"columnName": "task_ids",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"_id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "tasks",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `title` TEXT, `importance` INTEGER, `dueDate` INTEGER, `hideUntil` INTEGER, `created` INTEGER, `modified` INTEGER, `completed` INTEGER, `deleted` INTEGER, `notes` TEXT, `estimatedSeconds` INTEGER, `elapsedSeconds` INTEGER, `timerStart` INTEGER, `notificationFlags` INTEGER, `notifications` INTEGER, `lastNotified` INTEGER, `snoozeTime` INTEGER, `recurrence` TEXT, `repeatUntil` INTEGER, `calendarUri` TEXT, `remoteId` TEXT)",
"fields": [
{
"fieldPath": "id",
"columnName": "_id",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "priority",
"columnName": "importance",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "dueDate",
"columnName": "dueDate",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "hideUntil",
"columnName": "hideUntil",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "created",
"columnName": "created",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "modified",
"columnName": "modified",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "completed",
"columnName": "completed",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "deleted",
"columnName": "deleted",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "notes",
"columnName": "notes",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "estimatedSeconds",
"columnName": "estimatedSeconds",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "elapsedSeconds",
"columnName": "elapsedSeconds",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "timerStart",
"columnName": "timerStart",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "notificationFlags",
"columnName": "notificationFlags",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "notifications",
"columnName": "notifications",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "lastNotified",
"columnName": "lastNotified",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "snoozeTime",
"columnName": "snoozeTime",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "recurrence",
"columnName": "recurrence",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "repeatUntil",
"columnName": "repeatUntil",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "calendarUri",
"columnName": "calendarUri",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "remoteId",
"columnName": "remoteId",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"_id"
],
"autoGenerate": true
},
"indices": [
{
"name": "t_rid",
"unique": true,
"columnNames": [
"remoteId"
],
"createSql": "CREATE UNIQUE INDEX `t_rid` ON `${TABLE_NAME}` (`remoteId`)"
}
],
"foreignKeys": []
},
{
"tableName": "alarms",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `task` INTEGER NOT NULL, `time` INTEGER NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "task",
"columnName": "task",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "time",
"columnName": "time",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"_id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "places",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`place_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `uid` TEXT, `name` TEXT, `address` TEXT, `phone` TEXT, `url` TEXT, `latitude` REAL NOT NULL, `longitude` REAL NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "place_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "uid",
"columnName": "uid",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "address",
"columnName": "address",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "phone",
"columnName": "phone",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "url",
"columnName": "url",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "latitude",
"columnName": "latitude",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "longitude",
"columnName": "longitude",
"affinity": "REAL",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"place_id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "geofences",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`geofence_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `task` INTEGER NOT NULL, `place` TEXT, `radius` INTEGER NOT NULL, `arrival` INTEGER NOT NULL, `departure` INTEGER NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "geofence_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "task",
"columnName": "task",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "place",
"columnName": "place",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "radius",
"columnName": "radius",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "arrival",
"columnName": "arrival",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "departure",
"columnName": "departure",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"geofence_id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "tags",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `task` INTEGER NOT NULL, `name` TEXT, `tag_uid` TEXT, `task_uid` TEXT)",
"fields": [
{
"fieldPath": "id",
"columnName": "_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "task",
"columnName": "task",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "tagUid",
"columnName": "tag_uid",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "taskUid",
"columnName": "task_uid",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"_id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "google_tasks",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`gt_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `gt_task` INTEGER NOT NULL, `gt_remote_id` TEXT, `gt_list_id` TEXT, `gt_parent` INTEGER NOT NULL, `gt_remote_parent` TEXT, `gt_moved` INTEGER NOT NULL, `gt_order` INTEGER NOT NULL, `gt_remote_order` INTEGER NOT NULL, `gt_last_sync` INTEGER NOT NULL, `gt_deleted` INTEGER NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "gt_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "task",
"columnName": "gt_task",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "remoteId",
"columnName": "gt_remote_id",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "listId",
"columnName": "gt_list_id",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "parent",
"columnName": "gt_parent",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "remoteParent",
"columnName": "gt_remote_parent",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "moved",
"columnName": "gt_moved",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "order",
"columnName": "gt_order",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "remoteOrder",
"columnName": "gt_remote_order",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastSync",
"columnName": "gt_last_sync",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "deleted",
"columnName": "gt_deleted",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"gt_id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "filters",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `title` TEXT, `sql` TEXT, `values` TEXT, `criterion` TEXT)",
"fields": [
{
"fieldPath": "id",
"columnName": "_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "sql",
"columnName": "sql",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "values",
"columnName": "values",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "criterion",
"columnName": "criterion",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"_id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "google_task_lists",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `account` TEXT, `remote_id` TEXT, `title` TEXT, `remote_order` INTEGER NOT NULL, `last_sync` INTEGER NOT NULL, `deleted` INTEGER NOT NULL, `color` INTEGER)",
"fields": [
{
"fieldPath": "id",
"columnName": "_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "account",
"columnName": "account",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "remoteId",
"columnName": "remote_id",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "remoteOrder",
"columnName": "remote_order",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastSync",
"columnName": "last_sync",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "deleted",
"columnName": "deleted",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "color",
"columnName": "color",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"_id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "caldav_calendar",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `account` TEXT, `uuid` TEXT, `name` TEXT, `color` INTEGER NOT NULL, `ctag` TEXT, `url` TEXT)",
"fields": [
{
"fieldPath": "id",
"columnName": "_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "account",
"columnName": "account",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "uuid",
"columnName": "uuid",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "color",
"columnName": "color",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "ctag",
"columnName": "ctag",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "url",
"columnName": "url",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"_id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "caldav_tasks",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `task` INTEGER NOT NULL, `calendar` TEXT, `object` TEXT, `remote_id` TEXT, `etag` TEXT, `last_sync` INTEGER NOT NULL, `deleted` INTEGER NOT NULL, `vtodo` TEXT)",
"fields": [
{
"fieldPath": "id",
"columnName": "_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "task",
"columnName": "task",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "calendar",
"columnName": "calendar",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "object",
"columnName": "object",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "remoteId",
"columnName": "remote_id",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "etag",
"columnName": "etag",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "lastSync",
"columnName": "last_sync",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "deleted",
"columnName": "deleted",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "vtodo",
"columnName": "vtodo",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"_id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "caldav_account",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `uuid` TEXT, `name` TEXT, `url` TEXT, `username` TEXT, `password` TEXT, `error` TEXT)",
"fields": [
{
"fieldPath": "id",
"columnName": "_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "uuid",
"columnName": "uuid",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "url",
"columnName": "url",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "username",
"columnName": "username",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "password",
"columnName": "password",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "error",
"columnName": "error",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"_id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "google_task_accounts",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `account` TEXT, `error` TEXT, `etag` TEXT)",
"fields": [
{
"fieldPath": "id",
"columnName": "_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "account",
"columnName": "account",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "error",
"columnName": "error",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "etag",
"columnName": "etag",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"_id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"6a0e3b6d4ed545092c478951e77574ba\")"
]
}
}

@ -1,208 +0,0 @@
/*
* Copyright (c) 2012 Todoroo Inc
*
* See the file "LICENSE" for the full license governing this code.
*/
package com.todoroo.astrid.gtasks;
import static junit.framework.Assert.assertNotNull;
import static junit.framework.Assert.assertTrue;
import androidx.test.runner.AndroidJUnit4;
import com.google.api.services.tasks.model.TaskList;
import com.todoroo.astrid.dao.TaskDao;
import com.todoroo.astrid.data.Task;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.tasks.data.GoogleTask;
import org.tasks.data.GoogleTaskAccount;
import org.tasks.data.GoogleTaskDao;
import org.tasks.data.GoogleTaskList;
import org.tasks.data.GoogleTaskListDao;
import org.tasks.injection.InjectingTestCase;
import org.tasks.injection.TestComponent;
@SuppressWarnings("nls")
@RunWith(AndroidJUnit4.class)
public class GtasksIndentActionTest extends InjectingTestCase {
@Inject GtasksListService gtasksListService;
@Inject GoogleTaskListDao googleTaskListDao;
@Inject GtasksTaskListUpdater gtasksTaskListUpdater;
@Inject TaskDao taskDao;
@Inject GoogleTaskDao googleTaskDao;
private Task task;
private GoogleTaskList storeList;
@Test
public void testIndentWithoutMetadata() {
givenTask(taskWithoutMetadata());
whenIncreaseIndent();
// should not crash
}
@Test
public void disabled_testIndentWithMetadataButNoOtherTasks() {
givenTask(taskWithMetadata(0, 0));
whenIncreaseIndent();
thenExpectIndentationLevel(0);
}
@Test
public void testIndentWithMetadata() {
taskWithMetadata(0, 0);
givenTask(taskWithMetadata(1, 0));
whenIncreaseIndent();
thenExpectIndentationLevel(1);
}
@Test
public void testDeindentWithMetadata() {
givenTask(taskWithMetadata(0, 1));
whenDecreaseIndent();
thenExpectIndentationLevel(0);
}
@Test
public void testDeindentWithoutMetadata() {
givenTask(taskWithoutMetadata());
whenDecreaseIndent();
// should not crash
}
@Test
public void testDeindentWhenAlreadyZero() {
givenTask(taskWithMetadata(0, 0));
whenDecreaseIndent();
thenExpectIndentationLevel(0);
}
@Test
public void testIndentWithChildren() {
taskWithMetadata(0, 0);
givenTask(taskWithMetadata(1, 0));
Task child = taskWithMetadata(2, 1);
whenIncreaseIndent();
thenExpectIndentationLevel(1);
thenExpectIndentationLevel(child, 2);
}
@Test
public void testDeindentWithChildren() {
taskWithMetadata(0, 0);
givenTask(taskWithMetadata(1, 1));
Task child = taskWithMetadata(2, 2);
whenDecreaseIndent();
thenExpectIndentationLevel(0);
thenExpectIndentationLevel(child, 1);
}
@Test
public void testIndentWithSiblings() {
taskWithMetadata(0, 0);
givenTask(taskWithMetadata(1, 0));
Task sibling = taskWithMetadata(2, 0);
whenIncreaseIndent();
thenExpectIndentationLevel(1);
thenExpectIndentationLevel(sibling, 0);
}
@Test
public void testIndentWithChildrensChildren() {
taskWithMetadata(0, 0);
givenTask(taskWithMetadata(1, 0));
Task child = taskWithMetadata(2, 1);
Task grandchild = taskWithMetadata(3, 2);
whenIncreaseIndent();
thenExpectIndentationLevel(1);
thenExpectIndentationLevel(child, 2);
thenExpectIndentationLevel(grandchild, 3);
}
// --- helpers
private void whenIncreaseIndent() {
gtasksTaskListUpdater.indent(storeList, task.getId(), 1);
}
private void whenDecreaseIndent() {
gtasksTaskListUpdater.indent(storeList, task.getId(), -1);
}
@Override
public void setUp() {
super.setUp();
List<TaskList> items = new ArrayList<>();
TaskList list = new TaskList();
list.setId("list");
list.setTitle("Test Tasks");
items.add(list);
GoogleTaskAccount account = new GoogleTaskAccount("account");
googleTaskListDao.insert(account);
gtasksListService.updateLists(account, items);
storeList = googleTaskListDao.getLists("account").get(0);
}
@Override
protected void inject(TestComponent component) {
component.inject(this);
}
private Task taskWithMetadata(long order, int indentation) {
Task newTask = new Task();
taskDao.createNew(newTask);
GoogleTask metadata = new GoogleTask(newTask.getId(), "list");
metadata.setIndent(indentation);
metadata.setOrder(order);
googleTaskDao.insert(metadata);
return newTask;
}
private void thenExpectIndentationLevel(int expected) {
thenExpectIndentationLevel(task, expected);
}
private void thenExpectIndentationLevel(Task targetTask, int expected) {
GoogleTask metadata = googleTaskDao.getByTaskId(targetTask.getId());
assertNotNull("task has metadata", metadata);
int indentation = metadata.getIndent();
assertTrue("indentation: " + indentation, indentation == expected);
}
private void givenTask(Task taskToTest) {
task = taskToTest;
}
private Task taskWithoutMetadata() {
Task task = new Task();
taskDao.createNew(task);
return task;
}
}

@ -1,205 +0,0 @@
/*
* Copyright (c) 2012 Todoroo Inc
*
* See the file "LICENSE" for the full license governing this code.
*/
package com.todoroo.astrid.gtasks;
import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertNotNull;
import androidx.test.runner.AndroidJUnit4;
import com.google.api.services.tasks.model.TaskList;
import com.todoroo.astrid.dao.TaskDao;
import com.todoroo.astrid.data.Task;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.tasks.data.GoogleTask;
import org.tasks.data.GoogleTaskAccount;
import org.tasks.data.GoogleTaskDao;
import org.tasks.data.GoogleTaskList;
import org.tasks.data.GoogleTaskListDao;
import org.tasks.injection.InjectingTestCase;
import org.tasks.injection.TestComponent;
@SuppressWarnings("nls")
@RunWith(AndroidJUnit4.class)
public class GtasksTaskListUpdaterTest extends InjectingTestCase {
private static final int VALUE_UNSET = -1;
@Inject GtasksTaskListUpdater gtasksTaskListUpdater;
@Inject GtasksListService gtasksListService;
@Inject TaskDao taskDao;
@Inject GoogleTaskDao googleTaskDao;
@Inject GoogleTaskListDao googleTaskListDao;
@Test
public void testBasicParentComputation() {
Task[] tasks = givenTasksABCDE();
whenCalculatingParentsAndSiblings();
thenExpectParent(tasks[0], null);
thenExpectParent(tasks[1], tasks[0]);
thenExpectParent(tasks[2], tasks[0]);
thenExpectParent(tasks[3], tasks[2]);
thenExpectParent(tasks[4], null);
}
@Test
public void testBasicSiblingComputation() {
Task[] tasks = givenTasksABCDE();
whenCalculatingParentsAndSiblings();
thenExpectSibling(tasks[0], null);
thenExpectSibling(tasks[1], null);
thenExpectSibling(tasks[2], tasks[1]);
thenExpectSibling(tasks[3], null);
thenExpectSibling(tasks[4], tasks[0]);
}
@Ignore
@Test
public void testMetadataParentComputation() {
Task[] tasks = givenTasksABCDE();
thenExpectMetadataParent(tasks[0], null);
thenExpectMetadataParent(tasks[1], tasks[0]);
thenExpectMetadataParent(tasks[2], tasks[0]);
thenExpectMetadataParent(tasks[3], tasks[2]);
thenExpectMetadataParent(tasks[4], null);
}
@Test
public void testMetadataOrderComputation() {
Task[] tasks = givenTasksABCDE();
thenExpectMetadataIndentAndOrder(tasks[0], 0, 0);
thenExpectMetadataIndentAndOrder(tasks[1], 1, 1);
thenExpectMetadataIndentAndOrder(tasks[2], 2, 1);
thenExpectMetadataIndentAndOrder(tasks[3], 3, 2);
thenExpectMetadataIndentAndOrder(tasks[4], 4, 0);
}
@Ignore
@Test
public void testNewTaskOrder() {
givenTasksABCDE();
Task newTask = createTask("F", VALUE_UNSET, 0);
thenExpectMetadataIndentAndOrder(newTask, 5, 0);
}
// --- helpers
private void thenExpectMetadataIndentAndOrder(Task task, long order, int indent) {
GoogleTask metadata = googleTaskDao.getByTaskId(task.getId());
assertNotNull("metadata was found", metadata);
assertEquals("order", order, metadata.getOrder());
assertEquals("indentation", indent, metadata.getIndent());
}
private void thenExpectMetadataParent(Task task, Task expectedParent) {
GoogleTask metadata = googleTaskDao.getByTaskId(task.getId());
long parent = metadata.getParent();
if (expectedParent == null) {
assertEquals("Task " + task.getTitle() + " parent none", 0, parent);
} else {
assertEquals(
"Task " + task.getTitle() + " parent " + expectedParent.getTitle(),
expectedParent.getId(),
parent);
}
}
private void thenExpectSibling(Task task, Task expectedSibling) {
long sibling = gtasksTaskListUpdater.siblings.get(task.getId());
if (expectedSibling == null) {
assertEquals("Task " + task.getTitle() + " sibling null", 0L, sibling);
} else {
assertEquals(
"Task " + task.getTitle() + " sibling " + expectedSibling.getTitle(),
expectedSibling.getId(),
sibling);
}
}
private void thenExpectParent(Task task, Task expectedParent) {
long parent = gtasksTaskListUpdater.parents.get(task.getId());
if (expectedParent == null) {
assertEquals("Task " + task.getTitle() + " parent null", 0L, parent);
} else {
assertEquals(
"Task " + task.getTitle() + " parent " + expectedParent.getTitle(),
expectedParent.getId(),
parent);
}
}
@Override
public void setUp() {
super.setUp();
List<TaskList> items = new ArrayList<>();
TaskList list = new TaskList();
list.setId("1");
list.setTitle("Tim's Tasks");
items.add(list);
GoogleTaskAccount account = new GoogleTaskAccount("account");
googleTaskListDao.insert(account);
gtasksListService.updateLists(account, items);
}
@Override
protected void inject(TestComponent component) {
component.inject(this);
}
private void whenCalculatingParentsAndSiblings() {
createParentSiblingMaps();
}
private void createParentSiblingMaps() {
for (GoogleTaskList list : googleTaskListDao.getLists("account")) {
gtasksTaskListUpdater.updateParentSiblingMapsFor(list);
}
}
// A
// B
// C
// D
// E
private Task[] givenTasksABCDE() {
return new Task[] {
createTask("A", 0, 0),
createTask("B", 1, 1),
createTask("C", 2, 1),
createTask("D", 3, 2),
createTask("E", 4, 0),
};
}
private Task createTask(String title, long order, int indent) {
Task task = new Task();
task.setTitle(title);
taskDao.createNew(task);
GoogleTask metadata = new GoogleTask(task.getId(), "1");
if (order != VALUE_UNSET) {
metadata.setOrder(order);
}
if (indent != VALUE_UNSET) {
metadata.setIndent(indent);
}
googleTaskDao.insert(metadata);
return task;
}
}

@ -1,313 +0,0 @@
/*
* Copyright (c) 2012 Todoroo Inc
*
* See the file "LICENSE" for the full license governing this code.
*/
package com.todoroo.astrid.gtasks;
import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertNotNull;
import androidx.test.runner.AndroidJUnit4;
import com.google.api.services.tasks.model.TaskList;
import com.todoroo.astrid.dao.TaskDao;
import com.todoroo.astrid.data.Task;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.tasks.data.GoogleTask;
import org.tasks.data.GoogleTaskAccount;
import org.tasks.data.GoogleTaskDao;
import org.tasks.data.GoogleTaskList;
import org.tasks.data.GoogleTaskListDao;
import org.tasks.injection.InjectingTestCase;
import org.tasks.injection.TestComponent;
@SuppressWarnings("nls")
@RunWith(AndroidJUnit4.class)
public class GtasksTaskMovingTest extends InjectingTestCase {
private static final int VALUE_UNSET = -1;
@Inject GtasksListService gtasksListService;
@Inject GtasksTaskListUpdater gtasksTaskListUpdater;
@Inject TaskDao taskDao;
@Inject GoogleTaskDao googleTaskDao;
@Inject GoogleTaskListDao googleTaskListDao;
private Task A, B, C, D, E, F;
private GoogleTaskList list;
/* Starting State:
*
* A
* B
* C
* D
* E
* F
*/
@Test
public void testMoveDownFromListBottom() {
givenTasksABCDEF();
whenTriggerMove(F, null);
thenExpectMetadataOrderAndIndent(E, 4, 0);
thenExpectMetadataOrderAndIndent(F, 5, 0);
}
@Test
public void testMoveDownToListBottom() {
givenTasksABCDEF();
whenTriggerMove(E, null);
thenExpectMetadataOrderAndIndent(E, 5, 0);
thenExpectMetadataOrderAndIndent(F, 4, 0);
}
@Test
public void testMoveUpSimple() {
givenTasksABCDEF();
whenTriggerMove(F, E);
thenExpectMetadataOrderAndIndent(E, 5, 0);
thenExpectMetadataOrderAndIndent(F, 4, 0);
}
@Test
public void testMoveUpWithSubtasks() {
givenTasksABCDEF();
whenTriggerMove(C, B);
/*
* A
* C
* D
* B
*/
thenExpectMetadataOrderAndIndent(A, 0, 0);
thenExpectMetadataOrderAndIndent(B, 3, 1);
thenExpectMetadataOrderAndIndent(C, 1, 1);
thenExpectMetadataOrderAndIndent(D, 2, 2);
}
@Test
public void testMoveDownThroughSubtasks() {
givenTasksABCDEF();
whenTriggerMove(B, E);
/*
* A
* C
* D
* B
* E
*/
thenExpectMetadataOrderAndIndent(A, 0, 0);
thenExpectMetadataOrderAndIndent(B, 3, 0);
thenExpectMetadataOrderAndIndent(C, 1, 1);
thenExpectMetadataOrderAndIndent(D, 2, 2);
}
@Test
public void testMoveUpAboveParent() {
givenTasksABCDEF();
whenTriggerMove(B, A);
/*
* B
* A
* C
* D
* E
* F
*/
thenExpectMetadataOrderAndIndent(A, 1, 0);
thenExpectMetadataOrderAndIndent(B, 0, 0);
thenExpectMetadataOrderAndIndent(C, 2, 1);
}
@Test
public void testMoveDownWithChildren() {
givenTasksABCDEF();
whenTriggerMove(C, F);
/*
* A
* B
* E
* C
* D
* F
*/
thenExpectMetadataOrderAndIndent(A, 0, 0);
thenExpectMetadataOrderAndIndent(B, 1, 1);
thenExpectMetadataOrderAndIndent(C, 3, 0);
thenExpectMetadataOrderAndIndent(D, 4, 1);
thenExpectMetadataOrderAndIndent(E, 2, 0);
}
@Test
public void testMoveDownIndentingTwice() {
givenTasksABCDEF();
whenTriggerMove(D, F);
/*
* A
* B
* C
* E
* D
*/
thenExpectMetadataOrderAndIndent(A, 0, 0);
thenExpectMetadataOrderAndIndent(B, 1, 1);
thenExpectMetadataOrderAndIndent(C, 2, 1);
thenExpectMetadataOrderAndIndent(D, 4, 0);
thenExpectMetadataOrderAndIndent(E, 3, 0);
}
@Test
public void testMoveUpMultiple() {
givenTasksABCDEF();
whenTriggerMove(C, A);
/*
* C
* D
* A
* B
*/
thenExpectMetadataOrderAndIndent(A, 2, 0);
thenExpectMetadataOrderAndIndent(B, 3, 1);
thenExpectMetadataOrderAndIndent(C, 0, 0);
thenExpectMetadataOrderAndIndent(D, 1, 1);
}
@Test
public void testMoveUpIntoSublist() {
givenTasksABCDEF();
whenTriggerMove(F, D);
/*
* A
* B
* C
* F
* D
*/
thenExpectMetadataOrderAndIndent(A, 0, 0);
thenExpectMetadataOrderAndIndent(B, 1, 1);
thenExpectMetadataOrderAndIndent(C, 2, 1);
thenExpectMetadataOrderAndIndent(D, 4, 2);
thenExpectMetadataOrderAndIndent(E, 5, 0);
thenExpectMetadataOrderAndIndent(F, 3, 2);
}
@Test
public void testMoveDownMultiple() {
givenTasksABCDEF();
whenTriggerMove(B, F);
/*
* A
* C
* D
* E
* B
*/
thenExpectMetadataOrderAndIndent(A, 0, 0);
thenExpectMetadataOrderAndIndent(B, 4, 0);
thenExpectMetadataOrderAndIndent(C, 1, 1);
thenExpectMetadataOrderAndIndent(D, 2, 2);
thenExpectMetadataOrderAndIndent(E, 3, 0);
thenExpectMetadataOrderAndIndent(F, 5, 0);
}
// --- helpers
/** moveTo = null => move to end */
private void whenTriggerMove(Task target, Task moveTo) {
gtasksTaskListUpdater.moveTo(list, target.getId(), moveTo == null ? -1 : moveTo.getId());
}
private void thenExpectMetadataOrderAndIndent(Task task, long order, int indent) {
GoogleTask metadata = googleTaskDao.getByTaskId(task.getId());
assertNotNull("metadata was found", metadata);
assertEquals("order", order, metadata.getOrder());
assertEquals("indentation", indent, metadata.getIndent());
}
@Override
public void setUp() {
super.setUp();
List<TaskList> items = new ArrayList<>();
TaskList taskList = new TaskList();
taskList.setId("1");
taskList.setTitle("Tim's Tasks");
items.add(taskList);
GoogleTaskAccount account = new GoogleTaskAccount("account");
googleTaskListDao.insert(account);
gtasksListService.updateLists(account, items);
list = googleTaskListDao.getLists("account").get(0);
}
@Override
protected void inject(TestComponent component) {
component.inject(this);
}
// A
// B
// C
// D
// E
// F
private void givenTasksABCDEF() {
A = createTask("A", 0, 0);
B = createTask("B", 1, 1);
C = createTask("C", 2, 1);
D = createTask("D", 3, 2);
E = createTask("E", 4, 0);
F = createTask("F", 5, 0);
}
private Task createTask(String title, long order, int indent) {
Task task = new Task();
task.setTitle(title);
taskDao.createNew(task);
GoogleTask metadata = new GoogleTask(task.getId(), "1");
if (order != VALUE_UNSET) {
metadata.setOrder(order);
}
if (indent != VALUE_UNSET) {
metadata.setIndent(indent);
}
googleTaskDao.insert(metadata);
return task;
}
}

@ -0,0 +1,187 @@
package org.tasks.data;
import static com.natpryce.makeiteasy.MakeItEasy.with;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.tasks.makers.GoogleTaskMaker.REMOTE_ID;
import static org.tasks.makers.GoogleTaskMaker.TASK;
import static org.tasks.makers.GoogleTaskMaker.newGoogleTask;
import static org.tasks.makers.GtaskListMaker.newGtaskList;
import static org.tasks.makers.TaskMaker.newTask;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.todoroo.astrid.dao.TaskDao;
import com.todoroo.astrid.data.Task;
import java.util.List;
import javax.inject.Inject;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.tasks.injection.InjectingTestCase;
import org.tasks.injection.TestComponent;
@RunWith(AndroidJUnit4.class)
public class GoogleTaskDaoTests extends InjectingTestCase {
@Inject GoogleTaskListDao googleTaskListDao;
@Inject GoogleTaskDao googleTaskDao;
@Inject TaskDao taskDao;
@Override
@Before
public void setUp() {
super.setUp();
googleTaskListDao.insert(newGtaskList());
}
@Test
public void insertAtTopOfEmptyList() {
insertTop(newGoogleTask(with(REMOTE_ID, "1234")));
List<GoogleTask> tasks = googleTaskDao.getByLocalOrder("1");
assertEquals(1, tasks.size());
GoogleTask task = tasks.get(0);
assertEquals("1234", task.getRemoteId());
assertEquals(0, task.getOrder());
}
@Test
public void insertAtBottomOfEmptyList() {
insertBottom(newGoogleTask(with(REMOTE_ID, "1234")));
List<GoogleTask> tasks = googleTaskDao.getByLocalOrder("1");
assertEquals(1, tasks.size());
GoogleTask task = tasks.get(0);
assertEquals("1234", task.getRemoteId());
assertEquals(0, task.getOrder());
}
@Test
public void getPreviousIsNullForTopTask() {
googleTaskDao.insertAndShift(newGoogleTask(), true);
assertNull(googleTaskDao.getPrevious("1", 0, 0));
}
@Test
public void getPrevious() {
insertTop(newGoogleTask());
insertTop(newGoogleTask(with(REMOTE_ID, "1234")));
assertEquals("1234", googleTaskDao.getPrevious("1", 0, 1));
}
@Test
public void insertAtTopOfList() {
insertTop(newGoogleTask(with(REMOTE_ID, "1234")));
insertTop(newGoogleTask(with(REMOTE_ID, "5678")));
List<GoogleTask> tasks = googleTaskDao.getByLocalOrder("1");
assertEquals(2, tasks.size());
GoogleTask top = tasks.get(0);
assertEquals("5678", top.getRemoteId());
assertEquals(0, top.getOrder());
}
@Test
public void insertAtTopOfListShiftsExisting() {
insertTop(newGoogleTask(with(REMOTE_ID, "1234")));
insertTop(newGoogleTask(with(REMOTE_ID, "5678")));
List<GoogleTask> tasks = googleTaskDao.getByLocalOrder("1");
assertEquals(2, tasks.size());
GoogleTask bottom = tasks.get(1);
assertEquals("1234", bottom.getRemoteId());
assertEquals(1, bottom.getOrder());
}
@Test
public void getTaskFromRemoteId() {
googleTaskDao.insert(newGoogleTask(with(REMOTE_ID, "1234"), with(TASK, 4)));
assertEquals(4, googleTaskDao.getTask("1234"));
}
@Test
public void getRemoteIdForTask() {
googleTaskDao.insert(newGoogleTask(with(REMOTE_ID, "1234"), with(TASK, 4)));
assertEquals("1234", googleTaskDao.getRemoteId(4L));
}
@Test
public void moveDownInList() {
googleTaskDao.insertAndShift(newGoogleTask(with(REMOTE_ID, "1")), false);
googleTaskDao.insertAndShift(newGoogleTask(with(REMOTE_ID, "2")), false);
googleTaskDao.insertAndShift(newGoogleTask(with(REMOTE_ID, "3")), false);
GoogleTask two = googleTaskDao.getByRemoteId("2");
googleTaskDao.move(two, 0, 0);
assertEquals(0, googleTaskDao.getByRemoteId("2").getOrder());
assertEquals(1, googleTaskDao.getByRemoteId("1").getOrder());
assertEquals(2, googleTaskDao.getByRemoteId("3").getOrder());
}
@Test
public void moveUpInList() {
googleTaskDao.insertAndShift(newGoogleTask(with(REMOTE_ID, "1")), false);
googleTaskDao.insertAndShift(newGoogleTask(with(REMOTE_ID, "2")), false);
googleTaskDao.insertAndShift(newGoogleTask(with(REMOTE_ID, "3")), false);
GoogleTask one = googleTaskDao.getByRemoteId("1");
googleTaskDao.move(one, 0, 1);
assertEquals(0, googleTaskDao.getByRemoteId("2").getOrder());
assertEquals(1, googleTaskDao.getByRemoteId("1").getOrder());
assertEquals(2, googleTaskDao.getByRemoteId("3").getOrder());
}
@Test
public void moveToTop() {
googleTaskDao.insertAndShift(newGoogleTask(with(REMOTE_ID, "1")), false);
googleTaskDao.insertAndShift(newGoogleTask(with(REMOTE_ID, "2")), false);
googleTaskDao.insertAndShift(newGoogleTask(with(REMOTE_ID, "3")), false);
GoogleTask three = googleTaskDao.getByRemoteId("3");
googleTaskDao.move(three, 0, 0);
assertEquals(0, googleTaskDao.getByRemoteId("3").getOrder());
assertEquals(1, googleTaskDao.getByRemoteId("1").getOrder());
assertEquals(2, googleTaskDao.getByRemoteId("2").getOrder());
}
@Test
public void moveToBottom() {
googleTaskDao.insertAndShift(newGoogleTask(with(REMOTE_ID, "1")), false);
googleTaskDao.insertAndShift(newGoogleTask(with(REMOTE_ID, "2")), false);
googleTaskDao.insertAndShift(newGoogleTask(with(REMOTE_ID, "3")), false);
GoogleTask one = googleTaskDao.getByRemoteId("1");
googleTaskDao.move(one, 0, 2);
assertEquals(0, googleTaskDao.getByRemoteId("2").getOrder());
assertEquals(1, googleTaskDao.getByRemoteId("3").getOrder());
assertEquals(2, googleTaskDao.getByRemoteId("1").getOrder());
}
private void insertTop(GoogleTask googleTask) {
insert(googleTask, true);
}
private void insertBottom(GoogleTask googleTask) {
insert(googleTask, false);
}
private void insert(GoogleTask googleTask, boolean top) {
Task task = newTask();
taskDao.createNew(task);
googleTask.setTask(task.getId());
googleTaskDao.insertAndShift(googleTask, top);
}
@Override
protected void inject(TestComponent component) {
component.inject(this);
}
}

@ -2,11 +2,8 @@ package org.tasks.injection;
import com.todoroo.astrid.alarms.AlarmJobServiceTest;
import com.todoroo.astrid.dao.TaskDaoTests;
import com.todoroo.astrid.gtasks.GtasksIndentActionTest;
import com.todoroo.astrid.gtasks.GtasksListServiceTest;
import com.todoroo.astrid.gtasks.GtasksMetadataServiceTest;
import com.todoroo.astrid.gtasks.GtasksTaskListUpdaterTest;
import com.todoroo.astrid.gtasks.GtasksTaskMovingTest;
import com.todoroo.astrid.model.TaskTest;
import com.todoroo.astrid.reminders.ReminderServiceTest;
import com.todoroo.astrid.repeats.RepeatTaskHelperTest;
@ -17,20 +14,15 @@ import com.todoroo.astrid.subtasks.SubtasksMovingTest;
import com.todoroo.astrid.sync.NewSyncTestCase;
import dagger.Component;
import org.tasks.data.DeletionDaoTests;
import org.tasks.data.GoogleTaskDaoTests;
import org.tasks.jobs.BackupServiceTests;
@ApplicationScope
@Component(modules = TestModule.class)
public interface TestComponent extends ApplicationComponent {
void inject(GtasksIndentActionTest gtasksIndentActionTest);
void inject(GtasksTaskMovingTest gtasksTaskMovingTest);
void inject(GtasksListServiceTest gtasksListServiceTest);
void inject(GtasksTaskListUpdaterTest gtasksTaskListUpdaterTest);
void inject(ReminderServiceTest reminderServiceTest);
void inject(TaskTest taskTest);
@ -56,4 +48,6 @@ public interface TestComponent extends ApplicationComponent {
void inject(GtasksMetadataServiceTest gtasksMetadataServiceTest);
void inject(DeletionDaoTests deletionDaoTests);
void inject(GoogleTaskDaoTests googleTaskDaoTests);
}

@ -0,0 +1,32 @@
package org.tasks.makers;
import static com.natpryce.makeiteasy.Property.newProperty;
import static org.tasks.makers.Maker.make;
import com.natpryce.makeiteasy.Instantiator;
import com.natpryce.makeiteasy.Property;
import com.natpryce.makeiteasy.PropertyValue;
import com.todoroo.astrid.helper.UUIDHelper;
import org.tasks.data.GoogleTask;
public class GoogleTaskMaker {
public static final Property<GoogleTask, String> LIST = newProperty();
public static final Property<GoogleTask, Integer> ORDER = newProperty();
public static final Property<GoogleTask, String> REMOTE_ID = newProperty();
public static final Property<GoogleTask, Integer> TASK = newProperty();
private static final Instantiator<GoogleTask> instantiator = lookup -> {
GoogleTask task = new GoogleTask();
task.setListId(lookup.valueOf(LIST, "1"));
task.setOrder(lookup.valueOf(ORDER, 0));
task.setRemoteId(lookup.valueOf(REMOTE_ID, UUIDHelper.newUUID()));
task.setTask(lookup.valueOf(TASK, 1));
return task;
};
@SafeVarargs
public static GoogleTask newGoogleTask(PropertyValue<? super GoogleTask, ?>... properties) {
return make(instantiator, properties);
}
}

@ -18,19 +18,19 @@ public class GtaskListMaker {
private static final Property<GoogleTaskList, Integer> ORDER = newProperty();
private static final Property<GoogleTaskList, Integer> COLOR = newProperty();
private static final Instantiator<GoogleTaskList> instantiator =
lookup ->
new GoogleTaskList() {
{
setId(lookup.valueOf(GtaskListMaker.ID, 0L));
setAccount(lookup.valueOf(ACCOUNT, "account"));
setRemoteId(lookup.valueOf(REMOTE_ID, "1"));
setTitle(lookup.valueOf(NAME, "Default"));
setRemoteOrder(lookup.valueOf(ORDER, 0));
setLastSync(lookup.valueOf(LAST_SYNC, 0L));
setColor(lookup.valueOf(COLOR, -1));
}
};
lookup -> {
GoogleTaskList list = new GoogleTaskList();
list.setId(lookup.valueOf(GtaskListMaker.ID, 0L));
list.setAccount(lookup.valueOf(ACCOUNT, "account"));
list.setRemoteId(lookup.valueOf(REMOTE_ID, "1"));
list.setTitle(lookup.valueOf(NAME, "Default"));
list.setRemoteOrder(lookup.valueOf(ORDER, 0));
list.setLastSync(lookup.valueOf(LAST_SYNC, 0L));
list.setColor(lookup.valueOf(COLOR, -1));
return list;
};
@SafeVarargs
public static GoogleTaskList newGtaskList(
PropertyValue<? super GoogleTaskList, ?>... properties) {
return make(instantiator, properties);

@ -220,7 +220,7 @@ public final class TaskEditFragment extends InjectingFragment
if (isNewTask) {
((MainActivity) getActivity()).getTaskListFragment().onTaskCreated(model.getUuid());
} else {
((MainActivity) getActivity()).getTaskListFragment().onTaskSaved();
((MainActivity) getActivity()).getTaskListFragment().loadTaskListContent();
}
callback.removeTaskEditFragment();
} else {

@ -29,6 +29,7 @@ import androidx.appcompat.widget.Toolbar;
import androidx.coordinatorlayout.widget.CoordinatorLayout;
import androidx.lifecycle.ViewModelProviders;
import androidx.recyclerview.widget.DefaultItemAnimator;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
@ -89,30 +90,27 @@ import org.tasks.ui.MenuColorizer;
import org.tasks.ui.TaskListViewModel;
import org.tasks.ui.Toaster;
/**
* Primary activity for the Bente application. Shows a list of upcoming tasks and a user's coaches.
*
* @author Tim Su <tim@todoroo.com>
*/
public final class TaskListFragment extends InjectingFragment
implements SwipeRefreshLayout.OnRefreshListener, Toolbar.OnMenuItemClickListener {
public static final String TAGS_METADATA_JOIN = "for_tags"; // $NON-NLS-1$
public static final String GTASK_METADATA_JOIN = "for_gtask"; // $NON-NLS-1$
public static final String GTASK_METADATA_JOIN = "googletask"; // $NON-NLS-1$
public static final String CALDAV_METADATA_JOIN = "for_caldav"; // $NON-NLS-1$
public static final String ACTION_RELOAD = "action_reload";
public static final String ACTION_DELETED = "action_deleted";
public static final int REQUEST_MOVE_TASKS = 10103;
private static final int VOICE_RECOGNITION_REQUEST_CODE = 1234;
private static final String EXTRA_FILTER = "extra_filter";
private static final String FRAG_TAG_SORT_DIALOG = "frag_tag_sort_dialog";
private static final int REQUEST_CALDAV_SETTINGS = 10101;
private static final int REQUEST_GTASK_SETTINGS = 10102;
public static final int REQUEST_MOVE_TASKS = 10103;
private static final int REQUEST_FILTER_SETTINGS = 10104;
private static final int REQUEST_TAG_SETTINGS = 10105;
private static final int SEARCH_DEBOUNCE_TIMEOUT = 300;
private final RefreshReceiver refreshReceiver = new RefreshReceiver();
@Inject protected Tracker tracker;
protected CompositeDisposable disposables;
@Inject SyncAdapters syncAdapters;
@Inject TaskDeleter taskDeleter;
@Inject @ForActivity Context context;
@ -147,18 +145,11 @@ public final class TaskListFragment extends InjectingFragment
private TaskListViewModel taskListViewModel;
private TaskAdapter taskAdapter = null;
private TaskListRecyclerAdapter recyclerAdapter;
private final RefreshReceiver refreshReceiver = new RefreshReceiver();
private Filter filter;
private PublishSubject<String> searchSubject = PublishSubject.create();
private Disposable searchDisposable;
protected CompositeDisposable disposables;
private MenuItem search;
/*
* ======================================================================
* ======================================================= initialization
* ======================================================================
*/
private TaskListFragmentCallbackHandler callbacks;
static TaskListFragment newTaskListFragment(Context context, Filter filter) {
@ -202,8 +193,6 @@ public final class TaskListFragment extends InjectingFragment
@Override
public void onAttach(Activity activity) {
taskListViewModel = ViewModelProviders.of(getActivity()).get(TaskListViewModel.class);
super.onAttach(activity);
callbacks = (TaskListFragmentCallbackHandler) activity;
@ -214,10 +203,18 @@ public final class TaskListFragment extends InjectingFragment
component.inject(this);
}
/** Called when loading up the activity */
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
public void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
outState.putAll(recyclerAdapter.getSaveState());
}
@Override
public View onCreateView(
LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View parent = inflater.inflate(R.layout.fragment_task_list, container, false);
ButterKnife.bind(this, parent);
filter = getFilter();
@ -225,23 +222,36 @@ public final class TaskListFragment extends InjectingFragment
// set up list adapters
taskAdapter = taskAdapterProvider.createTaskAdapter(filter);
taskListViewModel = ViewModelProviders.of(getActivity()).get(TaskListViewModel.class);
taskListViewModel.setFilter(filter, taskAdapter.isManuallySorted());
recyclerAdapter =
new TaskListRecyclerAdapter(taskAdapter, viewHolderFactory, this, actionModeProvider);
new TaskListRecyclerAdapter(
taskAdapter, viewHolderFactory, this, actionModeProvider, taskListViewModel.getValue());
taskAdapter.setHelper(recyclerAdapter);
}
((DefaultItemAnimator) recyclerView.getItemAnimator()).setSupportsChangeAnimations(false);
new ItemTouchHelper(recyclerAdapter.getItemTouchHelperCallback())
.attachToRecyclerView(recyclerView);
recyclerView.setLayoutManager(new LinearLayoutManager(context));
@Override
public void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
taskListViewModel.observe(
this,
list -> {
recyclerAdapter.submitList(list);
outState.putAll(recyclerAdapter.getSaveState());
}
if (list.isEmpty()) {
swipeRefreshLayout.setVisibility(View.GONE);
emptyRefreshLayout.setVisibility(View.VISIBLE);
} else {
swipeRefreshLayout.setVisibility(View.VISIBLE);
emptyRefreshLayout.setVisibility(View.GONE);
}
});
recyclerView.setAdapter(recyclerAdapter);
@Override
public View onCreateView(
LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View parent = inflater.inflate(R.layout.fragment_task_list, container, false);
ButterKnife.bind(this, parent);
setupRefresh(swipeRefreshLayout);
setupRefresh(emptyRefreshLayout);
@ -273,7 +283,7 @@ public final class TaskListFragment extends InjectingFragment
if (preferences.getBoolean(R.string.p_show_completed_tasks, false)) {
completed.setChecked(true);
}
if (taskAdapter.isManuallySorted() || filter instanceof SearchFilter) {
if (!taskAdapter.supportsHiddenTasks() || filter instanceof SearchFilter) {
completed.setChecked(true);
completed.setEnabled(false);
hidden.setChecked(true);
@ -376,9 +386,10 @@ public final class TaskListFragment extends InjectingFragment
startActivityForResult(recognition, TaskListFragment.VOICE_RECOGNITION_REQUEST_CODE);
return true;
case R.id.menu_sort:
boolean supportsManualSort = filter.supportsSubtasks()
|| BuiltInFilterExposer.isInbox(context, filter)
|| BuiltInFilterExposer.isTodayFilter(context, filter);
boolean supportsManualSort =
filter.supportsSubtasks()
|| BuiltInFilterExposer.isInbox(context, filter)
|| BuiltInFilterExposer.isTodayFilter(context, filter);
SortDialog.newSortDialog(supportsManualSort)
.show(getChildFragmentManager(), FRAG_TAG_SORT_DIALOG);
return true;
@ -413,7 +424,8 @@ public final class TaskListFragment extends InjectingFragment
return true;
case R.id.menu_gtasks_list_settings:
Intent gtasksSettings = new Intent(getActivity(), GoogleTaskListSettingsActivity.class);
gtasksSettings.putExtra(GoogleTaskListSettingsActivity.EXTRA_STORE_DATA, ((GtasksFilter) filter).getList());
gtasksSettings.putExtra(
GoogleTaskListSettingsActivity.EXTRA_STORE_DATA, ((GtasksFilter) filter).getList());
startActivityForResult(gtasksSettings, REQUEST_GTASK_SETTINGS);
return true;
case R.id.menu_tag_settings:
@ -450,35 +462,6 @@ public final class TaskListFragment extends InjectingFragment
layout.setColorSchemeColors(checkBoxes.getPriorityColors());
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
taskListViewModel.observe(
this,
filter,
list -> {
if (list.isEmpty()) {
swipeRefreshLayout.setVisibility(View.GONE);
emptyRefreshLayout.setVisibility(View.VISIBLE);
} else {
swipeRefreshLayout.setVisibility(View.VISIBLE);
emptyRefreshLayout.setVisibility(View.GONE);
}
// stash selected items
Bundle saveState = recyclerAdapter.getSaveState();
recyclerAdapter.submitList(list);
recyclerAdapter.restoreSaveState(saveState);
});
((DefaultItemAnimator) recyclerView.getItemAnimator()).setSupportsChangeAnimations(false);
recyclerAdapter.applyToRecyclerView(recyclerView);
recyclerView.setLayoutManager(new LinearLayoutManager(context));
}
@Override
public void onResume() {
super.onResume();
@ -519,23 +502,12 @@ public final class TaskListFragment extends InjectingFragment
return search.isActionViewExpanded() && search.collapseActionView();
}
/**
* Called by the RefreshReceiver when the task list receives a refresh broadcast. Subclasses
* should override this.
*/
private void refresh() {
// TODO: compare indents in diff callback, then animate this
loadTaskListContent();
setSyncOngoing(preferences.isSyncOngoing());
}
/*
* ======================================================================
* =================================================== managing list view
* ======================================================================
*/
public void loadTaskListContent() {
taskListViewModel.invalidate();
}
@ -556,16 +528,6 @@ public final class TaskListFragment extends InjectingFragment
loadTaskListContent();
}
/*
* ======================================================================
* ============================================================== actions
* ======================================================================
*/
void onTaskSaved() {
recyclerAdapter.onTaskSaved();
}
public void onTaskDelete(Task task) {
MainActivity activity = (MainActivity) getActivity();
if (activity != null) {
@ -631,24 +593,13 @@ public final class TaskListFragment extends InjectingFragment
callbacks.onTaskListItemClicked(task);
}
/**
* Container Activity must implement this interface and we ensure that it does during the
* onAttach() callback
*/
public interface TaskListFragmentCallbackHandler {
void onTaskListItemClicked(Task task);
void onNavigationIconClicked();
}
/**
* Receiver which receives refresh intents
*
* @author Tim Su <tim@todoroo.com>
*/
protected class RefreshReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
refresh();

@ -13,6 +13,7 @@ import java.util.List;
import java.util.Map;
import org.tasks.data.TaskContainer;
import org.tasks.data.TaskListMetadata;
import org.tasks.tasklist.ViewHolder;
import timber.log.Timber;
public final class AstridTaskAdapter extends TaskAdapter {
@ -37,6 +38,11 @@ public final class AstridTaskAdapter extends TaskAdapter {
return updater.getIndentForTask(task.getUuid());
}
@Override
public boolean canMove(ViewHolder source, ViewHolder target) {
return true;
}
@Override
public boolean canIndent(int position, TaskContainer task) {
String parentUuid = getItemUuid(position - 1);
@ -137,4 +143,9 @@ public final class AstridTaskAdapter extends TaskAdapter {
chainedCompletions.put(itemId, chained);
}
}
@Override
public boolean supportsHiddenTasks() {
return false;
}
}

@ -1,51 +1,63 @@
package com.todoroo.astrid.adapter;
import android.text.TextUtils;
import com.todoroo.andlib.data.Property;
import com.todoroo.andlib.utility.DateUtilities;
import static com.todoroo.andlib.utility.DateUtilities.now;
import com.todoroo.astrid.dao.TaskDao;
import com.todoroo.astrid.data.SyncFlags;
import com.todoroo.astrid.data.Task;
import com.todoroo.astrid.gtasks.GtasksTaskListUpdater;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import org.tasks.BuildConfig;
import org.tasks.data.GoogleTask;
import org.tasks.data.GoogleTaskDao;
import org.tasks.data.GoogleTaskList;
import org.tasks.data.TaskContainer;
import timber.log.Timber;
import org.tasks.tasklist.ViewHolder;
public final class GoogleTaskAdapter extends TaskAdapter {
private final GoogleTaskList list;
private final GtasksTaskListUpdater updater;
private final TaskDao taskDao;
private final GoogleTaskDao googleTaskDao;
private final Map<Long, ArrayList<Long>> chainedCompletions =
Collections.synchronizedMap(new HashMap<>());
public GoogleTaskAdapter(
GoogleTaskList list,
GtasksTaskListUpdater updater,
TaskDao taskDao,
GoogleTaskDao googleTaskDao) {
this.list = list;
this.updater = updater;
GoogleTaskAdapter(TaskDao taskDao, GoogleTaskDao googleTaskDao) {
this.taskDao = taskDao;
this.googleTaskDao = googleTaskDao;
}
@Override
public int getIndent(TaskContainer task) {
return task.getIndent();
return task.getParent() > 0 ? 1 : 0;
}
@Override
public boolean canMove(ViewHolder sourceVh, ViewHolder targetVh) {
TaskContainer source = sourceVh.task;
int to = targetVh.getAdapterPosition();
if (!source.hasChildren() || to <= 0 || to >= getCount() - 1) {
return true;
}
TaskContainer target = targetVh.task;
if (sourceVh.getAdapterPosition() < to) {
if (target.hasChildren()) {
return false;
}
if (target.hasParent()) {
return target.isLastSubtask();
}
return true;
} else {
if (target.hasChildren()) {
return true;
}
if (target.hasParent()) {
return target.getParent() == source.getId() && target.secondarySort == 0;
}
return true;
}
}
@Override
public boolean canIndent(int position, TaskContainer task) {
Task parent = getTask(position - 1);
return parent != null && getIndent(task) == 0;
return position > 0 && !task.hasChildren() && !task.hasParent();
}
@Override
@ -55,81 +67,82 @@ public final class GoogleTaskAdapter extends TaskAdapter {
@Override
public void moved(int from, int to) {
long targetTaskId = getTaskId(from);
if (targetTaskId <= 0) {
return; // This can happen with gestures on empty parts of the list (e.g. extra space below
// tasks)
}
try {
if (to >= getCount()) {
updater.moveTo(list, targetTaskId, -1);
TaskContainer task = getTask(from);
GoogleTask googleTask = task.googletask;
if (to == 0) {
googleTaskDao.move(googleTask, 0, 0);
} else if (to == getCount()) {
TaskContainer previous = getTask(to - 1);
if (googleTask.getParent() > 0 && googleTask.getParent() == previous.getParent()) {
googleTaskDao.move(googleTask, googleTask.getParent(), previous.getSecondarySort());
} else {
long destinationTaskId = getTaskId(to);
updater.moveTo(list, targetTaskId, destinationTaskId);
googleTaskDao.move(googleTask, 0, previous.getPrimarySort());
}
} else if (from < to) {
TaskContainer previous = getTask(to - 1);
TaskContainer next = getTask(to);
if (previous.hasParent()) {
if (next.hasParent()) {
googleTaskDao.move(googleTask, next.getParent(), next.getSecondarySort());
} else if (task.getParent() == previous.getParent() || next.hasParent()) {
googleTaskDao.move(googleTask, previous.getParent(), previous.getSecondarySort());
} else {
googleTaskDao.move(googleTask, 0, previous.getPrimarySort());
}
} else if (previous.hasChildren()) {
googleTaskDao.move(googleTask, previous.getId(), 0);
} else if (task.hasParent()) {
googleTaskDao.move(googleTask, 0, next.getPrimarySort());
} else {
googleTaskDao.move(googleTask, 0, previous.getPrimarySort());
}
} else {
TaskContainer previous = getTask(to - 1);
TaskContainer next = getTask(to);
if (previous.hasParent()) {
if (next.hasParent()) {
googleTaskDao.move(googleTask, next.getParent(), next.getSecondarySort());
} else if (task.getParent() == previous.getParent()) {
googleTaskDao.move(googleTask, previous.getParent(), previous.getSecondarySort());
} else {
googleTaskDao.move(googleTask, 0, previous.getPrimarySort() + 1);
}
} else if (previous.hasChildren()) {
googleTaskDao.move(googleTask, previous.getId(), 0);
} else {
googleTaskDao.move(googleTask, 0, previous.getPrimarySort() + 1);
}
} catch (Exception e) {
Timber.e(e);
}
}
@Override
public void indented(int which, int delta) {
long targetTaskId = getTaskId(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(list, targetTaskId, delta);
} catch (Exception e) {
Timber.e(e);
}
}
Task update = task.getTask();
update.setModificationDate(now());
update.putTransitory(SyncFlags.FORCE_SYNC, true);
taskDao.save(update);
@Override
public void onTaskDeleted(Task task) {
updater.onDeleteTask(list, task.getId());
if (BuildConfig.DEBUG) {
googleTaskDao.validateSorting(task.getGoogleTaskList());
}
}
@Override
public void onCompletedTask(TaskContainer item, boolean completedState) {
final long itemId = item.getId();
final long completionDate = completedState ? DateUtilities.now() : 0;
if (!completedState) {
ArrayList<Long> chained = chainedCompletions.get(itemId);
if (chained != null) {
for (Long taskId : chained) {
Task task = taskDao.fetch(taskId);
task.setCompletionDate(completionDate);
taskDao.save(task);
}
}
return;
public void indented(int which, int delta) {
TaskContainer task = getTask(which);
TaskContainer previous;
GoogleTask current = googleTaskDao.getByTaskId(task.getId());
if (delta == -1) {
googleTaskDao.unindent(googleTaskDao.getByTaskId(task.getParent()), current);
} else {
previous = getTask(which - 1);
googleTaskDao.indent(googleTaskDao.getByTaskId(previous.getId()), current);
}
final ArrayList<Long> chained = new ArrayList<>();
final int parentIndent = item.getIndent();
updater.applyToChildren(
list,
itemId,
node -> {
Task childTask = taskDao.fetch(node.taskId);
if (!TextUtils.isEmpty(childTask.getRecurrence())) {
GoogleTask googleTask = updater.getTaskMetadata(node.taskId);
googleTask.setIndent(parentIndent);
googleTaskDao.update(googleTask);
}
childTask.setCompletionDate(completionDate);
taskDao.save(childTask);
chained.add(node.taskId);
});
if (chained.size() > 0) {
chainedCompletions.put(itemId, chained);
Task update = task.getTask();
update.setModificationDate(now());
update.putTransitory(SyncFlags.FORCE_SYNC, true);
taskDao.save(update);
if (BuildConfig.DEBUG) {
googleTaskDao.validateSorting(task.getGoogleTaskList());
}
}
}

@ -15,6 +15,7 @@ import java.util.List;
import java.util.Set;
import org.tasks.data.TaskContainer;
import org.tasks.tasklist.TaskListRecyclerAdapter;
import org.tasks.tasklist.ViewHolder;
/**
* Adapter for displaying a user's tasks as a list
@ -55,6 +56,10 @@ public class TaskAdapter {
return 0;
}
public boolean canMove(ViewHolder source, ViewHolder target) {
return false;
}
public boolean canIndent(int position, TaskContainer task) {
return false;
}
@ -80,12 +85,8 @@ public class TaskAdapter {
public void indented(int position, int delta) {}
long getTaskId(int position) {
return getTask(position).getId();
}
public Task getTask(int position) {
return helper.getItem(position).getTask();
public TaskContainer getTask(int position) {
return helper.getItem(position);
}
String getItemUuid(int position) {
@ -97,4 +98,8 @@ public class TaskAdapter {
public void onTaskCreated(String uuid) {}
public void onTaskDeleted(Task task) {}
public boolean supportsHiddenTasks() {
return true;
}
}

@ -10,8 +10,6 @@ import com.todoroo.astrid.core.BuiltInFilterExposer;
import com.todoroo.astrid.dao.TaskDao;
import com.todoroo.astrid.data.Task;
import com.todoroo.astrid.gtasks.GtasksListService;
import com.todoroo.astrid.gtasks.GtasksTaskListUpdater;
import com.todoroo.astrid.gtasks.sync.GtasksSyncService;
import com.todoroo.astrid.subtasks.SubtasksFilterUpdater;
import com.todoroo.astrid.subtasks.SubtasksHelper;
import javax.inject.Inject;
@ -35,7 +33,6 @@ public class TaskAdapterProvider {
private final TaskListMetadataDao taskListMetadataDao;
private final TaskDao taskDao;
private final GtasksListService gtasksListService;
private final GtasksSyncService gtasksSyncService;
private final GoogleTaskDao googleTaskDao;
private final CaldavDao caldavDao;
private final SubtasksHelper subtasksHelper;
@ -48,7 +45,6 @@ public class TaskAdapterProvider {
TaskListMetadataDao taskListMetadataDao,
TaskDao taskDao,
GtasksListService gtasksListService,
GtasksSyncService gtasksSyncService,
GoogleTaskDao googleTaskDao,
CaldavDao caldavDao,
SubtasksHelper subtasksHelper) {
@ -58,7 +54,6 @@ public class TaskAdapterProvider {
this.taskListMetadataDao = taskListMetadataDao;
this.taskDao = taskDao;
this.gtasksListService = gtasksListService;
this.gtasksSyncService = gtasksSyncService;
this.googleTaskDao = googleTaskDao;
this.caldavDao = caldavDao;
this.subtasksHelper = subtasksHelper;
@ -112,9 +107,9 @@ public class TaskAdapterProvider {
}
private TaskAdapter createManualGoogleTaskAdapter(GtasksFilter filter) {
GtasksTaskListUpdater updater = new GtasksTaskListUpdater(gtasksSyncService, googleTaskDao);
updater.initialize(filter);
return new GoogleTaskAdapter(filter.getList(), updater, taskDao, googleTaskDao);
String query = GtasksFilter.toManualOrder(filter.getSqlQuery());
filter.setFilterQueryOverride(query);
return new GoogleTaskAdapter(taskDao, googleTaskDao);
}
private TaskAdapter createManualFilterTaskAdapter(Filter filter) {

@ -50,21 +50,23 @@ public class GtasksFilter extends Filter {
}
public static String toManualOrder(String query) {
query =
query.replace(
"WHERE",
"JOIN (SELECT google_tasks.*, COUNT(c.gt_id) AS children, 0 AS siblings, google_tasks.gt_order AS primary_sort, NULL AS secondary_sort FROM google_tasks LEFT JOIN google_tasks AS c ON c.gt_parent = google_tasks.gt_task WHERE google_tasks.gt_parent = 0 GROUP BY google_tasks.gt_task UNION SELECT c.*, 0 AS children, COUNT(s.gt_id) AS siblings, p.gt_order AS primary_sort, c.gt_order AS secondary_sort FROM google_tasks AS c LEFT JOIN google_tasks AS p ON c.gt_parent = p.gt_task LEFT JOIN tasks ON c.gt_parent = tasks._id LEFT JOIN google_tasks AS s ON s.gt_parent = p.gt_task WHERE c.gt_parent > 0 AND ((tasks.completed=0) AND (tasks.deleted=0) AND (tasks.hideUntil<(strftime('%s','now')*1000))) GROUP BY c.gt_task) as g2 ON g2.gt_id = google_tasks.gt_id WHERE");
query = query.replaceAll("ORDER BY .*", "");
query = query + " ORDER BY google_tasks.`order` ASC";
return query.replace(
TaskDao.TaskCriteria.activeAndVisible().toString(),
TaskDao.TaskCriteria.notDeleted().toString());
query = query + "ORDER BY primary_sort ASC, secondary_sort ASC";
return query;
}
private static QueryTemplate getQueryTemplate(GoogleTaskList list) {
return new QueryTemplate()
.join(Join.left(GoogleTask.TABLE, Task.ID.eq(Field.field("google_tasks.task"))))
.join(Join.left(GoogleTask.TABLE, Task.ID.eq(Field.field("google_tasks.gt_task"))))
.where(
Criterion.and(
TaskDao.TaskCriteria.activeAndVisible(),
Field.field("google_tasks.deleted").eq(0),
Field.field("google_tasks.list_id").eq(list.getRemoteId())));
Field.field("google_tasks.gt_deleted").eq(0),
Field.field("google_tasks.gt_list_id").eq(list.getRemoteId())));
}
private static Map<String, Object> getValuesForNewTasks(GoogleTaskList list) {

@ -17,7 +17,6 @@ import android.text.TextUtils;
import com.todoroo.andlib.utility.DialogUtilities;
import com.todoroo.astrid.dao.TaskDao;
import com.todoroo.astrid.data.Task;
import com.todoroo.astrid.helper.UUIDHelper;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
@ -283,7 +282,6 @@ public class TasksXmlImporter {
googleTask.setRemoteId(xml.readString("value"));
googleTask.setListId(xml.readString("value2"));
googleTask.setParent(xml.readLong("value3"));
googleTask.setIndent(xml.readInteger("value4"));
googleTask.setOrder(xml.readLong("value5"));
googleTask.setRemoteOrder(xml.readLong("value6"));
googleTask.setLastSync(xml.readLong("value7"));

@ -58,7 +58,7 @@ import org.tasks.notifications.NotificationDao;
CaldavAccount.class,
GoogleTaskAccount.class
},
version = 62)
version = 63)
public abstract class Database extends RoomDatabase {
public static final String NAME = "database";

@ -6,6 +6,7 @@
package com.todoroo.astrid.dao;
import static com.google.common.collect.Lists.newArrayList;
import static com.todoroo.andlib.utility.DateUtilities.now;
import android.database.Cursor;
@ -88,10 +89,10 @@ public abstract class TaskDao {
@Query(
"SELECT tasks.* FROM tasks "
+ "LEFT JOIN google_tasks ON tasks._id = google_tasks.task "
+ "WHERE list_id IN (SELECT remote_id FROM google_task_lists WHERE account = :account)"
+ "AND (tasks.modified > google_tasks.last_sync "
+ "OR google_tasks.remote_id = '')")
+ "LEFT JOIN google_tasks ON tasks._id = google_tasks.gt_task "
+ "WHERE gt_list_id IN (SELECT remote_id FROM google_task_lists WHERE account = :account)"
+ "AND (tasks.modified > google_tasks.gt_last_sync OR google_tasks.gt_remote_id = '') "
+ "ORDER BY CASE WHEN gt_parent = 0 THEN 0 ELSE 1 END, gt_order ASC")
public abstract List<Task> getGoogleTasksToPush(String account);
@Query(
@ -181,15 +182,21 @@ public abstract class TaskDao {
/** Mark the given task as completed and save it. */
public void setComplete(Task item, boolean completed) {
if (completed) {
item.setCompletionDate(now());
} else {
item.setCompletionDate(0L);
}
List<Task> tasks = newArrayList(item);
tasks.addAll(getChildren(item.getId()));
setComplete(tasks, completed ? now() : 0L);
}
save(item);
private void setComplete(Iterable<Task> tasks, long completionDate) {
for (Task task : tasks) {
task.setCompletionDate(completionDate);
save(task);
}
}
@Query("SELECT tasks.* FROM tasks JOIN google_tasks ON tasks._id = gt_task WHERE gt_parent = :taskId")
abstract List<Task> getChildren(long taskId);
public int count(Filter filter) {
Cursor cursor = getCursor(filter.sqlQuery);
try {

@ -1,404 +0,0 @@
/*
* Copyright (c) 2012 Todoroo Inc
*
* See the file "LICENSE" for the full license governing this code.
*/
package com.todoroo.astrid.gtasks;
import com.todoroo.astrid.api.Filter;
import com.todoroo.astrid.api.GtasksFilter;
import com.todoroo.astrid.data.Task;
import com.todoroo.astrid.gtasks.sync.GtasksSyncService;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;
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 javax.inject.Inject;
import org.tasks.data.GoogleTask;
import org.tasks.data.GoogleTaskDao;
import org.tasks.data.GoogleTaskList;
import org.tasks.injection.ApplicationScope;
import timber.log.Timber;
@ApplicationScope
public class GtasksTaskListUpdater {
/** map of task -> parent task */
final HashMap<Long, Long> parents = new HashMap<>();
/** map of task -> prior sibling */
final HashMap<Long, Long> siblings = new HashMap<>();
private final GtasksSyncService gtasksSyncService;
private final GoogleTaskDao googleTaskDao;
@Inject
public GtasksTaskListUpdater(GtasksSyncService gtasksSyncService, GoogleTaskDao googleTaskDao) {
this.gtasksSyncService = gtasksSyncService;
this.googleTaskDao = googleTaskDao;
}
public void initialize(Filter filter) {
String query = GtasksFilter.toManualOrder(filter.getSqlQuery());
filter.setFilterQueryOverride(query);
}
// --- overrides
public GoogleTask getTaskMetadata(long taskId) {
return googleTaskDao.getByTaskId(taskId);
}
private void iterateThroughList(GoogleTaskList list, OrderedListIterator iterator) {
String listId = list.getRemoteId();
gtasksSyncService.iterateThroughList(listId, iterator, 0, false);
}
private void onMovedOrIndented(GoogleTaskList googleTaskList, GoogleTask googleTask) {
gtasksSyncService.triggerMoveForMetadata(googleTaskList, googleTask);
}
// --- used during synchronization
public void correctOrderAndIndentForList(String listId) {
orderAndIndentHelper(listId, new AtomicLong(0L), Task.NO_ID, 0, new HashSet<>());
}
private void orderAndIndentHelper(
final String listId,
final AtomicLong order,
final long parent,
final int indentLevel,
final Set<Long> alreadyChecked) {
for (GoogleTask curr : googleTaskDao.byRemoteOrder(listId, parent)) {
if (!alreadyChecked.contains(curr.getTask())) {
curr.setIndent(indentLevel);
curr.setOrder(order.getAndIncrement());
googleTaskDao.update(curr);
alreadyChecked.add(curr.getTask());
orderAndIndentHelper(listId, order, curr.getTask(), indentLevel + 1, alreadyChecked);
}
}
}
void updateParentSiblingMapsFor(GoogleTaskList list) {
final AtomicLong previousTask = new AtomicLong(Task.NO_ID);
final AtomicInteger previousIndent = new AtomicInteger(-1);
iterateThroughList(
list,
(taskId, metadata) -> {
int indent = metadata.getIndent();
try {
long parent, sibling;
if (indent > previousIndent.get()) {
parent = previousTask.get();
sibling = Task.NO_ID;
} else if (indent == previousIndent.get()) {
sibling = previousTask.get();
parent = parents.get(sibling);
} else {
// move up once for each indent
sibling = previousTask.get();
for (int i = indent; i < previousIndent.get(); i++) {
sibling = parents.get(sibling);
}
if (parents.containsKey(sibling)) {
parent = parents.get(sibling);
} else {
parent = Task.NO_ID;
}
}
parents.put(taskId, parent);
siblings.put(taskId, sibling);
} catch (Exception e) {
Timber.e(e);
}
previousTask.set(taskId);
previousIndent.set(indent);
});
}
/** Indent a task and all its children */
public void indent(final GoogleTaskList list, final long targetTaskId, final int delta) {
if (list == null) {
return;
}
updateParentSiblingMapsFor(list);
final AtomicInteger targetTaskIndent = new AtomicInteger(-1);
final AtomicInteger previousIndent = new AtomicInteger(-1);
final AtomicLong previousTask = new AtomicLong(Task.NO_ID);
final AtomicLong globalOrder = new AtomicLong(-1);
iterateThroughList(
list,
(taskId, googleTask) -> {
int indent = googleTask.getIndent();
long order = globalOrder.incrementAndGet();
googleTask.setOrder(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);
googleTask.setIndent(indent + delta);
long newParent = computeNewParent(list, taskId, indent + delta - 1);
if (newParent == taskId) {
googleTask.setParent(Task.NO_ID);
} else {
googleTask.setParent(newParent);
}
saveAndUpdateModifiedDate(googleTask);
}
} else if (targetTaskIndent.get() > -1) {
// found first task that is not beneath target
if (indent <= targetTaskIndent.get()) {
targetTaskIndent.set(-1);
} else {
googleTask.setIndent(indent + delta);
saveAndUpdateModifiedDate(googleTask);
}
} else {
previousIndent.set(indent);
previousTask.set(taskId);
}
saveAndUpdateModifiedDate(googleTask);
});
onMovedOrIndented(list, getTaskMetadata(targetTaskId));
}
/**
* Helper function to iterate through a list and compute a new parent for the target task based on
* the target parent's indent
*/
private long computeNewParent(GoogleTaskList list, long targetTaskId, int targetParentIndent) {
final AtomicInteger desiredParentIndent = new AtomicInteger(targetParentIndent);
final AtomicLong targetTask = new AtomicLong(targetTaskId);
final AtomicLong lastPotentialParent = new AtomicLong(Task.NO_ID);
final AtomicBoolean computedParent = new AtomicBoolean(false);
iterateThroughList(
list,
(taskId, googleTask) -> {
if (targetTask.get() == taskId) {
computedParent.set(true);
}
int indent = googleTask.getIndent();
if (!computedParent.get() && indent == desiredParentIndent.get()) {
lastPotentialParent.set(taskId);
}
});
if (lastPotentialParent.get() == Task.NO_ID) {
return Task.NO_ID;
}
return lastPotentialParent.get();
}
/**
* Move a task and all its children to the position right above taskIdToMoveto. Will change the
* indent level to match taskIdToMoveTo.
*/
public void moveTo(GoogleTaskList list, final long targetTaskId, final long moveBeforeTaskId) {
if (list == null) {
return;
}
Node root = buildTreeModel(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);
onMovedOrIndented(list, getTaskMetadata(targetTaskId));
}
// --- task moving
private boolean ancestorOf(Node ancestor, Node descendant) {
if (descendant.parent == ancestor) {
return true;
}
if (descendant.parent == null) {
return false;
}
return ancestorOf(ancestor, descendant.parent);
}
private void traverseTreeAndWriteValues(
GoogleTaskList list, Node node, AtomicLong order, int indent) {
if (node.taskId != Task.NO_ID) {
GoogleTask googleTask = getTaskMetadata(node.taskId);
if (googleTask == null) {
googleTask = new GoogleTask(node.taskId, list.getRemoteId());
}
googleTask.setOrder(order.getAndIncrement());
googleTask.setIndent(indent);
boolean parentChanged = false;
if (googleTask.getParent() != node.parent.taskId) {
parentChanged = true;
googleTask.setParent(node.parent.taskId);
}
saveAndUpdateModifiedDate(googleTask);
if (parentChanged) {
onMovedOrIndented(list, googleTask);
}
}
for (Node child : node.children) {
traverseTreeAndWriteValues(list, child, order, indent + 1);
}
}
private 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;
}
private Node buildTreeModel(GoogleTaskList list) {
final Node root = new Node(Task.NO_ID, null);
final AtomicInteger previoustIndent = new AtomicInteger(-1);
final AtomicReference<Node> currentNode = new AtomicReference<>(root);
iterateThroughList(
list,
(taskId, googleTask) -> {
int indent = googleTask.getIndent();
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;
break;
}
}
currentNode.set(new Node(taskId, node));
node.children.add(currentNode.get());
}
previoustIndent.set(indent);
});
return root;
}
private void saveAndUpdateModifiedDate(GoogleTask googleTask) {
googleTaskDao.update(googleTask);
}
/** Apply an operation only to the children of the task */
public void applyToChildren(GoogleTaskList list, long targetTaskId, OrderedListNodeVisitor visitor) {
Node root = buildTreeModel(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);
}
}
// --- task cascading operations
/** Removes a task from the order hierarchy and un-indent children */
public void onDeleteTask(GoogleTaskList list, final long targetTaskId) {
if (list == null) {
return;
}
Node root = buildTreeModel(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) {
node.parent = target.parent;
target.parent.children.add(targetIndex++, node);
}
}
traverseTreeAndWriteValues(list, root, new AtomicLong(0), -1);
}
public interface OrderedListIterator {
void processTask(long taskId, GoogleTask googleTask);
}
public interface OrderedListNodeVisitor {
void visitNode(Node node);
}
public static class Node {
public final long taskId;
final ArrayList<Node> children = new ArrayList<>();
Node parent;
Node(long taskId, Node parent) {
this.taskId = taskId;
this.parent = parent;
}
}
}

@ -4,6 +4,7 @@ import android.accounts.AccountManager;
import android.os.Bundle;
import androidx.annotation.Nullable;
import com.google.api.client.googleapis.auth.oauth2.GoogleCredential;
import com.google.api.client.googleapis.services.AbstractGoogleClientRequest;
import com.google.api.client.http.HttpResponseException;
import com.google.api.client.http.javanet.NetHttpTransport;
import com.google.api.client.json.GenericJson;
@ -15,6 +16,7 @@ import com.google.api.services.tasks.model.Task;
import com.google.api.services.tasks.model.TaskList;
import com.google.api.services.tasks.model.TaskLists;
import com.google.common.base.Strings;
import com.google.gson.GsonBuilder;
import java.io.IOException;
import org.tasks.BuildConfig;
import org.tasks.gtasks.GoogleAccountManager;
@ -54,33 +56,31 @@ public class GtasksInvoker {
}
public @Nullable com.google.api.services.tasks.model.Tasks getAllGtasksFromListId(
String listId, boolean includeHiddenAndDeleted, long lastSyncDate, @Nullable String pageToken)
throws IOException {
String listId, long lastSyncDate, @Nullable String pageToken) throws IOException {
return execute(
service
.tasks()
.list(listId)
.setMaxResults(100L)
.setShowDeleted(includeHiddenAndDeleted)
.setShowHidden(includeHiddenAndDeleted)
.setShowDeleted(true)
.setShowHidden(true)
.setPageToken(pageToken)
.setUpdatedMin(
GtasksApiUtilities.unixTimeToGtasksCompletionTime(lastSyncDate).toStringRfc3339()));
}
public @Nullable Task createGtask(String listId, Task task, String priorSiblingId)
public @Nullable Task createGtask(
String listId, Task task, @Nullable String parent, @Nullable String previous)
throws IOException {
Timber.d("createGtask: %s", prettyPrint(task));
return execute(service.tasks().insert(listId, task).setPrevious(priorSiblingId));
return execute(service.tasks().insert(listId, task).setParent(parent).setPrevious(previous));
}
public void updateGtask(String listId, Task task) throws IOException {
Timber.d("updateGtask: %s", prettyPrint(task));
execute(service.tasks().update(listId, task.getId(), task));
}
@Nullable
Task moveGtask(String listId, String taskId, String parentId, String previousId)
public Task moveGtask(String listId, String taskId, String parentId, String previousId)
throws IOException {
return execute(
service.tasks().move(listId, taskId).setParent(parentId).setPrevious(previousId));
@ -115,7 +115,7 @@ public class GtasksInvoker {
private synchronized @Nullable <T> T execute(TasksRequest<T> request, boolean retry)
throws IOException {
checkToken();
Timber.d("%s request: %s", getCaller(), request);
Timber.d("%s request: %s", getCaller(retry), prettyPrint(request));
T response;
try {
response = request.execute();
@ -128,7 +128,7 @@ public class GtasksInvoker {
throw e;
}
}
Timber.d("%s response: %s", getCaller(), prettyPrint(response));
Timber.d("%s response: %s", getCaller(retry), prettyPrint(response));
return response;
}
@ -136,15 +136,17 @@ public class GtasksInvoker {
if (BuildConfig.DEBUG) {
if (object instanceof GenericJson) {
return ((GenericJson) object).toPrettyString();
} else if (object instanceof AbstractGoogleClientRequest) {
return new GsonBuilder().setPrettyPrinting().create().toJson(object);
}
}
return object;
}
private String getCaller() {
private String getCaller(boolean retry) {
if (BuildConfig.DEBUG) {
try {
return Thread.currentThread().getStackTrace()[4].getMethodName();
return Thread.currentThread().getStackTrace()[retry ? 6 : 5].getMethodName();
} catch (Exception e) {
Timber.e(e);
}

@ -1,57 +0,0 @@
/*
* Copyright (c) 2012 Todoroo Inc
*
* See the file "LICENSE" for the full license governing this code.
*/
package com.todoroo.astrid.gtasks.api;
import com.google.api.services.tasks.model.Task;
import java.io.IOException;
import timber.log.Timber;
/**
* Encapsulates a request to the api to change the ordering on the given task
*
* @author Sam Bosley
*/
public class MoveRequest {
private final GtasksInvoker service;
private final String taskId;
private final String destinationList;
private String parentId;
private String priorSiblingId;
public MoveRequest(
GtasksInvoker service,
String taskId,
String destinationList,
String parentId,
String priorSiblingId) {
this.service = service;
this.taskId = taskId;
this.destinationList = destinationList;
this.parentId = parentId;
this.priorSiblingId = priorSiblingId;
}
public Task push() throws IOException {
try {
return executePush();
} catch (IOException e) {
Timber.e(e);
recover();
return executePush();
}
}
private Task executePush() throws IOException {
return service.moveGtask(destinationList, taskId, parentId, priorSiblingId);
}
private void recover() {
parentId = null;
priorSiblingId = null;
}
}

@ -1,205 +0,0 @@
/*
* Copyright (c) 2012 Todoroo Inc
*
* See the file "LICENSE" for the full license governing this code.
*/
package com.todoroo.astrid.gtasks.sync;
import android.text.TextUtils;
import com.google.api.client.googleapis.extensions.android.gms.auth.UserRecoverableAuthIOException;
import com.todoroo.andlib.utility.AndroidUtilities;
import com.todoroo.astrid.dao.TaskDao;
import com.todoroo.astrid.data.Task;
import com.todoroo.astrid.gtasks.GtasksTaskListUpdater;
import com.todoroo.astrid.gtasks.api.GtasksInvoker;
import com.todoroo.astrid.gtasks.api.MoveRequest;
import java.io.IOException;
import java.util.List;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
import javax.inject.Inject;
import org.tasks.analytics.Tracker;
import org.tasks.data.GoogleTask;
import org.tasks.data.GoogleTaskDao;
import org.tasks.data.GoogleTaskList;
import org.tasks.data.GoogleTaskListDao;
import org.tasks.gtasks.GoogleAccountManager;
import org.tasks.gtasks.GtaskSyncAdapterHelper;
import org.tasks.injection.ApplicationScope;
import org.tasks.preferences.Preferences;
import timber.log.Timber;
@ApplicationScope
public class GtasksSyncService {
private final TaskDao taskDao;
private final Preferences preferences;
private final LinkedBlockingQueue<SyncOnSaveOperation> operationQueue =
new LinkedBlockingQueue<>();
private final GtaskSyncAdapterHelper gtaskSyncAdapterHelper;
private final Tracker tracker;
private final GoogleTaskDao googleTaskDao;
private final GoogleAccountManager googleAccountManager;
@Inject
public GtasksSyncService(
TaskDao taskDao,
Preferences preferences,
GtaskSyncAdapterHelper gtaskSyncAdapterHelper,
Tracker tracker,
GoogleTaskDao googleTaskDao,
GoogleTaskListDao googleTaskListDao,
GoogleAccountManager googleAccountManager) {
this.taskDao = taskDao;
this.preferences = preferences;
this.gtaskSyncAdapterHelper = gtaskSyncAdapterHelper;
this.tracker = tracker;
this.googleTaskDao = googleTaskDao;
this.googleAccountManager = googleAccountManager;
new OperationPushThread(operationQueue).start();
}
public void triggerMoveForMetadata(GoogleTaskList googleTaskList, GoogleTask googleTask) {
if (googleTask == null) {
return;
}
if (googleTask.isSuppressSync()) {
googleTask.setSuppressSync(false);
return;
}
if (preferences.isSyncOngoing()) {
return;
}
if (!gtaskSyncAdapterHelper.isEnabled()) {
return;
}
operationQueue.offer(new MoveOp(googleTaskList, googleTask));
}
private void pushMetadataOnSave(GoogleTask model, GtasksInvoker invoker) throws IOException {
AndroidUtilities.sleepDeep(1000L);
String taskId = model.getRemoteId();
String listId = model.getListId();
String parent = getRemoteParentId(model);
String priorSibling = getRemoteSiblingId(listId, model);
MoveRequest move = new MoveRequest(invoker, taskId, listId, parent, priorSibling);
com.google.api.services.tasks.model.Task result = move.push();
// Update order googleTask from result
if (result != null) {
model.setRemoteOrder(Long.parseLong(result.getPosition()));
model.setSuppressSync(true);
googleTaskDao.update(model);
}
}
public void iterateThroughList(
String listId,
final GtasksTaskListUpdater.OrderedListIterator iterator,
long startAtOrder,
boolean reverse) {
List<GoogleTask> tasks =
reverse
? googleTaskDao.getTasksFromReverse(listId, startAtOrder)
: googleTaskDao.getTasksFrom(listId, startAtOrder);
for (GoogleTask entry : tasks) {
iterator.processTask(entry.getTask(), entry);
}
}
/** Gets the remote id string of the parent task */
public String getRemoteParentId(GoogleTask googleTask) {
String parent = null;
long parentId = googleTask.getParent();
GoogleTask parentTask = googleTaskDao.getByTaskId(parentId);
if (parentTask != null) {
parent = parentTask.getRemoteId();
if (TextUtils.isEmpty(parent)) {
parent = null;
}
}
return parent;
}
/** Gets the remote id string of the previous sibling task */
public String getRemoteSiblingId(String listId, GoogleTask gtasksMetadata) {
final AtomicInteger indentToMatch = new AtomicInteger(gtasksMetadata.getIndent());
final AtomicLong parentToMatch = new AtomicLong(gtasksMetadata.getParent());
final AtomicReference<String> sibling = new AtomicReference<>();
GtasksTaskListUpdater.OrderedListIterator iterator =
(taskId, googleTask) -> {
Task t = taskDao.fetch(taskId);
if (t == null || t.isDeleted()) {
return;
}
int currIndent = googleTask.getIndent();
long currParent = googleTask.getParent();
if (currIndent == indentToMatch.get() && currParent == parentToMatch.get()) {
if (sibling.get() == null) {
sibling.set(googleTask.getRemoteId());
}
}
};
iterateThroughList(listId, iterator, gtasksMetadata.getOrder(), true);
return sibling.get();
}
interface SyncOnSaveOperation {
void op() throws IOException;
}
private class MoveOp implements SyncOnSaveOperation {
final GoogleTask googleTask;
private final GoogleTaskList googleTaskList;
MoveOp(GoogleTaskList googleTaskList, GoogleTask googleTask) {
this.googleTaskList = googleTaskList;
this.googleTask = googleTask;
}
@Override
public void op() throws IOException {
GtasksInvoker invoker = new GtasksInvoker(googleTaskList.getAccount(), googleAccountManager);
pushMetadataOnSave(googleTask, invoker);
}
}
private class OperationPushThread extends Thread {
private final LinkedBlockingQueue<SyncOnSaveOperation> queue;
OperationPushThread(LinkedBlockingQueue<SyncOnSaveOperation> queue) {
this.queue = queue;
}
@Override
public void run() {
//noinspection InfiniteLoopStatement
while (true) {
SyncOnSaveOperation op;
try {
op = queue.take();
} catch (InterruptedException e) {
Timber.e(e);
continue;
}
try {
op.op();
} catch (UserRecoverableAuthIOException ignored) {
} catch (IOException e) {
tracker.reportException(e);
}
}
}
}
}

@ -99,14 +99,17 @@ public class TaskCreator {
createTags(task);
if (task.hasTransitory(GoogleTask.KEY)) {
googleTaskDao.insert(new GoogleTask(task.getId(), task.getTransitory(GoogleTask.KEY)));
googleTaskDao.insertAndShift(
new GoogleTask(task.getId(), task.getTransitory(GoogleTask.KEY)),
preferences.addGoogleTasksToTop());
} else if (task.hasTransitory(CaldavTask.KEY)) {
caldavDao.insert(new CaldavTask(task.getId(), task.getTransitory(CaldavTask.KEY), newUUID()));
} else {
Filter remoteList = defaultFilterProvider.getDefaultRemoteList();
if (remoteList instanceof GtasksFilter) {
googleTaskDao.insert(
new GoogleTask(task.getId(), ((GtasksFilter) remoteList).getRemoteId()));
googleTaskDao.insertAndShift(
new GoogleTask(task.getId(), ((GtasksFilter) remoteList).getRemoteId()),
preferences.addGoogleTasksToTop());
} else if (remoteList instanceof CaldavFilter) {
caldavDao.insert(
new CaldavTask(task.getId(), ((CaldavFilter) remoteList).getUuid(), newUUID()));

@ -1,5 +1,6 @@
package com.todoroo.astrid.service;
import static com.google.common.collect.Lists.partition;
import static com.todoroo.andlib.sql.Criterion.all;
import static com.todoroo.astrid.dao.TaskDao.TaskCriteria.isVisible;
import static com.todoroo.astrid.dao.TaskDao.TaskCriteria.notCompleted;
@ -9,13 +10,16 @@ import com.todoroo.astrid.api.Filter;
import com.todoroo.astrid.dao.TaskDao;
import com.todoroo.astrid.data.Task;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import javax.inject.Inject;
import org.tasks.LocalBroadcastManager;
import org.tasks.data.CaldavAccount;
import org.tasks.data.CaldavCalendar;
import org.tasks.data.DeletionDao;
import org.tasks.data.GoogleTaskAccount;
import org.tasks.data.GoogleTaskDao;
import org.tasks.data.GoogleTaskList;
import org.tasks.jobs.WorkManager;
@ -24,6 +28,7 @@ public class TaskDeleter {
private final WorkManager workManager;
private final TaskDao taskDao;
private final LocalBroadcastManager localBroadcastManager;
private final GoogleTaskDao googleTaskDao;
private final DeletionDao deletionDao;
@Inject
@ -31,11 +36,13 @@ public class TaskDeleter {
DeletionDao deletionDao,
WorkManager workManager,
TaskDao taskDao,
LocalBroadcastManager localBroadcastManager) {
LocalBroadcastManager localBroadcastManager,
GoogleTaskDao googleTaskDao) {
this.deletionDao = deletionDao;
this.workManager = workManager;
this.taskDao = taskDao;
this.localBroadcastManager = localBroadcastManager;
this.googleTaskDao = googleTaskDao;
}
public int purgeDeleted() {
@ -49,7 +56,11 @@ public class TaskDeleter {
}
public List<Task> markDeleted(List<Long> taskIds) {
deletionDao.markDeleted(taskIds);
Set<Long> ids = new HashSet<>(taskIds);
for (List<Long> partition : partition(taskIds, 999)) {
ids.addAll(googleTaskDao.getChildren(partition));
}
deletionDao.markDeleted(ids);
workManager.cleanup(taskIds);
workManager.sync(false);
localBroadcastManager.broadcastRefresh();

@ -16,6 +16,7 @@ import org.tasks.data.GoogleTask;
import org.tasks.data.GoogleTaskDao;
import org.tasks.data.Tag;
import org.tasks.data.TagDao;
import org.tasks.preferences.Preferences;
public class TaskDuplicator {
@ -23,6 +24,7 @@ public class TaskDuplicator {
private final TaskDao taskDao;
private final TagDao tagDao;
private final GoogleTaskDao googleTaskDao;
private final Preferences preferences;
private final LocalBroadcastManager localBroadcastManager;
@Inject
@ -31,12 +33,14 @@ public class TaskDuplicator {
TaskDao taskDao,
LocalBroadcastManager localBroadcastManager,
TagDao tagDao,
GoogleTaskDao googleTaskDao) {
GoogleTaskDao googleTaskDao,
Preferences preferences) {
this.gcalHelper = gcalHelper;
this.taskDao = taskDao;
this.localBroadcastManager = localBroadcastManager;
this.tagDao = tagDao;
this.googleTaskDao = googleTaskDao;
this.preferences = preferences;
}
public List<Task> duplicate(List<Long> taskIds) {
@ -69,7 +73,8 @@ public class TaskDuplicator {
tags, tag -> new Tag(clone.getId(), clone.getUuid(), tag.getName(), tag.getTagUid())));
if (googleTask != null) {
googleTaskDao.insert(new GoogleTask(clone.getId(), googleTask.getListId()));
googleTaskDao.insertAndShift(
new GoogleTask(clone.getId(), googleTask.getListId()), preferences.addGoogleTasksToTop());
}
gcalHelper.createTaskEventIfEnabled(clone);

@ -16,6 +16,7 @@ import org.tasks.data.CaldavTask;
import org.tasks.data.GoogleTask;
import org.tasks.data.GoogleTaskDao;
import org.tasks.data.GoogleTaskListDao;
import org.tasks.preferences.Preferences;
import org.tasks.sync.SyncAdapters;
public class TaskMover {
@ -24,6 +25,7 @@ public class TaskMover {
private final GoogleTaskDao googleTaskDao;
private final SyncAdapters syncAdapters;
private final GoogleTaskListDao googleTaskListDao;
private final Preferences preferences;
@Inject
public TaskMover(
@ -31,12 +33,14 @@ public class TaskMover {
CaldavDao caldavDao,
GoogleTaskDao googleTaskDao,
SyncAdapters syncAdapters,
GoogleTaskListDao googleTaskListDao) {
GoogleTaskListDao googleTaskListDao,
Preferences preferences) {
this.taskDao = taskDao;
this.caldavDao = caldavDao;
this.googleTaskDao = googleTaskDao;
this.syncAdapters = syncAdapters;
this.googleTaskListDao = googleTaskListDao;
this.preferences = preferences;
}
public void move(List<Long> tasks, Filter selectedList) {
@ -95,7 +99,9 @@ public class TaskMover {
}
if (selectedList instanceof GtasksFilter) {
googleTaskDao.insert(new GoogleTask(id, ((GtasksFilter) selectedList).getRemoteId()));
googleTaskDao.insertAndShift(
new GoogleTask(id, ((GtasksFilter) selectedList).getRemoteId()),
preferences.addGoogleTasksToTop());
} else if (selectedList instanceof CaldavFilter) {
caldavDao.insert(
new CaldavTask(id, ((CaldavFilter) selectedList).getUuid(), UUIDHelper.newUUID()));

@ -19,7 +19,7 @@ public abstract class DeletionDao {
@Query("DELETE FROM caldav_tasks WHERE task IN(:ids)")
abstract void deleteCaldavTasks(List<Long> ids);
@Query("DELETE FROM google_tasks WHERE task IN(:ids)")
@Query("DELETE FROM google_tasks WHERE gt_task IN(:ids)")
abstract void deleteGoogleTasks(List<Long> ids);
@Query("DELETE FROM tags WHERE task IN(:ids)")
@ -49,14 +49,14 @@ public abstract class DeletionDao {
@Query("UPDATE tasks SET modified = :timestamp, deleted = :timestamp WHERE _id IN(:ids)")
abstract void markDeleted(long timestamp, List<Long> ids);
public void markDeleted(List<Long> ids) {
public void markDeleted(Iterable<Long> ids) {
long now = now();
for (List<Long> partition : partition(ids, 997)) {
markDeleted(now, partition);
}
}
@Query("SELECT task FROM google_tasks WHERE deleted = 0 AND list_id = :listId")
@Query("SELECT gt_task FROM google_tasks WHERE gt_deleted = 0 AND gt_list_id = :listId")
abstract List<Long> getActiveGoogleTasks(String listId);
@Delete

@ -6,7 +6,6 @@ import androidx.room.Ignore;
import androidx.room.PrimaryKey;
import com.todoroo.andlib.data.Property;
import com.todoroo.andlib.data.Table;
import com.todoroo.andlib.utility.DateUtilities;
@Entity(tableName = "google_tasks")
public class GoogleTask {
@ -17,47 +16,47 @@ public class GoogleTask {
@Deprecated
public static final Property.IntegerProperty ORDER =
new Property.IntegerProperty(GoogleTask.TABLE, "`order`");
new Property.IntegerProperty(GoogleTask.TABLE, "gt_order");
@PrimaryKey(autoGenerate = true)
@ColumnInfo(name = "_id")
@ColumnInfo(name = "gt_id")
private transient long id;
@ColumnInfo(name = "task")
@ColumnInfo(name = "gt_task")
private transient long task;
@ColumnInfo(name = "remote_id")
@ColumnInfo(name = "gt_remote_id")
private String remoteId = "";
@ColumnInfo(name = "list_id")
@ColumnInfo(name = "gt_list_id")
private String listId = "";
@ColumnInfo(name = "parent")
@ColumnInfo(name = "gt_parent")
private long parent;
@ColumnInfo(name = "indent")
private int indent;
@ColumnInfo(name = "gt_remote_parent")
private String remoteParent;
@ColumnInfo(name = "order")
@ColumnInfo(name = "gt_moved")
private boolean moved;
@ColumnInfo(name = "gt_order")
private long order;
@ColumnInfo(name = "remote_order")
@ColumnInfo(name = "gt_remote_order")
private long remoteOrder;
@ColumnInfo(name = "last_sync")
@ColumnInfo(name = "gt_last_sync")
private long lastSync;
@ColumnInfo(name = "deleted")
@ColumnInfo(name = "gt_deleted")
private long deleted;
@Ignore private transient boolean suppressSync;
public GoogleTask() {}
@Ignore
public GoogleTask(long task, String listId) {
this.task = task;
this.order = DateUtilities.now();
this.listId = listId;
}
@ -101,14 +100,6 @@ public class GoogleTask {
this.parent = parent;
}
public int getIndent() {
return indent;
}
public void setIndent(int indent) {
this.indent = indent;
}
public long getOrder() {
return order;
}
@ -117,6 +108,18 @@ public class GoogleTask {
this.order = order;
}
public boolean isMoved() {
return moved;
}
public void setMoved(boolean moved) {
this.moved = moved;
}
public int getIndent() {
return parent > 0 ? 0 : 1;
}
public long getRemoteOrder() {
return remoteOrder;
}
@ -141,12 +144,12 @@ public class GoogleTask {
this.deleted = deleted;
}
public boolean isSuppressSync() {
return suppressSync;
public String getRemoteParent() {
return remoteParent;
}
public void setSuppressSync(boolean suppressSync) {
this.suppressSync = suppressSync;
public void setRemoteParent(String remoteParent) {
this.remoteParent = remoteParent;
}
@Override
@ -169,7 +172,7 @@ public class GoogleTask {
if (parent != that.parent) {
return false;
}
if (indent != that.indent) {
if (moved != that.moved) {
return false;
}
if (order != that.order) {
@ -184,13 +187,15 @@ public class GoogleTask {
if (deleted != that.deleted) {
return false;
}
if (suppressSync != that.suppressSync) {
if (remoteId != null ? !remoteId.equals(that.remoteId) : that.remoteId != null) {
return false;
}
if (remoteId != null ? !remoteId.equals(that.remoteId) : that.remoteId != null) {
if (listId != null ? !listId.equals(that.listId) : that.listId != null) {
return false;
}
return listId != null ? listId.equals(that.listId) : that.listId == null;
return remoteParent != null
? remoteParent.equals(that.remoteParent)
: that.remoteParent == null;
}
@Override
@ -200,12 +205,12 @@ public class GoogleTask {
result = 31 * result + (remoteId != null ? remoteId.hashCode() : 0);
result = 31 * result + (listId != null ? listId.hashCode() : 0);
result = 31 * result + (int) (parent ^ (parent >>> 32));
result = 31 * result + indent;
result = 31 * result + (moved ? 1 : 0);
result = 31 * result + (int) (order ^ (order >>> 32));
result = 31 * result + (remoteParent != null ? remoteParent.hashCode() : 0);
result = 31 * result + (int) (remoteOrder ^ (remoteOrder >>> 32));
result = 31 * result + (int) (lastSync ^ (lastSync >>> 32));
result = 31 * result + (int) (deleted ^ (deleted >>> 32));
result = 31 * result + (suppressSync ? 1 : 0);
return result;
}
@ -224,10 +229,13 @@ public class GoogleTask {
+ '\''
+ ", parent="
+ parent
+ ", indent="
+ indent
+ ", moved="
+ moved
+ ", order="
+ order
+ ", remoteParent='"
+ remoteParent
+ '\''
+ ", remoteOrder="
+ remoteOrder
+ ", lastSync="

@ -4,45 +4,188 @@ import androidx.room.Dao;
import androidx.room.Delete;
import androidx.room.Insert;
import androidx.room.Query;
import androidx.room.RoomWarnings;
import androidx.room.Transaction;
import androidx.room.Update;
import java.util.List;
import timber.log.Timber;
@Dao
public interface GoogleTaskDao {
public abstract class GoogleTaskDao {
@Insert
void insert(GoogleTask task);
public abstract void insert(GoogleTask task);
@Query("SELECT * FROM google_tasks WHERE task = :taskId AND deleted = 0 LIMIT 1")
GoogleTask getByTaskId(long taskId);
@Transaction
public void insertAndShift(GoogleTask task, boolean top) {
if (top) {
task.setOrder(0);
shiftDown(task.getListId(), task.getParent(), 0);
} else {
task.setOrder(getBottom(task.getListId(), task.getParent()));
}
insert(task);
}
@Update
void update(GoogleTask googleTask);
@Query(
"UPDATE google_tasks SET gt_order = gt_order + 1 WHERE gt_list_id = :listId AND gt_parent = :parent AND gt_order >= :position")
abstract void shiftDown(String listId, long parent, long position);
@Query(
"SELECT * FROM google_tasks WHERE list_id = :listId AND parent = :parent ORDER BY remote_order ASC")
List<GoogleTask> byRemoteOrder(String listId, long parent);
"UPDATE google_tasks SET gt_order = gt_order - 1 WHERE gt_list_id = :listId AND gt_parent = :parent AND gt_order > :from AND gt_order <= :to")
abstract void shiftUp(String listId, long parent, long from, long to);
@Query(
"SELECT * FROM google_tasks WHERE list_id = :listId AND `order` > :startAtOrder - 1 ORDER BY `order` ASC ")
List<GoogleTask> getTasksFrom(String listId, long startAtOrder);
"UPDATE google_tasks SET gt_order = gt_order + 1 WHERE gt_list_id = :listId AND gt_parent = :parent AND gt_order < :from AND gt_order >= :to")
abstract void shiftDown(String listId, long parent, long from, long to);
@Query(
"SELECT * FROM google_tasks WHERE list_id = :listId AND `order` < :startAtOrder ORDER BY `order` DESC")
List<GoogleTask> getTasksFromReverse(String listId, long startAtOrder);
"UPDATE google_tasks SET gt_order = gt_order - 1 WHERE gt_list_id = :listId AND gt_parent = :parent AND gt_order >= :position")
abstract void shiftUp(String listId, long parent, long position);
@Transaction
public void unindent(GoogleTask parent, GoogleTask task) {
String list = parent.getListId();
shiftUp(list, task.getParent(), task.getOrder());
long newPosition = parent.getOrder() + 1;
shiftDown(list, 0, newPosition);
task.setParent(0);
task.setOrder(newPosition);
task.setMoved(true);
update(task);
}
@Transaction
public void indent(GoogleTask previous, GoogleTask task) {
shiftUp(previous.getListId(), 0, task.getOrder());
if (previous.getParent() == 0) {
task.setParent(previous.getTask());
task.setOrder(0);
} else {
task.setParent(previous.getParent());
task.setOrder(previous.getOrder() + 1);
}
task.setMoved(true);
update(task);
}
@Transaction
public void move(GoogleTask task, long newParent, long newPosition) {
long previousParent = task.getParent();
long previousPosition = task.getOrder();
if (newParent == previousParent) {
if (previousPosition < newPosition) {
shiftUp(task.getListId(), newParent, previousPosition, newPosition);
} else {
shiftDown(task.getListId(), newParent, previousPosition, newPosition);
}
} else {
shiftUp(task.getListId(), previousParent, previousPosition);
shiftDown(task.getListId(), newParent, newPosition);
}
task.setMoved(true);
task.setParent(newParent);
task.setOrder(newPosition);
update(task);
}
@Query("SELECT * FROM google_tasks WHERE gt_task = :taskId AND gt_deleted = 0 LIMIT 1")
public abstract GoogleTask getByTaskId(long taskId);
@Update
public abstract void update(GoogleTask googleTask);
@Delete
void delete(GoogleTask deleted);
public abstract void delete(GoogleTask deleted);
@Query("SELECT * FROM google_tasks WHERE gt_remote_id = :remoteId LIMIT 1")
public abstract GoogleTask getByRemoteId(String remoteId);
@Query("SELECT * FROM google_tasks WHERE gt_task = :taskId AND gt_deleted > 0")
public abstract List<GoogleTask> getDeletedByTaskId(long taskId);
@Query("SELECT * FROM google_tasks WHERE gt_task = :taskId")
public abstract List<GoogleTask> getAllByTaskId(long taskId);
@Query("SELECT DISTINCT gt_list_id FROM google_tasks WHERE gt_deleted = 0 AND gt_task IN (:tasks)")
public abstract List<String> getLists(List<Long> tasks);
@Query("SELECT * FROM google_tasks WHERE remote_id = :remoteId LIMIT 1")
GoogleTask getByRemoteId(String remoteId);
@Query("SELECT gt_task FROM google_tasks WHERE gt_parent IN (:ids)")
public abstract List<Long> getChildren(List<Long> ids);
@Query(
"SELECT IFNULL(MAX(gt_order), -1) + 1 FROM google_tasks WHERE gt_list_id = :listId AND gt_parent = :parent")
public abstract long getBottom(String listId, long parent);
@Query(
"SELECT gt_remote_id FROM google_tasks JOIN tasks ON tasks._id = gt_task WHERE deleted = 0 AND gt_list_id = :listId AND gt_parent = :parent AND gt_order < :order AND gt_remote_id IS NOT NULL AND gt_remote_id != '' ORDER BY gt_order DESC")
public abstract String getPrevious(String listId, long parent, long order);
@Query("SELECT gt_remote_id FROM google_tasks WHERE gt_task = :task")
public abstract String getRemoteId(long task);
@Query("SELECT gt_task FROM google_tasks WHERE gt_remote_id = :remoteId")
public abstract long getTask(String remoteId);
@SuppressWarnings({RoomWarnings.CURSOR_MISMATCH, "AndroidUnresolvedRoomSqlReference"})
@Query(
"SELECT google_tasks.*, gt_order AS primary_sort, NULL AS secondary_sort FROM google_tasks JOIN tasks ON tasks._id = gt_task WHERE gt_parent = 0 AND gt_list_id = :listId AND tasks.deleted = 0 UNION SELECT c.*, p.gt_order AS primary_sort, c.gt_order AS secondary_sort FROM google_tasks AS c LEFT JOIN google_tasks AS p ON c.gt_parent = p.gt_task JOIN tasks ON tasks._id = c.gt_task WHERE c.gt_parent > 0 AND c.gt_list_id = :listId AND tasks.deleted = 0 ORDER BY primary_sort ASC, secondary_sort ASC")
abstract List<GoogleTask> getByLocalOrder(String listId);
@SuppressWarnings({RoomWarnings.CURSOR_MISMATCH, "AndroidUnresolvedRoomSqlReference"})
@Query(
"SELECT google_tasks.*, gt_remote_order AS primary_sort, NULL AS secondary_sort FROM google_tasks JOIN tasks ON tasks._id = gt_task WHERE gt_parent = 0 AND gt_list_id = :listId AND tasks.deleted = 0 UNION SELECT c.*, p.gt_remote_order AS primary_sort, c.gt_remote_order AS secondary_sort FROM google_tasks AS c LEFT JOIN google_tasks AS p ON c.gt_parent = p.gt_task JOIN tasks ON tasks._id = c.gt_task WHERE c.gt_parent > 0 AND c.gt_list_id = :listId AND tasks.deleted = 0 ORDER BY primary_sort ASC, secondary_sort ASC")
abstract List<GoogleTask> getByRemoteOrder(String listId);
@Query(
"UPDATE google_tasks SET gt_parent = IFNULL((SELECT gt_task FROM google_tasks AS p WHERE p.gt_remote_id = google_tasks.gt_remote_parent), gt_parent) WHERE gt_list_id = :listId AND gt_moved = 0 AND gt_remote_parent IS NOT NULL AND gt_remote_parent != ''")
abstract void updateParents(String listId);
@Query("SELECT * FROM google_tasks WHERE task = :taskId AND deleted > 0")
List<GoogleTask> getDeletedByTaskId(long taskId);
@Transaction
public void reposition(String listId) {
updateParents(listId);
@Query("SELECT * FROM google_tasks WHERE task = :taskId")
List<GoogleTask> getAllByTaskId(long taskId);
List<GoogleTask> orderedTasks = getByRemoteOrder(listId);
int subtasks = 0;
int parent = 0;
for (int i = 0; i < orderedTasks.size(); i++) {
GoogleTask task = orderedTasks.get(i);
if (task.getParent() > 0) {
if (task.getOrder() != subtasks && !task.isMoved()) {
task.setOrder(subtasks);
update(task);
}
subtasks++;
} else {
subtasks = 0;
if (task.getOrder() != parent && !task.isMoved()) {
task.setOrder(parent);
update(task);
}
parent++;
}
}
}
@Query("SELECT DISTINCT list_id FROM google_tasks WHERE deleted = 0 AND task IN (:tasks)")
List<String> getLists(List<Long> tasks);
public void validateSorting(String listId) {
List<GoogleTask> orderedTasks = getByLocalOrder(listId);
int subtasks = 0;
int parent = 0;
for (int i = 0; i < orderedTasks.size(); i++) {
GoogleTask task = orderedTasks.get(i);
if (task.getParent() > 0) {
if (task.getOrder() != subtasks) {
Timber.e("Subtask violation expected %s but was %s", subtasks, task.getOrder());
}
subtasks++;
} else {
subtasks = 0;
if (task.getOrder() != parent) {
Timber.e("Parent violation expected %s but was %s", parent, task.getOrder());
}
parent++;
}
}
}
}

@ -5,26 +5,21 @@ import com.todoroo.astrid.data.Task;
public class TaskContainer {
@Embedded public Task task;
@Embedded public GoogleTask googletask;
public String tags;
public String googletask;
public String caldav;
public int order;
public int indent;
public int getIndent() {
return indent;
}
public void setIndent(int indent) {
this.indent = indent;
}
public int children;
public int siblings;
public long primarySort;
public long secondarySort;
@Deprecated public int indent;
public String getTagsString() {
return tags;
}
public String getGoogleTaskList() {
return googletask;
return googletask == null ? null : googletask.getListId();
}
public String getCaldav() {
@ -79,6 +74,22 @@ public class TaskContainer {
return task.getId();
}
public long getPrimarySort() {
return primarySort;
}
public long getSecondarySort() {
return secondarySort;
}
public int getIndent() {
return indent;
}
public void setIndent(int indent) {
this.indent = indent;
}
@Override
public boolean equals(Object o) {
if (this == o) {
@ -90,28 +101,40 @@ public class TaskContainer {
TaskContainer that = (TaskContainer) o;
if (indent != that.indent) {
if (children != that.children) {
return false;
}
if (task != null ? !task.equals(that.task) : that.task != null) {
if (siblings != that.siblings) {
return false;
}
if (tags != null ? !tags.equals(that.tags) : that.tags != null) {
if (primarySort != that.primarySort) {
return false;
}
if (secondarySort != that.secondarySort) {
return false;
}
if (task != null ? !task.equals(that.task) : that.task != null) {
return false;
}
if (googletask != null ? !googletask.equals(that.googletask) : that.googletask != null) {
return false;
}
if (tags != null ? !tags.equals(that.tags) : that.tags != null) {
return false;
}
return caldav != null ? caldav.equals(that.caldav) : that.caldav == null;
}
@Override
public int hashCode() {
int result = task != null ? task.hashCode() : 0;
result = 31 * result + (tags != null ? tags.hashCode() : 0);
result = 31 * result + (googletask != null ? googletask.hashCode() : 0);
result = 31 * result + (tags != null ? tags.hashCode() : 0);
result = 31 * result + (caldav != null ? caldav.hashCode() : 0);
result = 31 * result + indent;
result = 31 * result + children;
result = 31 * result + siblings;
result = 31 * result + (int) (primarySort ^ (primarySort >>> 32));
result = 31 * result + (int) (secondarySort ^ (secondarySort >>> 32));
return result;
}
@ -120,15 +143,22 @@ public class TaskContainer {
return "TaskContainer{"
+ "task="
+ task
+ ", googletask="
+ googletask
+ ", tags='"
+ tags
+ '\''
+ ", googletask='"
+ googletask
+ '\''
+ ", caldav='"
+ caldav
+ '\''
+ ", children="
+ children
+ ", siblings="
+ siblings
+ ", primarySort="
+ primarySort
+ ", secondarySort="
+ secondarySort
+ ", indent="
+ indent
+ '}';
@ -137,4 +167,24 @@ public class TaskContainer {
public String getUuid() {
return task.getUuid();
}
public long getParent() {
return googletask == null ? 0 : googletask.getParent();
}
public void setParent(long parent) {
googletask.setParent(parent);
}
public boolean hasParent() {
return getParent() > 0;
}
public boolean hasChildren() {
return children > 0;
}
public boolean isLastSubtask() {
return secondarySort == siblings - 1;
}
}

@ -242,6 +242,21 @@ public class Migrations {
}
};
private static final Migration MIGRATION_62_63 =
new Migration(62, 63) {
@Override
public void migrate(@NonNull SupportSQLiteDatabase database) {
database.execSQL("ALTER TABLE `google_tasks` RENAME TO `gt-temp`");
database.execSQL(
"CREATE TABLE IF NOT EXISTS `google_tasks` (`gt_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `gt_task` INTEGER NOT NULL, `gt_remote_id` TEXT, `gt_list_id` TEXT, `gt_parent` INTEGER NOT NULL, `gt_remote_parent` TEXT, `gt_moved` INTEGER NOT NULL, `gt_order` INTEGER NOT NULL, `gt_remote_order` INTEGER NOT NULL, `gt_last_sync` INTEGER NOT NULL, `gt_deleted` INTEGER NOT NULL)");
database.execSQL(
"INSERT INTO `google_tasks` (`gt_id`, `gt_task`, `gt_remote_id`, `gt_list_id`, `gt_parent`, `gt_remote_parent`, `gt_moved`, `gt_order`, `gt_remote_order`, `gt_last_sync`, `gt_deleted`) "
+ "SELECT `_id`, `task`, `remote_id`, `list_id`, `parent`, '', 0, `order`, `remote_order`, `last_sync`, `deleted` FROM `gt-temp`");
database.execSQL("DROP TABLE `gt-temp`");
database.execSQL("UPDATE `google_task_lists` SET `last_sync` = 0");
}
};
public static final Migration[] MIGRATIONS =
new Migration[] {
MIGRATION_35_36,
@ -261,7 +276,8 @@ public class Migrations {
MIGRATION_58_59,
MIGRATION_59_60,
MIGRATION_60_61,
MIGRATION_61_62
MIGRATION_61_62,
MIGRATION_62_63
};
private static Migration NOOP(int from, int to) {

@ -8,6 +8,7 @@ import android.content.Intent;
import android.text.TextUtils;
import androidx.core.app.NotificationCompat;
import com.google.api.client.googleapis.extensions.android.gms.auth.UserRecoverableAuthIOException;
import com.google.api.client.googleapis.json.GoogleJsonResponseException;
import com.google.api.services.tasks.model.TaskList;
import com.google.api.services.tasks.model.TaskLists;
import com.google.api.services.tasks.model.Tasks;
@ -19,17 +20,17 @@ import com.todoroo.astrid.dao.TaskDao;
import com.todoroo.astrid.data.SyncFlags;
import com.todoroo.astrid.data.Task;
import com.todoroo.astrid.gtasks.GtasksListService;
import com.todoroo.astrid.gtasks.GtasksTaskListUpdater;
import com.todoroo.astrid.gtasks.api.GtasksApiUtilities;
import com.todoroo.astrid.gtasks.api.GtasksInvoker;
import com.todoroo.astrid.gtasks.api.HttpNotFoundException;
import com.todoroo.astrid.gtasks.sync.GtasksSyncService;
import com.todoroo.astrid.gtasks.sync.GtasksTaskContainer;
import com.todoroo.astrid.service.TaskCreator;
import com.todoroo.astrid.service.TaskDeleter;
import com.todoroo.astrid.utility.Constants;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import javax.inject.Inject;
import org.tasks.LocalBroadcastManager;
@ -53,11 +54,18 @@ public class GoogleTaskSynchronizer {
private static final String DEFAULT_LIST = "@default"; // $NON-NLS-1$
private static final Comparator<com.google.api.services.tasks.model.Task> PARENTS_FIRST =
(o1, o2) -> {
if (Strings.isNullOrEmpty(o1.getParent())) {
return Strings.isNullOrEmpty(o2.getParent()) ? 0 : -1;
} else {
return Strings.isNullOrEmpty(o2.getParent()) ? 1 : 0;
}
};
private final Context context;
private final GoogleTaskListDao googleTaskListDao;
private final GtasksSyncService gtasksSyncService;
private final GtasksListService gtasksListService;
private final GtasksTaskListUpdater gtasksTaskListUpdater;
private final Preferences preferences;
private final TaskDao taskDao;
private final Tracker tracker;
@ -75,9 +83,7 @@ public class GoogleTaskSynchronizer {
public GoogleTaskSynchronizer(
@ForApplication Context context,
GoogleTaskListDao googleTaskListDao,
GtasksSyncService gtasksSyncService,
GtasksListService gtasksListService,
GtasksTaskListUpdater gtasksTaskListUpdater,
Preferences preferences,
TaskDao taskDao,
Tracker tracker,
@ -92,9 +98,7 @@ public class GoogleTaskSynchronizer {
TaskDeleter taskDeleter) {
this.context = context;
this.googleTaskListDao = googleTaskListDao;
this.gtasksSyncService = gtasksSyncService;
this.gtasksListService = gtasksListService;
this.gtasksTaskListUpdater = gtasksTaskListUpdater;
this.preferences = preferences;
this.taskDao = taskDao;
this.tracker = tracker;
@ -139,9 +143,6 @@ public class GoogleTaskSynchronizer {
} catch (UserRecoverableAuthIOException e) {
Timber.e(e);
sendNotification(context, e.getIntent());
} catch (IOException e) {
account.setError(e.getMessage());
Timber.e(e);
} catch (Exception e) {
account.setError(e.getMessage());
tracker.reportException(e);
@ -208,23 +209,18 @@ public class GoogleTaskSynchronizer {
preferences.setString(R.string.p_default_remote_list, null);
}
}
for (final GoogleTaskList list : gtasksListService.getListsToUpdate(gtaskLists)) {
for (GoogleTaskList list : gtasksListService.getListsToUpdate(gtaskLists)) {
fetchAndApplyRemoteChanges(gtasksInvoker, list);
googleTaskDao.reposition(list.getRemoteId());
}
account.setEtag(eTag);
}
private void pushLocalChanges(GoogleTaskAccount account, GtasksInvoker gtasksInvoker)
throws UserRecoverableAuthIOException {
throws IOException {
List<Task> tasks = taskDao.getGoogleTasksToPush(account.getAccount());
for (Task task : tasks) {
try {
pushTask(task, gtasksInvoker);
} catch (UserRecoverableAuthIOException e) {
throw e;
} catch (IOException e) {
Timber.e(e);
}
pushTask(task, gtasksInvoker);
}
}
@ -290,30 +286,65 @@ public class GoogleTaskSynchronizer {
remoteModel.setStatus("needsAction"); // $NON-NLS-1$
}
if (!newlyCreated) {
try {
gtasksInvoker.updateGtask(listId, remoteModel);
} catch (HttpNotFoundException e) {
Timber.e(e);
googleTaskDao.delete(gtasksMetadata);
return;
}
} else {
String priorSibling = gtasksSyncService.getRemoteSiblingId(listId, gtasksMetadata);
if (newlyCreated) {
String localParent =
gtasksMetadata.getParent() > 0
? googleTaskDao.getRemoteId(gtasksMetadata.getParent())
: null;
String previous =
googleTaskDao.getPrevious(listId, gtasksMetadata.getParent(), gtasksMetadata.getOrder());
com.google.api.services.tasks.model.Task created =
gtasksInvoker.createGtask(listId, remoteModel, priorSibling);
gtasksInvoker.createGtask(listId, remoteModel, localParent, previous);
if (created != null) {
// Update the metadata for the newly created task
gtasksMetadata.setRemoteId(created.getId());
gtasksMetadata.setListId(listId);
gtasksMetadata.setRemoteOrder(Long.parseLong(created.getPosition()));
gtasksMetadata.setRemoteParent(created.getParent());
} else {
return;
}
} else {
try {
if (!task.isDeleted() && gtasksMetadata.isMoved()) {
try {
String localParent =
gtasksMetadata.getParent() > 0
? googleTaskDao.getRemoteId(gtasksMetadata.getParent())
: null;
String previous =
googleTaskDao.getPrevious(
listId, gtasksMetadata.getParent(), gtasksMetadata.getOrder());
com.google.api.services.tasks.model.Task result =
gtasksInvoker.moveGtask(listId, remoteModel.getId(), localParent, previous);
gtasksMetadata.setRemoteOrder(Long.parseLong(result.getPosition()));
gtasksMetadata.setRemoteParent(result.getParent());
gtasksMetadata.setParent(
Strings.isNullOrEmpty(result.getParent())
? 0
: googleTaskDao.getTask(result.getParent()));
} catch (GoogleJsonResponseException e) {
if (e.getStatusCode() == 400) {
Timber.e(e);
} else {
throw e;
}
}
}
gtasksInvoker.updateGtask(listId, remoteModel);
} catch (HttpNotFoundException e) {
Timber.e(e);
googleTaskDao.delete(gtasksMetadata);
return;
}
}
task.setModificationDate(DateUtilities.now());
gtasksMetadata.setMoved(false);
gtasksMetadata.setLastSync(DateUtilities.now() + 1000L);
if (gtasksMetadata.getId() == Task.NO_ID) {
googleTaskDao.insert(gtasksMetadata);
@ -325,75 +356,61 @@ public class GoogleTaskSynchronizer {
}
private synchronized void fetchAndApplyRemoteChanges(
GtasksInvoker gtasksInvoker, GoogleTaskList list) throws UserRecoverableAuthIOException {
GtasksInvoker gtasksInvoker, GoogleTaskList list) throws IOException {
String listId = list.getRemoteId();
long lastSyncDate = list.getLastSync();
List<com.google.api.services.tasks.model.Task> tasks = new ArrayList<>();
String nextPageToken = null;
do {
Tasks taskList =
gtasksInvoker.getAllGtasksFromListId(listId, lastSyncDate + 1000L, nextPageToken);
if (taskList == null) {
break;
}
List<com.google.api.services.tasks.model.Task> items = taskList.getItems();
if (items != null) {
tasks.addAll(items);
}
nextPageToken = taskList.getNextPageToken();
} while (nextPageToken != null);
boolean includeDeletedAndHidden = lastSyncDate != 0;
try {
List<com.google.api.services.tasks.model.Task> tasks = new ArrayList<>();
String nextPageToken = null;
do {
Tasks taskList =
gtasksInvoker.getAllGtasksFromListId(
listId, includeDeletedAndHidden, lastSyncDate + 1000L, nextPageToken);
if (taskList == null) {
break;
}
List<com.google.api.services.tasks.model.Task> items = taskList.getItems();
if (items != null) {
tasks.addAll(items);
}
nextPageToken = taskList.getNextPageToken();
} while (nextPageToken != null);
for (com.google.api.services.tasks.model.Task gtask : tasks) {
String remoteId = gtask.getId();
GoogleTask googleTask = getMetadataByGtaskId(remoteId);
Task task = null;
if (googleTask == null) {
googleTask = new GoogleTask(0, "");
} else if (googleTask.getTask() > 0) {
task = taskDao.fetch(googleTask.getTask());
}
com.google.api.client.util.DateTime updated = gtask.getUpdated();
if (updated != null) {
lastSyncDate = Math.max(lastSyncDate, updated.getValue());
}
Boolean isDeleted = gtask.getDeleted();
Boolean isHidden = gtask.getHidden();
if ((isDeleted != null && isDeleted) || (isHidden != null && isHidden)) {
if (task != null) {
taskDeleter.delete(task);
}
continue;
}
if (task == null) {
task = taskCreator.createWithValues("");
Collections.sort(tasks, PARENTS_FIRST);
for (com.google.api.services.tasks.model.Task gtask : tasks) {
String remoteId = gtask.getId();
GoogleTask googleTask = googleTaskDao.getByRemoteId(remoteId);
Task task = null;
if (googleTask == null) {
googleTask = new GoogleTask(0, "");
} else if (googleTask.getTask() > 0) {
task = taskDao.fetch(googleTask.getTask());
}
com.google.api.client.util.DateTime updated = gtask.getUpdated();
if (updated != null) {
lastSyncDate = Math.max(lastSyncDate, updated.getValue());
}
Boolean isDeleted = gtask.getDeleted();
Boolean isHidden = gtask.getHidden();
if ((isDeleted != null && isDeleted) || (isHidden != null && isHidden)) {
if (task != null) {
taskDeleter.delete(task);
}
GtasksTaskContainer container = new GtasksTaskContainer(gtask, task, listId, googleTask);
container.gtaskMetadata.setRemoteOrder(Long.parseLong(gtask.getPosition()));
container.gtaskMetadata.setParent(localIdForGtasksId(gtask.getParent()));
container.gtaskMetadata.setLastSync(DateUtilities.now() + 1000L);
write(container);
continue;
}
if (task == null) {
task = taskCreator.createWithValues("");
}
list.setLastSync(lastSyncDate);
googleTaskListDao.insertOrReplace(list);
gtasksTaskListUpdater.correctOrderAndIndentForList(listId);
} catch (UserRecoverableAuthIOException e) {
throw e;
} catch (IOException e) {
Timber.e(e);
GtasksTaskContainer container = new GtasksTaskContainer(gtask, task, listId, googleTask);
container.gtaskMetadata.setRemoteOrder(Long.parseLong(gtask.getPosition()));
container.gtaskMetadata.setRemoteParent(gtask.getParent());
container.gtaskMetadata.setParent(
Strings.isNullOrEmpty(gtask.getParent()) ? 0 : googleTaskDao.getTask(gtask.getParent()));
container.gtaskMetadata.setLastSync(DateUtilities.now() + 1000L);
write(container);
}
}
private long localIdForGtasksId(String gtasksId) {
GoogleTask metadata = getMetadataByGtaskId(gtasksId);
return metadata == null ? Task.NO_ID : metadata.getTask();
}
private GoogleTask getMetadataByGtaskId(String gtaskId) {
return googleTaskDao.getByRemoteId(gtaskId);
list.setLastSync(lastSyncDate);
googleTaskListDao.insertOrReplace(list);
}
private void write(GtasksTaskContainer task) {

@ -63,6 +63,10 @@ public class Preferences {
return context.getPackageName() + "_preferences";
}
public boolean addGoogleTasksToTop() {
return getBoolean(R.string.p_google_tasks_add_to_top, true);
}
public boolean backButtonSavesTask() {
return getBoolean(R.string.p_back_button_saves_task, false);
}

@ -1,26 +1,41 @@
package org.tasks.tasklist;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.DiffUtil.ItemCallback;
import androidx.recyclerview.widget.DiffUtil;
import com.todoroo.astrid.adapter.TaskAdapter;
import java.util.List;
import org.tasks.data.TaskContainer;
class DiffCallback extends ItemCallback<TaskContainer> {
class DiffCallback extends DiffUtil.Callback {
private final TaskAdapter adapter;
private final List<TaskContainer> oldList;
private final List<TaskContainer> newList;
@Deprecated private final TaskAdapter adapter;
public DiffCallback(TaskAdapter adapter) {
DiffCallback(List<TaskContainer> oldList, List<TaskContainer> newList, TaskAdapter adapter) {
this.oldList = oldList;
this.newList = newList;
this.adapter = adapter;
}
@Override
public boolean areItemsTheSame(@NonNull TaskContainer oldItem, @NonNull TaskContainer newItem) {
return oldItem.getId() == newItem.getId();
public int getOldListSize() {
return oldList.size();
}
@Override
public boolean areContentsTheSame(
@NonNull TaskContainer oldItem, @NonNull TaskContainer newItem) {
public int getNewListSize() {
return newList.size();
}
@Override
public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
return oldList.get(oldItemPosition).getId() == newList.get(newItemPosition).getId();
}
@Override
public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
TaskContainer oldItem = oldList.get(oldItemPosition);
TaskContainer newItem = newList.get(newItemPosition);
return oldItem.equals(newItem) && oldItem.getIndent() == adapter.getIndent(newItem);
}
}

@ -4,6 +4,7 @@ import static androidx.recyclerview.widget.ItemTouchHelper.DOWN;
import static androidx.recyclerview.widget.ItemTouchHelper.UP;
import android.graphics.Canvas;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.RecyclerView;
import com.todoroo.astrid.activity.TaskListFragment;
@ -14,14 +15,20 @@ public class ItemTouchHelperCallback extends ItemTouchHelper.Callback {
private final TaskAdapter adapter;
private final TaskListRecyclerAdapter recyclerAdapter;
private final TaskListFragment taskList;
private final Runnable onClear;
private int from = -1;
private int to = -1;
private boolean dragging;
ItemTouchHelperCallback(
TaskAdapter adapter, TaskListRecyclerAdapter recyclerAdapter, TaskListFragment taskList) {
TaskAdapter adapter,
TaskListRecyclerAdapter recyclerAdapter,
TaskListFragment taskList,
Runnable onClear) {
this.adapter = adapter;
this.recyclerAdapter = recyclerAdapter;
this.taskList = taskList;
this.onClear = onClear;
}
@Override
@ -30,6 +37,7 @@ public class ItemTouchHelperCallback extends ItemTouchHelper.Callback {
if (actionState == ItemTouchHelper.ACTION_STATE_DRAG) {
recyclerAdapter.startActionMode();
((ViewHolder) viewHolder).setMoving(true);
dragging = true;
}
}
@ -54,10 +62,15 @@ public class ItemTouchHelperCallback extends ItemTouchHelper.Callback {
@Override
public boolean onMove(
RecyclerView recyclerView, RecyclerView.ViewHolder source, RecyclerView.ViewHolder target) {
@NonNull RecyclerView recyclerView,
@NonNull RecyclerView.ViewHolder source,
@NonNull RecyclerView.ViewHolder target) {
recyclerAdapter.finishActionMode();
int fromPosition = source.getAdapterPosition();
int toPosition = target.getAdapterPosition();
if (!adapter.canMove((ViewHolder) source, (ViewHolder) target)) {
return false;
}
if (from == -1) {
((ViewHolder) source).setSelected(false);
from = fromPosition;
@ -93,6 +106,8 @@ public class ItemTouchHelperCallback extends ItemTouchHelper.Callback {
super.clearView(recyclerView, viewHolder);
ViewHolder vh = (ViewHolder) viewHolder;
vh.setMoving(false);
onClear.run();
dragging = false;
if (recyclerAdapter.isActionModeActive()) {
recyclerAdapter.toggle(vh);
} else {
@ -100,7 +115,7 @@ public class ItemTouchHelperCallback extends ItemTouchHelper.Callback {
if (from < to) {
to++;
}
adapter.moved(from, to);
recyclerAdapter.moved(from, to);
taskList.loadTaskListContent();
}
}
@ -114,4 +129,8 @@ public class ItemTouchHelperCallback extends ItemTouchHelper.Callback {
adapter.indented(viewHolder.getAdapterPosition(), direction == ItemTouchHelper.RIGHT ? 1 : -1);
taskList.loadTaskListContent();
}
public boolean isDragging() {
return dragging;
}
}

@ -1,23 +1,36 @@
package org.tasks.tasklist;
import static com.todoroo.andlib.utility.AndroidUtilities.assertMainThread;
import static com.todoroo.andlib.utility.AndroidUtilities.assertNotMainThread;
import android.os.Bundle;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.view.ActionMode;
import androidx.core.util.Pair;
import androidx.fragment.app.FragmentActivity;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.ListAdapter;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.DiffUtil.DiffResult;
import androidx.recyclerview.widget.ListUpdateCallback;
import androidx.recyclerview.widget.RecyclerView;
import com.google.common.primitives.Longs;
import com.todoroo.astrid.activity.TaskListFragment;
import com.todoroo.astrid.adapter.TaskAdapter;
import com.todoroo.astrid.api.Filter;
import com.todoroo.astrid.api.GtasksFilter;
import com.todoroo.astrid.utility.Flags;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.schedulers.Schedulers;
import io.reactivex.subjects.PublishSubject;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
import org.tasks.data.TaskContainer;
import org.tasks.intents.TaskIntents;
public class TaskListRecyclerAdapter extends ListAdapter<TaskContainer, ViewHolder>
public class TaskListRecyclerAdapter extends RecyclerView.Adapter<ViewHolder>
implements ViewHolder.ViewHolderCallbacks, ListUpdateCallback {
private static final String EXTRA_SELECTED_TASK_IDS = "extra_selected_task_ids";
@ -27,29 +40,72 @@ public class TaskListRecyclerAdapter extends ListAdapter<TaskContainer, ViewHold
private final TaskListFragment taskList;
private final ActionModeProvider actionModeProvider;
private final ItemTouchHelperCallback itemTouchHelperCallback;
private final boolean isGoogleTaskList;
private ActionMode mode = null;
private RecyclerView recyclerView;
private List<TaskContainer> list;
private PublishSubject<List<TaskContainer>> publishSubject = PublishSubject.create();
private Queue<Pair<List<TaskContainer>, DiffResult>> updates = new LinkedList<>();
private CompositeDisposable disposables = new CompositeDisposable();
public TaskListRecyclerAdapter(
TaskAdapter adapter,
ViewHolderFactory viewHolderFactory,
TaskListFragment taskList,
ActionModeProvider actionModeProvider) {
super(new DiffCallback(adapter));
ActionModeProvider actionModeProvider,
List<TaskContainer> list) {
this.adapter = adapter;
this.viewHolderFactory = viewHolderFactory;
this.taskList = taskList;
this.actionModeProvider = actionModeProvider;
itemTouchHelperCallback = new ItemTouchHelperCallback(adapter, this, taskList);
this.list = list;
itemTouchHelperCallback =
new ItemTouchHelperCallback(adapter, this, taskList, this::drainQueue);
isGoogleTaskList = taskList.getFilter() instanceof GtasksFilter;
Pair<List<TaskContainer>, DiffResult> initial = Pair.create(list, null);
disposables.add(
publishSubject
.observeOn(Schedulers.computation())
.scan(initial, this::calculateDiff)
.skip(1)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(this::applyDiff));
}
private Pair<List<TaskContainer>, DiffResult> calculateDiff(
Pair<List<TaskContainer>, DiffResult> last, List<TaskContainer> next) {
assertNotMainThread();
DiffCallback cb = new DiffCallback(last.first, next, adapter);
DiffResult result = DiffUtil.calculateDiff(cb, true);
return Pair.create(next, result);
}
public void applyToRecyclerView(RecyclerView recyclerView) {
this.recyclerView = recyclerView;
recyclerView.setAdapter(this);
private void drainQueue() {
assertMainThread();
new ItemTouchHelper(itemTouchHelperCallback).attachToRecyclerView(recyclerView);
Pair<List<TaskContainer>, DiffResult> update = updates.poll();
Bundle selections = getSaveState();
while (update != null) {
list = update.first;
update.second.dispatchUpdatesTo((ListUpdateCallback) this);
update = updates.poll();
}
restoreSaveState(selections);
}
private void applyDiff(Pair<List<TaskContainer>, DiffResult> update) {
assertMainThread();
updates.add(update);
if (!itemTouchHelperCallback.isDragging()) {
drainQueue();
}
}
public ItemTouchHelperCallback getItemTouchHelperCallback() {
return itemTouchHelperCallback;
}
public Bundle getSaveState() {
@ -78,7 +134,7 @@ public class TaskListRecyclerAdapter extends ListAdapter<TaskContainer, ViewHold
public void onBindViewHolder(ViewHolder holder, int position) {
TaskContainer task = getItem(position);
if (task != null) {
holder.bindView(task);
holder.bindView(task, isGoogleTaskList);
holder.setMoving(false);
int indent = adapter.getIndent(task);
task.setIndent(indent);
@ -87,6 +143,11 @@ public class TaskListRecyclerAdapter extends ListAdapter<TaskContainer, ViewHold
}
}
@Override
public int getItemCount() {
return list.size();
}
@Override
public void onCompletedTask(TaskContainer task, boolean newState) {
adapter.onCompletedTask(task, newState);
@ -112,6 +173,11 @@ public class TaskListRecyclerAdapter extends ListAdapter<TaskContainer, ViewHold
}
}
@Override
public void onDetachedFromRecyclerView(@NonNull RecyclerView recyclerView) {
disposables.dispose();
}
@Override
public boolean onLongPress(ViewHolder viewHolder) {
if (!adapter.isManuallySorted()) {
@ -156,6 +222,22 @@ public class TaskListRecyclerAdapter extends ListAdapter<TaskContainer, ViewHold
}
}
boolean isActionModeActive() {
return mode != null;
}
void onDestroyActionMode() {
mode = null;
}
public TaskContainer getItem(int position) {
return list.get(position);
}
public void submitList(List<TaskContainer> list) {
publishSubject.onNext(list);
}
@Override
public void onInserted(int position, int count) {
notifyItemRangeInserted(position, count);
@ -168,32 +250,17 @@ public class TaskListRecyclerAdapter extends ListAdapter<TaskContainer, ViewHold
@Override
public void onMoved(int fromPosition, int toPosition) {
notifyItemChanged(fromPosition);
notifyItemMoved(fromPosition, toPosition);
recyclerView.scrollToPosition(fromPosition);
}
@Override
public void onChanged(int position, int count, Object payload) {
public void onChanged(int position, int count, @Nullable Object payload) {
notifyItemRangeChanged(position, count, payload);
}
public void onTaskSaved() {
int scrollY = recyclerView.getScrollY();
notifyDataSetChanged();
recyclerView.setScrollY(scrollY);
}
boolean isActionModeActive() {
return mode != null;
}
void onDestroyActionMode() {
mode = null;
}
@Override
public TaskContainer getItem(int position) {
return super.getItem(position);
void moved(int from, int to) {
adapter.moved(from, to);
TaskContainer task = list.remove(from);
list.add(from < to ? to - 1 : to, task);
}
}

@ -36,7 +36,7 @@ import org.tasks.preferences.Preferences;
import org.tasks.ui.CheckBoxes;
import org.tasks.ui.ChipProvider;
class ViewHolder extends RecyclerView.ViewHolder {
public class ViewHolder extends RecyclerView.ViewHolder {
private final Activity context;
private final Preferences preferences;
@ -82,6 +82,7 @@ class ViewHolder extends RecyclerView.ViewHolder {
private int indent;
private boolean selected;
private boolean moving;
private boolean isGoogleTaskList;
ViewHolder(
Activity context,
@ -203,8 +204,9 @@ class ViewHolder extends RecyclerView.ViewHolder {
return indent > 0;
}
void bindView(TaskContainer task) {
void bindView(TaskContainer task, boolean isGoogleTaskList) {
this.task = task;
this.isGoogleTaskList = isGoogleTaskList;
setFieldContentsAndVisibility();
setTaskAppearance();
@ -280,7 +282,11 @@ class ViewHolder extends RecyclerView.ViewHolder {
List<String> tagUuids = tags != null ? newArrayList(tags.split(",")) : Lists.newArrayList();
List<Chip> chips =
chipProvider.getChips(context, task.getCaldav(), task.getGoogleTaskList(), tagUuids);
chipProvider.getChips(
context,
task.getCaldav(),
isGoogleTaskList ? null : task.getGoogleTaskList(),
tagUuids);
if (chips.isEmpty()) {
chipGroup.setVisibility(View.GONE);
} else {

@ -30,6 +30,7 @@ import io.reactivex.Single;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.schedulers.Schedulers;
import java.util.Collections;
import java.util.List;
import javax.inject.Inject;
import org.tasks.data.CaldavTask;
@ -42,26 +43,36 @@ import timber.log.Timber;
public class TaskListViewModel extends ViewModel {
private static final Field TASKS = field("tasks.*");
private static final StringProperty GTASK =
new StringProperty(null, GTASK_METADATA_JOIN + ".list_id").as("googletask");
private static final Field GTASK = field(GTASK_METADATA_JOIN + ".*");
private static final StringProperty CALDAV =
new StringProperty(null, CALDAV_METADATA_JOIN + ".calendar").as("caldav");
private static final Field INDENT = field("google_tasks.indent").as("indent");
private static final Field CHILDREN = field("children");
private static final Field SIBLINGS = field("siblings");
private static final Field PRIMARY_SORT = field("primary_sort").as("primarySort");
private static final Field SECONDARY_SORT = field("secondary_sort").as("secondarySort");
private static final StringProperty TAGS =
new StringProperty(null, "group_concat(" + TAGS_METADATA_JOIN + ".tag_uid" + ", ',')")
.as("tags");
private final MutableLiveData<List<TaskContainer>> tasks = new MutableLiveData<>();
@Inject Preferences preferences;
@Inject TaskDao taskDao;
@Inject Database database;
private MutableLiveData<List<TaskContainer>> tasks = new MutableLiveData<>();
private Filter filter;
private boolean manualSort;
private CompositeDisposable disposable = new CompositeDisposable();
public void observe(
LifecycleOwner owner, @NonNull Filter filter, Observer<List<TaskContainer>> observer) {
if (!filter.equals(this.filter) || !filter.getSqlQuery().equals(this.filter.getSqlQuery())) {
public void setFilter(@NonNull Filter filter, boolean manualSort) {
if (!filter.equals(this.filter)
|| !filter.getSqlQuery().equals(this.filter.getSqlQuery())
|| this.manualSort != manualSort) {
this.filter = filter;
this.manualSort = manualSort;
tasks = new MutableLiveData<>();
invalidate();
}
}
public void observe(LifecycleOwner owner, Observer<List<TaskContainer>> observer) {
tasks.observe(owner, observer);
}
@ -71,8 +82,8 @@ public class TaskListViewModel extends ViewModel {
Criterion tagsJoinCriterion = Criterion.and(Task.ID.eq(field(TAGS_METADATA_JOIN + ".task")));
Criterion gtaskJoinCriterion =
Criterion.and(
Task.ID.eq(field(GTASK_METADATA_JOIN + ".task")),
field(GTASK_METADATA_JOIN + ".deleted").eq(0));
Task.ID.eq(field(GTASK_METADATA_JOIN + ".gt_task")),
field(GTASK_METADATA_JOIN + ".gt_deleted").eq(0));
Criterion caldavJoinCriterion =
Criterion.and(
Task.ID.eq(field(CALDAV_METADATA_JOIN + ".task")),
@ -82,10 +93,12 @@ public class TaskListViewModel extends ViewModel {
tagsJoinCriterion =
Criterion.and(tagsJoinCriterion, field(TAGS_METADATA_JOIN + ".tag_uid").neq(uuid));
} else if (filter instanceof GtasksFilter) {
String listId = ((GtasksFilter) filter).getRemoteId();
gtaskJoinCriterion =
Criterion.and(gtaskJoinCriterion, field(GTASK_METADATA_JOIN + ".list_id").neq(listId));
fields.add(INDENT);
if (manualSort) {
fields.add(CHILDREN);
fields.add(SIBLINGS);
fields.add(PRIMARY_SORT);
fields.add(SECONDARY_SORT);
}
} else if (filter instanceof CaldavFilter) {
String uuid = ((CaldavFilter) filter).getUuid();
caldavJoinCriterion =
@ -104,14 +117,10 @@ public class TaskListViewModel extends ViewModel {
String query =
SortHelper.adjustQueryForFlagsAndSort(preferences, joinedQuery, preferences.getSortMode());
String groupedQuery;
if (query.contains("GROUP BY")) {
groupedQuery = query;
} else if (query.contains("ORDER BY")) {
groupedQuery = query.replace("ORDER BY", "GROUP BY " + Task.ID + " ORDER BY"); // $NON-NLS-1$
} else {
groupedQuery = query + " GROUP BY " + Task.ID;
}
String groupedQuery =
query.contains("ORDER BY")
? query.replace("ORDER BY", "GROUP BY " + Task.ID + " ORDER BY")
: query + " GROUP BY " + Task.ID;
return Query.select(fields.toArray(new Field[0]))
.withQueryTemplate(PermaSql.replacePlaceholdersForQuery(groupedQuery))
@ -138,4 +147,9 @@ public class TaskListViewModel extends ViewModel {
protected void onCleared() {
disposable.dispose();
}
public List<TaskContainer> getValue() {
List<TaskContainer> value = tasks.getValue();
return value != null ? value : Collections.emptyList();
}
}

@ -310,4 +310,5 @@
<string name="preference_screen">preference_screen</string>
<string name="p_add_google_task_account">add_google_task_account</string>
<string name="p_add_caldav_account">add_caldav_account</string>
<string name="p_google_tasks_add_to_top">google_tasks_add_to_top</string>
</resources>

@ -887,4 +887,5 @@ File %1$s contained %2$s.\n\n
<string name="changelog">Changelog</string>
<string name="version_string">Version %s</string>
<string name="invalid_backup_file">Invalid backup file</string>
<string name="google_tasks_add_to_top">New tasks on top</string>
</resources>

@ -11,7 +11,12 @@
<Preference
android:key="@string/p_add_google_task_account"
android:title="@string/add_account" />
android:title="@string/add_account"/>
<CheckBoxPreference
android:defaultValue="true"
android:key="@string/p_google_tasks_add_to_top"
android:title="@string/google_tasks_add_to_top"/>
<PreferenceCategory
android:key="@string/CalDAV"
@ -19,7 +24,7 @@
<Preference
android:key="@string/p_add_caldav_account"
android:title="@string/add_account" />
android:title="@string/add_account"/>
<PreferenceCategory android:title="@string/sync_SPr_interval_title">
<CheckBoxPreference

Loading…
Cancel
Save