From 087aae0a7b62f97b0d5ebb1e638af9c6f5c3759d Mon Sep 17 00:00:00 2001 From: Alex Baker Date: Thu, 16 May 2019 10:57:02 -0500 Subject: [PATCH] Complete rewrite of Google Task manual ordering --- .../com.todoroo.astrid.dao.Database/63.json | 996 ++++++++++++++++++ .../astrid/gtasks/GtasksIndentActionTest.java | 208 ---- .../gtasks/GtasksTaskListUpdaterTest.java | 205 ---- .../astrid/gtasks/GtasksTaskMovingTest.java | 313 ------ .../org/tasks/data/GoogleTaskDaoTests.java | 187 ++++ .../org/tasks/injection/TestComponent.java | 12 +- .../org/tasks/makers/GoogleTaskMaker.java | 32 + .../java/org/tasks/makers/GtaskListMaker.java | 24 +- .../astrid/activity/TaskEditFragment.java | 2 +- .../astrid/activity/TaskListFragment.java | 145 +-- .../astrid/adapter/AstridTaskAdapter.java | 11 + .../astrid/adapter/GoogleTaskAdapter.java | 195 ++-- .../todoroo/astrid/adapter/TaskAdapter.java | 17 +- .../astrid/adapter/TaskAdapterProvider.java | 11 +- .../com/todoroo/astrid/api/GtasksFilter.java | 16 +- .../astrid/backup/TasksXmlImporter.java | 2 - .../java/com/todoroo/astrid/dao/Database.java | 2 +- .../java/com/todoroo/astrid/dao/TaskDao.java | 27 +- .../astrid/gtasks/GtasksTaskListUpdater.java | 404 ------- .../astrid/gtasks/api/GtasksInvoker.java | 28 +- .../astrid/gtasks/api/MoveRequest.java | 57 - .../astrid/gtasks/sync/GtasksSyncService.java | 205 ---- .../todoroo/astrid/service/TaskCreator.java | 9 +- .../todoroo/astrid/service/TaskDeleter.java | 15 +- .../astrid/service/TaskDuplicator.java | 9 +- .../com/todoroo/astrid/service/TaskMover.java | 10 +- .../main/java/org/tasks/data/DeletionDao.java | 6 +- .../main/java/org/tasks/data/GoogleTask.java | 80 +- .../java/org/tasks/data/GoogleTaskDao.java | 185 +++- .../java/org/tasks/data/TaskContainer.java | 90 +- .../main/java/org/tasks/db/Migrations.java | 18 +- .../tasks/gtasks/GoogleTaskSynchronizer.java | 205 ++-- .../org/tasks/preferences/Preferences.java | 4 + .../java/org/tasks/tasklist/DiffCallback.java | 33 +- .../tasklist/ItemTouchHelperCallback.java | 25 +- .../tasklist/TaskListRecyclerAdapter.java | 135 ++- .../java/org/tasks/tasklist/ViewHolder.java | 12 +- .../java/org/tasks/ui/TaskListViewModel.java | 56 +- app/src/main/res/values/keys.xml | 1 + app/src/main/res/values/strings.xml | 1 + .../res/xml/preferences_synchronization.xml | 9 +- 41 files changed, 2097 insertions(+), 1905 deletions(-) create mode 100644 app/schemas/com.todoroo.astrid.dao.Database/63.json delete mode 100644 app/src/androidTest/java/com/todoroo/astrid/gtasks/GtasksIndentActionTest.java delete mode 100644 app/src/androidTest/java/com/todoroo/astrid/gtasks/GtasksTaskListUpdaterTest.java delete mode 100644 app/src/androidTest/java/com/todoroo/astrid/gtasks/GtasksTaskMovingTest.java create mode 100644 app/src/androidTest/java/org/tasks/data/GoogleTaskDaoTests.java create mode 100644 app/src/androidTest/java/org/tasks/makers/GoogleTaskMaker.java delete mode 100644 app/src/main/java/com/todoroo/astrid/gtasks/GtasksTaskListUpdater.java delete mode 100644 app/src/main/java/com/todoroo/astrid/gtasks/api/MoveRequest.java delete mode 100644 app/src/main/java/com/todoroo/astrid/gtasks/sync/GtasksSyncService.java diff --git a/app/schemas/com.todoroo.astrid.dao.Database/63.json b/app/schemas/com.todoroo.astrid.dao.Database/63.json new file mode 100644 index 000000000..44e7d9379 --- /dev/null +++ b/app/schemas/com.todoroo.astrid.dao.Database/63.json @@ -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\")" + ] + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/todoroo/astrid/gtasks/GtasksIndentActionTest.java b/app/src/androidTest/java/com/todoroo/astrid/gtasks/GtasksIndentActionTest.java deleted file mode 100644 index 04fbb03be..000000000 --- a/app/src/androidTest/java/com/todoroo/astrid/gtasks/GtasksIndentActionTest.java +++ /dev/null @@ -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 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; - } -} diff --git a/app/src/androidTest/java/com/todoroo/astrid/gtasks/GtasksTaskListUpdaterTest.java b/app/src/androidTest/java/com/todoroo/astrid/gtasks/GtasksTaskListUpdaterTest.java deleted file mode 100644 index 0ca0d47a5..000000000 --- a/app/src/androidTest/java/com/todoroo/astrid/gtasks/GtasksTaskListUpdaterTest.java +++ /dev/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; - -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 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; - } -} diff --git a/app/src/androidTest/java/com/todoroo/astrid/gtasks/GtasksTaskMovingTest.java b/app/src/androidTest/java/com/todoroo/astrid/gtasks/GtasksTaskMovingTest.java deleted file mode 100644 index b3d84a9f1..000000000 --- a/app/src/androidTest/java/com/todoroo/astrid/gtasks/GtasksTaskMovingTest.java +++ /dev/null @@ -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 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; - } -} diff --git a/app/src/androidTest/java/org/tasks/data/GoogleTaskDaoTests.java b/app/src/androidTest/java/org/tasks/data/GoogleTaskDaoTests.java new file mode 100644 index 000000000..310ed5de5 --- /dev/null +++ b/app/src/androidTest/java/org/tasks/data/GoogleTaskDaoTests.java @@ -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 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 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 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 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); + } +} diff --git a/app/src/androidTest/java/org/tasks/injection/TestComponent.java b/app/src/androidTest/java/org/tasks/injection/TestComponent.java index dc46cdc2f..e79348daf 100644 --- a/app/src/androidTest/java/org/tasks/injection/TestComponent.java +++ b/app/src/androidTest/java/org/tasks/injection/TestComponent.java @@ -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); } diff --git a/app/src/androidTest/java/org/tasks/makers/GoogleTaskMaker.java b/app/src/androidTest/java/org/tasks/makers/GoogleTaskMaker.java new file mode 100644 index 000000000..09f0ee68e --- /dev/null +++ b/app/src/androidTest/java/org/tasks/makers/GoogleTaskMaker.java @@ -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 LIST = newProperty(); + public static final Property ORDER = newProperty(); + public static final Property REMOTE_ID = newProperty(); + public static final Property TASK = newProperty(); + + private static final Instantiator 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... properties) { + return make(instantiator, properties); + } +} diff --git a/app/src/androidTest/java/org/tasks/makers/GtaskListMaker.java b/app/src/androidTest/java/org/tasks/makers/GtaskListMaker.java index 06a0d7302..4014a0278 100644 --- a/app/src/androidTest/java/org/tasks/makers/GtaskListMaker.java +++ b/app/src/androidTest/java/org/tasks/makers/GtaskListMaker.java @@ -18,19 +18,19 @@ public class GtaskListMaker { private static final Property ORDER = newProperty(); private static final Property COLOR = newProperty(); private static final Instantiator 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... properties) { return make(instantiator, properties); diff --git a/app/src/main/java/com/todoroo/astrid/activity/TaskEditFragment.java b/app/src/main/java/com/todoroo/astrid/activity/TaskEditFragment.java index 224045fd6..953495a6f 100755 --- a/app/src/main/java/com/todoroo/astrid/activity/TaskEditFragment.java +++ b/app/src/main/java/com/todoroo/astrid/activity/TaskEditFragment.java @@ -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 { diff --git a/app/src/main/java/com/todoroo/astrid/activity/TaskListFragment.java b/app/src/main/java/com/todoroo/astrid/activity/TaskListFragment.java index b33409c88..42b15c96d 100644 --- a/app/src/main/java/com/todoroo/astrid/activity/TaskListFragment.java +++ b/app/src/main/java/com/todoroo/astrid/activity/TaskListFragment.java @@ -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 - */ 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 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 - */ protected class RefreshReceiver extends BroadcastReceiver { - @Override public void onReceive(Context context, Intent intent) { refresh(); diff --git a/app/src/main/java/com/todoroo/astrid/adapter/AstridTaskAdapter.java b/app/src/main/java/com/todoroo/astrid/adapter/AstridTaskAdapter.java index 859bba1d7..cab923d9a 100644 --- a/app/src/main/java/com/todoroo/astrid/adapter/AstridTaskAdapter.java +++ b/app/src/main/java/com/todoroo/astrid/adapter/AstridTaskAdapter.java @@ -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; + } } diff --git a/app/src/main/java/com/todoroo/astrid/adapter/GoogleTaskAdapter.java b/app/src/main/java/com/todoroo/astrid/adapter/GoogleTaskAdapter.java index 9ca266923..325cf0335 100644 --- a/app/src/main/java/com/todoroo/astrid/adapter/GoogleTaskAdapter.java +++ b/app/src/main/java/com/todoroo/astrid/adapter/GoogleTaskAdapter.java @@ -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> 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 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 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()); } } } diff --git a/app/src/main/java/com/todoroo/astrid/adapter/TaskAdapter.java b/app/src/main/java/com/todoroo/astrid/adapter/TaskAdapter.java index fdf92ad26..15aa91029 100644 --- a/app/src/main/java/com/todoroo/astrid/adapter/TaskAdapter.java +++ b/app/src/main/java/com/todoroo/astrid/adapter/TaskAdapter.java @@ -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; + } } diff --git a/app/src/main/java/com/todoroo/astrid/adapter/TaskAdapterProvider.java b/app/src/main/java/com/todoroo/astrid/adapter/TaskAdapterProvider.java index 5cfae25eb..e9bcbeab7 100644 --- a/app/src/main/java/com/todoroo/astrid/adapter/TaskAdapterProvider.java +++ b/app/src/main/java/com/todoroo/astrid/adapter/TaskAdapterProvider.java @@ -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) { diff --git a/app/src/main/java/com/todoroo/astrid/api/GtasksFilter.java b/app/src/main/java/com/todoroo/astrid/api/GtasksFilter.java index e45edc413..3b87ce55b 100644 --- a/app/src/main/java/com/todoroo/astrid/api/GtasksFilter.java +++ b/app/src/main/java/com/todoroo/astrid/api/GtasksFilter.java @@ -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 getValuesForNewTasks(GoogleTaskList list) { diff --git a/app/src/main/java/com/todoroo/astrid/backup/TasksXmlImporter.java b/app/src/main/java/com/todoroo/astrid/backup/TasksXmlImporter.java index b7b197cf3..2590fb892 100755 --- a/app/src/main/java/com/todoroo/astrid/backup/TasksXmlImporter.java +++ b/app/src/main/java/com/todoroo/astrid/backup/TasksXmlImporter.java @@ -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")); diff --git a/app/src/main/java/com/todoroo/astrid/dao/Database.java b/app/src/main/java/com/todoroo/astrid/dao/Database.java index e0ba13845..dfd511880 100644 --- a/app/src/main/java/com/todoroo/astrid/dao/Database.java +++ b/app/src/main/java/com/todoroo/astrid/dao/Database.java @@ -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"; diff --git a/app/src/main/java/com/todoroo/astrid/dao/TaskDao.java b/app/src/main/java/com/todoroo/astrid/dao/TaskDao.java index db55b4c6e..bb116cd42 100644 --- a/app/src/main/java/com/todoroo/astrid/dao/TaskDao.java +++ b/app/src/main/java/com/todoroo/astrid/dao/TaskDao.java @@ -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 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 tasks = newArrayList(item); + tasks.addAll(getChildren(item.getId())); + setComplete(tasks, completed ? now() : 0L); + } - save(item); + private void setComplete(Iterable 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 getChildren(long taskId); + public int count(Filter filter) { Cursor cursor = getCursor(filter.sqlQuery); try { diff --git a/app/src/main/java/com/todoroo/astrid/gtasks/GtasksTaskListUpdater.java b/app/src/main/java/com/todoroo/astrid/gtasks/GtasksTaskListUpdater.java deleted file mode 100644 index 587760216..000000000 --- a/app/src/main/java/com/todoroo/astrid/gtasks/GtasksTaskListUpdater.java +++ /dev/null @@ -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 parents = new HashMap<>(); - - /** map of task -> prior sibling */ - final HashMap 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 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 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 children = new ArrayList<>(); - Node parent; - - Node(long taskId, Node parent) { - this.taskId = taskId; - this.parent = parent; - } - } -} diff --git a/app/src/main/java/com/todoroo/astrid/gtasks/api/GtasksInvoker.java b/app/src/main/java/com/todoroo/astrid/gtasks/api/GtasksInvoker.java index 2587b8138..f66c46d91 100644 --- a/app/src/main/java/com/todoroo/astrid/gtasks/api/GtasksInvoker.java +++ b/app/src/main/java/com/todoroo/astrid/gtasks/api/GtasksInvoker.java @@ -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 execute(TasksRequest 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); } diff --git a/app/src/main/java/com/todoroo/astrid/gtasks/api/MoveRequest.java b/app/src/main/java/com/todoroo/astrid/gtasks/api/MoveRequest.java deleted file mode 100644 index 3c2461be5..000000000 --- a/app/src/main/java/com/todoroo/astrid/gtasks/api/MoveRequest.java +++ /dev/null @@ -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; - } -} diff --git a/app/src/main/java/com/todoroo/astrid/gtasks/sync/GtasksSyncService.java b/app/src/main/java/com/todoroo/astrid/gtasks/sync/GtasksSyncService.java deleted file mode 100644 index 159665349..000000000 --- a/app/src/main/java/com/todoroo/astrid/gtasks/sync/GtasksSyncService.java +++ /dev/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 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 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 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 queue; - - OperationPushThread(LinkedBlockingQueue 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); - } - } - } - } -} diff --git a/app/src/main/java/com/todoroo/astrid/service/TaskCreator.java b/app/src/main/java/com/todoroo/astrid/service/TaskCreator.java index 2991e0c60..cd86e4f89 100644 --- a/app/src/main/java/com/todoroo/astrid/service/TaskCreator.java +++ b/app/src/main/java/com/todoroo/astrid/service/TaskCreator.java @@ -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())); diff --git a/app/src/main/java/com/todoroo/astrid/service/TaskDeleter.java b/app/src/main/java/com/todoroo/astrid/service/TaskDeleter.java index 1d52c5ef3..74363e9da 100644 --- a/app/src/main/java/com/todoroo/astrid/service/TaskDeleter.java +++ b/app/src/main/java/com/todoroo/astrid/service/TaskDeleter.java @@ -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 markDeleted(List taskIds) { - deletionDao.markDeleted(taskIds); + Set ids = new HashSet<>(taskIds); + for (List partition : partition(taskIds, 999)) { + ids.addAll(googleTaskDao.getChildren(partition)); + } + deletionDao.markDeleted(ids); workManager.cleanup(taskIds); workManager.sync(false); localBroadcastManager.broadcastRefresh(); diff --git a/app/src/main/java/com/todoroo/astrid/service/TaskDuplicator.java b/app/src/main/java/com/todoroo/astrid/service/TaskDuplicator.java index 114063873..09cb6488a 100644 --- a/app/src/main/java/com/todoroo/astrid/service/TaskDuplicator.java +++ b/app/src/main/java/com/todoroo/astrid/service/TaskDuplicator.java @@ -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 duplicate(List 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); diff --git a/app/src/main/java/com/todoroo/astrid/service/TaskMover.java b/app/src/main/java/com/todoroo/astrid/service/TaskMover.java index 39ec143c5..77710e9ef 100644 --- a/app/src/main/java/com/todoroo/astrid/service/TaskMover.java +++ b/app/src/main/java/com/todoroo/astrid/service/TaskMover.java @@ -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 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())); diff --git a/app/src/main/java/org/tasks/data/DeletionDao.java b/app/src/main/java/org/tasks/data/DeletionDao.java index 328da7a5c..eb37f0a67 100644 --- a/app/src/main/java/org/tasks/data/DeletionDao.java +++ b/app/src/main/java/org/tasks/data/DeletionDao.java @@ -19,7 +19,7 @@ public abstract class DeletionDao { @Query("DELETE FROM caldav_tasks WHERE task IN(:ids)") abstract void deleteCaldavTasks(List ids); - @Query("DELETE FROM google_tasks WHERE task IN(:ids)") + @Query("DELETE FROM google_tasks WHERE gt_task IN(:ids)") abstract void deleteGoogleTasks(List 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 ids); - public void markDeleted(List ids) { + public void markDeleted(Iterable ids) { long now = now(); for (List 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 getActiveGoogleTasks(String listId); @Delete diff --git a/app/src/main/java/org/tasks/data/GoogleTask.java b/app/src/main/java/org/tasks/data/GoogleTask.java index 34fc9823b..6cf7fd992 100644 --- a/app/src/main/java/org/tasks/data/GoogleTask.java +++ b/app/src/main/java/org/tasks/data/GoogleTask.java @@ -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=" diff --git a/app/src/main/java/org/tasks/data/GoogleTaskDao.java b/app/src/main/java/org/tasks/data/GoogleTaskDao.java index 5ffd744c3..3ae8fd933 100644 --- a/app/src/main/java/org/tasks/data/GoogleTaskDao.java +++ b/app/src/main/java/org/tasks/data/GoogleTaskDao.java @@ -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 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 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 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 getDeletedByTaskId(long taskId); + + @Query("SELECT * FROM google_tasks WHERE gt_task = :taskId") + public abstract List getAllByTaskId(long taskId); + + @Query("SELECT DISTINCT gt_list_id FROM google_tasks WHERE gt_deleted = 0 AND gt_task IN (:tasks)") + public abstract List getLists(List 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 getChildren(List 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 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 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 getDeletedByTaskId(long taskId); + @Transaction + public void reposition(String listId) { + updateParents(listId); - @Query("SELECT * FROM google_tasks WHERE task = :taskId") - List getAllByTaskId(long taskId); + List 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 getLists(List tasks); + public void validateSorting(String listId) { + List 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++; + } + } + } } diff --git a/app/src/main/java/org/tasks/data/TaskContainer.java b/app/src/main/java/org/tasks/data/TaskContainer.java index f6275cbdd..13eb9539d 100644 --- a/app/src/main/java/org/tasks/data/TaskContainer.java +++ b/app/src/main/java/org/tasks/data/TaskContainer.java @@ -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; + } } diff --git a/app/src/main/java/org/tasks/db/Migrations.java b/app/src/main/java/org/tasks/db/Migrations.java index 52f6481fd..d3e98b46b 100644 --- a/app/src/main/java/org/tasks/db/Migrations.java +++ b/app/src/main/java/org/tasks/db/Migrations.java @@ -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) { diff --git a/app/src/main/java/org/tasks/gtasks/GoogleTaskSynchronizer.java b/app/src/main/java/org/tasks/gtasks/GoogleTaskSynchronizer.java index 3d16b554b..ccb652d2f 100644 --- a/app/src/main/java/org/tasks/gtasks/GoogleTaskSynchronizer.java +++ b/app/src/main/java/org/tasks/gtasks/GoogleTaskSynchronizer.java @@ -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 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 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 tasks = new ArrayList<>(); + String nextPageToken = null; + do { + Tasks taskList = + gtasksInvoker.getAllGtasksFromListId(listId, lastSyncDate + 1000L, nextPageToken); + if (taskList == null) { + break; + } + List items = taskList.getItems(); + if (items != null) { + tasks.addAll(items); + } + nextPageToken = taskList.getNextPageToken(); + } while (nextPageToken != null); - boolean includeDeletedAndHidden = lastSyncDate != 0; - try { - List tasks = new ArrayList<>(); - String nextPageToken = null; - do { - Tasks taskList = - gtasksInvoker.getAllGtasksFromListId( - listId, includeDeletedAndHidden, lastSyncDate + 1000L, nextPageToken); - if (taskList == null) { - break; - } - List 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) { diff --git a/app/src/main/java/org/tasks/preferences/Preferences.java b/app/src/main/java/org/tasks/preferences/Preferences.java index 8c432ac99..f0e0a5743 100644 --- a/app/src/main/java/org/tasks/preferences/Preferences.java +++ b/app/src/main/java/org/tasks/preferences/Preferences.java @@ -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); } diff --git a/app/src/main/java/org/tasks/tasklist/DiffCallback.java b/app/src/main/java/org/tasks/tasklist/DiffCallback.java index 22ef4493b..e5a4f68ec 100644 --- a/app/src/main/java/org/tasks/tasklist/DiffCallback.java +++ b/app/src/main/java/org/tasks/tasklist/DiffCallback.java @@ -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 { +class DiffCallback extends DiffUtil.Callback { - private final TaskAdapter adapter; + private final List oldList; + private final List newList; + @Deprecated private final TaskAdapter adapter; - public DiffCallback(TaskAdapter adapter) { + DiffCallback(List oldList, List 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); } } diff --git a/app/src/main/java/org/tasks/tasklist/ItemTouchHelperCallback.java b/app/src/main/java/org/tasks/tasklist/ItemTouchHelperCallback.java index ca8758e87..6ccee6878 100644 --- a/app/src/main/java/org/tasks/tasklist/ItemTouchHelperCallback.java +++ b/app/src/main/java/org/tasks/tasklist/ItemTouchHelperCallback.java @@ -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; + } } diff --git a/app/src/main/java/org/tasks/tasklist/TaskListRecyclerAdapter.java b/app/src/main/java/org/tasks/tasklist/TaskListRecyclerAdapter.java index 53907ef88..c21164663 100644 --- a/app/src/main/java/org/tasks/tasklist/TaskListRecyclerAdapter.java +++ b/app/src/main/java/org/tasks/tasklist/TaskListRecyclerAdapter.java @@ -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 +public class TaskListRecyclerAdapter extends RecyclerView.Adapter 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 list; + private PublishSubject> publishSubject = PublishSubject.create(); + private Queue, 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 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, 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, DiffResult> calculateDiff( + Pair, DiffResult> last, List 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, 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, 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 list) { + publishSubject.onNext(list); + } + @Override public void onInserted(int position, int count) { notifyItemRangeInserted(position, count); @@ -168,32 +250,17 @@ public class TaskListRecyclerAdapter extends ListAdapter 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 tagUuids = tags != null ? newArrayList(tags.split(",")) : Lists.newArrayList(); List 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 { diff --git a/app/src/main/java/org/tasks/ui/TaskListViewModel.java b/app/src/main/java/org/tasks/ui/TaskListViewModel.java index b876c5c9d..8c382896e 100644 --- a/app/src/main/java/org/tasks/ui/TaskListViewModel.java +++ b/app/src/main/java/org/tasks/ui/TaskListViewModel.java @@ -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> tasks = new MutableLiveData<>(); @Inject Preferences preferences; @Inject TaskDao taskDao; @Inject Database database; + private MutableLiveData> tasks = new MutableLiveData<>(); private Filter filter; + private boolean manualSort; private CompositeDisposable disposable = new CompositeDisposable(); - public void observe( - LifecycleOwner owner, @NonNull Filter filter, Observer> 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> 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 getValue() { + List value = tasks.getValue(); + return value != null ? value : Collections.emptyList(); + } } diff --git a/app/src/main/res/values/keys.xml b/app/src/main/res/values/keys.xml index b45f07cc5..136efc3a1 100644 --- a/app/src/main/res/values/keys.xml +++ b/app/src/main/res/values/keys.xml @@ -310,4 +310,5 @@ preference_screen add_google_task_account add_caldav_account + google_tasks_add_to_top diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 39defc9e9..ee3008828 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -887,4 +887,5 @@ File %1$s contained %2$s.\n\n Changelog Version %s Invalid backup file + New tasks on top diff --git a/app/src/main/res/xml/preferences_synchronization.xml b/app/src/main/res/xml/preferences_synchronization.xml index 729da2b43..398f51e50 100644 --- a/app/src/main/res/xml/preferences_synchronization.xml +++ b/app/src/main/res/xml/preferences_synchronization.xml @@ -11,7 +11,12 @@ + android:title="@string/add_account"/> + + + android:title="@string/add_account"/>