diff --git a/app/schemas/com.todoroo.astrid.dao.Database/80.json b/app/schemas/com.todoroo.astrid.dao.Database/80.json new file mode 100644 index 000000000..64159bb6f --- /dev/null +++ b/app/schemas/com.todoroo.astrid.dao.Database/80.json @@ -0,0 +1,1343 @@ +{ + "formatVersion": 1, + "database": { + "version": 80, + "identityHash": "ce438be4d82f2eccb7df7ba50ea18d8d", + "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, `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": "reminderFlags", + "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" + ], + "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, `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" + ], + "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" + ], + "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": "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" + ], + "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, `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_vtodo` TEXT, `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": "vtodo", + "columnName": "cd_vtodo", + "affinity": "TEXT", + "notNull": false + }, + { + "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" + ], + "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" + ], + "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" + ], + "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" + ], + "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, 'ce438be4d82f2eccb7df7ba50ea18d8d')" + ] + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/org/tasks/caldav/SharingMailboxDotOrgTest.kt b/app/src/androidTest/java/org/tasks/caldav/SharingMailboxDotOrgTest.kt index a4583329f..1bda05075 100644 --- a/app/src/androidTest/java/org/tasks/caldav/SharingMailboxDotOrgTest.kt +++ b/app/src/androidTest/java/org/tasks/caldav/SharingMailboxDotOrgTest.kt @@ -65,10 +65,10 @@ class SharingMailboxDotOrgTest : CaldavTest() { val principal = principalDao.getAll().first() assertEquals(calendar.id, principal.list) - assertEquals("/principals/users/5", principal.principal) + assertEquals("/principals/users/5", principal.href) assertNull(principal.displayName) assertEquals(CaldavCalendar.INVITE_ACCEPTED, principal.inviteStatus) - assertEquals(CaldavCalendar.ACCESS_UNKNOWN, principal.access) + assertEquals(CaldavCalendar.ACCESS_UNKNOWN, principal.access.access) } companion object { diff --git a/app/src/androidTest/java/org/tasks/caldav/SharingOwncloudTest.kt b/app/src/androidTest/java/org/tasks/caldav/SharingOwncloudTest.kt index 9570d26ff..ea3f12c45 100644 --- a/app/src/androidTest/java/org/tasks/caldav/SharingOwncloudTest.kt +++ b/app/src/androidTest/java/org/tasks/caldav/SharingOwncloudTest.kt @@ -80,10 +80,10 @@ class SharingOwncloudTest : CaldavTest() { .first() assertEquals(calendar.id, principal.list) - assertEquals("principal:principals/users/user2", principal.principal) - assertEquals("user2", principal.displayName) + assertEquals("principal:principals/users/user2", principal.href) + assertEquals("user2", principal.name) assertEquals(CaldavCalendar.INVITE_ACCEPTED, principal.inviteStatus) - assertEquals(CaldavCalendar.ACCESS_READ_ONLY, principal.access) + assertEquals(ACCESS_READ_ONLY, principal.access.access) } @Test @@ -104,10 +104,10 @@ class SharingOwncloudTest : CaldavTest() { .first() assertEquals(calendar.id, principal.list) - assertEquals("principals/users/user1", principal.principal) + assertEquals("principals/users/user1", principal.href) assertEquals(null, principal.displayName) assertEquals(CaldavCalendar.INVITE_ACCEPTED, principal.inviteStatus) - assertEquals(CaldavCalendar.ACCESS_OWNER, principal.access) + assertEquals(ACCESS_OWNER, principal.access.access) } companion object { diff --git a/app/src/androidTest/java/org/tasks/caldav/SharingSabredavTest.kt b/app/src/androidTest/java/org/tasks/caldav/SharingSabredavTest.kt index 640e291e2..08aded864 100644 --- a/app/src/androidTest/java/org/tasks/caldav/SharingSabredavTest.kt +++ b/app/src/androidTest/java/org/tasks/caldav/SharingSabredavTest.kt @@ -102,10 +102,10 @@ class SharingSabredavTest : CaldavTest() { val principal = principalDao.getAll().first() assertEquals(calendar.id, principal.list) - assertEquals("mailto:user@example.com", principal.principal) + assertEquals("mailto:user@example.com", principal.href) assertEquals("Example User", principal.displayName) assertEquals(INVITE_ACCEPTED, principal.inviteStatus) - assertEquals(ACCESS_READ_WRITE, principal.access) + assertEquals(ACCESS_READ_WRITE, principal.access.access) } @Test @@ -126,10 +126,10 @@ class SharingSabredavTest : CaldavTest() { .first() assertEquals(calendar.id, principal.list) - assertEquals("/principals/user1", principal.principal) + assertEquals("/principals/user1", principal.href) assertEquals(null, principal.displayName) assertEquals(INVITE_ACCEPTED, principal.inviteStatus) - assertEquals(ACCESS_OWNER, principal.access) + assertEquals(ACCESS_OWNER, principal.access.access) } companion object { 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 9c2dafb81..d64f046dc 100644 --- a/app/src/main/java/com/todoroo/astrid/dao/Database.kt +++ b/app/src/main/java/com/todoroo/astrid/dao/Database.kt @@ -3,8 +3,39 @@ package com.todoroo.astrid.dao import androidx.room.Database import androidx.room.RoomDatabase import com.todoroo.astrid.data.Task -import org.tasks.data.* +import org.tasks.data.Alarm +import org.tasks.data.AlarmDao +import org.tasks.data.CaldavAccount +import org.tasks.data.CaldavCalendar +import org.tasks.data.CaldavDao +import org.tasks.data.CaldavTask +import org.tasks.data.ContentProviderDao +import org.tasks.data.DeletionDao +import org.tasks.data.Filter +import org.tasks.data.FilterDao +import org.tasks.data.Geofence +import org.tasks.data.GoogleTask +import org.tasks.data.GoogleTaskAccount +import org.tasks.data.GoogleTaskDao +import org.tasks.data.GoogleTaskList +import org.tasks.data.GoogleTaskListDao +import org.tasks.data.LocationDao +import org.tasks.data.Place +import org.tasks.data.Principal +import org.tasks.data.PrincipalAccess +import org.tasks.data.PrincipalDao +import org.tasks.data.Tag +import org.tasks.data.TagDao +import org.tasks.data.TagData +import org.tasks.data.TagDataDao +import org.tasks.data.TaskAttachment +import org.tasks.data.TaskAttachmentDao import org.tasks.data.TaskDao +import org.tasks.data.TaskListMetadata +import org.tasks.data.TaskListMetadataDao +import org.tasks.data.UpgraderDao +import org.tasks.data.UserActivity +import org.tasks.data.UserActivityDao import org.tasks.notifications.Notification import org.tasks.notifications.NotificationDao @@ -28,8 +59,9 @@ import org.tasks.notifications.NotificationDao CaldavAccount::class, GoogleTaskAccount::class, Principal::class, + PrincipalAccess::class ], - version = 79) + version = 80) abstract class Database : RoomDatabase() { abstract fun notificationDao(): NotificationDao abstract val tagDataDao: TagDataDao diff --git a/app/src/main/java/org/tasks/caldav/CaldavCalendarSettingsActivity.kt b/app/src/main/java/org/tasks/caldav/CaldavCalendarSettingsActivity.kt index 7cc1c4fdd..86660d681 100644 --- a/app/src/main/java/org/tasks/caldav/CaldavCalendarSettingsActivity.kt +++ b/app/src/main/java/org/tasks/caldav/CaldavCalendarSettingsActivity.kt @@ -24,8 +24,8 @@ import org.tasks.data.CaldavAccount.Companion.SERVER_TASKS import org.tasks.data.CaldavCalendar import org.tasks.data.CaldavCalendar.Companion.ACCESS_OWNER import org.tasks.data.Principal -import org.tasks.data.Principal.Companion.name import org.tasks.data.PrincipalDao +import org.tasks.data.PrincipalWithAccess import javax.inject.Inject @AndroidEntryPoint @@ -94,7 +94,7 @@ class CaldavCalendarSettingsActivity : BaseCaldavCalendarSettingsActivity() { private val canRemovePrincipals: Boolean get() = caldavCalendar?.access == ACCESS_OWNER && caldavAccount.canRemovePrincipal - private fun onRemove(principal: Principal) { + private fun onRemove(principal: PrincipalWithAccess) { if (requestInProgress()) { return } @@ -106,7 +106,7 @@ class CaldavCalendarSettingsActivity : BaseCaldavCalendarSettingsActivity() { .show() } - private fun removePrincipal(principal: Principal) = lifecycleScope.launch { + private fun removePrincipal(principal: PrincipalWithAccess) = lifecycleScope.launch { try { viewModel.removeUser(caldavAccount, caldavCalendar!!, principal) } catch (e: Exception) { diff --git a/app/src/main/java/org/tasks/caldav/CaldavCalendarViewModel.kt b/app/src/main/java/org/tasks/caldav/CaldavCalendarViewModel.kt index a668ffa41..63499f381 100644 --- a/app/src/main/java/org/tasks/caldav/CaldavCalendarViewModel.kt +++ b/app/src/main/java/org/tasks/caldav/CaldavCalendarViewModel.kt @@ -17,8 +17,8 @@ import org.tasks.data.CaldavCalendar import org.tasks.data.CaldavCalendar.Companion.ACCESS_READ_WRITE import org.tasks.data.CaldavCalendar.Companion.INVITE_UNKNOWN import org.tasks.data.CaldavDao -import org.tasks.data.Principal import org.tasks.data.PrincipalDao +import org.tasks.data.PrincipalWithAccess import org.tasks.sync.SyncAdapters import timber.log.Timber import javax.inject.Inject @@ -103,21 +103,17 @@ class CaldavCalendarViewModel @Inject constructor( withContext(Dispatchers.IO) { provider.forAccount(account, list.url!!).share(account, href) } - principalDao.insert(Principal().apply { - this.list = list.id - principal = href - inviteStatus = INVITE_UNKNOWN - access = ACCESS_READ_WRITE - }) + val principal = principalDao.getOrCreatePrincipal(account, href) + principalDao.getOrCreateAccess(list, principal, INVITE_UNKNOWN, ACCESS_READ_WRITE) syncAdapters.sync(true) } - suspend fun removeUser(account: CaldavAccount, list: CaldavCalendar, principal: Principal) = + suspend fun removeUser(account: CaldavAccount, list: CaldavCalendar, principal: PrincipalWithAccess) = doRequest { withContext(Dispatchers.IO) { - provider.forAccount(account).removePrincipal(account, list, principal) + provider.forAccount(account).removePrincipal(account, list, principal.href) } - principalDao.delete(principal) + principalDao.delete(principal.access) } private suspend fun doRequest(action: suspend () -> T): T? = diff --git a/app/src/main/java/org/tasks/caldav/CaldavClient.kt b/app/src/main/java/org/tasks/caldav/CaldavClient.kt index e435c6410..7fbbcf313 100644 --- a/app/src/main/java/org/tasks/caldav/CaldavClient.kt +++ b/app/src/main/java/org/tasks/caldav/CaldavClient.kt @@ -43,7 +43,6 @@ import org.tasks.data.CaldavAccount.Companion.SERVER_OWNCLOUD import org.tasks.data.CaldavAccount.Companion.SERVER_SABREDAV import org.tasks.data.CaldavAccount.Companion.SERVER_TASKS import org.tasks.data.CaldavCalendar -import org.tasks.data.Principal import org.tasks.ui.DisplayableException import org.xmlpull.v1.XmlPullParserException import org.xmlpull.v1.XmlPullParserFactory @@ -277,37 +276,37 @@ open class CaldavClient( suspend fun removePrincipal( account: CaldavAccount, calendar: CaldavCalendar, - principal: Principal, + href: String, ) { when (account.serverType) { - SERVER_TASKS, SERVER_SABREDAV -> removeSabrePrincipal(calendar, principal) - SERVER_OWNCLOUD -> removeOwncloudPrincipal(calendar, principal) + SERVER_TASKS, SERVER_SABREDAV -> removeSabrePrincipal(calendar, href) + SERVER_OWNCLOUD -> removeOwncloudPrincipal(calendar, href) else -> throw IllegalArgumentException() } } - private suspend fun removeOwncloudPrincipal(calendar: CaldavCalendar, principal: Principal) = + private suspend fun removeOwncloudPrincipal(calendar: CaldavCalendar, href: String) = withContext(Dispatchers.IO) { DavCollection(httpClient, calendar.url!!.toHttpUrl()) .post( """ - ${principal.principal} + $href """.trimIndent().toRequestBody(MIME_XML) ) {} } - private suspend fun removeSabrePrincipal(calendar: CaldavCalendar, principal: Principal) = + private suspend fun removeSabrePrincipal(calendar: CaldavCalendar, href: String) = withContext(Dispatchers.IO) { DavCollection(httpClient, calendar.url!!.toHttpUrl()) .post( """ - ${principal.principal} + $href diff --git a/app/src/main/java/org/tasks/caldav/CaldavSynchronizer.kt b/app/src/main/java/org/tasks/caldav/CaldavSynchronizer.kt index cf742fbf9..feecef9d5 100644 --- a/app/src/main/java/org/tasks/caldav/CaldavSynchronizer.kt +++ b/app/src/main/java/org/tasks/caldav/CaldavSynchronizer.kt @@ -180,12 +180,8 @@ class CaldavSynchronizer @Inject constructor( localBroadcastManager.broadcastRefreshList() } resource - .principals - .onEach { it.list = calendar.id } - .let { - principalDao.deleteRemoved(calendar.id, it.mapNotNull { p -> p.principal } ) - principalDao.insert(it) - } + .principals(account, calendar) + .let { principalDao.deleteRemoved(calendar.id, it.map(PrincipalAccess::id)) } sync(calendar, resource, caldavClient.httpClient) } setError(account, "") @@ -345,6 +341,57 @@ class CaldavSynchronizer @Inject constructor( Timber.d("SENT %s", caldavTask) } + fun Response.principals( + account: CaldavAccount, + list: CaldavCalendar + ): List { + val access = ArrayList() + this[Invite::class.java] + ?.sharees + ?.filter { it.href?.let { href -> !isCurrentUser(href) } ?: false } + ?.map { + val principal = principalDao.getOrCreatePrincipal( + account, + it.href!!, + it.properties + .find { p -> p is DisplayName } + ?.let { name -> (name as DisplayName).displayName } + ) + principalDao.getOrCreateAccess( + list, + principal, + invite = it.response?.toStatus ?: INVITE_UNKNOWN, + access = it.access?.access?.toAccess ?: ACCESS_UNKNOWN + ) + } + ?.let { access.addAll(it) } + this[OCInvite::class.java]?.users + ?.map { + val principal = principalDao.getOrCreatePrincipal(account, it.href) + principalDao.getOrCreateAccess( + list, + principal, + it.response.toStatus, + it.access.access.toAccess + ) + } + ?.let { + if (!isOwncloudOwner) { + val principal = principalDao.getOrCreatePrincipal( + account, this@principals[OCOwnerPrincipal::class.java]?.owner!! + ) + access.add(principalDao.getOrCreateAccess( + list, + principal, + INVITE_ACCEPTED, + ACCESS_OWNER + )) + } + access.addAll(it) + } + return access + } + companion object { init { prodId = ProdId("+//IDN tasks.org//android-" + BuildConfig.VERSION_CODE + "//EN") @@ -387,45 +434,6 @@ class CaldavSynchronizer @Inject constructor( private fun Response.isCurrentUser(href: String) = this[CurrentUserPrincipal::class.java]?.href?.endsWith("$href/") == true - val Response.principals: List - get() { - val principals = ArrayList() - this[Invite::class.java] - ?.sharees - ?.filter { it.href?.let { href -> !isCurrentUser(href) } ?: false } - ?.map { - Principal().apply { - principal = it.href - it.properties.find { it is DisplayName }?.let { name -> - displayName = (name as DisplayName).displayName - } - inviteStatus = it.response?.toStatus ?: INVITE_UNKNOWN - access = it.access?.access?.toAccess ?: ACCESS_UNKNOWN - } - } - ?.let { principals.addAll(it) } - this[OCInvite::class.java]?.users - ?.map { - Principal().apply { - principal = it.href - displayName = it.commonName - inviteStatus = it.response.toStatus - access = it.access.access.toAccess - } - } - ?.let { - if (!isOwncloudOwner) { - principals.add(Principal().apply { - principal = this@principals[OCOwnerPrincipal::class.java]?.owner - inviteStatus = INVITE_ACCEPTED - access = ACCESS_OWNER - }) - } - principals.addAll(it) - } - return principals - } - private val Property.Name.toAccess: Int get() = when (this) { SHARED_OWNER, OCAccess.SHARED_OWNER -> ACCESS_OWNER diff --git a/app/src/main/java/org/tasks/compose/PrincipalList.kt b/app/src/main/java/org/tasks/compose/PrincipalList.kt index 5b7779909..8ff42a695 100644 --- a/app/src/main/java/org/tasks/compose/PrincipalList.kt +++ b/app/src/main/java/org/tasks/compose/PrincipalList.kt @@ -29,20 +29,24 @@ import org.tasks.R import org.tasks.compose.Constants.HALF_KEYLINE import org.tasks.compose.Constants.ICON_ALPHA import org.tasks.compose.Constants.KEYLINE_FIRST -import org.tasks.data.CaldavCalendar +import org.tasks.data.CaldavCalendar.Companion.INVITE_ACCEPTED import org.tasks.data.CaldavCalendar.Companion.INVITE_DECLINED import org.tasks.data.CaldavCalendar.Companion.INVITE_INVALID import org.tasks.data.CaldavCalendar.Companion.INVITE_NO_RESPONSE import org.tasks.data.CaldavCalendar.Companion.INVITE_UNKNOWN import org.tasks.data.Principal -import org.tasks.data.Principal.Companion.name +import org.tasks.data.PrincipalAccess +import org.tasks.data.PrincipalWithAccess private val principals = listOf( - Principal().apply { - displayName = "user1" - inviteStatus = INVITE_INVALID - }, - Principal().apply { displayName = "a really really really really really long display name" }, + PrincipalWithAccess( + PrincipalAccess(list = 0, invite = INVITE_INVALID), + Principal(account = 0, href = "", displayName = "user1") + ), + PrincipalWithAccess( + PrincipalAccess(list = 0, invite = INVITE_ACCEPTED), + Principal(account = 0, href = "", displayName = "a really really really really really long display name") + ) ) @Preview(showBackground = true, backgroundColor = 0xFFFFFF) @@ -66,8 +70,8 @@ private fun NotOwner() = MaterialTheme { object ListSettingsComposables { @Composable fun PrincipalList( - principals: List, - onRemove: ((Principal) -> Unit)?, + principals: List, + onRemove: ((PrincipalWithAccess) -> Unit)?, ) { Column( modifier = Modifier @@ -88,8 +92,8 @@ object ListSettingsComposables { @Composable fun PrincipalRow( - principal: Principal, - onRemove: ((Principal) -> Unit)?, + principal: PrincipalWithAccess, + onRemove: ((PrincipalWithAccess) -> Unit)?, ) { Row( Modifier @@ -121,7 +125,7 @@ object ListSettingsComposables { style = MaterialTheme.typography.body1, color = colors.onBackground, ) - if (principal.inviteStatus != CaldavCalendar.INVITE_ACCEPTED) { + if (principal.inviteStatus != INVITE_ACCEPTED) { Text( stringResource(when (principal.inviteStatus) { INVITE_UNKNOWN, INVITE_NO_RESPONSE -> diff --git a/app/src/main/java/org/tasks/data/CaldavDao.kt b/app/src/main/java/org/tasks/data/CaldavDao.kt index fa478264b..bbc03aaae 100644 --- a/app/src/main/java/org/tasks/data/CaldavDao.kt +++ b/app/src/main/java/org/tasks/data/CaldavDao.kt @@ -243,13 +243,13 @@ SELECT EXISTS(SELECT 1 abstract suspend fun getCalendars(tasks: List): List @Query(""" -SELECT caldav_lists.*, COUNT(DISTINCT(tasks._id)) AS count, COUNT(DISTINCT(principals.principal_id)) AS principals +SELECT caldav_lists.*, COUNT(DISTINCT(tasks._id)) AS count, COUNT(DISTINCT(principal_access.id)) AS principals FROM caldav_lists LEFT JOIN caldav_tasks ON caldav_tasks.cd_calendar = caldav_lists.cdl_uuid LEFT JOIN tasks ON caldav_tasks.cd_task = tasks._id AND tasks.deleted = 0 AND tasks.completed = 0 AND tasks.hideUntil < :now AND cd_deleted = 0 - LEFT JOIN principals ON caldav_lists.cdl_id = principals.principal_list + LEFT JOIN principal_access ON caldav_lists.cdl_id = principal_access.list WHERE caldav_lists.cdl_account = :uuid GROUP BY caldav_lists.cdl_uuid """) diff --git a/app/src/main/java/org/tasks/data/Principal.kt b/app/src/main/java/org/tasks/data/Principal.kt index 8599f5fbb..bf303e799 100644 --- a/app/src/main/java/org/tasks/data/Principal.kt +++ b/app/src/main/java/org/tasks/data/Principal.kt @@ -1,76 +1,38 @@ package org.tasks.data -import androidx.room.* +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.Index +import androidx.room.PrimaryKey @Entity( tableName = "principals", - foreignKeys = [ForeignKey( - entity = CaldavCalendar::class, - parentColumns = arrayOf("cdl_id"), - childColumns = arrayOf("principal_list"), - onDelete = ForeignKey.CASCADE - )], - indices = [Index(value = ["principal_list", "principal"], unique = true)] + foreignKeys = [ + ForeignKey( + entity = CaldavAccount::class, + parentColumns = arrayOf("cda_id"), + childColumns = arrayOf("account"), + onDelete = ForeignKey.CASCADE + ) + ], + indices = [Index(value = ["account", "href"], unique = true)] ) -class Principal { - @PrimaryKey(autoGenerate = true) - @ColumnInfo(name = "principal_id") - @Transient - var id: Long = 0 - - @ColumnInfo(name = "principal_list") - var list: Long = 0 - - @ColumnInfo(name = "principal") - var principal: String? = null - - @ColumnInfo(name = "display_name") - var displayName: String? = null - - @ColumnInfo(name = "invite") - var inviteStatus: Int = CaldavCalendar.INVITE_UNKNOWN - - @ColumnInfo(name = "access") - var access: Int = CaldavCalendar.ACCESS_UNKNOWN - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as Principal - - if (id != other.id) return false - if (list != other.list) return false - if (principal != other.principal) return false - if (displayName != other.displayName) return false - if (inviteStatus != other.inviteStatus) return false - if (access != other.access) return false - - return true - } - - override fun hashCode(): Int { - var result = id.hashCode() - result = 31 * result + list.hashCode() - result = 31 * result + (principal?.hashCode() ?: 0) - result = 31 * result + (displayName?.hashCode() ?: 0) - result = 31 * result + inviteStatus - result = 31 * result + access - return result - } - - override fun toString(): String { - return "Principal(id=$id, list=$list, principal=$principal, displayName=$displayName, inviteStatus=$inviteStatus, access=$access)" - } +data class Principal( + @PrimaryKey(autoGenerate = true) var id: Long = 0, + val account: Long, + val href: String, + var email: String? = null, + @ColumnInfo(name = "display_name") var displayName: String? = null +) { + val name: String + get() = displayName + ?: href + .replace(MAILTO, "") + .replaceFirst(LAST_SEGMENT, "$1") companion object { private val MAILTO = "^mailto:".toRegex() private val LAST_SEGMENT = ".*/([^/]+).*".toRegex() - - val Principal.name: String? - get() = displayName - ?: principal - ?.replace(MAILTO, "") - ?.replaceFirst(LAST_SEGMENT, "$1") } } \ No newline at end of file diff --git a/app/src/main/java/org/tasks/data/PrincipalAccess.kt b/app/src/main/java/org/tasks/data/PrincipalAccess.kt new file mode 100644 index 000000000..e35073ec2 --- /dev/null +++ b/app/src/main/java/org/tasks/data/PrincipalAccess.kt @@ -0,0 +1,36 @@ +package org.tasks.data + +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.Index +import androidx.room.PrimaryKey +import org.tasks.data.CaldavCalendar.Companion.ACCESS_UNKNOWN +import org.tasks.data.CaldavCalendar.Companion.INVITE_UNKNOWN + +@Entity( + tableName = "principal_access", + foreignKeys = [ + ForeignKey( + entity = Principal::class, + parentColumns = arrayOf("id"), + childColumns = arrayOf("principal"), + onDelete = ForeignKey.CASCADE + ), + ForeignKey( + entity = CaldavCalendar::class, + parentColumns = arrayOf("cdl_id"), + childColumns = arrayOf("list"), + onDelete = ForeignKey.CASCADE + )], + indices = [ + Index(value = ["list", "principal"], unique = true), + Index(value = ["principal"]) + ] +) +data class PrincipalAccess( + @PrimaryKey(autoGenerate = true) var id: Long = 0, + val principal: Long = 0, + val list: Long, + var invite: Int = INVITE_UNKNOWN, + var access: Int = ACCESS_UNKNOWN +) \ No newline at end of file diff --git a/app/src/main/java/org/tasks/data/PrincipalDao.kt b/app/src/main/java/org/tasks/data/PrincipalDao.kt index 6d5ce04f0..7ac521b18 100644 --- a/app/src/main/java/org/tasks/data/PrincipalDao.kt +++ b/app/src/main/java/org/tasks/data/PrincipalDao.kt @@ -4,33 +4,68 @@ import androidx.lifecycle.LiveData import androidx.room.Dao import androidx.room.Delete import androidx.room.Insert -import androidx.room.OnConflictStrategy import androidx.room.Query +import androidx.room.Transaction +import androidx.room.Update @Dao interface PrincipalDao { @Insert - fun insert(principal: Principal) + fun insert(principal: Principal): Long - @Insert(onConflict = OnConflictStrategy.REPLACE) - fun insert(principals: List) + @Insert + fun insert(access: PrincipalAccess): Long + + @Update + fun update(access: PrincipalAccess) @Query(""" DELETE -FROM principals -WHERE principal_list = :list - AND principal NOT IN (:principals)""") - fun deleteRemoved(list: Long, principals: List) +FROM principal_access +WHERE list = :list + AND id NOT IN (:access)""") + fun deleteRemoved(list: Long, access: List) @Delete - fun delete(principal: Principal) + fun delete(access: PrincipalAccess) - @Delete - fun delete(principals: List) + @Transaction + @Query("SELECT * FROM principal_access") + fun getAll(): List + + fun getOrCreatePrincipal(account: CaldavAccount, href: String, displayName: String? = null) = + findPrincipal(account.id, href) + ?: Principal(account = account.id, href = href, displayName = displayName) + .apply { id = insert(this) } + + fun getOrCreateAccess( + calendar: CaldavCalendar, + principal: Principal, + invite: Int, + access: Int, + ): PrincipalAccess = + findAccess(calendar.id, principal.id) + ?.apply { + if (this.access != access || this.invite != invite) { + this.access = access + this.invite = invite + update(this) + } + } + ?: PrincipalAccess( + principal = principal.id, + list = calendar.id, + invite = invite, + access = access + ).apply { id = insert(this) } + + @Query("SELECT * FROM principals WHERE account = :account AND href = :href") + fun findPrincipal(account: Long, href: String): Principal? - @Query("SELECT * FROM principals") - fun getAll(): List + @Query("SELECT * FROM principal_access WHERE list = :list and principal = :principal") + fun findAccess(list: Long, principal: Long): PrincipalAccess? - @Query("SELECT * FROM principals WHERE principal_list = :id") - fun getPrincipals(id: Long): LiveData> + @Transaction + @Query("SELECT * FROM principal_access WHERE list = :id") + fun getPrincipals(id: Long): LiveData> } \ No newline at end of file diff --git a/app/src/main/java/org/tasks/data/PrincipalWithAccess.kt b/app/src/main/java/org/tasks/data/PrincipalWithAccess.kt new file mode 100644 index 000000000..e85d57ccd --- /dev/null +++ b/app/src/main/java/org/tasks/data/PrincipalWithAccess.kt @@ -0,0 +1,19 @@ +package org.tasks.data + +import androidx.room.Embedded +import androidx.room.Relation + +data class PrincipalWithAccess( + @Embedded val access: PrincipalAccess, + @Relation( + parentColumn = "principal", + entityColumn = "id" + ) + val principal: Principal +) { + val displayName get() = principal.displayName + val list get() = access.list + val href get() = principal.href + val inviteStatus get() = access.invite + val name get() = principal.name +} diff --git a/app/src/main/java/org/tasks/db/Migrations.kt b/app/src/main/java/org/tasks/db/Migrations.kt index 32dd3dd69..4406d650e 100644 --- a/app/src/main/java/org/tasks/db/Migrations.kt +++ b/app/src/main/java/org/tasks/db/Migrations.kt @@ -383,6 +383,27 @@ object Migrations { } } + private val MIGRATION_79_80 = object : Migration(79, 80) { + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL("DROP TABLE `principals`") + database.execSQL( + "CREATE TABLE IF NOT EXISTS `principals` (`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 )" + ) + database.execSQL( + "CREATE UNIQUE INDEX IF NOT EXISTS `index_principals_account_href` ON `principals` (`account`, `href`)" + ) + database.execSQL( + "CREATE TABLE IF NOT EXISTS `principal_access` (`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 )" + ) + database.execSQL( + "CREATE UNIQUE INDEX IF NOT EXISTS `index_principal_access_list_principal` ON `principal_access` (`list`, `principal`)" + ) + database.execSQL( + "CREATE INDEX IF NOT EXISTS `index_principal_access_principal` ON `principal_access` (`principal`)" + ) + } + } + val MIGRATIONS = arrayOf( MIGRATION_35_36, MIGRATION_36_37, @@ -419,6 +440,7 @@ object Migrations { MIGRATION_76_77, MIGRATION_77_78, MIGRATION_78_79, + MIGRATION_79_80, ) private fun noop(from: Int, to: Int): Migration = object : Migration(from, to) { diff --git a/app/src/test/java/org/tasks/data/PrincipalTest.kt b/app/src/test/java/org/tasks/data/PrincipalTest.kt index b7f12c65b..bbaf74f18 100644 --- a/app/src/test/java/org/tasks/data/PrincipalTest.kt +++ b/app/src/test/java/org/tasks/data/PrincipalTest.kt @@ -2,32 +2,25 @@ package org.tasks.data import org.junit.Assert.assertEquals import org.junit.Test -import org.tasks.data.Principal.Companion.name class PrincipalTest { @Test fun lastSegmentTrailingSlash() { - val principal = Principal().apply { - principal = "principals/users/user1/" - } + val principal = Principal(account = 0, href = "/principals/users/user1") assertEquals("user1", principal.name) } @Test fun lastSegmentNoTrailingSlash() { - val principal = Principal().apply { - principal = "principals/users/user1" - } + val principal = Principal(account = 0, href = "principals/users/user1") assertEquals("user1", principal.name) } @Test fun stripMailto() { - val principal = Principal().apply { - principal = "mailto:user@example.com" - } + val principal = Principal(account = 0, href = "mailto:user@example.com") assertEquals("user@example.com", principal.name) }