mirror of
https://github.com/bitfireAT/davx5-ose.git
synced 2026-02-28 04:47:33 -05:00
Compare commits
1 Commits
v4.4.4-ose
...
debug-buil
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
46e8c4522b |
2
.github/workflows/codeql.yml
vendored
2
.github/workflows/codeql.yml
vendored
@@ -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
|
||||
|
||||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -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
|
||||
|
||||
6
.github/workflows/test-dev.yml
vendored
6
.github/workflows/test-dev.yml
vendored
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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')"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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')"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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!!)
|
||||
|
||||
@@ -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()
|
||||
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -66,7 +66,7 @@ class LocalCalendarTest {
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
calendar.delete()
|
||||
calendar.deleteCollection()
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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!!)
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 }
|
||||
|
||||
|
||||
@@ -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) {}
|
||||
|
||||
}
|
||||
|
||||
|
||||
6
app/src/debug/res/values/colors.xml
Normal file
6
app/src/debug/res/values/colors.xml
Normal 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>
|
||||
4
app/src/debug/res/values/strings.xml
Normal file
4
app/src/debug/res/values/strings.xml
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name" translatable="false">DAVx⁵ Debug</string>
|
||||
</resources>
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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"))
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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}]")
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -66,7 +66,7 @@ fun Assistant(
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
fun Assistant_Preview_InScaffold() {
|
||||
fun Assistant_Preview() {
|
||||
Assistant(nextLabel = "Next") {
|
||||
Text("Some Content")
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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>()
|
||||
|
||||
@@ -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()
|
||||
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
),
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
<string name="account_invalid">Le compte n’existe 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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 e‑mail 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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user