From 9e85cc5b012bc204b4d476470c93993b6496fb0f Mon Sep 17 00:00:00 2001 From: Alex Baker Date: Wed, 18 Dec 2019 12:50:11 -0600 Subject: [PATCH] Add support for offline multi-level subtasks --- .../com.todoroo.astrid.dao.Database/70.json | 1114 +++++++++++++++++ .../com/todoroo/astrid/dao/TaskDaoTests.java | 36 + .../todoroo/astrid/service/TaskMoverTest.java | 106 +- .../java/org/tasks/data/CaldavDaoTests.java | 76 -- .../org/tasks/makers/CaldavTaskMaker.java | 3 - .../java/org/tasks/makers/TaskMaker.java | 4 + .../astrid/adapter/CaldavTaskAdapter.java | 5 +- .../java/com/todoroo/astrid/dao/Database.java | 2 +- .../java/com/todoroo/astrid/dao/TaskDao.java | 65 +- .../java/com/todoroo/astrid/data/Task.java | 71 +- .../todoroo/astrid/service/TaskCompleter.java | 6 +- .../todoroo/astrid/service/TaskDeleter.java | 6 +- .../com/todoroo/astrid/service/TaskMover.java | 198 +-- .../org/tasks/backup/TasksJsonImporter.java | 1 + .../org/tasks/caldav/CaldavConverter.java | 2 +- .../main/java/org/tasks/data/CaldavDao.java | 70 +- .../main/java/org/tasks/data/CaldavTask.java | 26 +- .../java/org/tasks/data/TaskContainer.java | 11 +- .../main/java/org/tasks/db/Migrations.java | 28 +- .../org/tasks/tasklist/SubtaskViewHolder.java | 4 +- .../tasklist/SubtasksRecyclerAdapter.java | 8 +- .../java/org/tasks/ui/SubtaskControlSet.java | 29 +- .../java/org/tasks/ui/TaskListViewModel.java | 23 +- app/src/main/res/values-de/strings.xml | 3 +- app/src/main/res/values-es/strings.xml | 3 +- app/src/main/res/values-eu/strings.xml | 1 - app/src/main/res/values-fr/strings.xml | 3 +- app/src/main/res/values-nl/strings.xml | 3 +- app/src/main/res/values-zh-rCN/strings.xml | 3 +- app/src/main/res/values/strings.xml | 1 - 30 files changed, 1577 insertions(+), 334 deletions(-) create mode 100644 app/schemas/com.todoroo.astrid.dao.Database/70.json diff --git a/app/schemas/com.todoroo.astrid.dao.Database/70.json b/app/schemas/com.todoroo.astrid.dao.Database/70.json new file mode 100644 index 000000000..2c747ff86 --- /dev/null +++ b/app/schemas/com.todoroo.astrid.dao.Database/70.json @@ -0,0 +1,1114 @@ +{ + "formatVersion": 1, + "database": { + "version": 70, + "identityHash": "db26c2e949706c1355326a948dbd98fe", + "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 IF NOT EXISTS `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, `td_icon` INTEGER)", + "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 + }, + { + "fieldPath": "icon", + "columnName": "td_icon", + "affinity": "INTEGER", + "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, `collapsed` INTEGER NOT NULL, `parent` INTEGER NOT NULL, `parent_uuid` 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 + }, + { + "fieldPath": "collapsed", + "columnName": "collapsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "parent", + "columnName": "parent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "parentUuid", + "columnName": "parent_uuid", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "_id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "t_rid", + "unique": true, + "columnNames": [ + "remoteId" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `t_rid` ON `${TABLE_NAME}` (`remoteId`)" + }, + { + "name": "active_and_visible", + "unique": false, + "columnNames": [ + "completed", + "deleted", + "hideUntil" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `active_and_visible` ON `${TABLE_NAME}` (`completed`, `deleted`, `hideUntil`)" + } + ], + "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": [ + { + "name": "place_uid", + "unique": true, + "columnNames": [ + "uid" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `place_uid` ON `${TABLE_NAME}` (`uid`)" + } + ], + "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": [ + { + "name": "geo_task", + "unique": false, + "columnNames": [ + "task" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `geo_task` ON `${TABLE_NAME}` (`task`)" + } + ], + "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": [ + { + "name": "tag_task", + "unique": false, + "columnNames": [ + "task" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `tag_task` ON `${TABLE_NAME}` (`task`)" + } + ], + "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": [ + { + "name": "gt_task", + "unique": false, + "columnNames": [ + "gt_task" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `gt_task` ON `${TABLE_NAME}` (`gt_task`)" + }, + { + "name": "gt_list_parent", + "unique": false, + "columnNames": [ + "gt_list_id", + "gt_parent" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `gt_list_parent` ON `${TABLE_NAME}` (`gt_list_id`, `gt_parent`)" + } + ], + "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, `f_color` INTEGER, `f_icon` INTEGER)", + "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 + }, + { + "fieldPath": "color", + "columnName": "f_color", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "icon", + "columnName": "f_icon", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "_id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "google_task_lists", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`gtl_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `gtl_account` TEXT, `gtl_remote_id` TEXT, `gtl_title` TEXT, `gtl_remote_order` INTEGER NOT NULL, `gtl_last_sync` INTEGER NOT NULL, `gtl_color` INTEGER, `gtl_icon` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "gtl_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "account", + "columnName": "gtl_account", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "remoteId", + "columnName": "gtl_remote_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "title", + "columnName": "gtl_title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "remoteOrder", + "columnName": "gtl_remote_order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastSync", + "columnName": "gtl_last_sync", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "color", + "columnName": "gtl_color", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "icon", + "columnName": "gtl_icon", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "gtl_id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "caldav_lists", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`cdl_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `cdl_account` TEXT, `cdl_uuid` TEXT, `cdl_name` TEXT, `cdl_color` INTEGER NOT NULL, `cdl_ctag` TEXT, `cdl_url` TEXT, `cdl_icon` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "cdl_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "account", + "columnName": "cdl_account", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "uuid", + "columnName": "cdl_uuid", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "cdl_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "color", + "columnName": "cdl_color", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ctag", + "columnName": "cdl_ctag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "cdl_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "icon", + "columnName": "cdl_icon", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "cdl_id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "caldav_tasks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`cd_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `cd_task` INTEGER NOT NULL, `cd_calendar` TEXT, `cd_object` TEXT, `cd_remote_id` TEXT, `cd_etag` TEXT, `cd_last_sync` INTEGER NOT NULL, `cd_deleted` INTEGER NOT NULL, `cd_vtodo` TEXT, `cd_remote_parent` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "cd_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "task", + "columnName": "cd_task", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "calendar", + "columnName": "cd_calendar", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "object", + "columnName": "cd_object", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "remoteId", + "columnName": "cd_remote_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "etag", + "columnName": "cd_etag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastSync", + "columnName": "cd_last_sync", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "deleted", + "columnName": "cd_deleted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "vtodo", + "columnName": "cd_vtodo", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "remoteParent", + "columnName": "cd_remote_parent", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "cd_id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "cd_task", + "unique": false, + "columnNames": [ + "cd_task" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `cd_task` ON `${TABLE_NAME}` (`cd_task`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "caldav_accounts", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`cda_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `cda_uuid` TEXT, `cda_name` TEXT, `cda_url` TEXT, `cda_username` TEXT, `cda_password` TEXT, `cda_error` TEXT, `cda_repeat` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "cda_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "uuid", + "columnName": "cda_uuid", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "cda_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "cda_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "username", + "columnName": "cda_username", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "password", + "columnName": "cda_password", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "error", + "columnName": "cda_error", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "suppressRepeatingTasks", + "columnName": "cda_repeat", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "cda_id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "google_task_accounts", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`gta_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `gta_account` TEXT, `gta_error` TEXT, `gta_etag` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "gta_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "account", + "columnName": "gta_account", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "error", + "columnName": "gta_error", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "etag", + "columnName": "gta_etag", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "gta_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, 'db26c2e949706c1355326a948dbd98fe')" + ] + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/todoroo/astrid/dao/TaskDaoTests.java b/app/src/androidTest/java/com/todoroo/astrid/dao/TaskDaoTests.java index 86ce21002..5a791d070 100644 --- a/app/src/androidTest/java/com/todoroo/astrid/dao/TaskDaoTests.java +++ b/app/src/androidTest/java/com/todoroo/astrid/dao/TaskDaoTests.java @@ -6,12 +6,20 @@ package com.todoroo.astrid.dao; +import static com.natpryce.makeiteasy.MakeItEasy.with; +import static java.util.Arrays.asList; +import static java.util.Collections.singletonList; import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertNotSame; import static junit.framework.Assert.assertNull; +import static org.tasks.makers.TaskMaker.ID; +import static org.tasks.makers.TaskMaker.PARENT; +import static org.tasks.makers.TaskMaker.newTask; import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SdkSuppress; import com.google.common.collect.ImmutableList; +import com.google.common.primitives.Longs; import com.todoroo.andlib.utility.DateUtilities; import com.todoroo.astrid.data.Task; import com.todoroo.astrid.service.TaskDeleter; @@ -155,6 +163,34 @@ public class TaskDaoTests extends InjectingTestCase { assertEquals(0, taskDao.getAll().size()); } + @Test + @SdkSuppress(minSdkVersion = 21) + public void findChildrenInList() { + taskDao.createNew(newTask(with(ID, 1L))); + taskDao.createNew(newTask(with(ID, 2L), with(PARENT, 1L))); + + assertEquals(singletonList(2L), taskDao.findChildrenInList(Longs.asList(1, 2))); + } + + @Test + @SdkSuppress(minSdkVersion = 21) + public void findRecursiveChildrenInList() { + taskDao.createNew(newTask(with(ID, 1L))); + taskDao.createNew(newTask(with(ID, 2L), with(PARENT, 1L))); + taskDao.createNew(newTask(with(ID, 3L), with(PARENT, 2L))); + + assertEquals(asList(2L, 3L), taskDao.findChildrenInList(Longs.asList(1, 2, 3))); + } + + @Test + @SdkSuppress(minSdkVersion = 21) + public void findRecursiveChildrenInListAfterSkippingParent() { + taskDao.createNew(newTask(with(ID, 1L))); + taskDao.createNew(newTask(with(ID, 2L), with(PARENT, 1L))); + taskDao.createNew(newTask(with(ID, 3L), with(PARENT, 2L))); + + assertEquals(singletonList(3L), taskDao.findChildrenInList(Longs.asList(1, 3))); + } @Override protected void inject(TestComponent component) { component.inject(this); diff --git a/app/src/androidTest/java/com/todoroo/astrid/service/TaskMoverTest.java b/app/src/androidTest/java/com/todoroo/astrid/service/TaskMoverTest.java index 2cb45e4c3..26ce59984 100644 --- a/app/src/androidTest/java/com/todoroo/astrid/service/TaskMoverTest.java +++ b/app/src/androidTest/java/com/todoroo/astrid/service/TaskMoverTest.java @@ -26,6 +26,7 @@ import com.google.common.primitives.Longs; import com.todoroo.astrid.api.CaldavFilter; import com.todoroo.astrid.api.GtasksFilter; import com.todoroo.astrid.dao.TaskDao; +import com.todoroo.astrid.data.Task; import java.util.List; import javax.inject.Inject; import org.junit.Before; @@ -41,6 +42,7 @@ import org.tasks.injection.TestComponent; import org.tasks.jobs.WorkManager; import org.tasks.makers.CaldavTaskMaker; import org.tasks.makers.GtaskListMaker; +import org.tasks.makers.TaskMaker; @RunWith(AndroidJUnit4.class) public class TaskMoverTest extends InjectingTestCase { @@ -125,7 +127,9 @@ public class TaskMoverTest extends InjectingTestCase { @Test @SdkSuppress(minSdkVersion = 21) public void moveRecursiveCaldavChildren() { - createTasks(1, 2, 3); + createTasks(1); + createSubtask(2, 1); + createSubtask(3, 2); caldavDao.insert( asList( newCaldavTask( @@ -133,13 +137,11 @@ public class TaskMoverTest extends InjectingTestCase { newCaldavTask( with(CaldavTaskMaker.TASK, 2L), with(CALENDAR, "1"), - with(CaldavTaskMaker.PARENT, 1L), with(REMOTE_ID, "b"), with(REMOTE_PARENT, "a")), newCaldavTask( with(CaldavTaskMaker.TASK, 3L), with(CALENDAR, "1"), - with(CaldavTaskMaker.PARENT, 2L), with(REMOTE_PARENT, "b")))); moveToCaldavList("2", 1); @@ -148,7 +150,7 @@ public class TaskMoverTest extends InjectingTestCase { assertEquals(3, deleted.size()); CaldavTask task = caldavDao.getTask(3); assertEquals("2", task.getCalendar()); - assertEquals(2, task.getParent()); + assertEquals(2, taskDao.fetch(3).getParent()); } @Test @@ -160,13 +162,49 @@ public class TaskMoverTest extends InjectingTestCase { moveToCaldavList("1", 1); CaldavTask task = caldavDao.getTask(2); - assertEquals(1L, task.getParent()); + assertEquals("1", task.getCalendar()); + assertEquals(1, taskDao.fetch(2).getParent()); + } + + @Test + @SdkSuppress(minSdkVersion = 21) + public void flattenLocalSubtasksWhenMovingToGoogleTasks() { + createTasks(1); + createSubtask(2, 1); + createSubtask(3, 2); + + moveToGoogleTasks("1", 1); + + assertEquals(1, googleTaskDao.getByTaskId(3).getParent()); + assertEquals(0, taskDao.fetch(3).getParent()); + } + + @Test + public void moveLocalChildToGoogleTasks() { + createTasks(1); + createSubtask(2, 1); + + moveToGoogleTasks("1", 2); + + assertEquals(0, taskDao.fetch(2).getParent()); + } + + @Test + public void moveLocalChildToCaldav() { + createTasks(1); + createSubtask(2, 1); + + moveToCaldavList("1", 2); + + assertEquals(0, taskDao.fetch(2).getParent()); } @Test @SdkSuppress(minSdkVersion = 21) public void flattenCaldavSubtasksWhenMovingToGoogleTasks() { - createTasks(1, 2, 3); + createTasks(1); + createSubtask(2, 1); + createSubtask(3, 2); caldavDao.insert( asList( newCaldavTask( @@ -174,13 +212,11 @@ public class TaskMoverTest extends InjectingTestCase { newCaldavTask( with(CaldavTaskMaker.TASK, 2L), with(CALENDAR, "1"), - with(CaldavTaskMaker.PARENT, 1L), with(REMOTE_ID, "b"), with(REMOTE_PARENT, "a")), newCaldavTask( with(CaldavTaskMaker.TASK, 3L), with(CALENDAR, "1"), - with(CaldavTaskMaker.PARENT, 2L), with(REMOTE_PARENT, "b")))); moveToGoogleTasks("1", 1); @@ -204,7 +240,8 @@ public class TaskMoverTest extends InjectingTestCase { @Test public void moveCaldavChildWithoutParent() { - createTasks(1, 2); + createTasks(1); + createSubtask(2, 1); caldavDao.insert( asList( newCaldavTask( @@ -212,13 +249,12 @@ public class TaskMoverTest extends InjectingTestCase { newCaldavTask( with(CaldavTaskMaker.TASK, 2L), with(CALENDAR, "1"), - with(CaldavTaskMaker.PARENT, 1L), with(REMOTE_PARENT, "a")))); moveToCaldavList("2", 2); - CaldavTask task = caldavDao.getTask(2); - assertEquals(0, task.getParent()); + assertEquals("2", caldavDao.getTask(2).getCalendar()); + assertEquals(0, taskDao.fetch(2).getParent()); } @Test @@ -241,6 +277,18 @@ public class TaskMoverTest extends InjectingTestCase { assertEquals("2", googleTaskDao.getByTaskId(1L).getListId()); } + @Test + public void moveLocalToCaldav() { + createTasks(1); + createSubtask(2, 1); + createSubtask(3, 2); + + moveToCaldavList("1", 1); + + assertEquals("1", caldavDao.getTask(3).getCalendar()); + assertEquals(2, taskDao.fetch(3).getParent()); + } + @Test public void dontSyncGoogleTask() { createTasks(1); @@ -272,12 +320,17 @@ public class TaskMoverTest extends InjectingTestCase { dontSync(1); assertNull(googleTaskDao.getByTaskId(2)); - assertFalse(taskDao.fetch(2).isDeleted()); + Task task = taskDao.fetch(2); + assertFalse(task.isDeleted()); + assertEquals(1, task.getParent()); + assertEquals(taskDao.fetch(1).getUuid(), task.getParentUuid()); } @Test public void dontSyncCaldavWithSubtasks() { - createTasks(1, 2); + createTasks(1); + createSubtask(2, 1); + createSubtask(3, 2); caldavDao.insert( asList( newCaldavTask( @@ -285,13 +338,20 @@ public class TaskMoverTest extends InjectingTestCase { newCaldavTask( with(CaldavTaskMaker.TASK, 2L), with(CALENDAR, "1"), - with(CaldavTaskMaker.PARENT, 1L), - with(REMOTE_PARENT, "a")))); + with(REMOTE_ID, "b"), + with(REMOTE_PARENT, "a")), + newCaldavTask( + with(CaldavTaskMaker.TASK, 3L), + with(CALENDAR, "1"), + with(REMOTE_PARENT, "b")))); - dontSync(2); + dontSync(1); - assertNull(caldavDao.getTask(2)); - assertFalse(taskDao.fetch(2).isDeleted()); + assertNull(caldavDao.getTask(3)); + Task task = taskDao.fetch(3); + assertFalse(task.isDeleted()); + assertEquals(2, task.getParent()); + assertEquals(taskDao.fetch(2).getUuid(), task.getParentUuid()); } @Test @@ -329,7 +389,8 @@ public class TaskMoverTest extends InjectingTestCase { @Test public void dontDuplicateWhenParentAndChildCaldavMoved() { - createTasks(1, 2); + createTasks(1); + createSubtask(2, 1); caldavDao.insert( asList( newCaldavTask( @@ -337,7 +398,6 @@ public class TaskMoverTest extends InjectingTestCase { newCaldavTask( with(CaldavTaskMaker.TASK, 2L), with(CALENDAR, "1"), - with(CaldavTaskMaker.PARENT, 1L), with(REMOTE_PARENT, "a")))); moveToCaldavList("2", 1, 2); @@ -351,6 +411,10 @@ public class TaskMoverTest extends InjectingTestCase { } } + private void createSubtask(long id, long parent) { + taskDao.createNew(newTask(with(ID, id), with(TaskMaker.PARENT, parent))); + } + private void moveToGoogleTasks(String list, long... tasks) { taskMover.move( Longs.asList(tasks), new GtasksFilter(newGtaskList(with(GtaskListMaker.REMOTE_ID, list)))); diff --git a/app/src/androidTest/java/org/tasks/data/CaldavDaoTests.java b/app/src/androidTest/java/org/tasks/data/CaldavDaoTests.java index 302af5aec..7a2c1aeac 100644 --- a/app/src/androidTest/java/org/tasks/data/CaldavDaoTests.java +++ b/app/src/androidTest/java/org/tasks/data/CaldavDaoTests.java @@ -1,14 +1,9 @@ package org.tasks.data; import static com.natpryce.makeiteasy.MakeItEasy.with; -import static java.util.Arrays.asList; import static java.util.Collections.singletonList; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; -import static org.tasks.makers.CaldavTaskMaker.CALENDAR; -import static org.tasks.makers.CaldavTaskMaker.REMOTE_ID; -import static org.tasks.makers.CaldavTaskMaker.REMOTE_PARENT; -import static org.tasks.makers.CaldavTaskMaker.newCaldavTask; import static org.tasks.makers.TagDataMaker.newTagData; import static org.tasks.makers.TagMaker.TAGDATA; import static org.tasks.makers.TagMaker.TASK; @@ -17,8 +12,6 @@ import static org.tasks.makers.TaskMaker.ID; import static org.tasks.makers.TaskMaker.newTask; import androidx.test.ext.junit.runners.AndroidJUnit4; -import androidx.test.filters.SdkSuppress; -import com.google.common.primitives.Longs; import com.todoroo.astrid.dao.TaskDao; import com.todoroo.astrid.data.Task; import javax.inject.Inject; @@ -26,7 +19,6 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.tasks.injection.InjectingTestCase; import org.tasks.injection.TestComponent; -import org.tasks.makers.CaldavTaskMaker; @RunWith(AndroidJUnit4.class) public class CaldavDaoTests extends InjectingTestCase { @@ -71,74 +63,6 @@ public class CaldavDaoTests extends InjectingTestCase { assertTrue(caldavDao.getTasksWithTags().isEmpty()); } - @Test - @SdkSuppress(minSdkVersion = 21) - public void findChildrenInList() { - taskDao.createNew(newTask(with(ID, 1L))); - taskDao.createNew(newTask(with(ID, 2L))); - caldavDao.insert( - asList( - newCaldavTask( - with(CaldavTaskMaker.TASK, 1L), with(CALENDAR, "1"), with(REMOTE_ID, "a")), - newCaldavTask( - with(CaldavTaskMaker.TASK, 2L), - with(CALENDAR, "1"), - with(CaldavTaskMaker.PARENT, 1L), - with(REMOTE_PARENT, "a")))); - - assertEquals(singletonList(2L), caldavDao.findChildrenInList(Longs.asList(1, 2))); - } - - @Test - @SdkSuppress(minSdkVersion = 21) - public void findRecursiveChildrenInList() { - taskDao.createNew(newTask(with(ID, 1L))); - taskDao.createNew(newTask(with(ID, 2L))); - taskDao.createNew(newTask(with(ID, 3L))); - caldavDao.insert( - asList( - newCaldavTask( - with(CaldavTaskMaker.TASK, 1L), with(CALENDAR, "1"), with(REMOTE_ID, "a")), - newCaldavTask( - with(CaldavTaskMaker.TASK, 2L), - with(CALENDAR, "1"), - with(CaldavTaskMaker.PARENT, 1L), - with(REMOTE_ID, "b"), - with(REMOTE_PARENT, "a")), - newCaldavTask( - with(CaldavTaskMaker.TASK, 3L), - with(CALENDAR, "1"), - with(CaldavTaskMaker.PARENT, 2L), - with(REMOTE_PARENT, "b")))); - - assertEquals(asList(2L, 3L), caldavDao.findChildrenInList(Longs.asList(1, 2, 3))); - } - - @Test - @SdkSuppress(minSdkVersion = 21) - public void findRecursiveChildrenInListAfterSkippingParent() { - taskDao.createNew(newTask(with(ID, 1L))); - taskDao.createNew(newTask(with(ID, 2L))); - taskDao.createNew(newTask(with(ID, 3L))); - caldavDao.insert( - asList( - newCaldavTask( - with(CaldavTaskMaker.TASK, 1L), with(CALENDAR, "1"), with(REMOTE_ID, "a")), - newCaldavTask( - with(CaldavTaskMaker.TASK, 2L), - with(CALENDAR, "1"), - with(CaldavTaskMaker.PARENT, 1L), - with(REMOTE_ID, "b"), - with(REMOTE_PARENT, "a")), - newCaldavTask( - with(CaldavTaskMaker.TASK, 3L), - with(CALENDAR, "1"), - with(CaldavTaskMaker.PARENT, 2L), - with(REMOTE_PARENT, "b")))); - - assertEquals(singletonList(3L), caldavDao.findChildrenInList(Longs.asList(1, 3))); - } - @Override protected void inject(TestComponent component) { component.inject(this); diff --git a/app/src/androidTest/java/org/tasks/makers/CaldavTaskMaker.java b/app/src/androidTest/java/org/tasks/makers/CaldavTaskMaker.java index e2180c782..56078f7dd 100644 --- a/app/src/androidTest/java/org/tasks/makers/CaldavTaskMaker.java +++ b/app/src/androidTest/java/org/tasks/makers/CaldavTaskMaker.java @@ -3,7 +3,6 @@ package org.tasks.makers; import static com.natpryce.makeiteasy.Property.newProperty; import static org.tasks.makers.Maker.make; -import com.google.common.base.Strings; import com.natpryce.makeiteasy.Instantiator; import com.natpryce.makeiteasy.Property; import com.natpryce.makeiteasy.PropertyValue; @@ -13,7 +12,6 @@ public class CaldavTaskMaker { public static final Property CALENDAR = newProperty(); public static final Property TASK = newProperty(); - public static final Property PARENT = newProperty(); public static final Property REMOTE_ID = newProperty(); public static final Property REMOTE_PARENT = newProperty(); @@ -21,7 +19,6 @@ public class CaldavTaskMaker { lookup -> { CaldavTask task = new CaldavTask(lookup.valueOf(TASK, 1L), lookup.valueOf(CALENDAR, "calendar")); - task.setParent(lookup.valueOf(PARENT, 0L)); task.setRemoteId(lookup.valueOf(REMOTE_ID, task.getRemoteId())); task.setRemoteParent(lookup.valueOf(REMOTE_PARENT, (String) null)); return task; diff --git a/app/src/androidTest/java/org/tasks/makers/TaskMaker.java b/app/src/androidTest/java/org/tasks/makers/TaskMaker.java index 43954eb27..ad7149b96 100644 --- a/app/src/androidTest/java/org/tasks/makers/TaskMaker.java +++ b/app/src/androidTest/java/org/tasks/makers/TaskMaker.java @@ -29,6 +29,8 @@ public class TaskMaker { public static final Property AFTER_COMPLETE = newProperty(); private static final Property TITLE = newProperty(); private static final Property PRIORITY = newProperty(); + public static final Property PARENT = newProperty(); + private static final Instantiator instantiator = lookup -> { Task task = new Task(); @@ -101,6 +103,8 @@ public class TaskMaker { DateTime creationTime = lookup.valueOf(CREATION_TIME, newDateTime()); task.setCreationDate(creationTime.getMillis()); + task.setParent(lookup.valueOf(PARENT, 0L)); + return task; }; diff --git a/app/src/main/java/com/todoroo/astrid/adapter/CaldavTaskAdapter.java b/app/src/main/java/com/todoroo/astrid/adapter/CaldavTaskAdapter.java index 4e0a6898a..acc273b9f 100644 --- a/app/src/main/java/com/todoroo/astrid/adapter/CaldavTaskAdapter.java +++ b/app/src/main/java/com/todoroo/astrid/adapter/CaldavTaskAdapter.java @@ -85,16 +85,17 @@ public final class CaldavTaskAdapter extends TaskAdapter { if (newParent == 0) { caldavTask.setRemoteParent(""); - caldavTask.setParent(0); + task.setParent(0); } else { CaldavTask parentTask = caldavDao.getTask(newParent); if (parentTask == null) { return; } caldavTask.setRemoteParent(parentTask.getRemoteId()); - caldavTask.setParent(newParent); + task.setParent(newParent); } caldavDao.update(caldavTask); + taskDao.save(task.getTask()); } private boolean taskIsChild(TaskContainer source, int destinationIndex) { 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 9128451ef..afe908a2a 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 = 69) + version = 70) 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 4f30ddddf..b4a895b47 100644 --- a/app/src/main/java/com/todoroo/astrid/dao/TaskDao.java +++ b/app/src/main/java/com/todoroo/astrid/dao/TaskDao.java @@ -8,6 +8,7 @@ package com.todoroo.astrid.dao; import static com.google.common.collect.Iterables.filter; import static com.google.common.collect.Iterables.transform; +import static com.google.common.collect.Lists.newArrayList; import static com.google.common.collect.Lists.transform; import static com.todoroo.andlib.sql.SqlConstants.COUNT; import static com.todoroo.andlib.utility.AndroidUtilities.atLeastLollipop; @@ -32,6 +33,7 @@ import com.todoroo.astrid.api.Filter; import com.todoroo.astrid.api.PermaSql; import com.todoroo.astrid.data.Task; import com.todoroo.astrid.helper.UUIDHelper; +import java.util.Collections; import java.util.List; import org.tasks.BuildConfig; import org.tasks.data.Place; @@ -100,7 +102,7 @@ public abstract class TaskDao { "SELECT tasks.* FROM tasks " + "LEFT JOIN google_tasks ON tasks._id = google_tasks.gt_task " + "WHERE gt_list_id IN (SELECT gtl_remote_id FROM google_task_lists WHERE gtl_account = :account)" - + "AND (tasks.modified > google_tasks.gt_last_sync OR google_tasks.gt_remote_id = '') " + + "AND (tasks.modified > google_tasks.gt_last_sync OR google_tasks.gt_remote_id = '' OR google_tasks.gt_deleted > 0) " + "ORDER BY CASE WHEN gt_parent = 0 THEN 0 ELSE 1 END, gt_order ASC") public abstract List getGoogleTasksToPush(String account); @@ -141,7 +143,7 @@ public abstract class TaskDao { public List fetchTasks(QueryCallback callback) { long start = BuildConfig.DEBUG ? now() : 0; boolean includeGoogleSubtasks = atLeastLollipop() && hasGoogleTaskSubtasks(); - boolean includeCaldavSubtasks = atLeastLollipop() && hasCaldavSubtasks(); + boolean includeCaldavSubtasks = atLeastLollipop() && hasSubtasks(); List queries = callback.getQueries(includeGoogleSubtasks, includeCaldavSubtasks); SupportSQLiteDatabase db = database.getOpenHelper().getWritableDatabase(); int last = queries.size() - 1; @@ -159,11 +161,8 @@ public abstract class TaskDao { @RawQuery abstract int count(SimpleSQLiteQuery query); - @Query( - "SELECT EXISTS(SELECT 1 FROM caldav_tasks " - + "INNER JOIN tasks ON cd_task = _id " - + "WHERE deleted = 0 AND cd_parent > 0 AND cd_deleted = 0)") - abstract boolean hasCaldavSubtasks(); + @Query("SELECT EXISTS(SELECT 1 FROM tasks WHERE parent > 0 AND deleted = 0)") + abstract boolean hasSubtasks(); @Query( "SELECT EXISTS(SELECT 1 FROM google_tasks " @@ -187,6 +186,58 @@ public abstract class TaskDao { @Query("UPDATE tasks SET modified = strftime('%s','now')*1000 WHERE _id in (:ids)") abstract void touchInternal(List ids); + @Query( + "UPDATE tasks SET parent = IFNULL((" + + " SELECT parent._id FROM tasks AS parent" + + " WHERE parent.remoteId = tasks.parent_uuid AND parent.deleted = 0), 0)" + + "WHERE parent_uuid IS NOT NULL AND parent_uuid != ''") + public abstract void updateParents(); + + @Query( + "UPDATE tasks SET parent_uuid = " + + " (SELECT parent.remoteId FROM tasks AS parent WHERE parent._id = tasks.parent)" + + " WHERE parent > 0 AND _id IN (:tasks)") + public abstract void updateParentUids(List tasks); + + @Query("UPDATE tasks SET parent = :parent, parent_uuid = :parentUuid WHERE _id IN (:children)") + public abstract void setParent(long parent, String parentUuid, List children); + + @Transaction + public List fetchChildren(long id) { + return fetch(getChildren(id)); + } + + public List getChildren(long id) { + return getChildren(Collections.singletonList(id)); + } + + public List getChildren(List ids) { + return atLeastLollipop() + ? getChildrenRecursive(ids) + : Collections.emptyList(); + } + + @Query( + "WITH RECURSIVE " + + " recursive_tasks (task) AS ( " + + " SELECT _id " + + " FROM tasks " + + "WHERE parent IN (:ids)" + + "UNION ALL " + + " SELECT _id " + + " FROM tasks " + + " INNER JOIN recursive_tasks " + + " ON recursive_tasks.task = tasks.parent" + + " WHERE tasks.deleted = 0)" + + "SELECT task FROM recursive_tasks") + abstract List getChildrenRecursive(List ids); + + public List findChildrenInList(List ids) { + List result = newArrayList(ids); + result.retainAll(getChildren(ids)); + return result; + } + @Query("UPDATE tasks SET collapsed = :collapsed WHERE _id = :id") public abstract void setCollapsed(long id, boolean collapsed); diff --git a/app/src/main/java/com/todoroo/astrid/data/Task.java b/app/src/main/java/com/todoroo/astrid/data/Task.java index 79594a4e6..64cad8d94 100644 --- a/app/src/main/java/com/todoroo/astrid/data/Task.java +++ b/app/src/main/java/com/todoroo/astrid/data/Task.java @@ -20,6 +20,7 @@ import androidx.room.Entity; import androidx.room.Ignore; import androidx.room.Index; import androidx.room.PrimaryKey; +import com.google.common.base.Objects; import com.google.common.base.Strings; import com.google.ical.values.RRule; import com.todoroo.andlib.data.Property.IntegerProperty; @@ -65,6 +66,7 @@ public class Task implements Parcelable { public static final LongProperty DELETION_DATE = new LongProperty(TABLE, "deleted"); public static final StringProperty NOTES = new StringProperty(TABLE, "notes"); public static final LongProperty TIMER_START = new LongProperty(TABLE, "timerStart"); + public static final LongProperty PARENT = new LongProperty(TABLE, "parent"); /** constant value for no uuid */ public static final String NO_UUID = "0"; // $NON-NLS-1$ @@ -187,6 +189,12 @@ public class Task implements Parcelable { @ColumnInfo(name = "collapsed") public boolean collapsed; + @ColumnInfo(name = "parent") + public transient long parent; + + @ColumnInfo(name = "parent_uuid") + public String parentUuid; + // --- due and hide until date management @Ignore private transient HashMap transitoryData = null; @@ -241,6 +249,8 @@ public class Task implements Parcelable { remoteId = parcel.readString(); transitoryData = parcel.readHashMap(ContentValues.class.getClassLoader()); collapsed = ParcelCompat.readBoolean(parcel); + parent = parcel.readLong(); + parentUuid = parcel.readString(); } /** @@ -600,6 +610,22 @@ public class Task implements Parcelable { this.calendarUri = calendarUri; } + public long getParent() { + return parent; + } + + public void setParent(long parent) { + this.parent = parent; + } + + public String getParentUuid() { + return parentUuid; + } + + public void setParentUuid(String parentUuid) { + this.parentUuid = parentUuid; + } + public boolean isNotifyModeNonstop() { return isReminderFlagSet(Task.NOTIFY_MODE_NONSTOP); } @@ -660,6 +686,8 @@ public class Task implements Parcelable { dest.writeString(remoteId); dest.writeMap(transitoryData); ParcelCompat.writeBoolean(dest, collapsed); + dest.writeLong(parent); + dest.writeString(parentUuid); } @Override @@ -714,6 +742,11 @@ public class Task implements Parcelable { + '\'' + ", collapsed=" + collapsed + + ", parent=" + + parent + + ", parentUuid='" + + parentUuid + + '\'' + ", transitoryData=" + transitoryData + '}'; @@ -786,6 +819,12 @@ public class Task implements Parcelable { if (calendarUri != null ? !calendarUri.equals(task.calendarUri) : task.calendarUri != null) { return false; } + if (parent != task.parent) { + return false; + } + if (!Objects.equal(parentUuid, task.parentUuid)) { + return false; + } return remoteId != null ? remoteId.equals(task.remoteId) : task.remoteId == null; } @@ -809,6 +848,9 @@ public class Task implements Parcelable { if (deleted != null ? !deleted.equals(original.deleted) : original.deleted != null) { return false; } + if (parent != original.parent) { + return false; + } return notes != null ? notes.equals(original.notes) : original.notes == null; } @@ -843,6 +885,9 @@ public class Task implements Parcelable { : original.recurrence != null) { return false; } + if (parent != original.parent) { + return false; + } return repeatUntil != null ? repeatUntil.equals(original.repeatUntil) : original.repeatUntil == null; @@ -902,6 +947,9 @@ public class Task implements Parcelable { if (collapsed != task.collapsed) { return false; } + if (parent != task.parent) { + return false; + } if (id != null ? !id.equals(task.id) : task.id != null) { return false; } @@ -932,31 +980,26 @@ public class Task implements Parcelable { if (notes != null ? !notes.equals(task.notes) : task.notes != null) { return false; } - if (estimatedSeconds != null - ? !estimatedSeconds.equals(task.estimatedSeconds) + if (estimatedSeconds != null ? !estimatedSeconds.equals(task.estimatedSeconds) : task.estimatedSeconds != null) { return false; } - if (elapsedSeconds != null - ? !elapsedSeconds.equals(task.elapsedSeconds) + if (elapsedSeconds != null ? !elapsedSeconds.equals(task.elapsedSeconds) : task.elapsedSeconds != null) { return false; } if (timerStart != null ? !timerStart.equals(task.timerStart) : task.timerStart != null) { return false; } - if (notificationFlags != null - ? !notificationFlags.equals(task.notificationFlags) + if (notificationFlags != null ? !notificationFlags.equals(task.notificationFlags) : task.notificationFlags != null) { return false; } - if (notifications != null - ? !notifications.equals(task.notifications) + if (notifications != null ? !notifications.equals(task.notifications) : task.notifications != null) { return false; } - if (lastNotified != null - ? !lastNotified.equals(task.lastNotified) + if (lastNotified != null ? !lastNotified.equals(task.lastNotified) : task.lastNotified != null) { return false; } @@ -975,8 +1018,10 @@ public class Task implements Parcelable { if (remoteId != null ? !remoteId.equals(task.remoteId) : task.remoteId != null) { return false; } - return transitoryData != null - ? transitoryData.equals(task.transitoryData) + if (parentUuid != null ? !parentUuid.equals(task.parentUuid) : task.parentUuid != null) { + return false; + } + return transitoryData != null ? transitoryData.equals(task.transitoryData) : task.transitoryData == null; } @@ -1004,6 +1049,8 @@ public class Task implements Parcelable { result = 31 * result + (calendarUri != null ? calendarUri.hashCode() : 0); result = 31 * result + (remoteId != null ? remoteId.hashCode() : 0); result = 31 * result + (collapsed ? 1 : 0); + result = 31 * result + (int) (parent ^ (parent >>> 32)); + result = 31 * result + (parentUuid != null ? parentUuid.hashCode() : 0); result = 31 * result + (transitoryData != null ? transitoryData.hashCode() : 0); return result; } diff --git a/app/src/main/java/com/todoroo/astrid/service/TaskCompleter.java b/app/src/main/java/com/todoroo/astrid/service/TaskCompleter.java index ae2a4b524..102075084 100644 --- a/app/src/main/java/com/todoroo/astrid/service/TaskCompleter.java +++ b/app/src/main/java/com/todoroo/astrid/service/TaskCompleter.java @@ -17,13 +17,11 @@ public class TaskCompleter { private final TaskDao taskDao; private final GoogleTaskDao googleTaskDao; - private final CaldavDao caldavDao; @Inject - TaskCompleter(TaskDao taskDao, GoogleTaskDao googleTaskDao, CaldavDao caldavDao) { + TaskCompleter(TaskDao taskDao, GoogleTaskDao googleTaskDao) { this.taskDao = taskDao; this.googleTaskDao = googleTaskDao; - this.caldavDao = caldavDao; } public void setComplete(long taskId) { @@ -39,7 +37,7 @@ public class TaskCompleter { long completionDate = completed ? now() : 0L; setComplete(Collections.singletonList(item), completionDate); List tasks = newArrayList(googleTaskDao.getChildTasks(item.getId())); - List caldavChildren = caldavDao.getChildren(item.getId()); + List caldavChildren = taskDao.getChildren(item.getId()); if (!caldavChildren.isEmpty()) { tasks.addAll(taskDao.fetch(caldavChildren)); } 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 ae7a6c2cd..4724198ad 100644 --- a/app/src/main/java/com/todoroo/astrid/service/TaskDeleter.java +++ b/app/src/main/java/com/todoroo/astrid/service/TaskDeleter.java @@ -15,7 +15,6 @@ import javax.inject.Inject; import org.tasks.LocalBroadcastManager; import org.tasks.data.CaldavAccount; import org.tasks.data.CaldavCalendar; -import org.tasks.data.CaldavDao; import org.tasks.data.DeletionDao; import org.tasks.data.GoogleTaskAccount; import org.tasks.data.GoogleTaskDao; @@ -31,7 +30,6 @@ public class TaskDeleter { private final TaskDao taskDao; private final LocalBroadcastManager localBroadcastManager; private final GoogleTaskDao googleTaskDao; - private final CaldavDao caldavDao; private final Preferences preferences; private final DeletionDao deletionDao; @@ -42,14 +40,12 @@ public class TaskDeleter { TaskDao taskDao, LocalBroadcastManager localBroadcastManager, GoogleTaskDao googleTaskDao, - CaldavDao caldavDao, Preferences preferences) { this.deletionDao = deletionDao; this.workManager = workManager; this.taskDao = taskDao; this.localBroadcastManager = localBroadcastManager; this.googleTaskDao = googleTaskDao; - this.caldavDao = caldavDao; this.preferences = preferences; } @@ -66,7 +62,7 @@ public class TaskDeleter { public List markDeleted(List taskIds) { Set ids = new HashSet<>(taskIds); ids.addAll(collect(taskIds, googleTaskDao::getChildren)); - ids.addAll(collect(taskIds, caldavDao::getChildren)); + ids.addAll(collect(taskIds, taskDao::getChildren)); deletionDao.markDeleted(ids); workManager.cleanup(ids); workManager.sync(false); 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 c62bcda8e..05da5eae7 100644 --- a/app/src/main/java/com/todoroo/astrid/service/TaskMover.java +++ b/app/src/main/java/com/todoroo/astrid/service/TaskMover.java @@ -1,17 +1,22 @@ package com.todoroo.astrid.service; +import static com.google.common.collect.FluentIterable.from; import static com.google.common.collect.Lists.newArrayList; import static com.google.common.collect.Lists.transform; +import static com.google.common.collect.Maps.newHashMap; import static com.todoroo.andlib.utility.DateUtilities.now; import static java.util.Collections.emptyList; +import androidx.annotation.Nullable; +import com.google.common.collect.Lists; import com.todoroo.astrid.api.CaldavFilter; import com.todoroo.astrid.api.Filter; import com.todoroo.astrid.api.GtasksFilter; import com.todoroo.astrid.dao.TaskDao; -import com.todoroo.astrid.data.SyncFlags; import com.todoroo.astrid.data.Task; +import java.util.ArrayList; import java.util.List; +import java.util.Map; import javax.inject.Inject; import org.tasks.LocalBroadcastManager; import org.tasks.data.CaldavDao; @@ -45,20 +50,6 @@ public class TaskMover { this.localBroadcastManager = localBroadcastManager; } - public void move(List tasks, Filter selectedList) { - tasks = newArrayList(tasks); - tasks.removeAll(googleTaskDao.findChildrenInList(tasks)); - tasks.removeAll(caldavDao.findChildrenInList(tasks)); - for (Task task : taskDao.fetch(tasks)) { - performMove(task, selectedList); - } - if (selectedList instanceof CaldavFilter) { - caldavDao.updateParents((((CaldavFilter) selectedList).getUuid())); - } - taskDao.touch(tasks); - localBroadcastManager.broadcastRefresh(); - } - public Filter getSingleFilter(List tasks) { List caldavCalendars = caldavDao.getCalendars(tasks); List googleTaskLists = googleTaskDao.getLists(tasks); @@ -74,44 +65,57 @@ public class TaskMover { return null; } - private void performMove(Task task, Filter selectedList) { + public void move(List tasks, Filter selectedList) { + tasks = newArrayList(tasks); + tasks.removeAll(googleTaskDao.findChildrenInList(tasks)); + tasks.removeAll(taskDao.findChildrenInList(tasks)); + taskDao.setParent(0, null, tasks); + for (Task task : taskDao.fetch(tasks)) { + performMove(task, selectedList); + } + if (selectedList instanceof CaldavFilter) { + caldavDao.updateParents((((CaldavFilter) selectedList).getUuid())); + } + taskDao.touch(tasks); + localBroadcastManager.broadcastRefresh(); + } + + private void performMove(Task task, @Nullable Filter selectedList) { long id = task.getId(); + GoogleTask googleTask = googleTaskDao.getByTaskId(id); - List googleTaskChildren = emptyList(); - List caldavChildren = emptyList(); - if (googleTask != null - && selectedList instanceof GtasksFilter - && googleTask.getListId().equals(((GtasksFilter) selectedList).getRemoteId())) { + if (googleTask != null) { + moveGoogleTask(task, googleTask, selectedList); return; } + CaldavTask caldavTask = caldavDao.getTask(id); - if (caldavTask != null - && selectedList instanceof CaldavFilter - && caldavTask.getCalendar().equals(((CaldavFilter) selectedList).getUuid())) { + if (caldavTask != null) { + moveCaldavTask(task, caldavTask, selectedList); return; } - if (googleTask != null) { - googleTaskChildren = googleTaskDao.getChildren(id); - googleTaskDao.markDeleted(now(), id); - } - if (caldavTask != null) { - List toDelete = newArrayList(caldavTask.getTask()); - List childIds = caldavDao.getChildren(caldavTask.getTask()); - if (!childIds.isEmpty()) { - caldavChildren = caldavDao.getTasks(childIds); - toDelete.addAll(childIds); - } - caldavDao.markDeleted(now(), toDelete); + moveLocalTask(task, selectedList); + } + + private void moveGoogleTask(Task task, GoogleTask googleTask, Filter selected) { + if (selected instanceof GtasksFilter + && googleTask.getListId().equals(((GtasksFilter) selected).getRemoteId())) { + return; } - if (selectedList instanceof GtasksFilter) { - String listId = ((GtasksFilter) selectedList).getRemoteId(); + long id = googleTask.getTask(); + List children = googleTaskDao.getChildren(id); + List childIds = from(children).transform(GoogleTask::getTask).toList(); + googleTaskDao.markDeleted(now(), id); + + if (selected instanceof GtasksFilter) { + String listId = ((GtasksFilter) selected).getRemoteId(); googleTaskDao.insertAndShift(new GoogleTask(id, listId), preferences.addGoogleTasksToTop()); - if (!googleTaskChildren.isEmpty()) { + if (!children.isEmpty()) { googleTaskDao.insert( transform( - googleTaskChildren, + children, child -> { GoogleTask newChild = new GoogleTask(child.getTask(), listId); newChild.setOrder(child.getOrder()); @@ -119,37 +123,91 @@ public class TaskMover { return newChild; })); } - if (!caldavChildren.isEmpty()) { - List children = newArrayList(); - for (int i = 0 ; i < caldavChildren.size() ; i++) { - CaldavTask child = caldavChildren.get(i); - GoogleTask newChild = new GoogleTask(child.getTask(), listId); - newChild.setOrder(i); - newChild.setParent(id); - children.add(newChild); - } - googleTaskDao.insert(children); - } - } else if (selectedList instanceof CaldavFilter) { - String listId = ((CaldavFilter) selectedList).getUuid(); - CaldavTask newParent = caldavTask == null - ? new CaldavTask(id, listId) - : new CaldavTask(id, listId, caldavTask.getRemoteId(), caldavTask.getObject()); - if (caldavTask != null) { - newParent.setVtodo(caldavTask.getVtodo()); - } + } else if (selected instanceof CaldavFilter) { + String listId = ((CaldavFilter) selected).getUuid(); + CaldavTask newParent = new CaldavTask(id, listId); + caldavDao.insert(newParent); + caldavDao.insert( + transform( + childIds, + child -> { + CaldavTask newChild = new CaldavTask(child, listId); + newChild.setRemoteParent(newParent.getRemoteId()); + return newChild; + })); + } else { + taskDao.setParent(task.getId(), task.getUuid(), childIds); + } + } + + private void moveCaldavTask(Task task, CaldavTask caldavTask, Filter selected) { + if (selected instanceof CaldavFilter + && caldavTask.getCalendar().equals(((CaldavFilter) selected).getUuid())) { + return; + } + + long id = task.getId(); + List childIds = taskDao.getChildren(id); + List toDelete = newArrayList(id); + List children = emptyList(); + if (!childIds.isEmpty()) { + children = caldavDao.getTasks(childIds); + toDelete.addAll(childIds); + } + caldavDao.markDeleted(now(), toDelete); + + if (selected instanceof CaldavFilter) { + long id1 = caldavTask.getTask(); + String listId = ((CaldavFilter) selected).getUuid(); + CaldavTask newParent = + new CaldavTask(id1, listId, caldavTask.getRemoteId(), caldavTask.getObject()); + newParent.setVtodo(caldavTask.getVtodo()); caldavDao.insert(newParent); - caldavDao.insert(transform(googleTaskChildren, child -> { - CaldavTask newChild = new CaldavTask(child.getTask(), listId); - newChild.setRemoteParent(newParent.getRemoteId()); - return newChild; - })); - caldavDao.insert(transform(caldavChildren, child -> { - CaldavTask newChild = new CaldavTask(child.getTask(), listId, child.getRemoteId(), child.getObject()); - newChild.setVtodo(child.getVtodo()); - newChild.setRemoteParent(child.getRemoteParent()); - return newChild; - })); + caldavDao.insert( + transform( + children, + child -> { + CaldavTask newChild = + new CaldavTask(child.getTask(), listId, child.getRemoteId(), child.getObject()); + newChild.setVtodo(child.getVtodo()); + newChild.setRemoteParent(child.getRemoteParent()); + return newChild; + })); + } else if (selected instanceof GtasksFilter) { + moveToGoogleTasks(id, childIds, (GtasksFilter) selected); + } else { + taskDao.updateParentUids(from(children).transform(CaldavTask::getTask).toList()); + } + } + + private void moveLocalTask(Task task, @Nullable Filter selected) { + if (selected instanceof GtasksFilter) { + moveToGoogleTasks(task.getId(), taskDao.getChildren(task.getId()), (GtasksFilter) selected); + } else if (selected instanceof CaldavFilter) { + long id = task.getId(); + String listId = ((CaldavFilter) selected).getUuid(); + Map tasks = newHashMap(); + tasks.put(id, new CaldavTask(id, listId)); + for (Task child : taskDao.fetchChildren(task.getId())) { + CaldavTask newTask = new CaldavTask(child.getId(), listId); + newTask.setRemoteParent(tasks.get(child.parent).getRemoteId()); + tasks.put(child.getId(), newTask); + } + caldavDao.insert(tasks.values()); + } + } + + private void moveToGoogleTasks(long id, List children, GtasksFilter filter) { + taskDao.setParent(0, null, children); + String listId = filter.getRemoteId(); + googleTaskDao.insertAndShift(new GoogleTask(id, listId), preferences.addGoogleTasksToTop()); + List newChildren = new ArrayList<>(); + for (int i = 0; i < children.size(); i++) { + GoogleTask newChild = new GoogleTask(children.get(i), listId); + newChild.setOrder(i); + newChild.setParent(id); + newChildren.add(newChild); } + googleTaskDao.insert(newChildren); } } diff --git a/app/src/main/java/org/tasks/backup/TasksJsonImporter.java b/app/src/main/java/org/tasks/backup/TasksJsonImporter.java index 3ebc1ad3c..037c21a58 100644 --- a/app/src/main/java/org/tasks/backup/TasksJsonImporter.java +++ b/app/src/main/java/org/tasks/backup/TasksJsonImporter.java @@ -243,6 +243,7 @@ public class TasksJsonImporter { googleTaskDao.updateParents(); caldavDao.updateParents(); + taskDao.updateParents(); for (Entry entry : backupContainer.getIntPrefs().entrySet()) { preferences.setInt(entry.getKey(), entry.getValue()); diff --git a/app/src/main/java/org/tasks/caldav/CaldavConverter.java b/app/src/main/java/org/tasks/caldav/CaldavConverter.java index a864a2b07..f231bdcb9 100644 --- a/app/src/main/java/org/tasks/caldav/CaldavConverter.java +++ b/app/src/main/java/org/tasks/caldav/CaldavConverter.java @@ -157,7 +157,7 @@ public class CaldavConverter { } remote.setLastModified(newDateTime(task.getModificationDate()).toUTC().getMillis()); remote.setPriority(toRemote(remote.getPriority(), task.getPriority())); - setParent(remote, caldavTask.getParent() == 0 ? null : caldavTask.getRemoteParent()); + setParent(remote, task.getParent() == 0 ? null : caldavTask.getRemoteParent()); return remote; } diff --git a/app/src/main/java/org/tasks/data/CaldavDao.java b/app/src/main/java/org/tasks/data/CaldavDao.java index 4b103442e..720e096f4 100644 --- a/app/src/main/java/org/tasks/data/CaldavDao.java +++ b/app/src/main/java/org/tasks/data/CaldavDao.java @@ -1,7 +1,5 @@ package org.tasks.data; -import static com.google.common.collect.Lists.newArrayList; -import static com.todoroo.andlib.utility.AndroidUtilities.atLeastLollipop; import static org.tasks.db.DbUtils.collect; import androidx.lifecycle.LiveData; @@ -11,7 +9,6 @@ import androidx.room.Insert; import androidx.room.Query; import androidx.room.Update; import io.reactivex.Single; -import java.util.Collections; import java.util.List; import org.tasks.filters.CaldavFilters; @@ -59,12 +56,12 @@ public abstract class CaldavDao { public abstract void update(CaldavTask caldavTask); public void update(SubsetCaldav caldavTask) { - update(caldavTask.getId(), caldavTask.getParent(), caldavTask.getRemoteParent()); + update(caldavTask.getId(), caldavTask.getRemoteParent()); } @Query( - "UPDATE caldav_tasks SET cd_parent = :parent, cd_remote_parent = :remoteParent WHERE cd_id = :id") - abstract void update(long id, long parent, String remoteParent); + "UPDATE caldav_tasks SET cd_remote_parent = :remoteParent WHERE cd_id = :id") + abstract void update(long id,String remoteParent); @Update public abstract void update(Iterable tasks); @@ -152,51 +149,24 @@ public abstract class CaldavDao { public abstract List getTasksWithTags(); @Query( - "UPDATE caldav_tasks" - + " SET cd_parent = IFNULL((" - + " SELECT cd_task FROM caldav_tasks AS p " - + " WHERE p.cd_remote_id = caldav_tasks.cd_remote_parent" - + " AND p.cd_calendar = caldav_tasks.cd_calendar" - + " AND p.cd_deleted = 0)," - + " 0)") + "UPDATE tasks SET parent = IFNULL((" + + " SELECT p.cd_task FROM caldav_tasks AS p" + + " INNER JOIN caldav_tasks ON caldav_tasks.cd_task = tasks._id" + + " WHERE p.cd_remote_id = caldav_tasks.cd_remote_parent" + + " AND p.cd_calendar = caldav_tasks.cd_calendar" + + " AND p.cd_deleted = 0), 0)" + + "WHERE _id IN (SELECT _id FROM tasks INNER JOIN caldav_tasks ON _id = cd_task WHERE cd_deleted = 0)") public abstract void updateParents(); - @Query("UPDATE caldav_tasks SET cd_parent = IFNULL((SELECT cd_task FROM caldav_tasks AS p WHERE p.cd_remote_id = caldav_tasks.cd_remote_parent), 0) WHERE cd_calendar = :calendar") + @Query( + "UPDATE tasks SET parent = IFNULL((" + + " SELECT p.cd_task FROM caldav_tasks AS p" + + " INNER JOIN caldav_tasks " + + " ON caldav_tasks.cd_task = tasks._id" + + " AND caldav_tasks.cd_calendar = :calendar" + + " WHERE p.cd_remote_id = caldav_tasks.cd_remote_parent" + + " AND p.cd_calendar = caldav_tasks.cd_calendar" + + " AND caldav_tasks.cd_deleted = 0), 0)" + + "WHERE _id IN (SELECT _id FROM tasks INNER JOIN caldav_tasks ON _id = cd_task WHERE cd_deleted = 0 AND cd_calendar = :calendar)") public abstract void updateParents(String calendar); - - public List getChildren(long id) { - return getChildren(Collections.singletonList(id)); - } - - public List getChildren(List ids) { - return atLeastLollipop() - ? getChildrenRecursive(ids) - : Collections.emptyList(); - } - - @Query("WITH RECURSIVE " - + " recursive_caldav (cd_task) AS ( " - + " SELECT cd_task " - + " FROM tasks " - + " INNER JOIN caldav_tasks " - + " ON _id = cd_task " - + " WHERE cd_parent IN (:ids) " - + " AND tasks.deleted = 0 AND caldav_tasks.cd_deleted = 0 " - + "UNION ALL " - + " SELECT caldav_tasks.cd_task " - + " FROM tasks " - + " INNER JOIN caldav_tasks " - + " ON _id = caldav_tasks.cd_task " - + " INNER JOIN recursive_caldav " - + " ON recursive_caldav.cd_task = caldav_tasks.cd_parent " - + " WHERE tasks.deleted = 0 AND caldav_tasks.cd_deleted = 0 " - + " ) " - + "SELECT cd_task FROM recursive_caldav") - abstract List getChildrenRecursive(List ids); - - public List findChildrenInList(List ids) { - List result = newArrayList(ids); - result.retainAll(getChildren(ids)); - return result; - } } diff --git a/app/src/main/java/org/tasks/data/CaldavTask.java b/app/src/main/java/org/tasks/data/CaldavTask.java index 1386e8921..17898bad5 100644 --- a/app/src/main/java/org/tasks/data/CaldavTask.java +++ b/app/src/main/java/org/tasks/data/CaldavTask.java @@ -10,23 +10,13 @@ import androidx.room.PrimaryKey; import com.todoroo.andlib.data.Property; import com.todoroo.andlib.data.Table; -@Entity( - tableName = "caldav_tasks", - indices = { - @Index(name = "cd_task", value = "cd_task"), - @Index( - name = "cd_calendar_parent", - value = {"cd_calendar", "cd_parent"}) - }) +@Entity(tableName = "caldav_tasks", indices = @Index(name = "cd_task", value = "cd_task")) public class CaldavTask { public static final String KEY = "caldav"; public static final Table TABLE = new Table("caldav_tasks"); - public static final Property.IntegerProperty PARENT = - new Property.IntegerProperty(TABLE, "cd_parent"); - public static final Property.IntegerProperty TASK = new Property.IntegerProperty(TABLE, "cd_task"); @@ -64,9 +54,6 @@ public class CaldavTask { @ColumnInfo(name = "cd_vtodo") private String vtodo; - @ColumnInfo(name = "cd_parent") - private transient long parent; - @ColumnInfo(name = "cd_remote_parent") private String remoteParent; @@ -160,14 +147,6 @@ public class CaldavTask { this.vtodo = vtodo; } - public long getParent() { - return parent; - } - - public void setParent(long parent) { - this.parent = parent; - } - public String getRemoteParent() { return remoteParent; } @@ -202,9 +181,6 @@ public class CaldavTask { + ", vtodo='" + vtodo + '\'' - + ", parent='" - + parent - + '\'' + ", remoteParent='" + remoteParent + '\'' diff --git a/app/src/main/java/org/tasks/data/TaskContainer.java b/app/src/main/java/org/tasks/data/TaskContainer.java index a7530abe6..a41c97677 100644 --- a/app/src/main/java/org/tasks/data/TaskContainer.java +++ b/app/src/main/java/org/tasks/data/TaskContainer.java @@ -1,5 +1,6 @@ package org.tasks.data; +import androidx.annotation.Nullable; import androidx.room.Embedded; import com.todoroo.astrid.data.Task; @@ -189,19 +190,19 @@ public class TaskContainer { public long getParent() { if (googletask != null) { return googletask.getParent(); - } else if (caldavTask != null) { - return caldavTask.getParent(); } else { - return 0; + return task.getParent(); } } public void setParent(long parent) { if (googletask != null) { + task.setParent(0); googletask.setParent(parent); - } else if (caldavTask != null) { - caldavTask.setParent(parent); + } else { + task.setParent(parent); } + task.setParentUuid(null); } public boolean hasParent() { diff --git a/app/src/main/java/org/tasks/db/Migrations.java b/app/src/main/java/org/tasks/db/Migrations.java index 7fd27ed90..b31916b00 100644 --- a/app/src/main/java/org/tasks/db/Migrations.java +++ b/app/src/main/java/org/tasks/db/Migrations.java @@ -356,6 +356,31 @@ public class Migrations { } }; + private static final Migration MIGRATION_69_70 = + new Migration(69, 70) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + database.execSQL("ALTER TABLE `tasks` ADD COLUMN `parent` INTEGER NOT NULL DEFAULT 0"); + database.execSQL("ALTER TABLE `tasks` ADD COLUMN `parent_uuid` TEXT"); + database.execSQL( + "UPDATE `tasks` SET `parent` = IFNULL((" + + " SELECT p.cd_task FROM caldav_tasks" + + " INNER JOIN caldav_tasks AS p ON p.cd_remote_id = caldav_tasks.cd_remote_parent" + + " WHERE caldav_tasks.cd_task = tasks._id" + + " AND caldav_tasks.cd_deleted = 0" + + " AND p.cd_calendar = caldav_tasks.cd_calendar" + + " AND p.cd_deleted = 0), 0)"); + database.execSQL("ALTER TABLE `caldav_tasks` RENAME TO `caldav_tasks-temp`"); + database.execSQL( + "CREATE TABLE IF NOT EXISTS `caldav_tasks` (`cd_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `cd_task` INTEGER NOT NULL, `cd_calendar` TEXT, `cd_object` TEXT, `cd_remote_id` TEXT, `cd_etag` TEXT, `cd_last_sync` INTEGER NOT NULL, `cd_deleted` INTEGER NOT NULL, `cd_vtodo` TEXT, `cd_remote_parent` TEXT)"); + database.execSQL( + "INSERT INTO `caldav_tasks` (`cd_id`, `cd_task`, `cd_calendar`, `cd_object`, `cd_remote_id`, `cd_etag`, `cd_last_sync`, `cd_deleted`, `cd_vtodo`, `cd_remote_parent`) " + + "SELECT `cd_id`, `cd_task`, `cd_calendar`, `cd_object`, `cd_remote_id`, `cd_etag`, `cd_last_sync`, `cd_deleted`, `cd_vtodo`, `cd_remote_parent` FROM `caldav_tasks-temp`"); + database.execSQL("DROP TABLE `caldav_tasks-temp`"); + database.execSQL("CREATE INDEX `cd_task` ON `caldav_tasks` (`cd_task`)"); + } + }; + public static final Migration[] MIGRATIONS = new Migration[] { MIGRATION_35_36, @@ -382,7 +407,8 @@ public class Migrations { MIGRATION_65_66, MIGRATION_66_67, MIGRATION_67_68, - MIGRATION_68_69 + MIGRATION_68_69, + MIGRATION_69_70 }; private static Migration NOOP(int from, int to) { diff --git a/app/src/main/java/org/tasks/tasklist/SubtaskViewHolder.java b/app/src/main/java/org/tasks/tasklist/SubtaskViewHolder.java index 85f4553c3..9fdbf4268 100644 --- a/app/src/main/java/org/tasks/tasklist/SubtaskViewHolder.java +++ b/app/src/main/java/org/tasks/tasklist/SubtaskViewHolder.java @@ -72,9 +72,9 @@ public class SubtaskViewHolder extends RecyclerView.ViewHolder { return Math.round(indent * getShiftSize()); } - void bindView(TaskContainer task, boolean multiLevelSubtasks) { + void bindView(TaskContainer task) { this.task = task; - setIndent(multiLevelSubtasks ? task.indent : 0); + setIndent(task.indent); if (task.hasChildren()) { chip.setText(locale.formatNumber(task.children)); chip.setVisibility(View.VISIBLE); diff --git a/app/src/main/java/org/tasks/tasklist/SubtasksRecyclerAdapter.java b/app/src/main/java/org/tasks/tasklist/SubtasksRecyclerAdapter.java index 74b2f73ce..d5225e0ba 100644 --- a/app/src/main/java/org/tasks/tasklist/SubtasksRecyclerAdapter.java +++ b/app/src/main/java/org/tasks/tasklist/SubtasksRecyclerAdapter.java @@ -50,7 +50,8 @@ public class SubtasksRecyclerAdapter extends RecyclerView.Adapter getNewSubtasks() { @@ -224,10 +223,6 @@ public class SubtaskControlSet extends TaskEditControlFragment implements Callba @OnClick(R.id.add_subtask) void addSubtask() { - if (remoteList == null) { - toaster.longToast(R.string.subtasks_enable_synchronization); - return; - } if (isGoogleTaskChild()) { toaster.longToast(R.string.subtasks_multilevel_google_task); return; @@ -290,7 +285,7 @@ public class SubtaskControlSet extends TaskEditControlFragment implements Callba } private void updateUI() { - if (remoteList == null || isGoogleTaskChild()) { + if (isGoogleTaskChild()) { recyclerView.setVisibility(View.GONE); newSubtaskContainer.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 d059f34ad..7c03c9055 100644 --- a/app/src/main/java/org/tasks/ui/TaskListViewModel.java +++ b/app/src/main/java/org/tasks/ui/TaskListViewModel.java @@ -157,19 +157,12 @@ public class TaskListViewModel extends ViewModel implements ObserverUnteraufgaben einklappen Unteraufgabe hinzufügen Unteraufgaben - Synchronisation aktivieren, um Unteraufgaben hinzuzufügen - Mehrere Unteraufgaben-Ebenen werden von Google Tasks nicht unterstützt + Mehrere Unteraufgaben-Ebenen werden von Google Tasks nicht unterstützt Titel eingeben \ No newline at end of file diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 9ac6aa1f6..17610c479 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -550,8 +550,7 @@ Contraer subtareas Añadir subtarea Subtareas - Habilitar la sincronización para añadir subtareas - Subtareas multinivel no compatibles con Google Tasks + Subtareas multinivel no compatibles con Google Tasks Introducir título Mostrar subtareas La visualización de subtareas degradará el rendimiento de la aplicación diff --git a/app/src/main/res/values-eu/strings.xml b/app/src/main/res/values-eu/strings.xml index d18eb2bff..eca778c76 100644 --- a/app/src/main/res/values-eu/strings.xml +++ b/app/src/main/res/values-eu/strings.xml @@ -557,7 +557,6 @@ Tolestu azpi-zereginak Gehitu azpi-zeregina Azpi-zereginak - Gaitu sinkronizazioa azpi-zereginak gehitzeko Google Tasks-ek ez ditu hainbat mailako azpi-zereginak onartzen Sartu izenburua Erakutsi azpi-zereginak diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 844ec1ff1..d9e4af047 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -551,8 +551,7 @@ est configuré correctement Réduire les sous-tâches Ajouter sous-tâche Sous-tâches - Activer la synchronisation pour ajouter des sous-tâches - Les sous-tâches multi-niveaux ne sont pas prises en charge par Google Tasks + Les sous-tâches multi-niveaux ne sont pas prises en charge par Google Tasks Entrer le titre Afficher les sous-tâches L\'affichage des sous-tâches dégradera les performances de l\'application diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 50a5c11e5..46de4f693 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -550,8 +550,7 @@ Deeltaken inklappen Deeltaak toevoegen Deeltaken - Zet synchronisatie aan om deeltaken toe te voegen - Deeltaken met meerdere niveau\'s worden niet ondersteund door Google Taken + Deeltaken met meerdere niveau\'s worden niet ondersteund door Google Taken Voer titel in Deeltaken tonen Deeltaken tonen vermindert prestaties van de app diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index faf5c8458..78f96681f 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -546,8 +546,7 @@ 缩回子任务列表 添加子任务 子任务 - 启用同步来添加子任务 - Google Tasks不支持多层子任务 + Google Tasks不支持多层子任务 输入标题 显示子任务 显示子任务将降级应用性能 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 77a2e7460..122843ddf 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -558,7 +558,6 @@ File %1$s contained %2$s.\n\n Let server schedule recurring tasks Expand subtasks Collapse subtasks - Enable synchronization to add subtasks Multi-level subtasks not supported by Google Tasks Enter title Show subtasks