diff --git a/app/schemas/at.bitfire.davdroid.db.AppDatabase/17.json b/app/schemas/at.bitfire.davdroid.db.AppDatabase/17.json new file mode 100644 index 000000000..426d917c7 --- /dev/null +++ b/app/schemas/at.bitfire.davdroid.db.AppDatabase/17.json @@ -0,0 +1,711 @@ +{ + "formatVersion": 1, + "database": { + "version": 17, + "identityHash": "3e8ce358d26c057ba1dfc24de3ac5ba5", + "entities": [ + { + "tableName": "account", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_account_name", + "unique": true, + "columnNames": [ + "name" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_account_name` ON `${TABLE_NAME}` (`name`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "service", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `accountName` TEXT NOT NULL, `type` TEXT NOT NULL, `principal` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountName", + "columnName": "accountName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "principal", + "columnName": "principal", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_service_accountName_type", + "unique": true, + "columnNames": [ + "accountName", + "type" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_service_accountName_type` ON `${TABLE_NAME}` (`accountName`, `type`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "homeset", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `serviceId` INTEGER NOT NULL, `personal` INTEGER NOT NULL, `url` TEXT NOT NULL, `privBind` INTEGER NOT NULL, `displayName` TEXT, FOREIGN KEY(`serviceId`) REFERENCES `service`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "serviceId", + "columnName": "serviceId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "personal", + "columnName": "personal", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "privBind", + "columnName": "privBind", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_homeset_serviceId_url", + "unique": true, + "columnNames": [ + "serviceId", + "url" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_homeset_serviceId_url` ON `${TABLE_NAME}` (`serviceId`, `url`)" + } + ], + "foreignKeys": [ + { + "table": "service", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "serviceId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "collection", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `serviceId` INTEGER NOT NULL, `homeSetId` INTEGER, `ownerId` INTEGER, `type` TEXT NOT NULL, `url` TEXT NOT NULL, `privWriteContent` INTEGER NOT NULL, `privUnbind` INTEGER NOT NULL, `forceReadOnly` INTEGER NOT NULL, `displayName` TEXT, `description` TEXT, `color` INTEGER, `timezoneId` TEXT, `supportsVEVENT` INTEGER, `supportsVTODO` INTEGER, `supportsVJOURNAL` INTEGER, `source` TEXT, `sync` INTEGER NOT NULL, `pushTopic` TEXT, `supportsWebPush` INTEGER NOT NULL DEFAULT 0, `pushSubscription` TEXT, `pushSubscriptionExpires` INTEGER, `pushSubscriptionCreated` INTEGER, FOREIGN KEY(`serviceId`) REFERENCES `service`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`homeSetId`) REFERENCES `homeset`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL , FOREIGN KEY(`ownerId`) REFERENCES `principal`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "serviceId", + "columnName": "serviceId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "homeSetId", + "columnName": "homeSetId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "ownerId", + "columnName": "ownerId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "privWriteContent", + "columnName": "privWriteContent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "privUnbind", + "columnName": "privUnbind", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "forceReadOnly", + "columnName": "forceReadOnly", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "color", + "columnName": "color", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "timezoneId", + "columnName": "timezoneId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "supportsVEVENT", + "columnName": "supportsVEVENT", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "supportsVTODO", + "columnName": "supportsVTODO", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "supportsVJOURNAL", + "columnName": "supportsVJOURNAL", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "source", + "columnName": "source", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sync", + "columnName": "sync", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pushTopic", + "columnName": "pushTopic", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "supportsWebPush", + "columnName": "supportsWebPush", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "pushSubscription", + "columnName": "pushSubscription", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pushSubscriptionExpires", + "columnName": "pushSubscriptionExpires", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "pushSubscriptionCreated", + "columnName": "pushSubscriptionCreated", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_collection_serviceId_type", + "unique": false, + "columnNames": [ + "serviceId", + "type" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_collection_serviceId_type` ON `${TABLE_NAME}` (`serviceId`, `type`)" + }, + { + "name": "index_collection_homeSetId_type", + "unique": false, + "columnNames": [ + "homeSetId", + "type" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_collection_homeSetId_type` ON `${TABLE_NAME}` (`homeSetId`, `type`)" + }, + { + "name": "index_collection_ownerId_type", + "unique": false, + "columnNames": [ + "ownerId", + "type" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_collection_ownerId_type` ON `${TABLE_NAME}` (`ownerId`, `type`)" + }, + { + "name": "index_collection_pushTopic_type", + "unique": false, + "columnNames": [ + "pushTopic", + "type" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_collection_pushTopic_type` ON `${TABLE_NAME}` (`pushTopic`, `type`)" + }, + { + "name": "index_collection_url", + "unique": false, + "columnNames": [ + "url" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_collection_url` ON `${TABLE_NAME}` (`url`)" + } + ], + "foreignKeys": [ + { + "table": "service", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "serviceId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "homeset", + "onDelete": "SET NULL", + "onUpdate": "NO ACTION", + "columns": [ + "homeSetId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "principal", + "onDelete": "SET NULL", + "onUpdate": "NO ACTION", + "columns": [ + "ownerId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "principal", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `serviceId` INTEGER NOT NULL, `url` TEXT NOT NULL, `displayName` TEXT, FOREIGN KEY(`serviceId`) REFERENCES `service`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "serviceId", + "columnName": "serviceId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_principal_serviceId_url", + "unique": true, + "columnNames": [ + "serviceId", + "url" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_principal_serviceId_url` ON `${TABLE_NAME}` (`serviceId`, `url`)" + } + ], + "foreignKeys": [ + { + "table": "service", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "serviceId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "syncstats", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `collectionId` INTEGER NOT NULL, `authority` TEXT NOT NULL, `lastSync` INTEGER NOT NULL, FOREIGN KEY(`collectionId`) REFERENCES `collection`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "collectionId", + "columnName": "collectionId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authority", + "columnName": "authority", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastSync", + "columnName": "lastSync", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_syncstats_collectionId_authority", + "unique": true, + "columnNames": [ + "collectionId", + "authority" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_syncstats_collectionId_authority` ON `${TABLE_NAME}` (`collectionId`, `authority`)" + } + ], + "foreignKeys": [ + { + "table": "collection", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "collectionId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "webdav_document", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `mountId` INTEGER NOT NULL, `parentId` INTEGER, `name` TEXT NOT NULL, `isDirectory` INTEGER NOT NULL, `displayName` TEXT, `mimeType` TEXT, `eTag` TEXT, `lastModified` INTEGER, `size` INTEGER, `mayBind` INTEGER, `mayUnbind` INTEGER, `mayWriteContent` INTEGER, `quotaAvailable` INTEGER, `quotaUsed` INTEGER, FOREIGN KEY(`mountId`) REFERENCES `webdav_mount`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`parentId`) REFERENCES `webdav_document`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mountId", + "columnName": "mountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "parentId", + "columnName": "parentId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isDirectory", + "columnName": "isDirectory", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mimeType", + "columnName": "mimeType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "eTag", + "columnName": "eTag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastModified", + "columnName": "lastModified", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "mayBind", + "columnName": "mayBind", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "mayUnbind", + "columnName": "mayUnbind", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "mayWriteContent", + "columnName": "mayWriteContent", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "quotaAvailable", + "columnName": "quotaAvailable", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "quotaUsed", + "columnName": "quotaUsed", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_webdav_document_mountId_parentId_name", + "unique": true, + "columnNames": [ + "mountId", + "parentId", + "name" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_webdav_document_mountId_parentId_name` ON `${TABLE_NAME}` (`mountId`, `parentId`, `name`)" + }, + { + "name": "index_webdav_document_parentId", + "unique": false, + "columnNames": [ + "parentId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_webdav_document_parentId` ON `${TABLE_NAME}` (`parentId`)" + } + ], + "foreignKeys": [ + { + "table": "webdav_mount", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "mountId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "webdav_document", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "parentId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "webdav_mount", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '3e8ce358d26c057ba1dfc24de3ac5ba5')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/at.bitfire.davdroid.db.AppDatabase/18.json b/app/schemas/at.bitfire.davdroid.db.AppDatabase/18.json new file mode 100644 index 000000000..7cff568c7 --- /dev/null +++ b/app/schemas/at.bitfire.davdroid.db.AppDatabase/18.json @@ -0,0 +1,723 @@ +{ + "formatVersion": 1, + "database": { + "version": 18, + "identityHash": "047898fb582f45636ade0f1d092e3260", + "entities": [ + { + "tableName": "account", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_account_name", + "unique": true, + "columnNames": [ + "name" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_account_name` ON `${TABLE_NAME}` (`name`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "service", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `accountName` TEXT NOT NULL, `type` TEXT NOT NULL, `principal` TEXT, FOREIGN KEY(`accountName`) REFERENCES `account`(`name`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountName", + "columnName": "accountName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "principal", + "columnName": "principal", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_service_accountName_type", + "unique": true, + "columnNames": [ + "accountName", + "type" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_service_accountName_type` ON `${TABLE_NAME}` (`accountName`, `type`)" + } + ], + "foreignKeys": [ + { + "table": "account", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "accountName" + ], + "referencedColumns": [ + "name" + ] + } + ] + }, + { + "tableName": "homeset", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `serviceId` INTEGER NOT NULL, `personal` INTEGER NOT NULL, `url` TEXT NOT NULL, `privBind` INTEGER NOT NULL, `displayName` TEXT, FOREIGN KEY(`serviceId`) REFERENCES `service`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "serviceId", + "columnName": "serviceId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "personal", + "columnName": "personal", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "privBind", + "columnName": "privBind", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_homeset_serviceId_url", + "unique": true, + "columnNames": [ + "serviceId", + "url" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_homeset_serviceId_url` ON `${TABLE_NAME}` (`serviceId`, `url`)" + } + ], + "foreignKeys": [ + { + "table": "service", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "serviceId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "collection", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `serviceId` INTEGER NOT NULL, `homeSetId` INTEGER, `ownerId` INTEGER, `type` TEXT NOT NULL, `url` TEXT NOT NULL, `privWriteContent` INTEGER NOT NULL, `privUnbind` INTEGER NOT NULL, `forceReadOnly` INTEGER NOT NULL, `displayName` TEXT, `description` TEXT, `color` INTEGER, `timezoneId` TEXT, `supportsVEVENT` INTEGER, `supportsVTODO` INTEGER, `supportsVJOURNAL` INTEGER, `source` TEXT, `sync` INTEGER NOT NULL, `pushTopic` TEXT, `supportsWebPush` INTEGER NOT NULL DEFAULT 0, `pushSubscription` TEXT, `pushSubscriptionExpires` INTEGER, `pushSubscriptionCreated` INTEGER, FOREIGN KEY(`serviceId`) REFERENCES `service`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`homeSetId`) REFERENCES `homeset`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL , FOREIGN KEY(`ownerId`) REFERENCES `principal`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "serviceId", + "columnName": "serviceId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "homeSetId", + "columnName": "homeSetId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "ownerId", + "columnName": "ownerId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "privWriteContent", + "columnName": "privWriteContent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "privUnbind", + "columnName": "privUnbind", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "forceReadOnly", + "columnName": "forceReadOnly", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "color", + "columnName": "color", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "timezoneId", + "columnName": "timezoneId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "supportsVEVENT", + "columnName": "supportsVEVENT", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "supportsVTODO", + "columnName": "supportsVTODO", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "supportsVJOURNAL", + "columnName": "supportsVJOURNAL", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "source", + "columnName": "source", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sync", + "columnName": "sync", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pushTopic", + "columnName": "pushTopic", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "supportsWebPush", + "columnName": "supportsWebPush", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "pushSubscription", + "columnName": "pushSubscription", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pushSubscriptionExpires", + "columnName": "pushSubscriptionExpires", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "pushSubscriptionCreated", + "columnName": "pushSubscriptionCreated", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_collection_serviceId_type", + "unique": false, + "columnNames": [ + "serviceId", + "type" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_collection_serviceId_type` ON `${TABLE_NAME}` (`serviceId`, `type`)" + }, + { + "name": "index_collection_homeSetId_type", + "unique": false, + "columnNames": [ + "homeSetId", + "type" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_collection_homeSetId_type` ON `${TABLE_NAME}` (`homeSetId`, `type`)" + }, + { + "name": "index_collection_ownerId_type", + "unique": false, + "columnNames": [ + "ownerId", + "type" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_collection_ownerId_type` ON `${TABLE_NAME}` (`ownerId`, `type`)" + }, + { + "name": "index_collection_pushTopic_type", + "unique": false, + "columnNames": [ + "pushTopic", + "type" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_collection_pushTopic_type` ON `${TABLE_NAME}` (`pushTopic`, `type`)" + }, + { + "name": "index_collection_url", + "unique": false, + "columnNames": [ + "url" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_collection_url` ON `${TABLE_NAME}` (`url`)" + } + ], + "foreignKeys": [ + { + "table": "service", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "serviceId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "homeset", + "onDelete": "SET NULL", + "onUpdate": "NO ACTION", + "columns": [ + "homeSetId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "principal", + "onDelete": "SET NULL", + "onUpdate": "NO ACTION", + "columns": [ + "ownerId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "principal", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `serviceId` INTEGER NOT NULL, `url` TEXT NOT NULL, `displayName` TEXT, FOREIGN KEY(`serviceId`) REFERENCES `service`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "serviceId", + "columnName": "serviceId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_principal_serviceId_url", + "unique": true, + "columnNames": [ + "serviceId", + "url" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_principal_serviceId_url` ON `${TABLE_NAME}` (`serviceId`, `url`)" + } + ], + "foreignKeys": [ + { + "table": "service", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "serviceId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "syncstats", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `collectionId` INTEGER NOT NULL, `authority` TEXT NOT NULL, `lastSync` INTEGER NOT NULL, FOREIGN KEY(`collectionId`) REFERENCES `collection`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "collectionId", + "columnName": "collectionId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authority", + "columnName": "authority", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastSync", + "columnName": "lastSync", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_syncstats_collectionId_authority", + "unique": true, + "columnNames": [ + "collectionId", + "authority" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_syncstats_collectionId_authority` ON `${TABLE_NAME}` (`collectionId`, `authority`)" + } + ], + "foreignKeys": [ + { + "table": "collection", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "collectionId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "webdav_document", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `mountId` INTEGER NOT NULL, `parentId` INTEGER, `name` TEXT NOT NULL, `isDirectory` INTEGER NOT NULL, `displayName` TEXT, `mimeType` TEXT, `eTag` TEXT, `lastModified` INTEGER, `size` INTEGER, `mayBind` INTEGER, `mayUnbind` INTEGER, `mayWriteContent` INTEGER, `quotaAvailable` INTEGER, `quotaUsed` INTEGER, FOREIGN KEY(`mountId`) REFERENCES `webdav_mount`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`parentId`) REFERENCES `webdav_document`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mountId", + "columnName": "mountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "parentId", + "columnName": "parentId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isDirectory", + "columnName": "isDirectory", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mimeType", + "columnName": "mimeType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "eTag", + "columnName": "eTag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastModified", + "columnName": "lastModified", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "mayBind", + "columnName": "mayBind", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "mayUnbind", + "columnName": "mayUnbind", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "mayWriteContent", + "columnName": "mayWriteContent", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "quotaAvailable", + "columnName": "quotaAvailable", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "quotaUsed", + "columnName": "quotaUsed", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_webdav_document_mountId_parentId_name", + "unique": true, + "columnNames": [ + "mountId", + "parentId", + "name" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_webdav_document_mountId_parentId_name` ON `${TABLE_NAME}` (`mountId`, `parentId`, `name`)" + }, + { + "name": "index_webdav_document_parentId", + "unique": false, + "columnNames": [ + "parentId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_webdav_document_parentId` ON `${TABLE_NAME}` (`parentId`)" + } + ], + "foreignKeys": [ + { + "table": "webdav_mount", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "mountId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "webdav_document", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "parentId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "webdav_mount", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '047898fb582f45636ade0f1d092e3260')" + ] + } +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/at/bitfire/davdroid/db/migration/AutoMigration16Test.kt b/app/src/androidTest/kotlin/at/bitfire/davdroid/db/migration/AutoMigration16Test.kt index 38a910a6c..8d4ede45d 100644 --- a/app/src/androidTest/kotlin/at/bitfire/davdroid/db/migration/AutoMigration16Test.kt +++ b/app/src/androidTest/kotlin/at/bitfire/davdroid/db/migration/AutoMigration16Test.kt @@ -12,7 +12,7 @@ import org.junit.Assert.assertNull import org.junit.Test @HiltAndroidTest -class AutoMigration16Test: DatabaseMigrationTest(toVersion = 16) { +class AutoMigration16Test: DatabaseMigrationTest(fromVersion = 15, toVersion = 16) { @Test fun testMigrate_WithTimeZone() = testMigration( diff --git a/app/src/androidTest/kotlin/at/bitfire/davdroid/db/migration/DatabaseMigrationTest.kt b/app/src/androidTest/kotlin/at/bitfire/davdroid/db/migration/DatabaseMigrationTest.kt index b0940512f..0eb961953 100644 --- a/app/src/androidTest/kotlin/at/bitfire/davdroid/db/migration/DatabaseMigrationTest.kt +++ b/app/src/androidTest/kotlin/at/bitfire/davdroid/db/migration/DatabaseMigrationTest.kt @@ -17,11 +17,13 @@ import org.junit.Rule import javax.inject.Inject /** - * Helper for testing the database migration from [toVersion] - 1 to [toVersion]. + * Helper for testing the database migration from [fromVersion] to [toVersion]. * + * @param fromVersion The source version to migrate from (usually `toVersion - 1`). * @param toVersion The target version to migrate to. */ abstract class DatabaseMigrationTest( + private val fromVersion: Int, private val toVersion: Int ) { @@ -42,14 +44,14 @@ abstract class DatabaseMigrationTest( /** - * Used for testing the migration process from [toVersion]-1 to [toVersion]. + * Used for testing the migration process from [fromVersion] to [toVersion]. * - * @param prepare Callback to prepare the database. Will be run with database schema in version [toVersion] - 1. - * @param validate Callback to validate the migration result. Will be run with database schema in version [toVersion]. + * @param prepare Callback to prepare the database. Will be run with database schema in version [fromVersion]. + * @param verify Callback to verify the migration result. Will be run with database schema in version [toVersion]. */ protected fun testMigration( prepare: (SupportSQLiteDatabase) -> Unit, - validate: (SupportSQLiteDatabase) -> Unit + verify: (SupportSQLiteDatabase) -> Unit ) { val helper = MigrationTestHelper( InstrumentationRegistry.getInstrumentation(), @@ -60,7 +62,7 @@ abstract class DatabaseMigrationTest( // Prepare the database with the initial version. val dbName = "test" - helper.createDatabase(dbName, version = toVersion - 1).apply { + helper.createDatabase(dbName, version = fromVersion).apply { prepare(this) close() } @@ -73,7 +75,7 @@ abstract class DatabaseMigrationTest( migrations = manualMigrations.toTypedArray() ) - validate(db) + verify(db) } } \ No newline at end of file diff --git a/app/src/androidTest/kotlin/at/bitfire/davdroid/db/migration/Migration17_18Test.kt b/app/src/androidTest/kotlin/at/bitfire/davdroid/db/migration/Migration17_18Test.kt new file mode 100644 index 000000000..919f9a878 --- /dev/null +++ b/app/src/androidTest/kotlin/at/bitfire/davdroid/db/migration/Migration17_18Test.kt @@ -0,0 +1,55 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.db.migration + +import at.bitfire.davdroid.db.Service +import dagger.hilt.android.testing.HiltAndroidTest +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test + +@Suppress("ClassName") +@HiltAndroidTest +class Migration17_18Test: DatabaseMigrationTest(fromVersion = 16, toVersion = 18) { + + @Test + fun testMigrate_WithServices() = testMigration( + prepare = { db -> + db.execSQL( + "INSERT INTO service (accountName, type) VALUES (?, ?)", + arrayOf("test1", Service.Companion.TYPE_CALDAV) + ) + db.execSQL( + "INSERT INTO service (accountName, type) VALUES (?, ?)", + arrayOf("test1", Service.Companion.TYPE_CARDDAV) + ) + + db.execSQL( + "INSERT INTO service (accountName, type) VALUES (?, ?)", + arrayOf("test2", Service.Companion.TYPE_CALDAV) + ) + + db.execSQL( + "INSERT INTO service (accountName, type) VALUES (?, ?)", + arrayOf("test3", Service.Companion.TYPE_CARDDAV) + ) + } + ) { db -> + db.query("SELECT name FROM account ORDER BY name").use { cursor -> + assertTrue(cursor.moveToFirst()) + assertEquals("test1", cursor.getString(0)) + + assertTrue(cursor.moveToNext()) + assertEquals("test2", cursor.getString(0)) + + assertTrue(cursor.moveToNext()) + assertEquals("test3", cursor.getString(0)) + + assertFalse(cursor.moveToNext()) + } + } + +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/at/bitfire/davdroid/repository/AccountRepositoryTest.kt b/app/src/androidTest/kotlin/at/bitfire/davdroid/repository/AccountRepositoryTest.kt new file mode 100644 index 000000000..6590fadf0 --- /dev/null +++ b/app/src/androidTest/kotlin/at/bitfire/davdroid/repository/AccountRepositoryTest.kt @@ -0,0 +1,55 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.repository + +import android.content.Context +import at.bitfire.davdroid.db.AppDatabase +import at.bitfire.davdroid.sync.account.TestAccount +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.android.testing.HiltAndroidRule +import dagger.hilt.android.testing.HiltAndroidTest +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import javax.inject.Inject +import at.bitfire.davdroid.db.Account as DbAccount + +@HiltAndroidTest +class AccountRepositoryTest { + + @Inject + lateinit var accountRepository: AccountRepository + + @Inject @ApplicationContext + lateinit var context: Context + + @Inject + lateinit var db: AppDatabase + + @get:Rule + val hiltRule = HiltAndroidRule(this) + + @Before + fun setUp() { + hiltRule.inject() + } + + + @Test + fun testRemoveOrphanedInDb() { + TestAccount.provide { systemAccount -> + val dao = db.accountDao() + dao.insertOrIgnore(DbAccount(id = 1, name = systemAccount.name)) + dao.insertOrIgnore(DbAccount(id = 2, name = "no-corresponding-system-account")) + + accountRepository.removeOrphanedInDb() + + // now the account without a corresponding system account should be removed + assertEquals(listOf(DbAccount(id = 1, name = systemAccount.name)), db.accountDao().getAll()) + } + } + +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/at/bitfire/davdroid/repository/DavCollectionRepositoryTest.kt b/app/src/androidTest/kotlin/at/bitfire/davdroid/repository/DavCollectionRepositoryTest.kt index 09a06e3de..f5c21f606 100644 --- a/app/src/androidTest/kotlin/at/bitfire/davdroid/repository/DavCollectionRepositoryTest.kt +++ b/app/src/androidTest/kotlin/at/bitfire/davdroid/repository/DavCollectionRepositoryTest.kt @@ -1,10 +1,12 @@ package at.bitfire.davdroid.repository +import android.accounts.Account import android.content.Context import at.bitfire.davdroid.db.AppDatabase import at.bitfire.davdroid.db.Collection import at.bitfire.davdroid.db.Service import at.bitfire.davdroid.settings.AccountSettings +import at.bitfire.davdroid.sync.account.TestAccount import dagger.Lazy import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.testing.HiltAndroidRule @@ -18,6 +20,7 @@ import org.junit.Before import org.junit.Rule import org.junit.Test import javax.inject.Inject +import at.bitfire.davdroid.db.Account as DbAccount @HiltAndroidTest class DavCollectionRepositoryTest { @@ -25,6 +28,9 @@ class DavCollectionRepositoryTest { @get:Rule var hiltRule = HiltAndroidRule(this) + @Inject + lateinit var accountRepository: AccountRepository + @Inject lateinit var accountSettingsFactory: AccountSettings.Factory @@ -38,18 +44,24 @@ class DavCollectionRepositoryTest { @Inject lateinit var serviceRepository: DavServiceRepository - var service: Service? = null + lateinit var account: Account + var serviceId: Long = 0L @Before fun setUp() { hiltRule.inject() - service = createTestService(Service.TYPE_CARDDAV)!! + + account = TestAccount.create() + db.accountDao().insertOrIgnore(DbAccount(name = account.name)) + + val service = Service(id=0, accountName=account.name, type= Service.TYPE_CALDAV, principal = null) + serviceId = serviceRepository.insertOrReplace(service) } @After - fun cleanUp() { - db.close() + fun tearDown() { serviceRepository.deleteAll() + TestAccount.remove(account) } @@ -57,7 +69,7 @@ class DavCollectionRepositoryTest { fun testOnChangeListener_setForceReadOnly() = runBlocking { val collectionId = db.collectionDao().insertOrUpdateByUrl( Collection( - serviceId = service!!.id, + serviceId = serviceId, type = Collection.TYPE_ADDRESSBOOK, url = "https://example.com".toHttpUrl(), forceReadOnly = false, @@ -65,6 +77,7 @@ class DavCollectionRepositoryTest { ) val testObserver = mockk(relaxed = true) val collectionRepository = DavCollectionRepository( + accountRepository, accountSettingsFactory, context, db, @@ -87,13 +100,4 @@ class DavCollectionRepositoryTest { } } - - // Test helpers and dependencies - - private fun createTestService(serviceType: String) : Service? { - val service = Service(id=0, accountName="test", type=serviceType, principal = null) - val serviceId = serviceRepository.insertOrReplace(service) - return serviceRepository.get(serviceId) - } - } \ No newline at end of file diff --git a/app/src/androidTest/kotlin/at/bitfire/davdroid/repository/DavHomeSetRepositoryTest.kt b/app/src/androidTest/kotlin/at/bitfire/davdroid/repository/DavHomeSetRepositoryTest.kt index 1af05be25..344656c3d 100644 --- a/app/src/androidTest/kotlin/at/bitfire/davdroid/repository/DavHomeSetRepositoryTest.kt +++ b/app/src/androidTest/kotlin/at/bitfire/davdroid/repository/DavHomeSetRepositoryTest.kt @@ -4,16 +4,21 @@ package at.bitfire.davdroid.repository +import android.accounts.Account +import at.bitfire.davdroid.db.AppDatabase import at.bitfire.davdroid.db.HomeSet import at.bitfire.davdroid.db.Service +import at.bitfire.davdroid.sync.account.TestAccount import dagger.hilt.android.testing.HiltAndroidRule import dagger.hilt.android.testing.HiltAndroidTest import okhttp3.HttpUrl.Companion.toHttpUrl +import org.junit.After import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Rule import org.junit.Test import javax.inject.Inject +import at.bitfire.davdroid.db.Account as DbAccount @HiltAndroidTest class DavHomeSetRepositoryTest { @@ -21,23 +26,39 @@ class DavHomeSetRepositoryTest { @get:Rule var hiltRule = HiltAndroidRule(this) + @Inject + lateinit var db: AppDatabase + @Inject lateinit var repository: DavHomeSetRepository @Inject lateinit var serviceRepository: DavServiceRepository + lateinit var account: Account + var serviceId: Long = 0L + + @Before fun setUp() { hiltRule.inject() + + account = TestAccount.create() + db.accountDao().insertOrIgnore(DbAccount(name = account.name)) + + val service = Service(id=0, accountName=account.name, type= Service.TYPE_CALDAV, principal = null) + serviceId = serviceRepository.insertOrReplace(service) + } + + @After + fun tearDown() { + TestAccount.remove(account) } @Test fun testInsertOrUpdate() { // should insert new row or update (upsert) existing row - without changing its key! - val serviceId = createTestService() - val entry1 = HomeSet(id=0, serviceId=serviceId, personal=true, url="https://example.com/1".toHttpUrl()) val insertId1 = repository.insertOrUpdateByUrl(entry1) assertEquals(1L, insertId1) @@ -57,8 +78,6 @@ class DavHomeSetRepositoryTest { @Test fun testDelete() { // should delete row with given primary key (id) - val serviceId = createTestService() - val entry1 = HomeSet(id=1, serviceId=serviceId, personal=true, url= "https://example.com/1".toHttpUrl()) val insertId1 = repository.insertOrUpdateByUrl(entry1) @@ -69,10 +88,4 @@ class DavHomeSetRepositoryTest { assertEquals(null, repository.getById(1L)) } - - private fun createTestService() : Long { - val service = Service(id=0, accountName="test", type= Service.TYPE_CALDAV, principal = null) - return serviceRepository.insertOrReplace(service) - } - } \ No newline at end of file diff --git a/app/src/androidTest/kotlin/at/bitfire/davdroid/resource/LocalTestAddressBook.kt b/app/src/androidTest/kotlin/at/bitfire/davdroid/resource/LocalTestAddressBook.kt index 0c501b898..1cebec3dc 100644 --- a/app/src/androidTest/kotlin/at/bitfire/davdroid/resource/LocalTestAddressBook.kt +++ b/app/src/androidTest/kotlin/at/bitfire/davdroid/resource/LocalTestAddressBook.kt @@ -11,6 +11,7 @@ import android.content.ContentUris import android.content.Context import android.provider.ContactsContract import at.bitfire.davdroid.R +import at.bitfire.davdroid.repository.AccountRepository import at.bitfire.davdroid.repository.DavCollectionRepository import at.bitfire.davdroid.repository.DavServiceRepository import at.bitfire.davdroid.settings.AccountSettings @@ -34,6 +35,7 @@ class LocalTestAddressBook @AssistedInject constructor( @Assisted("addressBook") addressBookAccount: Account, @Assisted provider: ContentProviderClient, @Assisted override val groupMethod: GroupMethod, + accountRepository: AccountRepository, accountSettingsFactory: AccountSettings.Factory, collectionRepository: DavCollectionRepository, @ApplicationContext private val context: Context, @@ -44,6 +46,7 @@ class LocalTestAddressBook @AssistedInject constructor( account = account, _addressBookAccount = addressBookAccount, provider = provider, + accountRepository = accountRepository, accountSettingsFactory = accountSettingsFactory, collectionRepository = collectionRepository, context = context, diff --git a/app/src/androidTest/kotlin/at/bitfire/davdroid/servicedetection/CollectionListRefresherTest.kt b/app/src/androidTest/kotlin/at/bitfire/davdroid/servicedetection/CollectionListRefresherTest.kt index 83fa34376..52e9ae8cc 100644 --- a/app/src/androidTest/kotlin/at/bitfire/davdroid/servicedetection/CollectionListRefresherTest.kt +++ b/app/src/androidTest/kotlin/at/bitfire/davdroid/servicedetection/CollectionListRefresherTest.kt @@ -4,6 +4,7 @@ package at.bitfire.davdroid.servicedetection +import android.accounts.Account import android.content.Context import android.security.NetworkSecurityPolicy import at.bitfire.davdroid.db.AppDatabase @@ -14,6 +15,7 @@ import at.bitfire.davdroid.db.Service import at.bitfire.davdroid.network.HttpClient import at.bitfire.davdroid.settings.Settings import at.bitfire.davdroid.settings.SettingsManager +import at.bitfire.davdroid.sync.account.TestAccount import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.testing.HiltAndroidRule import dagger.hilt.android.testing.HiltAndroidTest @@ -33,6 +35,7 @@ import org.junit.Rule import org.junit.Test import java.util.logging.Logger import javax.inject.Inject +import at.bitfire.davdroid.db.Account as DbAccount @HiltAndroidTest class CollectionListRefresherTest { @@ -69,6 +72,8 @@ class CollectionListRefresherTest { @Inject lateinit var settings: SettingsManager + lateinit var account: Account + private val mockServer = MockWebServer() private lateinit var client: HttpClient @@ -76,6 +81,9 @@ class CollectionListRefresherTest { fun setup() { hiltRule.inject() + account = TestAccount.create() + db.accountDao().insertOrIgnore(DbAccount(name = account.name)) + // Start mock web server mockServer.dispatcher = TestDispatcher(logger) mockServer.start() @@ -88,13 +96,13 @@ class CollectionListRefresherTest { @After fun teardown() { mockServer.shutdown() - db.close() + TestAccount.remove(account) } @Test fun testDiscoverHomesets() { - val service = createTestService(Service.TYPE_CARDDAV)!! + val service = createTestService() val baseUrl = mockServer.url(PATH_CARDDAV + SUBPATH_PRINCIPAL) // Query home sets @@ -110,7 +118,7 @@ class CollectionListRefresherTest { @Test fun refreshHomesetsAndTheirCollections_addsNewCollection() { - val service = createTestService(Service.TYPE_CARDDAV)!! + val service = createTestService() // save homeset in DB val homesetId = db.homeSetDao().insert( @@ -138,7 +146,7 @@ class CollectionListRefresherTest { @Test fun refreshHomesetsAndTheirCollections_updatesExistingCollection() { - val service = createTestService(Service.TYPE_CARDDAV)!! + val service = createTestService() // save "old" collection in DB val collectionId = db.collectionDao().insertOrUpdateByUrl( @@ -175,7 +183,7 @@ class CollectionListRefresherTest { @Test fun refreshHomesetsAndTheirCollections_preservesCollectionFlags() { - val service = createTestService(Service.TYPE_CARDDAV)!! + val service = createTestService() // save "old" collection in DB - with set flags val collectionId = db.collectionDao().insertOrUpdateByUrl( @@ -216,7 +224,7 @@ class CollectionListRefresherTest { @Test fun refreshHomesetsAndTheirCollections_marksRemovedCollectionsAsHomeless() { - val service = createTestService(Service.TYPE_CARDDAV)!! + val service = createTestService() // save homeset in DB - which is empty (zero address books) on the serverside val homesetId = db.homeSetDao().insert( @@ -244,7 +252,7 @@ class CollectionListRefresherTest { @Test fun refreshHomesetsAndTheirCollections_addsOwnerUrls() { - val service = createTestService(Service.TYPE_CARDDAV)!! + val service = createTestService() // save a homeset in DB val homesetId = db.homeSetDao().insert( @@ -283,7 +291,7 @@ class CollectionListRefresherTest { @Test fun refreshHomelessCollections_updatesExistingCollection() { - val service = createTestService(Service.TYPE_CARDDAV)!! + val service = createTestService() // place homeless collection in DB val collectionId = db.collectionDao().insertOrUpdateByUrl( @@ -318,7 +326,7 @@ class CollectionListRefresherTest { @Test fun refreshHomelessCollections_deletesInaccessibleCollections() { - val service = createTestService(Service.TYPE_CARDDAV)!! + val service = createTestService() // place homeless collection in DB - it is also inaccessible val collectionId = db.collectionDao().insertOrUpdateByUrl( @@ -341,7 +349,7 @@ class CollectionListRefresherTest { @Test fun refreshHomelessCollections_addsOwnerUrls() { - val service = createTestService(Service.TYPE_CARDDAV)!! + val service = createTestService() // place homeless collection in DB val collectionId = db.collectionDao().insertOrUpdateByUrl( @@ -375,7 +383,7 @@ class CollectionListRefresherTest { @Test fun refreshPrincipals_inaccessiblePrincipal() { - val service = createTestService(Service.TYPE_CARDDAV)!! + val service = createTestService() // place principal without display name in db val principalId = db.principalDao().insert( @@ -410,7 +418,7 @@ class CollectionListRefresherTest { @Test fun refreshPrincipals_updatesPrincipal() { - val service = createTestService(Service.TYPE_CARDDAV)!! + val service = createTestService() // place principal without display name in db val principalId = db.principalDao().insert( @@ -445,7 +453,7 @@ class CollectionListRefresherTest { @Test fun refreshPrincipals_deletesPrincipalsWithoutCollections() { - val service = createTestService(Service.TYPE_CARDDAV)!! + val service = createTestService() // place principal without collections in DB db.principalDao().insert( @@ -468,7 +476,7 @@ class CollectionListRefresherTest { @Test fun shouldPreselect_none() { - val service = createTestService(Service.TYPE_CARDDAV)!! + val service = createTestService() mockkObject(settings) { every { settings.getIntOrNull(Settings.PRESELECT_COLLECTIONS) } returns Settings.PRESELECT_COLLECTIONS_NONE @@ -492,7 +500,7 @@ class CollectionListRefresherTest { @Test fun shouldPreselect_all() { - val service = createTestService(Service.TYPE_CARDDAV)!! + val service = createTestService() mockkObject(settings) { every { settings.getIntOrNull(Settings.PRESELECT_COLLECTIONS) } returns Settings.PRESELECT_COLLECTIONS_ALL @@ -516,7 +524,7 @@ class CollectionListRefresherTest { @Test fun shouldPreselect_all_blacklisted() { - val service = createTestService(Service.TYPE_CARDDAV)!! + val service = createTestService() val url = mockServer.url("$PATH_CARDDAV$SUBPATH_ADDRESSBOOK/") mockkObject(settings) { @@ -541,7 +549,7 @@ class CollectionListRefresherTest { @Test fun shouldPreselect_personal_notPersonal() { - val service = createTestService(Service.TYPE_CARDDAV)!! + val service = createTestService() mockkObject(settings) { every { settings.getIntOrNull(Settings.PRESELECT_COLLECTIONS) } returns Settings.PRESELECT_COLLECTIONS_PERSONAL @@ -565,7 +573,7 @@ class CollectionListRefresherTest { @Test fun shouldPreselect_personal_isPersonal() { - val service = createTestService(Service.TYPE_CARDDAV)!! + val service = createTestService() mockkObject(settings) { every { settings.getIntOrNull(Settings.PRESELECT_COLLECTIONS) } returns Settings.PRESELECT_COLLECTIONS_PERSONAL @@ -589,7 +597,7 @@ class CollectionListRefresherTest { @Test fun shouldPreselect_personal_isPersonalButBlacklisted() { - val service = createTestService(Service.TYPE_CARDDAV)!! + val service = createTestService() val collectionUrl = mockServer.url("$PATH_CARDDAV$SUBPATH_ADDRESSBOOK/") mockkObject(settings) { @@ -614,10 +622,10 @@ class CollectionListRefresherTest { // Test helpers and dependencies - private fun createTestService(serviceType: String) : Service? { - val service = Service(id=0, accountName="test", type=serviceType, principal = null) + private fun createTestService(): Service { + val service = Service(id = 0, accountName = account.name, type = Service.TYPE_CARDDAV, principal = null) val serviceId = db.serviceDao().insertOrReplace(service) - return db.serviceDao().get(serviceId) + return db.serviceDao().get(serviceId)!! } diff --git a/app/src/androidTest/kotlin/at/bitfire/davdroid/settings/migration/AccountSettingsMigration17Test.kt b/app/src/androidTest/kotlin/at/bitfire/davdroid/settings/migration/AccountSettingsMigration17Test.kt index b45de7891..c3b773970 100644 --- a/app/src/androidTest/kotlin/at/bitfire/davdroid/settings/migration/AccountSettingsMigration17Test.kt +++ b/app/src/androidTest/kotlin/at/bitfire/davdroid/settings/migration/AccountSettingsMigration17Test.kt @@ -25,6 +25,7 @@ import org.junit.Before import org.junit.Rule import org.junit.Test import javax.inject.Inject +import at.bitfire.davdroid.db.Account as DbAccount @HiltAndroidTest class AccountSettingsMigration17Test { @@ -66,6 +67,7 @@ class AccountSettingsMigration17Test { accountManager.setAndVerifyUserData(addressBookAccount, LocalAddressBook.USER_DATA_URL, url) // and is known in database + db.accountDao().insertOrIgnore(DbAccount(name = account.name)) db.serviceDao().insertOrReplace( Service( id = 1, accountName = account.name, type = Service.TYPE_CARDDAV, principal = null diff --git a/app/src/androidTest/kotlin/at/bitfire/davdroid/sync/SyncAdapterServicesTest.kt b/app/src/androidTest/kotlin/at/bitfire/davdroid/sync/SyncAdapterServicesTest.kt index 6dfe5a615..9bf5055f7 100644 --- a/app/src/androidTest/kotlin/at/bitfire/davdroid/sync/SyncAdapterServicesTest.kt +++ b/app/src/androidTest/kotlin/at/bitfire/davdroid/sync/SyncAdapterServicesTest.kt @@ -13,6 +13,7 @@ import androidx.hilt.work.HiltWorkerFactory import androidx.work.WorkInfo import androidx.work.WorkManager import at.bitfire.davdroid.TestUtils +import at.bitfire.davdroid.repository.AccountRepository import at.bitfire.davdroid.repository.DavCollectionRepository import at.bitfire.davdroid.repository.DavServiceRepository import at.bitfire.davdroid.settings.AccountSettings @@ -49,6 +50,9 @@ class SyncAdapterServicesTest { lateinit var account: Account + @Inject + lateinit var accountRepository: AccountRepository + @Inject lateinit var accountSettingsFactory: AccountSettings.Factory @@ -98,6 +102,7 @@ class SyncAdapterServicesTest { syncWorkerManager: SyncWorkerManager ): SyncAdapterService.SyncAdapter = SyncAdapterService.SyncAdapter( + accountRepository = accountRepository, accountSettingsFactory = accountSettingsFactory, collectionRepository = collectionRepository, serviceRepository = serviceRepository, diff --git a/app/src/androidTest/kotlin/at/bitfire/davdroid/sync/account/AccountsCleanupWorkerTest.kt b/app/src/androidTest/kotlin/at/bitfire/davdroid/sync/account/AccountsCleanupWorkerTest.kt index bdd80ff68..2a214c169 100644 --- a/app/src/androidTest/kotlin/at/bitfire/davdroid/sync/account/AccountsCleanupWorkerTest.kt +++ b/app/src/androidTest/kotlin/at/bitfire/davdroid/sync/account/AccountsCleanupWorkerTest.kt @@ -11,20 +11,18 @@ import at.bitfire.davdroid.TestUtils import at.bitfire.davdroid.db.AppDatabase import at.bitfire.davdroid.db.Service import at.bitfire.davdroid.resource.LocalAddressBook -import at.bitfire.davdroid.resource.LocalTestAddressBook import at.bitfire.davdroid.settings.SettingsManager import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.testing.HiltAndroidRule import dagger.hilt.android.testing.HiltAndroidTest import org.junit.After import org.junit.Assert.assertEquals -import org.junit.Assert.assertNotNull -import org.junit.Assert.assertNull import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Rule import org.junit.Test import javax.inject.Inject +import at.bitfire.davdroid.db.Account as DbAccount @HiltAndroidTest class AccountsCleanupWorkerTest { @@ -35,7 +33,8 @@ class AccountsCleanupWorkerTest { @Inject lateinit var accountsCleanupWorkerFactory: AccountsCleanupWorker.Factory - @Inject @ApplicationContext + @Inject + @ApplicationContext lateinit var context: Context @Inject @@ -47,69 +46,35 @@ class AccountsCleanupWorkerTest { @Inject lateinit var workerFactory: HiltWorkerFactory - lateinit var accountManager: AccountManager + lateinit var account: Account + lateinit var service: Service + + val accountManager by lazy { AccountManager.get(context) } lateinit var addressBookAccountType: String lateinit var addressBookAccount: Account - lateinit var service: Service @Before fun setUp() { hiltRule.inject() TestUtils.setUpWorkManager(context, workerFactory) - accountManager = AccountManager.get(context) - service = createTestService() + account = TestAccount.create() + db.accountDao().insertOrIgnore(DbAccount(name = account.name)) + // Prepare test account addressBookAccountType = context.getString(R.string.account_type_address_book) - addressBookAccount = Account("Fancy address book account", addressBookAccountType) - - // Make sure there are no address books - LocalTestAddressBook.removeAll(context) + addressBookAccount = Account( + "Fancy address book account", + addressBookAccountType + ) } @After fun tearDown() { // Remove the account here in any case; Nice to have when the test fails accountManager.removeAccountExplicitly(addressBookAccount) - } - - @Test - fun testCleanUpServices_noAccount() { - // Insert service that reference to invalid account - db.serviceDao().insertOrReplace(Service(id = 1, accountName = "test", type = Service.TYPE_CALDAV, principal = null)) - assertNotNull(db.serviceDao().get(1)) - - // Create worker and run the method - val worker = TestListenableWorkerBuilder(context) - .setWorkerFactory(workerFactory) - .build() - worker.cleanUpServices() - - // Verify that service is deleted - assertNull(db.serviceDao().get(1)) - } - - @Test - fun testCleanUpServices_oneAccount() { - TestAccount.provide { existingAccount -> - // Insert services, one that reference the existing account and one that references an invalid account - db.serviceDao().insertOrReplace(Service(id = 1, accountName = existingAccount.name, type = Service.TYPE_CALDAV, principal = null)) - assertNotNull(db.serviceDao().get(1)) - - db.serviceDao().insertOrReplace(Service(id = 2, accountName = "not existing", type = Service.TYPE_CARDDAV, principal = null)) - assertNotNull(db.serviceDao().get(2)) - - // Create worker and run the method - val worker = TestListenableWorkerBuilder(context) - .setWorkerFactory(workerFactory) - .build() - worker.cleanUpServices() - - // Verify that one service is deleted and the other one is kept - assertNotNull(db.serviceDao().get(1)) - assertNull(db.serviceDao().get(2)) - } + TestAccount.remove(account) } @@ -117,7 +82,9 @@ class AccountsCleanupWorkerTest { fun testCleanUpAddressBooks_deletesAddressBookWithoutAccount() { // Create address book account without corresponding account assertTrue(accountManager.addAccountExplicitly(addressBookAccount, null, null)) - assertEquals(listOf(addressBookAccount), accountManager.getAccountsByType(addressBookAccountType).toList()) + + val addressBookAccounts = accountManager.getAccountsByType(addressBookAccountType) + assertEquals(addressBookAccount, addressBookAccounts.firstOrNull()) // Create worker and run the method val worker = TestListenableWorkerBuilder(context) @@ -131,33 +98,24 @@ class AccountsCleanupWorkerTest { @Test fun testCleanUpAddressBooks_keepsAddressBookWithAccount() { - TestAccount.provide { existingAccount -> - // Create address book account _with_ corresponding account and verify - val userData = Bundle(2).apply { - putString(LocalAddressBook.USER_DATA_ACCOUNT_NAME, existingAccount.name) - putString(LocalAddressBook.USER_DATA_ACCOUNT_TYPE, existingAccount.type) - } - assertTrue(accountManager.addAccountExplicitly(addressBookAccount, null, userData)) - assertEquals(listOf(addressBookAccount), accountManager.getAccountsByType(addressBookAccountType).toList()) - - // Create worker and run the method - val worker = TestListenableWorkerBuilder(context) - .setWorkerFactory(workerFactory) - .build() - worker.cleanUpAddressBooks() - - // Verify account was _not_ deleted - assertEquals(listOf(addressBookAccount), accountManager.getAccountsByType(addressBookAccountType).toList()) + // Create address book account _with_ corresponding account and verify + val userData = Bundle(2).apply { + putString(LocalAddressBook.USER_DATA_ACCOUNT_NAME, account.name) + putString(LocalAddressBook.USER_DATA_ACCOUNT_TYPE, account.type) } - } + assertTrue(accountManager.addAccountExplicitly(addressBookAccount, null, userData)) + val addressBookAccounts = accountManager.getAccountsByType(addressBookAccountType) + assertEquals(addressBookAccount, addressBookAccounts.firstOrNull()) - // helpers + // Create worker and run the method + val worker = TestListenableWorkerBuilder(context) + .setWorkerFactory(workerFactory) + .build() + worker.cleanUpAddressBooks() - private fun createTestService(): Service { - val service = Service(id=0, accountName="test", type=Service.TYPE_CARDDAV, principal = null) - val serviceId = db.serviceDao().insertOrReplace(service) - return db.serviceDao().get(serviceId)!! + // Verify account was _not_ deleted + assertEquals(addressBookAccount, addressBookAccounts.firstOrNull()) } } \ No newline at end of file diff --git a/app/src/androidTest/kotlin/at/bitfire/davdroid/sync/account/TestAccount.kt b/app/src/androidTest/kotlin/at/bitfire/davdroid/sync/account/TestAccount.kt index 58fe2d2c9..3fccc7bd8 100644 --- a/app/src/androidTest/kotlin/at/bitfire/davdroid/sync/account/TestAccount.kt +++ b/app/src/androidTest/kotlin/at/bitfire/davdroid/sync/account/TestAccount.kt @@ -12,8 +12,11 @@ import org.junit.Assert.assertTrue object TestAccount { + private val context by lazy { InstrumentationRegistry.getInstrumentation().context } private val targetContext by lazy { InstrumentationRegistry.getInstrumentation().targetContext } + val accountManager by lazy { AccountManager.get(context) } + /** * Creates a test account, usually in the `Before` setUp of a test. * @@ -23,9 +26,10 @@ object TestAccount { val accountType = targetContext.getString(R.string.account_type) val account = Account("Test Account", accountType) - val initialData = AccountSettings.initialUserData(null) - initialData.putString(AccountSettings.KEY_SETTINGS_VERSION, version.toString()) - assertTrue(SystemAccountUtils.createAccount(targetContext, account, initialData)) + val initialData = AccountSettings.initialUserData(null).apply { + putString(AccountSettings.KEY_SETTINGS_VERSION, version.toString()) + } + assertTrue(SystemAccountUtils.createAccount(context, account, initialData)) return account } @@ -34,8 +38,7 @@ object TestAccount { * Removes a test account, usually in the `@After` tearDown of a test. */ fun remove(account: Account) { - val am = AccountManager.get(targetContext) - assertTrue(am.removeAccountExplicitly(account)) + assertTrue(accountManager.removeAccountExplicitly(account)) } /** diff --git a/app/src/androidTest/kotlin/at/bitfire/davdroid/sync/worker/PeriodicSyncWorkerTest.kt b/app/src/androidTest/kotlin/at/bitfire/davdroid/sync/worker/PeriodicSyncWorkerTest.kt index 63200307f..cb135848d 100644 --- a/app/src/androidTest/kotlin/at/bitfire/davdroid/sync/worker/PeriodicSyncWorkerTest.kt +++ b/app/src/androidTest/kotlin/at/bitfire/davdroid/sync/worker/PeriodicSyncWorkerTest.kt @@ -12,7 +12,6 @@ import androidx.work.WorkerFactory import androidx.work.WorkerParameters import androidx.work.testing.TestListenableWorkerBuilder import androidx.work.workDataOf -import at.bitfire.davdroid.R import at.bitfire.davdroid.TestUtils import at.bitfire.davdroid.sync.SyncDataType import at.bitfire.davdroid.sync.account.TestAccount @@ -59,13 +58,12 @@ class PeriodicSyncWorkerTest { @Test fun doWork_cancelsItselfOnInvalidAccount() { - val invalidAccount = Account("invalid", context.getString(R.string.account_type)) + val invalidAccount = Account("invalid", "test") // Run PeriodicSyncWorker as TestWorker val inputData = workDataOf( BaseSyncWorker.INPUT_DATA_TYPE to SyncDataType.EVENTS.toString(), - BaseSyncWorker.INPUT_ACCOUNT_NAME to invalidAccount.name, - BaseSyncWorker.INPUT_ACCOUNT_TYPE to invalidAccount.type + BaseSyncWorker.INPUT_ACCOUNT_NAME to invalidAccount.name ) // mock WorkManager to observe cancellation call diff --git a/app/src/main/kotlin/at/bitfire/davdroid/db/Account.kt b/app/src/main/kotlin/at/bitfire/davdroid/db/Account.kt new file mode 100644 index 000000000..ab36212b1 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/db/Account.kt @@ -0,0 +1,25 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.db + +import androidx.room.Entity +import androidx.room.Index +import androidx.room.PrimaryKey + +/** + * Represents an account, which again has services (CalDAV/CardDAV). + */ +@Entity( + tableName = "account", + indices = [ + Index("name", unique = true) + ] +) +data class Account( + @PrimaryKey(autoGenerate = true) + val id: Long = 0L, + + val name: String +) \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/db/AccountDao.kt b/app/src/main/kotlin/at/bitfire/davdroid/db/AccountDao.kt new file mode 100644 index 000000000..5aa43a9a1 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/db/AccountDao.kt @@ -0,0 +1,33 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.db + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import org.jetbrains.annotations.TestOnly + +@Dao +interface AccountDao { + + @TestOnly + @Query("SELECT * FROM account") + fun getAll(): List + + @Insert(onConflict = OnConflictStrategy.IGNORE) + fun insertOrIgnore(account: Account) + + @Query("DELETE FROM account WHERE name=:name") + fun deleteByName(name: String) + + @Query("DELETE FROM account WHERE name NOT IN (:names)") + fun deleteExceptNames(names: List) + + @Query("UPDATE account SET name=:newName WHERE name=:oldName") + fun rename(oldName: String, newName: String) + + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/db/AppDatabase.kt b/app/src/main/kotlin/at/bitfire/davdroid/db/AppDatabase.kt index 3f803999f..79ec7a360 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/db/AppDatabase.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/db/AppDatabase.kt @@ -33,6 +33,7 @@ import java.io.Writer import javax.inject.Singleton @Database(entities = [ + Account::class, Service::class, HomeSet::class, Collection::class, @@ -40,7 +41,8 @@ import javax.inject.Singleton SyncStats::class, WebDavDocument::class, WebDavMount::class -], exportSchema = true, version = 16, autoMigrations = [ +], exportSchema = true, version = 18, autoMigrations = [ + AutoMigration(from = 17, to = 18), // belongs to Migration17 (adds the foreign key) AutoMigration(from = 15, to = 16, spec = AutoMigration16::class), AutoMigration(from = 14, to = 15), AutoMigration(from = 13, to = 14), @@ -98,6 +100,7 @@ abstract class AppDatabase: RoomDatabase() { // DAOs + abstract fun accountDao(): AccountDao abstract fun serviceDao(): ServiceDao abstract fun homeSetDao(): HomeSetDao abstract fun collectionDao(): CollectionDao diff --git a/app/src/main/kotlin/at/bitfire/davdroid/db/Service.kt b/app/src/main/kotlin/at/bitfire/davdroid/db/Service.kt index 66837d951..2ddd61a78 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/db/Service.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/db/Service.kt @@ -6,37 +6,40 @@ package at.bitfire.davdroid.db import androidx.annotation.StringDef import androidx.room.Entity +import androidx.room.ForeignKey import androidx.room.Index import androidx.room.PrimaryKey import okhttp3.HttpUrl -@Retention(AnnotationRetention.SOURCE) -@StringDef(Service.TYPE_CALDAV, Service.TYPE_CARDDAV) -annotation class ServiceType - /** * A service entity. * - * Services represent accounts and are unique. They are of type CardDAV or CalDAV and may have an associated principal. + * Services are unique per account. They are of type CardDAV or CalDAV and may have an associated principal. */ -@Entity(tableName = "service", - indices = [ - // only one service per type and account - Index("accountName", "type", unique = true) - ]) +@Entity( + tableName = "service", + foreignKeys = [ + ForeignKey(entity = Account::class, parentColumns = ["name"], childColumns = ["accountName"], onDelete = ForeignKey.CASCADE, onUpdate = ForeignKey.CASCADE) + ], + indices = [ + // only one service per account and (service) type + Index("accountName", "type", unique = true) + ] +) data class Service( @PrimaryKey(autoGenerate = true) var id: Long, - var accountName: String, - @ServiceType + @ServiceTypeDef var type: String, var principal: HttpUrl? ) { companion object { + @StringDef(TYPE_CALDAV, TYPE_CARDDAV) + annotation class ServiceTypeDef const val TYPE_CALDAV = "caldav" const val TYPE_CARDDAV = "carddav" } diff --git a/app/src/main/kotlin/at/bitfire/davdroid/db/ServiceDao.kt b/app/src/main/kotlin/at/bitfire/davdroid/db/ServiceDao.kt index 647c178ad..a64a47b96 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/db/ServiceDao.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/db/ServiceDao.kt @@ -37,7 +37,4 @@ interface ServiceDao { @Query("DELETE FROM service WHERE accountName NOT IN (:accountNames)") fun deleteExceptAccounts(accountNames: Array) - @Query("UPDATE service SET accountName=:newName WHERE accountName=:oldName") - suspend fun renameAccount(oldName: String, newName: String) - } \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/db/migration/Migration17.kt b/app/src/main/kotlin/at/bitfire/davdroid/db/migration/Migration17.kt new file mode 100644 index 000000000..468c178c2 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/db/migration/Migration17.kt @@ -0,0 +1,33 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.db.migration + +import androidx.room.migration.Migration +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import dagger.multibindings.IntoSet + +internal val Migration17 = Migration(16, 17) { db -> + // Add account table + db.execSQL("CREATE TABLE account (" + + "id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL," + + "name TEXT NOT NULL)") + db.execSQL("CREATE UNIQUE INDEX index_account_name ON account(name)") + + // Fill account names from services + db.execSQL("INSERT INTO account (name) SELECT DISTINCT accountName FROM service") + + // AutoMigration18 adds the foreign key of service(accountName) + // because it's not possible to add it with a simple SQL statement +} + +@Module +@InstallIn(SingletonComponent::class) +internal object Migration17Module { + @Provides @IntoSet + fun provide(): Migration = Migration17 +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/db/migration/Migration2.kt b/app/src/main/kotlin/at/bitfire/davdroid/db/migration/Migration2.kt index caae5c207..219008ce9 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/db/migration/Migration2.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/db/migration/Migration2.kt @@ -11,7 +11,7 @@ import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import dagger.multibindings.IntoSet -val Migration2 = Migration(1, 2) { db -> +internal val Migration2 = Migration(1, 2) { db -> db.execSQL("ALTER TABLE collections ADD COLUMN type TEXT NOT NULL DEFAULT ''") db.execSQL("ALTER TABLE collections ADD COLUMN source TEXT DEFAULT NULL") db.execSQL("UPDATE collections SET type=(" + diff --git a/app/src/main/kotlin/at/bitfire/davdroid/db/migration/Migration3.kt b/app/src/main/kotlin/at/bitfire/davdroid/db/migration/Migration3.kt index 4658ddb96..3702f69d8 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/db/migration/Migration3.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/db/migration/Migration3.kt @@ -12,7 +12,7 @@ import dagger.hilt.components.SingletonComponent import dagger.multibindings.IntoSet import java.util.logging.Logger -val Migration3 = Migration(2, 3) { db -> +internal val Migration3 = Migration(2, 3) { db -> // We don't have access to the context in a Room migration now, so // we will just drop those settings from old DAVx5 versions. Logger.getGlobal().warning("Dropping settings distrustSystemCerts and overrideProxy*") diff --git a/app/src/main/kotlin/at/bitfire/davdroid/db/migration/Migration4.kt b/app/src/main/kotlin/at/bitfire/davdroid/db/migration/Migration4.kt index b321596d6..78c878d42 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/db/migration/Migration4.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/db/migration/Migration4.kt @@ -11,7 +11,7 @@ import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import dagger.multibindings.IntoSet -val Migration4 = Migration(3, 4) { db -> +internal val Migration4 = Migration(3, 4) { db -> db.execSQL("ALTER TABLE collections ADD COLUMN forceReadOnly INTEGER DEFAULT 0 NOT NULL") } diff --git a/app/src/main/kotlin/at/bitfire/davdroid/db/migration/Migration5.kt b/app/src/main/kotlin/at/bitfire/davdroid/db/migration/Migration5.kt index d77581fcb..4d5ff13ed 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/db/migration/Migration5.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/db/migration/Migration5.kt @@ -11,7 +11,7 @@ import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import dagger.multibindings.IntoSet -val Migration5 = Migration(4, 5) { db -> +internal val Migration5 = Migration(4, 5) { db -> db.execSQL("ALTER TABLE collections ADD COLUMN privWriteContent INTEGER DEFAULT 0 NOT NULL") db.execSQL("UPDATE collections SET privWriteContent=NOT readOnly") diff --git a/app/src/main/kotlin/at/bitfire/davdroid/db/migration/Migration6.kt b/app/src/main/kotlin/at/bitfire/davdroid/db/migration/Migration6.kt index 91edacd0d..d9fdc2a74 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/db/migration/Migration6.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/db/migration/Migration6.kt @@ -11,7 +11,7 @@ import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import dagger.multibindings.IntoSet -val Migration6 = Migration(5, 6) { db -> +internal val Migration6 = Migration(5, 6) { db -> val sql = arrayOf( // migrate "services" to "service": rename columns, make id NOT NULL "CREATE TABLE service(" + diff --git a/app/src/main/kotlin/at/bitfire/davdroid/db/migration/Migration7.kt b/app/src/main/kotlin/at/bitfire/davdroid/db/migration/Migration7.kt index 7f831132b..3a3eac38f 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/db/migration/Migration7.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/db/migration/Migration7.kt @@ -11,7 +11,7 @@ import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import dagger.multibindings.IntoSet -val Migration7 = Migration(6, 7) { db -> +internal val Migration7 = Migration(6, 7) { db -> db.execSQL("ALTER TABLE homeset ADD COLUMN privBind INTEGER NOT NULL DEFAULT 1") db.execSQL("ALTER TABLE homeset ADD COLUMN displayName TEXT DEFAULT NULL") } diff --git a/app/src/main/kotlin/at/bitfire/davdroid/db/migration/Migration8.kt b/app/src/main/kotlin/at/bitfire/davdroid/db/migration/Migration8.kt index 76af39b0e..851d63aeb 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/db/migration/Migration8.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/db/migration/Migration8.kt @@ -11,7 +11,7 @@ import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import dagger.multibindings.IntoSet -val Migration8 = Migration(7, 8) { db -> +internal val Migration8 = Migration(7, 8) { db -> db.execSQL("ALTER TABLE homeset ADD COLUMN personal INTEGER NOT NULL DEFAULT 1") db.execSQL("ALTER TABLE collection ADD COLUMN homeSetId INTEGER DEFAULT NULL REFERENCES homeset(id) ON DELETE SET NULL") db.execSQL("ALTER TABLE collection ADD COLUMN owner TEXT DEFAULT NULL") diff --git a/app/src/main/kotlin/at/bitfire/davdroid/db/migration/Migration9.kt b/app/src/main/kotlin/at/bitfire/davdroid/db/migration/Migration9.kt index f066fc0bc..4d6048d17 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/db/migration/Migration9.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/db/migration/Migration9.kt @@ -11,7 +11,7 @@ import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import dagger.multibindings.IntoSet -val Migration9 = Migration(8, 9) { db -> +internal val Migration9 = Migration(8, 9) { db -> db.execSQL("CREATE TABLE syncstats (" + "id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT," + "collectionId INTEGER NOT NULL REFERENCES collection(id) ON DELETE CASCADE," + diff --git a/app/src/main/kotlin/at/bitfire/davdroid/push/PushRegistrationWorker.kt b/app/src/main/kotlin/at/bitfire/davdroid/push/PushRegistrationWorker.kt index f30f3d0a1..c40897ecc 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/push/PushRegistrationWorker.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/push/PushRegistrationWorker.kt @@ -17,9 +17,9 @@ import at.bitfire.dav4jvm.XmlUtils import at.bitfire.dav4jvm.XmlUtils.insertTag import at.bitfire.dav4jvm.exception.DavException import at.bitfire.dav4jvm.property.push.NS_WEBDAV_PUSH -import at.bitfire.davdroid.R import at.bitfire.davdroid.db.Collection import at.bitfire.davdroid.network.HttpClient +import at.bitfire.davdroid.repository.AccountRepository import at.bitfire.davdroid.repository.DavCollectionRepository import at.bitfire.davdroid.repository.DavServiceRepository import at.bitfire.davdroid.repository.PreferenceRepository @@ -47,6 +47,7 @@ import java.util.logging.Logger class PushRegistrationWorker @AssistedInject constructor( @Assisted context: Context, @Assisted workerParameters: WorkerParameters, + private val accountRepository: AccountRepository, private val accountSettingsFactory: AccountSettings.Factory, private val collectionRepository: DavCollectionRepository, private val logger: Logger, @@ -136,7 +137,7 @@ class PushRegistrationWorker @AssistedInject constructor( // no existing subscription or expiring soon logger.info("Registering push for ${collection.url}") serviceRepository.get(collection.serviceId)?.let { service -> - val account = Account(service.accountName, applicationContext.getString(R.string.account_type)) + val account = accountRepository.fromName(service.accountName) try { registerPushSubscription(collection, account, endpoint) } catch (e: DavException) { @@ -182,7 +183,7 @@ class PushRegistrationWorker @AssistedInject constructor( logger.info("Unregistering push for ${collection.url}") collection.pushSubscription?.toHttpUrlOrNull()?.let { url -> serviceRepository.get(collection.serviceId)?.let { service -> - val account = Account(service.accountName, applicationContext.getString(R.string.account_type)) + val account = accountRepository.fromName(service.accountName) unregisterPushSubscription(collection, account, url) } } diff --git a/app/src/main/kotlin/at/bitfire/davdroid/repository/AccountRepository.kt b/app/src/main/kotlin/at/bitfire/davdroid/repository/AccountRepository.kt index 95b574fce..fd057e28b 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/repository/AccountRepository.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/repository/AccountRepository.kt @@ -10,6 +10,8 @@ import android.accounts.OnAccountsUpdateListener import android.content.Context import at.bitfire.davdroid.InvalidAccountException import at.bitfire.davdroid.R +import at.bitfire.davdroid.db.AppDatabase +import at.bitfire.davdroid.db.Collection import at.bitfire.davdroid.db.Credentials import at.bitfire.davdroid.db.HomeSet import at.bitfire.davdroid.db.Service @@ -30,27 +32,36 @@ import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map import kotlinx.coroutines.withContext import java.util.logging.Level import java.util.logging.Logger import javax.inject.Inject +import at.bitfire.davdroid.db.Account as DbAccount /** * Repository for managing CalDAV/CardDAV accounts. * - * *Note:* This class is not related to address book accounts, which are managed by + * Currently the authoritative data source is the Android account manager. The accounts will however be mirrored into the database. + * In future, the authoritative data source should be the database, and the Android account manager should be updated accordingly. + * + * To map an account name to a system [Account], other classes should use [fromName] and not instantiate [Account] themselves. + * + * *Note:* This class is in no way related to local address books (address book accounts), which are managed by * [at.bitfire.davdroid.resource.LocalAddressBook]. */ class AccountRepository @Inject constructor( private val accountSettingsFactory: AccountSettings.Factory, private val automaticSyncManager: AutomaticSyncManager, @ApplicationContext private val context: Context, - private val collectionRepository: DavCollectionRepository, - private val homeSetRepository: DavHomeSetRepository, - private val localCalendarStore: Lazy, + private val collectionRepository: Lazy, + db: AppDatabase, + private val homeSetRepository: Lazy, private val localAddressBookStore: Lazy, + private val localCalendarStore: Lazy, private val logger: Logger, - private val serviceRepository: DavServiceRepository, + private val serviceRepository: Lazy, private val syncWorkerManager: SyncWorkerManager, private val tasksAppManager: Lazy ) { @@ -58,10 +69,14 @@ class AccountRepository @Inject constructor( private val accountType = context.getString(R.string.account_type) private val accountManager = AccountManager.get(context) + private val dao = db.accountDao() + /** * Creates a new account with discovered services and enables periodic syncs with * default sync interval times. * + * Creates both the system account and a corresponding entry in the database. + * * @param accountName name of the account * @param credentials server credentials * @param config discovered server capabilities for syncable authorities @@ -79,6 +94,9 @@ class AccountRepository @Inject constructor( if (!SystemAccountUtils.createAccount(context, account, userData, credentials?.password)) return null + // create account in database + mirrorToDb(account) + // add entries for account to database logger.log(Level.INFO, "Writing account configuration to database", config) try { @@ -112,6 +130,9 @@ class AccountRepository @Inject constructor( return account } + /** + * Deletes an account from both the system accounts and the database. + */ suspend fun delete(accountName: String): Boolean { val account = fromName(accountName) // remove account directly (bypassing the authenticator, which is our own) @@ -119,14 +140,15 @@ class AccountRepository @Inject constructor( accountManager.removeAccountExplicitly(account) // delete address books (= address book accounts) - serviceRepository.getByAccountAndType(accountName, Service.TYPE_CARDDAV)?.let { service -> - collectionRepository.getByService(service.id).forEach { collection -> + serviceRepository.get().getByAccountAndType(accountName, Service.TYPE_CARDDAV)?.let { service -> + collectionRepository.get().getByService(service.id).forEach { collection -> localAddressBookStore.get().deleteByCollectionId(collection.id) } } // delete from database - serviceRepository.deleteByAccount(accountName) + dao.deleteByName(accountName) + serviceRepository.get().deleteByAccount(accountName) true } catch (e: Exception) { @@ -143,22 +165,55 @@ class AccountRepository @Inject constructor( .getAccountsByType(accountType) .any { it.name == accountName } + /** + * Gets the account for the given collection. + * + * @param collection the collection to get the account for + * + * @throws kotlin.IllegalArgumentException if the collection, the service or the account can't be found + */ + fun fromCollection(collection: Collection): Account = + serviceRepository.get().get(collection.serviceId)?.let { service -> + fromName(service.accountName) + } ?: throw IllegalArgumentException("Couldn't fetch DB service or account from collection") + + /** + * Returns the system account for the given account name. + * + * Also makes sure that the account is present in the database. + */ fun fromName(accountName: String) = - Account(accountName, accountType) + mirrorToDb(Account(accountName, accountType)) - fun getAll(): Array = accountManager.getAccountsByType(accountType) + fun getAll(): Set = accountManager + .getAccountsByType(accountType) + .map { mirrorToDb(it) } + .toSet() + + fun getAllFlow() = + callbackFlow> { + val listener = OnAccountsUpdateListener { accounts -> + trySend(accounts.filter { it.type == accountType }) + } - fun getAllFlow() = callbackFlow> { - val listener = OnAccountsUpdateListener { accounts -> - trySend(accounts.filter { it.type == accountType }.toSet()) - } - withContext(Dispatchers.Default) { // causes disk I/O accountManager.addOnAccountsUpdatedListener(listener, null, true) - } - awaitClose { - accountManager.removeOnAccountsUpdatedListener(listener) - } + awaitClose { + accountManager.removeOnAccountsUpdatedListener(listener) + } + }.map { list -> + // mirror accounts to DB + for (account in list) + mirrorToDb(account) + list + }.flowOn(Dispatchers.IO) + + /** + * Deletes accounts in the database which don't have a corresponding system account. + */ + fun removeOrphanedInDb() { + val accounts = accountManager.getAccountsByType(accountType) + dao.deleteExceptNames(accounts.map { it.name }) } /** @@ -169,17 +224,18 @@ class AccountRepository @Inject constructor( * * @param oldName current name of the account * @param newName new name the account shall be re named to + * @return the renamed account * * @throws InvalidAccountException if the account does not exist * @throws IllegalArgumentException if the new account name already exists * @throws Exception (or sub-classes) on other errors */ - suspend fun rename(oldName: String, newName: String) { + suspend fun rename(oldName: String, newName: String): Account { val oldAccount = fromName(oldName) - val newAccount = fromName(newName) + val newAccount = Account(newName, accountType) // check whether new account name already exists - if (accountManager.getAccountsByType(context.getString(R.string.account_type)).contains(newAccount)) + if (accountManager.getAccountsByType(accountType).contains(newAccount)) throw IllegalArgumentException("Account with name \"$newName\" already exists") // rename account @@ -211,8 +267,8 @@ class AccountRepository @Inject constructor( for (dataType in SyncDataType.entries) syncWorkerManager.disablePeriodic(oldAccount, dataType) - // update account name references in database - serviceRepository.renameAccount(oldName, newName) + // update account in DB (propagates account name to services over foreign key) + dao.rename(oldName, newName) try { // update address books @@ -242,6 +298,8 @@ class AccountRepository @Inject constructor( // release AccountsCleanupWorker mutex at the end of this async coroutine AccountsCleanupWorker.unlockAccountsCleanup() } + + return newAccount } @@ -250,19 +308,24 @@ class AccountRepository @Inject constructor( private fun insertService(accountName: String, type: String, info: DavResourceFinder.Configuration.ServiceInfo): Long { // insert service val service = Service(0, accountName, type, info.principal) - val serviceId = serviceRepository.insertOrReplace(service) + val serviceId = serviceRepository.get().insertOrReplace(service) // insert home sets for (homeSet in info.homeSets) - homeSetRepository.insertOrUpdateByUrl(HomeSet(0, serviceId, true, homeSet)) + homeSetRepository.get().insertOrUpdateByUrl(HomeSet(0, serviceId, true, homeSet)) // insert collections for (collection in info.collections.values) { collection.serviceId = serviceId - collectionRepository.insertOrUpdateByUrl(collection) + collectionRepository.get().insertOrUpdateByUrl(collection) } return serviceId } + private fun mirrorToDb(account: Account): Account { + dao.insertOrIgnore(DbAccount(name = account.name)) + return account + } + } \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/repository/DavCollectionRepository.kt b/app/src/main/kotlin/at/bitfire/davdroid/repository/DavCollectionRepository.kt index d056b174c..fe3837014 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/repository/DavCollectionRepository.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/repository/DavCollectionRepository.kt @@ -20,7 +20,6 @@ import at.bitfire.dav4jvm.property.carddav.NS_CARDDAV import at.bitfire.dav4jvm.property.webdav.DisplayName import at.bitfire.dav4jvm.property.webdav.NS_WEBDAV import at.bitfire.dav4jvm.property.webdav.ResourceType -import at.bitfire.davdroid.R import at.bitfire.davdroid.db.AppDatabase import at.bitfire.davdroid.db.Collection import at.bitfire.davdroid.db.HomeSet @@ -54,6 +53,7 @@ import javax.inject.Inject * Implements an observer pattern that can be used to listen for changes of collections. */ class DavCollectionRepository @Inject constructor( + private val accountRepository: AccountRepository, private val accountSettingsFactory: AccountSettings.Factory, @ApplicationContext val context: Context, db: AppDatabase, @@ -175,7 +175,7 @@ class DavCollectionRepository @Inject constructor( /** Deletes the given collection from the server and the database. */ suspend fun deleteRemote(collection: Collection) { val service = serviceRepository.get(collection.serviceId) ?: throw IllegalArgumentException("Service not found") - val account = Account(service.accountName, context.getString(R.string.account_type)) + val account = accountRepository.fromName(service.accountName) HttpClient.Builder(context, accountSettingsFactory.create(account)) .setForeground(true) diff --git a/app/src/main/kotlin/at/bitfire/davdroid/repository/DavServiceRepository.kt b/app/src/main/kotlin/at/bitfire/davdroid/repository/DavServiceRepository.kt index 53b8e49a7..e3f09c096 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/repository/DavServiceRepository.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/repository/DavServiceRepository.kt @@ -34,9 +34,6 @@ class DavServiceRepository @Inject constructor( fun insertOrReplace(service: Service) = dao.insertOrReplace(service) - suspend fun renameAccount(oldName: String, newName: String) = - dao.renameAccount(oldName, newName) - // Delete diff --git a/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalAddressBook.kt b/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalAddressBook.kt index ba93542e8..730a0facc 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalAddressBook.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalAddressBook.kt @@ -16,8 +16,8 @@ import android.provider.ContactsContract.Groups import android.provider.ContactsContract.RawContacts import androidx.annotation.OpenForTesting import androidx.core.content.contentValuesOf -import at.bitfire.davdroid.R import at.bitfire.davdroid.db.SyncState +import at.bitfire.davdroid.repository.AccountRepository import at.bitfire.davdroid.repository.DavCollectionRepository import at.bitfire.davdroid.repository.DavServiceRepository import at.bitfire.davdroid.resource.workaround.ContactDirtyVerifier @@ -55,6 +55,7 @@ open class LocalAddressBook @AssistedInject constructor( @Assisted("account") val account: Account, @Assisted("addressBookAccount") _addressBookAccount: Account, @Assisted provider: ContentProviderClient, + private val accountRepository: AccountRepository, private val accountSettingsFactory: AccountSettings.Factory, private val collectionRepository: DavCollectionRepository, @ApplicationContext private val context: Context, @@ -91,7 +92,7 @@ open class LocalAddressBook @AssistedInject constructor( val account = manager.getUserData(addressBookAccount, USER_DATA_COLLECTION_ID)?.toLongOrNull()?.let { collectionId -> collectionRepository.get(collectionId)?.let { collection -> serviceRepository.get(collection.serviceId)?.let { service -> - Account(service.accountName, context.getString(R.string.account_type)) + accountRepository.fromName(service.accountName) } } } diff --git a/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalCalendarStore.kt b/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalCalendarStore.kt index 3c7a8629c..b6abfa3d8 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalCalendarStore.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalCalendarStore.kt @@ -13,9 +13,8 @@ import android.provider.CalendarContract import android.provider.CalendarContract.Calendars import androidx.core.content.contentValuesOf import at.bitfire.davdroid.Constants -import at.bitfire.davdroid.R import at.bitfire.davdroid.db.Collection -import at.bitfire.davdroid.repository.DavServiceRepository +import at.bitfire.davdroid.repository.AccountRepository import at.bitfire.davdroid.settings.AccountSettings import at.bitfire.davdroid.util.DavUtils.lastSegment import at.bitfire.ical4android.AndroidCalendar @@ -28,15 +27,14 @@ import java.util.logging.Logger import javax.inject.Inject class LocalCalendarStore @Inject constructor( - @ApplicationContext private val context: Context, + private val accountRepository: AccountRepository, private val accountSettingsFactory: AccountSettings.Factory, - private val logger: Logger, - private val serviceRepository: DavServiceRepository + @ApplicationContext private val context: Context, + private val logger: Logger ): LocalDataStore { override fun create(provider: ContentProviderClient, fromCollection: Collection): LocalCalendar? { - val service = serviceRepository.get(fromCollection.serviceId) ?: throw IllegalArgumentException("Couldn't fetch DB service from collection") - val account = Account(service.accountName, context.getString(R.string.account_type)) + val account = accountRepository.fromCollection(fromCollection) // If the collection doesn't have a color, use a default color. if (fromCollection.color != null) diff --git a/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalJtxCollectionStore.kt b/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalJtxCollectionStore.kt index 3deecf075..35534fe3d 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalJtxCollectionStore.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalJtxCollectionStore.kt @@ -11,9 +11,8 @@ import android.content.ContentValues import android.content.Context import androidx.core.content.contentValuesOf import at.bitfire.davdroid.Constants -import at.bitfire.davdroid.R -import at.bitfire.davdroid.db.AppDatabase import at.bitfire.davdroid.db.Collection +import at.bitfire.davdroid.repository.AccountRepository import at.bitfire.davdroid.repository.PrincipalRepository import at.bitfire.davdroid.settings.AccountSettings import at.bitfire.davdroid.util.DavUtils.lastSegment @@ -27,20 +26,17 @@ import javax.inject.Inject class LocalJtxCollectionStore @Inject constructor( @ApplicationContext val context: Context, - val accountSettingsFactory: AccountSettings.Factory, - db: AppDatabase, - val principalRepository: PrincipalRepository + private val accountRepository: AccountRepository, + private val accountSettingsFactory: AccountSettings.Factory, + private val principalRepository: PrincipalRepository ): LocalDataStore { - private val serviceDao = db.serviceDao() - override fun create(provider: ContentProviderClient, fromCollection: Collection): LocalJtxCollection? { // If the collection doesn't have a color, use a default color. if (fromCollection.color != null) fromCollection.color = Constants.DAVDROID_GREEN_RGBA - val service = serviceDao.get(fromCollection.serviceId) ?: throw IllegalArgumentException("Couldn't fetch DB service from collection") - val account = Account(service.accountName, context.getString(R.string.account_type)) + val account = accountRepository.fromCollection(fromCollection) val values = valuesFromCollection(fromCollection, account = account, withColor = true) val uri = JtxCollection.create(account, provider, values) diff --git a/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalTaskListStore.kt b/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalTaskListStore.kt index 20cf7b83a..452144964 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalTaskListStore.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalTaskListStore.kt @@ -12,9 +12,9 @@ import android.content.Context import android.net.Uri import androidx.core.content.contentValuesOf import at.bitfire.davdroid.Constants -import at.bitfire.davdroid.R import at.bitfire.davdroid.db.AppDatabase import at.bitfire.davdroid.db.Collection +import at.bitfire.davdroid.repository.AccountRepository import at.bitfire.davdroid.settings.AccountSettings import at.bitfire.davdroid.util.DavUtils.lastSegment import at.bitfire.ical4android.DmfsTaskList @@ -31,6 +31,7 @@ import java.util.logging.Logger class LocalTaskListStore @AssistedInject constructor( @Assisted private val providerName: TaskProvider.ProviderName, + val accountRepository: AccountRepository, val accountSettingsFactory: AccountSettings.Factory, @ApplicationContext val context: Context, val db: AppDatabase, @@ -42,12 +43,9 @@ class LocalTaskListStore @AssistedInject constructor( fun create(providerName: TaskProvider.ProviderName): LocalTaskListStore } - private val serviceDao = db.serviceDao() - override fun create(provider: ContentProviderClient, fromCollection: Collection): LocalTaskList? { - val service = serviceDao.get(fromCollection.serviceId) ?: throw IllegalArgumentException("Couldn't fetch DB service from collection") - val account = Account(service.accountName, context.getString(R.string.account_type)) + val account = accountRepository.fromCollection(fromCollection) logger.log(Level.INFO, "Adding local task list", fromCollection) val uri = create(account, provider, providerName, fromCollection) diff --git a/app/src/main/kotlin/at/bitfire/davdroid/servicedetection/RefreshCollectionsWorker.kt b/app/src/main/kotlin/at/bitfire/davdroid/servicedetection/RefreshCollectionsWorker.kt index 401b8cbf3..cc98fa947 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/servicedetection/RefreshCollectionsWorker.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/servicedetection/RefreshCollectionsWorker.kt @@ -4,7 +4,6 @@ package at.bitfire.davdroid.servicedetection -import android.accounts.Account import android.app.PendingIntent import android.content.Context import android.content.Intent @@ -39,8 +38,8 @@ import at.bitfire.dav4jvm.property.webdav.ResourceType import at.bitfire.davdroid.InvalidAccountException import at.bitfire.davdroid.R import at.bitfire.davdroid.network.HttpClient +import at.bitfire.davdroid.repository.AccountRepository import at.bitfire.davdroid.repository.DavServiceRepository -import at.bitfire.davdroid.servicedetection.RefreshCollectionsWorker.Companion.ARG_SERVICE_ID import at.bitfire.davdroid.settings.AccountSettings import at.bitfire.davdroid.ui.DebugInfoActivity import at.bitfire.davdroid.ui.NotificationRegistry @@ -75,6 +74,7 @@ import java.util.logging.Logger class RefreshCollectionsWorker @AssistedInject constructor( @Assisted appContext: Context, @Assisted workerParams: WorkerParameters, + private val accountRepository: AccountRepository, private val accountSettingsFactory: AccountSettings.Factory, private val collectionListRefresherFactory: CollectionListRefresher.Factory, private val logger: Logger, @@ -165,7 +165,7 @@ class RefreshCollectionsWorker @AssistedInject constructor( val serviceId: Long = inputData.getLong(ARG_SERVICE_ID, -1) val service = serviceRepository.get(serviceId) val account = service?.let { service -> - Account(service.accountName, applicationContext.getString(R.string.account_type)) + accountRepository.fromName(service.accountName) } override suspend fun doWork(): Result { diff --git a/app/src/main/kotlin/at/bitfire/davdroid/sync/SyncAdapterServices.kt b/app/src/main/kotlin/at/bitfire/davdroid/sync/SyncAdapterServices.kt index 25ccf0769..ab214d8c3 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/sync/SyncAdapterServices.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/sync/SyncAdapterServices.kt @@ -19,6 +19,7 @@ import androidx.work.WorkInfo import androidx.work.WorkManager import at.bitfire.davdroid.InvalidAccountException import at.bitfire.davdroid.R +import at.bitfire.davdroid.repository.AccountRepository import at.bitfire.davdroid.repository.DavCollectionRepository import at.bitfire.davdroid.repository.DavServiceRepository import at.bitfire.davdroid.resource.LocalAddressBook.Companion.USER_DATA_COLLECTION_ID @@ -60,6 +61,7 @@ abstract class SyncAdapterService: Service() { * All Sync Adapter Framework related interaction should happen inside [SyncFrameworkIntegration]. */ class SyncAdapter @Inject constructor( + private val accountRepository: AccountRepository, private val accountSettingsFactory: AccountSettings.Factory, private val collectionRepository: DavCollectionRepository, private val serviceRepository: DavServiceRepository, @@ -92,7 +94,7 @@ abstract class SyncAdapterService: Service() { ?.let { collectionId -> collectionRepository.get(collectionId)?.let { collection -> serviceRepository.get(collection.serviceId)?.let { service -> - Account(service.accountName, context.getString(R.string.account_type)) + accountRepository.fromName(service.accountName) } } } diff --git a/app/src/main/kotlin/at/bitfire/davdroid/sync/account/AccountsCleanupWorker.kt b/app/src/main/kotlin/at/bitfire/davdroid/sync/account/AccountsCleanupWorker.kt index a0b0137f9..b55d84ae8 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/sync/account/AccountsCleanupWorker.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/sync/account/AccountsCleanupWorker.kt @@ -23,7 +23,6 @@ import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import java.time.Duration import java.util.concurrent.Semaphore -import java.util.logging.Level import java.util.logging.Logger @HiltWorker @@ -46,7 +45,7 @@ class AccountsCleanupWorker @AssistedInject constructor( override fun doWork(): Result { lockAccountsCleanup() try { - cleanUpServices() + accountRepository.removeOrphanedInDb() cleanUpAddressBooks() } finally { unlockAccountsCleanup() @@ -54,23 +53,6 @@ class AccountsCleanupWorker @AssistedInject constructor( return Result.success() } - /** - * Deletes services in the database which are not associated to a valid account. - */ - @VisibleForTesting - internal fun cleanUpServices() { - // Later, accounts which are not in the DB should be deleted here - - // Delete orphaned services in DB – only necessary as long as accounts are implemented as system accounts (not in DB) - val accounts = accountRepository.getAll() - logger.log(Level.INFO, "Cleaning up accounts. Currently existing accounts:", accounts) - val serviceDao = db.serviceDao() - if (accounts.isEmpty()) - serviceDao.deleteAll() - else - serviceDao.deleteExceptAccounts(accounts.map { it.name }.toTypedArray()) - } - /** * Deletes address book accounts which are not assigned to a valid account. */ diff --git a/app/src/main/kotlin/at/bitfire/davdroid/sync/worker/BaseSyncWorker.kt b/app/src/main/kotlin/at/bitfire/davdroid/sync/worker/BaseSyncWorker.kt index 45bce8d4c..aed4ed31a 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/sync/worker/BaseSyncWorker.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/sync/worker/BaseSyncWorker.kt @@ -19,6 +19,7 @@ import androidx.work.WorkerParameters import at.bitfire.davdroid.InvalidAccountException import at.bitfire.davdroid.R import at.bitfire.davdroid.push.PushNotificationManager +import at.bitfire.davdroid.repository.AccountRepository import at.bitfire.davdroid.settings.AccountSettings import at.bitfire.davdroid.sync.AddressBookSyncer import at.bitfire.davdroid.sync.CalendarSyncer @@ -42,11 +43,14 @@ import java.util.logging.Logger import javax.inject.Inject abstract class BaseSyncWorker( - private val context: Context, + context: Context, private val workerParams: WorkerParameters, private val syncDispatcher: CoroutineDispatcher ) : CoroutineWorker(context, workerParams) { + @Inject + lateinit var accountRepository: AccountRepository + @Inject lateinit var accountSettingsFactory: AccountSettings.Factory @@ -80,9 +84,8 @@ abstract class BaseSyncWorker( override suspend fun doWork(): Result { // ensure we got the required arguments - val account = Account( - inputData.getString(INPUT_ACCOUNT_NAME) ?: throw IllegalArgumentException("INPUT_ACCOUNT_NAME required"), - inputData.getString(INPUT_ACCOUNT_TYPE) ?: throw IllegalArgumentException("INPUT_ACCOUNT_TYPE required") + val account = accountRepository.fromName( + inputData.getString(INPUT_ACCOUNT_NAME) ?: throw IllegalArgumentException("INPUT_ACCOUNT_NAME required") ) val dataType = SyncDataType.valueOf(inputData.getString(INPUT_DATA_TYPE) ?: throw IllegalArgumentException("INPUT_SYNC_DATA_TYPE required")) @@ -248,7 +251,6 @@ abstract class BaseSyncWorker( // common worker input parameters const val INPUT_ACCOUNT_NAME = "accountName" - const val INPUT_ACCOUNT_TYPE = "accountType" const val INPUT_DATA_TYPE = "dataType" /** set to `true` for user-initiated sync that skips network checks */ diff --git a/app/src/main/kotlin/at/bitfire/davdroid/sync/worker/SyncWorkerManager.kt b/app/src/main/kotlin/at/bitfire/davdroid/sync/worker/SyncWorkerManager.kt index 6866eb144..b57c92f95 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/sync/worker/SyncWorkerManager.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/sync/worker/SyncWorkerManager.kt @@ -28,7 +28,6 @@ import at.bitfire.davdroid.push.PushNotificationManager import at.bitfire.davdroid.sync.SyncDataType import at.bitfire.davdroid.sync.TasksAppManager import at.bitfire.davdroid.sync.worker.BaseSyncWorker.Companion.INPUT_ACCOUNT_NAME -import at.bitfire.davdroid.sync.worker.BaseSyncWorker.Companion.INPUT_ACCOUNT_TYPE import at.bitfire.davdroid.sync.worker.BaseSyncWorker.Companion.INPUT_DATA_TYPE import at.bitfire.davdroid.sync.worker.BaseSyncWorker.Companion.INPUT_MANUAL import at.bitfire.davdroid.sync.worker.BaseSyncWorker.Companion.INPUT_RESYNC @@ -76,7 +75,6 @@ class SyncWorkerManager @Inject constructor( val argumentsBuilder = Data.Builder() .putString(INPUT_DATA_TYPE, dataType.toString()) .putString(INPUT_ACCOUNT_NAME, account.name) - .putString(INPUT_ACCOUNT_TYPE, account.type) if (manual) argumentsBuilder.putBoolean(INPUT_MANUAL, true) if (resync != NO_RESYNC) @@ -190,7 +188,6 @@ class SyncWorkerManager @Inject constructor( val arguments = Data.Builder() .putString(INPUT_DATA_TYPE, dataType.toString()) .putString(INPUT_ACCOUNT_NAME, account.name) - .putString(INPUT_ACCOUNT_TYPE, account.type) .build() val constraints = Constraints.Builder() .setRequiredNetworkType( diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/account/AccountScreenModel.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/account/AccountScreenModel.kt index e1aa83fe1..27c5f99db 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/account/AccountScreenModel.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/account/AccountScreenModel.kt @@ -15,7 +15,6 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.asFlow import androidx.lifecycle.switchMap import androidx.lifecycle.viewModelScope -import at.bitfire.davdroid.R import at.bitfire.davdroid.db.Collection import at.bitfire.davdroid.repository.AccountRepository import at.bitfire.davdroid.repository.DavCollectionRepository @@ -156,10 +155,9 @@ class AccountScreenModel @AssistedInject constructor( fun renameAccount(newName: String) { notInterruptibleScope.launch { try { - accountRepository.rename(account.name, newName) + val newAccount = accountRepository.rename(account.name, newName) // synchronize again - val newAccount = Account(newName, context.getString(R.string.account_type)) syncWorkerManager.enqueueOneTimeAllAuthorities(newAccount, manual = true) } catch (e: Exception) { logger.log(Level.SEVERE, "Couldn't rename account", e)