mirror of
https://github.com/bitfireAT/davx5-ose.git
synced 2025-12-23 23:17:50 -05:00
[Push] Handle subscription expiration (#1131)
* Store subscription expiration in DB * Regularly run PushRegistrationWorker, if needed * Skip re-registering subscriptions that are not about to expire * Add back-off for PushRegistrationWorkerManager * Request expiration in 3 days * Show expiration in UI, timestamps in seconds * Fix tests
This commit is contained in:
675
app/schemas/at.bitfire.davdroid.db.AppDatabase/15.json
Normal file
675
app/schemas/at.bitfire.davdroid.db.AppDatabase/15.json
Normal file
@@ -0,0 +1,675 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 15,
|
||||
"identityHash": "ab1cb6057d8e050f6648bea46ae0943d",
|
||||
"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, `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": "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
|
||||
},
|
||||
{
|
||||
"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, 'ab1cb6057d8e050f6648bea46ae0943d')"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
package at.bitfire.davdroid
|
||||
|
||||
import at.bitfire.davdroid.push.PushRegistrationWorker
|
||||
import at.bitfire.davdroid.push.PushRegistrationWorkerManager
|
||||
import at.bitfire.davdroid.repository.DavCollectionRepository
|
||||
import at.bitfire.davdroid.startup.StartupPlugin
|
||||
import at.bitfire.davdroid.startup.TasksAppWatcher
|
||||
@@ -15,7 +15,7 @@ interface TestModules {
|
||||
@Module
|
||||
@TestInstallIn(
|
||||
components = [SingletonComponent::class],
|
||||
replaces = [PushRegistrationWorker.PushRegistrationWorkerModule::class]
|
||||
replaces = [PushRegistrationWorkerManager.PushRegistrationWorkerModule::class]
|
||||
)
|
||||
abstract class TestPushRegistrationWorkerModule {
|
||||
// provides empty set of listeners
|
||||
|
||||
@@ -5,6 +5,7 @@ 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 dagger.Lazy
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.android.testing.HiltAndroidRule
|
||||
import dagger.hilt.android.testing.HiltAndroidTest
|
||||
@@ -63,7 +64,17 @@ class DavCollectionRepositoryTest {
|
||||
)
|
||||
)
|
||||
val testObserver = mockk<DavCollectionRepository.OnChangeListener>(relaxed = true)
|
||||
val collectionRepository = DavCollectionRepository(accountSettingsFactory, context, db, mutableSetOf(testObserver), serviceRepository)
|
||||
val collectionRepository = DavCollectionRepository(
|
||||
accountSettingsFactory,
|
||||
context,
|
||||
db,
|
||||
object : Lazy<Set<DavCollectionRepository.OnChangeListener>> {
|
||||
override fun get(): Set<DavCollectionRepository.OnChangeListener> {
|
||||
return mutableSetOf(testObserver)
|
||||
}
|
||||
},
|
||||
serviceRepository
|
||||
)
|
||||
|
||||
assert(db.collectionDao().get(collectionId)?.forceReadOnly == false)
|
||||
verify(exactly = 0) {
|
||||
|
||||
@@ -44,12 +44,13 @@ import javax.inject.Singleton
|
||||
SyncStats::class,
|
||||
WebDavDocument::class,
|
||||
WebDavMount::class
|
||||
], exportSchema = true, version = 14, autoMigrations = [
|
||||
], exportSchema = true, version = 15, autoMigrations = [
|
||||
AutoMigration(from = 9, to = 10),
|
||||
AutoMigration(from = 10, to = 11),
|
||||
AutoMigration(from = 11, to = 12, spec = AppDatabase.AutoMigration11_12::class),
|
||||
AutoMigration(from = 12, to = 13),
|
||||
AutoMigration(from = 13, to = 14)
|
||||
AutoMigration(from = 13, to = 14),
|
||||
AutoMigration(from = 14, to = 15)
|
||||
])
|
||||
@TypeConverters(Converters::class)
|
||||
abstract class AppDatabase: RoomDatabase() {
|
||||
@@ -218,28 +219,6 @@ abstract class AppDatabase: RoomDatabase() {
|
||||
// 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*")
|
||||
|
||||
/*val edit = PreferenceManager.getDefaultSharedPreferences(context).edit()
|
||||
try {
|
||||
db.query("settings", arrayOf("setting", "value"), null, null, null, null, null).use { cursor ->
|
||||
while (cursor.moveToNext()) {
|
||||
when (cursor.getString(0)) {
|
||||
"distrustSystemCerts" -> edit.putBoolean(App.DISTRUST_SYSTEM_CERTIFICATES, cursor.getInt(1) != 0)
|
||||
"overrideProxy" -> edit.putBoolean(App.OVERRIDE_PROXY, cursor.getInt(1) != 0)
|
||||
"overrideProxyHost" -> edit.putString(App.OVERRIDE_PROXY_HOST, cursor.getString(1))
|
||||
"overrideProxyPort" -> edit.putInt(App.OVERRIDE_PROXY_PORT, cursor.getInt(1))
|
||||
|
||||
StartupDialogFragment.HINT_GOOGLE_PLAY_ACCOUNTS_REMOVED ->
|
||||
edit.putBoolean(StartupDialogFragment.HINT_GOOGLE_PLAY_ACCOUNTS_REMOVED, cursor.getInt(1) != 0)
|
||||
StartupDialogFragment.HINT_OPENTASKS_NOT_INSTALLED ->
|
||||
edit.putBoolean(StartupDialogFragment.HINT_OPENTASKS_NOT_INSTALLED, cursor.getInt(1) != 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
db.execSQL("DROP TABLE settings")
|
||||
} finally {
|
||||
edit.apply()
|
||||
}*/
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -127,7 +127,10 @@ data class Collection(
|
||||
/** WebDAV-Push subscription URL */
|
||||
var pushSubscription: String? = null,
|
||||
|
||||
/** when the [pushSubscription] was created/updated (used to determine whether we need to re-subscribe) */
|
||||
/** when the [pushSubscription] expires (timestamp, used to determine whether we need to re-subscribe) */
|
||||
var pushSubscriptionExpires: Long? = null,
|
||||
|
||||
/** when the [pushSubscription] was created/updated (timestamp) */
|
||||
var pushSubscriptionCreated: Long? = null
|
||||
|
||||
) {
|
||||
|
||||
@@ -38,6 +38,9 @@ interface CollectionDao {
|
||||
@Query("SELECT COUNT(*) FROM collection WHERE serviceId=:serviceId AND type=:type")
|
||||
suspend fun anyOfType(serviceId: Long, type: String): Boolean
|
||||
|
||||
@Query("SELECT COUNT(*) FROM collection WHERE supportsWebPush AND pushTopic IS NOT NULL")
|
||||
suspend fun anyPushCapable(): Boolean
|
||||
|
||||
/**
|
||||
* Returns collections which
|
||||
* - support VEVENT and/or VTODO (= supported calendar collections), or
|
||||
@@ -87,8 +90,8 @@ interface CollectionDao {
|
||||
@Query("UPDATE collection SET forceReadOnly=:forceReadOnly WHERE id=:id")
|
||||
suspend fun updateForceReadOnly(id: Long, forceReadOnly: Boolean)
|
||||
|
||||
@Query("UPDATE collection SET pushSubscription=:pushSubscription, pushSubscriptionCreated=:updatedAt WHERE id=:id")
|
||||
fun updatePushSubscription(id: Long, pushSubscription: String?, updatedAt: Long = System.currentTimeMillis())
|
||||
@Query("UPDATE collection SET pushSubscription=:pushSubscription, pushSubscriptionExpires=:pushSubscriptionExpires, pushSubscriptionCreated=:updatedAt WHERE id=:id")
|
||||
fun updatePushSubscription(id: Long, pushSubscription: String?, pushSubscriptionExpires: Long?, updatedAt: Long = System.currentTimeMillis()/1000)
|
||||
|
||||
@Query("UPDATE collection SET sync=:sync WHERE id=:id")
|
||||
suspend fun updateSync(id: Long, sync: Boolean)
|
||||
|
||||
@@ -7,15 +7,11 @@ package at.bitfire.davdroid.push
|
||||
import android.accounts.Account
|
||||
import android.content.Context
|
||||
import androidx.hilt.work.HiltWorker
|
||||
import androidx.work.Constraints
|
||||
import androidx.work.CoroutineWorker
|
||||
import androidx.work.ExistingWorkPolicy
|
||||
import androidx.work.NetworkType
|
||||
import androidx.work.OneTimeWorkRequestBuilder
|
||||
import androidx.work.WorkManager
|
||||
import androidx.work.WorkerParameters
|
||||
import at.bitfire.dav4jvm.DavCollection
|
||||
import at.bitfire.dav4jvm.DavResource
|
||||
import at.bitfire.dav4jvm.HttpUtils
|
||||
import at.bitfire.dav4jvm.Property
|
||||
import at.bitfire.dav4jvm.XmlUtils
|
||||
import at.bitfire.dav4jvm.XmlUtils.insertTag
|
||||
@@ -28,31 +24,23 @@ import at.bitfire.davdroid.repository.DavCollectionRepository
|
||||
import at.bitfire.davdroid.repository.DavServiceRepository
|
||||
import at.bitfire.davdroid.repository.PreferenceRepository
|
||||
import at.bitfire.davdroid.settings.AccountSettings
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import dagger.multibindings.IntoSet
|
||||
import kotlinx.coroutines.runInterruptible
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import java.io.IOException
|
||||
import java.io.StringWriter
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
import java.util.logging.Level
|
||||
import java.util.logging.Logger
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Worker that registers push for all collections that support it.
|
||||
* To be run as soon as a collection that supports push is changed (selected for sync status
|
||||
* changes, or collection is created, deleted, etc).
|
||||
*
|
||||
* TODO Should run periodically, too (to refresh registrations that are about to expire).
|
||||
* Not required for a first demonstration version.
|
||||
*/
|
||||
@Suppress("unused")
|
||||
@HiltWorker
|
||||
@@ -66,34 +54,15 @@ class PushRegistrationWorker @AssistedInject constructor(
|
||||
private val serviceRepository: DavServiceRepository
|
||||
) : CoroutineWorker(context, workerParameters) {
|
||||
|
||||
companion object {
|
||||
|
||||
private const val UNIQUE_WORK_NAME = "push-registration"
|
||||
|
||||
/**
|
||||
* Enqueues a push registration worker with a minimum delay of 5 seconds.
|
||||
*/
|
||||
fun enqueue(context: Context) {
|
||||
val constraints = Constraints.Builder()
|
||||
.setRequiredNetworkType(NetworkType.CONNECTED) // require a network connection
|
||||
.build()
|
||||
val workRequest = OneTimeWorkRequestBuilder<PushRegistrationWorker>()
|
||||
.setInitialDelay(5, TimeUnit.SECONDS)
|
||||
.setConstraints(constraints)
|
||||
.build()
|
||||
Logger.getGlobal().info("Enqueueing push registration worker")
|
||||
WorkManager.getInstance(context)
|
||||
.enqueueUniqueWork(UNIQUE_WORK_NAME, ExistingWorkPolicy.REPLACE, workRequest)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
override suspend fun doWork(): Result {
|
||||
logger.info("Running push registration worker")
|
||||
|
||||
registerSyncable()
|
||||
unregisterNotSyncable()
|
||||
try {
|
||||
registerSyncable()
|
||||
unregisterNotSyncable()
|
||||
} catch (_: IOException) {
|
||||
return Result.retry() // retry on I/O errors
|
||||
}
|
||||
|
||||
return Result.success()
|
||||
}
|
||||
@@ -108,17 +77,25 @@ class PushRegistrationWorker @AssistedInject constructor(
|
||||
.use { client ->
|
||||
val httpClient = client.okHttpClient
|
||||
|
||||
// requested expiration time: 3 days
|
||||
val requestedExpiration = Instant.now() + Duration.ofDays(3)
|
||||
|
||||
val serializer = XmlUtils.newSerializer()
|
||||
val writer = StringWriter()
|
||||
serializer.setOutput(writer)
|
||||
serializer.startDocument("UTF-8", true)
|
||||
serializer.insertTag(Property.Name(NS_WEBDAV_PUSH, "push-register")) {
|
||||
serializer.insertTag(Property.Name(NS_WEBDAV_PUSH, "subscription")) {
|
||||
// subscription URL
|
||||
serializer.insertTag(Property.Name(NS_WEBDAV_PUSH, "web-push-subscription")) {
|
||||
serializer.insertTag(Property.Name(NS_WEBDAV_PUSH, "push-resource")) {
|
||||
text(endpoint)
|
||||
}
|
||||
}
|
||||
// requested expiration
|
||||
serializer.insertTag(Property.Name(NS_WEBDAV_PUSH, "expires")) {
|
||||
text(HttpUtils.formatDate(requestedExpiration))
|
||||
}
|
||||
}
|
||||
}
|
||||
serializer.endDocument()
|
||||
@@ -126,9 +103,15 @@ class PushRegistrationWorker @AssistedInject constructor(
|
||||
val xml = writer.toString().toRequestBody(DavResource.MIME_XML)
|
||||
DavCollection(httpClient, collection.url).post(xml) { response ->
|
||||
if (response.isSuccessful) {
|
||||
response.header("Location")?.let { subscriptionUrl ->
|
||||
collectionRepository.updatePushSubscription(collection.id, subscriptionUrl)
|
||||
}
|
||||
val subscriptionUrl = response.header("Location")
|
||||
val expires = response.header("Expires")?.let { expiresDate ->
|
||||
HttpUtils.parseDate(expiresDate)
|
||||
} ?: requestedExpiration
|
||||
collectionRepository.updatePushSubscription(
|
||||
id = collection.id,
|
||||
subscriptionUrl = subscriptionUrl,
|
||||
expires = expires?.epochSecond
|
||||
)
|
||||
} else
|
||||
logger.warning("Couldn't register push for ${collection.url}: $response")
|
||||
}
|
||||
@@ -142,6 +125,15 @@ class PushRegistrationWorker @AssistedInject constructor(
|
||||
// register push subscription for syncable collections
|
||||
if (endpoint != null)
|
||||
for (collection in collectionRepository.getPushCapableAndSyncable()) {
|
||||
val expires = collection.pushSubscriptionExpires
|
||||
// calculate next run time, but use the duplicate interval for safety (times are not exact)
|
||||
val nextRun = Instant.now() + Duration.ofDays(2*PushRegistrationWorkerManager.INTERVAL_DAYS)
|
||||
if (expires != null && expires >= nextRun.epochSecond) {
|
||||
logger.fine("Push subscription for ${collection.url} is still valid until ${collection.pushSubscriptionExpires}")
|
||||
continue
|
||||
}
|
||||
|
||||
// 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))
|
||||
@@ -176,7 +168,11 @@ class PushRegistrationWorker @AssistedInject constructor(
|
||||
}
|
||||
|
||||
// remove registration URL from DB in any case
|
||||
collectionRepository.updatePushSubscription(collection.id, null)
|
||||
collectionRepository.updatePushSubscription(
|
||||
id = collection.id,
|
||||
subscriptionUrl = null,
|
||||
expires = null
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -193,25 +189,4 @@ class PushRegistrationWorker @AssistedInject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Listener that enqueues a push registration worker when the collection list changes.
|
||||
*/
|
||||
class CollectionsListener @Inject constructor(
|
||||
@ApplicationContext val context: Context
|
||||
): DavCollectionRepository.OnChangeListener {
|
||||
override fun onCollectionsChanged() = enqueue(context)
|
||||
}
|
||||
|
||||
/**
|
||||
* Hilt module that registers [CollectionsListener] in [DavCollectionRepository].
|
||||
*/
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
interface PushRegistrationWorkerModule {
|
||||
@Binds
|
||||
@IntoSet
|
||||
fun listener(impl: CollectionsListener): DavCollectionRepository.OnChangeListener
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
|
||||
*/
|
||||
|
||||
package at.bitfire.davdroid.push
|
||||
|
||||
import android.content.Context
|
||||
import androidx.work.BackoffPolicy
|
||||
import androidx.work.Constraints
|
||||
import androidx.work.ExistingPeriodicWorkPolicy
|
||||
import androidx.work.NetworkType
|
||||
import androidx.work.PeriodicWorkRequest
|
||||
import androidx.work.WorkManager
|
||||
import at.bitfire.davdroid.repository.DavCollectionRepository
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import dagger.multibindings.IntoSet
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.logging.Logger
|
||||
import javax.inject.Inject
|
||||
|
||||
class PushRegistrationWorkerManager @Inject constructor(
|
||||
@ApplicationContext val context: Context,
|
||||
val collectionRepository: DavCollectionRepository,
|
||||
val logger: Logger
|
||||
) {
|
||||
|
||||
/**
|
||||
* Determines whether there are any push-capable collections and updates the periodic worker accordingly.
|
||||
*
|
||||
* If there are push-capable collections, a unique periodic worker with an initial delay of 5 seconds is enqueued.
|
||||
* A potentially existing worker is replaced, so that the first run should be soon.
|
||||
*
|
||||
* Otherwise, a potentially existing worker is cancelled.
|
||||
*/
|
||||
fun updatePeriodicWorker() {
|
||||
val workerNeeded = runBlocking {
|
||||
collectionRepository.anyPushCapable()
|
||||
}
|
||||
|
||||
val workManager = WorkManager.getInstance(context)
|
||||
if (workerNeeded) {
|
||||
logger.info("Enqueuing periodic PushRegistrationWorker")
|
||||
workManager.enqueueUniquePeriodicWork(
|
||||
UNIQUE_WORK_NAME,
|
||||
ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE,
|
||||
PeriodicWorkRequest.Builder(PushRegistrationWorker::class, INTERVAL_DAYS, TimeUnit.DAYS)
|
||||
.setInitialDelay(5, TimeUnit.SECONDS)
|
||||
.setConstraints(
|
||||
Constraints.Builder()
|
||||
.setRequiredNetworkType(NetworkType.CONNECTED)
|
||||
.build()
|
||||
)
|
||||
.setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 1, TimeUnit.MINUTES)
|
||||
.build()
|
||||
)
|
||||
} else {
|
||||
logger.info("Cancelling periodic PushRegistrationWorker")
|
||||
workManager.cancelUniqueWork(UNIQUE_WORK_NAME)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
companion object {
|
||||
private const val UNIQUE_WORK_NAME = "push-registration"
|
||||
const val INTERVAL_DAYS = 1L
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Listener that enqueues a push registration worker when the collection list changes.
|
||||
*/
|
||||
class CollectionsListener @Inject constructor(
|
||||
@ApplicationContext val context: Context,
|
||||
val workerManager: PushRegistrationWorkerManager
|
||||
): DavCollectionRepository.OnChangeListener {
|
||||
|
||||
override fun onCollectionsChanged() {
|
||||
workerManager.updatePeriodicWorker()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Hilt module that registers [CollectionsListener] in [DavCollectionRepository].
|
||||
*/
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
interface PushRegistrationWorkerModule {
|
||||
@Binds
|
||||
@IntoSet
|
||||
fun listener(impl: CollectionsListener): DavCollectionRepository.OnChangeListener
|
||||
}
|
||||
}
|
||||
@@ -11,13 +11,13 @@ import at.bitfire.davdroid.repository.DavServiceRepository
|
||||
import at.bitfire.davdroid.repository.PreferenceRepository
|
||||
import at.bitfire.davdroid.sync.worker.SyncWorkerManager
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import java.util.logging.Level
|
||||
import java.util.logging.Logger
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import org.unifiedpush.android.connector.MessagingReceiver
|
||||
import java.util.logging.Level
|
||||
import java.util.logging.Logger
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class UnifiedPushReceiver: MessagingReceiver() {
|
||||
@@ -40,6 +40,9 @@ class UnifiedPushReceiver: MessagingReceiver() {
|
||||
@Inject
|
||||
lateinit var parsePushMessage: PushMessageParser
|
||||
|
||||
@Inject
|
||||
lateinit var pushRegistrationWorkerManager: PushRegistrationWorkerManager
|
||||
|
||||
@Inject
|
||||
lateinit var syncWorkerManager: SyncWorkerManager
|
||||
|
||||
@@ -49,7 +52,7 @@ class UnifiedPushReceiver: MessagingReceiver() {
|
||||
preferenceRepository.unifiedPushEndpoint(endpoint)
|
||||
|
||||
// register new endpoint at CalDAV/CardDAV servers
|
||||
PushRegistrationWorker.enqueue(context)
|
||||
pushRegistrationWorkerManager.updatePeriodicWorker()
|
||||
}
|
||||
|
||||
override fun onUnregistered(context: Context, instance: String) {
|
||||
|
||||
@@ -29,15 +29,12 @@ import at.bitfire.davdroid.servicedetection.RefreshCollectionsWorker
|
||||
import at.bitfire.davdroid.settings.AccountSettings
|
||||
import at.bitfire.davdroid.util.DavUtils
|
||||
import at.bitfire.ical4android.util.DateUtils
|
||||
import dagger.Lazy
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import dagger.multibindings.Multibinds
|
||||
import java.io.StringWriter
|
||||
import java.util.Collections
|
||||
import java.util.UUID
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.runInterruptible
|
||||
import kotlinx.coroutines.withContext
|
||||
@@ -46,6 +43,10 @@ import net.fortuna.ical4j.model.Component
|
||||
import net.fortuna.ical4j.model.ComponentList
|
||||
import net.fortuna.ical4j.model.component.VTimeZone
|
||||
import okhttp3.HttpUrl
|
||||
import java.io.StringWriter
|
||||
import java.util.Collections
|
||||
import java.util.UUID
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Repository for managing collections.
|
||||
@@ -56,14 +57,20 @@ class DavCollectionRepository @Inject constructor(
|
||||
private val accountSettingsFactory: AccountSettings.Factory,
|
||||
@ApplicationContext val context: Context,
|
||||
db: AppDatabase,
|
||||
defaultListeners: Set<@JvmSuppressWildcards OnChangeListener>,
|
||||
defaultListeners: Lazy<Set<@JvmSuppressWildcards OnChangeListener>>,
|
||||
private val serviceRepository: DavServiceRepository
|
||||
) {
|
||||
|
||||
private val listeners = Collections.synchronizedSet(defaultListeners.toMutableSet())
|
||||
private val listeners by lazy { Collections.synchronizedSet(defaultListeners.get().toMutableSet()) }
|
||||
|
||||
private val dao = db.collectionDao()
|
||||
|
||||
/**
|
||||
* Whether there are any collections that are registered for push.
|
||||
*/
|
||||
suspend fun anyPushCapable() =
|
||||
dao.anyPushCapable()
|
||||
|
||||
/**
|
||||
* Creates address book collection on server and locally
|
||||
*/
|
||||
@@ -256,8 +263,12 @@ class DavCollectionRepository @Inject constructor(
|
||||
notifyOnChangeListeners()
|
||||
}
|
||||
|
||||
fun updatePushSubscription(id: Long, subscriptionUrl: String?) {
|
||||
dao.updatePushSubscription(id, subscriptionUrl)
|
||||
fun updatePushSubscription(id: Long, subscriptionUrl: String?, expires: Long?) {
|
||||
dao.updatePushSubscription(
|
||||
id = id,
|
||||
pushSubscription = subscriptionUrl,
|
||||
pushSubscriptionExpires = expires
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -97,6 +97,7 @@ fun CollectionScreen(
|
||||
lastSynced = model.lastSynced.collectAsStateWithLifecycle(emptyList()).value,
|
||||
supportsWebPush = collection.supportsWebPush,
|
||||
pushSubscriptionCreated = collection.pushSubscriptionCreated,
|
||||
pushSubscriptionExpires = collection.pushSubscriptionExpires,
|
||||
url = collection.url.toString(),
|
||||
onDelete = model::delete,
|
||||
onNavUp = onNavUp
|
||||
@@ -121,6 +122,7 @@ fun CollectionScreen(
|
||||
lastSynced: List<DavSyncStatsRepository.LastSynced> = emptyList(),
|
||||
supportsWebPush: Boolean = false,
|
||||
pushSubscriptionCreated: Long? = null,
|
||||
pushSubscriptionExpires: Long? = null,
|
||||
url: String,
|
||||
onDelete: () -> Unit = {},
|
||||
onNavUp: () -> Unit = {}
|
||||
@@ -249,10 +251,13 @@ fun CollectionScreen(
|
||||
|
||||
if (supportsWebPush) {
|
||||
val text =
|
||||
if (pushSubscriptionCreated != null) {
|
||||
if (pushSubscriptionCreated != null && pushSubscriptionExpires != null) {
|
||||
val formatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.SHORT).withZone(ZoneId.systemDefault())
|
||||
val time = Instant.ofEpochMilli(pushSubscriptionCreated)
|
||||
stringResource(R.string.collection_push_subscribed_at, formatter.format(time))
|
||||
stringResource(
|
||||
R.string.collection_push_subscribed_at,
|
||||
formatter.format(Instant.ofEpochSecond(pushSubscriptionCreated)),
|
||||
formatter.format(Instant.ofEpochSecond(pushSubscriptionExpires))
|
||||
)
|
||||
} else
|
||||
stringResource(R.string.collection_push_web_push)
|
||||
CollectionScreen_Entry(
|
||||
@@ -360,7 +365,9 @@ fun CollectionScreen_Preview() {
|
||||
lastSynced = 1234567890
|
||||
)
|
||||
),
|
||||
supportsWebPush = true
|
||||
supportsWebPush = true,
|
||||
pushSubscriptionCreated = 1731846565,
|
||||
pushSubscriptionExpires = 1731847565
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -416,7 +416,7 @@
|
||||
<string name="collection_owner">Owner</string>
|
||||
<string name="collection_push_support">Push support</string>
|
||||
<string name="collection_push_web_push">Server advertises Push support</string>
|
||||
<string name="collection_push_subscribed_at">Subscribed (%s)</string>
|
||||
<string name="collection_push_subscribed_at">Subscribed at %1$s, expires at %2$s</string>
|
||||
<string name="collection_last_sync">Last sync (%s)</string>
|
||||
<string name="collection_url">Address (URL)</string>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user