From 983fa6644cf2d01066c87c540fd0606049b2106f Mon Sep 17 00:00:00 2001 From: Alex Baker Date: Fri, 26 Feb 2021 18:06:23 -0600 Subject: [PATCH] Add principals table --- .../com.todoroo.astrid.dao.Database/78.json | 1258 +++++++++++++++++ .../org/tasks/caldav/SharingOwncloudTest.kt | 52 + .../org/tasks/caldav/SharingSabredavTest.kt | 54 + .../java/com/todoroo/astrid/dao/Database.kt | 7 +- .../org/tasks/caldav/CaldavSynchronizer.kt | 103 +- .../org/tasks/caldav/property/OCAccess.kt | 4 +- .../java/org/tasks/data/CaldavCalendar.kt | 6 + app/src/main/java/org/tasks/data/Principal.kt | 65 + .../main/java/org/tasks/data/PrincipalDao.kt | 22 + app/src/main/java/org/tasks/db/Migrations.kt | 11 + .../org/tasks/injection/ApplicationModule.kt | 4 + 11 files changed, 1562 insertions(+), 24 deletions(-) create mode 100644 app/schemas/com.todoroo.astrid.dao.Database/78.json create mode 100644 app/src/main/java/org/tasks/data/Principal.kt create mode 100644 app/src/main/java/org/tasks/data/PrincipalDao.kt diff --git a/app/schemas/com.todoroo.astrid.dao.Database/78.json b/app/schemas/com.todoroo.astrid.dao.Database/78.json new file mode 100644 index 000000000..cb687aec7 --- /dev/null +++ b/app/schemas/com.todoroo.astrid.dao.Database/78.json @@ -0,0 +1,1258 @@ +{ + "formatVersion": 1, + "database": { + "version": 78, + "identityHash": "a8e17a01e685d01db0f26231c7aa0c9b", + "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)", + "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 + } + ], + "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}` (`principal_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `principal_list` INTEGER NOT NULL, `principal` TEXT, `display_name` TEXT, `invite` INTEGER NOT NULL, `access` INTEGER NOT NULL, FOREIGN KEY(`principal_list`) REFERENCES `caldav_lists`(`cdl_id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "principal_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "list", + "columnName": "principal_list", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "principal", + "columnName": "principal", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "displayName", + "columnName": "display_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inviteStatus", + "columnName": "invite", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "access", + "columnName": "access", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "principal_id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_principals_principal_list_principal", + "unique": true, + "columnNames": [ + "principal_list", + "principal" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_principals_principal_list_principal` ON `${TABLE_NAME}` (`principal_list`, `principal`)" + } + ], + "foreignKeys": [ + { + "table": "caldav_lists", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "principal_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, 'a8e17a01e685d01db0f26231c7aa0c9b')" + ] + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/org/tasks/caldav/SharingOwncloudTest.kt b/app/src/androidTest/java/org/tasks/caldav/SharingOwncloudTest.kt index 6e7d82b44..0f524ffdb 100644 --- a/app/src/androidTest/java/org/tasks/caldav/SharingOwncloudTest.kt +++ b/app/src/androidTest/java/org/tasks/caldav/SharingOwncloudTest.kt @@ -10,12 +10,16 @@ import org.tasks.data.CaldavAccount import org.tasks.data.CaldavCalendar import org.tasks.data.CaldavCalendar.Companion.ACCESS_OWNER import org.tasks.data.CaldavCalendar.Companion.ACCESS_READ_ONLY +import org.tasks.data.PrincipalDao import org.tasks.injection.ProductionModule +import javax.inject.Inject @UninstallModules(ProductionModule::class) @HiltAndroidTest class SharingOwncloudTest : CaldavTest() { + @Inject lateinit var principalDao: PrincipalDao + private suspend fun setupAccount(user: String) { account = CaldavAccount().apply { uuid = UUIDHelper.newUUID() @@ -58,6 +62,54 @@ class SharingOwncloudTest : CaldavTest() { assertEquals(ACCESS_READ_ONLY, caldavDao.getCalendarByUuid(calendar.uuid!!)?.access) } + @Test + fun principalForSharee() = runBlocking { + setupAccount("user1") + val calendar = CaldavCalendar().apply { + account = this@SharingOwncloudTest.account.uuid + ctag = "http://sabre.io/ns/sync/1" + url = "${this@SharingOwncloudTest.account.url}test-shared/" + caldavDao.insert(this) + } + enqueue(OC_OWNER) + + synchronizer.sync(account) + + val principal = principalDao.getAll() + .apply { assertTrue(size == 1) } + .first() + + assertEquals(calendar.id, principal.list) + assertEquals("principal:principals/users/user2", principal.principal) + assertEquals("user2", principal.displayName) + assertEquals(CaldavCalendar.INVITE_ACCEPTED, principal.inviteStatus) + assertEquals(CaldavCalendar.ACCESS_READ_ONLY, principal.access) + } + + @Test + fun principalForOwner() = runBlocking { + setupAccount("user2") + val calendar = CaldavCalendar().apply { + account = this@SharingOwncloudTest.account.uuid + ctag = "http://sabre.io/ns/sync/2" + url = "${this@SharingOwncloudTest.account.url}test-shared_shared_by_user1/" + caldavDao.insert(this) + } + enqueue(OC_READ_ONLY) + + synchronizer.sync(account) + + val principal = principalDao.getAll() + .apply { assertTrue(size == 1) } + .first() + + assertEquals(calendar.id, principal.list) + assertEquals("principals/users/user1", principal.principal) + assertEquals(null, principal.displayName) + assertEquals(CaldavCalendar.INVITE_ACCEPTED, principal.inviteStatus) + assertEquals(CaldavCalendar.ACCESS_OWNER, principal.access) + } + companion object { private val OC_OWNER = """ diff --git a/app/src/androidTest/java/org/tasks/caldav/SharingSabredavTest.kt b/app/src/androidTest/java/org/tasks/caldav/SharingSabredavTest.kt index 2fc87d6ea..4d7b5e754 100644 --- a/app/src/androidTest/java/org/tasks/caldav/SharingSabredavTest.kt +++ b/app/src/androidTest/java/org/tasks/caldav/SharingSabredavTest.kt @@ -5,17 +5,23 @@ import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.UninstallModules import kotlinx.coroutines.runBlocking import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue import org.junit.Test import org.tasks.data.CaldavAccount import org.tasks.data.CaldavCalendar import org.tasks.data.CaldavCalendar.Companion.ACCESS_OWNER import org.tasks.data.CaldavCalendar.Companion.ACCESS_READ_WRITE +import org.tasks.data.CaldavCalendar.Companion.INVITE_ACCEPTED +import org.tasks.data.PrincipalDao import org.tasks.injection.ProductionModule +import javax.inject.Inject @UninstallModules(ProductionModule::class) @HiltAndroidTest class SharingSabredavTest : CaldavTest() { + @Inject lateinit var principalDao: PrincipalDao + private suspend fun setupAccount(user: String) { account = CaldavAccount().apply { uuid = UUIDHelper.newUUID() @@ -64,6 +70,54 @@ class SharingSabredavTest : CaldavTest() { ) } + @Test + fun principalForSharee() = runBlocking { + setupAccount("user1") + val calendar = CaldavCalendar().apply { + account = this@SharingSabredavTest.account.uuid + ctag = "http://sabre.io/ns/sync/1" + url = "${this@SharingSabredavTest.account.url}940468858232147861/" + caldavDao.insert(this) + } + enqueue(SD_OWNER) + + synchronizer.sync(account) + + val principal = principalDao.getAll() + .apply { assertTrue(size == 1) } + .first() + + assertEquals(calendar.id, principal.list) + assertEquals("mailto:user@example.com", principal.principal) + assertEquals("Example User", principal.displayName) + assertEquals(INVITE_ACCEPTED, principal.inviteStatus) + assertEquals(ACCESS_READ_WRITE, principal.access) + } + + @Test + fun principalForOwner() = runBlocking { + setupAccount("user2") + val calendar = CaldavCalendar().apply { + account = this@SharingSabredavTest.account.uuid + ctag = "http://sabre.io/ns/sync/1" + url = "${this@SharingSabredavTest.account.url}c3853d69-cb7a-476c-a23b-30ffd70f110b/" + caldavDao.insert(this) + } + enqueue(SD_SHAREE) + + synchronizer.sync(account) + + val principal = principalDao.getAll() + .apply { assertTrue(size == 1) } + .first() + + assertEquals(calendar.id, principal.list) + assertEquals("/principals/user1", principal.principal) + assertEquals(null, principal.displayName) + assertEquals(INVITE_ACCEPTED, principal.inviteStatus) + assertEquals(ACCESS_OWNER, principal.access) + } + companion object { private val SD_OWNER = """ 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 41fe07e92..05c9682d3 100644 --- a/app/src/main/java/com/todoroo/astrid/dao/Database.kt +++ b/app/src/main/java/com/todoroo/astrid/dao/Database.kt @@ -26,8 +26,10 @@ import org.tasks.notifications.NotificationDao CaldavCalendar::class, CaldavTask::class, CaldavAccount::class, - GoogleTaskAccount::class], - version = 77) + GoogleTaskAccount::class, + Principal::class, + ], + version = 78) abstract class Database : RoomDatabase() { abstract fun notificationDao(): NotificationDao abstract val tagDataDao: TagDataDao @@ -45,6 +47,7 @@ abstract class Database : RoomDatabase() { abstract val deletionDao: DeletionDao abstract val contentProviderDao: ContentProviderDao abstract val upgraderDao: UpgraderDao + abstract val principalDao: PrincipalDao /** @return human-readable database name for debugging */ diff --git a/app/src/main/java/org/tasks/caldav/CaldavSynchronizer.kt b/app/src/main/java/org/tasks/caldav/CaldavSynchronizer.kt index 41a536ca7..a1d8262ad 100644 --- a/app/src/main/java/org/tasks/caldav/CaldavSynchronizer.kt +++ b/app/src/main/java/org/tasks/caldav/CaldavSynchronizer.kt @@ -1,11 +1,8 @@ package org.tasks.caldav import android.content.Context -import at.bitfire.dav4jvm.DavCalendar +import at.bitfire.dav4jvm.* import at.bitfire.dav4jvm.DavCalendar.Companion.MIME_ICALENDAR -import at.bitfire.dav4jvm.DavResource -import at.bitfire.dav4jvm.PropertyRegistry -import at.bitfire.dav4jvm.Response import at.bitfire.dav4jvm.Response.HrefRelation import at.bitfire.dav4jvm.exception.DavException import at.bitfire.dav4jvm.exception.HttpException @@ -30,23 +27,22 @@ import org.tasks.Strings.isNullOrEmpty import org.tasks.analytics.Firebase import org.tasks.billing.Inventory import org.tasks.caldav.iCalendar.Companion.fromVtodo -import org.tasks.caldav.property.Invite -import org.tasks.caldav.property.OCInvite -import org.tasks.caldav.property.OCOwnerPrincipal +import org.tasks.caldav.property.* import org.tasks.caldav.property.PropertyUtils.register -import org.tasks.caldav.property.ShareAccess import org.tasks.caldav.property.ShareAccess.Companion.READ import org.tasks.caldav.property.ShareAccess.Companion.READ_WRITE import org.tasks.caldav.property.ShareAccess.Companion.SHARED_OWNER -import org.tasks.data.CaldavAccount +import org.tasks.data.* import org.tasks.data.CaldavAccount.Companion.ERROR_UNAUTHORIZED -import org.tasks.data.CaldavCalendar import org.tasks.data.CaldavCalendar.Companion.ACCESS_OWNER import org.tasks.data.CaldavCalendar.Companion.ACCESS_READ_ONLY import org.tasks.data.CaldavCalendar.Companion.ACCESS_READ_WRITE import org.tasks.data.CaldavCalendar.Companion.ACCESS_UNKNOWN -import org.tasks.data.CaldavDao -import org.tasks.data.CaldavTask +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 timber.log.Timber import java.io.IOException import java.net.ConnectException @@ -67,8 +63,9 @@ class CaldavSynchronizer @Inject constructor( private val inventory: Inventory, private val firebase: Firebase, private val provider: CaldavClientProvider, - private val iCal: iCalendar) { - + private val iCal: iCalendar, + private val principalDao: PrincipalDao, +) { suspend fun sync(account: CaldavAccount) { Thread.currentThread().contextClassLoader = context.classLoader @@ -161,6 +158,13 @@ class CaldavSynchronizer @Inject constructor( caldavDao.update(calendar) localBroadcastManager.broadcastRefreshList() } + resource + .principals + .onEach { it.list = calendar.id } + .let { + principalDao.deleteRemoved(calendar.id, it.mapNotNull { p -> p.principal } ) + principalDao.insert(it) + } sync(calendar, resource, caldavClient.httpClient) } setError(account, "") @@ -319,6 +323,8 @@ class CaldavSynchronizer @Inject constructor( prodId = ProdId("+//IDN tasks.org//android-" + BuildConfig.VERSION_CODE + "//EN") } + private val MAILTO = "^mailto:".toRegex() + fun registerFactories() { PropertyRegistry.register( ShareAccess.Factory(), @@ -341,16 +347,75 @@ class CaldavSynchronizer @Inject constructor( else -> ACCESS_UNKNOWN } } - this[OCOwnerPrincipal::class.java]?.owner?.let { - val current = this[CurrentUserPrincipal::class.java]?.href - if (current?.endsWith("$it/") == true) { - return ACCESS_OWNER - } + if (isOwncloudOwner) { + return ACCESS_OWNER } return when (this[CurrentUserPrivilegeSet::class.java]?.mayWriteContent) { false -> ACCESS_READ_ONLY else -> ACCESS_READ_WRITE } } + + val Response.isOwncloudOwner: Boolean + get() = this[OCOwnerPrincipal::class.java]?.owner + ?.let { + this[CurrentUserPrincipal::class.java]?.href?.endsWith("$it/") == true + } + ?: false + + val Response.principals: List + get() { + val principals = ArrayList() + this[Invite::class.java]?.sharees + ?.map { + Principal().apply { + principal = it.href + it.properties.find { it is DisplayName }?.let { name -> + displayName = (name as DisplayName).displayName + ?: it.href.replace(MAILTO, "") + } + inviteStatus = it.response.toStatus + access = it.access.access.toAccess + } + } + ?.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 + } + + val Property.Name.toAccess: Int + get() = when(this) { + SHARED_OWNER, OCAccess.SHARED_OWNER -> ACCESS_OWNER + READ_WRITE, OCAccess.READ_WRITE -> ACCESS_READ_WRITE + READ, OCAccess.READ -> ACCESS_READ_ONLY + else -> ACCESS_UNKNOWN + } + + val Property.Name.toStatus: Int + get() = when (this) { + Sharee.INVITE_ACCEPTED, OCUser.INVITE_ACCEPTED -> INVITE_ACCEPTED + Sharee.INVITE_NORESPONSE, OCUser.INVITE_NORESPONSE -> INVITE_NO_RESPONSE + Sharee.INVITE_DECLINED, OCUser.INVITE_DECLINED -> INVITE_DECLINED + Sharee.INVITE_INVALID, OCUser.INVITE_INVALID -> INVITE_INVALID + else -> INVITE_UNKNOWN + } } } \ No newline at end of file diff --git a/app/src/main/java/org/tasks/caldav/property/OCAccess.kt b/app/src/main/java/org/tasks/caldav/property/OCAccess.kt index 9a4c8fa2e..68e3d18d7 100644 --- a/app/src/main/java/org/tasks/caldav/property/OCAccess.kt +++ b/app/src/main/java/org/tasks/caldav/property/OCAccess.kt @@ -16,9 +16,7 @@ class OCAccess(parser: XmlPullParser) : Property { var eventType = parser.eventType while (!(eventType == XmlPullParser.END_TAG && parser.depth == depth)) { if (eventType == XmlPullParser.START_TAG && parser.depth == depth + 1) { - when (val name = parser.propertyName()) { - SHARED_OWNER, READ_WRITE, NOT_SHARED, READ -> access = name - } + access = parser.propertyName() } eventType = parser.next() } diff --git a/app/src/main/java/org/tasks/data/CaldavCalendar.kt b/app/src/main/java/org/tasks/data/CaldavCalendar.kt index 56ff817e2..e5ffe87ce 100644 --- a/app/src/main/java/org/tasks/data/CaldavCalendar.kt +++ b/app/src/main/java/org/tasks/data/CaldavCalendar.kt @@ -134,6 +134,12 @@ class CaldavCalendar : Parcelable { const val ACCESS_READ_WRITE = 1 const val ACCESS_READ_ONLY = 2 + const val INVITE_UNKNOWN = -1 + const val INVITE_ACCEPTED = 0 + const val INVITE_NO_RESPONSE = 1 + const val INVITE_DECLINED = 2 + const val INVITE_INVALID = 3 + @JvmField val TABLE = Table("caldav_lists") val ACCOUNT = TABLE.column("cdl_account") @JvmField val UUID = TABLE.column("cdl_uuid") diff --git a/app/src/main/java/org/tasks/data/Principal.kt b/app/src/main/java/org/tasks/data/Principal.kt new file mode 100644 index 000000000..393f7a40d --- /dev/null +++ b/app/src/main/java/org/tasks/data/Principal.kt @@ -0,0 +1,65 @@ +package org.tasks.data + +import androidx.room.* + +@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)] +) +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)" + } +} \ 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 new file mode 100644 index 000000000..da40f9559 --- /dev/null +++ b/app/src/main/java/org/tasks/data/PrincipalDao.kt @@ -0,0 +1,22 @@ +package org.tasks.data + +import androidx.room.* + +@Dao +interface PrincipalDao { + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun insert(principal: List) + + @Query(""" +DELETE +FROM principals +WHERE principal_list = :list + AND principal NOT IN (:principals)""") + fun deleteRemoved(list: Long, principals: List) + + @Delete + fun delete(principals: List) + + @Query("SELECT * FROM principals") + fun getAll(): List +} \ No newline at end of file diff --git a/app/src/main/java/org/tasks/db/Migrations.kt b/app/src/main/java/org/tasks/db/Migrations.kt index 2bf1292ef..9e820ebe0 100644 --- a/app/src/main/java/org/tasks/db/Migrations.kt +++ b/app/src/main/java/org/tasks/db/Migrations.kt @@ -364,6 +364,16 @@ object Migrations { } } + private val MIGRATION_77_78: Migration = object : Migration(77, 78) { + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL( + "CREATE TABLE IF NOT EXISTS `principals` (`principal_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `principal_list` INTEGER NOT NULL, `principal` TEXT, `display_name` TEXT, `invite` INTEGER NOT NULL, `access` INTEGER NOT NULL, FOREIGN KEY(`principal_list`) REFERENCES `caldav_lists`(`cdl_id`) ON UPDATE NO ACTION ON DELETE CASCADE )") + database.execSQL( + "CREATE UNIQUE INDEX IF NOT EXISTS `index_principals_principal_list_principal` ON `principals` (`principal_list`, `principal`)" + ) + } + } + val MIGRATIONS = arrayOf( MIGRATION_35_36, MIGRATION_36_37, @@ -398,6 +408,7 @@ object Migrations { MIGRATION_74_75, MIGRATION_75_76, MIGRATION_76_77, + MIGRATION_77_78, ) private fun noop(from: Int, to: Int): Migration = object : Migration(from, to) { diff --git a/app/src/main/java/org/tasks/injection/ApplicationModule.kt b/app/src/main/java/org/tasks/injection/ApplicationModule.kt index e48d589d0..6aeb1a89f 100644 --- a/app/src/main/java/org/tasks/injection/ApplicationModule.kt +++ b/app/src/main/java/org/tasks/injection/ApplicationModule.kt @@ -91,6 +91,10 @@ class ApplicationModule { @Singleton fun getUpgraderDao(db: Database) = db.upgraderDao + @Provides + @Singleton + fun getPrincipalDao(db: Database) = db.principalDao + @Provides fun getBillingClient(@ApplicationContext context: Context, inventory: Inventory, firebase: Firebase): BillingClient = BillingClientImpl(context, inventory, firebase)