From 1154ba4be44aaba93d014879382d8b49516e17ae Mon Sep 17 00:00:00 2001 From: Alex Baker Date: Wed, 23 Feb 2022 23:02:54 -0600 Subject: [PATCH] Store icalendar data on disk --- .../com.todoroo.astrid.dao.Database/82.json | 1370 +++++++++++++++++ .../todoroo/astrid/service/TaskMoverTest.kt | 7 + .../astrid/service/Upgrade_11_3_Test.kt | 35 +- .../java/org/tasks/TestUtilities.kt | 2 +- .../org/tasks/makers/CaldavCalendarMaker.kt | 24 + .../java/org/tasks/makers/CaldavTaskMaker.kt | 2 - .../java/com/todoroo/astrid/dao/Database.kt | 2 +- .../com/todoroo/astrid/service/TaskDeleter.kt | 17 +- .../com/todoroo/astrid/service/TaskMover.kt | 18 +- .../todoroo/astrid/service/Upgrade_11_3.kt | 16 +- .../todoroo/astrid/service/Upgrade_12_4.kt | 12 +- .../com/todoroo/astrid/service/Upgrader.kt | 16 +- .../java/org/tasks/backup/BackupContainer.kt | 21 +- .../org/tasks/backup/TasksJsonExporter.kt | 38 +- .../org/tasks/backup/TasksJsonImporter.kt | 11 +- .../org/tasks/caldav/CaldavSynchronizer.kt | 25 +- .../main/java/org/tasks/caldav/FileStorage.kt | 30 + .../main/java/org/tasks/caldav/VtodoCache.kt | 76 + .../main/java/org/tasks/caldav/iCalendar.kt | 16 +- .../main/java/org/tasks/data/CaldavTask.kt | 11 +- .../org/tasks/data/CaldavTaskContainer.kt | 3 - .../main/java/org/tasks/data/DeletionDao.kt | 1 - .../main/java/org/tasks/data/UpgraderDao.kt | 2 - app/src/main/java/org/tasks/db/Migrations.kt | 33 +- .../org/tasks/etebase/EtebaseSynchronizer.kt | 19 +- .../main/java/org/tasks/extensions/Cursor.kt | 6 + .../org/tasks/injection/ProductionModule.kt | 9 +- 27 files changed, 1743 insertions(+), 79 deletions(-) create mode 100644 app/schemas/com.todoroo.astrid.dao.Database/82.json create mode 100644 app/src/commonTest/java/org/tasks/makers/CaldavCalendarMaker.kt create mode 100644 app/src/main/java/org/tasks/caldav/FileStorage.kt create mode 100644 app/src/main/java/org/tasks/caldav/VtodoCache.kt create mode 100644 app/src/main/java/org/tasks/extensions/Cursor.kt diff --git a/app/schemas/com.todoroo.astrid.dao.Database/82.json b/app/schemas/com.todoroo.astrid.dao.Database/82.json new file mode 100644 index 000000000..feefe4b60 --- /dev/null +++ b/app/schemas/com.todoroo.astrid.dao.Database/82.json @@ -0,0 +1,1370 @@ +{ + "formatVersion": 1, + "database": { + "version": 82, + "identityHash": "882fac0c549a96e6a88e63f66e277fd0", + "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" + ], + "orders": [], + "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, `td_order` INTEGER NOT NULL)", + "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 + }, + { + "fieldPath": "order", + "columnName": "td_order", + "affinity": "INTEGER", + "notNull": true + } + ], + "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 NOT NULL, `title` TEXT, `importance` INTEGER NOT NULL, `dueDate` INTEGER NOT NULL, `hideUntil` INTEGER NOT NULL, `created` INTEGER NOT NULL, `modified` INTEGER NOT NULL, `completed` INTEGER NOT NULL, `deleted` INTEGER NOT NULL, `notes` TEXT, `estimatedSeconds` INTEGER NOT NULL, `elapsedSeconds` INTEGER NOT NULL, `timerStart` INTEGER NOT NULL, `notificationFlags` INTEGER NOT NULL, `notifications` INTEGER NOT NULL, `lastNotified` INTEGER NOT NULL, `snoozeTime` INTEGER NOT NULL, `recurrence` TEXT, `repeatUntil` INTEGER NOT NULL, `calendarUri` TEXT, `remoteId` TEXT, `collapsed` INTEGER NOT NULL, `parent` INTEGER NOT NULL, `parent_uuid` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "priority", + "columnName": "importance", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "dueDate", + "columnName": "dueDate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hideUntil", + "columnName": "hideUntil", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "creationDate", + "columnName": "created", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "modificationDate", + "columnName": "modified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "completionDate", + "columnName": "completed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "deletionDate", + "columnName": "deleted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notes", + "columnName": "notes", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "estimatedSeconds", + "columnName": "estimatedSeconds", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "elapsedSeconds", + "columnName": "elapsedSeconds", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "timerStart", + "columnName": "timerStart", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ringFlags", + "columnName": "notificationFlags", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reminderPeriod", + "columnName": "notifications", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reminderLast", + "columnName": "lastNotified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reminderSnooze", + "columnName": "snoozeTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "recurrence", + "columnName": "recurrence", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "repeatUntil", + "columnName": "repeatUntil", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "calendarURI", + "columnName": "calendarUri", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "remoteId", + "columnName": "remoteId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isCollapsed", + "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" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `t_rid` ON `${TABLE_NAME}` (`remoteId`)" + }, + { + "name": "active_and_visible", + "unique": false, + "columnNames": [ + "completed", + "deleted", + "hideUntil" + ], + "orders": [], + "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, `type` INTEGER NOT NULL DEFAULT 0, `repeat` INTEGER NOT NULL DEFAULT 0, `interval` INTEGER NOT NULL DEFAULT 0)", + "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 + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "repeat", + "columnName": "repeat", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "interval", + "columnName": "interval", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "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, `place_color` INTEGER NOT NULL, `place_icon` INTEGER NOT NULL, `place_order` INTEGER 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 + }, + { + "fieldPath": "color", + "columnName": "place_color", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "icon", + "columnName": "place_icon", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "place_order", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "place_id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "place_uid", + "unique": true, + "columnNames": [ + "uid" + ], + "orders": [], + "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": "isArrival", + "columnName": "arrival", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isDeparture", + "columnName": "departure", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "geofence_id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "geo_task", + "unique": false, + "columnNames": [ + "task" + ], + "orders": [], + "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" + ], + "orders": [], + "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": "isMoved", + "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" + ], + "orders": [], + "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" + ], + "orders": [], + "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, `f_order` INTEGER NOT NULL)", + "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 + }, + { + "fieldPath": "order", + "columnName": "f_order", + "affinity": "INTEGER", + "notNull": true + } + ], + "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": "order", + "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, `cdl_order` INTEGER NOT NULL, `cdl_access` INTEGER NOT NULL)", + "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 + }, + { + "fieldPath": "order", + "columnName": "cdl_order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "access", + "columnName": "cdl_access", + "affinity": "INTEGER", + "notNull": true + } + ], + "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_remote_parent` TEXT, `cd_order` INTEGER)", + "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": "remoteParent", + "columnName": "cd_remote_parent", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "order", + "columnName": "cd_order", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "cd_id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "cd_task", + "unique": false, + "columnNames": [ + "cd_task" + ], + "orders": [], + "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, `cda_encryption_key` TEXT, `cda_account_type` INTEGER NOT NULL, `cda_collapsed` INTEGER NOT NULL, `cda_server_type` 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": "isSuppressRepeatingTasks", + "columnName": "cda_repeat", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "encryptionKey", + "columnName": "cda_encryption_key", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "accountType", + "columnName": "cda_account_type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isCollapsed", + "columnName": "cda_collapsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "serverType", + "columnName": "cda_server_type", + "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, `gta_collapsed` INTEGER NOT NULL)", + "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 + }, + { + "fieldPath": "isCollapsed", + "columnName": "gta_collapsed", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "gta_id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "principals", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `account` INTEGER NOT NULL, `href` TEXT NOT NULL, `email` TEXT, `display_name` TEXT, FOREIGN KEY(`account`) REFERENCES `caldav_accounts`(`cda_id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "account", + "columnName": "account", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "href", + "columnName": "href", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "displayName", + "columnName": "display_name", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_principals_account_href", + "unique": true, + "columnNames": [ + "account", + "href" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_principals_account_href` ON `${TABLE_NAME}` (`account`, `href`)" + } + ], + "foreignKeys": [ + { + "table": "caldav_accounts", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "account" + ], + "referencedColumns": [ + "cda_id" + ] + } + ] + }, + { + "tableName": "principal_access", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `principal` INTEGER NOT NULL, `list` INTEGER NOT NULL, `invite` INTEGER NOT NULL, `access` INTEGER NOT NULL, FOREIGN KEY(`principal`) REFERENCES `principals`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`list`) REFERENCES `caldav_lists`(`cdl_id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "principal", + "columnName": "principal", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "list", + "columnName": "list", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "invite", + "columnName": "invite", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "access", + "columnName": "access", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_principal_access_list_principal", + "unique": true, + "columnNames": [ + "list", + "principal" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_principal_access_list_principal` ON `${TABLE_NAME}` (`list`, `principal`)" + }, + { + "name": "index_principal_access_principal", + "unique": false, + "columnNames": [ + "principal" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_principal_access_principal` ON `${TABLE_NAME}` (`principal`)" + } + ], + "foreignKeys": [ + { + "table": "principals", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "principal" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "caldav_lists", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "list" + ], + "referencedColumns": [ + "cdl_id" + ] + } + ] + } + ], + "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, '882fac0c549a96e6a88e63f66e277fd0')" + ] + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/todoroo/astrid/service/TaskMoverTest.kt b/app/src/androidTest/java/com/todoroo/astrid/service/TaskMoverTest.kt index 0f01808d7..8e0c88fa1 100644 --- a/app/src/androidTest/java/com/todoroo/astrid/service/TaskMoverTest.kt +++ b/app/src/androidTest/java/com/todoroo/astrid/service/TaskMoverTest.kt @@ -16,6 +16,8 @@ import org.tasks.data.GoogleTaskDao import org.tasks.injection.InjectingTestCase import org.tasks.injection.ProductionModule import org.tasks.jobs.WorkManager +import org.tasks.makers.CaldavCalendarMaker.UUID +import org.tasks.makers.CaldavCalendarMaker.newCaldavCalendar import org.tasks.makers.CaldavTaskMaker import org.tasks.makers.CaldavTaskMaker.CALENDAR import org.tasks.makers.CaldavTaskMaker.REMOTE_ID @@ -79,6 +81,7 @@ class TaskMoverTest : InjectingTestCase() { @Test fun moveBetweenCaldavList() = runBlocking { createTasks(1) + caldavDao.insert(newCaldavCalendar(with(UUID, "1"))) caldavDao.insert(newCaldavTask(with(CaldavTaskMaker.TASK, 1L), with(CALENDAR, "1"))) moveToCaldavList("2", 1) assertEquals("2", caldavDao.getTask(1)!!.calendar) @@ -87,6 +90,7 @@ class TaskMoverTest : InjectingTestCase() { @Test fun deleteCaldavTaskAfterMove() = runBlocking { createTasks(1) + caldavDao.insert(newCaldavCalendar(with(UUID, "1"))) caldavDao.insert(newCaldavTask(with(CaldavTaskMaker.TASK, 1L), with(CALENDAR, "1"))) moveToCaldavList("2", 1) val deleted = caldavDao.getMoved("1") @@ -100,6 +104,7 @@ class TaskMoverTest : InjectingTestCase() { createTasks(1) createSubtask(2, 1) createSubtask(3, 2) + caldavDao.insert(newCaldavCalendar(with(UUID, "1"))) caldavDao.insert( listOf( newCaldavTask( @@ -196,6 +201,7 @@ class TaskMoverTest : InjectingTestCase() { fun moveCaldavChildWithoutParent() = runBlocking { createTasks(1) createSubtask(2, 1) + caldavDao.insert(newCaldavCalendar(with(UUID, "1"))) caldavDao.insert( listOf( newCaldavTask( @@ -266,6 +272,7 @@ class TaskMoverTest : InjectingTestCase() { fun dontDuplicateWhenParentAndChildCaldavMoved() = runBlocking { createTasks(1) createSubtask(2, 1) + caldavDao.insert(newCaldavCalendar(with(UUID, "1"))) caldavDao.insert( listOf( newCaldavTask( diff --git a/app/src/androidTest/java/com/todoroo/astrid/service/Upgrade_11_3_Test.kt b/app/src/androidTest/java/com/todoroo/astrid/service/Upgrade_11_3_Test.kt index 77217bb3e..7cc3ec61b 100644 --- a/app/src/androidTest/java/com/todoroo/astrid/service/Upgrade_11_3_Test.kt +++ b/app/src/androidTest/java/com/todoroo/astrid/service/Upgrade_11_3_Test.kt @@ -7,17 +7,20 @@ import com.todoroo.astrid.data.Task import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.UninstallModules import kotlinx.coroutines.runBlocking +import org.junit.Before import org.junit.Test import org.tasks.SuspendFreeze.Companion.freezeAt import org.tasks.TestUtilities.assertEquals +import org.tasks.caldav.VtodoCache +import org.tasks.data.CaldavCalendar import org.tasks.data.CaldavDao import org.tasks.data.TaskDao import org.tasks.injection.InjectingTestCase import org.tasks.injection.ProductionModule +import org.tasks.makers.CaldavCalendarMaker.newCaldavCalendar import org.tasks.makers.CaldavTaskMaker.CALENDAR import org.tasks.makers.CaldavTaskMaker.REMOTE_ID import org.tasks.makers.CaldavTaskMaker.TASK -import org.tasks.makers.CaldavTaskMaker.VTODO import org.tasks.makers.CaldavTaskMaker.newCaldavTask import org.tasks.makers.TaskMaker.DUE_DATE import org.tasks.makers.TaskMaker.HIDE_TYPE @@ -34,11 +37,26 @@ class Upgrade_11_3_Test : InjectingTestCase() { @Inject lateinit var caldavDao: CaldavDao @Inject lateinit var openTaskDao: TestOpenTaskDao @Inject lateinit var upgrader: Upgrade_11_3 + @Inject lateinit var vtodoCache: VtodoCache + + private lateinit var calendar: CaldavCalendar + + @Before + override fun setUp() { + super.setUp() + calendar = newCaldavCalendar() + runBlocking { + caldavDao.insert(calendar) + } + } @Test fun applyRemoteiCalendarStartDate() = runBlocking { val taskId = taskDao.insert(newTask()) - caldavDao.insert(newCaldavTask(with(TASK, taskId), with(VTODO, VTODO_WITH_START_DATE))) + val caldavTask = newCaldavTask(with(TASK, taskId), with(CALENDAR, calendar.uuid)) + caldavDao.insert(caldavTask) + vtodoCache.putVtodo(calendar, caldavTask, VTODO_WITH_START_DATE) + upgrader.applyiCalendarStartDates() assertEquals(DateTime(2021, 1, 21), taskDao.fetch(taskId)?.hideUntil) @@ -50,7 +68,10 @@ class Upgrade_11_3_Test : InjectingTestCase() { with(DUE_DATE, DateTime(2021, 1, 20)), with(HIDE_TYPE, Task.HIDE_UNTIL_DUE) )) - caldavDao.insert(newCaldavTask(with(TASK, taskId), with(VTODO, VTODO_WITH_START_DATE))) + val caldavTask = newCaldavTask(with(TASK, taskId), with(CALENDAR, calendar.uuid)) + caldavDao.insert(caldavTask) + vtodoCache.putVtodo(calendar, caldavTask, VTODO_WITH_START_DATE) + upgrader.applyiCalendarStartDates() assertEquals(DateTime(2021, 1, 20), taskDao.fetch(taskId)?.hideUntil) @@ -64,7 +85,9 @@ class Upgrade_11_3_Test : InjectingTestCase() { with(HIDE_TYPE, Task.HIDE_UNTIL_DUE), with(MODIFICATION_TIME, DateTime(2021, 1, 21, 9, 50, 4, 348)) )) - caldavDao.insert(newCaldavTask(with(TASK, taskId), with(VTODO, VTODO_WITH_START_DATE))) + val caldavTask = newCaldavTask(with(TASK, taskId), with(CALENDAR, calendar.uuid)) + caldavDao.insert(caldavTask) + vtodoCache.putVtodo(calendar, caldavTask, VTODO_WITH_START_DATE) freezeAt(upgradeTime) { upgrader.applyiCalendarStartDates() @@ -77,7 +100,9 @@ class Upgrade_11_3_Test : InjectingTestCase() { fun dontTouchWhenNoiCalendarStartDate() = runBlocking { val modificationTime = DateTime(2021, 1, 21, 9, 50, 4, 348) val taskId = taskDao.insert(newTask(with(MODIFICATION_TIME, modificationTime))) - caldavDao.insert(newCaldavTask(with(TASK, taskId), with(VTODO, VTODO_NO_START_DATE))) + val caldavTask = newCaldavTask(with(TASK, taskId), with(CALENDAR, calendar.uuid)) + caldavDao.insert(caldavTask) + vtodoCache.putVtodo(calendar, caldavTask, VTODO_NO_START_DATE) upgrader.applyiCalendarStartDates() diff --git a/app/src/commonTest/java/org/tasks/TestUtilities.kt b/app/src/commonTest/java/org/tasks/TestUtilities.kt index 2ac21a004..8652d63ed 100644 --- a/app/src/commonTest/java/org/tasks/TestUtilities.kt +++ b/app/src/commonTest/java/org/tasks/TestUtilities.kt @@ -54,7 +54,7 @@ object TestUtilities { val vtodo = readFile(path) val remote = fromString(vtodo) task.applyRemote(remote, null) - return Triple(task, CaldavTask().apply { this.vtodo = vtodo }, remote) + return Triple(task, CaldavTask(), remote) } private fun fromResource(path: String): at.bitfire.ical4android.Task = diff --git a/app/src/commonTest/java/org/tasks/makers/CaldavCalendarMaker.kt b/app/src/commonTest/java/org/tasks/makers/CaldavCalendarMaker.kt new file mode 100644 index 000000000..bd3d70bc3 --- /dev/null +++ b/app/src/commonTest/java/org/tasks/makers/CaldavCalendarMaker.kt @@ -0,0 +1,24 @@ +package org.tasks.makers + +import com.natpryce.makeiteasy.Instantiator +import com.natpryce.makeiteasy.Property +import com.natpryce.makeiteasy.Property.newProperty +import com.natpryce.makeiteasy.PropertyValue +import org.tasks.data.CaldavCalendar +import org.tasks.makers.Maker.make + +object CaldavCalendarMaker { + val ACCOUNT: Property = newProperty() + val UUID: Property = newProperty() + + private val instantiator = Instantiator { lookup -> + val calendar = CaldavCalendar() + calendar.account = lookup.valueOf(ACCOUNT, "account") + calendar.uuid = lookup.valueOf(UUID, "uuid") + calendar + } + + fun newCaldavCalendar(vararg properties: PropertyValue): CaldavCalendar { + return make(instantiator, *properties) + } +} \ No newline at end of file diff --git a/app/src/commonTest/java/org/tasks/makers/CaldavTaskMaker.kt b/app/src/commonTest/java/org/tasks/makers/CaldavTaskMaker.kt index 050cfee62..35fa8d63b 100644 --- a/app/src/commonTest/java/org/tasks/makers/CaldavTaskMaker.kt +++ b/app/src/commonTest/java/org/tasks/makers/CaldavTaskMaker.kt @@ -13,7 +13,6 @@ object CaldavTaskMaker { val REMOTE_ID: Property = newProperty() val REMOTE_PARENT: Property = newProperty() val REMOTE_ORDER: Property = newProperty() - val VTODO: Property = newProperty() val ETAG: Property = newProperty() val OBJECT: Property = newProperty() @@ -21,7 +20,6 @@ object CaldavTaskMaker { val task = CaldavTask(it.valueOf(TASK, 1L), it.valueOf(CALENDAR, "calendar")) task.remoteId = it.valueOf(REMOTE_ID, task.remoteId) task.remoteParent = it.valueOf(REMOTE_PARENT, null as String?) - task.vtodo = it.valueOf(VTODO, null as String?) task.order = it.valueOf(REMOTE_ORDER, null as Long?) task.etag = it.valueOf(ETAG, null as String?) task.`object` = it.valueOf(OBJECT, task.remoteId?.let { id -> "$id.ics" }) diff --git a/app/src/main/java/com/todoroo/astrid/dao/Database.kt b/app/src/main/java/com/todoroo/astrid/dao/Database.kt index 6c9f7bcbe..cc58b1029 100644 --- a/app/src/main/java/com/todoroo/astrid/dao/Database.kt +++ b/app/src/main/java/com/todoroo/astrid/dao/Database.kt @@ -61,7 +61,7 @@ import org.tasks.notifications.NotificationDao Principal::class, PrincipalAccess::class ], - version = 81 + version = 82 ) abstract class Database : RoomDatabase() { abstract fun notificationDao(): NotificationDao diff --git a/app/src/main/java/com/todoroo/astrid/service/TaskDeleter.kt b/app/src/main/java/com/todoroo/astrid/service/TaskDeleter.kt index 0a67e58d4..632f0108b 100644 --- a/app/src/main/java/com/todoroo/astrid/service/TaskDeleter.kt +++ b/app/src/main/java/com/todoroo/astrid/service/TaskDeleter.kt @@ -5,13 +5,20 @@ import com.todoroo.astrid.data.Task import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.runBlocking import org.tasks.LocalBroadcastManager -import org.tasks.data.* +import org.tasks.caldav.VtodoCache +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.data.TaskContainer +import org.tasks.data.TaskDao import org.tasks.db.QueryUtils import org.tasks.db.SuspendDbUtils.chunkedMap import org.tasks.jobs.WorkManager import org.tasks.preferences.Preferences import org.tasks.sync.SyncAdapters -import java.util.* import javax.inject.Inject class TaskDeleter @Inject constructor( @@ -21,7 +28,9 @@ class TaskDeleter @Inject constructor( private val localBroadcastManager: LocalBroadcastManager, private val googleTaskDao: GoogleTaskDao, private val preferences: Preferences, - private val syncAdapters: SyncAdapters) { + private val syncAdapters: SyncAdapters, + private val vtodoCache: VtodoCache, + ) { suspend fun markDeleted(item: Task) = markDeleted(persistentListOf(item.id)) @@ -73,12 +82,14 @@ class TaskDeleter @Inject constructor( } suspend fun delete(list: CaldavCalendar) { + vtodoCache.delete(list) val tasks = deletionDao.delete(list) delete(tasks) localBroadcastManager.broadcastRefreshList() } suspend fun delete(list: CaldavAccount) { + vtodoCache.delete(list) val tasks = deletionDao.delete(list) delete(tasks) localBroadcastManager.broadcastRefreshList() diff --git a/app/src/main/java/com/todoroo/astrid/service/TaskMover.kt b/app/src/main/java/com/todoroo/astrid/service/TaskMover.kt index 237f13c3a..c9b312d81 100644 --- a/app/src/main/java/com/todoroo/astrid/service/TaskMover.kt +++ b/app/src/main/java/com/todoroo/astrid/service/TaskMover.kt @@ -9,11 +9,16 @@ import com.todoroo.astrid.data.Task import dagger.hilt.android.qualifiers.ApplicationContext import org.tasks.BuildConfig import org.tasks.LocalBroadcastManager -import org.tasks.data.* +import org.tasks.caldav.VtodoCache +import org.tasks.data.CaldavDao +import org.tasks.data.CaldavTask +import org.tasks.data.GoogleTask +import org.tasks.data.GoogleTaskDao +import org.tasks.data.GoogleTaskListDao +import org.tasks.data.TaskDao import org.tasks.db.DbUtils.dbchunk import org.tasks.preferences.Preferences import org.tasks.sync.SyncAdapters -import java.util.* import javax.inject.Inject class TaskMover @Inject constructor( @@ -24,7 +29,9 @@ class TaskMover @Inject constructor( private val googleTaskListDao: GoogleTaskListDao, private val preferences: Preferences, private val localBroadcastManager: LocalBroadcastManager, - private val syncAdapters: SyncAdapters) { + private val syncAdapters: SyncAdapters, + private val vtodoCache: VtodoCache, +) { suspend fun getSingleFilter(tasks: List): Filter? { val caldavCalendars = caldavDao.getCalendars(tasks) @@ -129,15 +136,16 @@ class TaskMover @Inject constructor( caldavDao.markDeleted(toDelete, DateUtilities.now()) when (selected) { is CaldavFilter -> { + val from = caldavDao.getCalendar(caldavTask.calendar!!) val id1 = caldavTask.task val listId = selected.uuid val newParent = CaldavTask(id1, listId, caldavTask.remoteId, caldavTask.`object`) - newParent.vtodo = caldavTask.vtodo + vtodoCache.move(from!!, selected.calendar, caldavTask) caldavDao.insert(task, newParent, preferences.addTasksToTop()) children.takeIf { it.isNotEmpty() } ?.map { val newChild = CaldavTask(it.task, listId, it.remoteId, it.`object`) - newChild.vtodo = it.vtodo + vtodoCache.move(from, selected.calendar, it) newChild.remoteParent = it.remoteParent newChild } diff --git a/app/src/main/java/com/todoroo/astrid/service/Upgrade_11_3.kt b/app/src/main/java/com/todoroo/astrid/service/Upgrade_11_3.kt index a1b6b451d..d2fcdf903 100644 --- a/app/src/main/java/com/todoroo/astrid/service/Upgrade_11_3.kt +++ b/app/src/main/java/com/todoroo/astrid/service/Upgrade_11_3.kt @@ -2,6 +2,7 @@ package com.todoroo.astrid.service +import org.tasks.caldav.VtodoCache import org.tasks.caldav.iCalendar import org.tasks.caldav.iCalendar.Companion.apply import org.tasks.data.OpenTaskDao @@ -12,16 +13,21 @@ import javax.inject.Inject class Upgrade_11_3 @Inject constructor( private val upgraderDao: UpgraderDao, private val openTaskDao: OpenTaskDao, - private val taskDao: TaskDao + private val taskDao: TaskDao, + private val vtodoCache: VtodoCache, ) { internal suspend fun applyiCalendarStartDates() { val (hasStartDate, noStartDate) = upgraderDao.tasksWithVtodos().partition { it.startDate > 0 } for (task in noStartDate) { - task.vtodo?.let { iCalendar.fromVtodo(it) }?.dtStart?.let { - it.apply(task.task) - upgraderDao.setStartDate(task.id, task.startDate) - } + vtodoCache + .getVtodo(task.caldavTask) + ?.let { iCalendar.fromVtodo(it) } + ?.dtStart + ?.let { + it.apply(task.task) + upgraderDao.setStartDate(task.id, task.startDate) + } } hasStartDate .map { it.id } diff --git a/app/src/main/java/com/todoroo/astrid/service/Upgrade_12_4.kt b/app/src/main/java/com/todoroo/astrid/service/Upgrade_12_4.kt index b1e4902e7..52623e4c8 100644 --- a/app/src/main/java/com/todoroo/astrid/service/Upgrade_12_4.kt +++ b/app/src/main/java/com/todoroo/astrid/service/Upgrade_12_4.kt @@ -2,9 +2,11 @@ package com.todoroo.astrid.service +import org.tasks.caldav.VtodoCache import org.tasks.caldav.iCalendar import org.tasks.caldav.iCalendar.Companion.reminders import org.tasks.data.AlarmDao +import org.tasks.data.CaldavTaskContainer import org.tasks.data.TaskDao import org.tasks.data.UpgraderDao import javax.inject.Inject @@ -13,15 +15,17 @@ class Upgrade_12_4 @Inject constructor( private val alarmDao: AlarmDao, private val taskDao: TaskDao, private val upgraderDao: UpgraderDao, + private val vtodoCache: VtodoCache, ) { internal suspend fun syncExistingAlarms() { val existingAlarms = alarmDao.getActiveAlarms() - upgraderDao.tasksWithVtodos().forEach { caldav -> - val remoteTask = caldav.vtodo?.let(iCalendar::fromVtodo) ?: return@forEach + upgraderDao.tasksWithVtodos().map(CaldavTaskContainer::caldavTask).forEach { task -> + val remoteTask = + vtodoCache.getVtodo(task)?.let(iCalendar::fromVtodo) ?: return@forEach remoteTask .reminders - .filter { existingAlarms.find { e -> e.task == caldav.id && e.same(it) } == null } - .onEach { it.task = caldav.id } + .filter { existingAlarms.find { e -> e.task == task.task && e.same(it) } == null } + .onEach { it.task = task.task } .let { alarmDao.insert(it) } } taskDao.touch(existingAlarms.map { it.task }.toSet().toList()) diff --git a/app/src/main/java/com/todoroo/astrid/service/Upgrader.kt b/app/src/main/java/com/todoroo/astrid/service/Upgrader.kt index 30fe0d2ee..87976ea9b 100644 --- a/app/src/main/java/com/todoroo/astrid/service/Upgrader.kt +++ b/app/src/main/java/com/todoroo/astrid/service/Upgrader.kt @@ -12,6 +12,7 @@ import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.runBlocking import org.tasks.R import org.tasks.Strings.isNullOrEmpty +import org.tasks.caldav.VtodoCache import org.tasks.caldav.iCalendar import org.tasks.caldav.iCalendar.Companion.fromVtodo import org.tasks.caldav.iCalendar.Companion.order @@ -57,6 +58,7 @@ class Upgrader @Inject constructor( private val widgetManager: AppWidgetManager, private val taskMover: TaskMover, private val upgraderDao: UpgraderDao, + private val vtodoCache: VtodoCache, private val upgrade_11_3: Lazy, private val upgrade_11_12_3: Lazy, private val upgrade_12_4: Lazy, @@ -150,10 +152,9 @@ class Upgrader @Inject constructor( return getAndroidColor(context, index) } - private suspend fun applyCaldavOrder() { for (task in upgraderDao.tasksWithVtodos().map(CaldavTaskContainer::caldavTask)) { - val remoteTask = fromVtodo(task.vtodo!!) ?: continue + val remoteTask = vtodoCache.getVtodo(task)?.let { fromVtodo(it) } ?: continue val order: Long? = remoteTask.order if (order != null) { task.order = order @@ -169,7 +170,7 @@ class Upgrader @Inject constructor( if (tasksWithLocations.contains(taskId)) { continue } - val remoteTask = fromVtodo(task.vtodo!!) ?: continue + val remoteTask = vtodoCache.getVtodo(task)?.let { fromVtodo(it) } ?: continue val geo = remoteTask.geoPosition ?: continue iCal.setPlace(taskId, geo) } @@ -179,7 +180,7 @@ class Upgrader @Inject constructor( private suspend fun applyCaldavSubtasks() { val updated: MutableList = ArrayList() for (task in upgraderDao.tasksWithVtodos().map(CaldavTaskContainer::caldavTask)) { - val remoteTask = fromVtodo(task.vtodo!!) ?: continue + val remoteTask = vtodoCache.getVtodo(task)?.let { fromVtodo(it) } ?: continue task.remoteParent = remoteTask.parent if (!isNullOrEmpty(task.remoteParent)) { updated.add(task) @@ -192,10 +193,9 @@ class Upgrader @Inject constructor( private suspend fun applyCaldavCategories() { val tasksWithTags: List = upgraderDao.tasksWithTags() for (container in upgraderDao.tasksWithVtodos()) { - val remoteTask = fromVtodo(container.vtodo!!) - if (remoteTask != null) { - tagDao.insert(container.task, iCal.getTags(remoteTask.categories)) - } + val remoteTask = + vtodoCache.getVtodo(container.caldavTask)?.let { fromVtodo(it) } ?: continue + tagDao.insert(container.task, iCal.getTags(remoteTask.categories)) } taskDao.touch(tasksWithTags) } diff --git a/app/src/main/java/org/tasks/backup/BackupContainer.kt b/app/src/main/java/org/tasks/backup/BackupContainer.kt index 6b6cfd8bf..5afd0041a 100644 --- a/app/src/main/java/org/tasks/backup/BackupContainer.kt +++ b/app/src/main/java/org/tasks/backup/BackupContainer.kt @@ -2,7 +2,21 @@ package org.tasks.backup import com.todoroo.astrid.data.Task import org.tasks.backup.TasksJsonImporter.LegacyLocation -import org.tasks.data.* +import org.tasks.data.Alarm +import org.tasks.data.CaldavAccount +import org.tasks.data.CaldavCalendar +import org.tasks.data.CaldavTask +import org.tasks.data.Filter +import org.tasks.data.Geofence +import org.tasks.data.GoogleTask +import org.tasks.data.GoogleTaskAccount +import org.tasks.data.GoogleTaskList +import org.tasks.data.Place +import org.tasks.data.Tag +import org.tasks.data.TagData +import org.tasks.data.TaskAttachment +import org.tasks.data.TaskListMetadata +import org.tasks.data.UserActivity class BackupContainer( val tasks: List?, @@ -28,8 +42,9 @@ class BackupContainer( val google: List, val comments: List, val attachments: List?, - val caldavTasks: List?) { - + val caldavTasks: List?, + val vtodo: String?, + ) { val locations: List = emptyList() } } \ No newline at end of file diff --git a/app/src/main/java/org/tasks/backup/TasksJsonExporter.kt b/app/src/main/java/org/tasks/backup/TasksJsonExporter.kt index becf77bbf..46f59b4c5 100755 --- a/app/src/main/java/org/tasks/backup/TasksJsonExporter.kt +++ b/app/src/main/java/org/tasks/backup/TasksJsonExporter.kt @@ -14,7 +14,19 @@ import com.todoroo.astrid.data.Task import org.tasks.BuildConfig import org.tasks.R import org.tasks.backup.BackupContainer.TaskBackup -import org.tasks.data.* +import org.tasks.caldav.VtodoCache +import org.tasks.data.AlarmDao +import org.tasks.data.CaldavDao +import org.tasks.data.FilterDao +import org.tasks.data.GoogleTaskDao +import org.tasks.data.GoogleTaskListDao +import org.tasks.data.LocationDao +import org.tasks.data.TagDao +import org.tasks.data.TagDataDao +import org.tasks.data.TaskAttachmentDao +import org.tasks.data.TaskDao +import org.tasks.data.TaskListMetadataDao +import org.tasks.data.UserActivityDao import org.tasks.date.DateTimeUtils.newDateTime import org.tasks.extensions.Context.toast import org.tasks.files.FileHelper @@ -26,7 +38,6 @@ import java.io.IOException import java.io.OutputStream import java.io.OutputStreamWriter import java.nio.charset.Charset -import java.util.* import javax.inject.Inject class TasksJsonExporter @Inject constructor( @@ -43,7 +54,9 @@ class TasksJsonExporter @Inject constructor( private val taskAttachmentDao: TaskAttachmentDao, private val caldavDao: CaldavDao, private val workManager: WorkManager, - private val taskListMetadataDao: TaskListMetadataDao) { + private val taskListMetadataDao: TaskListMetadataDao, + private val vtodoCache: VtodoCache, + ) { private var context: Context? = null private var exportCount = 0 @@ -108,16 +121,19 @@ class TasksJsonExporter @Inject constructor( for (task in tasks) { setProgress(taskBackups.size, tasks.size) val taskId = task.id + val caldavTasks = caldavDao.getTasks(taskId) taskBackups.add( TaskBackup( - task, - alarmDao.getAlarms(taskId), - locationDao.getGeofencesForTask(taskId), - tagDao.getTagsForTask(taskId), - googleTaskDao.getAllByTaskId(taskId), - userActivityDao.getCommentsForTask(task.uuid), - taskAttachmentDao.getAttachments(task.uuid), - caldavDao.getTasks(taskId))) + task, + alarmDao.getAlarms(taskId), + locationDao.getGeofencesForTask(taskId), + tagDao.getTagsForTask(taskId), + googleTaskDao.getAllByTaskId(taskId), + userActivityDao.getCommentsForTask(task.uuid), + taskAttachmentDao.getAttachments(task.uuid), + caldavTasks, + vtodoCache.getVtodo( caldavTasks.firstOrNull { !it.isDeleted() }) + )) } val data: MutableMap = HashMap() data["version"] = BuildConfig.VERSION_CODE diff --git a/app/src/main/java/org/tasks/backup/TasksJsonImporter.kt b/app/src/main/java/org/tasks/backup/TasksJsonImporter.kt index 5f75de541..18713a307 100644 --- a/app/src/main/java/org/tasks/backup/TasksJsonImporter.kt +++ b/app/src/main/java/org/tasks/backup/TasksJsonImporter.kt @@ -16,6 +16,7 @@ import com.todoroo.astrid.service.Upgrader.Companion.V6_4 import com.todoroo.astrid.service.Upgrader.Companion.getAndroidColor import org.tasks.LocalBroadcastManager import org.tasks.R +import org.tasks.caldav.VtodoCache import org.tasks.data.Alarm import org.tasks.data.Alarm.Companion.TYPE_SNOOZE import org.tasks.data.AlarmDao @@ -56,7 +57,9 @@ class TasksJsonImporter @Inject constructor( private val caldavDao: CaldavDao, private val preferences: Preferences, private val taskMover: TaskMover, - private val taskListMetadataDao: TaskListMetadataDao) { + private val taskListMetadataDao: TaskListMetadataDao, + private val vtodoCache: VtodoCache, + ) { private val result = ImportResult() @@ -219,6 +222,12 @@ class TasksJsonImporter @Inject constructor( caldavTask.task = taskId caldavDao.insert(caldavTask) } + backup.vtodo?.let { + val caldavTask = + backup.caldavTasks?.firstOrNull { t -> !t.isDeleted() } ?: return@let + val caldavCalendar = caldavDao.getCalendar(caldavTask.calendar!!) ?: return@let + vtodoCache.putVtodo(caldavCalendar, caldavTask, it) + } result.importCount++ } googleTaskDao.updateParents() diff --git a/app/src/main/java/org/tasks/caldav/CaldavSynchronizer.kt b/app/src/main/java/org/tasks/caldav/CaldavSynchronizer.kt index 8b15d0351..5fdd3a1a4 100644 --- a/app/src/main/java/org/tasks/caldav/CaldavSynchronizer.kt +++ b/app/src/main/java/org/tasks/caldav/CaldavSynchronizer.kt @@ -92,6 +92,7 @@ class CaldavSynchronizer @Inject constructor( private val provider: CaldavClientProvider, private val iCal: iCalendar, private val principalDao: PrincipalDao, + private val vtodoCache: VtodoCache, ) { suspend fun sync(account: CaldavAccount) { Thread.currentThread().contextClassLoader = context.classLoader @@ -293,11 +294,11 @@ class CaldavSynchronizer @Inject constructor( private suspend fun pushLocalChanges( caldavCalendar: CaldavCalendar, httpClient: OkHttpClient, httpUrl: HttpUrl) { for (task in caldavDao.getMoved(caldavCalendar.uuid!!)) { - deleteRemoteResource(httpClient, httpUrl, task) + deleteRemoteResource(httpClient, httpUrl, caldavCalendar, task) } for (task in taskDao.getCaldavTasksToPush(caldavCalendar.uuid!!)) { try { - pushTask(task, httpClient, httpUrl) + pushTask(caldavCalendar, task, httpClient, httpUrl) } catch (e: IOException) { Timber.e(e) } @@ -305,7 +306,11 @@ class CaldavSynchronizer @Inject constructor( } private suspend fun deleteRemoteResource( - httpClient: OkHttpClient, httpUrl: HttpUrl, caldavTask: CaldavTask): Boolean { + httpClient: OkHttpClient, + httpUrl: HttpUrl, + calendar: CaldavCalendar, + caldavTask: CaldavTask + ): Boolean { try { if (!isNullOrEmpty(caldavTask.`object`)) { val remote = DavResource( @@ -321,20 +326,26 @@ class CaldavSynchronizer @Inject constructor( Timber.e(e) return false } + vtodoCache.delete(calendar, caldavTask) caldavDao.delete(caldavTask) return true } - private suspend fun pushTask(task: Task, httpClient: OkHttpClient, httpUrl: HttpUrl) { + private suspend fun pushTask( + calendar: CaldavCalendar, + task: Task, + httpClient: OkHttpClient, + httpUrl: HttpUrl + ) { Timber.d("pushing %s", task) val caldavTask = caldavDao.getTask(task.id) ?: return if (task.isDeleted) { - if (deleteRemoteResource(httpClient, httpUrl, caldavTask)) { + if (deleteRemoteResource(httpClient, httpUrl, calendar, caldavTask)) { taskDeleter.delete(task) } return } - val data = iCal.toVtodo(caldavTask, task) + val data = iCal.toVtodo(calendar, caldavTask, task) val requestBody = RequestBody.create(MIME_ICALENDAR, data) try { val remote = DavResource( @@ -343,7 +354,7 @@ class CaldavSynchronizer @Inject constructor( val getETag = fromResponse(it) if (getETag != null && !isNullOrEmpty(getETag.eTag)) { caldavTask.etag = getETag.eTag - caldavTask.vtodo = String(data) + vtodoCache.putVtodo(calendar, caldavTask, String(data)) } } } catch (e: HttpException) { diff --git a/app/src/main/java/org/tasks/caldav/FileStorage.kt b/app/src/main/java/org/tasks/caldav/FileStorage.kt new file mode 100644 index 000000000..29d06945a --- /dev/null +++ b/app/src/main/java/org/tasks/caldav/FileStorage.kt @@ -0,0 +1,30 @@ +package org.tasks.caldav + +import android.content.Context +import dagger.hilt.android.qualifiers.ApplicationContext +import java.io.File +import javax.inject.Inject + +class FileStorage @Inject constructor( + @ApplicationContext context: Context +) { + val root = File(context.filesDir, "vtodo") + + @Suppress("NULLABILITY_MISMATCH_BASED_ON_JAVA_ANNOTATIONS") + fun getFile(vararg segments: String?): File? = + if (segments.none { it.isNullOrBlank() }) { + segments.fold(root) { f, p -> File(f, p) } + } else { + null + } + + fun read(file: File?): String? = file?.takeIf { it.exists() }?.readText() + + fun write(file: File, data: String?) { + if (data.isNullOrBlank()) { + file.delete() + } else { + file.writeText(data) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/tasks/caldav/VtodoCache.kt b/app/src/main/java/org/tasks/caldav/VtodoCache.kt new file mode 100644 index 000000000..68b13278b --- /dev/null +++ b/app/src/main/java/org/tasks/caldav/VtodoCache.kt @@ -0,0 +1,76 @@ +package org.tasks.caldav + +import org.tasks.data.CaldavAccount +import org.tasks.data.CaldavCalendar +import org.tasks.data.CaldavDao +import org.tasks.data.CaldavTask +import java.io.File +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class VtodoCache @Inject constructor( + private val caldavDao: CaldavDao, + private val fileStorage: FileStorage, +) { + fun move(from: CaldavCalendar, to: CaldavCalendar, task: CaldavTask) { + val source = + fileStorage.getFile(from.account, from.uuid, task.`object`) + if (source?.exists() != true) { + return + } + val target = + fileStorage.getFile(to.account, to.uuid) + ?.apply { mkdirs() } + ?.let { File(it, task.`object`!!) } + ?: return + source.copyTo(target, overwrite = true) + source.delete() + } + + suspend fun getVtodo(caldavTask: CaldavTask?): String? { + if (caldavTask == null) { + return null + } + val calendar = caldavDao.getCalendar(caldavTask.calendar!!) ?: return null + return getVtodo(calendar, caldavTask) + } + + fun getVtodo(calendar: CaldavCalendar?, caldavTask: CaldavTask?): String? { + val file = fileStorage.getFile( + calendar?.account, + caldavTask?.calendar, + caldavTask?.`object` + ) + return fileStorage.read(file) + } + + fun putVtodo(calendar: CaldavCalendar, caldavTask: CaldavTask, vtodo: String?) { + val directory = + fileStorage + .getFile(calendar.account, caldavTask.calendar) + ?.apply { mkdirs() } + ?: return + fileStorage.write(File(directory, caldavTask.`object`!!), vtodo) + } + + suspend fun delete(taskIds: List) { + val tasks = caldavDao.getTasks(taskIds).groupBy { it.calendar!! } + tasks.forEach { (c, t) -> + val calendar = caldavDao.getCalendar(c) ?: return@forEach + t.forEach { delete(calendar, it) } + } + } + + fun delete(calendar: CaldavCalendar, caldavTask: CaldavTask) { + fileStorage + .getFile(calendar.account!!, caldavTask.calendar!!, caldavTask.`object`!!) + ?.delete() + } + + fun delete(calendar: CaldavCalendar) = + fileStorage.getFile(calendar.account!!, calendar.uuid!!)?.deleteRecursively() + + fun delete(account: CaldavAccount) = + fileStorage.getFile(account.uuid!!)?.deleteRecursively() +} \ No newline at end of file diff --git a/app/src/main/java/org/tasks/caldav/iCalendar.kt b/app/src/main/java/org/tasks/caldav/iCalendar.kt index 9640b2166..571aa1231 100644 --- a/app/src/main/java/org/tasks/caldav/iCalendar.kt +++ b/app/src/main/java/org/tasks/caldav/iCalendar.kt @@ -76,6 +76,7 @@ class iCalendar @Inject constructor( private val caldavDao: CaldavDao, private val alarmDao: AlarmDao, private val alarmService: AlarmService, + private val vtodoCache: VtodoCache, ) { suspend fun setPlace(taskId: Long, geo: Geo?) { @@ -123,11 +124,16 @@ class iCalendar @Inject constructor( return tags } - suspend fun toVtodo(caldavTask: CaldavTask, task: com.todoroo.astrid.data.Task): ByteArray { + suspend fun toVtodo( + calendar: CaldavCalendar, + caldavTask: CaldavTask, + task: com.todoroo.astrid.data.Task + ): ByteArray { var remoteModel: Task? = null try { - if (!isNullOrEmpty(caldavTask.vtodo)) { - remoteModel = fromVtodo(caldavTask.vtodo!!) + val vtodo = vtodoCache.getVtodo(calendar, caldavTask) + if (vtodo?.isNotBlank() == true) { + remoteModel = fromVtodo(vtodo) } } catch (e: java.lang.Exception) { Timber.e(e) @@ -180,7 +186,7 @@ class iCalendar @Inject constructor( } val caldavTask = existing ?: CaldavTask(task.id, calendar.uuid, remote.uid, obj) val dirty = task.modificationDate > caldavTask.lastSync || caldavTask.lastSync == 0L - val local = caldavTask.vtodo?.let { fromVtodo(it) } + val local = vtodoCache.getVtodo(calendar, caldavTask)?.let { fromVtodo(it) } task.applyRemote(remote, local) caldavTask.applyRemote(remote, local) @@ -207,7 +213,7 @@ class iCalendar @Inject constructor( task.suppressSync() task.suppressRefresh() taskDao.save(task) - caldavTask.vtodo = vtodo + vtodoCache.putVtodo(calendar, caldavTask, vtodo) caldavTask.etag = eTag if (!dirty) { caldavTask.lastSync = task.modificationDate diff --git a/app/src/main/java/org/tasks/data/CaldavTask.kt b/app/src/main/java/org/tasks/data/CaldavTask.kt index 8e29c45fb..106bc36c5 100644 --- a/app/src/main/java/org/tasks/data/CaldavTask.kt +++ b/app/src/main/java/org/tasks/data/CaldavTask.kt @@ -1,6 +1,10 @@ package org.tasks.data -import androidx.room.* +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.Ignore +import androidx.room.Index +import androidx.room.PrimaryKey import com.todoroo.andlib.data.Table import com.todoroo.astrid.helper.UUIDHelper @@ -34,9 +38,6 @@ class CaldavTask { @ColumnInfo(name = "cd_deleted") var deleted: Long = 0 - @ColumnInfo(name = "cd_vtodo") - var vtodo: String? = null - @ColumnInfo(name = "cd_remote_parent") var remoteParent: String? = null @@ -65,7 +66,7 @@ class CaldavTask { fun isDeleted() = deleted > 0 override fun toString(): String = - "CaldavTask(id=$id, task=$task, calendar=$calendar, `object`=$`object`, remoteId=$remoteId, etag=$etag, lastSync=$lastSync, deleted=$deleted, vtodo=$vtodo, remoteParent=$remoteParent, order=$order)" + "CaldavTask(id=$id, task=$task, calendar=$calendar, `object`=$`object`, remoteId=$remoteId, etag=$etag, lastSync=$lastSync, deleted=$deleted, remoteParent=$remoteParent, order=$order)" companion object { const val KEY = "caldav" diff --git a/app/src/main/java/org/tasks/data/CaldavTaskContainer.kt b/app/src/main/java/org/tasks/data/CaldavTaskContainer.kt index c511113a8..9d40f5d64 100644 --- a/app/src/main/java/org/tasks/data/CaldavTaskContainer.kt +++ b/app/src/main/java/org/tasks/data/CaldavTaskContainer.kt @@ -17,9 +17,6 @@ class CaldavTaskContainer { val isDeleted: Boolean get() = task.isDeleted - val vtodo: String? - get() = caldavTask.vtodo - val sortOrder: Long get() = caldavTask.order ?: DateTime(task.creationDate).toAppleEpoch() diff --git a/app/src/main/java/org/tasks/data/DeletionDao.kt b/app/src/main/java/org/tasks/data/DeletionDao.kt index 1b6cf4b59..f392e0f1b 100644 --- a/app/src/main/java/org/tasks/data/DeletionDao.kt +++ b/app/src/main/java/org/tasks/data/DeletionDao.kt @@ -7,7 +7,6 @@ import androidx.room.Transaction import org.tasks.data.CaldavDao.Companion.LOCAL import org.tasks.db.SuspendDbUtils.chunkedMap import org.tasks.db.SuspendDbUtils.eachChunk -import java.util.* @Dao abstract class DeletionDao { diff --git a/app/src/main/java/org/tasks/data/UpgraderDao.kt b/app/src/main/java/org/tasks/data/UpgraderDao.kt index 92e20c241..7dbaac479 100644 --- a/app/src/main/java/org/tasks/data/UpgraderDao.kt +++ b/app/src/main/java/org/tasks/data/UpgraderDao.kt @@ -10,8 +10,6 @@ SELECT task.*, caldav_task.* FROM tasks AS task INNER JOIN caldav_tasks AS caldav_task ON _id = cd_task WHERE cd_deleted = 0 - AND cd_vtodo IS NOT NULL - AND cd_vtodo != '' """) suspend fun tasksWithVtodos(): List diff --git a/app/src/main/java/org/tasks/db/Migrations.kt b/app/src/main/java/org/tasks/db/Migrations.kt index 0be765c2e..4bf053260 100644 --- a/app/src/main/java/org/tasks/db/Migrations.kt +++ b/app/src/main/java/org/tasks/db/Migrations.kt @@ -9,11 +9,13 @@ import com.todoroo.astrid.data.Task.Companion.NOTIFY_AT_DEADLINE import com.todoroo.astrid.data.Task.Companion.NOTIFY_AT_START import com.todoroo.astrid.data.Task.Companion.NOTIFY_MODE_FIVE import com.todoroo.astrid.data.Task.Companion.NOTIFY_MODE_NONSTOP +import org.tasks.caldav.FileStorage import org.tasks.data.Alarm.Companion.TYPE_RANDOM import org.tasks.data.Alarm.Companion.TYPE_REL_END import org.tasks.data.Alarm.Companion.TYPE_REL_START import org.tasks.data.Alarm.Companion.TYPE_SNOOZE import org.tasks.data.CaldavAccount.Companion.SERVER_UNKNOWN +import org.tasks.extensions.getString import timber.log.Timber import java.util.concurrent.TimeUnit.HOURS @@ -443,7 +445,35 @@ object Migrations { } } - val MIGRATIONS = arrayOf( + @Suppress("FunctionName") + private fun migration_81_82(fileStorage: FileStorage) = object : Migration(81, 82) { + override fun migrate(database: SupportSQLiteDatabase) { + database + .query("SELECT `cdl_account`, `cd_calendar`, `cd_object`, `cd_vtodo` FROM `caldav_tasks` INNER JOIN `caldav_lists` ON `cdl_uuid` = `cd_calendar`") + .use { + while (it.moveToNext()) { + val file = fileStorage.getFile( + it.getString("cdl_account"), + it.getString("cd_calendar"), + it.getString("cd_object"), + ) ?: continue + fileStorage.write(file, it.getString("cd_vtodo")) + } + } + 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_remote_parent` TEXT, `cd_order` INTEGER)" + ) + database.execSQL("DROP INDEX `cd_task`") + database.execSQL("CREATE INDEX IF NOT EXISTS `cd_task` ON `caldav_tasks` (`cd_task`)") + 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_remote_parent`, `cd_order`) SELECT `cd_id`, `cd_task`, `cd_calendar`, `cd_object`, `cd_remote_id`, `cd_etag`, `cd_last_sync`, `cd_deleted`, `cd_remote_parent`, `cd_order` FROM `caldav_tasks-temp`" + ) + database.execSQL("DROP TABLE `caldav_tasks-temp`") + } + } + + fun migrations(fileStorage: FileStorage) = arrayOf( MIGRATION_35_36, MIGRATION_36_37, MIGRATION_37_38, @@ -481,6 +511,7 @@ object Migrations { MIGRATION_78_79, MIGRATION_79_80, MIGRATION_80_81, + migration_81_82(fileStorage), ) private fun noop(from: Int, to: Int): Migration = object : Migration(from, to) { diff --git a/app/src/main/java/org/tasks/etebase/EtebaseSynchronizer.kt b/app/src/main/java/org/tasks/etebase/EtebaseSynchronizer.kt index 6de7c5d55..f77fb34ca 100644 --- a/app/src/main/java/org/tasks/etebase/EtebaseSynchronizer.kt +++ b/app/src/main/java/org/tasks/etebase/EtebaseSynchronizer.kt @@ -19,6 +19,7 @@ import org.tasks.LocalBroadcastManager import org.tasks.R import org.tasks.Strings.isNullOrEmpty import org.tasks.billing.Inventory +import org.tasks.caldav.VtodoCache import org.tasks.caldav.iCalendar import org.tasks.caldav.iCalendar.Companion.fromVtodo import org.tasks.data.CaldavAccount @@ -35,7 +36,9 @@ class EtebaseSynchronizer @Inject constructor( private val taskDeleter: TaskDeleter, private val inventory: Inventory, private val clientProvider: EtebaseClientProvider, - private val iCal: iCalendar) { + private val iCal: iCalendar, + private val vtodoCache: VtodoCache, +) { companion object { init { prodId = ProdId("+//IDN tasks.org//android-" + BuildConfig.VERSION_CODE + "//EN") @@ -142,7 +145,10 @@ class EtebaseSynchronizer @Inject constructor( for (caldavTask in caldavDao.getMoved(caldavCalendar.uuid!!)) { client.deleteItem(collection, caldavTask) ?.let { changes.add(it) } - ?: caldavDao.delete(caldavTask) + ?: run { + vtodoCache.delete(caldavCalendar, caldavTask) + caldavDao.delete(caldavTask) + } } for (change in caldavDao.getCaldavTasksToPush(caldavCalendar.uuid!!)) { val task = change.task @@ -154,7 +160,11 @@ class EtebaseSynchronizer @Inject constructor( ?: taskDeleter.delete(task) } else { changes.add( - client.updateItem(collection, caldavTask, iCal.toVtodo(caldavTask, task)) + client.updateItem( + collection, + caldavTask, + iCal.toVtodo(caldavCalendar, caldavTask, task) + ) ) } } @@ -178,6 +188,7 @@ class EtebaseSynchronizer @Inject constructor( if (item.isDeleted) { if (caldavTask != null) { if (caldavTask.isDeleted()) { + vtodoCache.delete(caldavCalendar, caldavTask) caldavDao.delete(caldavTask) } else { taskDeleter.delete(caldavTask.task) @@ -185,7 +196,7 @@ class EtebaseSynchronizer @Inject constructor( } } else if (isLocalChange) { caldavTask?.let { - it.vtodo = vtodo + vtodoCache.putVtodo(caldavCalendar, it, vtodo) it.lastSync = item.meta.mtime ?: currentTimeMillis() caldavDao.update(it) } diff --git a/app/src/main/java/org/tasks/extensions/Cursor.kt b/app/src/main/java/org/tasks/extensions/Cursor.kt new file mode 100644 index 000000000..0985599c9 --- /dev/null +++ b/app/src/main/java/org/tasks/extensions/Cursor.kt @@ -0,0 +1,6 @@ +package org.tasks.extensions + +import android.database.Cursor + +fun Cursor.getString(columnName: String): String? = + getColumnIndex(columnName).takeIf { it >= 0 }?.let { getString(it) } diff --git a/app/src/main/java/org/tasks/injection/ProductionModule.kt b/app/src/main/java/org/tasks/injection/ProductionModule.kt index 447a911af..5f93318e6 100644 --- a/app/src/main/java/org/tasks/injection/ProductionModule.kt +++ b/app/src/main/java/org/tasks/injection/ProductionModule.kt @@ -10,6 +10,7 @@ import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent import org.tasks.BuildConfig import org.tasks.R +import org.tasks.caldav.FileStorage import org.tasks.data.CaldavDao import org.tasks.data.GoogleTaskListDao import org.tasks.data.OpenTaskDao @@ -26,9 +27,13 @@ import javax.inject.Singleton internal class ProductionModule { @Provides @Singleton - fun getAppDatabase(@ApplicationContext context: Context, preferences: Preferences): Database { + fun getAppDatabase( + @ApplicationContext context: Context, + preferences: Preferences, + fileStorage: FileStorage, + ): Database { val builder = Room.databaseBuilder(context, Database::class.java, Database.NAME) - .addMigrations(*Migrations.MIGRATIONS) + .addMigrations(*Migrations.migrations(fileStorage)) if (!BuildConfig.DEBUG || !preferences.getBoolean(R.string.p_crash_main_queries, false)) { builder.allowMainThreadQueries() }