diff --git a/app/schemas/at.bitfire.davdroid.db.AppDatabase/19.json b/app/schemas/at.bitfire.davdroid.db.AppDatabase/19.json new file mode 100644 index 0000000000..cb80c604a7 --- /dev/null +++ b/app/schemas/at.bitfire.davdroid.db.AppDatabase/19.json @@ -0,0 +1,653 @@ +{ + "formatVersion": 1, + "database": { + "version": 19, + "identityHash": "6a30d678208d0aa38dd86951c67c2abe", + "entities": [ + { + "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" + } + ], + "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`)" + } + ] + }, + { + "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" + } + ], + "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, `personal` INTEGER, `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, `pushVapidKey` TEXT, `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" + }, + { + "fieldPath": "ownerId", + "columnName": "ownerId", + "affinity": "INTEGER" + }, + { + "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": "personal", + "columnName": "personal", + "affinity": "INTEGER" + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT" + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT" + }, + { + "fieldPath": "color", + "columnName": "color", + "affinity": "INTEGER" + }, + { + "fieldPath": "timezoneId", + "columnName": "timezoneId", + "affinity": "TEXT" + }, + { + "fieldPath": "supportsVEVENT", + "columnName": "supportsVEVENT", + "affinity": "INTEGER" + }, + { + "fieldPath": "supportsVTODO", + "columnName": "supportsVTODO", + "affinity": "INTEGER" + }, + { + "fieldPath": "supportsVJOURNAL", + "columnName": "supportsVJOURNAL", + "affinity": "INTEGER" + }, + { + "fieldPath": "source", + "columnName": "source", + "affinity": "TEXT" + }, + { + "fieldPath": "sync", + "columnName": "sync", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pushTopic", + "columnName": "pushTopic", + "affinity": "TEXT" + }, + { + "fieldPath": "supportsWebPush", + "columnName": "supportsWebPush", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "pushVapidKey", + "columnName": "pushVapidKey", + "affinity": "TEXT" + }, + { + "fieldPath": "pushSubscription", + "columnName": "pushSubscription", + "affinity": "TEXT" + }, + { + "fieldPath": "pushSubscriptionExpires", + "columnName": "pushSubscriptionExpires", + "affinity": "INTEGER" + }, + { + "fieldPath": "pushSubscriptionCreated", + "columnName": "pushSubscriptionCreated", + "affinity": "INTEGER" + } + ], + "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" + } + ], + "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, `dataType` 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": "dataType", + "columnName": "dataType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastSync", + "columnName": "lastSync", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_syncstats_collectionId_dataType", + "unique": true, + "columnNames": [ + "collectionId", + "dataType" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_syncstats_collectionId_dataType` ON `${TABLE_NAME}` (`collectionId`, `dataType`)" + } + ], + "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" + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isDirectory", + "columnName": "isDirectory", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT" + }, + { + "fieldPath": "mimeType", + "columnName": "mimeType", + "affinity": "TEXT" + }, + { + "fieldPath": "eTag", + "columnName": "eTag", + "affinity": "TEXT" + }, + { + "fieldPath": "lastModified", + "columnName": "lastModified", + "affinity": "INTEGER" + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER" + }, + { + "fieldPath": "mayBind", + "columnName": "mayBind", + "affinity": "INTEGER" + }, + { + "fieldPath": "mayUnbind", + "columnName": "mayUnbind", + "affinity": "INTEGER" + }, + { + "fieldPath": "mayWriteContent", + "columnName": "mayWriteContent", + "affinity": "INTEGER" + }, + { + "fieldPath": "quotaAvailable", + "columnName": "quotaAvailable", + "affinity": "INTEGER" + }, + { + "fieldPath": "quotaUsed", + "columnName": "quotaUsed", + "affinity": "INTEGER" + } + ], + "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" + ] + } + } + ], + "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, '6a30d678208d0aa38dd86951c67c2abe')" + ] + } +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/at/bitfire/davdroid/db/CollectionTest.kt b/app/src/androidTest/kotlin/at/bitfire/davdroid/db/CollectionTest.kt index 56166c88c2..eff28992d1 100644 --- a/app/src/androidTest/kotlin/at/bitfire/davdroid/db/CollectionTest.kt +++ b/app/src/androidTest/kotlin/at/bitfire/davdroid/db/CollectionTest.kt @@ -71,7 +71,7 @@ class CollectionTest { lateinit var info: Collection DavResource(httpClient.okHttpClient, server.url("/")) .propfind(0, ResourceType.NAME) { response, _ -> - info = Collection.fromDavResponse(response) ?: throw IllegalArgumentException() + info = Collection.fromDavResponse(response, null) ?: throw IllegalArgumentException() } assertEquals(Collection.TYPE_ADDRESSBOOK, info.type) assertTrue(info.privWriteContent) @@ -127,7 +127,7 @@ class CollectionTest { lateinit var info: Collection DavResource(httpClient.okHttpClient, server.url("/")) .propfind(0, ResourceType.NAME) { response, _ -> - info = Collection.fromDavResponse(response)!! + info = Collection.fromDavResponse(response, null)!! } assertEquals(Collection.TYPE_CALENDAR, info.type) assertFalse(info.privWriteContent) @@ -163,7 +163,7 @@ class CollectionTest { lateinit var info: Collection DavResource(httpClient.okHttpClient, server.url("/")) .propfind(0, ResourceType.NAME) { response, _ -> - info = Collection.fromDavResponse(response)!! + info = Collection.fromDavResponse(response, null)!! } assertEquals(Collection.TYPE_CALENDAR, info.type) assertFalse(info.privWriteContent) @@ -197,7 +197,7 @@ class CollectionTest { lateinit var info: Collection DavResource(httpClient.okHttpClient, server.url("/")) .propfind(0, ResourceType.NAME) { response, _ -> - info = Collection.fromDavResponse(response) ?: throw IllegalArgumentException() + info = Collection.fromDavResponse(response, null) ?: throw IllegalArgumentException() } assertEquals(Collection.TYPE_WEBCAL, info.type) assertEquals("Sample Subscription", info.displayName) 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 77e11df426..a536d676ee 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/db/AppDatabase.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/db/AppDatabase.kt @@ -43,7 +43,8 @@ import javax.inject.Singleton SyncStats::class, WebDavDocument::class, WebDavMount::class -], exportSchema = true, version = 18, autoMigrations = [ +], exportSchema = true, version = 19, autoMigrations = [ + AutoMigration(from = 18, to = 19), // collection: add personal flag AutoMigration(from = 17, to = 18, spec = AutoMigration18::class), AutoMigration(from = 16, to = 17), // collection: add VAPID key AutoMigration(from = 15, to = 16, spec = AutoMigration16::class), diff --git a/app/src/main/kotlin/at/bitfire/davdroid/db/Collection.kt b/app/src/main/kotlin/at/bitfire/davdroid/db/Collection.kt index ef6dedbc77..5ae9406930 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/db/Collection.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/db/Collection.kt @@ -25,6 +25,7 @@ import at.bitfire.dav4jvm.property.push.WebPush import at.bitfire.dav4jvm.property.webdav.CurrentUserPrivilegeSet import at.bitfire.dav4jvm.property.webdav.DisplayName import at.bitfire.dav4jvm.property.webdav.ResourceType +import at.bitfire.davdroid.servicedetection.ServiceDetectionUtils import at.bitfire.davdroid.util.DavUtils.lastSegment import at.bitfire.davdroid.util.trimToNull import at.bitfire.ical4android.util.DateUtils @@ -100,6 +101,12 @@ data class Collection( */ val forceReadOnly: Boolean = false, + /** + * Whether this collection's `DAV:owner` property matches `current-user-principal`. + * `null` indicates an unknown principal or that `DAV:owner` was not set. + */ + val personal: Boolean? = null, + /** * Human-readable name of the collection */ @@ -162,7 +169,7 @@ data class Collection( * @param dav WebDAV response * @return null if the response doesn't represent a collection */ - fun fromDavResponse(dav: Response): Collection? { + fun fromDavResponse(dav: Response, principalUrl: HttpUrl?): Collection? { val url = UrlUtils.withTrailingSlash(dav.href) val type: String = dav[ResourceType::class.java]?.let { resourceType -> when { @@ -252,7 +259,8 @@ data class Collection( source = source, supportsWebPush = supportsWebPush, pushVapidKey = vapidPublicKey, - pushTopic = pushTopic + pushTopic = pushTopic, + personal = ServiceDetectionUtils.isPersonal(principalUrl, dav) ) } diff --git a/app/src/main/kotlin/at/bitfire/davdroid/servicedetection/CollectionsWithoutHomeSetRefresher.kt b/app/src/main/kotlin/at/bitfire/davdroid/servicedetection/CollectionsWithoutHomeSetRefresher.kt index e7da2659e2..0d4d2419e5 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/servicedetection/CollectionsWithoutHomeSetRefresher.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/servicedetection/CollectionsWithoutHomeSetRefresher.kt @@ -49,7 +49,7 @@ class CollectionsWithoutHomeSetRefresher @AssistedInject constructor( } // Save or update the collection, if usable, otherwise delete it - Collection.fromDavResponse(response)?.let { collection -> + Collection.fromDavResponse(response, service.principal)?.let { collection -> if (!ServiceDetectionUtils.isUsableCollection(service, collection)) return@let collectionRepository.insertOrUpdateByUrlRememberSync(collection.copy( diff --git a/app/src/main/kotlin/at/bitfire/davdroid/servicedetection/DavResourceFinder.kt b/app/src/main/kotlin/at/bitfire/davdroid/servicedetection/DavResourceFinder.kt index 494d9c1153..d3f010b871 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/servicedetection/DavResourceFinder.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/servicedetection/DavResourceFinder.kt @@ -324,7 +324,7 @@ class DavResourceFinder @AssistedInject constructor( davResponse[ResourceType::class.java]?.let { // Is it a calendar or an address book, ... if (it.types.contains(resourceType)) - Collection.fromDavResponse(davResponse)?.let { info -> + Collection.fromDavResponse(davResponse, principal)?.let { info -> log.info("Found resource of type $resourceType at ${info.url}") config.collections[info.url] = info } diff --git a/app/src/main/kotlin/at/bitfire/davdroid/servicedetection/HomeSetRefresher.kt b/app/src/main/kotlin/at/bitfire/davdroid/servicedetection/HomeSetRefresher.kt index f5c4b5007a..84a9c0faf7 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/servicedetection/HomeSetRefresher.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/servicedetection/HomeSetRefresher.kt @@ -77,12 +77,13 @@ class HomeSetRefresher @AssistedInject constructor( homeSetRepository.insertOrUpdateByUrlBlocking( localHomeset.copy( displayName = response[DisplayName::class.java]?.displayName, - privBind = response[CurrentUserPrivilegeSet::class.java]?.mayBind != false + privBind = response[CurrentUserPrivilegeSet::class.java]?.mayBind != false, + personal = ServiceDetectionUtils.isPersonal(service.principal, response) == true ) ) // in any case, check whether the response is about a usable collection - var collection = Collection.fromDavResponse(response) ?: return@propfind + var collection = Collection.fromDavResponse(response, service.principal) ?: return@propfind collection = collection.copy( serviceId = service.id, homeSetId = localHomeset.id, diff --git a/app/src/main/kotlin/at/bitfire/davdroid/servicedetection/ServiceDetectionUtils.kt b/app/src/main/kotlin/at/bitfire/davdroid/servicedetection/ServiceDetectionUtils.kt index b6f4078ee4..02a27eeb1f 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/servicedetection/ServiceDetectionUtils.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/servicedetection/ServiceDetectionUtils.kt @@ -5,6 +5,8 @@ package at.bitfire.davdroid.servicedetection import at.bitfire.dav4jvm.Property +import at.bitfire.dav4jvm.Response +import at.bitfire.dav4jvm.equalsForWebDAV import at.bitfire.dav4jvm.property.caldav.CalendarColor import at.bitfire.dav4jvm.property.caldav.CalendarDescription import at.bitfire.dav4jvm.property.caldav.CalendarTimezone @@ -21,6 +23,7 @@ import at.bitfire.dav4jvm.property.webdav.ResourceType import at.bitfire.davdroid.db.Collection import at.bitfire.davdroid.db.Service import at.bitfire.davdroid.db.ServiceType +import okhttp3.HttpUrl object ServiceDetectionUtils { @@ -63,4 +66,29 @@ object ServiceDetectionUtils { (service.type == Service.TYPE_CALDAV && arrayOf(Collection.TYPE_CALENDAR, Collection.TYPE_WEBCAL).contains(collection.type)) || (collection.type == Collection.TYPE_WEBCAL && collection.source != null) + /** + * Evaluates whether a response is personal or not. + * It takes the [Owner] property from the response, and compares its value against [principal]. + * + * If either one of those is not set (`null`), this function returns `null`. + * @param principal The current principal url to compare the owner against. + * @param davResponse The response to process. + * @return + * - `null` if either [principal] or [davResponse]'s [Owner] are null, or if the owner url cannot be resolved on [principal]. + * - `true` if the owner matches a principal. + * - `false` if the owner doesn't match a principal or the owner and/or principal is not set / unknown. + */ + fun isPersonal(principal: HttpUrl?, davResponse: Response): Boolean? { + // Owner must be set in order to check if the home set is personal + val ownerHref = davResponse[Owner::class.java]?.href ?: return null + principal ?: return null + + // Try to resolve the owner href + val ownerResolvedHref = principal.resolve(ownerHref) + if (ownerResolvedHref == null) return null + + // If both fields are set, compare them + return ownerResolvedHref.equalsForWebDAV(principal) + } + } \ No newline at end of file