Compare commits

..

1 Commits

Author SHA1 Message Date
Arnau Mora
46e8c4522b Updated name, package and color
Signed-off-by: Arnau Mora Gras <arnyminerz@proton.me>
2024-10-25 17:47:23 +02:00
114 changed files with 1482 additions and 3794 deletions

View File

@@ -33,7 +33,7 @@ jobs:
with:
distribution: temurin
java-version: 21
- uses: gradle/actions/setup-gradle@v4
- uses: gradle/actions/setup-gradle@v3
with:
cache-encryption-key: ${{ secrets.gradle_encryption_key }}
cache-read-only: true # gradle user home cache is generated by test jobs

View File

@@ -24,7 +24,7 @@ jobs:
with:
distribution: temurin
java-version: 21
- uses: gradle/actions/setup-gradle@v4
- uses: gradle/actions/setup-gradle@v3
- name: Prepare keystore
run: echo ${{ secrets.android_keystore_base64 }} | base64 -d >$GITHUB_WORKSPACE/keystore.jks

View File

@@ -21,7 +21,7 @@ jobs:
java-version: 21
# See https://community.gradle.org/github-actions/docs/setup-gradle/ for more information
- uses: gradle/actions/setup-gradle@v4 # creates build cache when on main branch
- uses: gradle/actions/setup-gradle@v3 # creates build cache when on main branch
with:
cache-encryption-key: ${{ secrets.gradle_encryption_key }}
dependency-graph: generate-and-submit # submit Github Dependency Graph info
@@ -39,7 +39,7 @@ jobs:
with:
distribution: temurin
java-version: 21
- uses: gradle/actions/setup-gradle@v4
- uses: gradle/actions/setup-gradle@v3
with:
cache-encryption-key: ${{ secrets.gradle_encryption_key }}
cache-read-only: true
@@ -60,7 +60,7 @@ jobs:
with:
distribution: temurin
java-version: 21
- uses: gradle/actions/setup-gradle@v4
- uses: gradle/actions/setup-gradle@v3
with:
cache-encryption-key: ${{ secrets.gradle_encryption_key }}
cache-read-only: true

View File

@@ -18,8 +18,8 @@ android {
defaultConfig {
applicationId = "at.bitfire.davdroid"
versionCode = 404040002
versionName = "4.4.4"
versionCode = 404030200
versionName = "4.4.3.2"
setProperty("archivesBaseName", "davx5-ose-$versionName")
@@ -82,6 +82,9 @@ android {
signingConfig = signingConfigs.findByName("bitfire")
}
getByName("debug") {
applicationIdSuffix = ".debug"
}
}
lint {

View File

@@ -1,675 +0,0 @@
{
"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')"
]
}
}

View File

@@ -1,675 +0,0 @@
{
"formatVersion": 1,
"database": {
"version": 16,
"identityHash": "2ff7560d957e03a78b4b7de88aa9593b",
"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, `timezoneId` TEXT, `supportsVEVENT` INTEGER, `supportsVTODO` INTEGER, `supportsVJOURNAL` INTEGER, `source` TEXT, `sync` INTEGER NOT NULL, `pushTopic` TEXT, `supportsWebPush` INTEGER NOT NULL DEFAULT 0, `pushSubscription` TEXT, `pushSubscriptionExpires` INTEGER, `pushSubscriptionCreated` INTEGER, FOREIGN KEY(`serviceId`) REFERENCES `service`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`homeSetId`) REFERENCES `homeset`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL , FOREIGN KEY(`ownerId`) REFERENCES `principal`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL )",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "serviceId",
"columnName": "serviceId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "homeSetId",
"columnName": "homeSetId",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "ownerId",
"columnName": "ownerId",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "url",
"columnName": "url",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "privWriteContent",
"columnName": "privWriteContent",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "privUnbind",
"columnName": "privUnbind",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "forceReadOnly",
"columnName": "forceReadOnly",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "displayName",
"columnName": "displayName",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "description",
"columnName": "description",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "color",
"columnName": "color",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "timezoneId",
"columnName": "timezoneId",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "supportsVEVENT",
"columnName": "supportsVEVENT",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "supportsVTODO",
"columnName": "supportsVTODO",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "supportsVJOURNAL",
"columnName": "supportsVJOURNAL",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "source",
"columnName": "source",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "sync",
"columnName": "sync",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "pushTopic",
"columnName": "pushTopic",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "supportsWebPush",
"columnName": "supportsWebPush",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "pushSubscription",
"columnName": "pushSubscription",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "pushSubscriptionExpires",
"columnName": "pushSubscriptionExpires",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "pushSubscriptionCreated",
"columnName": "pushSubscriptionCreated",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_collection_serviceId_type",
"unique": false,
"columnNames": [
"serviceId",
"type"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_collection_serviceId_type` ON `${TABLE_NAME}` (`serviceId`, `type`)"
},
{
"name": "index_collection_homeSetId_type",
"unique": false,
"columnNames": [
"homeSetId",
"type"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_collection_homeSetId_type` ON `${TABLE_NAME}` (`homeSetId`, `type`)"
},
{
"name": "index_collection_ownerId_type",
"unique": false,
"columnNames": [
"ownerId",
"type"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_collection_ownerId_type` ON `${TABLE_NAME}` (`ownerId`, `type`)"
},
{
"name": "index_collection_pushTopic_type",
"unique": false,
"columnNames": [
"pushTopic",
"type"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_collection_pushTopic_type` ON `${TABLE_NAME}` (`pushTopic`, `type`)"
},
{
"name": "index_collection_url",
"unique": false,
"columnNames": [
"url"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_collection_url` ON `${TABLE_NAME}` (`url`)"
}
],
"foreignKeys": [
{
"table": "service",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"serviceId"
],
"referencedColumns": [
"id"
]
},
{
"table": "homeset",
"onDelete": "SET NULL",
"onUpdate": "NO ACTION",
"columns": [
"homeSetId"
],
"referencedColumns": [
"id"
]
},
{
"table": "principal",
"onDelete": "SET NULL",
"onUpdate": "NO ACTION",
"columns": [
"ownerId"
],
"referencedColumns": [
"id"
]
}
]
},
{
"tableName": "principal",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `serviceId` INTEGER NOT NULL, `url` TEXT NOT NULL, `displayName` TEXT, FOREIGN KEY(`serviceId`) REFERENCES `service`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "serviceId",
"columnName": "serviceId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "url",
"columnName": "url",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "displayName",
"columnName": "displayName",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_principal_serviceId_url",
"unique": true,
"columnNames": [
"serviceId",
"url"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_principal_serviceId_url` ON `${TABLE_NAME}` (`serviceId`, `url`)"
}
],
"foreignKeys": [
{
"table": "service",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"serviceId"
],
"referencedColumns": [
"id"
]
}
]
},
{
"tableName": "syncstats",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `collectionId` INTEGER NOT NULL, `authority` TEXT NOT NULL, `lastSync` INTEGER NOT NULL, FOREIGN KEY(`collectionId`) REFERENCES `collection`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "collectionId",
"columnName": "collectionId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "authority",
"columnName": "authority",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "lastSync",
"columnName": "lastSync",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_syncstats_collectionId_authority",
"unique": true,
"columnNames": [
"collectionId",
"authority"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_syncstats_collectionId_authority` ON `${TABLE_NAME}` (`collectionId`, `authority`)"
}
],
"foreignKeys": [
{
"table": "collection",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"collectionId"
],
"referencedColumns": [
"id"
]
}
]
},
{
"tableName": "webdav_document",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `mountId` INTEGER NOT NULL, `parentId` INTEGER, `name` TEXT NOT NULL, `isDirectory` INTEGER NOT NULL, `displayName` TEXT, `mimeType` TEXT, `eTag` TEXT, `lastModified` INTEGER, `size` INTEGER, `mayBind` INTEGER, `mayUnbind` INTEGER, `mayWriteContent` INTEGER, `quotaAvailable` INTEGER, `quotaUsed` INTEGER, FOREIGN KEY(`mountId`) REFERENCES `webdav_mount`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`parentId`) REFERENCES `webdav_document`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "mountId",
"columnName": "mountId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "parentId",
"columnName": "parentId",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "isDirectory",
"columnName": "isDirectory",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "displayName",
"columnName": "displayName",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "mimeType",
"columnName": "mimeType",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "eTag",
"columnName": "eTag",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "lastModified",
"columnName": "lastModified",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "size",
"columnName": "size",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "mayBind",
"columnName": "mayBind",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "mayUnbind",
"columnName": "mayUnbind",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "mayWriteContent",
"columnName": "mayWriteContent",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "quotaAvailable",
"columnName": "quotaAvailable",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "quotaUsed",
"columnName": "quotaUsed",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_webdav_document_mountId_parentId_name",
"unique": true,
"columnNames": [
"mountId",
"parentId",
"name"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_webdav_document_mountId_parentId_name` ON `${TABLE_NAME}` (`mountId`, `parentId`, `name`)"
},
{
"name": "index_webdav_document_parentId",
"unique": false,
"columnNames": [
"parentId"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_webdav_document_parentId` ON `${TABLE_NAME}` (`parentId`)"
}
],
"foreignKeys": [
{
"table": "webdav_mount",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"mountId"
],
"referencedColumns": [
"id"
]
},
{
"table": "webdav_document",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"parentId"
],
"referencedColumns": [
"id"
]
}
]
},
{
"tableName": "webdav_mount",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "url",
"columnName": "url",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '2ff7560d957e03a78b4b7de88aa9593b')"
]
}
}

View File

@@ -1,6 +1,6 @@
package at.bitfire.davdroid
import at.bitfire.davdroid.push.PushRegistrationWorkerManager
import at.bitfire.davdroid.push.PushRegistrationWorker
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 = [PushRegistrationWorkerManager.PushRegistrationWorkerModule::class]
replaces = [PushRegistrationWorker.PushRegistrationWorkerModule::class]
)
abstract class TestPushRegistrationWorkerModule {
// provides empty set of listeners

View File

@@ -1,165 +0,0 @@
/*
* 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.testing.MigrationTestHelper
import androidx.sqlite.db.SupportSQLiteDatabase
import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory
import androidx.test.filters.SdkSuppress
import androidx.test.platform.app.InstrumentationRegistry
import at.bitfire.davdroid.db.Collection.Companion.TYPE_CALENDAR
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import javax.inject.Inject
@HiltAndroidTest
class AppDatabaseMigrationsTest {
@get:Rule
val hiltRule = HiltAndroidRule(this)
@Inject
@ApplicationContext
lateinit var context: Context
@Before
fun setup() {
hiltRule.inject()
}
/**
* Used for testing the migration process between two versions.
*
* @param fromVersion The version from which to start testing
* @param toVersion The target version to test
* @param prepare Callback to prepare the database. Will be run with database schema in version [fromVersion].
* @param validate Callback to validate the migration result. Will be run with database schema in version [toVersion].
*/
private fun testMigration(
fromVersion: Int,
toVersion: Int,
prepare: (SupportSQLiteDatabase) -> Unit,
validate: (SupportSQLiteDatabase) -> Unit
) {
val helper = MigrationTestHelper(
InstrumentationRegistry.getInstrumentation(),
AppDatabase::class.java,
AppDatabase.getAutoMigrationSpecs(context),
FrameworkSQLiteOpenHelperFactory()
)
// Prepare the database with the initial version.
helper.createDatabase(TEST_DB, version = fromVersion).apply {
prepare(this)
close()
}
// Re-open the database with the new version and provide all the migrations.
val db = helper.runMigrationsAndValidate(
name = TEST_DB,
version = toVersion,
validateDroppedTables = true,
migrations = AppDatabase.manualMigrations
)
validate(db)
}
@Test
@SdkSuppress(minSdkVersion = 34)
fun migrate15To16_WithTimeZone() {
testMigration(
fromVersion = 15,
toVersion = 16,
prepare = { db ->
val minimalVTimezone = """
BEGIN:VCALENDAR
VERSION:2.0
PRODID:DAVx5
BEGIN:VTIMEZONE
TZID:America/New_York
END:VTIMEZONE
END:VCALENDAR
""".trimIndent()
db.execSQL(
"INSERT INTO service (id, accountName, type) VALUES (?, ?, ?)",
arrayOf(1, "test", Service.TYPE_CALDAV)
)
db.execSQL(
"INSERT INTO collection (id, serviceId, type, url, privWriteContent, privUnbind, forceReadOnly, sync, timezone) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
arrayOf(1, 1, TYPE_CALENDAR, "https://example.com", true, true, false, false, minimalVTimezone)
)
}
) { db ->
db.query("SELECT timezoneId FROM collection WHERE id=1").use { cursor ->
cursor.moveToFirst()
assertEquals("America/New_York", cursor.getString(0))
}
}
}
@Test
@SdkSuppress(minSdkVersion = 34)
fun migrate15To16_WithTimeZone_Unparseable() {
testMigration(
fromVersion = 15,
toVersion = 16,
prepare = { db ->
db.execSQL(
"INSERT INTO service (id, accountName, type) VALUES (?, ?, ?)",
arrayOf(1, "test", Service.TYPE_CALDAV)
)
db.execSQL(
"INSERT INTO collection (id, serviceId, type, url, privWriteContent, privUnbind, forceReadOnly, sync, timezone) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
arrayOf(1, 1, TYPE_CALENDAR, "https://example.com", true, true, false, false, "Some Garbage Content")
)
}
) { db ->
db.query("SELECT timezoneId FROM collection WHERE id=1").use { cursor ->
cursor.moveToFirst()
assertNull(cursor.getString(0))
}
}
}
@Test
@SdkSuppress(minSdkVersion = 34)
fun migrate15To16_WithoutTimezone() {
testMigration(
fromVersion = 15,
toVersion = 16,
prepare = { db ->
db.execSQL(
"INSERT INTO service (id, accountName, type) VALUES (?, ?, ?)",
arrayOf(1, "test", Service.TYPE_CALDAV)
)
db.execSQL(
"INSERT INTO collection (id, serviceId, type, url, privWriteContent, privUnbind, forceReadOnly, sync) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
arrayOf(1, 1, TYPE_CALENDAR, "https://example.com", true, true, false, false)
)
}
) { db ->
db.query("SELECT timezoneId FROM collection WHERE id=1").use { cursor ->
cursor.moveToFirst()
assertNull(cursor.getString(0))
}
}
}
companion object {
const val TEST_DB = "test"
}
}

View File

@@ -9,64 +9,42 @@ import androidx.room.Room
import androidx.room.testing.MigrationTestHelper
import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory
import androidx.test.platform.app.InstrumentationRegistry
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import java.util.logging.Logger
import javax.inject.Inject
@HiltAndroidTest
class AppDatabaseTest {
@get:Rule
val hiltRule = HiltAndroidRule(this)
val TEST_DB = "test"
@Inject
@ApplicationContext
lateinit var context: Context
val context: Context = InstrumentationRegistry.getInstrumentation().targetContext
@Inject
lateinit var logger: Logger
@Before
fun setup() {
hiltRule.inject()
}
@Rule
@JvmField
val helper = MigrationTestHelper(
InstrumentationRegistry.getInstrumentation(),
AppDatabase::class.java,
listOf(), // no auto migrations until v8
FrameworkSQLiteOpenHelperFactory()
)
/**
* Creates a database with schema version 8 (the first exported one) and then migrates it to the latest version.
*/
@Test
fun testAllMigrations() {
// Create DB with v8
MigrationTestHelper(
InstrumentationRegistry.getInstrumentation(),
AppDatabase::class.java,
listOf(), // no auto migrations until v8
FrameworkSQLiteOpenHelperFactory()
).createDatabase(TEST_DB, 8).close()
// DB schema is available since version 8, so create DB with v8
helper.createDatabase(TEST_DB, 8).close()
// open and migrate (to current version) database
Room.databaseBuilder(context, AppDatabase::class.java, TEST_DB)
val db = Room.databaseBuilder(context, AppDatabase::class.java, TEST_DB)
// manual migrations
.addMigrations(*AppDatabase.manualMigrations)
.addMigrations(*AppDatabase.migrations)
// auto-migrations that need to be specified explicitly
.apply {
for (spec in AppDatabase.getAutoMigrationSpecs(context))
addAutoMigrationSpec(spec)
}
.addAutoMigrationSpec(AppDatabase.AutoMigration11_12(context))
.build()
.openHelper.writableDatabase // this will run all migrations
.close()
}
companion object {
const val TEST_DB = "test"
try {
// open (with version 8) + migrate (to current version) database
db.openHelper.writableDatabase
} finally {
db.close()
}
}
}

View File

@@ -92,93 +92,35 @@ class CollectionTest {
@Test
@SmallTest
fun testFromDavResponseCalendar_FullTimezone() {
fun testFromDavResponseCalendar() {
// read-only calendar, no display name
server.enqueue(MockResponse()
.setResponseCode(207)
.setBody("<multistatus xmlns='DAV:' xmlns:CAL='urn:ietf:params:xml:ns:caldav' xmlns:ICAL='http://apple.com/ns/ical/'>" +
"<response>" +
" <href>/</href>" +
" <propstat><prop>" +
" <resourcetype><collection/><CAL:calendar/></resourcetype>" +
" <current-user-privilege-set><privilege><read/></privilege></current-user-privilege-set>" +
" <CAL:calendar-description>My Calendar</CAL:calendar-description>" +
" <CAL:calendar-timezone>BEGIN:VCALENDAR\n" +
"PRODID:-//Example Corp.//CalDAV Client//EN\n" +
"VERSION:2.0\n" +
"BEGIN:VTIMEZONE\n" +
"TZID:US-Eastern\n" +
"LAST-MODIFIED:19870101T000000Z\n" +
"BEGIN:STANDARD\n" +
"DTSTART:19671029T020000\n" +
"RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10\n" +
"TZOFFSETFROM:-0400\n" +
"TZOFFSETTO:-0500\n" +
"TZNAME:Eastern Standard Time (US & Canada)\n" +
"END:STANDARD\n" +
"BEGIN:DAYLIGHT\n" +
"DTSTART:19870405T020000\n" +
"RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4\n" +
"TZOFFSETFROM:-0500\n" +
"TZOFFSETTO:-0400\n" +
"TZNAME:Eastern Daylight Time (US & Canada)\n" +
"END:DAYLIGHT\n" +
"END:VTIMEZONE\n" +
"END:VCALENDAR\n" +
"</CAL:calendar-timezone>" +
" <ICAL:calendar-color>#ff0000</ICAL:calendar-color>" +
" </prop></propstat>" +
"</response>" +
"</multistatus>"))
.setResponseCode(207)
.setBody("<multistatus xmlns='DAV:' xmlns:CAL='urn:ietf:params:xml:ns:caldav' xmlns:ICAL='http://apple.com/ns/ical/'>" +
"<response>" +
" <href>/</href>" +
" <propstat><prop>" +
" <resourcetype><collection/><CAL:calendar/></resourcetype>" +
" <current-user-privilege-set><privilege><read/></privilege></current-user-privilege-set>" +
" <CAL:calendar-description>My Calendar</CAL:calendar-description>" +
" <CAL:calendar-timezone>tzdata</CAL:calendar-timezone>" +
" <ICAL:calendar-color>#ff0000</ICAL:calendar-color>" +
" </prop></propstat>" +
"</response>" +
"</multistatus>"))
lateinit var info: Collection
DavResource(httpClient.okHttpClient, server.url("/"))
.propfind(0, ResourceType.NAME) { response, _ ->
info = Collection.fromDavResponse(response)!!
}
.propfind(0, ResourceType.NAME) { response, _ ->
info = Collection.fromDavResponse(response) ?: throw IllegalArgumentException()
}
assertEquals(Collection.TYPE_CALENDAR, info.type)
assertFalse(info.privWriteContent)
assertFalse(info.privUnbind)
assertNull(info.displayName)
assertEquals("My Calendar", info.description)
assertEquals(0xFFFF0000.toInt(), info.color)
assertEquals("US-Eastern", info.timezoneId)
assertTrue(info.supportsVEVENT!!)
assertTrue(info.supportsVTODO!!)
assertTrue(info.supportsVJOURNAL!!)
}
@Test
@SmallTest
fun testFromDavResponseCalendar_OnlyTzId() {
// read-only calendar, no display name
server.enqueue(MockResponse()
.setResponseCode(207)
.setBody("<multistatus xmlns='DAV:' xmlns:CAL='urn:ietf:params:xml:ns:caldav' xmlns:ICAL='http://apple.com/ns/ical/'>" +
"<response>" +
" <href>/</href>" +
" <propstat><prop>" +
" <resourcetype><collection/><CAL:calendar/></resourcetype>" +
" <current-user-privilege-set><privilege><read/></privilege></current-user-privilege-set>" +
" <CAL:calendar-description>My Calendar</CAL:calendar-description>" +
" <CAL:calendar-timezone-id>US-Eastern</CAL:calendar-timezone-id>" +
" <ICAL:calendar-color>#ff0000</ICAL:calendar-color>" +
" </prop></propstat>" +
"</response>" +
"</multistatus>"))
lateinit var info: Collection
DavResource(httpClient.okHttpClient, server.url("/"))
.propfind(0, ResourceType.NAME) { response, _ ->
info = Collection.fromDavResponse(response)!!
}
assertEquals(Collection.TYPE_CALENDAR, info.type)
assertFalse(info.privWriteContent)
assertFalse(info.privUnbind)
assertNull(info.displayName)
assertEquals("My Calendar", info.description)
assertEquals(0xFFFF0000.toInt(), info.color)
assertEquals("US-Eastern", info.timezoneId)
assertEquals("tzdata", info.timezone)
assertTrue(info.supportsVEVENT!!)
assertTrue(info.supportsVTODO!!)
assertTrue(info.supportsVJOURNAL!!)

View File

@@ -26,12 +26,8 @@ class MemoryDbModule {
@Singleton
fun inMemoryDatabase(@ApplicationContext context: Context): AppDatabase =
Room.inMemoryDatabaseBuilder(context, AppDatabase::class.java)
// auto-migration specs that need to be specified explicitly
.apply {
for (spec in AppDatabase.getAutoMigrationSpecs(context)) {
addAutoMigrationSpec(spec)
}
}
// auto-migrations that need to be specified explicitly
.addAutoMigrationSpec(AppDatabase.AutoMigration11_12(context))
.build()
}

View File

@@ -5,7 +5,6 @@ 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
@@ -64,17 +63,7 @@ class DavCollectionRepositoryTest {
)
)
val testObserver = mockk<DavCollectionRepository.OnChangeListener>(relaxed = true)
val collectionRepository = DavCollectionRepository(
accountSettingsFactory,
context,
db,
object : Lazy<Set<DavCollectionRepository.OnChangeListener>> {
override fun get(): Set<DavCollectionRepository.OnChangeListener> {
return mutableSetOf(testObserver)
}
},
serviceRepository
)
val collectionRepository = DavCollectionRepository(accountSettingsFactory, context, db, mutableSetOf(testObserver), serviceRepository)
assert(db.collectionDao().get(collectionId)?.forceReadOnly == false)
verify(exactly = 0) {

View File

@@ -1,162 +0,0 @@
package at.bitfire.davdroid.resource
import android.accounts.Account
import android.content.ContentProviderClient
import android.content.Context
import at.bitfire.davdroid.R
import at.bitfire.davdroid.db.Collection
import at.bitfire.davdroid.db.Service
import at.bitfire.davdroid.sync.account.SystemAccountUtils
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import io.mockk.every
import io.mockk.impl.annotations.InjectMockKs
import io.mockk.impl.annotations.SpyK
import io.mockk.junit4.MockKRule
import io.mockk.just
import io.mockk.mockk
import io.mockk.mockkObject
import io.mockk.runs
import io.mockk.verify
import okhttp3.HttpUrl.Companion.toHttpUrl
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Rule
import org.junit.Test
@HiltAndroidTest
class LocalAddressBookStoreTest {
@get:Rule
val hiltRule = HiltAndroidRule(this)
@get:Rule
val mockkRule = MockKRule(this)
val context: Context = mockk(relaxed = true) {
every { getString(R.string.account_type_address_book) } returns "com.bitfire.davdroid.addressbook"
}
// val account = Account("MrRobert@example.com", "com.bitfire.davdroid.addressbook")
val account: Account = mockk(relaxed = true) {
// every { name } returns "MrRobert@example.com"
// every { type } returns "com.bitfire.davdroid.addressbook"
}
val provider = mockk<ContentProviderClient>(relaxed = true)
val addressBook: LocalAddressBook = mockk(relaxed = true) {
every { updateSyncFrameworkSettings() } just runs
every { addressBookAccount } returns account
every { settings } returns LocalAddressBookStore.contactsProviderSettings
}
@SpyK
@InjectMockKs
var localAddressBookStore = LocalAddressBookStore(
collectionRepository = mockk(relaxed = true),
context = context,
localAddressBookFactory = mockk(relaxed = true) {
every { create(account, provider) } returns addressBook
},
logger = mockk(relaxed = true),
serviceRepository = mockk(relaxed = true) {
every { get(any<Long>()) } returns null
every { get(200) } returns mockk<Service> {
every { accountName } returns "MrRobert@example.com"
}
},
settings = mockk(relaxed = true)
)
@Test
fun test_accountName_missingService() {
val collection = mockk<Collection> {
every { id } returns 42
every { url } returns "https://example.com/addressbook/funnyfriends".toHttpUrl()
every { displayName } returns null
every { serviceId } returns 404
}
assertEquals("funnyfriends #42", localAddressBookStore.accountName(collection))
}
@Test
fun test_accountName_missingDisplayName() {
val collection = mockk<Collection> {
every { id } returns 42
every { url } returns "https://example.com/addressbook/funnyfriends".toHttpUrl()
every { displayName } returns null
every { serviceId } returns 200
}
val accountName = localAddressBookStore.accountName(collection)
assertEquals("funnyfriends (MrRobert@example.com) #42", accountName)
}
@Test
fun test_accountName_missingDisplayNameAndService() {
val collection = mockk<Collection>(relaxed = true) {
every { id } returns 1
every { url } returns "https://example.com/addressbook/funnyfriends".toHttpUrl()
every { displayName } returns null
every { serviceId } returns 404 // missing service
}
assertEquals("funnyfriends #1", localAddressBookStore.accountName(collection))
}
@Test
fun test_create_createAccountReturnsNull() {
val collection = mockk<Collection>(relaxed = true) {
every { id } returns 1
every { url } returns "https://example.com/addressbook/funnyfriends".toHttpUrl()
}
every { localAddressBookStore.createAccount(any(), any(), any()) } returns null
assertEquals(null, localAddressBookStore.create(provider, collection))
}
@Test
fun test_create_createAccountReturnsAccount() {
val collection = mockk<Collection>(relaxed = true) {
every { id } returns 1
every { url } returns "https://example.com/addressbook/funnyfriends".toHttpUrl()
}
every { localAddressBookStore.createAccount(any(), any(), any()) } returns account
every { addressBook.readOnly } returns true
val addrBook = localAddressBookStore.create(provider, collection)!!
verify(exactly = 1) { addressBook.updateSyncFrameworkSettings() }
assertEquals(account, addrBook.addressBookAccount)
assertEquals(LocalAddressBookStore.contactsProviderSettings, addrBook.settings)
assertEquals(true, addrBook.readOnly)
every { addressBook.readOnly } returns false
val addrBook2 = localAddressBookStore.create(provider, collection)!!
assertEquals(false, addrBook2.readOnly)
}
@Test
fun test_createAccount_succeeds() {
mockkObject(SystemAccountUtils)
every { SystemAccountUtils.createAccount(any(), any(), any()) } returns true
val account = Account("MrRobert@example.com", "com.bitfire.davdroid.addressbook")
val createdAccount: Account = localAddressBookStore.createAccount(
"MrRobert@example.com", 42, "https://example.com/addressbook/funnyfriends"
)!!
verify(exactly = 1) { SystemAccountUtils.createAccount(context, account, any()) }
assertEquals(account, createdAccount)
}
/**
* Tests the calculation of read only state is correct
*/
@Test
fun test_shouldBeReadOnly() {
val collectionReadOnly = mockk<Collection> { every { readOnly() } returns true }
assertTrue(LocalAddressBookStore.shouldBeReadOnly(collectionReadOnly, false))
assertTrue(LocalAddressBookStore.shouldBeReadOnly(collectionReadOnly, true))
val collectionNotReadOnly = mockk<Collection> { every { readOnly() } returns false }
assertFalse(LocalAddressBookStore.shouldBeReadOnly(collectionNotReadOnly, false))
assertTrue(LocalAddressBookStore.shouldBeReadOnly(collectionNotReadOnly, true))
}
}

View File

@@ -6,7 +6,6 @@ package at.bitfire.davdroid.resource
import android.Manifest
import android.accounts.Account
import android.accounts.AccountManager
import android.content.ContentProviderClient
import android.content.ContentUris
import android.content.Context
@@ -20,6 +19,8 @@ import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import ezvcard.property.Telephone
import java.util.LinkedList
import javax.inject.Inject
import org.junit.After
import org.junit.AfterClass
import org.junit.Assert.assertEquals
@@ -30,8 +31,6 @@ import org.junit.BeforeClass
import org.junit.ClassRule
import org.junit.Rule
import org.junit.Test
import java.util.LinkedList
import javax.inject.Inject
@HiltAndroidTest
class LocalAddressBookTest {
@@ -60,8 +59,7 @@ class LocalAddressBookTest {
@After
fun tearDown() {
// remove address book
val accountManager = AccountManager.get(context)
accountManager.removeAccountExplicitly(addressBook.addressBookAccount)
addressBook.deleteCollection()
}
@@ -125,6 +123,8 @@ class LocalAddressBookTest {
assertEquals("Test Group", group.displayName)
}
companion object {
@JvmField

View File

@@ -66,7 +66,7 @@ class LocalCalendarTest {
@After
fun tearDown() {
calendar.delete()
calendar.deleteCollection()
}

View File

@@ -12,7 +12,6 @@ import android.os.Build
import android.provider.CalendarContract
import android.provider.CalendarContract.ACCOUNT_TYPE_LOCAL
import android.provider.CalendarContract.Events
import androidx.datastore.dataStore
import androidx.test.platform.app.InstrumentationRegistry
import at.bitfire.davdroid.InitCalendarProviderRule
import at.bitfire.ical4android.AndroidCalendar
@@ -75,7 +74,7 @@ class LocalEventTest {
@After
fun removeCalendar() {
calendar.delete()
calendar.deleteCollection()
}
@@ -283,7 +282,7 @@ class LocalEventTest {
})
}
val localEvent = LocalEvent(calendar, event, "filename.ics", null, null, 0)
localEvent.add()
val uri = localEvent.add()
calendar.findById(localEvent.id!!)

View File

@@ -20,7 +20,6 @@ import dagger.assisted.AssistedInject
import dagger.hilt.android.qualifiers.ApplicationContext
import org.junit.Assert.assertTrue
import java.io.FileNotFoundException
import java.util.Optional
import java.util.logging.Logger
class LocalTestAddressBook @AssistedInject constructor(
@@ -31,16 +30,7 @@ class LocalTestAddressBook @AssistedInject constructor(
@ApplicationContext context: Context,
logger: Logger,
serviceRepository: DavServiceRepository
): LocalAddressBook(
_addressBookAccount = ACCOUNT,
provider = provider,
accountSettingsFactory = accountSettingsFactory,
collectionRepository = collectionRepository,
context = context,
dirtyVerifier = Optional.empty(),
logger = logger,
serviceRepository = serviceRepository
) {
): LocalAddressBook(ACCOUNT, provider, accountSettingsFactory, collectionRepository, context, logger, serviceRepository) {
@AssistedFactory
interface Factory {

View File

@@ -21,6 +21,8 @@ class LocalTestCollection(
override val readOnly: Boolean
get() = throw NotImplementedError()
override fun deleteCollection(): Boolean = true
override fun findDeleted() = entries.filter { it.deleted }
override fun findDirty() = entries.filter { it.dirty }

View File

@@ -7,42 +7,56 @@ package at.bitfire.davdroid.sync
import android.accounts.Account
import android.content.ContentProviderClient
import at.bitfire.davdroid.db.Collection
import at.bitfire.davdroid.resource.LocalDataStore
import at.bitfire.davdroid.sync.account.TestAccountAuthenticator
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import io.mockk.every
import io.mockk.impl.annotations.InjectMockKs
import io.mockk.impl.annotations.RelaxedMockK
import io.mockk.impl.annotations.SpyK
import io.mockk.junit4.MockKRule
import io.mockk.just
import io.mockk.mockk
import io.mockk.runs
import io.mockk.spyk
import io.mockk.verify
import okhttp3.HttpUrl.Companion.toHttpUrl
import org.junit.After
import org.junit.Assert.assertArrayEquals
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import java.util.logging.Logger
import javax.inject.Inject
@HiltAndroidTest
class SyncerTest {
@get:Rule
val mockkRule = MockKRule(this)
val hiltRule = HiltAndroidRule(this)
@RelaxedMockK
lateinit var logger: Logger
@Inject
lateinit var testSyncer: TestSyncer.Factory
val dataStore: LocalTestStore = mockk(relaxed = true)
val provider: ContentProviderClient = mockk(relaxed = true)
lateinit var account: Account
@SpyK
@InjectMockKs
var syncer = TestSyncer(mockk(relaxed = true), emptyArray(), SyncResult(), dataStore)
private lateinit var syncer: TestSyncer
@Before
fun setUp() {
hiltRule.inject()
account = TestAccountAuthenticator.create()
syncer = spyk(testSyncer.create(account, emptyArray(), SyncResult()))
}
@After
fun tearDown() {
TestAccountAuthenticator.remove(account)
}
@Test
fun testSync_prepare_fails() {
val provider = mockk<ContentProviderClient>()
every { syncer.prepare(provider) } returns false
every { syncer.getSyncEnabledCollections() } returns emptyMap()
@@ -54,6 +68,7 @@ class SyncerTest {
@Test
fun testSync_prepare_succeeds() {
val provider = mockk<ContentProviderClient>()
every { syncer.prepare(provider) } returns true
every { syncer.getSyncEnabledCollections() } returns emptyMap()
@@ -68,12 +83,13 @@ class SyncerTest {
fun testUpdateCollections_deletesCollection() {
val localCollection = mockk<LocalTestCollection>()
every { localCollection.collectionUrl } returns "http://delete.the/collection"
every { localCollection.deleteCollection() } returns true
every { localCollection.title } returns "Collection to be deleted locally"
// Should delete the localCollection if dbCollection (remote) does not exist
val localCollections = mutableListOf(localCollection)
val result = syncer.updateCollections(mockk(), localCollections, emptyMap())
verify(exactly = 1) { dataStore.delete(localCollection) }
verify(exactly = 1) { localCollection.deleteCollection() }
// Updated local collection list should be empty
assertTrue(result.isEmpty())
@@ -89,8 +105,8 @@ class SyncerTest {
every { localCollection.title } returns "The Local Collection"
// Should update the localCollection if it exists
val result = syncer.updateCollections(provider, listOf(localCollection), dbCollections)
verify(exactly = 1) { dataStore.update(provider, localCollection, dbCollection) }
val result = syncer.updateCollections(mockk(), listOf(localCollection), dbCollections)
verify(exactly = 1) { syncer.update(localCollection, dbCollection) }
// Updated local collection list should be same as input
assertArrayEquals(arrayOf(localCollection), result.toTypedArray())
@@ -98,18 +114,12 @@ class SyncerTest {
@Test
fun testUpdateCollections_findsNewCollection() {
val dbCollection = mockk<Collection> {
every { url } returns "http://newly.found/collection".toHttpUrl()
}
val localCollections = listOf(mockk<LocalTestCollection> {
every { collectionUrl } returns "http://newly.found/collection"
})
val dbCollections = listOf(dbCollection)
val dbCollectionsMap = mapOf(dbCollection.url to dbCollection)
every { syncer.createLocalCollections(provider, dbCollections) } returns localCollections
val dbCollection = mockk<Collection>()
every { dbCollection.url } returns "http://newly.found/collection".toHttpUrl()
val dbCollections = mapOf(dbCollection.url to dbCollection)
// Should return the new collection, because it was not updated
val result = syncer.updateCollections(provider, emptyList(), dbCollectionsMap)
val result = syncer.updateCollections(mockk(), emptyList(), dbCollections)
// Updated local collection list contain new entry
assertEquals(1, result.size)
@@ -119,18 +129,21 @@ class SyncerTest {
@Test
fun testCreateLocalCollections() {
val provider = mockk<ContentProviderClient>()
val localCollection = mockk<LocalTestCollection>()
val dbCollection = mockk<Collection>()
every { dataStore.create(provider, dbCollection) } returns localCollection
every { syncer.create(provider, dbCollection) } returns localCollection
every { dbCollection.url } returns "http://newly.found/collection".toHttpUrl()
// Should return list of newly created local collections
val result = syncer.createLocalCollections(provider, listOf(dbCollection))
assertEquals(listOf(localCollection), result)
}
@Test
fun testSyncCollectionContents() {
val provider = mockk<ContentProviderClient>()
val dbCollection1 = mockk<Collection>()
val dbCollection2 = mockk<Collection>()
val dbCollections = mapOf(
@@ -142,7 +155,6 @@ class SyncerTest {
val localCollections = listOf(localCollection1, localCollection2)
every { localCollection1.collectionUrl } returns "http://newly.found/collection1"
every { localCollection2.collectionUrl } returns "http://newly.found/collection2"
every { syncer.syncCollection(provider, any(), any()) } just runs
// Should call the collection content sync on both collections
syncer.syncCollectionContents(provider, localCollections, dbCollections)
@@ -153,65 +165,41 @@ class SyncerTest {
// Test helpers
class TestSyncer (
account: Account,
extras: Array<String>,
syncResult: SyncResult,
theDataStore: LocalTestStore
) : Syncer<LocalTestStore, LocalTestCollection>(account, extras, syncResult) {
class TestSyncer @AssistedInject constructor(
@Assisted account: Account,
@Assisted extras: Array<String>,
@Assisted syncResult: SyncResult
) : Syncer<LocalTestCollection>(account, extras, syncResult) {
override val dataStore: LocalTestStore =
theDataStore
@AssistedFactory
interface Factory {
fun create(account: Account, extras: Array<String>, syncResult: SyncResult): TestSyncer
}
override val authority: String
get() = throw NotImplementedError()
get() = ""
override val serviceType: String
get() = throw NotImplementedError()
get() = ""
override fun prepare(provider: ContentProviderClient): Boolean =
throw NotImplementedError()
true
override fun getLocalCollections(provider: ContentProviderClient): List<LocalTestCollection> =
emptyList()
override fun getDbSyncCollections(serviceId: Long): List<Collection> =
throw NotImplementedError()
emptyList()
override fun create(provider: ContentProviderClient, remoteCollection: Collection): LocalTestCollection =
LocalTestCollection(remoteCollection.url.toString())
override fun syncCollection(
provider: ContentProviderClient,
localCollection: LocalTestCollection,
remoteCollection: Collection
) {
throw NotImplementedError()
}
) {}
}
class LocalTestStore : LocalDataStore<LocalTestCollection> {
override fun create(
provider: ContentProviderClient,
fromCollection: Collection
): LocalTestCollection? {
throw NotImplementedError()
}
override fun getAll(
account: Account,
provider: ContentProviderClient
): List<LocalTestCollection> {
throw NotImplementedError()
}
override fun update(
provider: ContentProviderClient,
localCollection: LocalTestCollection,
fromCollection: Collection
) {
throw NotImplementedError()
}
override fun delete(localCollection: LocalTestCollection) {
throw NotImplementedError()
}
override fun update(localCollection: LocalTestCollection, remoteCollection: Collection) {}
}

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="primaryColor">#E07C25</color>
<color name="primaryLightColor">#E5A371</color>
<color name="primaryDarkColor">#7C3E07</color>
</resources>

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name" translatable="false">DAVx⁵ Debug</string>
</resources>

View File

@@ -14,6 +14,11 @@
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/>
<!-- account management permissions not required for own accounts since API level 22 -->
<uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS" android:maxSdkVersion="22"/>
<uses-permission android:name="android.permission.GET_ACCOUNTS" android:maxSdkVersion="22"/>
<uses-permission android:name="android.permission.MANAGE_ACCOUNTS" android:maxSdkVersion="22"/>
<!-- other permissions -->
<!-- android.permission-group.CONTACTS -->
<uses-permission android:name="android.permission.READ_CONTACTS"/>

View File

@@ -15,7 +15,6 @@ import androidx.room.AutoMigration
import androidx.room.Database
import androidx.room.DeleteColumn
import androidx.room.ProvidedAutoMigrationSpec
import androidx.room.RenameColumn
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
@@ -27,7 +26,6 @@ import at.bitfire.davdroid.TextTable
import at.bitfire.davdroid.servicedetection.RefreshCollectionsWorker
import at.bitfire.davdroid.ui.AccountsActivity
import at.bitfire.davdroid.ui.NotificationRegistry
import at.bitfire.ical4android.util.DateUtils
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@@ -46,14 +44,12 @@ import javax.inject.Singleton
SyncStats::class,
WebDavDocument::class,
WebDavMount::class
], exportSchema = true, version = 16, autoMigrations = [
], exportSchema = true, version = 14, 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 = 14, to = 15),
AutoMigration(from = 15, to = 16, spec = AppDatabase.AutoMigration15_16::class)
AutoMigration(from = 13, to = 14)
])
@TypeConverters(Converters::class)
abstract class AppDatabase: RoomDatabase() {
@@ -61,64 +57,41 @@ abstract class AppDatabase: RoomDatabase() {
@Module
@InstallIn(SingletonComponent::class)
object AppDatabaseModule {
@Provides
@Singleton
fun appDatabase(
@ApplicationContext context: Context,
notificationRegistry: NotificationRegistry
): AppDatabase = Room
.databaseBuilder(context, AppDatabase::class.java, "services.db")
.addMigrations(*manualMigrations)
.apply {
for (spec in getAutoMigrationSpecs(context))
addAutoMigrationSpec(spec)
}
.fallbackToDestructiveMigration() // as a last fallback, recreate database instead of crashing
.addCallback(object: Callback() {
override fun onDestructiveMigration(db: SupportSQLiteDatabase) {
notificationRegistry.notifyIfPossible(NotificationRegistry.NOTIFY_DATABASE_CORRUPTED) {
val launcherIntent = Intent(context, AccountsActivity::class.java)
NotificationCompat.Builder(context, notificationRegistry.CHANNEL_GENERAL)
.setSmallIcon(R.drawable.ic_warning_notify)
.setContentTitle(context.getString(R.string.database_destructive_migration_title))
.setContentText(context.getString(R.string.database_destructive_migration_text))
.setCategory(NotificationCompat.CATEGORY_ERROR)
.setContentIntent(PendingIntent.getActivity(context, 0, launcherIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE))
.setAutoCancel(true)
.build()
): 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) {
notificationRegistry.notifyIfPossible(NotificationRegistry.NOTIFY_DATABASE_CORRUPTED) {
val launcherIntent = Intent(context, AccountsActivity::class.java)
NotificationCompat.Builder(context, notificationRegistry.CHANNEL_GENERAL)
.setSmallIcon(R.drawable.ic_warning_notify)
.setContentTitle(context.getString(R.string.database_destructive_migration_title))
.setContentText(context.getString(R.string.database_destructive_migration_text))
.setCategory(NotificationCompat.CATEGORY_ERROR)
.setContentIntent(PendingIntent.getActivity(context, 0, launcherIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE))
.setAutoCancel(true)
.build()
}
// remove all accounts because they're unfortunately useless without database
val am = AccountManager.get(context)
for (account in am.getAccountsByType(context.getString(R.string.account_type)))
am.removeAccountExplicitly(account)
}
// remove all accounts because they're unfortunately useless without database
val am = AccountManager.get(context)
for (account in am.getAccountsByType(context.getString(R.string.account_type)))
am.removeAccountExplicitly(account)
}
})
.build()
})
.build()
}
// auto migrations
@ProvidedAutoMigrationSpec
@RenameColumn(tableName = "collection", fromColumnName = "timezone", toColumnName = "timezoneId")
class AutoMigration15_16: AutoMigrationSpec {
override fun onPostMigrate(db: SupportSQLiteDatabase) {
// The timezone column has been renamed to timezoneId, but still contains the VTIMEZONE.
// So we need to parse the VTIMEZONE, extract the timezone ID and save it back.
db.query("SELECT id, timezoneId FROM collection").use { cursor ->
while (cursor.moveToNext()) {
val id: Long = cursor.getLong(0)
val timezoneDef: String = cursor.getString(1) ?: continue
val vTimeZone = DateUtils.parseVTimeZone(timezoneDef)
val timezoneId = vTimeZone?.timeZoneId?.value
db.execSQL("UPDATE collection SET timezoneId=? WHERE id=?", arrayOf(timezoneId, id))
}
}
}
}
@ProvidedAutoMigrationSpec
@DeleteColumn(tableName = "collection", columnName = "owner")
class AutoMigration11_12(val context: Context): AutoMigrationSpec {
@@ -136,119 +109,150 @@ abstract class AppDatabase: RoomDatabase() {
companion object {
// automatic migrations
fun getAutoMigrationSpecs(context: Context) = listOf(
AutoMigration11_12(context),
AutoMigration15_16()
)
// manual migrations
val manualMigrations: Array<Migration> = arrayOf(
Migration(8, 9) { db ->
db.execSQL("CREATE TABLE syncstats (" +
"id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT," +
"collectionId INTEGER NOT NULL REFERENCES collection(id) ON DELETE CASCADE," +
"authority TEXT NOT NULL," +
"lastSync INTEGER NOT NULL)")
db.execSQL("CREATE UNIQUE INDEX index_syncstats_collectionId_authority ON syncstats(collectionId, authority)")
db.execSQL("CREATE INDEX index_collection_url ON collection(url)")
},
Migration(7, 8) { db ->
db.execSQL("ALTER TABLE homeset ADD COLUMN personal INTEGER NOT NULL DEFAULT 1")
db.execSQL("ALTER TABLE collection ADD COLUMN homeSetId INTEGER DEFAULT NULL REFERENCES homeset(id) ON DELETE SET NULL")
db.execSQL("ALTER TABLE collection ADD COLUMN owner TEXT DEFAULT NULL")
db.execSQL("CREATE INDEX index_collection_homeSetId_type ON collection(homeSetId, type)")
},
Migration(6, 7) { db ->
db.execSQL("ALTER TABLE homeset ADD COLUMN privBind INTEGER NOT NULL DEFAULT 1")
db.execSQL("ALTER TABLE homeset ADD COLUMN displayName TEXT DEFAULT NULL")
},
Migration(5, 6) { db ->
val sql = arrayOf(
// migrate "services" to "service": rename columns, make id NOT NULL
"CREATE TABLE service(" +
val migrations: Array<Migration> = arrayOf(
object : Migration(8, 9) {
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL("CREATE TABLE syncstats (" +
"id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT," +
"accountName TEXT NOT NULL," +
"type TEXT NOT NULL," +
"principal TEXT DEFAULT NULL" +
"collectionId INTEGER NOT NULL REFERENCES collection(id) ON DELETE CASCADE," +
"authority TEXT NOT NULL," +
"lastSync INTEGER NOT NULL)")
db.execSQL("CREATE UNIQUE INDEX index_syncstats_collectionId_authority ON syncstats(collectionId, authority)")
db.execSQL("CREATE INDEX index_collection_url ON collection(url)")
}
},
object : Migration(7, 8) {
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL("ALTER TABLE homeset ADD COLUMN personal INTEGER NOT NULL DEFAULT 1")
db.execSQL("ALTER TABLE collection ADD COLUMN homeSetId INTEGER DEFAULT NULL REFERENCES homeset(id) ON DELETE SET NULL")
db.execSQL("ALTER TABLE collection ADD COLUMN owner TEXT DEFAULT NULL")
db.execSQL("CREATE INDEX index_collection_homeSetId_type ON collection(homeSetId, type)")
}
},
object : Migration(6, 7) {
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL("ALTER TABLE homeset ADD COLUMN privBind INTEGER NOT NULL DEFAULT 1")
db.execSQL("ALTER TABLE homeset ADD COLUMN displayName TEXT DEFAULT NULL")
}
},
object : Migration(5, 6) {
override fun migrate(db: SupportSQLiteDatabase) {
val sql = arrayOf(
// migrate "services" to "service": rename columns, make id NOT NULL
"CREATE TABLE service(" +
"id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT," +
"accountName TEXT NOT NULL," +
"type TEXT NOT NULL," +
"principal TEXT DEFAULT NULL" +
")",
"CREATE UNIQUE INDEX index_service_accountName_type ON service(accountName, type)",
"INSERT INTO service(id, accountName, type, principal) SELECT _id, accountName, service, principal FROM services",
"DROP TABLE services",
// migrate "homesets" to "homeset": rename columns, make id NOT NULL
"CREATE TABLE homeset(" +
"id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT," +
"serviceId INTEGER NOT NULL," +
"url TEXT NOT NULL," +
"FOREIGN KEY (serviceId) REFERENCES service(id) ON DELETE CASCADE" +
")",
"CREATE UNIQUE INDEX index_homeset_serviceId_url ON homeset(serviceId, url)",
"INSERT INTO homeset(id, serviceId, url) SELECT _id, serviceID, url FROM homesets",
"DROP TABLE homesets",
// migrate "collections" to "collection": rename columns, make id NOT NULL
"CREATE TABLE collection(" +
"id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT," +
"serviceId INTEGER NOT NULL," +
"type TEXT NOT NULL," +
"url TEXT NOT NULL," +
"privWriteContent INTEGER NOT NULL DEFAULT 1," +
"privUnbind INTEGER NOT NULL DEFAULT 1," +
"forceReadOnly INTEGER NOT NULL DEFAULT 0," +
"displayName TEXT DEFAULT NULL," +
"description TEXT DEFAULT NULL," +
"color INTEGER DEFAULT NULL," +
"timezone TEXT DEFAULT NULL," +
"supportsVEVENT INTEGER DEFAULT NULL," +
"supportsVTODO INTEGER DEFAULT NULL," +
"supportsVJOURNAL INTEGER DEFAULT NULL," +
"source TEXT DEFAULT NULL," +
"sync INTEGER NOT NULL DEFAULT 0," +
"FOREIGN KEY (serviceId) REFERENCES service(id) ON DELETE CASCADE" +
")",
"CREATE INDEX index_collection_serviceId_type ON collection(serviceId,type)",
"INSERT INTO collection(id, serviceId, type, url, privWriteContent, privUnbind, forceReadOnly, displayName, description, color, timezone, supportsVEVENT, supportsVTODO, source, sync) " +
"SELECT _id, serviceID, type, url, privWriteContent, privUnbind, forceReadOnly, displayName, description, color, timezone, supportsVEVENT, supportsVTODO, source, sync FROM collections",
"DROP TABLE collections"
)
sql.forEach { db.execSQL(it) }
}
},
object : Migration(4, 5) {
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL("ALTER TABLE collections ADD COLUMN privWriteContent INTEGER DEFAULT 0 NOT NULL")
db.execSQL("UPDATE collections SET privWriteContent=NOT readOnly")
db.execSQL("ALTER TABLE collections ADD COLUMN privUnbind INTEGER DEFAULT 0 NOT NULL")
db.execSQL("UPDATE collections SET privUnbind=NOT readOnly")
// there's no DROP COLUMN in SQLite, so just keep the "readOnly" column
}
},
object : Migration(3, 4) {
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL("ALTER TABLE collections ADD COLUMN forceReadOnly INTEGER DEFAULT 0 NOT NULL")
}
},
object : Migration(2, 3) {
override fun migrate(db: SupportSQLiteDatabase) {
// 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()
}*/
}
},
object : Migration(1, 2) {
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL("ALTER TABLE collections ADD COLUMN type TEXT NOT NULL DEFAULT ''")
db.execSQL("ALTER TABLE collections ADD COLUMN source TEXT DEFAULT NULL")
db.execSQL("UPDATE collections SET type=(" +
"SELECT CASE service WHEN ? THEN ? ELSE ? END " +
"FROM services WHERE _id=collections.serviceID" +
")",
"CREATE UNIQUE INDEX index_service_accountName_type ON service(accountName, type)",
"INSERT INTO service(id, accountName, type, principal) SELECT _id, accountName, service, principal FROM services",
"DROP TABLE services",
// migrate "homesets" to "homeset": rename columns, make id NOT NULL
"CREATE TABLE homeset(" +
"id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT," +
"serviceId INTEGER NOT NULL," +
"url TEXT NOT NULL," +
"FOREIGN KEY (serviceId) REFERENCES service(id) ON DELETE CASCADE" +
")",
"CREATE UNIQUE INDEX index_homeset_serviceId_url ON homeset(serviceId, url)",
"INSERT INTO homeset(id, serviceId, url) SELECT _id, serviceID, url FROM homesets",
"DROP TABLE homesets",
// migrate "collections" to "collection": rename columns, make id NOT NULL
"CREATE TABLE collection(" +
"id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT," +
"serviceId INTEGER NOT NULL," +
"type TEXT NOT NULL," +
"url TEXT NOT NULL," +
"privWriteContent INTEGER NOT NULL DEFAULT 1," +
"privUnbind INTEGER NOT NULL DEFAULT 1," +
"forceReadOnly INTEGER NOT NULL DEFAULT 0," +
"displayName TEXT DEFAULT NULL," +
"description TEXT DEFAULT NULL," +
"color INTEGER DEFAULT NULL," +
"timezone TEXT DEFAULT NULL," +
"supportsVEVENT INTEGER DEFAULT NULL," +
"supportsVTODO INTEGER DEFAULT NULL," +
"supportsVJOURNAL INTEGER DEFAULT NULL," +
"source TEXT DEFAULT NULL," +
"sync INTEGER NOT NULL DEFAULT 0," +
"FOREIGN KEY (serviceId) REFERENCES service(id) ON DELETE CASCADE" +
")",
"CREATE INDEX index_collection_serviceId_type ON collection(serviceId,type)",
"INSERT INTO collection(id, serviceId, type, url, privWriteContent, privUnbind, forceReadOnly, displayName, description, color, timezone, supportsVEVENT, supportsVTODO, source, sync) " +
"SELECT _id, serviceID, type, url, privWriteContent, privUnbind, forceReadOnly, displayName, description, color, timezone, supportsVEVENT, supportsVTODO, source, sync FROM collections",
"DROP TABLE collections"
)
sql.forEach { db.execSQL(it) }
},
Migration(4, 5) { db ->
db.execSQL("ALTER TABLE collections ADD COLUMN privWriteContent INTEGER DEFAULT 0 NOT NULL")
db.execSQL("UPDATE collections SET privWriteContent=NOT readOnly")
db.execSQL("ALTER TABLE collections ADD COLUMN privUnbind INTEGER DEFAULT 0 NOT NULL")
db.execSQL("UPDATE collections SET privUnbind=NOT readOnly")
// there's no DROP COLUMN in SQLite, so just keep the "readOnly" column
},
Migration(3, 4) { db ->
db.execSQL("ALTER TABLE collections ADD COLUMN forceReadOnly INTEGER DEFAULT 0 NOT NULL")
},
Migration(2, 3) { db ->
// We don't have access to the context in a Room migration now, so
// we will just drop those settings from old DAVx5 versions.
Logger.getGlobal().warning("Dropping settings distrustSystemCerts and overrideProxy*")
},
Migration(1, 2) { db ->
db.execSQL("ALTER TABLE collections ADD COLUMN type TEXT NOT NULL DEFAULT ''")
db.execSQL("ALTER TABLE collections ADD COLUMN source TEXT DEFAULT NULL")
db.execSQL("UPDATE collections SET type=(" +
"SELECT CASE service WHEN ? THEN ? ELSE ? END " +
"FROM services WHERE _id=collections.serviceID" +
")",
arrayOf("caldav", "CALENDAR", "ADDRESS_BOOK"))
arrayOf("caldav", "CALENDAR", "ADDRESS_BOOK"))
}
}
)

View File

@@ -14,7 +14,6 @@ import at.bitfire.dav4jvm.UrlUtils
import at.bitfire.dav4jvm.property.caldav.CalendarColor
import at.bitfire.dav4jvm.property.caldav.CalendarDescription
import at.bitfire.dav4jvm.property.caldav.CalendarTimezone
import at.bitfire.dav4jvm.property.caldav.CalendarTimezoneId
import at.bitfire.dav4jvm.property.caldav.Source
import at.bitfire.dav4jvm.property.caldav.SupportedCalendarComponentSet
import at.bitfire.dav4jvm.property.carddav.AddressbookDescription
@@ -25,7 +24,6 @@ import at.bitfire.dav4jvm.property.webdav.DisplayName
import at.bitfire.dav4jvm.property.webdav.ResourceType
import at.bitfire.davdroid.util.DavUtils.lastSegment
import at.bitfire.davdroid.util.trimToNull
import at.bitfire.ical4android.util.DateUtils
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
@@ -101,8 +99,8 @@ data class Collection(
// CalDAV only
var color: Int? = null,
/** default timezone (only timezone ID, like `Europe/Vienna`) */
var timezoneId: String? = null,
/** timezone definition (full VTIMEZONE) - not a TZID! **/
var timezone: String? = null,
/** whether the collection supports VEVENT; in case of calendars: null means true */
var supportsVEVENT: Boolean? = null,
@@ -129,10 +127,7 @@ data class Collection(
/** WebDAV-Push subscription URL */
var pushSubscription: String? = null,
/** 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) */
/** when the [pushSubscription] was created/updated (used to determine whether we need to re-subscribe) */
var pushSubscriptionCreated: Long? = null
) {
@@ -170,7 +165,7 @@ data class Collection(
var description: String? = null
var color: Int? = null
var timezoneId: String? = null
var timezone: String? = null
var supportsVEVENT: Boolean? = null
var supportsVTODO: Boolean? = null
var supportsVJOURNAL: Boolean? = null
@@ -182,11 +177,7 @@ data class Collection(
TYPE_CALENDAR, TYPE_WEBCAL -> {
dav[CalendarDescription::class.java]?.let { description = it.description }
dav[CalendarColor::class.java]?.let { color = it.color }
dav[CalendarTimezoneId::class.java]?.let { timezoneId = it.identifier }
if (timezoneId == null)
dav[CalendarTimezone::class.java]?.vTimeZone?.let {
timezoneId = DateUtils.parseVTimeZone(it)?.timeZoneId?.value
}
dav[CalendarTimezone::class.java]?.let { timezone = it.vTimeZone }
if (type == TYPE_CALENDAR) {
supportsVEVENT = true
@@ -226,7 +217,7 @@ data class Collection(
displayName = displayName,
description = description,
color = color,
timezoneId = timezoneId,
timezone = timezone,
supportsVEVENT = supportsVEVENT,
supportsVTODO = supportsVTODO,
supportsVJOURNAL = supportsVJOURNAL,

View File

@@ -38,9 +38,6 @@ 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
@@ -90,8 +87,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, pushSubscriptionExpires=:pushSubscriptionExpires, pushSubscriptionCreated=:updatedAt WHERE id=:id")
fun updatePushSubscription(id: Long, pushSubscription: String?, pushSubscriptionExpires: Long?, updatedAt: Long = System.currentTimeMillis()/1000)
@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 sync=:sync WHERE id=:id")
suspend fun updateSync(id: Long, sync: Boolean)

View File

@@ -7,11 +7,15 @@ 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
@@ -24,23 +28,31 @@ 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.time.Duration
import java.time.Instant
import java.util.concurrent.TimeUnit
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
@@ -54,15 +66,34 @@ 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")
try {
registerSyncable()
unregisterNotSyncable()
} catch (_: IOException) {
return Result.retry() // retry on I/O errors
}
registerSyncable()
unregisterNotSyncable()
return Result.success()
}
@@ -77,41 +108,27 @@ 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()
val xml = writer.toString().toRequestBody(DavResource.MIME_XML)
DavCollection(httpClient, collection.url).post(xml) { response ->
if (response.isSuccessful) {
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
)
response.header("Location")?.let { subscriptionUrl ->
collectionRepository.updatePushSubscription(collection.id, subscriptionUrl)
}
} else
logger.warning("Couldn't register push for ${collection.url}: $response")
}
@@ -125,15 +142,6 @@ 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))
@@ -168,11 +176,7 @@ class PushRegistrationWorker @AssistedInject constructor(
}
// remove registration URL from DB in any case
collectionRepository.updatePushSubscription(
id = collection.id,
subscriptionUrl = null,
expires = null
)
collectionRepository.updatePushSubscription(collection.id, null)
}
}
}
@@ -189,4 +193,25 @@ 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
}
}

View File

@@ -1,98 +0,0 @@
/*
* 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
}
}

View File

@@ -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,9 +40,6 @@ class UnifiedPushReceiver: MessagingReceiver() {
@Inject
lateinit var parsePushMessage: PushMessageParser
@Inject
lateinit var pushRegistrationWorkerManager: PushRegistrationWorkerManager
@Inject
lateinit var syncWorkerManager: SyncWorkerManager
@@ -52,7 +49,7 @@ class UnifiedPushReceiver: MessagingReceiver() {
preferenceRepository.unifiedPushEndpoint(endpoint)
// register new endpoint at CalDAV/CardDAV servers
pushRegistrationWorkerManager.updatePeriodicWorker()
PushRegistrationWorker.enqueue(context)
}
override fun onUnregistered(context: Context, instance: String) {

View File

@@ -15,7 +15,7 @@ import at.bitfire.davdroid.R
import at.bitfire.davdroid.db.Credentials
import at.bitfire.davdroid.db.HomeSet
import at.bitfire.davdroid.db.Service
import at.bitfire.davdroid.resource.LocalAddressBookStore
import at.bitfire.davdroid.resource.LocalAddressBook
import at.bitfire.davdroid.resource.LocalTaskList
import at.bitfire.davdroid.servicedetection.DavResourceFinder
import at.bitfire.davdroid.servicedetection.RefreshCollectionsWorker
@@ -49,7 +49,6 @@ class AccountRepository @Inject constructor(
@ApplicationContext val context: Context,
private val collectionRepository: DavCollectionRepository,
private val homeSetRepository: DavHomeSetRepository,
private val localAddressBookStore: Lazy<LocalAddressBookStore>,
private val logger: Logger,
private val settingsManager: SettingsManager,
private val serviceRepository: DavServiceRepository,
@@ -141,7 +140,7 @@ class AccountRepository @Inject constructor(
// delete address books (= address book accounts)
serviceRepository.getByAccountAndType(accountName, Service.TYPE_CARDDAV)?.let { service ->
collectionRepository.getByService(service.id).forEach { collection ->
localAddressBookStore.get().deleteByCollectionId(collection.id)
LocalAddressBook.deleteByCollection(context, collection.id)
}
}

View File

@@ -29,12 +29,15 @@ 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
@@ -43,10 +46,6 @@ 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.
@@ -57,20 +56,14 @@ class DavCollectionRepository @Inject constructor(
private val accountSettingsFactory: AccountSettings.Factory,
@ApplicationContext val context: Context,
db: AppDatabase,
defaultListeners: Lazy<Set<@JvmSuppressWildcards OnChangeListener>>,
defaultListeners: Set<@JvmSuppressWildcards OnChangeListener>,
private val serviceRepository: DavServiceRepository
) {
private val listeners by lazy { Collections.synchronizedSet(defaultListeners.get().toMutableSet()) }
private val listeners = Collections.synchronizedSet(defaultListeners.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
*/
@@ -158,7 +151,7 @@ class DavCollectionRepository @Inject constructor(
displayName = displayName,
description = description,
color = color,
timezoneId = timeZoneId,
timezone = timeZoneId?.let { getVTimeZone(it)?.toString() },
supportsVEVENT = supportVEVENT,
supportsVTODO = supportVTODO,
supportsVJOURNAL = supportVJOURNAL
@@ -263,12 +256,8 @@ class DavCollectionRepository @Inject constructor(
notifyOnChangeListeners()
}
fun updatePushSubscription(id: Long, subscriptionUrl: String?, expires: Long?) {
dao.updatePushSubscription(
id = id,
pushSubscription = subscriptionUrl,
pushSubscriptionExpires = expires
)
fun updatePushSubscription(id: Long, subscriptionUrl: String?) {
dao.updatePushSubscription(id, subscriptionUrl)
}
/**

View File

@@ -10,6 +10,7 @@ import android.content.ContentResolver
import android.content.ContentUris
import android.content.ContentValues
import android.content.Context
import android.os.Build
import android.os.Bundle
import android.os.RemoteException
import android.provider.ContactsContract
@@ -17,13 +18,15 @@ import android.provider.ContactsContract.CommonDataKinds.GroupMembership
import android.provider.ContactsContract.Groups
import android.provider.ContactsContract.RawContacts
import androidx.annotation.OpenForTesting
import androidx.annotation.VisibleForTesting
import at.bitfire.davdroid.R
import at.bitfire.davdroid.db.Collection
import at.bitfire.davdroid.db.SyncState
import at.bitfire.davdroid.repository.DavCollectionRepository
import at.bitfire.davdroid.repository.DavServiceRepository
import at.bitfire.davdroid.resource.workaround.ContactDirtyVerifier
import at.bitfire.davdroid.settings.AccountSettings
import at.bitfire.davdroid.sync.account.SystemAccountUtils
import at.bitfire.davdroid.util.DavUtils.lastSegment
import at.bitfire.davdroid.util.setAndVerifyUserData
import at.bitfire.vcard4android.AndroidAddressBook
import at.bitfire.vcard4android.AndroidContact
@@ -33,9 +36,12 @@ import at.bitfire.vcard4android.GroupMethod
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import dagger.hilt.EntryPoint
import dagger.hilt.InstallIn
import dagger.hilt.android.EntryPointAccessors
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import java.util.LinkedList
import java.util.Optional
import java.util.logging.Level
import java.util.logging.Logger
@@ -57,7 +63,6 @@ open class LocalAddressBook @AssistedInject constructor(
private val accountSettingsFactory: AccountSettings.Factory,
private val collectionRepository: DavCollectionRepository,
@ApplicationContext val context: Context,
val dirtyVerifier: Optional<ContactDirtyVerifier>,
private val logger: Logger,
private val serviceRepository: DavServiceRepository
): AndroidAddressBook<LocalContact, LocalGroup>(_addressBookAccount, provider, LocalContact.Factory, LocalGroup.Factory), LocalCollection<LocalAddress> {
@@ -67,6 +72,7 @@ open class LocalAddressBook @AssistedInject constructor(
fun create(addressBookAccount: Account, provider: ContentProviderClient): LocalAddressBook
}
override val tag: String
get() = "contacts-${addressBookAccount.name}"
@@ -94,7 +100,7 @@ open class LocalAddressBook @AssistedInject constructor(
val accountSettings = accountSettingsFactory.create(account)
accountSettings.getGroupMethod()
}
val includeGroups
private val includeGroups
get() = groupMethod == GroupMethod.GROUP_VCARDS
@Deprecated("Local collection should be identified by ID, not by URL")
@@ -103,42 +109,9 @@ open class LocalAddressBook @AssistedInject constructor(
?: throw IllegalStateException("Address book has no URL")
set(url) = AccountManager.get(context).setAndVerifyUserData(addressBookAccount, USER_DATA_URL, url)
/**
* Read-only flag for the address book itself.
*
* Setting this flag:
*
* - stores the new value in [USER_DATA_READ_ONLY] and
* - sets the read-only flag for all contacts and groups in the address book in the content provider, which will
* prevent non-sync-adapter apps from modifying them. However new entries can still be created, so the address book
* is not really read-only.
*
* Reading this flag returns the stored value from [USER_DATA_READ_ONLY].
*/
override var readOnly: Boolean
get() = AccountManager.get(context).getUserData(addressBookAccount, USER_DATA_READ_ONLY) != null
set(readOnly) {
// set read-only flag for address book itself
AccountManager.get(context).setAndVerifyUserData(addressBookAccount, USER_DATA_READ_ONLY, if (readOnly) "1" else null)
// update raw contacts
val rawContactValues = ContentValues(1).apply {
put(RawContacts.RAW_CONTACT_IS_READ_ONLY, if (readOnly) 1 else 0)
}
provider!!.update(rawContactsSyncUri(), rawContactValues, null, null)
// update data rows
val dataValues = ContentValues(1).apply {
put(ContactsContract.Data.IS_READ_ONLY, if (readOnly) 1 else 0)
}
provider!!.update(syncAdapterURI(ContactsContract.Data.CONTENT_URI), dataValues, null, null)
// update group rows
val groupValues = ContentValues(1).apply {
put(Groups.GROUP_IS_READ_ONLY, if (readOnly) 1 else 0)
}
provider!!.update(groupsSyncUri(), groupValues, null, null)
}
set(readOnly) = AccountManager.get(context).setAndVerifyUserData(addressBookAccount, USER_DATA_READ_ONLY, if (readOnly) "1" else null)
override var lastSyncState: SyncState?
get() = syncState?.let { SyncState.fromString(String(it)) }
@@ -174,6 +147,58 @@ open class LocalAddressBook @AssistedInject constructor(
return number
}
/**
* Updates the address book settings.
*
* @param info collection where to take the settings from
* @param forceReadOnly `true`: set the address book to "force read-only";
* `false`: determine read-only flag from [info];
* `null`: don't change the existing value
*/
fun update(info: Collection, forceReadOnly: Boolean? = null) {
logger.log(Level.INFO, "Updating local address book $addressBookAccount with collection $info")
val accountManager = AccountManager.get(context)
// Update the account name
val newAccountName = accountName(context, info)
if (addressBookAccount.name != newAccountName)
// rename, move contacts/groups and update [AndroidAddressBook.]account
renameAccount(newAccountName)
// Update the account user data
accountManager.setAndVerifyUserData(addressBookAccount, USER_DATA_COLLECTION_ID, info.id.toString())
accountManager.setAndVerifyUserData(addressBookAccount, USER_DATA_URL, info.url.toString())
// Update force read only
if (forceReadOnly != null) {
val nowReadOnly = forceReadOnly || !info.privWriteContent || info.forceReadOnly
if (nowReadOnly != readOnly) {
logger.info("Address book now read-only = $nowReadOnly, updating contacts")
// update address book itself
readOnly = nowReadOnly
// update raw contacts
val rawContactValues = ContentValues(1)
rawContactValues.put(RawContacts.RAW_CONTACT_IS_READ_ONLY, if (nowReadOnly) 1 else 0)
provider!!.update(rawContactsSyncUri(), rawContactValues, null, null)
// update data rows
val dataValues = ContentValues(1)
dataValues.put(ContactsContract.Data.IS_READ_ONLY, if (nowReadOnly) 1 else 0)
provider!!.update(syncAdapterURI(ContactsContract.Data.CONTENT_URI), dataValues, null, null)
// update group rows
val groupValues = ContentValues(1)
groupValues.put(Groups.GROUP_IS_READ_ONLY, if (nowReadOnly) 1 else 0)
provider!!.update(groupsSyncUri(), groupValues, null, null)
}
}
// make sure it will still be synchronized when contacts are updated
updateSyncFrameworkSettings()
}
/**
* Renames an address book account and moves the contacts and groups (without making them dirty).
* Does not keep user data of the old account, so these have to be set again.
@@ -187,6 +212,7 @@ open class LocalAddressBook @AssistedInject constructor(
*
* @return whether the account was renamed successfully
*/
@VisibleForTesting
internal fun renameAccount(newName: String): Boolean {
val oldAccount = addressBookAccount
logger.info("Renaming address book from \"${oldAccount.name}\" to \"$newName\"")
@@ -220,6 +246,11 @@ open class LocalAddressBook @AssistedInject constructor(
return true
}
override fun deleteCollection(): Boolean {
val accountManager = AccountManager.get(context)
return accountManager.removeAccountExplicitly(addressBookAccount)
}
/**
* Updates the sync framework settings for this address book:
@@ -313,6 +344,39 @@ open class LocalAddressBook @AssistedInject constructor(
}
/**
* Queries all contacts with DIRTY flag and checks whether their data checksum has changed, i.e.
* if they're "really dirty" (= data has changed, not only metadata, which is not hashed).
* The DIRTY flag is removed from contacts which are not "really dirty", i.e. from contacts
* whose contact data checksum has not changed.
* @return number of "really dirty" contacts
* @throws RemoteException on content provider errors
*/
fun verifyDirty(): Int {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
throw IllegalStateException("verifyDirty() should not be called on Android != 7.0")
var reallyDirty = 0
for (contact in findDirtyContacts()) {
val lastHash = contact.getLastHashCode()
val currentHash = contact.dataHashCode()
if (lastHash == currentHash) {
// hash is code still the same, contact is not "really dirty" (only metadata been have changed)
logger.log(Level.FINE, "Contact data hash has not changed, resetting dirty flag", contact)
contact.resetDirty()
} else {
logger.log(Level.FINE, "Contact data has changed from hash $lastHash to $currentHash", contact)
reallyDirty++
}
}
if (includeGroups)
reallyDirty += findDirtyGroups().size
return reallyDirty
}
/* special group operations */
/**
@@ -347,27 +411,125 @@ open class LocalAddressBook @AssistedInject constructor(
companion object {
/**
* URL of the corresponding CardDAV address book.
*
* User data of the address book account (String).
*/
@Deprecated("Use the URL of the DB collection instead")
@EntryPoint
@InstallIn(SingletonComponent::class)
interface LocalAddressBookCompanionEntryPoint {
fun localAddressBookFactory(): Factory
fun serviceRepository(): DavServiceRepository
fun logger(): Logger
}
const val USER_DATA_URL = "url"
/**
* ID of the corresponding database [at.bitfire.davdroid.db.Collection].
*
* User data of the address book account (Long).
*/
const val USER_DATA_COLLECTION_ID = "collection_id"
const val USER_DATA_READ_ONLY = "read_only"
// create/query/delete
/**
* Indicates whether the address book is currently set to read-only (i.e. its contacts and groups have the read-only flag).
* Creates a new local address book.
*
* User data of the address book account (Boolean).
* @param context app context to resolve string resources
* @param provider contacts provider client
* @param info collection where to take the name and settings from
* @param forceReadOnly `true`: set the address book to "force read-only"; `false`: determine read-only flag from [info]
*/
const val USER_DATA_READ_ONLY = "read_only"
fun create(context: Context, provider: ContentProviderClient, info: Collection, forceReadOnly: Boolean): LocalAddressBook {
val entryPoint = EntryPointAccessors.fromApplication<LocalAddressBookCompanionEntryPoint>(context)
val logger = entryPoint.logger()
val account = Account(accountName(context, info), context.getString(R.string.account_type_address_book))
val userData = initialUserData(info.url.toString(), info.id.toString())
logger.log(Level.INFO, "Creating local address book $account", userData)
if (!SystemAccountUtils.createAccount(context, account, userData))
throw IllegalStateException("Couldn't create address book account")
val factory = entryPoint.localAddressBookFactory()
val addressBook = factory.create(account, provider)
addressBook.updateSyncFrameworkSettings()
// initialize Contacts Provider Settings
val values = ContentValues(2)
values.put(ContactsContract.Settings.SHOULD_SYNC, 1)
values.put(ContactsContract.Settings.UNGROUPED_VISIBLE, 1)
addressBook.settings = values
addressBook.readOnly = forceReadOnly || !info.privWriteContent || info.forceReadOnly
return addressBook
}
/**
* Finds a [LocalAddressBook] based on its corresponding collection.
*
* @param id collection ID to look for
*
* @return The [LocalAddressBook] for the given collection or *null* if not found
*/
fun findByCollection(context: Context, provider: ContentProviderClient, id: Long): LocalAddressBook? {
val entryPoint = EntryPointAccessors.fromApplication<LocalAddressBookCompanionEntryPoint>(context)
val factory = entryPoint.localAddressBookFactory()
val accountManager = AccountManager.get(context)
return accountManager
.getAccountsByType(context.getString(R.string.account_type_address_book))
.filter { account ->
accountManager.getUserData(account, USER_DATA_COLLECTION_ID)?.toLongOrNull() == id
}
.map { account -> factory.create(account, provider) }
.firstOrNull()
}
/**
* Deletes a [LocalAddressBook] based on its corresponding database collection.
*
* @param id collection ID to look for
*/
fun deleteByCollection(context: Context, id: Long) {
val accountManager = AccountManager.get(context)
val addressBookAccount = accountManager.getAccountsByType(context.getString(R.string.account_type_address_book)).firstOrNull { account ->
accountManager.getUserData(account, USER_DATA_COLLECTION_ID)?.toLongOrNull() == id
}
if (addressBookAccount != null)
accountManager.removeAccountExplicitly(addressBookAccount)
}
// helpers
/**
* Creates a name for the address book account from its corresponding db collection info.
*
* The address book account name contains
* - the collection display name or last URL path segment
* - the actual account name
* - the collection ID, to make it unique.
*
* @param info The corresponding collection
*/
fun accountName(context: Context, info: Collection): String {
// Name the address book after given collection display name, otherwise use last URL path segment
val sb = StringBuilder(info.displayName.let {
if (it.isNullOrEmpty())
info.url.lastSegment
else
it
})
// Add the actual account name to the address book account name
val entryPoint = EntryPointAccessors.fromApplication<LocalAddressBookCompanionEntryPoint>(context)
val serviceRepository = entryPoint.serviceRepository()
serviceRepository.get(info.serviceId)?.let { service ->
sb.append(" (${service.accountName})")
}
// Add the collection ID for uniqueness
sb.append(" #${info.id}")
return sb.toString()
}
private fun initialUserData(url: String, collectionId: String): Bundle {
val bundle = Bundle(3)
bundle.putString(USER_DATA_COLLECTION_ID, collectionId)
bundle.putString(USER_DATA_URL, url)
return bundle
}
}

View File

@@ -1,216 +0,0 @@
/*
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
*/
package at.bitfire.davdroid.resource
import android.accounts.Account
import android.accounts.AccountManager
import android.content.ContentProviderClient
import android.content.ContentValues
import android.content.Context
import android.os.Bundle
import android.provider.ContactsContract
import androidx.annotation.VisibleForTesting
import at.bitfire.davdroid.R
import at.bitfire.davdroid.db.Collection
import at.bitfire.davdroid.db.Service
import at.bitfire.davdroid.repository.DavCollectionRepository
import at.bitfire.davdroid.repository.DavServiceRepository
import at.bitfire.davdroid.resource.LocalAddressBook.Companion.USER_DATA_COLLECTION_ID
import at.bitfire.davdroid.resource.LocalAddressBook.Companion.USER_DATA_URL
import at.bitfire.davdroid.settings.Settings
import at.bitfire.davdroid.settings.SettingsManager
import at.bitfire.davdroid.sync.account.SystemAccountUtils
import at.bitfire.davdroid.util.DavUtils.lastSegment
import at.bitfire.davdroid.util.setAndVerifyUserData
import dagger.hilt.android.qualifiers.ApplicationContext
import java.util.logging.Level
import java.util.logging.Logger
import javax.inject.Inject
import kotlin.collections.orEmpty
class LocalAddressBookStore @Inject constructor(
val collectionRepository: DavCollectionRepository,
@ApplicationContext val context: Context,
val localAddressBookFactory: LocalAddressBook.Factory,
val logger: Logger,
val serviceRepository: DavServiceRepository,
val settings: SettingsManager
): LocalDataStore<LocalAddressBook> {
/** whether a (usually managed) setting wants all address-books to be read-only **/
val forceAllReadOnly: Boolean
get() = settings.getBoolean(Settings.FORCE_READ_ONLY_ADDRESSBOOKS)
/**
* Assembles a name for the address book (account) from its corresponding database [Collection].
*
* The address book account name contains
*
* - the collection display name or last URL path segment
* - the actual account name
* - the collection ID, to make it unique.
*
* @param info Collection to take info from
*/
fun accountName(info: Collection): String {
// Name the address book after given collection display name, otherwise use last URL path segment
val sb = StringBuilder(info.displayName.let {
if (it.isNullOrEmpty())
info.url.lastSegment
else
it
})
// Add the actual account name to the address book account name
serviceRepository.get(info.serviceId)?.let { service ->
sb.append(" (${service.accountName})")
}
// Add the collection ID for uniqueness
sb.append(" #${info.id}")
return sb.toString()
}
override fun create(provider: ContentProviderClient, fromCollection: Collection): LocalAddressBook? {
val name = accountName(fromCollection)
val account = createAccount(
name = name,
id = fromCollection.id,
url = fromCollection.url.toString()
) ?: return null
val addressBook = localAddressBookFactory.create(account, provider)
// update settings
addressBook.updateSyncFrameworkSettings()
addressBook.settings = contactsProviderSettings
addressBook.readOnly = shouldBeReadOnly(fromCollection, forceAllReadOnly)
return addressBook
}
fun createAccount(name: String, id: Long, url: String): Account? {
// create account with collection ID and URL
val account = Account(name, context.getString(R.string.account_type_address_book))
val userData = Bundle(2).apply {
putString(USER_DATA_COLLECTION_ID, id.toString())
putString(USER_DATA_URL, url)
}
if (!SystemAccountUtils.createAccount(context, account, userData)) {
logger.warning("Couldn't create address book account: $account")
return null
}
return account
}
override fun getAll(account: Account, provider: ContentProviderClient): List<LocalAddressBook> =
serviceRepository.getByAccountAndType(account.name, Service.TYPE_CARDDAV)?.let { service ->
// get all collections for the CardDAV service
collectionRepository.getByService(service.id).mapNotNull { collection ->
// and map to a LocalAddressBook, if applicable
findByCollection(provider, collection.id)
}
}.orEmpty()
/**
* Finds a [LocalAddressBook] based on its corresponding collection.
*
* @param id collection ID to look for
*
* @return The [LocalAddressBook] for the given collection or *null* if not found
*/
private fun findByCollection(provider: ContentProviderClient, id: Long): LocalAddressBook? {
val accountManager = AccountManager.get(context)
return accountManager
.getAccountsByType(context.getString(R.string.account_type_address_book))
.filter { account ->
accountManager.getUserData(account, USER_DATA_COLLECTION_ID)?.toLongOrNull() == id
}
.map { account -> localAddressBookFactory.create(account, provider) }
.firstOrNull()
}
override fun update(provider: ContentProviderClient, localCollection: LocalAddressBook, fromCollection: Collection) {
var currentAccount = localCollection.addressBookAccount
logger.log(Level.INFO, "Updating local address book $currentAccount from collection $fromCollection")
// Update the account name
val newAccountName = accountName(fromCollection)
if (currentAccount.name != newAccountName) {
// rename, move contacts/groups and update [AndroidAddressBook.]account
localCollection.renameAccount(newAccountName)
currentAccount.name = newAccountName
}
// Update the account user data
val accountManager = AccountManager.get(context)
accountManager.setAndVerifyUserData(currentAccount, USER_DATA_COLLECTION_ID, fromCollection.id.toString())
accountManager.setAndVerifyUserData(currentAccount, USER_DATA_URL, fromCollection.url.toString())
// Set contacts provider settings
localCollection.settings = contactsProviderSettings
// Update force read only
val nowReadOnly = shouldBeReadOnly(fromCollection, forceAllReadOnly)
if (nowReadOnly != localCollection.readOnly) {
logger.info("Address book has changed to read-only = $nowReadOnly")
localCollection.readOnly = nowReadOnly
}
// make sure it will still be synchronized when contacts are updated
localCollection.updateSyncFrameworkSettings()
}
override fun delete(localCollection: LocalAddressBook) {
val accountManager = AccountManager.get(context)
accountManager.removeAccountExplicitly(localCollection.addressBookAccount)
}
/**
* Deletes a [LocalAddressBook] based on its corresponding database collection.
*
* @param id [Collection.id] to look for
*/
fun deleteByCollectionId(id: Long) {
val accountManager = AccountManager.get(context)
val addressBookAccount = accountManager.getAccountsByType(context.getString(R.string.account_type_address_book)).firstOrNull { account ->
accountManager.getUserData(account, USER_DATA_COLLECTION_ID)?.toLongOrNull() == id
}
if (addressBookAccount != null)
accountManager.removeAccountExplicitly(addressBookAccount)
}
companion object {
/**
* Contacts Provider Settings (equal for every address book)
*/
val contactsProviderSettings
get() = ContentValues(2).apply {
// SHOULD_SYNC is just a hint that an account's contacts (the contacts of this local address book) are syncable.
put(ContactsContract.Settings.SHOULD_SYNC, 1)
// UNGROUPED_VISIBLE is required for making contacts work over Bluetooth (especially with some car systems).
put(ContactsContract.Settings.UNGROUPED_VISIBLE, 1)
}
/**
* Determines whether the address book should be set to read-only.
*
* @param forceAllReadOnly Whether (usually managed, app-wide) setting should overwrite local read-only information
* @param info Collection data to determine read-only status from (either user-set read-only flag or missing write privilege)
*/
@VisibleForTesting
internal fun shouldBeReadOnly(info: Collection, forceAllReadOnly: Boolean): Boolean =
info.readOnly() || forceAllReadOnly
}
}

View File

@@ -8,12 +8,17 @@ import android.accounts.Account
import android.content.ContentProviderClient
import android.content.ContentUris
import android.content.ContentValues
import android.net.Uri
import android.provider.CalendarContract.Calendars
import android.provider.CalendarContract.Events
import at.bitfire.davdroid.Constants
import at.bitfire.davdroid.db.Collection
import at.bitfire.davdroid.db.SyncState
import at.bitfire.davdroid.util.DavUtils.lastSegment
import at.bitfire.ical4android.AndroidCalendar
import at.bitfire.ical4android.AndroidCalendarFactory
import at.bitfire.ical4android.BatchOperation
import at.bitfire.ical4android.util.DateUtils
import at.bitfire.ical4android.util.MiscUtils.asSyncAdapter
import java.util.LinkedList
import java.util.logging.Level
@@ -37,6 +42,60 @@ class LocalCalendar private constructor(
private val logger: Logger
get() = Logger.getGlobal()
fun create(account: Account, provider: ContentProviderClient, info: Collection): Uri {
// If the collection doesn't have a color, use a default color.
if (info.color != null)
info.color = Constants.DAVDROID_GREEN_RGBA
val values = valuesFromCollectionInfo(info, withColor = true)
// ACCOUNT_NAME and ACCOUNT_TYPE are required (see docs)! If it's missing, other apps will crash.
values.put(Calendars.ACCOUNT_NAME, account.name)
values.put(Calendars.ACCOUNT_TYPE, account.type)
// Email address for scheduling. Used by the calendar provider to determine whether the
// user is ORGANIZER/ATTENDEE for a certain event.
values.put(Calendars.OWNER_ACCOUNT, account.name)
// flag as visible & synchronizable at creation, might be changed by user at any time
values.put(Calendars.VISIBLE, 1)
values.put(Calendars.SYNC_EVENTS, 1)
return create(account, provider, values)
}
private fun valuesFromCollectionInfo(info: Collection, withColor: Boolean): ContentValues {
val values = ContentValues()
values.put(Calendars.NAME, info.url.toString())
values.put(Calendars.CALENDAR_DISPLAY_NAME,
if (info.displayName.isNullOrBlank()) info.url.lastSegment else info.displayName)
if (withColor && info.color != null)
values.put(Calendars.CALENDAR_COLOR, info.color)
if (info.privWriteContent && !info.forceReadOnly) {
values.put(Calendars.CALENDAR_ACCESS_LEVEL, Calendars.CAL_ACCESS_OWNER)
values.put(Calendars.CAN_MODIFY_TIME_ZONE, 1)
values.put(Calendars.CAN_ORGANIZER_RESPOND, 1)
} else
values.put(Calendars.CALENDAR_ACCESS_LEVEL, Calendars.CAL_ACCESS_READ)
info.timezone?.let { tzData ->
try {
val timeZone = DateUtils.parseVTimeZone(tzData)
timeZone.timeZoneId?.let { tzId ->
values.put(Calendars.CALENDAR_TIME_ZONE, DateUtils.findAndroidTimezoneID(tzId.value))
}
} catch(e: IllegalArgumentException) {
logger.log(Level.WARNING, "Couldn't parse calendar default time zone", e)
}
}
// add base values for Calendars
values.putAll(calendarBaseValues)
return values
}
}
override val collectionUrl: String?
@@ -52,6 +111,8 @@ class LocalCalendar private constructor(
override val readOnly
get() = accessLevel <= Calendars.CAL_ACCESS_READ
override fun deleteCollection(): Boolean = delete()
override var lastSyncState: SyncState?
get() = provider.query(calendarSyncURI(), arrayOf(COLUMN_SYNC_STATE), null, null, null)?.use { cursor ->
if (cursor.moveToNext())
@@ -71,6 +132,9 @@ class LocalCalendar private constructor(
accessLevel = info.getAsInteger(Calendars.CALENDAR_ACCESS_LEVEL) ?: Calendars.CAL_ACCESS_OWNER
}
fun update(info: Collection, updateColor: Boolean) =
update(valuesFromCollectionInfo(info, updateColor))
override fun findDeleted() =
queryEvents("${Events.DELETED} AND ${Events.ORIGINAL_ID} IS NULL", null)

View File

@@ -1,109 +0,0 @@
/*
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
*/
package at.bitfire.davdroid.resource
import android.accounts.Account
import android.content.ContentProviderClient
import android.content.ContentUris
import android.content.ContentValues
import android.content.Context
import android.provider.CalendarContract.Calendars
import at.bitfire.davdroid.Constants
import at.bitfire.davdroid.R
import at.bitfire.davdroid.db.AppDatabase
import at.bitfire.davdroid.db.Collection
import at.bitfire.davdroid.settings.AccountSettings
import at.bitfire.davdroid.util.DavUtils.lastSegment
import at.bitfire.ical4android.AndroidCalendar
import at.bitfire.ical4android.AndroidCalendar.Companion.calendarBaseValues
import at.bitfire.ical4android.util.DateUtils
import dagger.hilt.android.qualifiers.ApplicationContext
import java.util.logging.Level
import java.util.logging.Logger
import javax.inject.Inject
class LocalCalendarStore @Inject constructor(
@ApplicationContext val context: Context,
val accountSettingsFactory: AccountSettings.Factory,
db: AppDatabase,
val logger: Logger
): LocalDataStore<LocalCalendar> {
private val serviceDao = db.serviceDao()
override fun create(provider: ContentProviderClient, fromCollection: Collection): LocalCalendar? {
val service = serviceDao.get(fromCollection.serviceId) ?: throw IllegalArgumentException("Couldn't fetch DB service from collection")
val account = Account(service.accountName, context.getString(R.string.account_type))
// If the collection doesn't have a color, use a default color.
if (fromCollection.color != null)
fromCollection.color = Constants.DAVDROID_GREEN_RGBA
val values = valuesFromCollectionInfo(fromCollection, withColor = true)
// ACCOUNT_NAME and ACCOUNT_TYPE are required (see docs)! If it's missing, other apps will crash.
values.put(Calendars.ACCOUNT_NAME, account.name)
values.put(Calendars.ACCOUNT_TYPE, account.type)
// Email address for scheduling. Used by the calendar provider to determine whether the
// user is ORGANIZER/ATTENDEE for a certain event.
values.put(Calendars.OWNER_ACCOUNT, account.name)
// flag as visible & syncable at creation, might be changed by user at any time
values.put(Calendars.VISIBLE, 1)
values.put(Calendars.SYNC_EVENTS, 1)
logger.log(Level.INFO, "Adding local calendar", values)
val uri = AndroidCalendar.create(account, provider, values)
return AndroidCalendar.findByID(account, provider, LocalCalendar.Factory, ContentUris.parseId(uri))
}
override fun getAll(account: Account, provider: ContentProviderClient) =
AndroidCalendar.find(account, provider, LocalCalendar.Factory, "${Calendars.SYNC_EVENTS}!=0", null)
override fun update(provider: ContentProviderClient, localCollection: LocalCalendar, fromCollection: Collection) {
val accountSettings = accountSettingsFactory.create(localCollection.account)
val values = valuesFromCollectionInfo(fromCollection, withColor = accountSettings.getManageCalendarColors())
logger.log(Level.FINE, "Updating local calendar ${fromCollection.url}", values)
localCollection.update(values)
}
private fun valuesFromCollectionInfo(info: Collection, withColor: Boolean): ContentValues {
val values = ContentValues()
values.put(Calendars.NAME, info.url.toString())
values.put(Calendars.CALENDAR_DISPLAY_NAME,
if (info.displayName.isNullOrBlank()) info.url.lastSegment else info.displayName)
if (withColor && info.color != null)
values.put(Calendars.CALENDAR_COLOR, info.color)
if (info.privWriteContent && !info.forceReadOnly) {
values.put(Calendars.CALENDAR_ACCESS_LEVEL, Calendars.CAL_ACCESS_OWNER)
values.put(Calendars.CAN_MODIFY_TIME_ZONE, 1)
values.put(Calendars.CAN_ORGANIZER_RESPOND, 1)
} else
values.put(Calendars.CALENDAR_ACCESS_LEVEL, Calendars.CAL_ACCESS_READ)
info.timezoneId?.let { tzId ->
values.put(Calendars.CALENDAR_TIME_ZONE, DateUtils.findAndroidTimezoneID(tzId))
}
// add base values for Calendars
values.putAll(calendarBaseValues)
return values
}
override fun delete(localCollection: LocalCalendar) {
logger.log(Level.INFO, "Deleting local calendar", localCollection)
localCollection.delete()
}
}

View File

@@ -4,6 +4,7 @@
package at.bitfire.davdroid.resource
import android.provider.CalendarContract.Events
import at.bitfire.davdroid.db.SyncState
interface LocalCollection<out T: LocalResource<*>> {
@@ -26,6 +27,13 @@ interface LocalCollection<out T: LocalResource<*>> {
*/
val readOnly: Boolean
/**
* Deletes the local collection.
*
* @return true if the collection was deleted, false otherwise
*/
fun deleteCollection(): Boolean
/**
* Finds local resources of this collection which have been marked as *deleted* by the user
* or an app acting on their behalf.

View File

@@ -5,6 +5,7 @@
package at.bitfire.davdroid.resource
import android.content.ContentValues
import android.os.Build
import android.os.RemoteException
import android.provider.ContactsContract
import android.provider.ContactsContract.CommonDataKinds.GroupMembership
@@ -24,7 +25,7 @@ import at.bitfire.vcard4android.Contact
import ezvcard.Ezvcard
import java.io.FileNotFoundException
import java.util.UUID
import kotlin.jvm.optionals.getOrNull
import java.util.logging.Logger
class LocalContact: AndroidContact, LocalAddress {
@@ -38,6 +39,8 @@ class LocalContact: AndroidContact, LocalAddress {
const val COLUMN_HASHCODE = ContactsContract.RawContacts.SYNC3
}
private val logger: Logger = Logger.getGlobal()
override val addressBook: LocalAddressBook
get() = super.addressBook as LocalAddressBook
@@ -88,13 +91,6 @@ class LocalContact: AndroidContact, LocalAddress {
return "$uid.vcf"
}
/**
* Clears cached [contact] so that the next read of [contact] will query the content provider again.
*/
fun clearCachedContact() {
_contact = null
}
override fun clearDirty(fileName: String?, eTag: String?, scheduleTag: String?) {
if (scheduleTag != null)
throw IllegalArgumentException("Contacts must not have a Schedule-Tag")
@@ -105,8 +101,12 @@ class LocalContact: AndroidContact, LocalAddress {
values.put(COLUMN_ETAG, eTag)
values.put(ContactsContract.RawContacts.DIRTY, 0)
// Android 7 workaround
addressBook.dirtyVerifier.getOrNull()?.setHashCodeColumn(this, values)
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
// workaround for Android 7 which sets DIRTY flag when only meta-data is changed
val hashCode = dataHashCode()
values.put(COLUMN_HASHCODE, hashCode)
logger.finer("Clearing dirty flag with eTag = $eTag, contact hash = $hashCode")
}
addressBook.provider!!.update(rawContactSyncURI(), values, null, null)
@@ -136,6 +136,54 @@ class LocalContact: AndroidContact, LocalAddress {
}
/**
* Calculates a hash code from the contact's data (VCard) and group memberships.
* Attention: re-reads {@link #contact} from the database, discarding all changes in memory
* @return hash code of contact data (including group memberships)
*/
internal fun dataHashCode(): Int {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
throw IllegalStateException("dataHashCode() should not be called on Android != 7")
// reset contact so that getContact() reads from database
_contact = null
// groupMemberships is filled by getContact()
val dataHash = getContact().hashCode()
val groupHash = groupMemberships.hashCode()
logger.finest("Calculated data hash = $dataHash, group memberships hash = $groupHash")
return dataHash xor groupHash
}
fun updateHashCode(batch: BatchOperation?) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
throw IllegalStateException("updateHashCode() should not be called on Android != 7")
val hashCode = dataHashCode()
logger.fine("Storing contact hash = $hashCode")
if (batch == null) {
val values = ContentValues(1)
values.put(COLUMN_HASHCODE, hashCode)
addressBook.provider!!.update(rawContactSyncURI(), values, null, null)
} else
batch.enqueue(BatchOperation.CpoBuilder
.newUpdate(rawContactSyncURI())
.withValue(COLUMN_HASHCODE, hashCode))
}
fun getLastHashCode(): Int {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
throw IllegalStateException("getLastHashCode() should not be called on Android != 7")
addressBook.provider!!.query(rawContactSyncURI(), arrayOf(COLUMN_HASHCODE), null, null, null)?.use { c ->
if (c.moveToNext() && !c.isNull(0))
return c.getInt(0)
}
return 0
}
fun addToGroup(batch: BatchOperation, groupID: Long) {
batch.enqueue(BatchOperation.CpoBuilder
.newInsert(dataSyncURI())
@@ -190,7 +238,6 @@ class LocalContact: AndroidContact, LocalAddress {
// data rows
override fun buildContact(builder: BatchOperation.CpoBuilder, update: Boolean) {
builder.withValue(COLUMN_FLAGS, flags)
super.buildContact(builder, update)
@@ -203,4 +250,4 @@ class LocalContact: AndroidContact, LocalAddress {
LocalContact(addressBook as LocalAddressBook, values)
}
}
}

View File

@@ -1,53 +0,0 @@
/*
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
*/
package at.bitfire.davdroid.resource
import android.accounts.Account
import android.content.ContentProviderClient
import at.bitfire.davdroid.db.Collection
/**
* Represents a local data store for a specific collection type.
* Manages creation, update, and deletion of collections of the given type.
*/
interface LocalDataStore<T: LocalCollection<*>> {
/**
* Creates a new local collection from the given (remote) collection info.
*
* @param provider the content provider client
* @param fromCollection collection info
*
* @return the new local collection, or `null` if creation failed
*/
fun create(provider: ContentProviderClient, fromCollection: Collection): T?
/**
* Returns all local collections of the data store.
*
* @param account the account that the data store is associated with
* @param provider the content provider client
*
* @return a list of all local collections
*/
fun getAll(account: Account, provider: ContentProviderClient): List<T>
/**
* Updates the local collection with the data from the given (remote) collection info.
*
* @param provider the content provider client
* @param localCollection the local collection to update
* @param fromCollection collection info
*/
fun update(provider: ContentProviderClient, localCollection: T, fromCollection: Collection)
/**
* Deletes the local collection.
*
* @param localCollection the local collection to delete
*/
fun delete(localCollection: T)
}

View File

@@ -25,7 +25,6 @@ import at.bitfire.vcard4android.Contact
import java.util.LinkedList
import java.util.UUID
import java.util.logging.Logger
import kotlin.jvm.optionals.getOrNull
class LocalGroup: AndroidGroup, LocalAddress {
@@ -91,14 +90,11 @@ class LocalGroup: AndroidGroup, LocalAddress {
changeContactIDs += missingMember.id!!
}
addressBook.dirtyVerifier.getOrNull()?.let { verifier ->
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O)
// workaround for Android 7 which sets DIRTY flag when only meta-data is changed
changeContactIDs
.map { id -> addressBook.findContactById(id) }
.forEach { contact ->
verifier.updateHashCode(contact, batch)
}
}
.map { addressBook.findContactById(it) }
.forEach { it.updateHashCode(batch) }
batch.commit()
}

View File

@@ -6,20 +6,64 @@ package at.bitfire.davdroid.resource
import android.accounts.Account
import android.content.ContentProviderClient
import android.content.ContentValues
import android.net.Uri
import at.bitfire.davdroid.Constants
import at.bitfire.davdroid.db.Collection
import at.bitfire.davdroid.db.Principal
import at.bitfire.davdroid.db.SyncState
import at.bitfire.davdroid.util.DavUtils.lastSegment
import at.bitfire.ical4android.JtxCollection
import at.bitfire.ical4android.JtxCollectionFactory
import at.bitfire.ical4android.JtxICalObject
import at.techbee.jtx.JtxContract
import java.util.logging.Logger
class LocalJtxCollection(account: Account, client: ContentProviderClient, id: Long):
JtxCollection<JtxICalObject>(account, client, LocalJtxICalObject.Factory, id),
LocalCollection<LocalJtxICalObject>{
companion object {
fun create(account: Account, client: ContentProviderClient, info: Collection, owner: Principal?): Uri {
// If the collection doesn't have a color, use a default color.
if (info.color != null)
info.color = Constants.DAVDROID_GREEN_RGBA
val values = valuesFromCollection(info, account, owner, withColor = true)
return create(account, client, values)
}
fun valuesFromCollection(info: Collection, account: Account, owner: Principal?, withColor: Boolean) =
ContentValues().apply {
put(JtxContract.JtxCollection.URL, info.url.toString())
put(
JtxContract.JtxCollection.DISPLAYNAME,
info.displayName ?: info.url.lastSegment
)
put(JtxContract.JtxCollection.DESCRIPTION, info.description)
if (owner != null)
put(JtxContract.JtxCollection.OWNER, owner.url.toString())
else
Logger.getGlobal().warning("No collection owner given. Will create jtx collection without owner")
put(JtxContract.JtxCollection.OWNER_DISPLAYNAME, owner?.displayName)
if (withColor && info.color != null)
put(JtxContract.JtxCollection.COLOR, info.color)
put(JtxContract.JtxCollection.SUPPORTSVEVENT, info.supportsVEVENT)
put(JtxContract.JtxCollection.SUPPORTSVJOURNAL, info.supportsVJOURNAL)
put(JtxContract.JtxCollection.SUPPORTSVTODO, info.supportsVTODO)
put(JtxContract.JtxCollection.ACCOUNT_NAME, account.name)
put(JtxContract.JtxCollection.ACCOUNT_TYPE, account.type)
put(JtxContract.JtxCollection.READONLY, info.forceReadOnly || !info.privWriteContent)
}
}
override val readOnly: Boolean
get() = throw NotImplementedError()
override fun deleteCollection(): Boolean = delete()
override val tag: String
get() = "jtx-${account.name}-$id"
override val collectionUrl: String?
@@ -30,6 +74,10 @@ class LocalJtxCollection(account: Account, client: ContentProviderClient, id: Lo
get() = SyncState.fromString(syncstate)
set(value) { syncstate = value.toString() }
fun updateCollection(info: Collection, owner: Principal?, updateColor: Boolean) {
val values = valuesFromCollection(info, account, owner, updateColor)
update(values)
}
override fun findDeleted(): List<LocalJtxICalObject> {
val values = queryDeletedICalObjects()

View File

@@ -1,89 +0,0 @@
/*
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
*/
package at.bitfire.davdroid.resource
import android.accounts.Account
import android.content.ContentProviderClient
import android.content.ContentUris
import android.content.ContentValues
import android.content.Context
import at.bitfire.davdroid.Constants
import at.bitfire.davdroid.R
import at.bitfire.davdroid.db.AppDatabase
import at.bitfire.davdroid.db.Collection
import at.bitfire.davdroid.repository.PrincipalRepository
import at.bitfire.davdroid.settings.AccountSettings
import at.bitfire.davdroid.util.DavUtils.lastSegment
import at.bitfire.ical4android.JtxCollection
import at.techbee.jtx.JtxContract
import dagger.hilt.android.qualifiers.ApplicationContext
import java.util.logging.Logger
import javax.inject.Inject
class LocalJtxCollectionStore @Inject constructor(
@ApplicationContext val context: Context,
val accountSettingsFactory: AccountSettings.Factory,
db: AppDatabase,
val principalRepository: PrincipalRepository
): LocalDataStore<LocalJtxCollection> {
private val serviceDao = db.serviceDao()
override fun create(provider: ContentProviderClient, fromCollection: Collection): LocalJtxCollection? {
// If the collection doesn't have a color, use a default color.
if (fromCollection.color != null)
fromCollection.color = Constants.DAVDROID_GREEN_RGBA
val service = serviceDao.get(fromCollection.serviceId) ?: throw IllegalArgumentException("Couldn't fetch DB service from collection")
val account = Account(service.accountName, context.getString(R.string.account_type))
val values = valuesFromCollection(fromCollection, account = account, withColor = true)
val uri = JtxCollection.create(account, provider, values)
return LocalJtxCollection(account, provider, ContentUris.parseId(uri))
}
private fun valuesFromCollection(info: Collection, account: Account, withColor: Boolean): ContentValues {
val owner = info.ownerId?.let { principalRepository.get(it) }
return ContentValues().apply {
put(JtxContract.JtxCollection.URL, info.url.toString())
put(
JtxContract.JtxCollection.DISPLAYNAME,
info.displayName ?: info.url.lastSegment
)
put(JtxContract.JtxCollection.DESCRIPTION, info.description)
if (owner != null)
put(JtxContract.JtxCollection.OWNER, owner.url.toString())
else
Logger.getGlobal().warning("No collection owner given. Will create jtx collection without owner")
put(JtxContract.JtxCollection.OWNER_DISPLAYNAME, owner?.displayName)
if (withColor && info.color != null)
put(JtxContract.JtxCollection.COLOR, info.color)
put(JtxContract.JtxCollection.SUPPORTSVEVENT, info.supportsVEVENT)
put(JtxContract.JtxCollection.SUPPORTSVJOURNAL, info.supportsVJOURNAL)
put(JtxContract.JtxCollection.SUPPORTSVTODO, info.supportsVTODO)
put(JtxContract.JtxCollection.ACCOUNT_NAME, account.name)
put(JtxContract.JtxCollection.ACCOUNT_TYPE, account.type)
put(JtxContract.JtxCollection.READONLY, info.forceReadOnly || !info.privWriteContent)
}
}
override fun getAll(account: Account, provider: ContentProviderClient): List<LocalJtxCollection> =
JtxCollection.find(account, provider, context, LocalJtxCollection.Factory, null, null)
override fun update(provider: ContentProviderClient, localCollection: LocalJtxCollection, fromCollection: Collection) {
val accountSettings = accountSettingsFactory.create(localCollection.account)
val values = valuesFromCollection(fromCollection, account = localCollection.account, withColor = accountSettings.getManageCalendarColors())
localCollection.update(values)
}
override fun delete(localCollection: LocalJtxCollection) {
localCollection.delete()
}
}

View File

@@ -9,7 +9,11 @@ import android.annotation.SuppressLint
import android.content.ContentProviderClient
import android.content.ContentValues
import android.content.Context
import android.net.Uri
import at.bitfire.davdroid.Constants
import at.bitfire.davdroid.db.Collection
import at.bitfire.davdroid.db.SyncState
import at.bitfire.davdroid.util.DavUtils.lastSegment
import at.bitfire.ical4android.DmfsTaskList
import at.bitfire.ical4android.DmfsTaskListFactory
import at.bitfire.ical4android.TaskProvider
@@ -31,6 +35,18 @@ class LocalTaskList private constructor(
companion object {
fun create(account: Account, provider: ContentProviderClient, providerName: TaskProvider.ProviderName, info: Collection): Uri {
// If the collection doesn't have a color, use a default color.
if (info.color != null)
info.color = Constants.DAVDROID_GREEN_RGBA
val values = valuesFromCollectionInfo(info, withColor = true)
values.put(TaskLists.OWNER, account.name)
values.put(TaskLists.SYNC_ENABLED, 1)
values.put(TaskLists.VISIBLE, 1)
return create(account, provider, providerName, values)
}
@SuppressLint("Recycle")
@Throws(Exception::class)
fun onRenameAccount(context: Context, oldName: String, newName: String) {
@@ -45,6 +61,23 @@ class LocalTaskList private constructor(
}
}
private fun valuesFromCollectionInfo(info: Collection, withColor: Boolean): ContentValues {
val values = ContentValues(3)
values.put(TaskLists._SYNC_ID, info.url.toString())
values.put(TaskLists.LIST_NAME,
if (info.displayName.isNullOrBlank()) info.url.lastSegment else info.displayName)
if (withColor && info.color != null)
values.put(TaskLists.LIST_COLOR, info.color)
if (info.privWriteContent && !info.forceReadOnly)
values.put(TaskListColumns.ACCESS_LEVEL, TaskListColumns.ACCESS_LEVEL_OWNER)
else
values.put(TaskListColumns.ACCESS_LEVEL, TaskListColumns.ACCESS_LEVEL_READ)
return values
}
}
private val logger = Logger.getGlobal()
@@ -55,6 +88,8 @@ class LocalTaskList private constructor(
accessLevel != TaskListColumns.ACCESS_LEVEL_UNDEFINED &&
accessLevel <= TaskListColumns.ACCESS_LEVEL_READ
override fun deleteCollection(): Boolean = delete()
override val collectionUrl: String?
get() = syncId
@@ -91,6 +126,9 @@ class LocalTaskList private constructor(
accessLevel = values.getAsInteger(TaskListColumns.ACCESS_LEVEL)
}
fun update(info: Collection, updateColor: Boolean) =
update(valuesFromCollectionInfo(info, updateColor))
override fun findDeleted() = queryTasks(Tasks._DELETED, null)

View File

@@ -1,102 +0,0 @@
/*
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
*/
package at.bitfire.davdroid.resource
import android.accounts.Account
import android.content.ContentProviderClient
import android.content.ContentUris
import android.content.ContentValues
import android.content.Context
import android.net.Uri
import at.bitfire.davdroid.Constants
import at.bitfire.davdroid.R
import at.bitfire.davdroid.db.AppDatabase
import at.bitfire.davdroid.db.Collection
import at.bitfire.davdroid.settings.AccountSettings
import at.bitfire.davdroid.util.DavUtils.lastSegment
import at.bitfire.ical4android.DmfsTaskList
import at.bitfire.ical4android.TaskProvider
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import dagger.hilt.android.qualifiers.ApplicationContext
import org.dmfs.tasks.contract.TaskContract.TaskListColumns
import org.dmfs.tasks.contract.TaskContract.TaskLists
import java.util.logging.Level
import java.util.logging.Logger
class LocalTaskListStore @AssistedInject constructor(
@Assisted authority: String,
val accountSettingsFactory: AccountSettings.Factory,
@ApplicationContext val context: Context,
val db: AppDatabase,
val logger: Logger
): LocalDataStore<LocalTaskList> {
@AssistedFactory
interface Factory {
fun create(authority: String): LocalTaskListStore
}
private val serviceDao = db.serviceDao()
private val providerName = TaskProvider.ProviderName.fromAuthority(authority)
override fun create(provider: ContentProviderClient, fromCollection: Collection): LocalTaskList? {
val service = serviceDao.get(fromCollection.serviceId) ?: throw IllegalArgumentException("Couldn't fetch DB service from collection")
val account = Account(service.accountName, context.getString(R.string.account_type))
logger.log(Level.INFO, "Adding local task list", fromCollection)
val uri = create(account, provider, providerName, fromCollection)
return DmfsTaskList.findByID(account, provider, providerName, LocalTaskList.Factory, ContentUris.parseId(uri))
}
private fun create(account: Account, provider: ContentProviderClient, providerName: TaskProvider.ProviderName, info: Collection): Uri {
// If the collection doesn't have a color, use a default color.
if (info.color != null)
info.color = Constants.DAVDROID_GREEN_RGBA
val values = valuesFromCollectionInfo(info, withColor = true)
values.put(TaskLists.OWNER, account.name)
values.put(TaskLists.SYNC_ENABLED, 1)
values.put(TaskLists.VISIBLE, 1)
return DmfsTaskList.Companion.create(account, provider, providerName, values)
}
private fun valuesFromCollectionInfo(info: Collection, withColor: Boolean): ContentValues {
val values = ContentValues(3)
values.put(TaskLists._SYNC_ID, info.url.toString())
values.put(TaskLists.LIST_NAME,
if (info.displayName.isNullOrBlank()) info.url.lastSegment else info.displayName)
if (withColor && info.color != null)
values.put(TaskLists.LIST_COLOR, info.color)
if (info.privWriteContent && !info.forceReadOnly)
values.put(TaskListColumns.ACCESS_LEVEL, TaskListColumns.ACCESS_LEVEL_OWNER)
else
values.put(TaskListColumns.ACCESS_LEVEL, TaskListColumns.ACCESS_LEVEL_READ)
return values
}
override fun getAll(account: Account, provider: ContentProviderClient) =
DmfsTaskList.find(account, LocalTaskList.Factory, provider, providerName, null, null)
override fun update(provider: ContentProviderClient, localCollection: LocalTaskList, fromCollection: Collection) {
logger.log(Level.FINE, "Updating local task list ${fromCollection.url}", fromCollection)
val accountSettings = accountSettingsFactory.create(localCollection.account)
localCollection.update(valuesFromCollectionInfo(fromCollection, withColor = accountSettings.getManageCalendarColors()))
}
override fun delete(localCollection: LocalTaskList) {
localCollection.delete()
}
}

View File

@@ -1,160 +0,0 @@
/*
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
*/
package at.bitfire.davdroid.resource.workaround
import android.content.ContentValues
import android.os.Build
import at.bitfire.davdroid.resource.LocalAddressBook
import at.bitfire.davdroid.resource.LocalContact
import at.bitfire.davdroid.resource.LocalContact.Companion.COLUMN_HASHCODE
import at.bitfire.vcard4android.BatchOperation
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import java.util.Optional
import java.util.logging.Level
import java.util.logging.Logger
import javax.inject.Inject
import javax.inject.Provider
/**
* Android 7.x introduced a new behavior in the Contacts provider: when metadata of a contact (like the "last contacted" time)
* changes, the contact is marked as "dirty" (i.e. the [android.provider.ContactsContract.RawContacts.DIRTY] flag is set).
* So, under Android 7.x, every time a user calls a contact or writes an SMS to a contact, the contact is marked as dirty.
*
* **This behavior is not present in Android ≤ 6.x nor in ≥ Android 8.x, where a contact is only marked as dirty
* when its data actually change.**
*
* So, as a dirty workaround for Android 7.x, we need to calculate a hash code from the contact data and group memberships every
* time we change the contact. When then a contact is marked as dirty, we compare the hash code of the current contact data with
* the previous hash code. If the hash code has changed, the contact is "really dirty" and we need to upload it. Otherwise,
* we reset the dirty flag to ignore the meta-data change.
*
* @constructor May only be called on Android 7.x, otherwise an [IllegalStateException] is thrown.
*/
class Android7DirtyVerifier @Inject constructor(
val logger: Logger
): ContactDirtyVerifier {
init {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
throw IllegalStateException("Android7DirtyVerifier must not be used on Android != 7.x")
}
// address-book level functions
override fun prepareAddressBook(addressBook: LocalAddressBook, isUpload: Boolean): Boolean {
val reallyDirty = verifyDirtyContacts(addressBook)
val deleted = addressBook.findDeleted().size
if (isUpload && reallyDirty == 0 && deleted == 0) {
logger.info("This sync was called to up-sync dirty/deleted contacts, but no contacts have been changed")
return false
}
return true
}
/**
* Queries all contacts with the [android.provider.ContactsContract.RawContacts.DIRTY] flag and checks whether their data
* checksum has changed, i.e. if they're "really dirty" (= data has changed, not only metadata, which is not hashed).
*
* The dirty flag is removed from contacts which are not "really dirty", i.e. from contacts whose contact data
* checksum has not changed.
*
* @return number of "really dirty" contacts
*/
private fun verifyDirtyContacts(addressBook: LocalAddressBook): Int {
var reallyDirty = 0
for (contact in addressBook.findDirtyContacts()) {
val lastHash = getLastHashCode(addressBook, contact)
val currentHash = contactDataHashCode(contact)
if (lastHash == currentHash) {
// hash is code still the same, contact is not "really dirty" (only metadata been have changed)
logger.log(Level.FINE, "Contact data hash has not changed, resetting dirty flag", contact)
contact.resetDirty()
} else {
logger.log(Level.FINE, "Contact data has changed from hash $lastHash to $currentHash", contact)
reallyDirty++
}
}
if (addressBook.includeGroups)
reallyDirty += addressBook.findDirtyGroups().size
return reallyDirty
}
private fun getLastHashCode(addressBook: LocalAddressBook, contact: LocalContact): Int {
addressBook.provider!!.query(contact.rawContactSyncURI(), arrayOf(COLUMN_HASHCODE), null, null, null)?.use { c ->
if (c.moveToNext() && !c.isNull(0))
return c.getInt(0)
}
return 0
}
// contact level functions
/**
* Calculates a hash code from the [at.bitfire.vcard4android.Contact] data and group memberships.
* Attention: re-reads {@link #contact} from the database, discarding all changes in memory!
*
* @return hash code of contact data (including group memberships)
*/
private fun contactDataHashCode(contact: LocalContact): Int {
contact.clearCachedContact()
// groupMemberships is filled by getContact()
val dataHash = contact.getContact().hashCode()
val groupHash = contact.groupMemberships.hashCode()
val combinedHash = dataHash xor groupHash
logger.log(Level.FINE, "Calculated data hash = $dataHash, group memberships hash = $groupHash → combined hash = $combinedHash", contact)
return combinedHash
}
override fun setHashCodeColumn(contact: LocalContact, toValues: ContentValues) {
val hashCode = contactDataHashCode(contact)
toValues.put(COLUMN_HASHCODE, hashCode)
}
override fun updateHashCode(addressBook: LocalAddressBook, contact: LocalContact) {
val values = ContentValues(1)
setHashCodeColumn(contact, values)
addressBook.provider!!.update(contact.rawContactSyncURI(), values, null, null)
}
override fun updateHashCode(contact: LocalContact, batch: BatchOperation) {
val hashCode = contactDataHashCode(contact)
batch.enqueue(BatchOperation.CpoBuilder
.newUpdate(contact.rawContactSyncURI())
.withValue(COLUMN_HASHCODE, hashCode))
}
// factory
@Module
@InstallIn(SingletonComponent::class)
object Android7DirtyVerifierModule {
/**
* Provides an [Android7DirtyVerifier] on Android 7.x, or an empty [Optional] on other versions.
*/
@Provides
fun provide(android7DirtyVerifier: Provider<Android7DirtyVerifier>): Optional<ContactDirtyVerifier> =
if (/* Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && */ Build.VERSION.SDK_INT < Build.VERSION_CODES.O)
Optional.of(android7DirtyVerifier.get())
else
Optional.empty()
}
}

View File

@@ -1,54 +0,0 @@
/*
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
*/
package at.bitfire.davdroid.resource.workaround
import android.content.ContentValues
import at.bitfire.davdroid.resource.LocalAddressBook
import at.bitfire.davdroid.resource.LocalContact
import at.bitfire.vcard4android.BatchOperation
/**
* Only required for [Android7DirtyVerifier]. If that class is removed because the minimum SDK is raised to Android 8,
* this interface and all calls to it can be removed as well.
*/
interface ContactDirtyVerifier {
// address-book level functions
/**
* Checks whether contacts which are marked as "dirty" are really dirty, i.e. their data has changed.
* If contacts are not really dirty (because only the metadata like "last contacted" changed), the "dirty" flag is removed.
*
* Intended to be called by [at.bitfire.davdroid.sync.ContactsSyncManager.prepare].
*
* @param addressBook the address book
* @param isUpload whether this sync is an upload
*
* @return `true` if the address book should be synced, `false` if the sync is an upload and no contacts have been changed
*/
fun prepareAddressBook(addressBook: LocalAddressBook, isUpload: Boolean): Boolean
// contact level functions
/**
* Sets the [LocalContact.COLUMN_HASHCODE] column in the given [ContentValues] to the hash code of the contact data.
*
* @param contact the contact to calculate the hash code for
* @param toValues set the hash code into these values
*/
fun setHashCodeColumn(contact: LocalContact, toValues: ContentValues)
/**
* Sets the [LocalContact.COLUMN_HASHCODE] field of the contact to the hash code of the contact data directly in the content provider.
*/
fun updateHashCode(addressBook: LocalAddressBook, contact: LocalContact)
/**
Sets the [LocalContact.COLUMN_HASHCODE] field of the contact to the hash code of the contact data in a content provider batch operation.
*/
fun updateHashCode(contact: LocalContact, batch: BatchOperation)
}

View File

@@ -24,8 +24,6 @@ import androidx.work.WorkerParameters
import at.bitfire.dav4jvm.exception.UnauthorizedException
import at.bitfire.dav4jvm.property.caldav.CalendarColor
import at.bitfire.dav4jvm.property.caldav.CalendarDescription
import at.bitfire.dav4jvm.property.caldav.CalendarTimezone
import at.bitfire.dav4jvm.property.caldav.CalendarTimezoneId
import at.bitfire.dav4jvm.property.caldav.Source
import at.bitfire.dav4jvm.property.caldav.SupportedCalendarComponentSet
import at.bitfire.dav4jvm.property.carddav.AddressbookDescription
@@ -87,14 +85,14 @@ class RefreshCollectionsWorker @AssistedInject constructor(
const val ARG_SERVICE_ID = "serviceId"
const val WORKER_TAG = "refreshCollectionsWorker"
// Collection properties to ask for in a propfind request to the CalDAV/CardDAV server
// Collection properties to ask for in a propfind request to the Cal- or CardDAV server
val DAV_COLLECTION_PROPERTIES = arrayOf(
ResourceType.NAME,
CurrentUserPrivilegeSet.NAME,
DisplayName.NAME,
Owner.NAME,
AddressbookDescription.NAME, SupportedAddressData.NAME,
CalendarDescription.NAME, CalendarColor.NAME, CalendarTimezone.NAME, CalendarTimezoneId.NAME, SupportedCalendarComponentSet.NAME,
CalendarDescription.NAME, CalendarColor.NAME, SupportedCalendarComponentSet.NAME,
Source.NAME,
// WebDAV Push
PushTransports.NAME,

View File

@@ -21,10 +21,10 @@ import androidx.work.WorkManager
import at.bitfire.davdroid.R
import at.bitfire.davdroid.db.AppDatabase
import at.bitfire.davdroid.db.Service
import at.bitfire.davdroid.repository.AccountRepository
import at.bitfire.davdroid.repository.DavCollectionRepository
import at.bitfire.davdroid.repository.DavServiceRepository
import at.bitfire.davdroid.resource.LocalAddressBook
import at.bitfire.davdroid.resource.LocalAddressBookStore
import at.bitfire.davdroid.resource.LocalTask
import at.bitfire.davdroid.sync.SyncUtils
import at.bitfire.davdroid.sync.TasksAppManager
@@ -52,9 +52,9 @@ class AccountSettingsMigrations @AssistedInject constructor(
@Assisted val account: Account,
@Assisted val accountSettings: AccountSettings,
@ApplicationContext val context: Context,
private val accountRepository: AccountRepository,
private val collectionRepository: DavCollectionRepository,
private val db: AppDatabase,
private val localAddressBookStore: LocalAddressBookStore,
private val localAddressBookFactory: LocalAddressBook.Factory,
private val logger: Logger,
private val serviceRepository: DavServiceRepository,
@@ -69,12 +69,8 @@ class AccountSettingsMigrations @AssistedInject constructor(
val accountManager: AccountManager = AccountManager.get(context)
/* IMPORTANT: No more migrations must be added without tests. Maybe https://github.com/bitfireAT/davx5-ose/issues/935
is done before adding new migrations. Then that PR should establish a way to define migration tests. Otherwise,
the next migration must add a test and therefore defines the method how run migration tests. */
/**
* With DAVx5 4.4.3 address book account names now contain the collection ID as a unique
* With DAVx5 4.3.3 address book account names now contain the collection ID as a unique
* identifier. We need to update the address book account names.
*/
@Suppress("unused","FunctionName")
@@ -99,11 +95,11 @@ class AccountSettingsMigrations @AssistedInject constructor(
for (oldAddressBookAccount in oldAddressBookAccounts) {
// Old address books only have a URL, so use it to determine the collection ID
logger.info("Migrating address book ${oldAddressBookAccount.name}")
val oldAddressBook = localAddressBookFactory.create(oldAddressBookAccount, provider)
val url = accountManager.getUserData(oldAddressBookAccount, LocalAddressBook.USER_DATA_URL)
collectionRepository.getByServiceAndUrl(service.id, url)?.let { collection ->
// Set collection ID and rename the account
localAddressBookStore.update(provider, oldAddressBook, collection)
val localAddressBook = localAddressBookFactory.create(oldAddressBookAccount, provider)
localAddressBook.update(collection)
}
}
}

View File

@@ -11,7 +11,8 @@ import at.bitfire.davdroid.db.Collection
import at.bitfire.davdroid.db.Service
import at.bitfire.davdroid.network.HttpClient
import at.bitfire.davdroid.resource.LocalAddressBook
import at.bitfire.davdroid.resource.LocalAddressBookStore
import at.bitfire.davdroid.settings.Settings
import at.bitfire.davdroid.settings.SettingsManager
import at.bitfire.davdroid.util.setAndVerifyUserData
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
@@ -25,16 +26,20 @@ class AddressBookSyncer @AssistedInject constructor(
@Assisted account: Account,
@Assisted extras: Array<String>,
@Assisted syncResult: SyncResult,
addressBookStore: LocalAddressBookStore,
private val contactsSyncManagerFactory: ContactsSyncManager.Factory
): Syncer<LocalAddressBookStore, LocalAddressBook>(account, extras, syncResult) {
private val contactsSyncManagerFactory: ContactsSyncManager.Factory,
settingsManager: SettingsManager
): Syncer<LocalAddressBook>(account, extras, syncResult) {
@AssistedFactory
interface Factory {
fun create(account: Account, extras: Array<String>, syncResult: SyncResult): AddressBookSyncer
}
override val dataStore = addressBookStore
companion object {
const val PREVIOUS_GROUP_METHOD = "previous_group_method"
}
private val forceAllReadOnly = settingsManager.getBoolean(Settings.FORCE_READ_ONLY_ADDRESSBOOKS)
override val serviceType: String
get() = Service.TYPE_CARDDAV
@@ -42,9 +47,31 @@ class AddressBookSyncer @AssistedInject constructor(
get() = ContactsContract.AUTHORITY // Address books use the contacts authority for sync
override fun getLocalCollections(provider: ContentProviderClient): List<LocalAddressBook> =
serviceRepository.getByAccountAndType(account.name, serviceType)?.let { service ->
// Get _all_ address books; Otherwise address book accounts of unchecked address books will not be removed
collectionRepository.getByService(service.id).mapNotNull { collection ->
LocalAddressBook.findByCollection(context, provider, collection.id)
}
}.orEmpty()
override fun getDbSyncCollections(serviceId: Long): List<Collection> =
collectionRepository.getByServiceAndSync(serviceId)
override fun update(localCollection: LocalAddressBook, remoteCollection: Collection) {
try {
logger.log(Level.FINE, "Updating local address book ${remoteCollection.url}", remoteCollection)
localCollection.update(remoteCollection, forceAllReadOnly)
} catch (e: Exception) {
logger.log(Level.WARNING, "Couldn't rename address book account", e)
}
}
override fun create(provider: ContentProviderClient, remoteCollection: Collection): LocalAddressBook {
logger.log(Level.INFO, "Adding local address book", remoteCollection)
return LocalAddressBook.create(context, provider, remoteCollection, forceAllReadOnly)
}
override fun syncCollection(provider: ContentProviderClient, localCollection: LocalAddressBook, remoteCollection: Collection) {
logger.info("Synchronizing address book: ${localCollection.addressBookAccount.name}")
syncAddressBook(
@@ -106,11 +133,4 @@ class AddressBookSyncer @AssistedInject constructor(
logger.info("Contacts sync complete")
}
companion object {
const val PREVIOUS_GROUP_METHOD = "previous_group_method"
}
}

View File

@@ -6,15 +6,16 @@ package at.bitfire.davdroid.sync
import android.accounts.Account
import android.content.ContentProviderClient
import android.content.ContentUris
import android.provider.CalendarContract
import at.bitfire.davdroid.db.Collection
import at.bitfire.davdroid.db.Service
import at.bitfire.davdroid.resource.LocalCalendar
import at.bitfire.davdroid.resource.LocalCalendarStore
import at.bitfire.ical4android.AndroidCalendar
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import java.util.logging.Level
/**
* Sync logic for calendars
@@ -23,23 +24,23 @@ class CalendarSyncer @AssistedInject constructor(
@Assisted account: Account,
@Assisted extras: Array<String>,
@Assisted syncResult: SyncResult,
calendarStore: LocalCalendarStore,
private val calendarSyncManagerFactory: CalendarSyncManager.Factory
): Syncer<LocalCalendarStore, LocalCalendar>(account, extras, syncResult) {
): Syncer<LocalCalendar>(account, extras, syncResult) {
@AssistedFactory
interface Factory {
fun create(account: Account, extras: Array<String>, syncResult: SyncResult): CalendarSyncer
}
override val dataStore = calendarStore
override val serviceType: String
get() = Service.TYPE_CALDAV
override val authority: String
get() = CalendarContract.AUTHORITY
override fun getLocalCollections(provider: ContentProviderClient): List<LocalCalendar>
= AndroidCalendar.find(account, provider, LocalCalendar.Factory, "${CalendarContract.Calendars.SYNC_EVENTS}!=0", null)
override fun prepare(provider: ContentProviderClient): Boolean {
// Update colors
if (accountSettings.getEventColors())
@@ -68,4 +69,15 @@ class CalendarSyncer @AssistedInject constructor(
syncManager.performSync()
}
override fun update(localCollection: LocalCalendar, remoteCollection: Collection) {
logger.log(Level.FINE, "Updating local calendar ${remoteCollection.url}", remoteCollection)
localCollection.update(remoteCollection, accountSettings.getManageCalendarColors())
}
override fun create(provider: ContentProviderClient, remoteCollection: Collection): LocalCalendar {
logger.log(Level.INFO, "Adding local calendar", remoteCollection)
val uri = LocalCalendar.create(account, provider, remoteCollection)
return AndroidCalendar.findByID(account, provider, LocalCalendar.Factory, ContentUris.parseId(uri))
}
}

View File

@@ -7,6 +7,7 @@ package at.bitfire.davdroid.sync
import android.accounts.Account
import android.content.ContentProviderClient
import android.content.ContentResolver
import android.os.Build
import android.text.format.Formatter
import at.bitfire.dav4jvm.DavAddressBook
import at.bitfire.dav4jvm.MultiResponseCallback
@@ -30,7 +31,6 @@ import at.bitfire.davdroid.resource.LocalAddressBook
import at.bitfire.davdroid.resource.LocalContact
import at.bitfire.davdroid.resource.LocalGroup
import at.bitfire.davdroid.resource.LocalResource
import at.bitfire.davdroid.resource.workaround.ContactDirtyVerifier
import at.bitfire.davdroid.settings.AccountSettings
import at.bitfire.davdroid.sync.groups.CategoriesStrategy
import at.bitfire.davdroid.sync.groups.VCard4Strategy
@@ -54,9 +54,7 @@ import java.io.ByteArrayOutputStream
import java.io.IOException
import java.io.Reader
import java.io.StringReader
import java.util.Optional
import java.util.logging.Level
import kotlin.jvm.optionals.getOrNull
/**
* Synchronization manager for CardDAV collections; handles contacts and groups.
@@ -102,8 +100,7 @@ class ContactsSyncManager @AssistedInject constructor(
@Assisted syncResult: SyncResult,
@Assisted val provider: ContentProviderClient,
@Assisted localAddressBook: LocalAddressBook,
@Assisted collection: Collection,
val dirtyVerifier: Optional<ContactDirtyVerifier>
@Assisted collection: Collection
): SyncManager<LocalAddress, LocalAddressBook, DavAddressBook>(
account,
accountSettings,
@@ -148,13 +145,18 @@ class ContactsSyncManager @AssistedInject constructor(
override fun prepare(): Boolean {
if (dirtyVerifier.isPresent) {
logger.info("Sync will verify dirty contacts (Android 7.x workaround)")
if (!dirtyVerifier.get().prepareAddressBook(localCollection, isUpload = extras.contains(ContentResolver.SYNC_EXTRAS_UPLOAD)))
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
// workaround for Android 7 which sets DIRTY flag when only meta-data is changed
val reallyDirty = localCollection.verifyDirty()
val deleted = localCollection.findDeleted().size
if (extras.contains(ContentResolver.SYNC_EXTRAS_UPLOAD) && reallyDirty == 0 && deleted == 0) {
logger.info("This sync was called to up-sync dirty/deleted contacts, but no contacts have been changed")
return false
}
}
davCollection = DavAddressBook(httpClient.okHttpClient, collection.url)
resourceDownloader = ResourceDownloader(davCollection.location)
logger.info("Contact group strategy: ${groupStrategy::class.java.simpleName}")
@@ -424,12 +426,9 @@ class ContactsSyncManager @AssistedInject constructor(
syncResult.stats.numInserts++
}
dirtyVerifier.getOrNull()?.let { verifier ->
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O)
// workaround for Android 7 which sets DIRTY flag when only meta-data is changed
(local as? LocalContact)?.let { localContact ->
verifier.updateHashCode(localCollection, localContact)
}
}
(local as? LocalContact)?.updateHashCode(null)
}
}

View File

@@ -7,15 +7,19 @@ package at.bitfire.davdroid.sync
import android.accounts.Account
import android.accounts.AccountManager
import android.content.ContentProviderClient
import android.content.ContentUris
import android.os.Build
import at.bitfire.davdroid.db.Collection
import at.bitfire.davdroid.db.Service
import at.bitfire.davdroid.repository.PrincipalRepository
import at.bitfire.davdroid.resource.LocalJtxCollection
import at.bitfire.davdroid.resource.LocalJtxCollectionStore
import at.bitfire.ical4android.JtxCollection
import at.bitfire.ical4android.TaskProvider
import at.techbee.jtx.JtxContract
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import java.util.logging.Level
/**
* Sync logic for jtx board
@@ -24,24 +28,25 @@ class JtxSyncer @AssistedInject constructor(
@Assisted account: Account,
@Assisted extras: Array<String>,
@Assisted syncResult: SyncResult,
localJtxCollectionStore: LocalJtxCollectionStore,
private val jtxSyncManagerFactory: JtxSyncManager.Factory,
private val principalRepository: PrincipalRepository,
private val tasksAppManager: dagger.Lazy<TasksAppManager>
): Syncer<LocalJtxCollectionStore, LocalJtxCollection>(account, extras, syncResult) {
): Syncer<LocalJtxCollection>(account, extras, syncResult) {
@AssistedFactory
interface Factory {
fun create(account: Account, extras: Array<String>, syncResult: SyncResult): JtxSyncer
}
override val dataStore = localJtxCollectionStore
override val serviceType: String
get() = Service.TYPE_CALDAV
override val authority: String
get() = TaskProvider.ProviderName.JtxBoard.authority
override fun getLocalCollections(provider: ContentProviderClient): List<LocalJtxCollection>
= JtxCollection.find(account, provider, context, LocalJtxCollection.Factory, null, null)
override fun prepare(provider: ContentProviderClient): Boolean {
// check whether jtx Board is new enough
try {
@@ -67,6 +72,26 @@ class JtxSyncer @AssistedInject constructor(
override fun getDbSyncCollections(serviceId: Long): List<Collection> =
collectionRepository.getSyncJtxCollections(serviceId)
override fun update(localCollection: LocalJtxCollection, remoteCollection: Collection) {
logger.log(Level.FINE, "Updating local jtx collection ${remoteCollection.url}", remoteCollection)
val owner = remoteCollection.ownerId?.let { principalRepository.get(it) }
localCollection.updateCollection(remoteCollection, owner, accountSettings.getManageCalendarColors())
}
override fun create(provider: ContentProviderClient, remoteCollection: Collection): LocalJtxCollection {
logger.log(Level.INFO, "Adding local jtx collection", remoteCollection)
val owner = remoteCollection.ownerId?.let { principalRepository.get(it) }
val uri = LocalJtxCollection.create(account, provider, remoteCollection, owner)
return JtxCollection.find(
account,
provider,
context,
LocalJtxCollection.Factory,
"${JtxContract.JtxCollection.ID} = ?",
arrayOf("${ContentUris.parseId(uri)}")
).first()
}
override fun syncCollection(provider: ContentProviderClient, localCollection: LocalJtxCollection, remoteCollection: Collection) {
logger.info("Synchronizing jtx collection $localCollection")

View File

@@ -56,7 +56,6 @@ import at.bitfire.ical4android.CalendarStorageException
import at.bitfire.ical4android.Ical4Android
import at.bitfire.ical4android.TaskProvider
import at.bitfire.vcard4android.ContactsStorageException
import com.google.common.base.Ascii
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
@@ -854,25 +853,21 @@ abstract class SyncManager<ResourceType: LocalResource<*>, out CollectionType: L
}
}
private fun buildDebugInfoIntent(e: Throwable, local: LocalResource<*>?, remote: HttpUrl?): Intent {
val builder = DebugInfoActivity.IntentBuilder(context)
private fun buildDebugInfoIntent(e: Throwable, local: LocalResource<*>?, remote: HttpUrl?) =
DebugInfoActivity.IntentBuilder(context)
.withAccount(account)
.withAuthority(authority)
.withCause(e)
if (local != null)
try {
// Truncate the string to avoid the Intent to be > 1 MB, which doesn't work (IPC limit)
builder.withLocalResource(Ascii.truncate(local.toString(), 10000, "[…]"))
} catch (_: OutOfMemoryError) {
// For instance because of a huge contact photo; maybe we're lucky and can catch it
}
if (remote != null)
builder.withRemoteResource(remote)
return builder.build()
}
.withLocalResource(
try {
local?.toString()
} catch (e: OutOfMemoryError) {
// for instance because of a huge contact photo; maybe we're lucky and can fetch it
null
}
)
.withRemoteResource(remote)
.build()
private fun buildViewItemAction(local: LocalResource<*>): NotificationCompat.Action? {
logger.log(Level.FINE, "Adding view action for local resource", local)

View File

@@ -15,7 +15,6 @@ import at.bitfire.davdroid.network.HttpClient
import at.bitfire.davdroid.repository.DavCollectionRepository
import at.bitfire.davdroid.repository.DavServiceRepository
import at.bitfire.davdroid.resource.LocalCollection
import at.bitfire.davdroid.resource.LocalDataStore
import at.bitfire.davdroid.settings.AccountSettings
import dagger.hilt.android.qualifiers.ApplicationContext
import okhttp3.HttpUrl
@@ -30,7 +29,7 @@ import javax.inject.Inject
*
* Contains generic sync code, equal for all sync authorities.
*/
abstract class Syncer<StoreType: LocalDataStore<CollectionType>, CollectionType: LocalCollection<*>>(
abstract class Syncer<CollectionType: LocalCollection<*>>(
protected val account: Account,
protected val extras: Array<String>,
protected val syncResult: SyncResult
@@ -59,8 +58,6 @@ abstract class Syncer<StoreType: LocalDataStore<CollectionType>, CollectionType:
}
abstract val dataStore: StoreType
@Inject
lateinit var accountSettingsFactory: AccountSettings.Factory
@@ -98,7 +95,7 @@ abstract class Syncer<StoreType: LocalDataStore<CollectionType>, CollectionType:
// Find collections in database and provider which should be synced (are sync-enabled)
val dbCollections = getSyncEnabledCollections()
val localCollections = dataStore.getAll(account, provider)
val localCollections = getLocalCollections(provider)
// Create/update/delete local collections according to DB
val updatedLocalCollections = updateCollections(provider, localCollections, dbCollections)
@@ -151,12 +148,12 @@ abstract class Syncer<StoreType: LocalDataStore<CollectionType>, CollectionType:
if (dbCollection == null) {
// Collection not available in db = on server (anymore), delete and remove from the updated list
logger.fine("Deleting local collection ${localCollection.title}")
dataStore.delete(localCollection)
localCollection.deleteCollection()
updatedLocalCollections -= localCollection
} else {
// Collection exists locally, update local collection and remove it from "to be created" map
logger.fine("Updating local collection ${localCollection.title} with $dbCollection")
dataStore.update(provider, localCollection, dbCollection)
update(localCollection, dbCollection)
newDbCollections -= dbCollection.url
}
}
@@ -186,10 +183,7 @@ abstract class Syncer<StoreType: LocalDataStore<CollectionType>, CollectionType:
provider: ContentProviderClient,
dbCollections: List<Collection>
): List<CollectionType> =
dbCollections.map { collection ->
dataStore.create(provider, collection)
?: throw IllegalStateException("Couldn't create local collection for $collection")
}
dbCollections.map { collection -> create(provider, collection) }
/**
* Synchronize the actual collection contents.
@@ -217,6 +211,17 @@ abstract class Syncer<StoreType: LocalDataStore<CollectionType>, CollectionType:
*/
open fun prepare(provider: ContentProviderClient): Boolean = true
/**
* Gets all local collections (not from the database, but from the content provider).
*
* [Syncer] will remove collections which are returned by this method, but not by
* [getDbSyncCollections], and add collections which are returned by [getDbSyncCollections], but not by this method.
*
* @param provider Content provider to access local collections
* @return Local collections to be updated
*/
abstract fun getLocalCollections(provider: ContentProviderClient): List<CollectionType>
/**
* Get the local database collections which are sync-enabled (should by synchronized).
*
@@ -228,6 +233,22 @@ abstract class Syncer<StoreType: LocalDataStore<CollectionType>, CollectionType:
*/
abstract fun getDbSyncCollections(serviceId: Long): List<Collection>
/**
* Updates an existing local collection (in the content provider) with remote collection information (from the DB).
*
* @param localCollection The local collection to be updated
* @param remoteCollection The new remote collection information
*/
abstract fun update(localCollection: CollectionType, remoteCollection: Collection)
/**
* Creates a new local collection (in the content provider) from remote collection information (from the DB).
*
* @param provider The content provider client to create the local collection
* @param remoteCollection The remote collection to be created locally
*/
abstract fun create(provider: ContentProviderClient, remoteCollection: Collection): CollectionType
/**
* Synchronizes local with remote collection contents.
*

View File

@@ -7,15 +7,18 @@ package at.bitfire.davdroid.sync
import android.accounts.Account
import android.accounts.AccountManager
import android.content.ContentProviderClient
import android.content.ContentUris
import android.os.Build
import at.bitfire.davdroid.db.Collection
import at.bitfire.davdroid.db.Service
import at.bitfire.davdroid.resource.LocalTaskList
import at.bitfire.davdroid.resource.LocalTaskListStore
import at.bitfire.ical4android.DmfsTaskList
import at.bitfire.ical4android.TaskProvider
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import org.dmfs.tasks.contract.TaskContract.TaskLists
import java.util.logging.Level
/**
* Sync logic for tasks in CalDAV collections ({@code VTODO}).
@@ -25,10 +28,9 @@ class TaskSyncer @AssistedInject constructor(
@Assisted override val authority: String,
@Assisted extras: Array<String>,
@Assisted syncResult: SyncResult,
private val localTaskListStoreFactory: LocalTaskListStore.Factory,
private val tasksAppManager: dagger.Lazy<TasksAppManager>,
private val tasksSyncManagerFactory: TasksSyncManager.Factory,
): Syncer<LocalTaskListStore, LocalTaskList>(account, extras, syncResult) {
): Syncer<LocalTaskList>(account, extras, syncResult) {
@AssistedFactory
interface Factory {
@@ -37,11 +39,11 @@ class TaskSyncer @AssistedInject constructor(
private val providerName = TaskProvider.ProviderName.fromAuthority(authority)
override val dataStore = localTaskListStoreFactory.create(authority)
override val serviceType: String
get() = Service.TYPE_CALDAV
override fun getLocalCollections(provider: ContentProviderClient): List<LocalTaskList>
= DmfsTaskList.find(account, LocalTaskList.Factory, provider, providerName, "${TaskLists.SYNC_ENABLED}!=0", null)
override fun prepare(provider: ContentProviderClient): Boolean {
// Don't sync if task provider is too old
@@ -68,6 +70,17 @@ class TaskSyncer @AssistedInject constructor(
override fun getDbSyncCollections(serviceId: Long): List<Collection> =
collectionRepository.getSyncTaskLists(serviceId)
override fun update(localCollection: LocalTaskList, remoteCollection: Collection) {
logger.log(Level.FINE, "Updating local task list ${remoteCollection.url}", remoteCollection)
localCollection.update(remoteCollection, accountSettings.getManageCalendarColors())
}
override fun create(provider: ContentProviderClient, remoteCollection: Collection): LocalTaskList {
logger.log(Level.INFO, "Adding local task list", remoteCollection)
val uri = LocalTaskList.create(account, provider, providerName, remoteCollection)
return DmfsTaskList.findByID(account, provider, providerName, LocalTaskList.Factory, ContentUris.parseId(uri))
}
override fun syncCollection(provider: ContentProviderClient, localCollection: LocalTaskList, remoteCollection: Collection) {
logger.info("Synchronizing task list #${localCollection.id} [${localCollection.syncId}]")

View File

@@ -16,7 +16,6 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
@@ -249,7 +248,6 @@ fun MenuEntry_Preview() {
fun BrandingHeader() {
Column(
Modifier
.statusBarsPadding()
.background(Color.DarkGray)
.fillMaxWidth()
.padding(16.dp)

View File

@@ -11,7 +11,6 @@ import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
@@ -155,9 +154,7 @@ fun AccountsScreen(
ModalNavigationDrawer(
drawerState = drawerState,
drawerContent = {
ModalDrawerSheet(
windowInsets = WindowInsets(0.dp)
) {
ModalDrawerSheet {
accountsDrawerHandler.AccountsDrawer(
snackbarHostState = snackbarHostState,
onCloseDrawer = {

View File

@@ -4,9 +4,7 @@
package at.bitfire.davdroid.ui
import androidx.activity.SystemBarStyle
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import android.app.Activity
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
@@ -16,6 +14,7 @@ import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.platform.LocalView
import androidx.core.view.WindowCompat
import at.bitfire.davdroid.ui.composable.SafeAndroidUriHandler
@Composable
@@ -23,26 +22,24 @@ fun AppTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
content: @Composable () -> Unit
) {
val colorScheme = if (!darkTheme)
M3ColorScheme.lightScheme
else
M3ColorScheme.darkScheme
val view = LocalView.current
SideEffect {
// If applicable, call Activity.enableEdgeToEdge to enable edge-to-edge layout on Android <15, too.
// When we have moved everything into one Activity with Compose navigation, we can call it there instead.
(view.context as? AppCompatActivity)?.enableEdgeToEdge(
navigationBarStyle = SystemBarStyle.auto(
lightScrim = M3ColorScheme.lightScheme.scrim.toArgb(),
darkScrim = M3ColorScheme.darkScheme.scrim.toArgb()
) { darkTheme }
)
if (!view.isInEditMode) {
SideEffect {
val window = (view.context as Activity).window
window.statusBarColor = colorScheme.primary.toArgb()
WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme
}
}
// Apply SafeAndroidUriHandler to the composition
val uriHandler = SafeAndroidUriHandler(LocalContext.current)
CompositionLocalProvider(LocalUriHandler provides uriHandler) {
MaterialTheme(
colorScheme = if (!darkTheme)
M3ColorScheme.lightScheme
else
M3ColorScheme.darkScheme,
colorScheme = colorScheme,
content = content
)
}

View File

@@ -97,7 +97,6 @@ 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
@@ -122,7 +121,6 @@ fun CollectionScreen(
lastSynced: List<DavSyncStatsRepository.LastSynced> = emptyList(),
supportsWebPush: Boolean = false,
pushSubscriptionCreated: Long? = null,
pushSubscriptionExpires: Long? = null,
url: String,
onDelete: () -> Unit = {},
onNavUp: () -> Unit = {}
@@ -251,13 +249,10 @@ fun CollectionScreen(
if (supportsWebPush) {
val text =
if (pushSubscriptionCreated != null && pushSubscriptionExpires != null) {
if (pushSubscriptionCreated != null) {
val formatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.SHORT).withZone(ZoneId.systemDefault())
stringResource(
R.string.collection_push_subscribed_at,
formatter.format(Instant.ofEpochSecond(pushSubscriptionCreated)),
formatter.format(Instant.ofEpochSecond(pushSubscriptionExpires))
)
val time = Instant.ofEpochMilli(pushSubscriptionCreated)
stringResource(R.string.collection_push_subscribed_at, formatter.format(time))
} else
stringResource(R.string.collection_push_web_push)
CollectionScreen_Entry(
@@ -365,9 +360,7 @@ fun CollectionScreen_Preview() {
lastSynced = 1234567890
)
),
supportsWebPush = true,
pushSubscriptionCreated = 1731846565,
pushSubscriptionExpires = 1731847565
supportsWebPush = true
)
}

View File

@@ -66,7 +66,7 @@ fun Assistant(
@Composable
@Preview
fun Assistant_Preview_InScaffold() {
fun Assistant_Preview() {
Assistant(nextLabel = "Next") {
Text("Some Content")
}

View File

@@ -19,9 +19,9 @@ import javax.inject.Inject
class BatteryOptimizationsPage @Inject constructor(
private val application: Application,
private val settingsManager: SettingsManager
): IntroPage() {
): IntroPage {
override fun getShowPolicy(): ShowPolicy {
override fun getShowPolicy(): IntroPage.ShowPolicy {
// show fragment when:
// 1. DAVx5 is not whitelisted yet and "don't show anymore" has not been clicked, and/or
// 2a. evil manufacturer AND
@@ -30,9 +30,9 @@ class BatteryOptimizationsPage @Inject constructor(
(!BatteryOptimizationsPageModel.isExempted(application) && settingsManager.getBooleanOrNull(HINT_BATTERY_OPTIMIZATIONS) != false) ||
(BatteryOptimizationsPageModel.manufacturerWarning && settingsManager.getBooleanOrNull(HINT_AUTOSTART_PERMISSION) != false)
)
ShowPolicy.SHOW_ALWAYS
IntroPage.ShowPolicy.SHOW_ALWAYS
else
ShowPolicy.DONT_SHOW
IntroPage.ShowPolicy.DONT_SHOW
}
@Composable

View File

@@ -6,8 +6,10 @@ import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
@@ -201,8 +203,11 @@ fun BatteryOptimizationsPageContent(
stringResource(R.string.app_settings_reset_hints)
),
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)
modifier = Modifier
.padding(top = 8.dp)
.padding(horizontal = 16.dp)
)
Spacer(modifier = Modifier.height(90.dp))
}
}

View File

@@ -13,6 +13,7 @@ import androidx.activity.compose.setContent
import androidx.activity.result.contract.ActivityResultContract
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.runtime.rememberCoroutineScope
import at.bitfire.davdroid.ui.AppTheme
@@ -20,6 +21,7 @@ import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
@AndroidEntryPoint
@OptIn(ExperimentalFoundationApi::class)
class IntroActivity : AppCompatActivity() {
val model by viewModels<IntroModel>()

View File

@@ -6,7 +6,7 @@ package at.bitfire.davdroid.ui.intro
import androidx.compose.runtime.Composable
abstract class IntroPage {
interface IntroPage {
enum class ShowPolicy {
DONT_SHOW,
@@ -14,14 +14,6 @@ abstract class IntroPage {
SHOW_ONLY_WITH_OTHERS
}
/**
* Whether insets are handled by [ComposePage].
*
* If `true`, [ComposePage] must add top/side insets for edge-to-edge layout itself. Bottom insets are handled by the bottom bar.
* If `false`, [IntroScreen] will apply all insets to give [ComposePage] a safe content area.
*/
open val customTopInsets: Boolean = false
/**
* Used to determine whether an intro page of this type (for instance,
* the [BatteryOptimizationsPage]) should be shown.
@@ -32,12 +24,12 @@ abstract class IntroPage {
* * [DONT_SHOW] (0): don't show the page
* * ≥ 0: show the page (lower numbers are shown first)
*/
abstract fun getShowPolicy(): ShowPolicy
fun getShowPolicy(): ShowPolicy
/**
* Composes this page. Will only be called when [getShowPolicy] is not [DONT_SHOW].
*/
@Composable
abstract fun ComposePage()
fun ComposePage()
}

View File

@@ -3,23 +3,19 @@ package at.bitfire.davdroid.ui.intro
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeDrawing
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.PagerState
import androidx.compose.foundation.pager.rememberPagerState
@@ -31,7 +27,6 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.contentColorFor
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
@@ -55,6 +50,7 @@ import at.bitfire.davdroid.ui.M3ColorScheme
import kotlinx.coroutines.launch
@Composable
@OptIn(ExperimentalFoundationApi::class)
fun IntroScreen(
pages: List<IntroPage>,
pagerState: PagerState = rememberPagerState { pages.size },
@@ -62,15 +58,20 @@ fun IntroScreen(
) {
val scope = rememberCoroutineScope()
Scaffold(
bottomBar = {
Scaffold { paddingValues ->
Column(modifier = Modifier.fillMaxSize().padding(paddingValues)) {
HorizontalPager(
state = pagerState,
modifier = Modifier
.fillMaxWidth()
.weight(1f)
) { pages[it].ComposePage() }
Box(
modifier = Modifier
.fillMaxWidth()
.background(M3ColorScheme.primaryLight)
// consume bottom and side insets of safe drawing area, like BottomAppBar
.windowInsetsPadding(WindowInsets.safeDrawing.only(WindowInsetsSides.Horizontal + WindowInsetsSides.Bottom))
.height(90.dp)
.background(M3ColorScheme.primaryLight)
) {
PositionIndicator(
index = pagerState.currentPage,
@@ -104,41 +105,22 @@ fun IntroScreen(
}
}
}
},
contentWindowInsets = WindowInsets(0.dp)
) { paddingValues ->
Column(modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
) {
HorizontalPager(state = pagerState) { idxPage ->
val page = pages[idxPage]
Box(
modifier = if (page.customTopInsets)
Modifier // ComposePage() handles insets itself
else
// consume top and horizontal sides of safe drawing padding (like TopAppBar)
// bottom is handled by the bottom bar
Modifier.windowInsetsPadding(WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal))
) {
page.ComposePage()
}
}
}
}
}
@Preview(
showSystemUi = true,
showBackground = true
showSystemUi = true
)
@Composable
@OptIn(ExperimentalFoundationApi::class)
fun IntroScreen_Preview() {
AppTheme {
IntroScreen(
listOf(
object : IntroPage() {
override fun getShowPolicy(): ShowPolicy = ShowPolicy.SHOW_ALWAYS
object : IntroPage {
override fun getShowPolicy(): IntroPage.ShowPolicy =
IntroPage.ShowPolicy.SHOW_ALWAYS
@Composable
override fun ComposePage() {
@@ -146,13 +128,12 @@ fun IntroScreen_Preview() {
modifier = Modifier
.fillMaxSize()
.background(MaterialTheme.colorScheme.surface)
) {
Text("Some Text")
}
)
}
},
object : IntroPage() {
override fun getShowPolicy(): ShowPolicy = ShowPolicy.SHOW_ALWAYS
object : IntroPage {
override fun getShowPolicy(): IntroPage.ShowPolicy =
IntroPage.ShowPolicy.SHOW_ALWAYS
@Composable
override fun ComposePage() {
@@ -160,9 +141,7 @@ fun IntroScreen_Preview() {
modifier = Modifier
.fillMaxSize()
.background(MaterialTheme.colorScheme.primary)
) {
Text("Some Text")
}
)
}
}
),

View File

@@ -7,8 +7,10 @@ package at.bitfire.davdroid.ui.intro
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
@@ -39,13 +41,13 @@ import javax.inject.Inject
class OpenSourcePage @Inject constructor(
private val settingsManager: SettingsManager
): IntroPage() {
): IntroPage {
override fun getShowPolicy(): ShowPolicy {
override fun getShowPolicy(): IntroPage.ShowPolicy {
return if (System.currentTimeMillis() > (settingsManager.getLongOrNull(Model.SETTING_NEXT_DONATION_POPUP) ?: 0))
ShowPolicy.SHOW_ALWAYS
IntroPage.ShowPolicy.SHOW_ALWAYS
else
ShowPolicy.DONT_SHOW
IntroPage.ShowPolicy.DONT_SHOW
}
@Composable
@@ -140,5 +142,6 @@ fun OpenSourcePage(
)
}
}
Spacer(Modifier.height(90.dp))
}
}

View File

@@ -16,20 +16,20 @@ import javax.inject.Inject
class PermissionsIntroPage @Inject constructor(
private val application: Application
): IntroPage() {
): IntroPage {
var model: PermissionsModel? = null
override fun getShowPolicy(): ShowPolicy {
override fun getShowPolicy(): IntroPage.ShowPolicy {
// show PermissionsFragment as intro fragment when no permissions are granted
val permissions = CONTACT_PERMISSIONS + CALENDAR_PERMISSIONS +
TaskProvider.PERMISSIONS_JTX +
TaskProvider.PERMISSIONS_OPENTASKS +
TaskProvider.PERMISSIONS_TASKS_ORG
return if (PermissionUtils.haveAnyPermission(application, permissions))
ShowPolicy.DONT_SHOW
IntroPage.ShowPolicy.DONT_SHOW
else
ShowPolicy.SHOW_ALWAYS
IntroPage.ShowPolicy.SHOW_ALWAYS
}
@Composable

View File

@@ -16,13 +16,13 @@ class TasksIntroPage @Inject constructor(
private val application: Application,
private val settingsManager: SettingsManager,
private val tasksAppManager: TasksAppManager
): IntroPage() {
): IntroPage {
override fun getShowPolicy(): ShowPolicy {
override fun getShowPolicy(): IntroPage.ShowPolicy {
return if (tasksAppManager.currentProvider() != null || settingsManager.getBooleanOrNull(TasksModel.HINT_OPENTASKS_NOT_INSTALLED) == false)
ShowPolicy.DONT_SHOW
IntroPage.ShowPolicy.DONT_SHOW
else
ShowPolicy.SHOW_ALWAYS
IntroPage.ShowPolicy.SHOW_ALWAYS
}
@Composable

View File

@@ -14,7 +14,6 @@ import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeContentPadding
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
@@ -33,11 +32,9 @@ import at.bitfire.davdroid.R
import at.bitfire.davdroid.ui.AppTheme
import at.bitfire.davdroid.ui.M3ColorScheme
class WelcomePage: IntroPage() {
class WelcomePage: IntroPage {
override val customTopInsets: Boolean = true
override fun getShowPolicy() = ShowPolicy.SHOW_ONLY_WITH_OTHERS
override fun getShowPolicy() = IntroPage.ShowPolicy.SHOW_ONLY_WITH_OTHERS
@Composable
override fun ComposePage() {
@@ -53,8 +50,7 @@ class WelcomePage: IntroPage() {
Column(
modifier = Modifier
.fillMaxSize()
.background(color = M3ColorScheme.primaryLight) // fill background color edge-to-edge
.safeContentPadding()
.background(color = M3ColorScheme.primaryLight),
) {
Image(
painter = painterResource(R.drawable.ic_launcher_foreground),
@@ -126,8 +122,7 @@ class WelcomePage: IntroPage() {
Row(
modifier = Modifier
.fillMaxSize()
.background(color = MaterialTheme.colorScheme.primary)
.safeContentPadding(),
.background(color = MaterialTheme.colorScheme.primary),
verticalAlignment = Alignment.CenterVertically
) {
Image(

View File

@@ -178,7 +178,7 @@ fun WebdavMountsScreen(
verticalArrangement = Arrangement.spacedBy(12.dp),
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 8.dp)
.padding(8.dp)
) {
items(mountInfos, key = { it.mount.id }, contentType = { "mount" }) {
WebdavMountsItem(

View File

@@ -17,13 +17,10 @@ import java.util.logging.Logger
*/
fun AccountManager.setAndVerifyUserData(account: Account, key: String, value: String?) {
for (i in 1..10) {
if (getUserData(account, key) == value)
/* already set / success */
return
setUserData(account, key, value)
if (getUserData(account, key) == value)
return /* success */
// wait a bit because AccountManager access sometimes seems a bit asynchronous
Thread.sleep(100)
}
Logger.getGlobal().warning("AccountManager failed to set $account user data $key := $value")

View File

@@ -54,6 +54,7 @@
<!--AboutActivity-->
<string name="about_libraries">المكتبات</string>
<string name="about_version">النسخة %1$s (%2$d)</string>
<string name="about_build_date">التجميع على %s</string>
<string name="about_license_info_no_warranty">يقدَّم هذا البرنامج دون أدنى مسؤولية. إنه برنامج حر، وندعوك لإعادة توزيعه حسب أحكام محددة.</string>
<!--global settings-->
<string name="logging_couldnt_create_file">لا يمكن إنشاء ملف سجل</string>
@@ -67,6 +68,7 @@
<string name="navigation_drawer_website">موقع الويب</string>
<string name="navigation_drawer_manual">دليل الاستخدام</string>
<string name="navigation_drawer_faq">الأسئلة الشائعة</string>
<string name="account_list_empty">مرحباً بك في DAVx⁵ !\n\n يمكنك إضافة حساب CalDAV أو CardDAV الآن.</string>
<!--RefreshCollectionsWorker-->
<string name="refresh_collections_worker_refresh_failed">فشل اكتشاف الخدمة</string>
<string name="refresh_collections_worker_refresh_couldnt_refresh">لم يتمكن التطبيق من تجديد قائمة المجموعة</string>
@@ -75,7 +77,9 @@
<string name="app_settings">الإعدادات</string>
<string name="app_settings_debug">تصحيح العلل</string>
<string name="app_settings_show_debug_info">عرض معلومات التصحيح</string>
<string name="app_settings_show_debug_info_details">عرض/مشاركة تفاصيل البرنامج والضبط</string>
<string name="app_settings_logging">التسجيل المفصّل</string>
<string name="app_settings_logging_on">التسجيل مفعَّل</string>
<string name="app_settings_logging_off">التسجيل معطَّل</string>
<string name="app_settings_connection">الاتصال</string>
<string name="app_settings_security">الأمن</string>
@@ -119,12 +123,13 @@
<string name="login_user_name">اسم المستخدم</string>
<string name="login_base_url">URL الأساس</string>
<string name="login_select_certificate">اختيار الشهادة</string>
<string name="login_add_account">إضافة حساب</string>
<string name="login_create_account">إنشاء حساب</string>
<string name="login_account_name">اسم الحساب</string>
<string name="login_account_name_info">استخدم عنوان بريدك الإلكتروني اسماً للحساب لأن آندرويد يستخدم اسم الحساب في حقل المنظّم ORGANIZER للأحداث التي تنشئها. لايمكن أن تمتلك حسابين بالاسم نفسه.</string>
<string name="login_account_contact_group_method">طريقة مجموعة جهة الاتصال:</string>
<string name="login_account_name_required">اسم الحساب مطلوب</string>
<string name="login_account_name_already_taken">اسم الحساب مأخوذ بالفعل</string>
<string name="login_account_not_created">لم نتمكن من إنشاء الحساب</string>
<string name="login_configuration_detection">اكتشاف الضبط</string>
<string name="login_querying_server">يجري استعلام الخادم … يرجى الانتظار</string>
<string name="login_no_service">لم نجِد خدمة CalDAV أو CardDAV.</string>

View File

@@ -45,6 +45,7 @@
<string name="intro_tasks_opentasks">OpenTasks</string>
<string name="intro_tasks_opentasks_info">Изглежда не се разработва вече, не се препоръчва.</string>
<string name="intro_tasks_tasks_org">Tasks.org</string>
<string name="intro_tasks_tasks_org_info"><![CDATA[Някои възможности <a href="https://www.davx5.com/faq/tasks/advanced-task-features">(още) не се поддържат</a>.]]></string>
<string name="intro_tasks_no_app_store">Няма достъпен магазин за приложения</string>
<string name="intro_tasks_dont_show">Не се нуждая от поддръжка на задачи.*</string>
<string name="intro_open_source_title">Приложение с отворен код</string>
@@ -87,6 +88,7 @@
<string name="wifi_permissions_background_location_permission_label">Разрешаване във всички случаи</string>
<string name="wifi_permissions_background_location_permission_on">Разрешението за местоположението е зададено на: %s</string>
<string name="wifi_permissions_background_location_permission_off">Разрешението за местоположението не е зададено на: %s</string>
<string name="wifi_permissions_background_location_disclaimer">%s използва разрешението за местоположение за достъп до името на мрежата, когато се изисква синхронизиране само през определена мрежа. Това се случва дори и когато приложението се изпълнява във фонов режим. Не се събират, съхраняват, обработват и разпространяват данни за местоположението.</string>
<string name="wifi_permissions_location_enabled">Винаги включено местоположение</string>
<string name="wifi_permissions_location_enabled_on">Услугата за местоположението е включена</string>
<string name="wifi_permissions_location_enabled_off">Услугата за местоположението е изключена</string>
@@ -94,6 +96,7 @@
<string name="about_translations">Преводи</string>
<string name="about_libraries">Библиотеки</string>
<string name="about_version">Издание %1$s (%2$d)</string>
<string name="about_build_date">Компилирано на %s</string>
<string name="about_copyright">© Ricki Hirner, Bernhard Stockmann (bitfire web engineering GmbH) и сътрудници</string>
<string name="about_license_info_no_warranty">Тази програма се предлага с АБСОЛЮТНО НИКАКВА ГАРАНЦИЯ. Тя е свободен софтуер и можете да я разпространявате при определени условия.</string>
<!--global settings-->
@@ -126,6 +129,7 @@
<string name="account_list_manage_battery_saver">Икономия на батерия</string>
<string name="account_list_low_storage">Пространството за съхранение е малко. Андроид няма да синхронизира местните промени веднага, а по време на следващата редовна синхронизация.</string>
<string name="account_list_manage_storage">Управление на хранилището</string>
<string name="account_list_empty">Добре дошли при DAVx⁵!\n\nМожете да добавите регистрация за CalDAV/CardDAV.</string>
<string name="accounts_sync_all">Синхронизиране на всички профили</string>
<!--RefreshCollectionsWorker-->
<string name="refresh_collections_worker_refresh_failed">Грешка при откриване на услугите</string>
@@ -137,7 +141,9 @@
<string name="app_settings">Настройки</string>
<string name="app_settings_debug">Отстраняване на дефекти</string>
<string name="app_settings_show_debug_info">Информация за отстраняване на дефекти</string>
<string name="app_settings_show_debug_info_details">Преглед/споделяне на информация за приложението и настройките</string>
<string name="app_settings_logging">Подробен дневник</string>
<string name="app_settings_logging_on">Дневникът е включен</string>
<string name="app_settings_logging_off">Дневникът е изключен</string>
<string name="app_settings_battery_optimization">Оптимизиране на батерията</string>
<string name="app_settings_battery_optimization_exempted">Приложението е в белия списък (препоръчително)</string>
@@ -224,13 +230,14 @@
<string name="login_base_url">Основен адрес</string>
<string name="login_base_url_info"><![CDATA[Основата на адресът ще бъде директно проверен, но <a href="%s">услугите също могат да бъдат открити</a>чрез записи в DNS и добре познати адреси.]]></string>
<string name="login_select_certificate">Избор на сертификат</string>
<string name="login_add_account">Нова регистрация</string>
<string name="login_create_account">Създаване на регистрация</string>
<string name="login_account_name">Име на регистрация</string>
<string name="login_account_avoid_apostrophe">Използването на апострофи (\') изглежда води до проблеми при някои устройства.</string>
<string name="login_account_name_info">Използвайте адрес за електронна поща вместо име, защото Android ще го използва като адрес на организатора на събитията, които създавате. Не може да има две регистрации с еднакво име.</string>
<string name="login_account_contact_group_method">Метод за съхранение на групи от контакти:</string>
<string name="login_account_name_required">Изисква се име на регистрацията</string>
<string name="login_account_name_already_taken">Това име на регистрация вече се използва</string>
<string name="login_account_not_created">Регистрацията не е създадена</string>
<string name="login_type_advanced">Вход за напреднали</string>
<string name="login_no_client_certificate_optional">Липсва клиентски сертификат*</string>
<string name="login_client_certificate_selected">Клиентски сертификат: %s</string>

View File

@@ -45,13 +45,13 @@
<string name="intro_tasks_opentasks">OpenTasks</string>
<string name="intro_tasks_opentasks_info">Sembla que ja no es desenvolupa, no es recomana.</string>
<string name="intro_tasks_tasks_org">Tasks.org</string>
<string name="intro_tasks_tasks_org_info"><![CDATA[Algunes funcions <a href="https://www.davx5.com/faq/tasks/advanced-task-features">no estan disponibles</a> (encara).]]></string>
<string name="intro_tasks_no_app_store">No hi ha cap mercat d\'aplicacions disponible</string>
<string name="intro_tasks_dont_show">No necessito suport per les tasques.*</string>
<string name="intro_open_source_title">Programari de codi obert</string>
<string name="intro_open_source_text">Ens alegra saber que utilitzes %s, que és programari de codi obert. El desenvolupament, manteniment i suport requereixen un gran treball. Si us plau, considera contribuir (hi ha moltes formes) o realitzar una donació. Seria molt apreciat!</string>
<string name="intro_open_source_details">Com contribuir/donar</string>
<string name="intro_open_source_dont_show">No mostrar en el futur pròxim</string>
<string name="intro_next">Següent</string>
<!--PermissionsActivity-->
<string name="permissions_title">Permisos</string>
<string name="permissions_text">%s requereix permisos per a funcionar correctament.</string>
@@ -88,6 +88,7 @@
<string name="wifi_permissions_background_location_permission_label">Permetre tota l\'estona</string>
<string name="wifi_permissions_background_location_permission_on">El permís d\'ubicació s\'ha definit a: %s</string>
<string name="wifi_permissions_background_location_permission_off">El permís d\'ubicació no s\'ha definit a: %s</string>
<string name="wifi_permissions_background_location_disclaimer">%s utilitza el permís de localització només per a determinar l\'SSID de la Wi-Fi actual per als comptes restringits de SSID. Això ocorrerà fins i tot quan l\'aplicació estigui en segon pla. No es recullen, s\'emmagatzemen, es processen ni s\'envien a cap lloc.</string>
<string name="wifi_permissions_location_enabled">Localització sempre habilitada</string>
<string name="wifi_permissions_location_enabled_on">El servei de localització està habilitat</string>
<string name="wifi_permissions_location_enabled_off">Servei de localització deshabilitat</string>
@@ -95,6 +96,7 @@
<string name="about_translations">Traduccions</string>
<string name="about_libraries">Biblioteques</string>
<string name="about_version">Versió %1$s (%2$d)</string>
<string name="about_build_date">Compilat el %s</string>
<string name="about_copyright">© Ricki Hirner, Bernhard Stockmann (bitfire web engineering GmbH) i col·laboradors</string>
<string name="about_license_info_no_warranty">Aquest programa és distribuït sense CAP MENA DE GARANTIA. És programari lliure, i està permesa la seva redistribució segons certes condicions.</string>
<!--global settings-->
@@ -127,6 +129,7 @@
<string name="account_list_manage_battery_saver">Gestiona l\'estalvi de bateria</string>
<string name="account_list_low_storage">Espai d\'emmagatzematge baix. L\'Android no sincronitzarà els canvis locals immediatament, sinó durant la pròxima sincronització normal.</string>
<string name="account_list_manage_storage">Gestiona l\'emmagatzematge</string>
<string name="account_list_empty">Us donem la benvinguda a DAVx⁵! \n\nAra podeu afegir un compte CalDAV / CardDAV.</string>
<string name="accounts_sync_all">Sincronitza tots els comptes</string>
<!--RefreshCollectionsWorker-->
<string name="refresh_collections_worker_refresh_failed">Ha fallat la detecció del servei</string>
@@ -138,7 +141,9 @@
<string name="app_settings">Configuració</string>
<string name="app_settings_debug">Depurador</string>
<string name="app_settings_show_debug_info">Informació de depuració</string>
<string name="app_settings_show_debug_info_details">Mostra/comparteix detalls del programari i de la configuració</string>
<string name="app_settings_logging">Registre detallat</string>
<string name="app_settings_logging_on">Registre actiu</string>
<string name="app_settings_logging_off">Registre inactiu</string>
<string name="app_settings_battery_optimization">Optimització de la bateria</string>
<string name="app_settings_battery_optimization_exempted">L\'aplicació està exempta (recomanat)</string>
@@ -225,13 +230,14 @@
<string name="login_base_url">URL base</string>
<string name="login_base_url_info"><![CDATA[L\'URL base es comprovarà directament, però els <a href="%s">serveis també es descobreixen</a> utilitzant els registres de DNS i els URL ben coneguts.]]></string>
<string name="login_select_certificate">Selecciona el certificat</string>
<string name="login_add_account">Afegeix un compte</string>
<string name="login_create_account">Crea un compte</string>
<string name="login_account_name">Nom del compte</string>
<string name="login_account_avoid_apostrophe">L\'ús d\'apòstrofs (\') sembla que provoca problemes en alguns dispositius.</string>
<string name="login_account_name_info">Utilitzeu la vostra adreça de correu electrònic com a nom del compte perquè l\'Android utilitzarà el nom del compte com a camp ORGANITZADOR per als esdeveniments que creeu. No poden haver-hi dos comptes amb el mateix nom.</string>
<string name="login_account_contact_group_method">Mètode dels grups de contactes:</string>
<string name="login_account_name_required">Nom del compte obligatori</string>
<string name="login_account_name_already_taken">Nom de compte existent</string>
<string name="login_account_not_created">No s\'ha pogut crear el compte</string>
<string name="login_type_advanced">Inici de sessió avançat</string>
<string name="login_no_client_certificate_optional">Sense certificat del client*</string>
<string name="login_client_certificate_selected">Certificat del client: %s</string>

View File

@@ -45,13 +45,13 @@
<string name="intro_tasks_opentasks">OpenTasks</string>
<string name="intro_tasks_opentasks_info">Nezdá se už být vyvíjeno nedoporučeno.</string>
<string name="intro_tasks_tasks_org">Tasks.org</string>
<string name="intro_tasks_tasks_org_info"><![CDATA[Některé funkce (ještě) <a href="https://www.davx5.com/faq/tasks/advanced-task-features">nejsou podporovány</a>.]]></string>
<string name="intro_tasks_no_app_store">Není k dispozici žádný obchod s aplikacemi</string>
<string name="intro_tasks_dont_show">Nepotřebuji podporu pro úkoly.*</string>
<string name="intro_open_source_title">Opensource software</string>
<string name="intro_open_source_text">Jsme rádi, že %s používáte. Jde o opensource software. Vývoj, údržba a podpora je ale těžká práce. Prosím zvažte zapojení se (je mnoho způsobů jak) nebo podpoření vývoje darem. Bude to velmi oceněno!</string>
<string name="intro_open_source_details">Jak se zapojit / podpořit vývoj darem</string>
<string name="intro_open_source_dont_show">Nějakou dobu teď nezobrazovat</string>
<string name="intro_next">Další</string>
<!--PermissionsActivity-->
<string name="permissions_title">Oprávnění</string>
<string name="permissions_text">Aby fungovalo správně, %s vyžaduje oprávnění.</string>
@@ -88,6 +88,7 @@
<string name="wifi_permissions_background_location_permission_label">Povolit napořád</string>
<string name="wifi_permissions_background_location_permission_on">Stav oprávnění polohy je: %s</string>
<string name="wifi_permissions_background_location_permission_off">Stav oprávnění polohy není: %s</string>
<string name="wifi_permissions_background_location_disclaimer">%s používá oprávnění k přístupu k poloze pouze pro zjišťování názvu (SSID) WiFi sítě, ke které jste právě připojení a to pro účely účtů, omezených právě na dané SSID. Toto se děje i když je aplikace spuštěná na pozadí. Žádná data o poloze nejsou shromažďována, ukládána, zpracovávána ani nikam odesílána.</string>
<string name="wifi_permissions_location_enabled">Určování polohy vždy zapnuté</string>
<string name="wifi_permissions_location_enabled_on">Služba určování polohy je zapnutá</string>
<string name="wifi_permissions_location_enabled_off">Služba určování polohy je vypnutá</string>
@@ -95,6 +96,7 @@
<string name="about_translations">Překlady</string>
<string name="about_libraries">Knihovny</string>
<string name="about_version">Verze %1$s (%2$d)</string>
<string name="about_build_date">Sestaveno %s</string>
<string name="about_copyright">© Ricki Hirner, Bernhard Stockmann (bitfire web engineering GmbH) a přispěvatelé</string>
<string name="about_license_info_no_warranty">Na tento program nejsou poskytovány ŽÁDNÉ ZÁRUKY. Jedná se o svobodný software a jeho šíření dál je vítáno, ovšem za podmínky, že stejné svobody zůstanou zachovány i všem dalším příjemcům.</string>
<!--global settings-->
@@ -127,6 +129,7 @@
<string name="account_list_manage_battery_saver">Spravovat spořič baterie</string>
<string name="account_list_low_storage">Málo volného místa v úložišti. Android nebude synchronizovat místní změny okamžitě, ale při další běžné synchronizaci.</string>
<string name="account_list_manage_storage">Spravovat úložiště</string>
<string name="account_list_empty">Vítejte v aplikaci DAVx⁵!\n\nNyní můžete přidat CalDAV/CardDAV účet.</string>
<string name="accounts_sync_all">Synchronizovat všechny účty</string>
<!--RefreshCollectionsWorker-->
<string name="refresh_collections_worker_refresh_failed">Vyhledání služby se nezdařilo</string>
@@ -138,7 +141,9 @@
<string name="app_settings">Nastavení</string>
<string name="app_settings_debug">Ladění</string>
<string name="app_settings_show_debug_info">Zobrazit ladící informace</string>
<string name="app_settings_show_debug_info_details">Zobrazit/sdílet podrobnosti o software a nastavení</string>
<string name="app_settings_logging">Podrobnější zaznamenávání událostí</string>
<string name="app_settings_logging_on">Zaznamenávání událostí je aktivní</string>
<string name="app_settings_logging_off">Zaznamenávání událostí je vypnuté</string>
<string name="app_settings_battery_optimization">Optimalizace akumulátoru</string>
<string name="app_settings_battery_optimization_exempted">Aplikace je vyjmuta (doporučeno)</string>
@@ -222,13 +227,14 @@
<string name="login_user_name_optional">Uživatelské jméno*</string>
<string name="login_base_url">Základ URL</string>
<string name="login_select_certificate">Vybrat certifikát</string>
<string name="login_add_account">Přidat účet</string>
<string name="login_create_account">Vytvořit účet</string>
<string name="login_account_name">Název účtu</string>
<string name="login_account_avoid_apostrophe">Používání apostrofů (\') může způsobit problémy na některých zařízeních.</string>
<string name="login_account_name_info">Pro jméno účtu použijte svou e-mailovou adresu, protože Android bude brát jméno účtu jako údaj pro ORGANIZÁTORA vytvořených událostí. Nelze mít dva účty stejného jména.</string>
<string name="login_account_contact_group_method">Metoda seskupování kontaktů:</string>
<string name="login_account_name_required">Je třeba zadat název pro účet</string>
<string name="login_account_name_already_taken">Tento název účtu už je používán někým jiným</string>
<string name="login_account_not_created">Účet nelze vytvořit</string>
<string name="login_type_advanced">Pokročilé přihlášení</string>
<string name="login_no_client_certificate_optional">Bez certifikátu klienta</string>
<string name="login_client_certificate_selected">Certifikát klienta: %s</string>

View File

@@ -45,13 +45,13 @@
<string name="intro_tasks_opentasks">OpenTasks</string>
<string name="intro_tasks_opentasks_info">Ser ikke ud til at blive udviklet længere - ikke anbefalet.</string>
<string name="intro_tasks_tasks_org">Tasks.org</string>
<string name="intro_tasks_tasks_org_info"><![CDATA[Nogle funktioner <a href="https://www.davx5.com/faq/tasks/advanced-task-features">understøttes ikke</a> (endnu).]]></string>
<string name="intro_tasks_no_app_store">Ingen app-store tilgængelig</string>
<string name="intro_tasks_dont_show">Jeg behøver ikke opgaveunderstøttelse.*</string>
<string name="intro_open_source_title">Åben kilde program</string>
<string name="intro_open_source_text">Vi er glade for, at du bruger %s, som er åben-kilde software. Udvikling, vedligeholdelse og support er hårdt arbejde. Overvej at bidrage (der er mange måner) eller donere. Det ville være meget værdsat!</string>
<string name="intro_open_source_details">Sådan bidrager/donerer du</string>
<string name="intro_open_source_dont_show">Vis ikke i den nærmeste fremtid</string>
<string name="intro_next">Næste</string>
<!--PermissionsActivity-->
<string name="permissions_title">Tilladelser</string>
<string name="permissions_text">%s kræver tilladelser for at virke rigtig.</string>
@@ -88,6 +88,7 @@
<string name="wifi_permissions_background_location_permission_label">Tillad altid</string>
<string name="wifi_permissions_background_location_permission_on">Lokaliseringstilladelse sat til: %s</string>
<string name="wifi_permissions_background_location_permission_off">Lokationstilladelse ikke sat til: %s</string>
<string name="wifi_permissions_background_location_disclaimer">%s bruger kun placering rettighed for at bestemme nuværende trådløs SSID for SSID begrænset konti. Dette sker selvom programmet kører i baggrund. Ingen placering data bliver opsamlet, gemt, behandlet eller sendt.</string>
<string name="wifi_permissions_location_enabled">Placering altid aktiveret</string>
<string name="wifi_permissions_location_enabled_on">Placering tjeneste er aktiveret</string>
<string name="wifi_permissions_location_enabled_off">Placering tjeneste er deaktiveret</string>
@@ -95,6 +96,7 @@
<string name="about_translations">Oversættelser</string>
<string name="about_libraries">Biblioteker</string>
<string name="about_version">Version %1$s (%2$d)</string>
<string name="about_build_date">Oversat den %s</string>
<string name="about_copyright">© Ricki Hirner, Bernhard Stockmann (bitfire web engineering GmbH) og bidragydere</string>
<string name="about_license_info_no_warranty">Dette program leveres ABSOLUT UDEN GARANTI. Det er fri software, og du er velkommen til at videredistribuere det under visse betingelse.</string>
<!--global settings-->
@@ -129,6 +131,7 @@ Synkronisering kører muligvis ikke.</string>
<string name="account_list_low_storage">124
Lav lagerplads. Android vil ikke synkronisere lokale ændringer med det samme, men under den næste almindelige synkronisering.</string>
<string name="account_list_manage_storage">Administrer lagerplads</string>
<string name="account_list_empty">Velkommen til DAVx⁵!\n\nDu kan nu tilføje en CalDAV/CardDAV konto.</string>
<string name="accounts_sync_all">Synkroniser alle konti</string>
<!--RefreshCollectionsWorker-->
<string name="refresh_collections_worker_refresh_failed">Registrering af tjeneste kunne ikke foretages</string>
@@ -140,7 +143,9 @@ Lav lagerplads. Android vil ikke synkronisere lokale ændringer med det samme, m
<string name="app_settings">Indstillinger</string>
<string name="app_settings_debug">Fejlsøgning</string>
<string name="app_settings_show_debug_info">Vis fejlsøgnings information</string>
<string name="app_settings_show_debug_info_details">Vis/del software og opsætningsoplysninger</string>
<string name="app_settings_logging">Uddybende logning</string>
<string name="app_settings_logging_on">Logning er aktiv</string>
<string name="app_settings_logging_off">Logning er deaktiveret</string>
<string name="app_settings_battery_optimization">Batteri optimering</string>
<string name="app_settings_battery_optimization_exempted">Appen er undtaget (anbefalet)</string>
@@ -227,13 +232,14 @@ Lav lagerplads. Android vil ikke synkronisere lokale ændringer med det samme, m
<string name="login_base_url">Basis URL</string>
<string name="login_base_url_info"><![CDATA[Basis-URL\'en vil blive tjekket direkte, men <a href="%s">tjenester opdages også</a> ved hjælp af DNS-records og velkendte URL\'er]]>.</string>
<string name="login_select_certificate">Vælge certifikat</string>
<string name="login_add_account">Tilføj konto</string>
<string name="login_create_account">Oprette konto</string>
<string name="login_account_name">Kontonavn</string>
<string name="login_account_avoid_apostrophe">Brug af apostroffer (\') ser ud til at give problemer på nogle enheder.</string>
<string name="login_account_name_info">Brug en e-mail adresse som kontonavn da Android bruger kontonavn til ORGANIZER-felt for oprettede aktiviteter. Man kan ikke have to konti med samme navn.</string>
<string name="login_account_contact_group_method">Gruppering af kontakter:</string>
<string name="login_account_name_required">Kontonavn påkrævet</string>
<string name="login_account_name_already_taken">Konto navn er allerede i brug</string>
<string name="login_account_not_created">Konto kunne ikke oprettes</string>
<string name="login_type_advanced">Avanceret login</string>
<string name="login_no_client_certificate_optional">Intet certifikat fra klienten*</string>
<string name="login_client_certificate_selected">Klientcertifikat: %s</string>

View File

@@ -45,13 +45,13 @@
<string name="intro_tasks_opentasks">OpenTasks</string>
<string name="intro_tasks_opentasks_info">Wird anscheinend nicht weiterentwickelt - nicht empfehlenswert.</string>
<string name="intro_tasks_tasks_org">Tasks.org</string>
<string name="intro_tasks_tasks_org_info"><![CDATA[Einige Funktionen <a href="https://www.davx5.com/faq/tasks/advanced-task-features">werden (noch) nicht unterstützt</a>.]]></string>
<string name="intro_tasks_no_app_store">Kein App-Store verfügbar</string>
<string name="intro_tasks_dont_show">Ich brauche keine Unterstützung für Aufgaben.*</string>
<string name="intro_open_source_title">Open-Source-Software</string>
<string name="intro_open_source_text">Wir freuen uns, dass Sie die Open-Source-Software %s verwenden. Entwicklung, Wartung und Support sind viel Arbeit. Ziehen Sie daher bitte in Betracht, mitzuhelfen (dazu gibt es viele Möglichkeiten) oder zu spenden. Vielen Dank!</string>
<string name="intro_open_source_details">Infos zum Mithelfen/Spenden</string>
<string name="intro_open_source_dont_show">In nächster Zeit nicht anzeigen</string>
<string name="intro_next">Weiter</string>
<!--PermissionsActivity-->
<string name="permissions_title">Rechteverwaltung</string>
<string name="permissions_text">%s benötigt Berechtigungen, um ordnungsgemäß zu funktionieren.</string>
@@ -88,6 +88,7 @@
<string name="wifi_permissions_background_location_permission_label">Immer zulassen</string>
<string name="wifi_permissions_background_location_permission_on">Standort-Zugriff eingestellt auf: %s</string>
<string name="wifi_permissions_background_location_permission_off">Standort-Zugriff nicht eingestellt auf: %s</string>
<string name="wifi_permissions_background_location_disclaimer">%s verwendet die Standort-Berechtigung ausschließlich zum Bestimmen der aktuellen WLAN-SSID für SSID-beschränkte Accounts. Dies findet auch statt, wenn die App geschlossen ist bzw. im Hintergrund läuft. Standortdaten werden weder gesammelt, gespeichert, verarbeitet noch irgendwohin gesendet.</string>
<string name="wifi_permissions_location_enabled">Standort-Dienst immer aktiviert</string>
<string name="wifi_permissions_location_enabled_on">Standort-Dienst aktiv</string>
<string name="wifi_permissions_location_enabled_off">Standort-Dienst inaktiv</string>
@@ -95,6 +96,7 @@
<string name="about_translations">Übersetzungen</string>
<string name="about_libraries">Bibliotheken</string>
<string name="about_version">Version %1$s (%2$d)</string>
<string name="about_build_date">Erstellt am %s</string>
<string name="about_copyright">© Ricki Hirner, Bernhard Stockmann (bitfire web engineering GmbH) und Mitwirkende</string>
<string name="about_license_info_no_warranty">Dieses Programm wird OHNE JEDE GEWÄHRLEISTUNG bereitgestellt. Es ist freie Software Sie können es also unter bestimmten Bedingungen weiterverbreiten.</string>
<!--global settings-->
@@ -127,6 +129,7 @@
<string name="account_list_manage_battery_saver">Akkusparen verwalten</string>
<string name="account_list_low_storage">Wenig Speicherplatz. Android wird lokale Änderungen nicht sofort synchronisieren, sondern bei der nächsten regulären Synchronisierung.</string>
<string name="account_list_manage_storage">Speicherplatz verwalten</string>
<string name="account_list_empty">Herzlich willkommen!\n\nSie können jetzt ein CalDAV/CardDAV-Konto hinzufügen.</string>
<string name="accounts_sync_all">Alle Konten synchronisieren</string>
<!--RefreshCollectionsWorker-->
<string name="refresh_collections_worker_refresh_failed">Diensterkennung fehlgeschlagen</string>
@@ -138,7 +141,9 @@
<string name="app_settings">Einstellungen</string>
<string name="app_settings_debug">Fehlersuche</string>
<string name="app_settings_show_debug_info">Informationen zur Fehlersuche</string>
<string name="app_settings_show_debug_info_details">Software- und Konfigurationsdetails anzeigen und teilen</string>
<string name="app_settings_logging">Ausführliche Protokollierung</string>
<string name="app_settings_logging_on">Protokollierung läuft</string>
<string name="app_settings_logging_off">Keine Protokollierung</string>
<string name="app_settings_battery_optimization">Akku-Optimierung</string>
<string name="app_settings_battery_optimization_exempted">App ist ausgenommen (empfohlen)</string>
@@ -225,13 +230,14 @@
<string name="login_base_url">Basis-URL</string>
<string name="login_base_url_info"><![CDATA[Die Basis-URL wird direkt geprüft, aber <a href="%s">Dienste werden auch ermittelt</a>, anhand von DNS-Einträgen und bekannten URLs.]]></string>
<string name="login_select_certificate">Zertifikat auswählen</string>
<string name="login_add_account">Konto hinzufügen</string>
<string name="login_create_account">Konto anlegen</string>
<string name="login_account_name">Kontoname</string>
<string name="login_account_avoid_apostrophe">Das Verwenden von Apostrophen (\') scheint auf einigen Geräten Probleme zu verursachen.</string>
<string name="login_account_name_info">Verwenden Sie Ihre E-Mail-Adresse als Kontonamen, da Android den Kontonamen als ORGANIZER einsetzt. Es kann allerdings keine zwei Konten mit dem gleichen Namen geben.</string>
<string name="login_account_contact_group_method">Kontaktgruppen-Methode:</string>
<string name="login_account_name_required">Kontoname wird benötigt</string>
<string name="login_account_name_already_taken">Kontoname bereits verwendet</string>
<string name="login_account_not_created">Konto konnte nicht angelegt werden</string>
<string name="login_type_advanced">Erweiterte Anmeldung</string>
<string name="login_no_client_certificate_optional">Kein Client-Zertifikat*</string>
<string name="login_client_certificate_selected">Client-Zertifikat: %s</string>

View File

@@ -45,6 +45,7 @@
<string name="intro_tasks_opentasks">OpenTasks</string>
<string name="intro_tasks_opentasks_info">Δεν φαίνεται να αναπτύσσεται πλέον - δεν συνιστάται.</string>
<string name="intro_tasks_tasks_org">Tasks.org</string>
<string name="intro_tasks_tasks_org_info"><![CDATA[Ορισμένες λειτουργίες <a href="https://www.davx5.com/faq/tasks/advanced-task-features"> δεν υποστηρίζονται </a> (ακόμα).]]></string>
<string name="intro_tasks_no_app_store">Δεν υπάρχει διαθέσιμο κατάστημα εφαρμογών</string>
<string name="intro_tasks_dont_show">Δεν χρειάζομαι υποστήριξη εργασιών.*</string>
<string name="intro_open_source_title">Λογισμικό ανοικτού κώδικα</string>
@@ -87,6 +88,7 @@
<string name="wifi_permissions_background_location_permission_label">Να επιτρέπεται συνέχεια</string>
<string name="wifi_permissions_background_location_permission_on">Η άδεια τοποθεσίας έχει οριστεί σε: %s</string>
<string name="wifi_permissions_background_location_permission_off">Η άδεια τοποθεσίας δεν έχει οριστεί σε: %s</string>
<string name="wifi_permissions_background_location_disclaimer">%s χρησιμοποιεί την υπηρεσία εντοπισμού μόνο για να προσδιορίσει το SSID του τρέχοντος WIFI για λογαριασμούς με περιορισμούς σε SSIDs. Αυτό γίνεται ακόμα και όταν η εφαρμογή βρίσκεται στο παρασκήνιο. Κανένα δεδομένο εντοπισμού δεν συλλέγεται, καταχωρείται, επεξεργάζεται η αποστέλνεται οπουδήποτε.</string>
<string name="wifi_permissions_location_enabled">Η υπηρεσία εντοπισμού είναι πάντα ενεργοποιημένη</string>
<string name="wifi_permissions_location_enabled_on">Η υπηρεσία εντοπισμού είναι ενεργοποιημένη</string>
<string name="wifi_permissions_location_enabled_off">Η υπηρεσία εντοπισμού είναι απενεργοποιημένη</string>
@@ -94,6 +96,7 @@
<string name="about_translations">Μεταφράσεις</string>
<string name="about_libraries">Βιβλιοθήκες</string>
<string name="about_version">Έκδοση %1$s (%2$d)</string>
<string name="about_build_date">Μεταγλωττισμένο σε %s</string>
<string name="about_copyright">© Ricki Hirner, Bernhard Stockmann (bitfire web engineering GmbH) και συνεργάτες</string>
<string name="about_license_info_no_warranty">Αυτό το πρόγραμμα συνοδεύεται χωρις ΚΑΜΙΑ ΕΓΓΥΗΣΗ. Είναι ελεύθερο λογισμικό και είστε ευπρόσδεκτοι να το αναδιανείμετε υπό ορισμένες προϋποθέσεις.</string>
<!--global settings-->
@@ -126,6 +129,7 @@
<string name="account_list_manage_battery_saver">Διαχείριση εξοικονόμησης μπαταρίας</string>
<string name="account_list_low_storage">Χαμηλός αποθηκευτικός χώρος. Το Android δεν θα συγχρονίσει τις τοπικές αλλαγές αμέσως, αλλά κατά τη διάρκεια του επόμενου τακτικού συγχρονισμού.</string>
<string name="account_list_manage_storage">Διαχείριση αποθηκευτικού χώρου</string>
<string name="account_list_empty">Καλώς ήρθατε στο DAVx⁵!\n\nΜπορείτε να προσθέσετε τώρα έναν λογαριασμό CalDAV/CardDAV.</string>
<string name="accounts_sync_all">Συγχρονισμός όλων των λογαριασμών</string>
<!--RefreshCollectionsWorker-->
<string name="refresh_collections_worker_refresh_failed">Αποτυχία ανίχνευσης υπηρεσίας</string>
@@ -137,7 +141,9 @@
<string name="app_settings">Ρυθμίσεις</string>
<string name="app_settings_debug">Αποσφαλμάτωση</string>
<string name="app_settings_show_debug_info">Προβολή πληροφοριών αποσφαλμάτωσης</string>
<string name="app_settings_show_debug_info_details">Προβολή/διαμοιρασμός λογισμικού και λεπτομερειών διαμόρφωσης</string>
<string name="app_settings_logging">Λεπτομερής καταγραφή</string>
<string name="app_settings_logging_on">Η καταγραφή είναι ενεργή</string>
<string name="app_settings_logging_off">Η καταγραφή είναι απενεργοποιημένη</string>
<string name="app_settings_battery_optimization">Βελτιστοποίηση μπαταρίας</string>
<string name="app_settings_battery_optimization_exempted">Η εφαρμογή εξαιρείται (συνιστάται)</string>
@@ -224,13 +230,14 @@
<string name="login_base_url">Βασική URL</string>
<string name="login_base_url_info"><![CDATA[Η βασική διεύθυνση URL θα ελεγχθεί άμεσα, αλλά <a href="%s">οι υπηρεσίες εντοπίζονται επίσης</a> χρησιμοποιώντας εγγραφές DNS και γνωστές διευθύνσεις URL.]]></string>
<string name="login_select_certificate">Επιλογή πιστοποιητικού</string>
<string name="login_add_account">Προσθήκη λογαριασμού</string>
<string name="login_create_account">Δημιουργία λογαριασμού</string>
<string name="login_account_name">Όνομα λογαριασμού</string>
<string name="login_account_avoid_apostrophe">Η χρήση των αποσιωπητικών (\') φαίνεται να προκαλεί προβλήματα σε ορισμένες συσκευές.</string>
<string name="login_account_name_info">Χρησιμοποιήστε τη διεύθυνση ηλεκτρονικού ταχυδρομείου ως όνομα λογαριασμού, επειδή το Android θα χρησιμοποιεί το όνομα του λογαριασμού ως πεδίο ORGANIZER για τα συμβάντα που δημιουργείτε. Δεν μπορείτε να έχετε δύο λογαριασμούς με το ίδιο όνομα.</string>
<string name="login_account_contact_group_method">Μέθοδος ομάδας επαφών:</string>
<string name="login_account_name_required">Απαιτείται όνομα λογαριασμού</string>
<string name="login_account_name_already_taken">Το όνομα λογαριασμού έχει ήδη ληφθεί</string>
<string name="login_account_not_created">Αδυναμία δημιουργίας λογαριασμού</string>
<string name="login_type_advanced">Σύνδεση για προχωρημένους</string>
<string name="login_no_client_certificate_optional">Δεν υπάρχει πιστοποιητικό πελάτη*</string>
<string name="login_client_certificate_selected">Πιστοποιητικό πελάτη: %s</string>

View File

@@ -40,6 +40,7 @@
<string name="intro_tasks_opentasks">OpenTasks</string>
<string name="intro_tasks_opentasks_info">Doesn\'t seem to be developed anymore not recommended.</string>
<string name="intro_tasks_tasks_org">Tasks.org</string>
<string name="intro_tasks_tasks_org_info"><![CDATA[Some features <a href="https://www.davx5.com/faq/tasks/advanced-task-features">are not supported</a> (yet).]]></string>
<string name="intro_tasks_no_app_store">No app store available</string>
<string name="intro_tasks_dont_show">I don\'t need tasks support.*</string>
<string name="intro_open_source_title">Open-source software</string>
@@ -80,6 +81,7 @@
<string name="wifi_permissions_location_permission_off">Location permission denied</string>
<string name="wifi_permissions_background_location_permission">Background location permission</string>
<string name="wifi_permissions_background_location_permission_label">Allow all the time</string>
<string name="wifi_permissions_background_location_disclaimer">%s uses the Location permission only to determine the current WiFi\'s SSID for SSID-restricted accounts. This will happen even when the app is in background. No location data are collected, stored, processed or sent anywhere.</string>
<string name="wifi_permissions_location_enabled">Location always enabled</string>
<string name="wifi_permissions_location_enabled_on">Location service is enabled</string>
<string name="wifi_permissions_location_enabled_off">Location service is disabled</string>
@@ -87,6 +89,7 @@
<string name="about_translations">Translations</string>
<string name="about_libraries">Libraries</string>
<string name="about_version">Version %1$s (%2$d)</string>
<string name="about_build_date">Compiled on %s</string>
<string name="about_copyright">© Ricki Hirner, Bernhard Stockmann (bitfire web engineering GmbH) and contributors</string>
<string name="about_license_info_no_warranty">This program comes with ABSOLUTELY NO WARRANTY. It is free software, and you are welcome to redistribute it under certain conditions.</string>
<!--global settings-->
@@ -115,6 +118,7 @@
<string name="account_list_manage_datasaver">Manage data saver</string>
<string name="account_list_low_storage">Storage space low. Android will not sync local changes immediately, but during the next regular sync.</string>
<string name="account_list_manage_storage">Manage storage</string>
<string name="account_list_empty">Welcome to DAVx⁵!\n\nYou can add a CalDAV/CardDAV account now.</string>
<string name="accounts_sync_all">Sync all accounts</string>
<!--RefreshCollectionsWorker-->
<string name="refresh_collections_worker_refresh_failed">Service detection failed</string>
@@ -126,7 +130,9 @@
<string name="app_settings">Settings</string>
<string name="app_settings_debug">Debugging</string>
<string name="app_settings_show_debug_info">Show debug info</string>
<string name="app_settings_show_debug_info_details">View/share software and configuration details</string>
<string name="app_settings_logging">Verbose logging</string>
<string name="app_settings_logging_on">Logging is active</string>
<string name="app_settings_logging_off">Logging is disabled</string>
<string name="app_settings_battery_optimization">Battery optimisation</string>
<string name="app_settings_connection">Connection</string>
@@ -194,12 +200,13 @@
<string name="login_user_name">User name</string>
<string name="login_base_url">Base URL</string>
<string name="login_select_certificate">Select certificate</string>
<string name="login_add_account">Add account</string>
<string name="login_create_account">Create account</string>
<string name="login_account_name">Account name</string>
<string name="login_account_name_info">Use your email address as account name because Android will use the account name as ORGANISER field for events you create. You can\'t have two accounts with the same name.</string>
<string name="login_account_contact_group_method">Contact group method:</string>
<string name="login_account_name_required">Account name required</string>
<string name="login_account_name_already_taken">Account name already taken</string>
<string name="login_account_not_created">Account could not be created</string>
<string name="login_no_certificate_found">No certificate found</string>
<string name="login_install_certificate">Install certificate</string>
<string name="login_type_google">Google Contacts / Calendar</string>

View File

@@ -45,13 +45,13 @@
<string name="intro_tasks_opentasks">OpenTasks</string>
<string name="intro_tasks_opentasks_info">Al parecer ya no tiene soporte no se recomienda.</string>
<string name="intro_tasks_tasks_org">Tasks.org</string>
<string name="intro_tasks_tasks_org_info"><![CDATA[Algunas características <a href="https://www.davx5.com/faq/tasks/advanced-task-features">no son compatibles</a> (todavía).]]></string>
<string name="intro_tasks_no_app_store">Ninguna tienda de aplicaciones disponible</string>
<string name="intro_tasks_dont_show">No necesito soporte para tareas.*</string>
<string name="intro_open_source_title">Software open-source</string>
<string name="intro_open_source_text">Nos complace que use%s, que es software open-source. Desarrollar, mantener y asistir a usuarios es un trabajo duro. Por favor, considere contribuir (hay muchas maneras) o hacer una donación. ¡Se agradecería mucho!</string>
<string name="intro_open_source_details">Cómo contribuir o donar</string>
<string name="intro_open_source_dont_show">No mostrar en un futuro próximo</string>
<string name="intro_next">Siguiente</string>
<!--PermissionsActivity-->
<string name="permissions_title">Permisos</string>
<string name="permissions_text">%s necesita permisos para funcionar correctamente.</string>
@@ -88,6 +88,7 @@
<string name="wifi_permissions_background_location_permission_label">Permitir todo el tiemp</string>
<string name="wifi_permissions_background_location_permission_on">Acceso a la ubicación concedido para: %s</string>
<string name="wifi_permissions_background_location_permission_off">Acceso a la ubicación no concedido para: %s</string>
<string name="wifi_permissions_background_location_disclaimer">%s usa el permiso de ubicación solo para determinar el SSID del WiFi actual para las cuentas con restricción de SSID. Esto ocurrirá incluso cuando la aplicación esté en segundo plano. No se recogen, almacenan, procesan o envían datos de ubicación a ninguna parte.</string>
<string name="wifi_permissions_location_enabled">Ubicación siempre activada</string>
<string name="wifi_permissions_location_enabled_on">El servicio de ubicación está activado</string>
<string name="wifi_permissions_location_enabled_off">El servicio de ubicación está desactivado</string>
@@ -95,6 +96,7 @@
<string name="about_translations">Traducciones</string>
<string name="about_libraries">Bibliotecas</string>
<string name="about_version">Versión %1$s (%2$d)</string>
<string name="about_build_date">Compilada en %s</string>
<string name="about_copyright">© Ricki Hirner, Bernhard Stockmann (bitfire web engineering GmbH) y colaboradores </string>
<string name="about_license_info_no_warranty">Este programa viene sin NINGÚN TIPO DE GARANTÍA. Es software libre, y cualquier contribución es bienvenida y redistribuida bajo ciertas condiciones.</string>
<!--global settings-->
@@ -127,6 +129,7 @@
<string name="account_list_manage_battery_saver">Administrar ahorro de batería</string>
<string name="account_list_low_storage">Poco espacio de almacenamiento disponible. Android no sincronizará los cambios hechos localmente de manera inmediata, pero sí lo hará en la siguiente sincronización programada.</string>
<string name="account_list_manage_storage">Administrar almacenamiento</string>
<string name="account_list_empty">Bienvenido a DAVx⁵!\n\nAhora puedes añadir una cuenta CalDAV/CardDAV.</string>
<string name="accounts_sync_all">Sincronizar todas las cuentas</string>
<!--RefreshCollectionsWorker-->
<string name="refresh_collections_worker_refresh_failed">Falló la detección del servicio</string>
@@ -138,7 +141,9 @@
<string name="app_settings">Ajustes</string>
<string name="app_settings_debug">Depuración</string>
<string name="app_settings_show_debug_info">Mostrar la información de depuración</string>
<string name="app_settings_show_debug_info_details">Ver/compartir detalles de software y configuración</string>
<string name="app_settings_logging">Registro extendido</string>
<string name="app_settings_logging_on">El registro está activo</string>
<string name="app_settings_logging_off">El registro está deshabilitado</string>
<string name="app_settings_battery_optimization">Optimización de batería</string>
<string name="app_settings_battery_optimization_exempted">La app está exenta (recomendado)</string>
@@ -225,13 +230,14 @@
<string name="login_base_url">URL base</string>
<string name="login_base_url_info"><![CDATA[La URL base será comprobada directamente, pero <a href="%s">los servicios también son detectados</a> usando registros DNS y URLs well-known.]]></string>
<string name="login_select_certificate">Seleccionar un certificado</string>
<string name="login_add_account">Añadir cuenta</string>
<string name="login_create_account">Crear cuenta</string>
<string name="login_account_name">Nombre de cuenta</string>
<string name="login_account_avoid_apostrophe">El uso de comillas (\') puede causar problemas en algunos dispositivos.</string>
<string name="login_account_name_info">Usa tu dirección de correo como nombre de cuenta puesto que Android usará el nombre de la cuenta como campo de \"organizador\" en los eventos que cree. No puedes tener dos cuentas con el mismo nombre.</string>
<string name="login_account_contact_group_method">Método de contacto de grupo:</string>
<string name="login_account_name_required">Nombre de cuenta requerido</string>
<string name="login_account_name_already_taken">El nombre de la cuenta ya está siendo utilizado</string>
<string name="login_account_not_created">La cuenta no pudo ser creada</string>
<string name="login_type_advanced">Inicio de sesión avanzado</string>
<string name="login_no_client_certificate_optional">Sin certificado de cliente*</string>
<string name="login_client_certificate_selected">Certificado de cliente: %s</string>

View File

@@ -1,148 +0,0 @@
<?xml version='1.0' encoding='UTF-8'?>
<resources xmlns:tools="http://schemas.android.com/tools">
<!--common strings-->
<string name="account_invalid">Kasutajakontot ei leidu (enam)</string>
<string name="account_title_address_book">DAVx⁵ aadressiraamat</string>
<string name="address_books_authority_title">Aadressiraamatud</string>
<string name="dialog_delete">Kustuta</string>
<string name="dialog_remove">Eemalda</string>
<string name="dialog_deny">Katkesta</string>
<string name="field_required">See väli on kohustuslik</string>
<string name="help">Abiteave</string>
<string name="manage_accounts">Halda kasutajakontosid</string>
<string name="navigate_up">Liigu üles</string>
<string name="optional_label">* valikuline</string>
<string name="options_menu">Valikute menüü</string>
<string name="share">Jaga</string>
<string name="database_destructive_migration_title">Andmebaas on vigane</string>
<string name="database_destructive_migration_text">Kõik kasutajakontod on kohalikust seadmest eemaldatud</string>
<string name="notification_channel_debugging">Silumine ja veaotsing</string>
<string name="notification_channel_general">Muud olulised sõnumid</string>
<string name="notification_channel_status">Väheolulised olekuteated</string>
<string name="notification_channel_sync">Sünkroniseerimine</string>
<string name="notification_channel_sync_errors">Sünkroniseerimisvead</string>
<string name="notification_channel_sync_errors_desc">Olulised vead, mis peatavad sünkroniseerimise, nagu näiteks ootamatud päringuvastused serverist</string>
<string name="notification_channel_sync_warnings">Sünkroniseerimishoiatused</string>
<string name="notification_channel_sync_warnings_desc">Vähetõsised sünkroniseerimisteated näiteks vigaste failide kohta</string>
<string name="notification_channel_sync_io_errors">Võrgu- ja sisend/väljundvead</string>
<string name="notification_channel_sync_io_errors_desc">Ühenduste aegumine ja muud sarnased probleemid (tihti ajutised)</string>
<!--IntroActivity-->
<string name="intro_slogan1">Sinu andmed. Sinu valik.</string>
<string name="intro_more_info">Lisateave</string>
<string name="intro_open_source_title">Avatud lähtekoodiga tarkvara</string>
<!--PermissionsActivity-->
<!--WifiPermissionsActivity-->
<!--AboutActivity-->
<string name="about_translations">Tõlked</string>
<string name="about_libraries">Teegid</string>
<string name="about_version">Versioon %1$s (%2$d)</string>
<string name="about_copyright">© Ricki Hirner, Bernhard Stockmann (bitfire web engineering GmbH) ja kaasautorid</string>
<!--global settings-->
<string name="logging_couldnt_create_file">Logifaili loomine ei õnnestunud</string>
<string name="logging_notification_view_share">Vaata/jaga</string>
<!--AccountsScreen-->
<string name="navigation_drawer_about">Teave / litsents</string>
<string name="navigation_drawer_beta_feedback">Beetaversiooni tagasiside</string>
<string name="install_browser">Palun paigalda veebibrauser</string>
<string name="navigation_drawer_settings">Seadistused</string>
<string name="navigation_drawer_news_updates">Uudised ja uuendused</string>
<string name="navigation_drawer_tools">Tarvikud</string>
<string name="navigation_drawer_external_links">Välised lingid</string>
<string name="navigation_drawer_website">Veebisait</string>
<string name="navigation_drawer_manual">Käsiraamat</string>
<string name="navigation_drawer_faq">KKK</string>
<string name="navigation_drawer_community">Kogukond</string>
<string name="navigation_drawer_support_project">Toeta projekti</string>
<string name="navigation_drawer_contribute">Osalemise viisid</string>
<string name="navigation_drawer_privacy_policy">Privaatsuspoliitika</string>
<string name="accounts_sync_all">Sünkroniseeri kõik kasutajakontod</string>
<!--RefreshCollectionsWorker-->
<string name="refresh_collections_worker_refresh_failed">Teenuse tuvastamine ei õnnestunud</string>
<!--Foreground service used by WorkManager on Android <12-->
<!--AppSettingsActivity-->
<string name="app_settings">Seadistused</string>
<string name="app_settings_debug">Silumine ja veaotsing</string>
<string name="app_settings_show_debug_info">Näita silumisteavet</string>
<!--AccountScreen-->
<string name="account_carddav">CardDAV</string>
<string name="account_caldav">CalDAV</string>
<string name="account_webcal">Webcal</string>
<string name="account_manage_permissions">Halda õigusi</string>
<string name="account_synchronize_now">Sünkroniseeri nüüd</string>
<string name="account_settings">Kasutajakonto seadistused</string>
<string name="account_rename">Muuda kasutajakonto nime</string>
<string name="account_rename_new_name">Kasutajakonto uus nimi</string>
<string name="account_rename_rename">Muuda nime</string>
<string name="account_rename_exists_already">Selline nimi on juba kasutusel</string>
<string name="account_rename_couldnt_rename">Kasutajakonto nime muutmine ei õnnestunud</string>
<string name="account_delete">Kustuta kasutajakonto</string>
<string name="account_delete_confirmation_title">Kas tõesti kustutame kasutajakonto?</string>
<string name="account_delete_confirmation_text">Sellega kustutame ka kõik aadresside, kalendrite ja ülesannete kohalikud koopiad.</string>
<string name="account_install_icsx5">Paigalda ICSx⁵</string>
<!--AddAccountActivity-->
<string name="login_title">Lisa kasutajakonto</string>
<string name="login_generic_login">Üldine sisselogimine</string>
<string name="login_provider_login">Teenusepakkujakohane sisselogimine</string>
<string name="login_continue">Jätka</string>
<string name="login_login">Logi sisse</string>
<string name="login_type_email">Logi sisse e-posti aadressiga</string>
<string name="login_email_address">E-posti aadress</string>
<string name="login_email_address_error">Nõutav on korrektne e-posti aadress</string>
<string name="login_password">Salasõna</string>
<string name="login_password_hide">Peida salasõna</string>
<string name="login_password_show">Näita salasõna</string>
<string name="login_password_optional">Salasõna*</string>
<string name="login_type_url">Logi sisse võrguaadressi ja kasutajanimega</string>
<string name="login_user_name">Kasutajanimi</string>
<string name="login_user_name_optional">Kasutajanimi*</string>
<string name="login_base_url">Baas-võrguaadress</string>
<string name="login_select_certificate">Vali sertifikaat</string>
<string name="login_add_account">Lisa kasutajakonto</string>
<string name="login_account_name">Kasutajakonto nimi</string>
<string name="login_account_name_required">Kasutajakonto nimi on nõutav</string>
<string name="login_account_name_already_taken">Selline nimi on juba kasutusel</string>
<string name="login_install_certificate">Paigalda sertifikaat</string>
<string name="login_google_account">Google\'i kasutajakonto</string>
<string name="login_nextcloud_login_flow_server_address">Nextcloudi serveri aadress</string>
<string name="login_check_credentials">Palun samuti topeltkontrolli autentimist (tavaliselt kasutajanimi ja salasõna)</string>
<string name="login_logs_available">Täiendav tehniline teade leidub logides.</string>
<string name="login_view_logs">Vaata logisid</string>
<!--AccountSettingsActivity-->
<string name="settings_sync">Sünkroniseerimine</string>
<string name="settings_username">Kasutajanimi</string>
<string name="settings_password">Salasõna</string>
<string name="settings_new_password">Uus salasõna</string>
<string name="settings_password_summary">Uuenda salasõna vastavalt oma serveri juhendile.</string>
<string name="settings_certificate_alias">Kliendi sertifikaat</string>
<string name="settings_certificate_alias_empty">Sertifikaati pole saadaval või paigaldatud</string>
<string name="settings_certificate_install">Paigalda sertifikaat</string>
<string name="settings_caldav">CalDAV</string>
<string name="settings_carddav">CardDAV</string>
<!--CreateAddressBookScreen, CreateCalendarScreen-->
<string name="create_addressbook">Loo aadressiraamat</string>
<string name="create_calendar">Loo kalender</string>
<string name="create_calendar_time_zone_optional">Vaikimisi ajavöönd*</string>
<string name="create_collection_optional">* valikuline</string>
<!--CollectionScreen-->
<string name="collection_synchronization">Sünkroniseerimine</string>
<!--debugging and DebugInfoActivity-->
<string name="debug_info_view_details">Vaata üksikasju</string>
<string name="debug_info_logs_view">Vaata logisid</string>
<!--ExceptionInfoFragment-->
<string name="exception">Tekkis viga.</string>
<string name="exception_httpexception">Tekkis http-viga.</string>
<string name="exception_ioexception">Tekkis sisend-väljundviga.</string>
<string name="exception_show_details">Näita üksikasju</string>
<!--WebDAV accounts-->
<string name="webdav_mounts_quota_used_available">Kasutatud mahukvoot: %1$s / saadaval: %2$s</string>
<string name="webdav_mounts_share_content">Jaga sisu</string>
<string name="webdav_add_mount_url">WebDAVi võrguaadress</string>
<string name="webdav_add_mount_url_invalid">Vigane võrguaadress</string>
<string name="webdav_add_mount_username">Kasutajanimi</string>
<string name="webdav_add_mount_password">Salasõna</string>
<!--sync-->
<!--widgets-->
<string name="widget_sync_all">Sünkroniseeri kõik</string>
<string name="widget_sync_all_accounts">Sünkroniseeri kõik kasutajakontod</string>
<!--cert4android-->
</resources>

View File

@@ -45,6 +45,7 @@
<string name="intro_tasks_opentasks">OpenTasks</string>
<string name="intro_tasks_opentasks_info">Badirudi ez dela garatzen ez da gomendatzen.</string>
<string name="intro_tasks_tasks_org">Tasks.org</string>
<string name="intro_tasks_tasks_org_info"><![CDATA[Ezaugarri batzuk <a href="https://www.davx5.com/faq/tasks/advanced-task-features">ez dira onartzen</a> (oraindik).]]></string>
<string name="intro_tasks_no_app_store">Ez dago denda aplikaziorik eskuragarri </string>
<string name="intro_tasks_dont_show">Ez dut zereginen funtzionalitatea behar.*</string>
<string name="intro_open_source_title">Kode irekiko softwarea</string>
@@ -88,6 +89,7 @@
<string name="wifi_permissions_background_location_permission_on">Kokapen-baimena honela ezarri da: %s</string>
<string name="wifi_permissions_background_location_permission_off">Kokapen-baimena ez da honela ezarri:
%s</string>
<string name="wifi_permissions_background_location_disclaimer">%s zure kokapen baimena erabiltzen du uneko WiFi SSIDa SSIDz murriztatutako kontuen kontra egiaztatzeko. Hau aplikazioa atzeko planoan badago ere gertatuko da. Ez da kokapen daturik biltzen, gordetzen, prozesatzen edo inora bidaltzen.</string>
<string name="wifi_permissions_location_enabled">Kokapena beti gaituta</string>
<string name="wifi_permissions_location_enabled_on">Kokapen zerbitzua gaituta dago</string>
<string name="wifi_permissions_location_enabled_off">Kokapen zerbitzua desgaituta dago</string>
@@ -95,6 +97,7 @@
<string name="about_translations">Itzulpenak</string>
<string name="about_libraries">Liburutegiak</string>
<string name="about_version">%1$s (%2$d) bertsioa</string>
<string name="about_build_date">%s(e)an konpilatuta</string>
<string name="about_copyright">© Ricki Hirner, Bernhard Stockmann (bitfire web engineering GmbH) eta kolaboratzaileak</string>
<string name="about_license_info_no_warranty">Programa hau INOLAKO BERMERIK GABE dator. Software librea da, eta birbanatzeko baimena duzu baldintza batzuk kontuan hartuz.</string>
<!--global settings-->
@@ -127,6 +130,7 @@
<string name="account_list_manage_battery_saver">Kudeatu bateria-arrezpena</string>
<string name="account_list_low_storage">Biltegiratze lekua baxua. Android-ek ez ditu tokiko aldaketak berehala sinkronizatuko, hurrengo sinkronizazio arruntean baizik.</string>
<string name="account_list_manage_storage">Kudeatu biltegia</string>
<string name="account_list_empty">Ongi etorri DAVx⁵ aplikaziora!\n\nCalDAV/CardDAV kontu bat gehitu dezakezu orain.</string>
<string name="accounts_sync_all">Sinkronizatu kontu guztiak</string>
<!--RefreshCollectionsWorker-->
<string name="refresh_collections_worker_refresh_failed">Zerbitzuaren detekzioak huts egin du</string>
@@ -138,7 +142,9 @@
<string name="app_settings">Ezarpenak</string>
<string name="app_settings_debug">Arazketa</string>
<string name="app_settings_show_debug_info">Erakutsi arazte informazioa</string>
<string name="app_settings_show_debug_info_details">Ikusi/partekatu software eta konfigurazio xehetasunak</string>
<string name="app_settings_logging">Erregistro xehatuak</string>
<string name="app_settings_logging_on">Erregistratzea gaituta dago</string>
<string name="app_settings_logging_off">Erregistratzea desgaituta dago</string>
<string name="app_settings_battery_optimization">Bateria optimizazioa</string>
<string name="app_settings_battery_optimization_exempted">Aplikazioa salbuetsita dago (gomendatua)</string>
@@ -225,13 +231,14 @@
<string name="login_base_url">Oinarri URL</string>
<string name="login_base_url_info"><![CDATA[Oinarrizko URLa zuzenean egiaztatuko da, baina <a href="%s">zerbitzuak ere aurkitzen dira</a> DNS erregistroak eta URL ezagunak erabilita.]]></string>
<string name="login_select_certificate">Aukeratu ziurtagiria</string>
<string name="login_add_account">Gehitu kontua</string>
<string name="login_create_account">Sortu kontua</string>
<string name="login_account_name">Kontuaren izena</string>
<string name="login_account_avoid_apostrophe">Apostrofoak (\') erabiltzeak gailu batzuetan.arazoak sortzen dituela dirudi.</string>
<string name="login_account_name_info">Erabili zure eposta helbidea kontu izen bezala Androidek kontuaren izena ANTOLATZAILE eremuan ezarriko duelako sortzen dituzun gertaerentzako. Ezin dituzu bi kontu izen berdinarekin eduki.</string>
<string name="login_account_contact_group_method">Kontaktuen taldekatze metodoa:</string>
<string name="login_account_name_required">Kontuaren izena beharrezkoa</string>
<string name="login_account_name_already_taken">Kontuaren izena hartuta dago</string>
<string name="login_account_not_created">Ezin izan da kontua sortu</string>
<string name="login_type_advanced">Saio-hasiera aurreratua</string>
<string name="login_no_client_certificate_optional">Ez dago bezeroaren ziurtagiririk*</string>
<string name="login_client_certificate_selected">Bezeroaren ziurtagiria: %s</string>

View File

@@ -40,6 +40,7 @@
<string name="intro_tasks_opentasks">وظایف را باز کنید</string>
<string name="intro_tasks_opentasks_info">عدم توسعه توسط برنامه نویسان - توصیه نمیشود.</string>
<string name="intro_tasks_tasks_org">Tasks.org</string>
<string name="intro_tasks_tasks_org_info"><![CDATA[برخی از قابلیت ها <a href="https://www.davx5.com/faq/tasks/advanced-task-features">پشتیبانی نمیشود</a> (فعلا).]]></string>
<string name="intro_tasks_no_app_store">فروشگاه در دسترس نیست</string>
<string name="intro_tasks_dont_show">من به پشتیبانی وظایف نیاز ندارم. *</string>
<string name="intro_open_source_title">برنامه‌های متن باز</string>
@@ -80,6 +81,7 @@
<string name="wifi_permissions_location_permission_off">مجوز مکان رد شد</string>
<string name="wifi_permissions_background_location_permission">مجوز مکان پس زمینه</string>
<string name="wifi_permissions_background_location_permission_label">همیشه اجازه دهید</string>
<string name="wifi_permissions_background_location_disclaimer">%sفقط از مجوز مکان برای تعیین SSID WiFi فعلی برای حساب‌های محدود شده با SSID استفاده می‌کند. حتی وقتی برنامه در پس زمینه باشد این اتفاق می افتد. هیچ کدام اطلاعات مکانی ، ذخیره ، پردازش و یا ارسال نمی شود.</string>
<string name="wifi_permissions_location_enabled">مکان همیشه فعال است</string>
<string name="wifi_permissions_location_enabled_on">سرویس مکان فعال است</string>
<string name="wifi_permissions_location_enabled_off">سرویس مکان غیرفعال است</string>
@@ -87,6 +89,7 @@
<string name="about_translations">ترجمه ها</string>
<string name="about_libraries">کتابخانه ها</string>
<string name="about_version">ورژن %1$s (%2$d)</string>
<string name="about_build_date">در %s وارد شده است</string>
<string name="about_license_info_no_warranty">این برنامه کاملاً بدون ضمانت است. این یک نرم افزار رایگان است ، و شما می توانید تحت شرایط خاص توزیع مجدد آن را انجام دهید.</string>
<!--global settings-->
<string name="logging_couldnt_create_file">پرونده ثبت ایجاد نشد</string>
@@ -112,6 +115,7 @@
<string name="account_list_datasaver_enabled">محافظ داده فعال است. همگام سازی در پس زمینه محدود می شود.</string>
<string name="account_list_manage_datasaver">مدیریت محافظ داده</string>
<string name="account_list_manage_storage">مدیریت حافظه</string>
<string name="account_list_empty">به همگام‌ساز DAVx⁵ خوش آمدید</string>
<string name="accounts_sync_all">همگام سازی همه حساب‌ها</string>
<!--RefreshCollectionsWorker-->
<string name="refresh_collections_worker_refresh_failed">تشخیص سرویس ناموفق بود</string>
@@ -123,7 +127,9 @@
<string name="app_settings">تنظیمات</string>
<string name="app_settings_debug">اشکال زدایی</string>
<string name="app_settings_show_debug_info">نمایش اطلاعات اشکال زدایی</string>
<string name="app_settings_show_debug_info_details">جزئیات نرم افزار و پیکربندی را مشاهده یا به اشتراک بگذارید</string>
<string name="app_settings_logging">ورود به سیستم</string>
<string name="app_settings_logging_on">ورود به سیستم فعال است</string>
<string name="app_settings_logging_off">ورود به سیستم غیرفعال است</string>
<string name="app_settings_battery_optimization">بهینه ساز باتری</string>
<string name="app_settings_connection">ارتباط</string>
@@ -191,12 +197,13 @@
<string name="login_user_name">نام کاربری</string>
<string name="login_base_url">آدرس پایه</string>
<string name="login_select_certificate">گواهی را انتخاب کنید</string>
<string name="login_add_account">افزودن حساب</string>
<string name="login_create_account">ساخت حساب</string>
<string name="login_account_name">عنوان حساب</string>
<string name="login_account_name_info">از آدرس ایمیل خود به عنوان نام حساب استفاده کنید زیرا Android از نام حساب به عنوان قسمت ORGANIZER برای رویدادهایی که ایجاد می کنید استفاده خواهد کرد. نمی توانید دو حساب با یک نام داشته باشید.</string>
<string name="login_account_contact_group_method">روش گروه تماس:</string>
<string name="login_account_name_required">نام حساب لازم است</string>
<string name="login_account_name_already_taken">نام حساب قبلاً گرفته شده است</string>
<string name="login_account_not_created">حساب ایجاد نشد</string>
<string name="login_no_certificate_found">گواهی یافت نشد</string>
<string name="login_install_certificate">نصب گواهی</string>
<string name="login_configuration_detection">تشخیص پیکربندی</string>

View File

@@ -4,7 +4,6 @@
<string name="account_invalid">Le compte nexiste plus (supprimé)</string>
<string name="account_title_address_book">Carnet d\'adresses DAVx⁵</string>
<string name="address_books_authority_title">Carnets d\'adresses</string>
<string name="dialog_delete">Supprimer</string>
<string name="dialog_remove">Retirer</string>
<string name="dialog_deny">Annuler</string>
<string name="field_required">Ce champ est requis</string>
@@ -31,27 +30,27 @@
<string name="intro_slogan1">Vos données, votre choix.</string>
<string name="intro_slogan2">Prenez le contrôle.</string>
<string name="intro_battery_title">Intervalles de synchronisation régulières</string>
<string name="intro_battery_text">Pour une synchronisation à intervalles réguliers, %s doit être autorisé à fonctionner en arrière-plan. Sinon, Android peut interrompre la synchronisation à tout moment.</string>
<string name="intro_battery_dont_show">Je n\'ai pas besoin d\'intervalles de synchronisation réguliers.*</string>
<string name="intro_battery_text">Pour une synchronisation à intervalles régulières, %s doit être autorisé à fonctionner en arrière-plan. Sinon, Android peut interrompre la synchronisation à tout moment.</string>
<string name="intro_battery_dont_show">Je n\'ai pas besoin d\'intervalles de synchronisation régulières.*</string>
<string name="intro_autostart_title">%s compatibilité</string>
<string name="intro_autostart_text">Cet appareil bloque probablement la synchronisation. Si vous êtes affecté, vous ne pouvez résoudre ce problème que manuellement.</string>
<string name="intro_autostart_dont_show">J\'ai fait les réglages nécessaires. Ne me le rappelez plus.*</string>
<string name="intro_leave_unchecked">* Laisser non coché pour un rappel ultérieur. Peut être réinitialisé dans les paramètres de l\'application / %s.</string>
<string name="intro_more_info">Plus d\'informations</string>
<string name="intro_tasks_jtx">jtx Board</string>
<string name="intro_tasks_jtx_info"><![CDATA[Prend en charge la synchro des Tâches, Journaux et Notes.]]></string>
<string name="intro_tasks_jtx_info"><![CDATA[Prends en charge la synchro des Tâches, Journaux et Notes.]]></string>
<string name="intro_tasks_title">Gestion des taches</string>
<string name="intro_tasks_text1">Si les tâches sont prises en charge par votre serveur, elles peuvent être synchronisées avec une application de tâches externe :</string>
<string name="intro_tasks_opentasks">OpenTasks</string>
<string name="intro_tasks_opentasks_info">Ne semble plus être développé - non recommandé.</string>
<string name="intro_tasks_tasks_org">Tasks.org</string>
<string name="intro_tasks_tasks_org_info"><![CDATA[Certaines fonctionnalités <a href="https://www.davx5.com/faq/tasks/advanced-task-features">ne sont pas (encore) prises en charge</a>.]]></string>
<string name="intro_tasks_no_app_store">Pas de magasin d\'application disponible</string>
<string name="intro_tasks_dont_show">Je n\'ai pas besoin de support des tâches.*</string>
<string name="intro_open_source_title">Logiciels open-source</string>
<string name="intro_open_source_text">Nous sommes heureux que vous utilisiez %s, qui est un logiciel open-source. Le développement, la maintenance et l\'assistance sont un travail difficile. Veuillez envisager de contribuer (il y a plusieurs façons de le faire) ou de faire un don. Ce serait très apprécié !</string>
<string name="intro_open_source_details">Comment contribuer / donner</string>
<string name="intro_open_source_details">Comment contribuer/donner</string>
<string name="intro_open_source_dont_show">Ne plus afficher dans le futur</string>
<string name="intro_next">Suivant</string>
<!--PermissionsActivity-->
<string name="permissions_title">Autorisations</string>
<string name="permissions_text">%s nécessite des autorisations pour fonctionner correctement.</string>
@@ -88,6 +87,7 @@
<string name="wifi_permissions_background_location_permission_label">Autorisez tout le temps</string>
<string name="wifi_permissions_background_location_permission_on">Autorisation de localisation réglée sur : %s</string>
<string name="wifi_permissions_background_location_permission_off">Autorisation de localisation non réglée sur : %s</string>
<string name="wifi_permissions_background_location_disclaimer">%s utilise l\'autorisation de localisation uniquement pour déterminer le SSID du WiFi actuel pour les comptes à SSID restreint. Cela se produit même lorsque l\'application est en arrière-plan. Aucune donnée de localisation n\'est collectée, stockée, traitée ou envoyée nulle part.</string>
<string name="wifi_permissions_location_enabled">Localisation toujours activée</string>
<string name="wifi_permissions_location_enabled_on">Le service de localisation est activé</string>
<string name="wifi_permissions_location_enabled_off">Le service de localisation est désactivé</string>
@@ -95,6 +95,7 @@
<string name="about_translations">Traductions</string>
<string name="about_libraries">Librairies</string>
<string name="about_version">Version %1$s (%2$d)</string>
<string name="about_build_date">Générée le %s</string>
<string name="about_copyright">© Ricki Hirner, Bernhard Stockmann (bitfire web engineering GmbH) et les contributeurs</string>
<string name="about_license_info_no_warranty">Ce programme est fourni sans AUCUNE GARANTIE. C\'est un logiciel libre, et vous êtes en droit de le redistribuer sous certaines conditions.</string>
<!--global settings-->
@@ -127,6 +128,7 @@
<string name="account_list_manage_battery_saver">Gérer l\'économiseur de batterie</string>
<string name="account_list_low_storage">Espace de stockage faible. Android ne synchronisera pas les changements locaux immédiatement mais pendant la prochaine synchronisation.</string>
<string name="account_list_manage_storage">Gérer le stockage</string>
<string name="account_list_empty">Bienvenue sur DAVx⁵!\n\nVous pouvez maintenant ajouter un compte CalDAV ou CardDAV.</string>
<string name="accounts_sync_all">Synchroniser tous les comptes</string>
<!--RefreshCollectionsWorker-->
<string name="refresh_collections_worker_refresh_failed">La détection du service a échoué</string>
@@ -138,7 +140,9 @@
<string name="app_settings">Paramètres</string>
<string name="app_settings_debug">Débogage</string>
<string name="app_settings_show_debug_info">Afficher les infos de débogage</string>
<string name="app_settings_show_debug_info_details">Voir/partager l\'application et les détails de configuration</string>
<string name="app_settings_logging">Journalisation verbeuse</string>
<string name="app_settings_logging_on">La journalisation est activée</string>
<string name="app_settings_logging_off">La journalisation est désactivée</string>
<string name="app_settings_battery_optimization">Optimisation de la batterie</string>
<string name="app_settings_battery_optimization_exempted">Pas de restriction (recommandé)</string>
@@ -197,11 +201,8 @@
<string name="account_synchronize_this_collection">Synchroniser cette collection</string>
<string name="account_read_only">En lecture seulement</string>
<string name="account_calendar">calendrier</string>
<string name="account_contacts">contacts</string>
<string name="account_journal">journal</string>
<string name="account_task_list">tâches</string>
<string name="account_only_personal">N\'afficher que les comptes personnels</string>
<string name="account_refresh_collections">Rafraîchir la liste</string>
<string name="account_webcal_external_app">Les abonnements Webcal peuvent être synchronisés avec des applications tierces.</string>
<string name="account_no_webcal_handler_found">Aucune application compatible WebCal</string>
<string name="account_install_icsx5">Installer ICSx⁵</string>
@@ -225,13 +226,14 @@
<string name="login_base_url">URL de base</string>
<string name="login_base_url_info"><![CDATA[L\'URL de base sera vérifié directement, et <a href="%s">les services sont aussi découverts</a> en utilisant les enregistrements DNS et les URL connues.]]></string>
<string name="login_select_certificate">Choisir le certificat</string>
<string name="login_add_account">Ajouter un compte</string>
<string name="login_create_account">Créer un compte</string>
<string name="login_account_name">Nom du compte</string>
<string name="login_account_avoid_apostrophe">L\'utilisation d\'apostrophe (\') semble poser problème sur certains appareils.</string>
<string name="login_account_name_info">Utilisez votre adresse de courriel comme nom de compte car Android utilisera ce nom en tant que champ ORGANISATEUR pour les événements que vous créerez. Vous ne pouvez pas avoir deux comptes avec le même nom.</string>
<string name="login_account_contact_group_method">Méthode pour les contacts de type groupe :</string>
<string name="login_account_name_required">Nom du compte requis</string>
<string name="login_account_name_already_taken">Le nom du compte est déjà pris</string>
<string name="login_account_not_created">Le compte n\'a pas pu être créé</string>
<string name="login_type_advanced">Connexion avancée</string>
<string name="login_no_client_certificate_optional">Pas de certificat client*</string>
<string name="login_client_certificate_selected">Certificat client : %s</string>
@@ -257,7 +259,6 @@
<string name="login_querying_server">Veuillez patienter, nous interrogeons le serveur …</string>
<string name="login_no_service">Aucun accès possible au service CalDAV ou CardDAV.</string>
<string name="login_no_service_info">L\'URL de base ne semble pas être une URL CalDAV/CardDAV et la détection du service a échoué.</string>
<string name="login_see_tested_services"><![CDATA[Merci de vous reporter au manuel de votre fournisseur de services et <a href="%s">notre liste de services testés</a> ainsi que leurs URL de base.]]></string>
<string name="login_check_credentials">Merci de bien vérifier l\'authentification (souvent le nom d\'utilisateur et le mot de passe).</string>
<string name="login_logs_available">Plus d\'information technique est disponible dans les journaux.</string>
<string name="login_view_logs">Voir les journaux</string>
@@ -328,38 +329,21 @@
</string-array>
<!--CreateAddressBookScreen, CreateCalendarScreen-->
<string name="create_addressbook">Créer un carnet d\'adresses</string>
<string name="create_addressbook_maybe_not_supported">La création de carnet d\'adresses via CardDAV n\'est peut-être pas supportée par ce serveur.</string>
<string name="create_calendar">Créer un calendrier</string>
<string name="create_calendar_time_zone_optional">Fuseau horaire par défaut*</string>
<string name="create_calendar_time_zone_none"></string>
<string name="create_calendar_type">Entrées possibles de calendrier</string>
<string name="create_calendar_type_vevent">Événements</string>
<string name="create_calendar_type_vtodo">Tâches</string>
<string name="create_calendar_type_vjournal">Notes / journal</string>
<string name="create_calendar_maybe_not_supported">La création de calendrier via CalDAV n\'est peut-être pas supportée par ce serveur.</string>
<string name="create_collection_color">Couleur</string>
<string name="create_collection_display_name">Titre</string>
<string name="create_collection_home_set">Emplacement de stockage</string>
<string name="create_collection_description_optional">Description</string>
<string name="create_collection_create">Créer</string>
<string name="create_collection_optional">* facultatif</string>
<!--CollectionScreen-->
<string name="collection_delete">Supprimer la collection</string>
<string name="collection_delete_warning">Cette collection (%s) et toutes ses données vont être définitivement supprimées, sur cet appareil et sur le serveur.</string>
<string name="collection_synchronization">Synchronisation</string>
<string name="collection_synchronization_on">Synchronisation activée</string>
<string name="collection_synchronization_off">Synchronisation désactivée</string>
<string name="collection_read_only">Lecture seule</string>
<string name="collection_read_only_by_server">Lecture seule (côté serveur)</string>
<string name="collection_read_only_forced">Lecture seule (sur cet appareil)</string>
<string name="collection_read_write">Lecture / écriture</string>
<string name="collection_title">Titre</string>
<string name="collection_description">Description</string>
<string name="collection_owner">Responsable</string>
<string name="collection_push_support">Support du \'Push\'</string>
<string name="collection_push_web_push">Le serveur annonce supporter \'Push\'</string>
<string name="collection_last_sync">Dernière synchro (%s)</string>
<string name="collection_url">Adresses (URL)</string>
<!--debugging and DebugInfoActivity-->
<string name="debug_info_title">Infos de débogage</string>
<string name="debug_info_archive_caption">Archive ZIP</string>

View File

@@ -45,6 +45,7 @@
<string name="intro_tasks_opentasks">OpenTasks</string>
<string name="intro_tasks_opentasks_info">Semella que xa non está en desenvolvemento non recomendado.</string>
<string name="intro_tasks_tasks_org">Task.org</string>
<string name="intro_tasks_tasks_org_info"><![CDATA[Algunhas características <a href="https://www.davx5.com/faq/tasks/advanced-task-features">non están soportadas</a> (aínda).]]></string>
<string name="intro_tasks_no_app_store">Sen tenda de apps dispoñible</string>
<string name="intro_tasks_dont_show">Non necesito soporte para tarefas.*</string>
<string name="intro_open_source_title">Software de código aberto</string>
@@ -87,6 +88,7 @@
<string name="wifi_permissions_background_location_permission_label">Permitir en todo momento</string>
<string name="wifi_permissions_background_location_permission_on">Permiso de localización establecido como: %s</string>
<string name="wifi_permissions_background_location_permission_off">O permiso de localización non é: %s</string>
<string name="wifi_permissions_background_location_disclaimer">%s utiliza o permiso de Localización só para determinar o SSID da WiFi para contas restrinxidas a uns SSIDs. Esto acontecerá incluso cando a app está en segundo plano. Non se recollen datos de localización, nin se gardan, procesan ou envían a ningún sitio.</string>
<string name="wifi_permissions_location_enabled">Localización sempre activada</string>
<string name="wifi_permissions_location_enabled_on">Servizo de localización activado</string>
<string name="wifi_permissions_location_enabled_off">Servizo de localización desactivado</string>
@@ -94,6 +96,7 @@
<string name="about_translations">Traducións</string>
<string name="about_libraries">Bibliotecas</string>
<string name="about_version">Versión %1$s (%2$d)</string>
<string name="about_build_date">Compilada en %s</string>
<string name="about_copyright">© Ricki Hirner, Bernhard Stockmann (bitfire web engineering GmbH) e colaboradoras</string>
<string name="about_license_info_no_warranty">Este programa non proporciona NINGUNHA GARANTÍA. É software libre, e convidámoste a redistribuílo baixo certas condicións.</string>
<!--global settings-->
@@ -126,6 +129,7 @@
<string name="account_list_manage_battery_saver">Xestionar aforro da batería</string>
<string name="account_list_low_storage">Queda pouco espazo de almacenaxe. Android non vai sincronizar os cambios locais inmediatamente, farao na próxima sincronización regular.</string>
<string name="account_list_manage_storage">Xestionar almacenaxe</string>
<string name="account_list_empty">Benvida a DAVx⁵!\n\nXa podes engadir unha conta CalDAV/CardDAV</string>
<string name="accounts_sync_all">Sincroniza todas as contas</string>
<!--RefreshCollectionsWorker-->
<string name="refresh_collections_worker_refresh_failed">Fallou a detección do servizo</string>
@@ -137,7 +141,9 @@
<string name="app_settings">Axustes</string>
<string name="app_settings_debug">Depurando</string>
<string name="app_settings_show_debug_info">Mostrar info de depuración</string>
<string name="app_settings_show_debug_info_details">Ver/compartir detalles do software e configuración</string>
<string name="app_settings_logging">Rexistro polo miúdo</string>
<string name="app_settings_logging_on">O rexistro está activo</string>
<string name="app_settings_logging_off">O rexistro está desactivado</string>
<string name="app_settings_battery_optimization">Optimización da batería</string>
<string name="app_settings_battery_optimization_exempted">A app está excluída (recomendado)</string>
@@ -225,13 +231,14 @@
<string name="login_base_url">URL base</string>
<string name="login_base_url_info"><![CDATA[O URL base compróbase directamente, mais os <a href="%s">servizos tamén se atopan</a> usando rexistros DNS e URLs tipo well-known.]]></string>
<string name="login_select_certificate">Escoller certificado</string>
<string name="login_add_account">Engadir conta</string>
<string name="login_create_account">Crear conta</string>
<string name="login_account_name">Nome da conta</string>
<string name="login_account_avoid_apostrophe">O uso de apóstrofes (\') semella que causa problemas nalgúns dispositivos.</string>
<string name="login_account_name_info">Utiliza o teu enderezo de correo electrónico como nome de conta xa que Android utilizará o nome da conta como campo ORGANIZADOR para os eventos que cree. Non poderás ter dúas contas co mesmo nome.</string>
<string name="login_account_contact_group_method">Método para agrupar contacto:</string>
<string name="login_account_name_required">Nome de conta requerido</string>
<string name="login_account_name_already_taken">O nome de conta xa está a ser utilizado</string>
<string name="login_account_not_created">Non se creou a conta</string>
<string name="login_type_advanced">Acceso avanzado</string>
<string name="login_no_client_certificate_optional">Sen certificado cliente*</string>
<string name="login_client_certificate_selected">Certificado cliente: %s</string>

View File

@@ -31,6 +31,7 @@
<string name="intro_tasks_title">Podrška za zadatke</string>
<string name="intro_tasks_text1">Ukoliko su zadatci podržani od strane vašeg poslužitelja, moguće ih je sinkronizirati sa podržanom aplikacijom za zadatke:</string>
<string name="intro_tasks_opentasks">OpenTasks</string>
<string name="intro_tasks_tasks_org_info"><![CDATA[Neke mogućnosti <a href="https://www.davx5.com/faq/tasks/advanced-task-features">nisu podržane</a> (još).]]></string>
<string name="intro_tasks_no_app_store">Trgovina aplikacijama nije dostupna</string>
<string name="intro_tasks_dont_show">Ne trebam podršku za zadatke.*</string>
<string name="intro_open_source_title">Softver otvorenog koda</string>
@@ -65,6 +66,7 @@
<string name="wifi_permissions_location_permission_off">Dopuštenja lokacije odbijena</string>
<string name="wifi_permissions_background_location_permission">Pozadinska dopuštenja lokacije</string>
<string name="wifi_permissions_background_location_permission_label">Dopusti cijelo vrijeme</string>
<string name="wifi_permissions_background_location_disclaimer">%s koristi dopuštenje za lokaciju samo kako bi se utvrdilo sadašnje WiFi SSID za SSID-ograničenim računa. To će se dogoditi čak i kada je aplikacija u pozadini. Podaci o lokaciji se ne prikupljaju, pohranjuju, obrađuju ili šalju bilo gdje.</string>
<string name="wifi_permissions_location_enabled">Lokacija uvijek omogućena</string>
<string name="wifi_permissions_location_enabled_on">Lokacijske usluge su omogućene</string>
<string name="wifi_permissions_location_enabled_off">Lokacijske usluge su onemogućene</string>
@@ -72,6 +74,7 @@
<string name="about_translations">Prijevodi</string>
<string name="about_libraries">Bibiloteke</string>
<string name="about_version">Verzija %1$s(%2$d)</string>
<string name="about_build_date">Kompilirano na %s</string>
<string name="about_license_info_no_warranty">Ovaj program dolazi BEZ APSOLUTNO BILO KAKVOG JAMSTVA. To je besplatni softver i možete ga distribuirati pod određenim uvjetima. </string>
<!--global settings-->
<string name="logging_couldnt_create_file">Couldn\'t create log file</string>
@@ -90,6 +93,7 @@
<string name="navigation_drawer_manual">Upute</string>
<string name="navigation_drawer_faq">FAQ</string>
<string name="navigation_drawer_privacy_policy">Pravila o zaštiti privatnosti</string>
<string name="account_list_empty">Dobrodošli u DAVx⁵!\n\nSada možete dodati CalDAV/CardDAV račun.</string>
<string name="accounts_sync_all">Sinkroniziraj sve račune</string>
<!--RefreshCollectionsWorker-->
<string name="refresh_collections_worker_refresh_failed">Detekcija servisa nije uspjela</string>
@@ -101,7 +105,9 @@
<string name="app_settings">Postavke</string>
<string name="app_settings_debug">Debugging</string>
<string name="app_settings_show_debug_info">Prikaži debug informacije</string>
<string name="app_settings_show_debug_info_details">Pregledaj/podijeli softver i konfiguracijske detalje</string>
<string name="app_settings_logging">Verbose logging</string>
<string name="app_settings_logging_on">Logging je aktivan</string>
<string name="app_settings_logging_off">Logging je onemogućen</string>
<string name="app_settings_connection">Veza</string>
<string name="app_settings_security">Sigurnost</string>
@@ -152,12 +158,13 @@
<string name="login_user_name">Korisničko ime</string>
<string name="login_base_url">Osnovni URL</string>
<string name="login_select_certificate">Odaberi certifikat</string>
<string name="login_add_account">Dodaj račun</string>
<string name="login_create_account">Kreiraj račun</string>
<string name="login_account_name">Naziv računa</string>
<string name="login_account_name_info">Koristite svoju adresu e-pošte kao naziv računa jer Android će koristiti naziv računa kao ORGANIZER polje za događaje koje kreirate. Nije moguće imati dva računa sa istim imenom.</string>
<string name="login_account_contact_group_method">Metoda kontaktnih grupa:</string>
<string name="login_account_name_required">Potreban je naziv računa</string>
<string name="login_account_name_already_taken">Naziv računa se već koristi</string>
<string name="login_account_not_created">Nije moguće napraviti račun</string>
<string name="login_no_certificate_found">Certifikat nije pronađen</string>
<string name="login_install_certificate">Instaliraj certifikat</string>
<string name="login_configuration_detection">Detektiranje konfiguracije</string>

View File

@@ -44,13 +44,13 @@
<string name="intro_tasks_opentasks">OpenTasks</string>
<string name="intro_tasks_opentasks_info">A fejlesztése leállt nem ajánlott.</string>
<string name="intro_tasks_tasks_org">Tasks.org</string>
<string name="intro_tasks_tasks_org_info"><![CDATA[Néhány funkció <a href="https://www.davx5.com/faq/tasks/advanced-task-features">nem támogatott</a> (még).]]></string>
<string name="intro_tasks_no_app_store">Nincs elérhető alkalmazás-áruház</string>
<string name="intro_tasks_dont_show">Nincs szükségem a feladatok támogatására.*</string>
<string name="intro_open_source_title">Nyílt forráskódú szoftver</string>
<string name="intro_open_source_text">Nagyon örülünk, hogy a %s felhasználói közé tartozik, amely nyílt forráskódú szoftver. A fejlesztés, karbantartás és támogatás ugyanakkor kemény munkát igényel. Fontolja meg, hogy ezt pénzzel, vagy valamilyen más módon támogassa (több lehetőség közül is választhat). Nagyon megköszönnénk!</string>
<string name="intro_open_source_details">A hozzájárulás lehetőségei</string>
<string name="intro_open_source_dont_show">Ne mutassa a közeljövőben</string>
<string name="intro_next">Tovább</string>
<!--PermissionsActivity-->
<string name="permissions_title">Engedélyek</string>
<string name="permissions_text">A %s megfelelő működése bizonyos engedélyeket igényel.</string>
@@ -87,6 +87,7 @@
<string name="wifi_permissions_background_location_permission_label">Engedélyezés mindig</string>
<string name="wifi_permissions_background_location_permission_on">A Helyadatok engedély erre van állítva: %s</string>
<string name="wifi_permissions_background_location_permission_off">A Helyadatok engedély nincs erre állítva: %s</string>
<string name="wifi_permissions_background_location_disclaimer">%s a Helyadatok engedélyt csak arra használja, hogy a jelenlegi WiFi SSID-jét meghatározza az SSID-ra korlátozott fiókokhoz. Ez a funkció akkor is működik, ha az alkalmazás háttérben van. Semmilyen helyadatot nem gyűjtünk, tárolunk, dolgozunk fel vagy küldünk bárhová.</string>
<string name="wifi_permissions_location_enabled">A helyadatok mindig engedélyezve vannak</string>
<string name="wifi_permissions_location_enabled_on">A helyadat-szolgáltatás bekapcsolva</string>
<string name="wifi_permissions_location_enabled_off">A helyadat-szolgáltatás kikapcsolva</string>
@@ -94,6 +95,7 @@
<string name="about_translations">Fordítások</string>
<string name="about_libraries">Programkönyvtárak</string>
<string name="about_version">Verziószám:%1$s (%2$d)</string>
<string name="about_build_date">Fordítás ideje: %s</string>
<string name="about_copyright">© Ricki Hirner, Bernhard Stockmann (bitfire web engineering GmbH) és a közreműködők</string>
<string name="about_license_info_no_warranty">Ehhez a program SEMMIFÉLE GARANCIA NEM JÁR. Szabad szoftver, amely bizonyos feltételek mellett szabadon terjeszthető.</string>
<!--global settings-->
@@ -126,6 +128,7 @@
<string name="account_list_manage_battery_saver">Akkumulátorkímélés kezelése</string>
<string name="account_list_low_storage">A rendelkezésre álló tárhely kevés. Az Android nem fogja a helyi erőforrásokat azonnal szinkronizálni, csak a következő rendes alkalommal.</string>
<string name="account_list_manage_storage">Tárhely kezelése</string>
<string name="account_list_empty">Üdvözöljük a DAVx⁵ felhasználók között!\n\nMost már felvehet CalDAV/CardDav fiókokat.</string>
<string name="accounts_sync_all">Az összes fiók szinkronizálása</string>
<!--RefreshCollectionsWorker-->
<string name="refresh_collections_worker_refresh_failed">Szolgáltatások felderítése nem sikerült</string>
@@ -137,7 +140,9 @@
<string name="app_settings">Beállítások</string>
<string name="app_settings_debug">Hibakeresés</string>
<string name="app_settings_show_debug_info">Hibakeresési információ megtekintése</string>
<string name="app_settings_show_debug_info_details">Szoftver- és konfigurációs részletek megtekintése/megosztása</string>
<string name="app_settings_logging">Részletes naplózás</string>
<string name="app_settings_logging_on">Naplózás bekapcsolva</string>
<string name="app_settings_logging_off">Naplózás kikapcsolva</string>
<string name="app_settings_battery_optimization">Akkumulátorhasználat optimalizálása</string>
<string name="app_settings_battery_optimization_exempted">Az alkalmazás kivétel alóla (ajánlott)</string>
@@ -221,13 +226,14 @@
<string name="login_base_url">Alapwebcím</string>
<string name="login_base_url_info"><![CDATA[Az alapwebcím közvetlenül lesz ellenőrizve, de a <a href="%s">szolgáltatások felfedezése</a> DNS rekordok és jól ismert webcímek alapján is történik.]]></string>
<string name="login_select_certificate">Tanúsítvány kiválasztása</string>
<string name="login_add_account">Fiók hozzáadása</string>
<string name="login_create_account">Fiók létrehozása</string>
<string name="login_account_name">A fiók neve</string>
<string name="login_account_avoid_apostrophe">Az aposztrófok (\') használata a visszajelzések szerinte egyes eszközökön problémát okoz.</string>
<string name="login_account_name_info">Használja az e-mail-címét fióknévként, mert később a létrehozandó események szervezőjeként (ORGANIZER mező) az Android ezt fogja használni. Két fiókot nem lehet azonos néven létrehozni.</string>
<string name="login_account_contact_group_method">A csoportok kezelésének módja:</string>
<string name="login_account_name_required">A fióknév kötelező</string>
<string name="login_account_name_already_taken">A fióknév már használatban van</string>
<string name="login_account_not_created">A fiók létrehozása nem sikerült</string>
<string name="login_type_advanced">Speciális bejelentkezés</string>
<string name="login_no_client_certificate_optional">Nincs klienstanúsítvány*</string>
<string name="login_client_certificate_selected">Klienstanúsítvány: %s</string>

View File

@@ -43,6 +43,7 @@
<string name="intro_tasks_opentasks">OpenTasks</string>
<string name="intro_tasks_opentasks_info">Non sembra essere più sviluppato - non raccomandato.</string>
<string name="intro_tasks_tasks_org">Tasks.org</string>
<string name="intro_tasks_tasks_org_info"><![CDATA[Alcune funzionalità <a href="https://www.davx5.com/faq/tasks/advanced-task-features"> non sono (ancora) supportate </a>.]]></string>
<string name="intro_tasks_no_app_store">Nessun app store disponibile</string>
<string name="intro_tasks_dont_show">Non ho bisogno del supporto alle attività.*</string>
<string name="intro_open_source_title">Software open-source</string>
@@ -83,6 +84,7 @@
<string name="wifi_permissions_background_location_permission_label">Permettere sempre</string>
<string name="wifi_permissions_background_location_permission_on">Permessi di localizzazione impostati a: %s</string>
<string name="wifi_permissions_background_location_permission_off">Permessi di localizzazione non impostati a: %s</string>
<string name="wifi_permissions_background_location_disclaimer">1%s utilizza l\'autorizzazione della posizione solo per determinare l\'SSID del WIFI attuale per gli account SSID-limitati. Questo accade anche quando l\'applicazione è in modalità background. Nessun dato sulla posizione è raccolto, salvato, processato o mandato da qualche parte.</string>
<string name="wifi_permissions_location_enabled">Posizione sempre disabilitata</string>
<string name="wifi_permissions_location_enabled_on">Servizio di posizione abiltato</string>
<string name="wifi_permissions_location_enabled_off">Servizio di posizione disabilitato</string>
@@ -90,6 +92,7 @@
<string name="about_translations">Traduzioni</string>
<string name="about_libraries">Librerie</string>
<string name="about_version">Versione %1$s (%2$d)</string>
<string name="about_build_date">Compilato su %s</string>
<string name="about_copyright">© Ricki Hirner, Bernhard Stockmann (bitfire web engineering GmbH) e contibutori</string>
<string name="about_license_info_no_warranty">Il programma è distribuito SENZA ALCUNA GARANZIA. È software libero e può essere redistribuito sotto alcune condizioni.</string>
<!--global settings-->
@@ -121,6 +124,7 @@
<string name="account_list_manage_battery_saver">Gestisci risparmio energetico</string>
<string name="account_list_low_storage">Spazio di memorizzazione scarso. Androin non salverà immediatamente i cambiamente, ma alla prossima sincronizzazione programmata.</string>
<string name="account_list_manage_storage">Gestisci spazio di memorizzazione</string>
<string name="account_list_empty">Benvenuto a DAVx⁵!\n\nÈ ora possibile aggiungere account CalDAV/CardDAV.</string>
<string name="accounts_sync_all">Sincronizzazione di tutti gli account</string>
<!--RefreshCollectionsWorker-->
<string name="refresh_collections_worker_refresh_failed">Impossibile trovare il servizio</string>
@@ -132,7 +136,9 @@
<string name="app_settings">Impostazioni</string>
<string name="app_settings_debug">Debug</string>
<string name="app_settings_show_debug_info">Mostra informazioni di debug</string>
<string name="app_settings_show_debug_info_details">Mostra e condividi i dettagli del programma e della configurazione</string>
<string name="app_settings_logging">Log completo</string>
<string name="app_settings_logging_on">Log attivo</string>
<string name="app_settings_logging_off">Log disabilitato</string>
<string name="app_settings_battery_optimization">Ottimizzazione batteria</string>
<string name="app_settings_connection">Connessione</string>
@@ -217,13 +223,14 @@
<string name="login_base_url">Base URL</string>
<string name="login_base_url_info"><![CDATA[La URL base viene controllata direttamente, ma <a href="%s">i servizi sono individuati anche </a> usando record DNS records e le URL well-known.]]></string>
<string name="login_select_certificate">Seleziona certificato</string>
<string name="login_add_account">Aggiungi account</string>
<string name="login_create_account">Crea account</string>
<string name="login_account_name">Nome account</string>
<string name="login_account_avoid_apostrophe">L\'uso degli apostrofi (\') potrebbe causare problemi su alcuni dispositivi.</string>
<string name="login_account_name_info">Inserisci il tuo indirizzo email come nome dell\'account in quanto Android userà il nome dell\'account nel campo ORGANIZER degli eventi creati. Non è possibile avere due account con nome uguale.</string>
<string name="login_account_contact_group_method">Metodo del contact group:</string>
<string name="login_account_name_required">Richiesto il nome dell\'account</string>
<string name="login_account_name_already_taken">Nome account già usato</string>
<string name="login_account_not_created">L\'account non può essere creato</string>
<string name="login_type_advanced">Login avanzato</string>
<string name="login_no_client_certificate_optional">Nessun certificato client*</string>
<string name="login_client_certificate_selected">Certificato client: %s</string>

View File

@@ -45,13 +45,13 @@
<string name="intro_tasks_opentasks">OpenTasks</string>
<string name="intro_tasks_opentasks_info">開発を停止している可能性があります 非推奨</string>
<string name="intro_tasks_tasks_org">Tasks.org</string>
<string name="intro_tasks_tasks_org_info"><![CDATA[いくつかの機能は (まだ) <a href="https://www.davx5.com/faq/tasks/advanced-task-features">対応していません</a>]]></string>
<string name="intro_tasks_no_app_store">アプリストアが利用できません</string>
<string name="intro_tasks_dont_show">ToDo リスト対応は不要です*</string>
<string name="intro_open_source_title">オープンソースソフトウェア</string>
<string name="intro_open_source_text">オープンソースソフトウェアとして %s をお届けできることをとても嬉しく思っています。開発・維持・サポートは簡単ではありません。貢献 (さまざまな方法があります) や寄付をご検討ください。プロジェクトはそれらに支えられています。</string>
<string name="intro_open_source_details">貢献/寄付の方法</string>
<string name="intro_open_source_dont_show">しばらく非表示にする</string>
<string name="intro_next">次へ</string>
<!--PermissionsActivity-->
<string name="permissions_title">許可</string>
<string name="permissions_text">%s が正しく動作するには権限を許可する必要があります</string>
@@ -88,6 +88,7 @@
<string name="wifi_permissions_background_location_permission_label">常に許可</string>
<string name="wifi_permissions_background_location_permission_on">位置情報の権限は %s に設定されています</string>
<string name="wifi_permissions_background_location_permission_off">位置情報の権限は %s に設定されていません</string>
<string name="wifi_permissions_background_location_disclaimer">%s は位置情報の権限を SSID で制限されたアカウントのために現在の WiFi SSID を確認する目的にのみ使用します。これはアプリがバックグラウンドでも発生します。位置情報データが収集、保存、処理および送信されることはありません。</string>
<string name="wifi_permissions_location_enabled">位置情報は常に有効です</string>
<string name="wifi_permissions_location_enabled_on">位置情報サービスは有効です</string>
<string name="wifi_permissions_location_enabled_off">位置情報サービスは無効です</string>
@@ -95,6 +96,7 @@
<string name="about_translations">翻訳</string>
<string name="about_libraries">ライブラリー</string>
<string name="about_version">バージョン %1$s (%2$d)</string>
<string name="about_build_date">コンパイル日時 %s</string>
<string name="about_copyright">© Ricki Hirner、Bernhard Stockmann (bitfire web engineering GmbH) と貢献者</string>
<string name="about_license_info_no_warranty">このプログラムは完全に無保証で提供されます。これはフリーソフトウェアで、特定の条件下での再頒布を歓迎します。</string>
<!--global settings-->
@@ -127,6 +129,7 @@
<string name="account_list_manage_battery_saver">バッテリー最適化機能を管理</string>
<string name="account_list_low_storage">ストレージの容量が残りわずかです。Android はローカルの変更を即座に同期しませんが、次の通常サイクルで同期します。</string>
<string name="account_list_manage_storage">ストレージを管理</string>
<string name="account_list_empty">DAVx⁵ にようこそ!\n\nCalDAV/CardDAV アカウントを追加できるようになりました。</string>
<string name="accounts_sync_all">すべてのアカウントを同期</string>
<!--RefreshCollectionsWorker-->
<string name="refresh_collections_worker_refresh_failed">サービスの検出に失敗しました</string>
@@ -138,7 +141,9 @@
<string name="app_settings">設定</string>
<string name="app_settings_debug">デバッグ</string>
<string name="app_settings_show_debug_info">デバッグ情報を表示</string>
<string name="app_settings_show_debug_info_details">ソフトウェアと設定の詳細を表示/共有します</string>
<string name="app_settings_logging">詳細ログ</string>
<string name="app_settings_logging_on">ログを取得します</string>
<string name="app_settings_logging_off">ログを取得しません</string>
<string name="app_settings_battery_optimization">バッテリー最適化</string>
<string name="app_settings_battery_optimization_exempted">アプリは除外されています (推奨)</string>
@@ -225,13 +230,14 @@
<string name="login_base_url">ベース URL</string>
<string name="login_base_url_info"><![CDATA[ベース URL は直接確認します。ただし、<a href="%s">サービスの検出</a>には DNS レコードと well-known URL も使用します。]]></string>
<string name="login_select_certificate">証明書を選択</string>
<string name="login_add_account">アカウントを追加</string>
<string name="login_create_account">アカウントを作成</string>
<string name="login_account_name">アカウント名</string>
<string name="login_account_avoid_apostrophe">アポストロフィー「\'」を使用すると一部のデバイスで問題が発生します。</string>
<string name="login_account_name_info">Android はあなたが作成した予定の ORGANIZER フィールドにアカウント名を使用するので、アカウント名としてメールアドレスを使用してください。同じ名前のアカウントを 2 つ持つことはできません。</string>
<string name="login_account_contact_group_method">連絡先グループ方法:</string>
<string name="login_account_name_required">アカウント名が必要です</string>
<string name="login_account_name_already_taken">アカウント名はすでに取得されています</string>
<string name="login_account_not_created">アカウントを作成できません</string>
<string name="login_type_advanced">高度なログイン</string>
<string name="login_no_client_certificate_optional">クライアント証明書なし*</string>
<string name="login_client_certificate_selected">クライアント証明書: %s</string>

View File

@@ -45,6 +45,7 @@
<string name="intro_tasks_opentasks">OpenTasks</string>
<string name="intro_tasks_opentasks_info">აღარ მიმდინარეობს განვითარება - არ არის რეკომენდებული.</string>
<string name="intro_tasks_tasks_org">Tasks.org</string>
<string name="intro_tasks_tasks_org_info"><![CDATA[Some features <a href="https://www.davx5.com/faq/tasks/advanced-task-features">are not supported</a> (yet).]]></string>
<string name="intro_tasks_no_app_store">აპების მაღაზია ხელმიუწვდომია</string>
<string name="intro_tasks_dont_show">მე არ მჭირდება დავალებების მხარდაჭერა.*</string>
<string name="intro_open_source_title">ღია კოდის პროგრამული უზრუნველყოფა</string>
@@ -87,6 +88,7 @@
<string name="wifi_permissions_background_location_permission_label">ყოველთვის დაშვება</string>
<string name="wifi_permissions_background_location_permission_on">ადგილმდებარეობის უფლების მნიშვნელობა: %s</string>
<string name="wifi_permissions_background_location_permission_off">ადგილმდებარეობის უფლება არ არის შემდეგი: %s</string>
<string name="wifi_permissions_background_location_disclaimer">%s იყენებს ადგილმდებარეობის უფლებას მხოლოდ რათ დაადგინოს მიმდინარე WiFi-ს SSID SSID-თი შეზღუდული ანგარიშებისთვის. ეს მოხდება მაშინაც, როდესაც აპი ფონურ რეჟიმშია. არანაირი ადგილმდებარეობიაპის მონაცემები არ იკრიფება, ინახება, მუშავდება ან სადმე იგზავნება.</string>
<string name="wifi_permissions_location_enabled">ადგილმდებარეობა ყოველთვის ჩართულია</string>
<string name="wifi_permissions_location_enabled_on">ადგილმდებარეობის სერვისი ჩართულია</string>
<string name="wifi_permissions_location_enabled_off">ადგილმდებარეობის სერვისი გათიშულია</string>
@@ -94,6 +96,7 @@
<string name="about_translations">თარგმანი</string>
<string name="about_libraries">ბიბლიოთეკები</string>
<string name="about_version">ვერსია %1$s (%2$d)</string>
<string name="about_build_date">კომპილირებულია %s-ს</string>
<string name="about_copyright">© Ricki Hirner, Bernhard Stockmann (bitfire web engineering GmbH) და მონაწილეები</string>
<string name="about_license_info_no_warranty">ამ პროგრამას არ აქვს არანაირი გარანტია. იგი არის უფასო პროგრამული უზრუნველყოფა, ხოლო თქვენ შეგეძლეიათ იგი გაავრცელოთ გარკვეული პირობების გათვალისწინებით.</string>
<!--global settings-->
@@ -126,6 +129,7 @@
<string name="account_list_manage_battery_saver">კვების ელემენტის შემნახველის მართვა</string>
<string name="account_list_low_storage">მეხსიერება ცოტა დარჩა. Android არ დაასინქრონიზირებს ადგილობრივ ცვლილებებს დაუყონებლივ, ხოლო დაასინქრონიზირებს შემდეგი რეგულარული სინქრონიზაციის დროს.</string>
<string name="account_list_manage_storage">მეხსიერების მართვა</string>
<string name="account_list_empty">კეტილი იყოს თქვენი მობრძანება DAVx⁵-ში!\n\nაწი შეგიძლიათ დაამატოთ CalDAV/CardDAV ანგარიში.</string>
<string name="accounts_sync_all">ყველა ანგარიშის სინქრონიზაცია</string>
<!--RefreshCollectionsWorker-->
<string name="refresh_collections_worker_refresh_failed">სერვისის აღმოჩენა ჩაიშალა</string>
@@ -137,7 +141,9 @@
<string name="app_settings">პარამეტრები</string>
<string name="app_settings_debug">დებაგი</string>
<string name="app_settings_show_debug_info">დებაგის ინფორმაციის ჩვენება</string>
<string name="app_settings_show_debug_info_details">პროგრამული უზრუნველყოფის და კონფიგურაციის დეტალების ნახვა/გაზიარება</string>
<string name="app_settings_logging">დეტალური ჟურნალში ჩაწერა</string>
<string name="app_settings_logging_on">ჟურნალში ჩაწერა აქტიურია</string>
<string name="app_settings_logging_off">ჟურნალში ჩაწერა გათიშულია</string>
<string name="app_settings_battery_optimization">კვების ელემენტის ოპტიმიზაცია</string>
<string name="app_settings_battery_optimization_exempted">აპი გამორიცხულია (რეკომენდებულია)</string>
@@ -224,13 +230,14 @@
<string name="login_base_url">საბაზო URL</string>
<string name="login_base_url_info"><![CDATA[საბაზო URL-ი პირადპირ იქნება შემოწმებული, მაგრამ <a href="%s">ასევე აღმოჩენილია სერვისები</a> DNS ჩანაწერების და კარგად ცნობილი URL-ების მეშვეობით.]]></string>
<string name="login_select_certificate">სერტიფიკატის არჩევა</string>
<string name="login_add_account">ანგარიშის დამატებ</string>
<string name="login_create_account">ანგარიშის შექმნ</string>
<string name="login_account_name">ანგარიშის სახელი</string>
<string name="login_account_avoid_apostrophe">აპოსტროფების (\') გამოყენება იწვევს პრობლემებს ზოგ მოწყობილობაზე.</string>
<string name="login_account_name_info">გამოიყენეთ თქვენი ელ. ფოსტის მსიამართი ანგარიშის სახელად, რადგან Android გამოიყენებს ანგარიშის სახელს ორგანიზატორის ველში თქვენს მიერ შექმნილ ღონისძიებებისთვის. თქვენ არ შეიძლება გქონდეთ ორი ანგარიში იგივე სახელით.</string>
<string name="login_account_contact_group_method">კონტაქტების დაჯგუფების მეთოდი:</string>
<string name="login_account_name_required">საჭიროა ანგარიშის სახელი</string>
<string name="login_account_name_already_taken">ანგარიშის სახელი უკვე დაკავებულია</string>
<string name="login_account_not_created">ანგარიში ვერ შეიქმნა</string>
<string name="login_type_advanced">გაფართოებული შესვლა</string>
<string name="login_no_client_certificate_optional">კლიენტის სერტიფიკატი არ არის*</string>
<string name="login_client_certificate_selected">კლიენტის სერტიფიკატი: %s</string>

View File

@@ -36,6 +36,7 @@
<string name="intro_tasks_title">작업 지원</string>
<string name="intro_tasks_text1">서버에서 작업을 지원하는 경우 지원되는 작업 앱과 동기화할 수 있습니다.</string>
<string name="intro_tasks_opentasks">OpenTasks</string>
<string name="intro_tasks_tasks_org_info"><![CDATA[일부 기능은 <a href="https://www.davx5.com/faq/tasks/advanced-task-features">아직 지원되지 않습니다.</a>(yet)]]></string>
<string name="intro_tasks_no_app_store">사용 가능한 앱 스토어 없음</string>
<string name="intro_tasks_dont_show">업무 지원은 필요 없습니다.*</string>
<string name="intro_open_source_title">오픈-소스 소프트웨어</string>
@@ -73,6 +74,7 @@
<string name="wifi_permissions_location_permission_off">위치 권한 거부</string>
<string name="wifi_permissions_background_location_permission">백그라운드 위치 권한</string>
<string name="wifi_permissions_background_location_permission_label">항상 허용</string>
<string name="wifi_permissions_background_location_disclaimer">%s은 SSID 제한 계정에 대한 현재 WiFi의 SSID를 확인하는 데만 위치 권한을 사용합니다. 이것은 앱이 백그라운드에 있을 때에도 발생할 것이다. 어떤 위치 데이터도 수집, 저장, 처리 또는 어디로든 전송되지 않습니다.</string>
<string name="wifi_permissions_location_enabled">위치 정보 항상 사용</string>
<string name="wifi_permissions_location_enabled_on">위치 서비스를 사용할 수 있습니다.</string>
<string name="wifi_permissions_location_enabled_off">위치 서비스가 거부되었습니다.</string>
@@ -80,6 +82,7 @@
<string name="about_translations">번역</string>
<string name="about_libraries">라이브러리</string>
<string name="about_version">버전 %1$s (%2$d)</string>
<string name="about_build_date">%s에서 컴파일</string>
<string name="about_copyright">© Ricki Hirner, Bernhard Stockmann (bitfire web engineering GmbH) and contributors</string>
<string name="about_license_info_no_warranty">이 프로그램은 보증 없이 제공됩니다. 그것은 무료 소프트웨어이며, 특정한 조건 하에서 재배포하는 것을 환영합니다.</string>
<!--global settings-->
@@ -101,6 +104,8 @@
<string name="navigation_drawer_faq">FAQ</string>
<string name="navigation_drawer_community">커뮤니티</string>
<string name="navigation_drawer_privacy_policy">개인 정보 보호 정책</string>
<string name="account_list_empty">DAVx⁵에 오신 것을 환영 합니다!/n/n
당신은 CalDAV/CardDAV 계정을 지금 추가 할 수 있습니다.</string>
<string name="accounts_sync_all">모든 계정 동기화</string>
<!--RefreshCollectionsWorker-->
<string name="refresh_collections_worker_refresh_failed">서비스 검색 실패</string>
@@ -112,7 +117,9 @@
<string name="app_settings">설정</string>
<string name="app_settings_debug">Debugging</string>
<string name="app_settings_show_debug_info">debug info 보기</string>
<string name="app_settings_show_debug_info_details">소프트웨어 및 구성 세부 정보 보기/공유</string>
<string name="app_settings_logging">상세 로그</string>
<string name="app_settings_logging_on">logging이 활성되었습니다.</string>
<string name="app_settings_logging_off">logging이 비활성화되었습니다.</string>
<string name="app_settings_battery_optimization">배터리 최적화</string>
<string name="app_settings_connection">연결</string>
@@ -180,12 +187,13 @@
<string name="login_user_name">사용자 이름</string>
<string name="login_base_url">기본 URL</string>
<string name="login_select_certificate">인증서 선택</string>
<string name="login_add_account">계정 추가</string>
<string name="login_create_account">계정 생성</string>
<string name="login_account_name">계정 이름</string>
<string name="login_account_name_info">Android는 사용자가 만든 이벤트에 대해 계정 이름을 ORGANGER 필드로 사용하므로 전자 메일 주소를 계정 이름으로 사용합니다. 이름이 같은 두 개의 계정을 가질 수 없습니다.</string>
<string name="login_account_contact_group_method">연락처 분류 방법:</string>
<string name="login_account_name_required">계정 이름 필요</string>
<string name="login_account_name_already_taken">계정 이름이 이미 사용되었습니다.</string>
<string name="login_account_not_created">계정을 만들 수 없습니다.</string>
<string name="login_no_certificate_found">인증서를 찾을 수 없음</string>
<string name="login_install_certificate">인증서 설치</string>
<string name="login_configuration_detection">구성 탐색</string>

View File

@@ -22,6 +22,7 @@
<!--AboutActivity-->
<string name="about_libraries">Bibliotek</string>
<string name="about_version">Versjon %1$s(%2$d)</string>
<string name="about_build_date">Kompilert på %s</string>
<string name="about_license_info_no_warranty">Dette programmet kommer uten NOEN FORM FOR GARANTI. Det er fri programvare, og du er velkommen til å redistribuere det under gitte forhold.</string>
<!--global settings-->
<string name="logging_couldnt_create_file">Kan ikke opprette loggfil</string>
@@ -35,6 +36,7 @@
<string name="navigation_drawer_website">Nettside</string>
<string name="navigation_drawer_manual">Manuell</string>
<string name="navigation_drawer_faq">O-S-S</string>
<string name="account_list_empty">Velkommen til DAVx⁵.\n\nDu kan legge til en CalDAV/CardDAV-konto nå.</string>
<!--RefreshCollectionsWorker-->
<string name="refresh_collections_worker_refresh_failed">Tjenesteoppdagelse mislyktes</string>
<string name="refresh_collections_worker_refresh_couldnt_refresh">Kunne ikke gjenoppfriske innsamlingsliste</string>
@@ -43,7 +45,9 @@
<string name="app_settings">Innstillinger</string>
<string name="app_settings_debug">Feilretting</string>
<string name="app_settings_show_debug_info">Vis feilrettingsinfo</string>
<string name="app_settings_show_debug_info_details">Vis/del programvare- og oppsettsdetaljer</string>
<string name="app_settings_logging">Grundig logging</string>
<string name="app_settings_logging_on">Logging er aktivert</string>
<string name="app_settings_logging_off">Logging er skrudd av</string>
<string name="app_settings_connection">Tilkobling</string>
<string name="app_settings_security">Sikkerhet</string>
@@ -85,12 +89,13 @@
<string name="login_type_url">Logg inn med nettadresse og brukernavn</string>
<string name="login_user_name">Brukernavn</string>
<string name="login_base_url">Landings-nettadresse</string>
<string name="login_add_account">Legg til konto</string>
<string name="login_create_account">Opprett konto</string>
<string name="login_account_name">Kontonavn</string>
<string name="login_account_name_info">Bruk din e-postadresse som kontonavn fordi Android vil bruke kontonavnet som ORGANISATOR-felt for hendelser du oppretter. Du kan ikke ha to kontoer med samme navn.</string>
<string name="login_account_contact_group_method">Kontaktgruppemetode:</string>
<string name="login_account_name_required">Kontonavn påkrevd</string>
<string name="login_account_name_already_taken">Brukernavnet er allerede i bruk</string>
<string name="login_account_not_created">Kontonavnet kan ikke opprettes</string>
<string name="login_configuration_detection">Oppdagelse av oppsett</string>
<string name="login_querying_server">Vent, spør tjener…</string>
<string name="login_no_service">Fant ikke CalDAV eller CardDAV-tjeneste.</string>

View File

@@ -45,13 +45,13 @@
<string name="intro_tasks_opentasks">OpenTasks</string>
<string name="intro_tasks_opentasks_info">Schijnt niet meer ontwikkeld te worden - niet aanbevolen.</string>
<string name="intro_tasks_tasks_org">Tasks.org</string>
<string name="intro_tasks_tasks_org_info"><![CDATA[Sommige functies<a href="https://www.davx5.com/faq/tasks/advanced-task-features">worden (nog) niet ondersteund</a>.]]></string>
<string name="intro_tasks_no_app_store">Geen app-store beschikbaar</string>
<string name="intro_tasks_dont_show">Ik hoef geen ondersteuning van taken.*</string>
<string name="intro_open_source_title">Open-source software</string>
<string name="intro_open_source_text">We zijn blij dat de keuze valt op open source software %s. Ontwikkelen, onderhouden en ondersteunen is veel werk. Overweeg daarom bij te dragen (kan op vele manieren) of een donatie. Wij waarderen het zeer!</string>
<string name="intro_open_source_details">Hoe bijdragen/doneren</string>
<string name="intro_open_source_dont_show">In de nabije toekomst niet weergeven</string>
<string name="intro_next">Volgende</string>
<!--PermissionsActivity-->
<string name="permissions_title">Rechten toestaan</string>
<string name="permissions_text">%s heeft rechten nodig om goed te werken.</string>
@@ -88,6 +88,7 @@
<string name="wifi_permissions_background_location_permission_label">Onbeperkt toestaan</string>
<string name="wifi_permissions_background_location_permission_on">Locatietoestemming ingesteld op: %s</string>
<string name="wifi_permissions_background_location_permission_off">Locatietoestemming niet ingesteld op: %s</string>
<string name="wifi_permissions_background_location_disclaimer">%sgebruikt het locatie recht alleen om de huidige WiFi-SSID voor SSID-beperkte accounts te bepalen. Dit gebeurt zelfs als de app zich op de achtergrond bevindt. Locatiegegevens worden niet verzameld, opgeslagen, verwerkt of verzonden.</string>
<string name="wifi_permissions_location_enabled">Toegang tot locatie altijd ingeschakeld</string>
<string name="wifi_permissions_location_enabled_on">Toegang tot locatie is ingeschakeld</string>
<string name="wifi_permissions_location_enabled_off">Toegang tot locatie is uitgeschakeld</string>
@@ -95,6 +96,7 @@
<string name="about_translations">Vertalingen</string>
<string name="about_libraries">Bibliotheken</string>
<string name="about_version">Versie%1$s (%2$d)</string>
<string name="about_build_date">Gecompileerd op %s</string>
<string name="about_copyright">© Ricki Hirner, Bernhard Stockmann (bitfire web engineering GmbH) en bijdragers</string>
<string name="about_license_info_no_warranty">Dit programma wordt geleverd met ABSOLUUT GEEN GARANTIE. Het is gratis software, en mag opnieuw worden verspreid onder bepaalde voorwaarden.</string>
<!--global settings-->
@@ -127,6 +129,7 @@
<string name="account_list_manage_battery_saver">Batterijbesparing beheren</string>
<string name="account_list_low_storage">Weinig opslagruimte. Android zal lokale wijzigingen niet onmiddellijk synchroniseren, maar tijdens de volgende reguliere synchronisatie.</string>
<string name="account_list_manage_storage">Opslag beheren</string>
<string name="account_list_empty">Welkom bij DAVx⁵!\n\nJe kunt nu een CalDAV/CardDAV account toevoegen.</string>
<string name="accounts_sync_all">Alle accounts synchroniseren</string>
<!--RefreshCollectionsWorker-->
<string name="refresh_collections_worker_refresh_failed">Service herkenning is mislukt</string>
@@ -138,7 +141,9 @@
<string name="app_settings">Instellingen</string>
<string name="app_settings_debug">Debuggen</string>
<string name="app_settings_show_debug_info">Debug-info</string>
<string name="app_settings_show_debug_info_details">Details over software en configuratie bekijken en delen</string>
<string name="app_settings_logging">Uitgebreid loggen</string>
<string name="app_settings_logging_on">Loggen is actief</string>
<string name="app_settings_logging_off">Loggen is niet actief</string>
<string name="app_settings_battery_optimization">Batterijoptimalisatie</string>
<string name="app_settings_battery_optimization_exempted">App is vrijgesteld (aanbevolen)</string>
@@ -225,13 +230,14 @@
<string name="login_base_url">Basis-URL</string>
<string name="login_base_url_info"><![CDATA[De basis URL wordt direct gecontroleerd, maar <a href="%s">services worden ook ontdekt</a> met behulp van DNS records en bekende URL\'s.]]></string>
<string name="login_select_certificate">Certificaat selecteren</string>
<string name="login_add_account">Account toevoegen</string>
<string name="login_create_account">Account aanmaken</string>
<string name="login_account_name">Accountnaam</string>
<string name="login_account_avoid_apostrophe">Het gebruik van apostrofs (\') lijkt problemen te veroorzaken op sommige apparaten.</string>
<string name="login_account_name_info">Gebruik het eigen e-mailadres als accountnaam, want Android gebruikt het als ORGANIZER veld voor gebeurtenissen. Twee accounts met hetzelfde adres kan niet.</string>
<string name="login_account_contact_group_method">Methode voor contact-groepen:</string>
<string name="login_account_name_required">Accountnaam verplicht</string>
<string name="login_account_name_already_taken">Accountnaam is al in gebruik</string>
<string name="login_account_not_created">Account aanmaken lukt niet.</string>
<string name="login_type_advanced">Geavanceerd inloggen</string>
<string name="login_no_client_certificate_optional">Geen cliëntcertificaat*</string>
<string name="login_client_certificate_selected">Cliëntcertificaat: %s</string>

View File

@@ -41,13 +41,13 @@
<string name="intro_tasks_opentasks">OpenTasks</string>
<string name="intro_tasks_opentasks_info">Wydaje się, że nie jest już rozwijany nie jest zalecany.</string>
<string name="intro_tasks_tasks_org">Tasks.org</string>
<string name="intro_tasks_tasks_org_info"><![CDATA[Niektóre funkcje <a href="https://www.davx5.com/faq/tasks/advanced-task-features">nie są (jeszcze) wspierane</a>.]]></string>
<string name="intro_tasks_no_app_store">Sklep aplikacji nie jest dostępny</string>
<string name="intro_tasks_dont_show">Nie potrzebuję obsługi zadań.*</string>
<string name="intro_open_source_title">Oprogramowanie open-source</string>
<string name="intro_open_source_text">Cieszymy się, że używasz %s, czyli oprogramowania typu open-source. Rozwój, utrzymanie i wsparcie to ciężka praca. Prosimy o rozważenie wniesienia swojego wkładu (jest wiele sposobów) lub darowizny. Byłoby to bardzo cenne!</string>
<string name="intro_open_source_details">Jak wspomóc/wesprzeć</string>
<string name="intro_open_source_dont_show">Nie pokazuj w najbliższej przyszłości</string>
<string name="intro_next">Dalej</string>
<!--PermissionsActivity-->
<string name="permissions_title">Uprawnienia</string>
<string name="permissions_text">%s wymaga uprawnień do prawidłowego działania</string>
@@ -82,6 +82,7 @@
<string name="wifi_permissions_location_permission_off">Uprawnienie lokalizacji odebrane</string>
<string name="wifi_permissions_background_location_permission">Uprawnienie lokalizacji w tle</string>
<string name="wifi_permissions_background_location_permission_label">Zezwól przez cały czas</string>
<string name="wifi_permissions_background_location_disclaimer">%s uzywa uprawnień Lokalizacji tylko dla ustalenia obecnego WiFi\'s SSID dla kont z zatrzeżonym SSID. Będzie się to działo nawet gdy aplikacja działa w tle. Dane o lokalizacji nie są zbierane, zachowywane, przetwarzane ani nigdzie wysyłane.</string>
<string name="wifi_permissions_location_enabled">Lokalizacja zawsze włączona</string>
<string name="wifi_permissions_location_enabled_on">Usługa lokalizacji jest włączona</string>
<string name="wifi_permissions_location_enabled_off">Usługa lokalizacji jest wyłączona</string>
@@ -89,6 +90,7 @@
<string name="about_translations">Tłumaczenia</string>
<string name="about_libraries">Biblioteki</string>
<string name="about_version">Wersja %1$s (%2$d)</string>
<string name="about_build_date">Skompilowano %s</string>
<string name="about_copyright">© Ricki Hirner, Bernhard Stockmann (bitfire web engineering GmbH) i współpracownicy</string>
<string name="about_license_info_no_warranty">Ten program jest ABSOLUTNIE BEZ GWARANCJI. To jest wolne oprogramowanie i mile widziane jest dalsze rozpowszechnianie go zgodnie z warunkami licencji.</string>
<!--global settings-->
@@ -117,6 +119,7 @@
<string name="account_list_manage_datasaver">Zarządzaj oszczędzaniem danych</string>
<string name="account_list_low_storage">Mało miejsca do przechowywania. Android nie zsynchronizuje lokalnych zmian od razu, ale podczas następnej regularnej synchronizacji.</string>
<string name="account_list_manage_storage">Zarządzaj pamięcią</string>
<string name="account_list_empty">Witamy w DAVx⁵!\n\nMożesz teraz dodać konto CalDAV/CardDAV.</string>
<string name="accounts_sync_all">Synchronizuj wszystkie konta</string>
<!--RefreshCollectionsWorker-->
<string name="refresh_collections_worker_refresh_failed">Wykrycie serwisu nie powiodło się</string>
@@ -128,7 +131,9 @@
<string name="app_settings">Ustawienia</string>
<string name="app_settings_debug">Debugowanie</string>
<string name="app_settings_show_debug_info">Pokaż informacje do debugowania</string>
<string name="app_settings_show_debug_info_details">Przejrzyj lub udostępnij informacje o programie i jego konfiguracji</string>
<string name="app_settings_logging">Rozszerzone logowanie</string>
<string name="app_settings_logging_on">Logowanie jest włączone</string>
<string name="app_settings_logging_off">Logowanie jest wyłączone</string>
<string name="app_settings_battery_optimization">Optymalizacja baterii</string>
<string name="app_settings_connection">Łączność</string>
@@ -196,12 +201,13 @@
<string name="login_user_name">Nazwa użytkownika</string>
<string name="login_base_url">Podstawowy adres URL</string>
<string name="login_select_certificate">Wybierz certyfikat</string>
<string name="login_add_account">Dodaj konto</string>
<string name="login_create_account">Stwórz konto</string>
<string name="login_account_name">Nazwa konta</string>
<string name="login_account_name_info">Użyj swojego adresu email jako nazwy konta, ponieważ Android będzie używał nazwy konta jako pola ORGANIZATOR dla wydarzeń, które stworzysz. Nie możesz posiadać dwóch kont o takiej samej nazwie.</string>
<string name="login_account_contact_group_method">Metoda grupowania kontaktów:</string>
<string name="login_account_name_required">Wymagana nazwa konta</string>
<string name="login_account_name_already_taken">Nazwa konta jest już zajęta</string>
<string name="login_account_not_created">Konto nie mogło zostać stworzone</string>
<string name="login_no_certificate_found">Nie znaleziono certyfikatu</string>
<string name="login_install_certificate">Zainstaluj certyfikat</string>
<string name="login_type_google">Kontakty Google / Kalendarz</string>

View File

@@ -45,13 +45,13 @@
<string name="intro_tasks_opentasks">OpenTasks</string>
<string name="intro_tasks_opentasks_info">Parece não ser mais desenvolvido -- não recomendado.</string>
<string name="intro_tasks_tasks_org">Tasks.org</string>
<string name="intro_tasks_tasks_org_info"><![CDATA[Algumas funcionalidades <a href="https://www.davx5.com/faq/tasks/advanced-task-features">não são suportadas</a> (por enquanto).]]></string>
<string name="intro_tasks_no_app_store">Nenhuma loja de aplicativos disponível</string>
<string name="intro_tasks_dont_show">Não preciso de suporte a tarefas.*</string>
<string name="intro_open_source_title">Software Livre</string>
<string name="intro_open_source_text">Estamos felizes por usar o %s, que é um software de código aberto. Desenvolvimento, manutenção e suporte são um trabalho árduo. Considere contribuir (existem várias maneiras) ou fazer uma doação. Seria muito apreciado!</string>
<string name="intro_open_source_details">Como contribuir/doar</string>
<string name="intro_open_source_dont_show">Não mostrar no futuro próximo</string>
<string name="intro_next">Próximo</string>
<!--PermissionsActivity-->
<string name="permissions_title">Permissões</string>
<string name="permissions_text">%s requer permissões para trabalhar corretamente.</string>
@@ -88,6 +88,7 @@
<string name="wifi_permissions_background_location_permission_label">Permitir o tempo todo</string>
<string name="wifi_permissions_background_location_permission_on">Permissão de localização está definida como:%s</string>
<string name="wifi_permissions_background_location_permission_off">Permissão de localização não está definida como:%s</string>
<string name="wifi_permissions_background_location_disclaimer">%s usa a permissão de Localização somente para determinar o SSID do WiFi atual para contas que são restritas por SSID. Isso acontecerá mesmo quando o app está em segundo plano. Nenhum dado de localização é coletado, armazenado, processado, ou enviado a lugar algum.</string>
<string name="wifi_permissions_location_enabled">Localização sempre ativa</string>
<string name="wifi_permissions_location_enabled_on">Serviço de localização está ativado</string>
<string name="wifi_permissions_location_enabled_off">Serviço de localização está desativado</string>
@@ -95,6 +96,7 @@
<string name="about_translations">Traduções</string>
<string name="about_libraries">Bibliotecas</string>
<string name="about_version">Versão %1$s (%2$d)</string>
<string name="about_build_date">Compilado em %s</string>
<string name="about_copyright">© Ricki Hirner, Bernhard Stockmann (bitfire web engineering GmbH) e contribuidores</string>
<string name="about_license_info_no_warranty">Este programa é distribuído SEM NENHUMA GARANTIA. Ele é software livre e pode ser redistribuído sob algumas condições.</string>
<!--global settings-->
@@ -127,6 +129,7 @@
<string name="account_list_manage_battery_saver">Gerenciar a economia de bateria</string>
<string name="account_list_low_storage">Pouco espaço de armazenamento. O Android não sincronizará as mudanças locais imediatamente, mas sim na próxima sincronização regular.</string>
<string name="account_list_manage_storage">Gerenciar armazenamento</string>
<string name="account_list_empty">Bem-vindo(a) ao DAVx⁵!\n\nVocê pode adicionar uma conta CalDAV/CardDAV agora.</string>
<string name="accounts_sync_all">Sincronizar todas as contas</string>
<!--RefreshCollectionsWorker-->
<string name="refresh_collections_worker_refresh_failed">Falha na detecção do serviço</string>
@@ -138,7 +141,9 @@
<string name="app_settings">Configurações</string>
<string name="app_settings_debug">Depuração</string>
<string name="app_settings_show_debug_info">Mostrar informações de depuração</string>
<string name="app_settings_show_debug_info_details">Exibe/compartilha o software e os detalhes da configuração</string>
<string name="app_settings_logging">Registro de atividades detalhado</string>
<string name="app_settings_logging_on">Registro de atividades ativo</string>
<string name="app_settings_logging_off">Registro de atividades desativado</string>
<string name="app_settings_battery_optimization">Otimização da bateria</string>
<string name="app_settings_battery_optimization_exempted">O app está isento (recomendado)</string>
@@ -225,13 +230,14 @@
<string name="login_base_url">URL base</string>
<string name="login_base_url_info"><![CDATA[A base da URL será verificada diretamente, mas <a href="%s">serviços são também descobertos</a> usando DNS e URLs conhecidas.]]></string>
<string name="login_select_certificate">Selecionar certificado</string>
<string name="login_add_account">Adicionar conta</string>
<string name="login_create_account">Criar conta</string>
<string name="login_account_name">Nome da conta</string>
<string name="login_account_avoid_apostrophe">O uso de apóstrofos (\') pode causar problemas em certos dispositivos.</string>
<string name="login_account_name_info">Use seu endereço de e-mail como nome da conta porque o Android irá usar esse nome como campo AGENDA nos eventos que você criar. Não é possível ter duas contas com o mesmo nome.</string>
<string name="login_account_contact_group_method">Método do grupo Contato:</string>
<string name="login_account_name_required">É necessário um nome de conta</string>
<string name="login_account_name_already_taken">O nome da conta já foi utilizado</string>
<string name="login_account_not_created">A conta não pôde ser criada</string>
<string name="login_type_advanced">Login avançado</string>
<string name="login_no_client_certificate_optional">Sem certificado de cliente*</string>
<string name="login_client_certificate_selected">Certificado do cliente: %s</string>

View File

@@ -45,6 +45,7 @@
<string name="intro_tasks_opentasks">OpenTasks</string>
<string name="intro_tasks_opentasks_info">Nu pare a mai fi dezvoltat nu este recomandat.</string>
<string name="intro_tasks_tasks_org">Tasks.org</string>
<string name="intro_tasks_tasks_org_info"><![CDATA[Unele caracteristici <a href="https://www.davx5.com/faq/tasks/advanced-task-features">nu sunt acceptate</a> (încă).]]></string>
<string name="intro_tasks_no_app_store">Nu există un magazin de aplicații disponibil</string>
<string name="intro_tasks_dont_show">Nu am nevoie de suport pentru sarcini.*</string>
<string name="intro_open_source_title">Software cu sursă deschisă</string>
@@ -87,6 +88,7 @@
<string name="wifi_permissions_background_location_permission_label">Permite tot timpul</string>
<string name="wifi_permissions_background_location_permission_on">Permisiunea locației setată la: %s</string>
<string name="wifi_permissions_background_location_permission_off">Permisiunea de locație nu este setată la: %s</string>
<string name="wifi_permissions_background_location_disclaimer">%s folosește permisiunea Locație numai pentru a determina SSID-ul WiFi actual pentru conturile restricționate de SSID. Acest lucru se va întâmpla chiar și atunci când aplicația este în fundal. Nu sunt colectate, stocate, procesate sau trimise nicăieri date despre locație.</string>
<string name="wifi_permissions_location_enabled">Locația este întotdeauna activată</string>
<string name="wifi_permissions_location_enabled_on">Serviciul de localizare este activat</string>
<string name="wifi_permissions_location_enabled_off">Serviciul de localizare este dezactivat</string>
@@ -94,6 +96,7 @@
<string name="about_translations">Traduceri</string>
<string name="about_libraries">Biblioteci</string>
<string name="about_version">Versiune %1$s (%2$d)</string>
<string name="about_build_date">Compilată la %s</string>
<string name="about_copyright">© Ricki Hirner, Bernhard Stockmann (inginerie web bitfire GmbH) și contribuitori</string>
<string name="about_license_info_no_warranty">Acest program vine cu ABSOLUT NICIO GARANȚIE. Este software gratuit și ești binevenit să îl redistribui în anumite condiții.</string>
<!--global settings-->
@@ -126,6 +129,7 @@
<string name="account_list_manage_battery_saver">Gestionează economisirea bateriei</string>
<string name="account_list_low_storage">Spațiu de depozitare redus. Android nu va sincroniza modificările locale imediat, ci în timpul următoarei sincronizări obișnuite.</string>
<string name="account_list_manage_storage">Gestionează stocarea</string>
<string name="account_list_empty">Bun venit la DAVx⁵!\n\nPoți adăuga acum un cont CalDAV/CardDAV.</string>
<string name="accounts_sync_all">Sincronizează toate conturile</string>
<!--RefreshCollectionsWorker-->
<string name="refresh_collections_worker_refresh_failed">Detectarea serviciului a eșuat</string>
@@ -137,7 +141,9 @@
<string name="app_settings">Setări</string>
<string name="app_settings_debug">Depanare</string>
<string name="app_settings_show_debug_info">Afișează informațiile de depanare</string>
<string name="app_settings_show_debug_info_details">Vizualizează/partajează software-ul și detaliile de configurare</string>
<string name="app_settings_logging">Jurnalizare detaliată</string>
<string name="app_settings_logging_on">Înregistrarea este activă</string>
<string name="app_settings_logging_off">Înregistrarea este dezactivată</string>
<string name="app_settings_battery_optimization">Optimizarea bateriei</string>
<string name="app_settings_battery_optimization_exempted">Aplicația este exclusă (recomandat)</string>
@@ -224,13 +230,14 @@
<string name="login_base_url">Adresa URL de bază</string>
<string name="login_base_url_info"><![CDATA[Adresa URL de bază va fi verificată direct, dar <a href="%s">serviciile sunt de asemenea descoperite</a> folosind înregistrări DNS și adrese URL bine-cunoscute.]]></string>
<string name="login_select_certificate">Selectează certificatul</string>
<string name="login_add_account">Adaugă contul</string>
<string name="login_create_account">Creează cont</string>
<string name="login_account_name">Nume de cont</string>
<string name="login_account_avoid_apostrophe">Utilizarea apostrofelor (\') pare să cauzeze probleme pe unele dispozitive.</string>
<string name="login_account_name_info">Utilizează adresa de e-mail ca nume de cont, deoarece Android va folosi numele contului ca câmp ORGANIZATOR pentru evenimentele pe care le creezi. Nu poți avea două conturi cu același nume.</string>
<string name="login_account_contact_group_method">Metoda de grupare a contactelor:</string>
<string name="login_account_name_required">Numele contului este necesar</string>
<string name="login_account_name_already_taken">Numele contului este deja luat</string>
<string name="login_account_not_created">Contul nu a putut fi creat</string>
<string name="login_type_advanced">Autentificare avansată</string>
<string name="login_no_client_certificate_optional">Fără certificat de client*</string>
<string name="login_client_certificate_selected">Certificat de client: %s</string>

View File

@@ -45,13 +45,13 @@
<string name="intro_tasks_opentasks">OpenTasks</string>
<string name="intro_tasks_opentasks_info">По всей видимости, больше не разрабатывается (не рекомендуется)</string>
<string name="intro_tasks_tasks_org">Tasks.org</string>
<string name="intro_tasks_tasks_org_info"><![CDATA[Некоторые возможности <a href="https://www.davx5.com/faq/tasks/advanced-task-features">не поддерживаются</a> (пока).]]></string>
<string name="intro_tasks_no_app_store">Магазин приложений недоступен</string>
<string name="intro_tasks_dont_show">Мне не нужна поддержка задач.*</string>
<string name="intro_open_source_title">ПО с открытым исходным кодом</string>
<string name="intro_open_source_text">Мы рады, что вы используете %s, программное обеспечение с открытым исходным кодом. Разработка, сопровождение и поддержка - это тяжелая работа. Пожалуйста, подумайте о том, чтобы внести свой вклад (существует множество способов) или сделать пожертвование. Будем очень признательны!</string>
<string name="intro_open_source_details">Как внести свой вклад/пожертвовать</string>
<string name="intro_open_source_dont_show">Не показывать в ближайшем будущем</string>
<string name="intro_next">Далее</string>
<!--PermissionsActivity-->
<string name="permissions_title">Разрешения</string>
<string name="permissions_text">Для правильной работы %s требуются разрешения.</string>
@@ -88,6 +88,7 @@
<string name="wifi_permissions_background_location_permission_label">Разрешать всегда</string>
<string name="wifi_permissions_background_location_permission_on">Геолокация задана: %s</string>
<string name="wifi_permissions_background_location_permission_off">Геолокация не задана: %s</string>
<string name="wifi_permissions_background_location_disclaimer">%s использует разрешение на доступ к местоположению только для определения текущего SSID WiFi для аккацнтов с ограничениями SSID. Это будет происходить даже когда приложение находится в фоновом режиме. Никакие данные о местоположении не собираются, не хранятся, не обрабатываются и не отправляются куда-либо.</string>
<string name="wifi_permissions_location_enabled">Определение местоположения всегда включено</string>
<string name="wifi_permissions_location_enabled_on">Служба определения местоположения включена</string>
<string name="wifi_permissions_location_enabled_off">Служба определения местоположения отключена</string>
@@ -95,6 +96,7 @@
<string name="about_translations">Переводы</string>
<string name="about_libraries">Библиотеки</string>
<string name="about_version">Версия %1$s (%2$d)</string>
<string name="about_build_date">Скомпилировано %s</string>
<string name="about_copyright">© Рикки Хирнер (Ricki Hirner), Бернхард Штокманн (Bernhard Stockmann) (bitfire web engineering GmbH) и контрибьюторы</string>
<string name="about_license_info_no_warranty">Эта программа поставляется БЕЗ КАКИХ-ЛИБО ГАРАНТИЙ. Это свободное программное обеспечение и вы можете распространять его при соблюдении определенных условий.</string>
<!--global settings-->
@@ -127,6 +129,7 @@
<string name="account_list_manage_battery_saver">Управлять экономией заряда батареи</string>
<string name="account_list_low_storage">Недостаточно памяти для хранения данных. Android будет синхронизировать локальные изменения не сразу, а во время следующей регулярной синхронизации.</string>
<string name="account_list_manage_storage">Управление хранилищем</string>
<string name="account_list_empty">Добро пожаловать в DAVx⁵!\n\nТеперь вы можете добавить аккаунт CalDAV/CardDAV.</string>
<string name="accounts_sync_all">Синхронизировать все аккаунты</string>
<!--RefreshCollectionsWorker-->
<string name="refresh_collections_worker_refresh_failed">Не удалось обнаружить службу</string>
@@ -138,7 +141,9 @@
<string name="app_settings">Настройки</string>
<string name="app_settings_debug">Отладка</string>
<string name="app_settings_show_debug_info">Показать отладочную информацию</string>
<string name="app_settings_show_debug_info_details">Просмотреть/поделиться сведениями о ПО и конфигурации</string>
<string name="app_settings_logging">Подробное логирование</string>
<string name="app_settings_logging_on">Логирование активно</string>
<string name="app_settings_logging_off">Логирование отключено</string>
<string name="app_settings_battery_optimization">Оптимизация батареи</string>
<string name="app_settings_battery_optimization_exempted">Приложение исключено (рекомендуется)</string>
@@ -225,13 +230,14 @@
<string name="login_base_url">Базовый URL</string>
<string name="login_base_url_info"><![CDATA[Базовый URL проверяется напрямую, но <a href="%s">сервисы также обнаруживаются</a> по записям DNS и известным URL.]]></string>
<string name="login_select_certificate">Выберите сертификат</string>
<string name="login_add_account">Добавить аккаунт</string>
<string name="login_create_account">Создать аккаунт</string>
<string name="login_account_name">Название аккаунта</string>
<string name="login_account_avoid_apostrophe">Использование апострофов (\'), как оказалось, вызывает проблемы на некоторых устройствах.</string>
<string name="login_account_name_info">Укажите ваш адрес email в качестве названия аккаунта, поскольку Android будет его использовать в поле ORGANIZER для создаваемых событий. У вас не может быть двух аккаунтов с тем же именем.</string>
<string name="login_account_contact_group_method">Метод группировки контактов:</string>
<string name="login_account_name_required">Название аккаунта обязательно</string>
<string name="login_account_name_already_taken">Название аккаунта уже используется</string>
<string name="login_account_not_created">Аккаунт не может быть создан</string>
<string name="login_type_advanced">Расширенный вход</string>
<string name="login_no_client_certificate_optional">Нет клиентского сертификата*</string>
<string name="login_client_certificate_selected">Сертификат клиента: %s</string>

View File

@@ -21,6 +21,7 @@
<!--AboutActivity-->
<string name="about_libraries">Knižnice</string>
<string name="about_version">Verzia %1$s (%2$d)</string>
<string name="about_build_date">Preložené na %s</string>
<string name="about_license_info_no_warranty">Tento program sa poskytuje BEZ AKEJKOĽVEK ZÁRUKY. Je to slobodný softvér a môžete ho ďalej šíriť pri splnení určitých podmienok.</string>
<!--global settings-->
<string name="logging_couldnt_create_file">Nie je možné vytvoriť súbor protokolu</string>
@@ -36,6 +37,7 @@
<string name="navigation_drawer_manual">Manuál</string>
<string name="navigation_drawer_faq">FAQ</string>
<string name="navigation_drawer_privacy_policy">Zásady bezpečnosti</string>
<string name="account_list_empty">Vitajte v programe DAVx⁵!\n\nTeraz môžete pridať používateľský účet pre CalDAV/CardDAV .</string>
<!--RefreshCollectionsWorker-->
<string name="refresh_collections_worker_refresh_failed">Zisťovanie služby zlyhalo</string>
<string name="refresh_collections_worker_refresh_couldnt_refresh">Nie je možné obnoviť zoznam kolekcií</string>
@@ -44,7 +46,9 @@
<string name="app_settings">Nastavenia</string>
<string name="app_settings_debug">Ladenie</string>
<string name="app_settings_show_debug_info">Zobraziť ladiace informácie</string>
<string name="app_settings_show_debug_info_details">Zobraziť/zdieľať softvér a konfiguračné detaily</string>
<string name="app_settings_logging">Zvýšené protokolovanie</string>
<string name="app_settings_logging_on">Protokolovanie je aktívne</string>
<string name="app_settings_logging_off">Protokolovanie je zakázané</string>
<string name="app_settings_connection">Spojenie</string>
<string name="app_settings_security">Zabezpečenie</string>
@@ -88,12 +92,13 @@
<string name="login_user_name">Používateľské meno</string>
<string name="login_base_url">Základné URL</string>
<string name="login_select_certificate">Zvoliť certifikát</string>
<string name="login_add_account">Pridať účet</string>
<string name="login_create_account">Vytvoriť používateľský účet</string>
<string name="login_account_name">Meno používateľského účtu</string>
<string name="login_account_name_info">Použite vašu e-mailovú adresu ako meno používateľského účtu pretože Android používa meno účtu v poli ORGANIZÁTOR pre udalosti ktoré vytvoríte. Nie je možné mať dva používateľské účty s rovnakým menom.</string>
<string name="login_account_contact_group_method">Spôsob práce so skupinami</string>
<string name="login_account_name_required">Vyžaduje sa meno používateľského účtu</string>
<string name="login_account_name_already_taken">Meno účtu sa už používa</string>
<string name="login_account_not_created">Nie je možné vytvoriť používateľský účet</string>
<string name="login_configuration_detection">Zisťuje sa konfigurácia</string>
<string name="login_querying_server">Čakajte, prosím, zasiela sa dopyt na server...</string>
<string name="login_no_service">Nie je možné nájsť služby CalDAV ani CardDAV.</string>

View File

@@ -52,6 +52,7 @@
<!--AboutActivity-->
<string name="about_libraries">Knjižnice </string>
<string name="about_version">Verzija %1$s (%2$d)</string>
<string name="about_build_date">Združeno na %s</string>
<string name="about_license_info_no_warranty">Ta program ne vsebuje NIČ garancije. To je brezplačna programska oprema in jo lahko pod določenimi pogoji delite naprej.</string>
<!--global settings-->
<string name="logging_couldnt_create_file">Ni bilo mogoče ustvariti zapisnika</string>
@@ -65,6 +66,7 @@
<string name="navigation_drawer_website">Spletna stran</string>
<string name="navigation_drawer_manual">Priročnik</string>
<string name="navigation_drawer_faq">Pogosta vprašanja</string>
<string name="account_list_empty">Dobrodošli v DAVx⁵!\n\nZdaj lahko dodate CalDAV/CardDAV račun.</string>
<!--RefreshCollectionsWorker-->
<string name="refresh_collections_worker_refresh_failed">Zaznava storitve ni uspela</string>
<string name="refresh_collections_worker_refresh_couldnt_refresh">Zbirke ni bilo mogoče osvežiti</string>
@@ -73,7 +75,9 @@
<string name="app_settings">Nastavitve</string>
<string name="app_settings_debug">Razhroščevalnik</string>
<string name="app_settings_show_debug_info">Prikaži informacije razhroščevalnika</string>
<string name="app_settings_show_debug_info_details">Preglej/deli podrobnosti programske opreme in konfiguracije</string>
<string name="app_settings_logging">Podrobno zapisovanje procesov</string>
<string name="app_settings_logging_on">Zapisovanje je aktivno</string>
<string name="app_settings_logging_off">Zapisovanje je onemogočeno</string>
<string name="app_settings_connection">Povezava</string>
<string name="app_settings_security">Varnost</string>
@@ -119,12 +123,13 @@
<string name="login_user_name">Uporabniško ime</string>
<string name="login_base_url">URL osnova</string>
<string name="login_select_certificate">Izberi certifikat</string>
<string name="login_add_account">Dodaj račun</string>
<string name="login_create_account">Ustvari račun</string>
<string name="login_account_name">Ime računa</string>
<string name="login_account_name_info">Uporabi email naslov kot ime računa, ker bo Android uporabil to ime računa kot organizacijsko povelj za dogodke, ki jih ustvariš. Dveh računov z istim imenom ni mogoče imeti.</string>
<string name="login_account_contact_group_method">Metoda skupine kontaktov:</string>
<string name="login_account_name_required">Zahtevano je ime računa</string>
<string name="login_account_name_already_taken">Ima računa že obstaja</string>
<string name="login_account_not_created">Računa ni bilo mogoče ustvariti</string>
<string name="login_configuration_detection">Zaznava konfiguracije</string>
<string name="login_querying_server">Prosim počakajte, povezava s strežnikom je v teku...</string>
<string name="login_no_service">CalDAV ali CardDAV storitve ni bilo mogoče najti.</string>

View File

@@ -39,12 +39,12 @@
<string name="intro_tasks_opentasks">OpenTasks</string>
<string name="intro_tasks_opentasks_info">Изгледа да се више не развија - не препоручује се.</string>
<string name="intro_tasks_tasks_org">Tasks.org</string>
<string name="intro_tasks_tasks_org_info"><![CDATA[Неке функционалности <a href="https://www.davx5.com/faq/tasks/advanced-task-features">нису подржане</a> (за сада).]]></string>
<string name="intro_tasks_no_app_store">Ниједна продавница апликација није доступна</string>
<string name="intro_tasks_dont_show">Не треба ми подршка за задатке.*</string>
<string name="intro_open_source_title">Софтвер отвореног кода</string>
<string name="intro_open_source_details">Како допринети/донирати</string>
<string name="intro_open_source_dont_show">Не приказуј у блиској будућности</string>
<string name="intro_next">Следеће</string>
<!--PermissionsActivity-->
<string name="permissions_title">Дозволе</string>
<string name="permissions_text">%s захтева дозволе да би исправно радила.</string>
@@ -84,6 +84,7 @@
<string name="about_translations">Преводи</string>
<string name="about_libraries">Библиотеке</string>
<string name="about_version">Издање %1$s (%2$d)</string>
<string name="about_build_date">Компилован %s</string>
<string name="about_license_info_no_warranty">Овај програм НЕМА НИКАКВЕ ГАРАНЦИЈЕ. Бесплатан је софтвер којег можете слободно да делите под одређеним условима.</string>
<!--global settings-->
<string name="logging_couldnt_create_file">Није се могла направити датотека записа</string>
@@ -106,6 +107,7 @@
<string name="navigation_drawer_privacy_policy">Политика приватности</string>
<string name="account_list_no_notification_permission">Обавештења су онемогућена. Нећете бити обавештени о проблемима са синхронизацијом.</string>
<string name="account_list_manage_storage">Управљајте складиштем</string>
<string name="account_list_empty">Добро дошли у ДАВдроид!\n\nМожете сада да додате КалДАВ/КардДАВ налог.</string>
<string name="accounts_sync_all">Синхронизуј све налоге</string>
<!--RefreshCollectionsWorker-->
<string name="refresh_collections_worker_refresh_failed">Откривање услуге није успело</string>
@@ -116,6 +118,7 @@
<string name="app_settings">Поставке</string>
<string name="app_settings_debug">Тражење грешака</string>
<string name="app_settings_show_debug_info">Прикажи податке за исправљање грешака</string>
<string name="app_settings_show_debug_info_details">Приказ/дељење детаља софтвера и поставки</string>
<string name="app_settings_logging">Исцрпна евиденција</string>
<string name="app_settings_battery_optimization">Оптимизација батерије</string>
<string name="app_settings_connection">Повезивање</string>
@@ -182,12 +185,13 @@
<string name="login_user_name">Корисничко име</string>
<string name="login_base_url">Корени УРЛ</string>
<string name="login_select_certificate">Изабери сертификат</string>
<string name="login_add_account">Додај налог</string>
<string name="login_create_account">Направи налог</string>
<string name="login_account_name">Назив налога</string>
<string name="login_account_name_info">Користите вашу е-адресу за назив налога јер Андроид користи назив налога за поље ОРГАНИЗАТОР за догађаје које направите. Не можете имати два налога истог назива.</string>
<string name="login_account_contact_group_method">Режим група контаката:</string>
<string name="login_account_name_required">Назив налога је обавезан</string>
<string name="login_account_name_already_taken">Назив налога је већ заузет</string>
<string name="login_account_not_created">Не могох направити налог</string>
<string name="login_no_certificate_found">Сертификат није пронађен</string>
<string name="login_install_certificate">Инсталирај сертификат</string>
<string name="login_type_google">Гугл контакти / календар</string>

View File

@@ -44,6 +44,7 @@
<string name="intro_tasks_opentasks">OpenTasks</string>
<string name="intro_tasks_opentasks_info">Verkar inte utvecklas och underhållas längre - ej rekommenderad.</string>
<string name="intro_tasks_tasks_org">Tasks.org</string>
<string name="intro_tasks_tasks_org_info"><![CDATA[Vissa funktioner <a href="https://www.davx5.com/faq/tasks/advanced-task-features">stöds inte</a> (ännu).]]></string>
<string name="intro_tasks_no_app_store">Ingen app butik tillgänglig</string>
<string name="intro_tasks_dont_show">Jag behöver inte stöd för att-göra.*</string>
<string name="intro_open_source_title">Öppen källkod mjukvara</string>
@@ -86,6 +87,7 @@
<string name="wifi_permissions_background_location_permission_label">Tillåt hela tiden</string>
<string name="wifi_permissions_background_location_permission_on">Platsbehörighet satt till: %s</string>
<string name="wifi_permissions_background_location_permission_off">Platsbehörighet inte satt till: %s</string>
<string name="wifi_permissions_background_location_disclaimer">%s använder platsbehörigheten endast för att bestämma den aktuella WiFi:s SSID för konton med SSID-begränsning. Detta kommer att hända även när appen är i bakgrunden. Ingen platsdata samlas in, lagras, bearbetas eller skickas någonstans.</string>
<string name="wifi_permissions_location_enabled">Plats alltid påslagen</string>
<string name="wifi_permissions_location_enabled_on">Platstjänster är påslagna</string>
<string name="wifi_permissions_location_enabled_off">Platstjänster är avstängda</string>
@@ -93,6 +95,7 @@
<string name="about_translations">Översättningar</string>
<string name="about_libraries">Bibliotek</string>
<string name="about_version">Version %1$s (%2$d)</string>
<string name="about_build_date">Kompilerad på %s</string>
<string name="about_copyright">© Ricki Hirner, Bernhard Stockmann (bitfire web engineering GmbH) och bidragsgivare</string>
<string name="about_license_info_no_warranty">Detta program levereras med ABSOLUT INGEN GARANTI. Det är fri programvara, och du kan vidaredistribuera den under vissa förutsättningar.</string>
<!--global settings-->
@@ -125,6 +128,7 @@
<string name="account_list_manage_battery_saver">Hantera batterisparare</string>
<string name="account_list_low_storage">Lagringsutrymmet är lågt. Android kommer inte synka lokala ändringar direkt, utan vänta till nästa regelbundna synkronisering.</string>
<string name="account_list_manage_storage">Hantera lagring</string>
<string name="account_list_empty">Välkommen till DAVx⁵!\n\nDu kan lägga till ett CalDAV/CardDAV-konto nu.</string>
<string name="accounts_sync_all">Synkronisera alla konton</string>
<!--RefreshCollectionsWorker-->
<string name="refresh_collections_worker_refresh_failed">Servicedetektering misslyckades</string>
@@ -136,7 +140,9 @@
<string name="app_settings">Inställningar</string>
<string name="app_settings_debug">Felsökning</string>
<string name="app_settings_show_debug_info">Visa felsökningsinformation</string>
<string name="app_settings_show_debug_info_details">Visa/dela programvara och konfigurationsdetaljer</string>
<string name="app_settings_logging">Omfattande loggning</string>
<string name="app_settings_logging_on">Loggning är påslagen</string>
<string name="app_settings_logging_off">Loggning är avstängd</string>
<string name="app_settings_battery_optimization">Batterioptimering</string>
<string name="app_settings_battery_optimization_exempted">Appen är undantagen (rekommenderas)</string>
@@ -218,13 +224,14 @@
<string name="login_user_name_optional">Användarnamn*</string>
<string name="login_base_url">Bas-URL</string>
<string name="login_select_certificate">Välj certifikat</string>
<string name="login_add_account">Lägg till konto</string>
<string name="login_create_account">Skapa konto</string>
<string name="login_account_name">Kontonamn</string>
<string name="login_account_avoid_apostrophe">Användning av apostrof (\') verkar orsaka problem på vissa enheter.</string>
<string name="login_account_name_info">Använd din e-postadress som kontonamn eftersom Android kommer att använda kontonamnet som fält för ARRANGÖR för händelser du skapar. Du kan inte ha två konton med samma namn.</string>
<string name="login_account_contact_group_method">Kontaktgruppsmetod:</string>
<string name="login_account_name_required">Konto namn krävs</string>
<string name="login_account_name_already_taken">Kontonamn är upptaget</string>
<string name="login_account_not_created">Konto kunde inte skapas</string>
<string name="login_type_advanced">Avancerad inloggning</string>
<string name="login_no_client_certificate_optional">Inget klientcertifikat*</string>
<string name="login_client_certificate_selected">Klientcertifikat: %s</string>

View File

@@ -37,6 +37,7 @@
<string name="about_translations">Przekłady</string>
<string name="about_libraries">Bibliotyki</string>
<string name="about_version">Wersyjo %1$s (%2$d)</string>
<string name="about_build_date">Skōmpilowano %s</string>
<string name="about_license_info_no_warranty">Tyn program przichodzi BEZ ŻODNYJ GWARANCYJE. To je wolne ôprogramowanie i możesz je rozkludzać pod ôkryślōnymi warōnkami.</string>
<!--global settings-->
<string name="logging_couldnt_create_file">Niy szło stworzić zbioru dziynnika</string>
@@ -52,6 +53,7 @@
<string name="navigation_drawer_manual">Ryncznie</string>
<string name="navigation_drawer_faq">Pytania i ôdpowiedzi</string>
<string name="navigation_drawer_privacy_policy">Polityka prywatności</string>
<string name="account_list_empty">Witōmy w DAVx⁵!\n\nMożesz teroz przidać kōnto CalDAV/CardDAV.</string>
<!--RefreshCollectionsWorker-->
<string name="refresh_collections_worker_refresh_failed">Niy szło ôdświyżyć serwisu</string>
<string name="refresh_collections_worker_refresh_couldnt_refresh">Niy szło ôdświyżyć listy kolekcyje</string>
@@ -60,7 +62,9 @@
<string name="app_settings">Sztelōnki</string>
<string name="app_settings_debug">Debugowanie</string>
<string name="app_settings_show_debug_info">Pokoż informacyje do debugowanio</string>
<string name="app_settings_show_debug_info_details">Przejzdrzij abo udostympnij informacyje ô programie i jego kōnfiguracyji</string>
<string name="app_settings_logging">Rozwlykłe zapisowanie</string>
<string name="app_settings_logging_on">Zapisowanie je aktywne</string>
<string name="app_settings_logging_off">Zapisowanie je zastawiōne</string>
<string name="app_settings_connection">Łōnczność</string>
<string name="app_settings_security">Bezpieczyństwo</string>
@@ -104,12 +108,13 @@
<string name="login_user_name">Miano używocza</string>
<string name="login_base_url">Bazowy URL</string>
<string name="login_select_certificate">Ôbier certyfikat</string>
<string name="login_add_account">Przidej kōnto</string>
<string name="login_create_account">Stwōrz kōnto</string>
<string name="login_account_name">Miano kōnta</string>
<string name="login_account_name_info">Użyj swojij adresy e-mail za miano kōnta, bo Android bydzie używoł miana kōnta za pola ÔRGANIZATŌR dlo zdarzyń, co je stworzisz. Niy możesz posiadać dwōch kōnt ze takim samym mianym.</string>
<string name="login_account_contact_group_method">Spusōb grupowanio kōntaktōw:</string>
<string name="login_account_name_required">Wymogane miano kōnta</string>
<string name="login_account_name_already_taken">Miano kōnta je już zajynte</string>
<string name="login_account_not_created">Kōnto niy mogło być stworzōne</string>
<string name="login_configuration_detection">Wykrywanie kōnfiguracyje</string>
<string name="login_querying_server">Czekej, ôdpytowanie serwera…</string>
<string name="login_no_service">Niy idzie znojść usugi CalDAV abo CardDAV.</string>

Some files were not shown because too many files have changed in this diff Show More