mirror of
https://github.com/bitfireAT/davx5-ose
synced 2024-07-23 11:39:15 +00:00
show real name of davowner (bitfireAT/davx5#208)
* add principal table with dao * add principal table with dao * collection saves ownerId instead of owner URL * save and refresh principals * show display name of collection owner in GUI * show only the owner name (preferably) or respective url * remove principals which do not own any collections * Don't mock AppDatabase * ensure we are really dealing with a principal and save it even without its display name * ensure owner label is hidden when neither owner-displayname nor owner-url are available * save principal urls without trailing slash * use a custom query to find principals without collections * Some changes - insertOrUpdateByUrl - don't explicitly set id=0 when not necessary, - make it work when there are already entries with trailing slahes - added TODOs * Small changes - Update principal only if display name changed - Rename methods - Kdoc - Tests * stop using simple methods with vague names * rename method insertOrUpdateOrGet to insertOrUpdate and leave existing kdoc for explanation --------- Co-authored-by: Ricki Hirner <hirner@bitfire.at>
This commit is contained in:
parent
26961d9749
commit
b83d116f40
615
app/schemas/at.bitfire.davdroid.db.AppDatabase/12.json
Normal file
615
app/schemas/at.bitfire.davdroid.db.AppDatabase/12.json
Normal file
|
@ -0,0 +1,615 @@
|
|||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 12,
|
||||
"identityHash": "67fafceecee2d97cac6a62d46fa2c3e2",
|
||||
"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",
|
||||
"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, `timezone` TEXT, `supportsVEVENT` INTEGER, `supportsVTODO` INTEGER, `supportsVJOURNAL` INTEGER, `source` TEXT, `sync` INTEGER NOT NULL, 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": "timezone",
|
||||
"columnName": "timezone",
|
||||
"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
|
||||
}
|
||||
],
|
||||
"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_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`)"
|
||||
}
|
||||
],
|
||||
"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, '67fafceecee2d97cac6a62d46fa2c3e2')"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -5,8 +5,6 @@
|
|||
package at.bitfire.davdroid
|
||||
|
||||
import android.content.Context
|
||||
import androidx.room.Room
|
||||
import at.bitfire.davdroid.db.AppDatabase
|
||||
import at.bitfire.davdroid.settings.SettingsManager
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
|
@ -20,17 +18,11 @@ import javax.inject.Singleton
|
|||
@TestInstallIn(
|
||||
components = [ SingletonComponent::class ],
|
||||
replaces = [
|
||||
AppDatabase.AppDatabaseModule::class,
|
||||
SettingsManager.SettingsManagerModule::class
|
||||
]
|
||||
)
|
||||
class MockingModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun spykAppDatabase(@ApplicationContext context: Context): AppDatabase =
|
||||
spyk(Room.inMemoryDatabaseBuilder(context, AppDatabase::class.java).build())
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun spykSettingsManager(@ApplicationContext context: Context): SettingsManager =
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
package at.bitfire.davdroid.db
|
||||
|
||||
import android.content.Context
|
||||
import androidx.room.Room
|
||||
import androidx.room.testing.MigrationTestHelper
|
||||
import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory
|
||||
|
@ -15,27 +16,34 @@ class AppDatabaseTest {
|
|||
|
||||
val TEST_DB = "test"
|
||||
|
||||
val context: Context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
|
||||
@Rule
|
||||
@JvmField
|
||||
val helper = MigrationTestHelper(
|
||||
InstrumentationRegistry.getInstrumentation(),
|
||||
AppDatabase::class.java.canonicalName,
|
||||
FrameworkSQLiteOpenHelperFactory()
|
||||
InstrumentationRegistry.getInstrumentation(),
|
||||
AppDatabase::class.java,
|
||||
listOf(), // no auto migrations until v8
|
||||
FrameworkSQLiteOpenHelperFactory()
|
||||
)
|
||||
|
||||
|
||||
@Test
|
||||
fun testAllMigrations() {
|
||||
// DB schema is available since version 8
|
||||
// DB schema is available since version 8, so create DB with v8
|
||||
helper.createDatabase(TEST_DB, 8).close()
|
||||
|
||||
Room.databaseBuilder(
|
||||
InstrumentationRegistry.getInstrumentation().targetContext,
|
||||
AppDatabase::class.java,
|
||||
TEST_DB
|
||||
).addMigrations(*AppDatabase.migrations).build().apply {
|
||||
openHelper.writableDatabase
|
||||
close()
|
||||
val db = Room.databaseBuilder(context, AppDatabase::class.java, TEST_DB)
|
||||
// manual migrations
|
||||
.addMigrations(*AppDatabase.migrations)
|
||||
// auto-migrations that need to be specified explicitly
|
||||
.addAutoMigrationSpec(AppDatabase.AutoMigration11_12(context))
|
||||
.build()
|
||||
try {
|
||||
// open (with version 8) + migrate (to current version) database
|
||||
db.openHelper.writableDatabase
|
||||
} finally {
|
||||
db.close()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -4,26 +4,32 @@
|
|||
|
||||
package at.bitfire.davdroid.db
|
||||
|
||||
import androidx.room.Room
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
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
|
||||
|
||||
@HiltAndroidTest
|
||||
class HomesetDaoTest {
|
||||
|
||||
private lateinit var db: AppDatabase
|
||||
@get:Rule
|
||||
var hiltRule = HiltAndroidRule(this)
|
||||
|
||||
@Inject
|
||||
lateinit var db: AppDatabase
|
||||
|
||||
@Before
|
||||
fun createDb() {
|
||||
val context = InstrumentationRegistry.getInstrumentation().context
|
||||
db = Room.inMemoryDatabaseBuilder(context, AppDatabase::class.java).build()
|
||||
fun setUp() {
|
||||
hiltRule.inject()
|
||||
}
|
||||
|
||||
@After
|
||||
fun closeDb() {
|
||||
fun tearDown() {
|
||||
db.close()
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
/***************************************************************************************************
|
||||
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
|
||||
**************************************************************************************************/
|
||||
|
||||
package at.bitfire.davdroid.db
|
||||
|
||||
import android.content.Context
|
||||
import androidx.room.Room
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import dagger.hilt.testing.TestInstallIn
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@TestInstallIn(
|
||||
components = [ SingletonComponent::class ],
|
||||
replaces = [
|
||||
AppDatabase.AppDatabaseModule::class
|
||||
]
|
||||
)
|
||||
class MemoryDbModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun inMemoryDatabase(@ApplicationContext context: Context): AppDatabase =
|
||||
Room.inMemoryDatabaseBuilder(context, AppDatabase::class.java)
|
||||
// auto-migrations that need to be specified explicitly
|
||||
.addAutoMigrationSpec(AppDatabase.AutoMigration11_12(context))
|
||||
.build()
|
||||
|
||||
}
|
|
@ -12,15 +12,10 @@ import androidx.test.platform.app.InstrumentationRegistry
|
|||
import androidx.work.Configuration
|
||||
import androidx.work.WorkManager
|
||||
import androidx.work.testing.WorkManagerTestInitHelper
|
||||
import at.bitfire.dav4jvm.DavResource
|
||||
import at.bitfire.dav4jvm.property.AddressbookHomeSet
|
||||
import at.bitfire.davdroid.HttpClient
|
||||
import at.bitfire.davdroid.TestUtils.workScheduledOrRunning
|
||||
import at.bitfire.davdroid.db.AppDatabase
|
||||
import at.bitfire.davdroid.db.*
|
||||
import at.bitfire.davdroid.db.Collection
|
||||
import at.bitfire.davdroid.db.Credentials
|
||||
import at.bitfire.davdroid.db.HomeSet
|
||||
import at.bitfire.davdroid.db.Service
|
||||
import at.bitfire.davdroid.log.Logger
|
||||
import at.bitfire.davdroid.settings.SettingsManager
|
||||
import at.bitfire.davdroid.ui.NotificationUtils
|
||||
|
@ -31,6 +26,7 @@ import okhttp3.mockwebserver.Dispatcher
|
|||
import okhttp3.mockwebserver.MockResponse
|
||||
import okhttp3.mockwebserver.MockWebServer
|
||||
import okhttp3.mockwebserver.RecordedRequest
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import org.junit.*
|
||||
import org.junit.Assert.*
|
||||
|
||||
|
@ -70,9 +66,9 @@ class RefreshCollectionsWorkerTest {
|
|||
companion object {
|
||||
private const val PATH_CALDAV = "/caldav"
|
||||
private const val PATH_CARDDAV = "/carddav"
|
||||
private const val PATH_CALDAV_AND_CARDDAV = "/both-caldav-carddav"
|
||||
|
||||
private const val SUBPATH_PRINCIPAL = "/principal"
|
||||
private const val SUBPATH_PRINCIPAL_WITHOUT_COLLECTIONS = "/principal2"
|
||||
private const val SUBPATH_ADDRESSBOOK_HOMESET = "/addressbooks-homeset"
|
||||
private const val SUBPATH_ADDRESSBOOK_HOMESET_EMPTY = "/addressbooks-homeset-empty"
|
||||
private const val SUBPATH_ADDRESSBOOK = "/addressbooks/my-contacts"
|
||||
|
@ -139,10 +135,8 @@ class RefreshCollectionsWorkerTest {
|
|||
val baseUrl = mockServer.url(PATH_CARDDAV + SUBPATH_PRINCIPAL)
|
||||
|
||||
// Query home sets
|
||||
DavResource(client.okHttpClient, baseUrl).propfind(0, AddressbookHomeSet.NAME) { response, _ ->
|
||||
RefreshCollectionsWorker.Refresher(db, service, settings, client.okHttpClient)
|
||||
.queryHomeSets(baseUrl)
|
||||
}
|
||||
RefreshCollectionsWorker.Refresher(db, service, settings, client.okHttpClient)
|
||||
.queryHomeSets(baseUrl)
|
||||
|
||||
// Check home sets have been saved to database
|
||||
assertEquals(mockServer.url("$PATH_CARDDAV$SUBPATH_ADDRESSBOOK_HOMESET/"), db.homeSetDao().getByService(service.id).first().url)
|
||||
|
@ -171,6 +165,7 @@ class RefreshCollectionsWorkerTest {
|
|||
1,
|
||||
service.id,
|
||||
homesetId,
|
||||
1, // will have gotten an owner too
|
||||
Collection.TYPE_ADDRESSBOOK,
|
||||
mockServer.url("$PATH_CARDDAV$SUBPATH_ADDRESSBOOK/"),
|
||||
displayName = "My Contacts",
|
||||
|
@ -190,6 +185,7 @@ class RefreshCollectionsWorkerTest {
|
|||
0,
|
||||
service.id,
|
||||
null,
|
||||
null,
|
||||
Collection.TYPE_ADDRESSBOOK,
|
||||
mockServer.url("$PATH_CARDDAV$SUBPATH_ADDRESSBOOK/"),
|
||||
displayName = "My Contacts",
|
||||
|
@ -207,6 +203,7 @@ class RefreshCollectionsWorkerTest {
|
|||
collectionId,
|
||||
service.id,
|
||||
null,
|
||||
null,
|
||||
Collection.TYPE_ADDRESSBOOK,
|
||||
mockServer.url("$PATH_CARDDAV$SUBPATH_ADDRESSBOOK/"),
|
||||
displayName = "My Contacts",
|
||||
|
@ -226,6 +223,7 @@ class RefreshCollectionsWorkerTest {
|
|||
0,
|
||||
service.id,
|
||||
null,
|
||||
null,
|
||||
Collection.TYPE_ADDRESSBOOK,
|
||||
mockServer.url("$PATH_CARDDAV$SUBPATH_ADDRESSBOOK/"),
|
||||
displayName = "My Contacts",
|
||||
|
@ -245,6 +243,7 @@ class RefreshCollectionsWorkerTest {
|
|||
collectionId,
|
||||
service.id,
|
||||
null,
|
||||
null,
|
||||
Collection.TYPE_ADDRESSBOOK,
|
||||
mockServer.url("$PATH_CARDDAV$SUBPATH_ADDRESSBOOK/"),
|
||||
displayName = "My Contacts",
|
||||
|
@ -271,6 +270,7 @@ class RefreshCollectionsWorkerTest {
|
|||
0,
|
||||
service.id,
|
||||
homesetId,
|
||||
null,
|
||||
Collection.TYPE_ADDRESSBOOK,
|
||||
mockServer.url("$PATH_CARDDAV$SUBPATH_ADDRESSBOOK/")
|
||||
)
|
||||
|
@ -284,6 +284,43 @@ class RefreshCollectionsWorkerTest {
|
|||
assertEquals(null, db.collectionDao().get(collectionId)!!.homeSetId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun refreshHomesetsAndTheirCollections_addsOwnerUrls() {
|
||||
val service = createTestService(Service.TYPE_CARDDAV)!!
|
||||
|
||||
// save a homeset in DB
|
||||
val homesetId = db.homeSetDao().insert(
|
||||
HomeSet(id=0, service.id, true, mockServer.url("$PATH_CARDDAV$SUBPATH_ADDRESSBOOK_HOMESET"))
|
||||
)
|
||||
|
||||
// place collection in DB - as part of the homeset
|
||||
val collectionId = db.collectionDao().insertOrUpdateByUrl(
|
||||
Collection(
|
||||
0,
|
||||
service.id,
|
||||
homesetId, // part of above home set
|
||||
null,
|
||||
Collection.TYPE_ADDRESSBOOK,
|
||||
mockServer.url("$PATH_CARDDAV$SUBPATH_ADDRESSBOOK/")
|
||||
)
|
||||
)
|
||||
|
||||
// Refresh - homesets and their collections
|
||||
assertEquals(0, db.principalDao().get(service.id).size)
|
||||
RefreshCollectionsWorker.Refresher(db, service, settings, client.okHttpClient)
|
||||
.refreshHomesetsAndTheirCollections()
|
||||
|
||||
// Check principal saved and the collection was updated with its reference
|
||||
val principals = db.principalDao().get(service.id)
|
||||
assertEquals(1, principals.size)
|
||||
assertEquals(mockServer.url("$PATH_CARDDAV$SUBPATH_PRINCIPAL"), principals[0].url)
|
||||
assertEquals(null, principals[0].displayName)
|
||||
assertEquals(
|
||||
principals[0].id,
|
||||
db.collectionDao().get(collectionId)!!.ownerId
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
// refreshHomelessCollections
|
||||
|
||||
|
@ -297,6 +334,7 @@ class RefreshCollectionsWorkerTest {
|
|||
0,
|
||||
service.id,
|
||||
null,
|
||||
null,
|
||||
Collection.TYPE_ADDRESSBOOK,
|
||||
mockServer.url("$PATH_CARDDAV$SUBPATH_ADDRESSBOOK/"),
|
||||
)
|
||||
|
@ -312,6 +350,7 @@ class RefreshCollectionsWorkerTest {
|
|||
collectionId,
|
||||
service.id,
|
||||
null,
|
||||
1, // will have gotten an owner too
|
||||
Collection.TYPE_ADDRESSBOOK,
|
||||
mockServer.url("$PATH_CARDDAV$SUBPATH_ADDRESSBOOK/"),
|
||||
displayName = "My Contacts",
|
||||
|
@ -331,6 +370,7 @@ class RefreshCollectionsWorkerTest {
|
|||
0,
|
||||
service.id,
|
||||
null,
|
||||
null,
|
||||
Collection.TYPE_ADDRESSBOOK,
|
||||
mockServer.url("$PATH_CARDDAV$SUBPATH_ADDRESSBOOK_INACCESSIBLE")
|
||||
)
|
||||
|
@ -343,10 +383,104 @@ class RefreshCollectionsWorkerTest {
|
|||
// Check the collection got deleted
|
||||
assertEquals(null, db.collectionDao().get(collectionId))
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun refreshHomelessCollections_addsOwnerUrls() {
|
||||
val service = createTestService(Service.TYPE_CARDDAV)!!
|
||||
|
||||
// place homeless collection in DB
|
||||
val collectionId = db.collectionDao().insertOrUpdateByUrl(
|
||||
Collection(
|
||||
0,
|
||||
service.id,
|
||||
null,
|
||||
null,
|
||||
Collection.TYPE_ADDRESSBOOK,
|
||||
mockServer.url("$PATH_CARDDAV$SUBPATH_ADDRESSBOOK/"),
|
||||
)
|
||||
)
|
||||
|
||||
// Refresh homeless collections
|
||||
assertEquals(0, db.principalDao().get(service.id).size)
|
||||
RefreshCollectionsWorker.Refresher(db, service, settings, client.okHttpClient)
|
||||
.refreshHomelessCollections()
|
||||
|
||||
// Check principal saved and the collection was updated with its reference
|
||||
val principals = db.principalDao().get(service.id)
|
||||
assertEquals(1, principals.size)
|
||||
assertEquals(mockServer.url("$PATH_CARDDAV$SUBPATH_PRINCIPAL"), principals[0].url)
|
||||
assertEquals(null, principals[0].displayName)
|
||||
assertEquals(
|
||||
principals[0].id,
|
||||
db.collectionDao().get(collectionId)!!.ownerId
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
// refreshPrincipals
|
||||
|
||||
@Test
|
||||
fun refreshPrincipals_updatesPrincipal() {
|
||||
val service = createTestService(Service.TYPE_CARDDAV)!!
|
||||
|
||||
// place principal without display name in db
|
||||
val principalId = db.principalDao().insert(
|
||||
Principal(
|
||||
0,
|
||||
service.id,
|
||||
mockServer.url("$PATH_CARDDAV$SUBPATH_PRINCIPAL"), // no trailing slash
|
||||
null // no display name for now
|
||||
)
|
||||
)
|
||||
// add an associated collection - as the principal is rightfully removed otherwise
|
||||
db.collectionDao().insertOrUpdateByUrl(
|
||||
Collection(
|
||||
0,
|
||||
service.id,
|
||||
null,
|
||||
principalId, // create association with principal
|
||||
Collection.TYPE_ADDRESSBOOK,
|
||||
mockServer.url("$PATH_CARDDAV$SUBPATH_ADDRESSBOOK/"), // with trailing slash
|
||||
)
|
||||
)
|
||||
|
||||
// Refresh principals
|
||||
RefreshCollectionsWorker.Refresher(db, service, settings, client.okHttpClient)
|
||||
.refreshPrincipals()
|
||||
|
||||
// Check principal now got a display name
|
||||
val principals = db.principalDao().get(service.id)
|
||||
assertEquals(1, principals.size)
|
||||
assertEquals(mockServer.url("$PATH_CARDDAV$SUBPATH_PRINCIPAL"), principals[0].url)
|
||||
assertEquals("Mr. Wobbles", principals[0].displayName)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun refreshPrincipals_deletesPrincipalsWithoutCollections() {
|
||||
val service = createTestService(Service.TYPE_CARDDAV)!!
|
||||
|
||||
// place principal without collections in DB
|
||||
db.principalDao().insert(
|
||||
Principal(
|
||||
0,
|
||||
service.id,
|
||||
mockServer.url("$PATH_CARDDAV$SUBPATH_PRINCIPAL_WITHOUT_COLLECTIONS/")
|
||||
)
|
||||
)
|
||||
|
||||
// Refresh principals - detecting it does not own collections
|
||||
RefreshCollectionsWorker.Refresher(db, service, settings, client.okHttpClient)
|
||||
.refreshPrincipals()
|
||||
|
||||
// Check principal was deleted
|
||||
val principals = db.principalDao().get(service.id)
|
||||
assertEquals(0, principals.size)
|
||||
}
|
||||
|
||||
|
||||
// Test helpers and dependencies
|
||||
|
||||
fun createTestService(serviceType: String) : Service? {
|
||||
private fun createTestService(serviceType: String) : Service? {
|
||||
val service = Service(id=0, accountName="test", type=serviceType, principal = null)
|
||||
val serviceId = db.serviceDao().insertOrReplace(service)
|
||||
return db.serviceDao().get(serviceId)
|
||||
|
@ -355,7 +489,7 @@ class RefreshCollectionsWorkerTest {
|
|||
class TestDispatcher: Dispatcher() {
|
||||
|
||||
override fun dispatch(request: RecordedRequest): MockResponse {
|
||||
val path = request.path!!
|
||||
val path = StringUtils.removeEnd(request.path!!, "/")
|
||||
|
||||
if (request.method.equals("PROPFIND", true)) {
|
||||
val properties = when (path) {
|
||||
|
@ -366,18 +500,29 @@ class RefreshCollectionsWorkerTest {
|
|||
"</current-user-principal>"
|
||||
|
||||
PATH_CARDDAV + SUBPATH_PRINCIPAL ->
|
||||
"<resourcetype><principal/></resourcetype>" +
|
||||
"<displayname>Mr. Wobbles</displayname>" +
|
||||
"<CARD:addressbook-home-set>" +
|
||||
" <href>${PATH_CARDDAV}${SUBPATH_ADDRESSBOOK_HOMESET}</href>" +
|
||||
"</CARD:addressbook-home-set>"
|
||||
|
||||
PATH_CARDDAV + SUBPATH_ADDRESSBOOK + "/",
|
||||
PATH_CARDDAV + SUBPATH_PRINCIPAL_WITHOUT_COLLECTIONS ->
|
||||
"<CARD:addressbook-home-set>" +
|
||||
" <href>${PATH_CARDDAV}${SUBPATH_ADDRESSBOOK_HOMESET_EMPTY}</href>" +
|
||||
"</CARD:addressbook-home-set>" +
|
||||
"<displayname>Mr. Wobbles Jr.</displayname>"
|
||||
|
||||
PATH_CARDDAV + SUBPATH_ADDRESSBOOK,
|
||||
PATH_CARDDAV + SUBPATH_ADDRESSBOOK_HOMESET ->
|
||||
"<resourcetype>" +
|
||||
" <collection/>" +
|
||||
" <CARD:addressbook/>" +
|
||||
"</resourcetype>" +
|
||||
"<displayname>My Contacts</displayname>" +
|
||||
"<CARD:addressbook-description>My Contacts Description</CARD:addressbook-description>"
|
||||
"<CARD:addressbook-description>My Contacts Description</CARD:addressbook-description>" +
|
||||
"<owner>" +
|
||||
" <href>${PATH_CARDDAV + SUBPATH_PRINCIPAL}</href>" +
|
||||
"</owner>"
|
||||
|
||||
PATH_CALDAV + SUBPATH_PRINCIPAL ->
|
||||
"<CAL:calendar-user-address-set>" +
|
||||
|
@ -386,11 +531,13 @@ class RefreshCollectionsWorkerTest {
|
|||
" <href>mailto:email2@example.com</href>" +
|
||||
"</CAL:calendar-user-address-set>"
|
||||
|
||||
SUBPATH_ADDRESSBOOK_HOMESET_EMPTY -> ""
|
||||
|
||||
else -> ""
|
||||
}
|
||||
|
||||
var responseBody: String = ""
|
||||
var responseCode: Int = 207
|
||||
var responseBody = ""
|
||||
var responseCode = 207
|
||||
when (path) {
|
||||
PATH_CARDDAV + SUBPATH_ADDRESSBOOK_HOMESET ->
|
||||
responseBody =
|
||||
|
|
|
@ -13,11 +13,13 @@ import androidx.core.app.NotificationCompat
|
|||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.core.database.getStringOrNull
|
||||
import androidx.room.*
|
||||
import androidx.room.migration.AutoMigrationSpec
|
||||
import androidx.room.migration.Migration
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
import at.bitfire.davdroid.R
|
||||
import at.bitfire.davdroid.TextTable
|
||||
import at.bitfire.davdroid.log.Logger
|
||||
import at.bitfire.davdroid.servicedetection.RefreshCollectionsWorker
|
||||
import at.bitfire.davdroid.ui.AccountsActivity
|
||||
import at.bitfire.davdroid.ui.NotificationUtils
|
||||
import at.bitfire.davdroid.ui.NotificationUtils.notifyIfPossible
|
||||
|
@ -34,12 +36,14 @@ import javax.inject.Singleton
|
|||
Service::class,
|
||||
HomeSet::class,
|
||||
Collection::class,
|
||||
Principal::class,
|
||||
SyncStats::class,
|
||||
WebDavDocument::class,
|
||||
WebDavMount::class
|
||||
], exportSchema = true, version = 11, autoMigrations = [
|
||||
], exportSchema = true, version = 12, autoMigrations = [
|
||||
AutoMigration(from = 9, to = 10),
|
||||
AutoMigration(from = 10, to = 11)
|
||||
AutoMigration(from = 10, to = 11),
|
||||
AutoMigration(from = 11, to = 12, spec = AppDatabase.AutoMigration11_12::class)
|
||||
])
|
||||
@TypeConverters(Converters::class)
|
||||
abstract class AppDatabase: RoomDatabase() {
|
||||
|
@ -52,6 +56,7 @@ abstract class AppDatabase: RoomDatabase() {
|
|||
fun appDatabase(@ApplicationContext context: Context): AppDatabase =
|
||||
Room.databaseBuilder(context.applicationContext, AppDatabase::class.java, "services.db")
|
||||
.addMigrations(*migrations)
|
||||
.addAutoMigrationSpec(AutoMigration11_12(context))
|
||||
.fallbackToDestructiveMigration() // as a last fallback, recreate database instead of crashing
|
||||
.addCallback(object: Callback() {
|
||||
override fun onDestructiveMigration(db: SupportSQLiteDatabase) {
|
||||
|
@ -76,9 +81,26 @@ abstract class AppDatabase: RoomDatabase() {
|
|||
.build()
|
||||
}
|
||||
|
||||
// auto migrations
|
||||
|
||||
@ProvidedAutoMigrationSpec
|
||||
@DeleteColumn(tableName = "collection", columnName = "owner")
|
||||
class AutoMigration11_12(val context: Context): AutoMigrationSpec {
|
||||
override fun onPostMigrate(db: SupportSQLiteDatabase) {
|
||||
Logger.log.info("Database update to v12, refreshing services to get display names of owners")
|
||||
db.query("SELECT id FROM service", arrayOf()).use { cursor ->
|
||||
while (cursor.moveToNext()) {
|
||||
val serviceId = cursor.getLong(0)
|
||||
RefreshCollectionsWorker.refreshCollections(context, serviceId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
companion object {
|
||||
|
||||
// migrations
|
||||
// manual migrations
|
||||
|
||||
val migrations: Array<Migration> = arrayOf(
|
||||
object : Migration(8, 9) {
|
||||
|
@ -233,6 +255,7 @@ abstract class AppDatabase: RoomDatabase() {
|
|||
abstract fun serviceDao(): ServiceDao
|
||||
abstract fun homeSetDao(): HomeSetDao
|
||||
abstract fun collectionDao(): CollectionDao
|
||||
abstract fun principalDao(): PrincipalDao
|
||||
abstract fun syncStatsDao(): SyncStatsDao
|
||||
abstract fun webDavDocumentDao(): WebDavDocumentDao
|
||||
abstract fun webDavMountDao(): WebDavMountDao
|
||||
|
|
|
@ -16,7 +16,8 @@ import org.apache.commons.lang3.StringUtils
|
|||
@Entity(tableName = "collection",
|
||||
foreignKeys = [
|
||||
ForeignKey(entity = Service::class, parentColumns = arrayOf("id"), childColumns = arrayOf("serviceId"), onDelete = ForeignKey.CASCADE),
|
||||
ForeignKey(entity = HomeSet::class, parentColumns = arrayOf("id"), childColumns = arrayOf("homeSetId"), onDelete = ForeignKey.SET_NULL)
|
||||
ForeignKey(entity = HomeSet::class, parentColumns = arrayOf("id"), childColumns = arrayOf("homeSetId"), onDelete = ForeignKey.SET_NULL),
|
||||
ForeignKey(entity = Principal::class, parentColumns = arrayOf("id"), childColumns = arrayOf("ownerId"), onDelete = ForeignKey.SET_NULL)
|
||||
],
|
||||
indices = [
|
||||
Index("serviceId","type"),
|
||||
|
@ -40,13 +41,18 @@ data class Collection(
|
|||
*/
|
||||
var homeSetId: Long? = null,
|
||||
|
||||
/**
|
||||
* Principal who is owner of this collection.
|
||||
*/
|
||||
var ownerId: Long? = null,
|
||||
|
||||
/**
|
||||
* Type of service. CalDAV or CardDAV
|
||||
*/
|
||||
var type: String,
|
||||
|
||||
/**
|
||||
* Address where this collection lives
|
||||
* Address where this collection lives - with trailing slash
|
||||
*/
|
||||
var url: HttpUrl,
|
||||
|
||||
|
@ -56,7 +62,6 @@ data class Collection(
|
|||
|
||||
var displayName: String? = null,
|
||||
var description: String? = null,
|
||||
var owner: HttpUrl? = null,
|
||||
|
||||
// CalDAV only
|
||||
var color: Int? = null,
|
||||
|
@ -111,9 +116,6 @@ data class Collection(
|
|||
}
|
||||
|
||||
val displayName = StringUtils.trimToNull(dav[DisplayName::class.java]?.displayName)
|
||||
val owner = dav[Owner::class.java]?.href?.let { ownerHref ->
|
||||
dav.href.resolve(ownerHref)
|
||||
}
|
||||
|
||||
var description: String? = null
|
||||
var color: Int? = null
|
||||
|
@ -160,7 +162,6 @@ data class Collection(
|
|||
privWriteContent = privWriteContent,
|
||||
privUnbind = privUnbind,
|
||||
displayName = displayName,
|
||||
owner = owner,
|
||||
description = description,
|
||||
color = color,
|
||||
timezone = timezone,
|
||||
|
|
|
@ -71,6 +71,7 @@ interface CollectionDao {
|
|||
* This method preserves the primary key, as opposed to using "@Insert(onConflict = OnConflictStrategy.REPLACE)"
|
||||
* which will create a new row with incremented ID and thus breaks entity relationships!
|
||||
*
|
||||
* @param collection Collection to be inserted or updated
|
||||
* @return ID of the row, that has been inserted or updated. -1 If the insert fails due to other reasons.
|
||||
*/
|
||||
@Transaction
|
||||
|
@ -82,6 +83,23 @@ interface CollectionDao {
|
|||
localCollection.id
|
||||
} ?: insert(collection)
|
||||
|
||||
/**
|
||||
* Inserts or updates the collection. On update it will not update flag values ([Collection.sync],
|
||||
* [Collection.forceReadOnly]), but use the values of the already existing collection.
|
||||
*
|
||||
* @param newCollection Collection to be inserted or updated
|
||||
*/
|
||||
fun insertOrUpdateByUrlAndRememberFlags(newCollection: Collection) {
|
||||
// remember locally set flags
|
||||
getByServiceAndUrl(newCollection.serviceId, newCollection.url.toString())?.let { oldCollection ->
|
||||
newCollection.sync = oldCollection.sync
|
||||
newCollection.forceReadOnly = oldCollection.forceReadOnly
|
||||
}
|
||||
|
||||
// commit to database
|
||||
insertOrUpdateByUrl(newCollection)
|
||||
}
|
||||
|
||||
@Delete
|
||||
fun delete(collection: Collection)
|
||||
|
||||
|
|
69
app/src/main/java/at/bitfire/davdroid/db/Principal.kt
Normal file
69
app/src/main/java/at/bitfire/davdroid/db/Principal.kt
Normal file
|
@ -0,0 +1,69 @@
|
|||
/***************************************************************************************************
|
||||
* 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.ForeignKey
|
||||
import androidx.room.Index
|
||||
import androidx.room.PrimaryKey
|
||||
import at.bitfire.dav4jvm.Response
|
||||
import at.bitfire.dav4jvm.UrlUtils
|
||||
import at.bitfire.dav4jvm.property.DisplayName
|
||||
import at.bitfire.dav4jvm.property.ResourceType
|
||||
import okhttp3.HttpUrl
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
|
||||
@Entity(tableName = "principal",
|
||||
foreignKeys = [
|
||||
ForeignKey(entity = Service::class, parentColumns = arrayOf("id"), childColumns = arrayOf("serviceId"), onDelete = ForeignKey.CASCADE)
|
||||
],
|
||||
indices = [
|
||||
// index by service, urls are unique
|
||||
Index("serviceId", "url", unique = true)
|
||||
]
|
||||
)
|
||||
data class Principal(
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
var id: Long = 0,
|
||||
var serviceId: Long,
|
||||
/** URL of the principal, always without trailing slash */
|
||||
var url: HttpUrl,
|
||||
var displayName: String? = null
|
||||
) {
|
||||
|
||||
companion object {
|
||||
|
||||
/**
|
||||
* Generates a principal entity from a WebDAV response.
|
||||
* @param dav WebDAV response (make sure that you have queried `DAV:resource-type` and `DAV:display-name`)
|
||||
* @return generated principal data object (with `id`=0), `null` if the response doesn't represent a principal
|
||||
*/
|
||||
fun fromDavResponse(serviceId: Long, dav: Response): Principal? {
|
||||
// Check if response is a principal
|
||||
val resourceType = dav[ResourceType::class.java] ?: return null
|
||||
if (!resourceType.types.contains(ResourceType.PRINCIPAL))
|
||||
return null
|
||||
|
||||
// Try getting the display name of the principal
|
||||
val displayName: String? = StringUtils.trimToNull(
|
||||
dav[DisplayName::class.java]?.displayName
|
||||
)
|
||||
|
||||
// Create and return principal - even without it's display name
|
||||
return Principal(
|
||||
serviceId = serviceId,
|
||||
url = UrlUtils.omitTrailingSlash(dav.href),
|
||||
displayName = displayName
|
||||
)
|
||||
}
|
||||
|
||||
fun fromServiceAndUrl(service: Service, url: HttpUrl) = Principal(
|
||||
serviceId = service.id,
|
||||
url = UrlUtils.omitTrailingSlash(url)
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
}
|
52
app/src/main/java/at/bitfire/davdroid/db/PrincipalDao.kt
Normal file
52
app/src/main/java/at/bitfire/davdroid/db/PrincipalDao.kt
Normal file
|
@ -0,0 +1,52 @@
|
|||
/***************************************************************************************************
|
||||
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
|
||||
**************************************************************************************************/
|
||||
|
||||
package at.bitfire.davdroid.db
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.room.*
|
||||
import okhttp3.HttpUrl
|
||||
|
||||
@Dao
|
||||
interface PrincipalDao {
|
||||
|
||||
@Query("SELECT * FROM principal WHERE serviceId=:serviceId")
|
||||
fun get(serviceId: Long): List<Principal>
|
||||
|
||||
@Query("SELECT * FROM principal WHERE id=:id")
|
||||
fun getLive(id: Long): LiveData<Principal>
|
||||
|
||||
@Query("SELECT * FROM principal WHERE serviceId=:serviceId AND url=:url")
|
||||
fun getByUrl(serviceId: Long, url: HttpUrl): Principal?
|
||||
|
||||
/**
|
||||
* Gets all principals who do not own any collections
|
||||
*/
|
||||
@Query("SELECT * FROM principal WHERE principal.id NOT IN (SELECT ownerId FROM collection WHERE ownerId IS NOT NULL)")
|
||||
fun getAllWithoutCollections(): List<Principal>
|
||||
|
||||
@Insert
|
||||
fun insert(principal: Principal): Long
|
||||
|
||||
@Update
|
||||
fun update(principal: Principal)
|
||||
|
||||
@Delete
|
||||
fun delete(principal: Principal)
|
||||
|
||||
/**
|
||||
* Inserts, updates or just gets existing principal if its display name has not
|
||||
* changed (will not update/overwrite with null values).
|
||||
*
|
||||
* @param principal Principal to be inserted or updated
|
||||
* @return ID of the newly inserted or already existing principal
|
||||
*/
|
||||
fun insertOrUpdate(serviceId: Long, principal: Principal): Long =
|
||||
getByUrl(serviceId, principal.url)?.let { oldPrincipal ->
|
||||
if (principal.displayName != oldPrincipal.displayName)
|
||||
update(principal.copy(id = oldPrincipal.id))
|
||||
return oldPrincipal.id
|
||||
} ?: insert(principal)
|
||||
|
||||
}
|
|
@ -31,7 +31,7 @@ class LocalJtxCollection(account: Account, client: ContentProviderClient, id: Lo
|
|||
put(JtxContract.JtxCollection.URL, info.url.toString())
|
||||
put(JtxContract.JtxCollection.DISPLAYNAME, info.displayName ?: DavUtils.lastSegmentOfUrl(info.url))
|
||||
put(JtxContract.JtxCollection.DESCRIPTION, info.description)
|
||||
put(JtxContract.JtxCollection.OWNER, info.owner?.toString())
|
||||
put(JtxContract.JtxCollection.OWNER, info.ownerId)
|
||||
put(JtxContract.JtxCollection.COLOR, info.color)
|
||||
put(JtxContract.JtxCollection.SUPPORTSVEVENT, info.supportsVEVENT)
|
||||
put(JtxContract.JtxCollection.SUPPORTSVJOURNAL, info.supportsVJOURNAL)
|
||||
|
|
|
@ -19,11 +19,10 @@ import at.bitfire.dav4jvm.property.*
|
|||
import at.bitfire.davdroid.HttpClient
|
||||
import at.bitfire.davdroid.InvalidAccountException
|
||||
import at.bitfire.davdroid.R
|
||||
import at.bitfire.davdroid.db.AppDatabase
|
||||
import at.bitfire.davdroid.db.*
|
||||
import at.bitfire.davdroid.db.Collection
|
||||
import at.bitfire.davdroid.db.HomeSet
|
||||
import at.bitfire.davdroid.db.Service
|
||||
import at.bitfire.davdroid.log.Logger
|
||||
import at.bitfire.davdroid.servicedetection.RefreshCollectionsWorker.Companion.ARG_SERVICE_ID
|
||||
import at.bitfire.davdroid.settings.AccountSettings
|
||||
import at.bitfire.davdroid.settings.Settings
|
||||
import at.bitfire.davdroid.settings.SettingsManager
|
||||
|
@ -77,6 +76,12 @@ class RefreshCollectionsWorker @AssistedInject constructor(
|
|||
Source.NAME
|
||||
)
|
||||
|
||||
// Principal properties to ask the server
|
||||
val DAV_PRINCIPAL_PROPERTIES = arrayOf(
|
||||
DisplayName.NAME,
|
||||
ResourceType.NAME
|
||||
)
|
||||
|
||||
/**
|
||||
* Uniquely identifies a refresh worker. Useful for stopping work, or querying its state.
|
||||
*
|
||||
|
@ -149,17 +154,20 @@ class RefreshCollectionsWorker @AssistedInject constructor(
|
|||
val httpClient = client.okHttpClient
|
||||
val refresher = Refresher(db, service, settings, httpClient)
|
||||
|
||||
// refresh home set list (from principal url) and save them
|
||||
// refresh home set list (from principal url)
|
||||
service.principal?.let { principalUrl ->
|
||||
Logger.log.fine("Querying principal $principalUrl for home sets")
|
||||
refresher.queryHomeSets(principalUrl)
|
||||
}
|
||||
|
||||
// now refresh home sets and their member collections
|
||||
// refresh home sets and their member collections
|
||||
refresher.refreshHomesetsAndTheirCollections()
|
||||
|
||||
// also check/refresh collections without a homeset
|
||||
// also refresh collections without a home set
|
||||
refresher.refreshHomelessCollections()
|
||||
|
||||
// Lastly, refresh the principals (collection owners)
|
||||
refresher.refreshPrincipals()
|
||||
}
|
||||
|
||||
} catch(e: InvalidAccountException) {
|
||||
|
@ -219,24 +227,6 @@ class RefreshCollectionsWorker @AssistedInject constructor(
|
|||
val httpClient: OkHttpClient
|
||||
) {
|
||||
|
||||
private fun getHomesets(serviceId: Long) = db.homeSetDao().getByService(serviceId).associateBy { it.url }.toMutableMap()
|
||||
private fun getHomelessCollections(serviceId: Long) = db.collectionDao().getByServiceAndHomeset(serviceId, null).associateBy { it.url }.toMutableMap()
|
||||
|
||||
private fun deleteHomeset(homeset: HomeSet) = db.homeSetDao().delete(homeset)
|
||||
private fun deleteCollection(collection: Collection) = db.collectionDao().delete(collection)
|
||||
|
||||
private fun saveHomeset(homeset: HomeSet) = db.homeSetDao().insertOrUpdateByUrl(homeset)
|
||||
private fun saveCollection(newCollection: Collection) {
|
||||
// remember locally set flags
|
||||
db.collectionDao().getByServiceAndUrl(newCollection.serviceId, newCollection.url.toString())?.let { oldCollection ->
|
||||
newCollection.sync = oldCollection.sync
|
||||
newCollection.forceReadOnly = oldCollection.forceReadOnly
|
||||
}
|
||||
|
||||
// commit to database
|
||||
db.collectionDao().insertOrUpdateByUrl(newCollection)
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given URL defines home sets and adds them to given home set list.
|
||||
*
|
||||
|
@ -280,7 +270,7 @@ class RefreshCollectionsWorker @AssistedInject constructor(
|
|||
for (href in homeSet.hrefs)
|
||||
dav.location.resolve(href)?.let {
|
||||
val foundUrl = UrlUtils.withTrailingSlash(it)
|
||||
saveHomeset(HomeSet(0, service.id, personal, foundUrl))
|
||||
db.homeSetDao().insertOrUpdateByUrl(HomeSet(0, service.id, personal, foundUrl))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -325,7 +315,8 @@ class RefreshCollectionsWorker @AssistedInject constructor(
|
|||
* and a null value for it's homeset. Refreshing of collections without homesets is then handled by [refreshHomelessCollections].
|
||||
*/
|
||||
internal fun refreshHomesetsAndTheirCollections() {
|
||||
getHomesets(service.id).forEach { (homeSetUrl, localHomeset) ->
|
||||
val homesets = db.homeSetDao().getByService(service.id).associateBy { it.url }.toMutableMap()
|
||||
for((homeSetUrl, localHomeset) in homesets) {
|
||||
Logger.log.fine("Listing home set $homeSetUrl")
|
||||
|
||||
// To find removed collections in this homeset: create a queue from existing collections and remove every collection that
|
||||
|
@ -345,7 +336,7 @@ class RefreshCollectionsWorker @AssistedInject constructor(
|
|||
// this response is about the homeset itself
|
||||
localHomeset.displayName = response[DisplayName::class.java]?.displayName
|
||||
localHomeset.privBind = response[CurrentUserPrivilegeSet::class.java]?.mayBind ?: true
|
||||
saveHomeset(localHomeset)
|
||||
db.homeSetDao().insertOrUpdateByUrl(localHomeset)
|
||||
}
|
||||
|
||||
// in any case, check whether the response is about a usable collection
|
||||
|
@ -354,13 +345,21 @@ class RefreshCollectionsWorker @AssistedInject constructor(
|
|||
collection.serviceId = service.id
|
||||
collection.homeSetId = localHomeset.id
|
||||
collection.sync = settings.getBoolean(Settings.SYNC_ALL_COLLECTIONS)
|
||||
collection.owner = response[Owner::class.java]?.href?.let { response.href.resolve(it) }
|
||||
|
||||
// .. and save the principal url (collection owner)
|
||||
response[Owner::class.java]?.href
|
||||
?.let { response.href.resolve(it) }
|
||||
?.let { principalUrl ->
|
||||
val principal = Principal.fromServiceAndUrl(service, principalUrl)
|
||||
val id = db.principalDao().insertOrUpdate(service.id, principal)
|
||||
collection.ownerId = id
|
||||
}
|
||||
|
||||
Logger.log.log(Level.FINE, "Found collection", collection)
|
||||
|
||||
// save or update collection if usable (ignore it otherwise)
|
||||
if (isUsableCollection(collection))
|
||||
saveCollection(collection)
|
||||
db.collectionDao().insertOrUpdateByUrlAndRememberFlags(collection)
|
||||
|
||||
// Remove this collection from queue - because it was found in the home set
|
||||
localHomesetCollections.remove(collection.url)
|
||||
|
@ -368,13 +367,13 @@ class RefreshCollectionsWorker @AssistedInject constructor(
|
|||
} catch (e: HttpException) {
|
||||
// delete home set locally if it was not accessible (40x)
|
||||
if (e.code in arrayOf(403, 404, 410))
|
||||
deleteHomeset(localHomeset)
|
||||
db.homeSetDao().delete(localHomeset)
|
||||
}
|
||||
|
||||
// Mark leftover (not rediscovered) collections from queue as homeless (remove association)
|
||||
for ((_, homelessCollection) in localHomesetCollections) {
|
||||
homelessCollection.homeSetId = null
|
||||
saveCollection(homelessCollection)
|
||||
db.collectionDao().insertOrUpdateByUrlAndRememberFlags(homelessCollection)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -386,29 +385,65 @@ class RefreshCollectionsWorker @AssistedInject constructor(
|
|||
* It queries each stored collection with a homeSetId of "null" and either updates or deletes (if inaccessible or unusable) them.
|
||||
*/
|
||||
internal fun refreshHomelessCollections() {
|
||||
getHomelessCollections(service.id).forEach { (url, localCollection) ->
|
||||
try {
|
||||
DavResource(httpClient, url).propfind(0, *DAV_COLLECTION_PROPERTIES) { response, _ ->
|
||||
if (!response.isSuccess()) {
|
||||
deleteCollection(localCollection)
|
||||
return@propfind
|
||||
}
|
||||
|
||||
// Save or update the collection, if usable, otherwise delete it
|
||||
Collection.fromDavResponse(response)?.let { collection ->
|
||||
if (!isUsableCollection(collection))
|
||||
return@let
|
||||
collection.serviceId = localCollection.serviceId // use same service ID as previous entry
|
||||
saveCollection(collection)
|
||||
} ?: deleteCollection(localCollection)
|
||||
val homelessCollections = db.collectionDao().getByServiceAndHomeset(service.id, null).associateBy { it.url }.toMutableMap()
|
||||
for((url, localCollection) in homelessCollections) try {
|
||||
DavResource(httpClient, url).propfind(0, *DAV_COLLECTION_PROPERTIES) { response, _ ->
|
||||
if (!response.isSuccess()) {
|
||||
db.collectionDao().delete(localCollection)
|
||||
return@propfind
|
||||
}
|
||||
} catch (e: HttpException) {
|
||||
// delete collection locally if it was not accessible (40x)
|
||||
if (e.code in arrayOf(403, 404, 410))
|
||||
deleteCollection(localCollection)
|
||||
else
|
||||
throw e
|
||||
|
||||
// Save or update the collection, if usable, otherwise delete it
|
||||
Collection.fromDavResponse(response)?.let { collection ->
|
||||
if (!isUsableCollection(collection))
|
||||
return@let
|
||||
collection.serviceId = localCollection.serviceId // use same service ID as previous entry
|
||||
|
||||
// .. and save the principal url (collection owner)
|
||||
response[Owner::class.java]?.href
|
||||
?.let { response.href.resolve(it) }
|
||||
?.let { principalUrl ->
|
||||
val principal = Principal.fromServiceAndUrl(service, principalUrl)
|
||||
val principalId = db.principalDao().insertOrUpdate(service.id, principal)
|
||||
collection.ownerId = principalId
|
||||
}
|
||||
|
||||
db.collectionDao().insertOrUpdateByUrlAndRememberFlags(collection)
|
||||
} ?: db.collectionDao().delete(localCollection)
|
||||
}
|
||||
} catch (e: HttpException) {
|
||||
// delete collection locally if it was not accessible (40x)
|
||||
if (e.code in arrayOf(403, 404, 410))
|
||||
db.collectionDao().delete(localCollection)
|
||||
else
|
||||
throw e
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes the principals (get their current display names).
|
||||
* Also removes principals which do not own any collections anymore.
|
||||
*/
|
||||
internal fun refreshPrincipals() {
|
||||
// Refresh principals (collection owner urls)
|
||||
val principals = db.principalDao().get(service.id)
|
||||
for (oldPrincipal in principals) {
|
||||
val principalUrl = oldPrincipal.url
|
||||
Logger.log.fine("Querying principal $principalUrl")
|
||||
DavResource(httpClient, principalUrl).propfind(0, *DAV_PRINCIPAL_PROPERTIES) { response, _ ->
|
||||
if (!response.isSuccess())
|
||||
return@propfind
|
||||
Principal.fromDavResponse(service.id, response)?.let { principal ->
|
||||
Logger.log.fine("Got principal: $principal")
|
||||
db.principalDao().insertOrUpdate(service.id, principal)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Delete principals which don't own any collections
|
||||
db.principalDao().getAllWithoutCollections().forEach {principal ->
|
||||
db.principalDao().delete(principal)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ import android.view.View
|
|||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.Transformations
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import at.bitfire.davdroid.databinding.CollectionPropertiesBinding
|
||||
|
@ -66,6 +67,11 @@ class CollectionInfoFragment: DialogFragment() {
|
|||
}
|
||||
|
||||
var collection = db.collectionDao().getLive(collectionId)
|
||||
var owner = Transformations.switchMap(collection) { collection ->
|
||||
collection.ownerId?.let { ownerId ->
|
||||
db.principalDao().getLive(ownerId)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -53,19 +53,29 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginBottom="4dp"
|
||||
android:visibility="@{model.collection.owner != null ? View.VISIBLE : View.GONE}"
|
||||
android:visibility="@{model.owner.displayName != null || model.owner.url != null ? View.VISIBLE : View.GONE}"
|
||||
android:text="@string/collection_properties_owner"
|
||||
android:labelFor="@id/owner" />
|
||||
android:labelFor="@id/ownerDisplayName" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/owner"
|
||||
android:id="@+id/ownerDisplayName"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="@{model.collection.owner != null ? View.VISIBLE : View.GONE}"
|
||||
android:visibility="@{model.owner.displayName != null ? View.VISIBLE : View.GONE}"
|
||||
android:textSize="16sp"
|
||||
android:textIsSelectable="true"
|
||||
android:text="@{model.owner.displayName}"
|
||||
tools:text="Max Murmelmann"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/ownerUrl"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="@{model.owner.displayName == null && model.owner.url != null ? View.VISIBLE : View.GONE}"
|
||||
android:fontFamily="monospace"
|
||||
android:textSize="12sp"
|
||||
android:textIsSelectable="true"
|
||||
android:text="@{model.collection.owner.toString()}"
|
||||
android:text="@{model.owner.url.toString()}"
|
||||
tools:text="/remote.php/dav/principals/users/sample-owner/"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
|
Loading…
Reference in a new issue