Compare commits

...

25 Commits

Author SHA1 Message Date
Arnau Mora
e9fb031d0a Upgrade dependencies
Signed-off-by: Arnau Mora <arnyminerz@proton.me>
2025-06-04 19:55:31 +02:00
Arnau Mora
d1c3548ccc Upgrade dependencies
Signed-off-by: Arnau Mora <arnyminerz@proton.me>
2025-06-04 19:54:46 +02:00
Arnau Mora
762095c7ce Merge branch 'main-ose' into nav3-migration
# Conflicts:
#	app/src/main/kotlin/at/bitfire/davdroid/db/AppDatabase.kt
#	app/src/main/kotlin/at/bitfire/davdroid/ui/AccountsScreen.kt
2025-06-04 19:53:43 +02:00
Ricki Hirner
fa50fe4c30 [CI] Run tests on API level 35 2025-06-04 16:14:00 +02:00
Arnau Mora
ba4d3b2fd1 Increase SDK to 36 (#1513)
* Upgrade to SDK 36

Signed-off-by: Arnau Mora <arnyminerz@proton.me>

* Enable on back invoked callback

Signed-off-by: Arnau Mora <arnyminerz@proton.me>

* Remove `enableOnBackInvokedCallback`

Signed-off-by: Arnau Mora <arnyminerz@proton.me>

---------

Signed-off-by: Arnau Mora <arnyminerz@proton.me>
2025-06-04 15:42:57 +02:00
Sunik Kupfer
0fed85fdc3 Add app password hint under password field (#1507)
* Add app password hint under password field

Signed-off-by: Sunik Kupfer <kupfer@bitfire.at>

* Change text to use prefer

Signed-off-by: Sunik Kupfer <kupfer@bitfire.at>

* Append path encoded

Signed-off-by: Sunik Kupfer <kupfer@bitfire.at>

* Update app password help URL and text

---------

Signed-off-by: Sunik Kupfer <kupfer@bitfire.at>
Co-authored-by: Ricki Hirner <hirner@bitfire.at>
2025-06-04 15:42:40 +02:00
Sunik Kupfer
6fbaea9487 [SyncManager]s Remove authority (#1491)
* Update sync stats to store sync data type instead of authority

Signed-off-by: Sunik Kupfer <kupfer@bitfire.at>

* Use a real authority in the tests

Signed-off-by: Sunik Kupfer <kupfer@bitfire.at>

* Replace authority with syncDataType in sync managers

Signed-off-by: Sunik Kupfer <kupfer@bitfire.at>

* Minor changes
- import index
- edit comments

Signed-off-by: Sunik Kupfer <kupfer@bitfire.at>

* Use lowercase localised strings for datatypes

Signed-off-by: Sunik Kupfer <kupfer@bitfire.at>

* Pass sync data type extra as string

Signed-off-by: Sunik Kupfer <kupfer@bitfire.at>

* Remove unknown datatype

Signed-off-by: Sunik Kupfer <kupfer@bitfire.at>

* Move datatype name strings to collection screen section

Signed-off-by: Sunik Kupfer <kupfer@bitfire.at>

* Update string usages

Signed-off-by: Sunik Kupfer <kupfer@bitfire.at>

* Update test

Signed-off-by: Sunik Kupfer <kupfer@bitfire.at>

* Add any type annotations to arrays

Signed-off-by: Sunik Kupfer <kupfer@bitfire.at>

---------

Signed-off-by: Sunik Kupfer <kupfer@bitfire.at>
2025-06-04 14:44:12 +02:00
Arnau Mora
fc2bc8aa47 Pass state to modal drawer for automatic back handler (#1495)
* Pass state to modal drawer for automatic back handler

Signed-off-by: Arnau Mora <arnyminerz@proton.me>

* Fix deprecation: Use toUri instead of Uri.parse

Signed-off-by: Sunik Kupfer <kupfer@bitfire.at>

---------

Signed-off-by: Arnau Mora <arnyminerz@proton.me>
Signed-off-by: Sunik Kupfer <kupfer@bitfire.at>
Co-authored-by: Sunik Kupfer <kupfer@bitfire.at>
2025-06-02 17:12:41 +02:00
Ricki Hirner
0321e4ab8f [Lint] Convert URIs to strings using toUri() (#1506) 2025-06-02 12:00:13 +02:00
Ricki Hirner
711543c5f1 Credentials / dav4jvm: store passwords as CharArray (#1483)
* Credentials / dav4jvm: store passwords as CharArray

* Fix tests
2025-05-30 17:37:14 +02:00
Ricki Hirner
5c485834e9 Update Gradle wrapper and Android Gradle Plugin versions 2025-05-30 17:36:10 +02:00
Ricki Hirner
f349f1fec8 Bump version to 4.4.11 2025-05-30 16:28:23 +02:00
Ricki Hirner
e6413506cb Fetch translations from Transifex 2025-05-30 16:26:53 +02:00
Arnau Mora
d9b36a0e34 Fix predictive back for drawer
Signed-off-by: Arnau Mora <arnyminerz@proton.me>
2025-05-27 15:21:23 +02:00
Arnau Mora
514623c0f2 Use ComponentActivity instead of AppCompatActivity
Signed-off-by: Arnau Mora <arnyminerz@proton.me>
2025-05-27 15:14:07 +02:00
Arnau Mora
9978850594 Moved nav back stack holding to viewmodel
Signed-off-by: Arnau Mora <arnyminerz@proton.me>
2025-05-26 20:07:50 +02:00
Arnau Mora
e1f5b2e3c1 Upgrade Activity Compose
Signed-off-by: Arnau Mora <arnyminerz@proton.me>
2025-05-26 20:06:49 +02:00
Arnau Mora
ad0cdb5c0c Use SDK 36
Signed-off-by: Arnau Mora <arnyminerz@proton.me>
2025-05-26 19:43:30 +02:00
Arnau Mora
de9d58bc20 Migrated AccountsActivity to MainActivity
Signed-off-by: Arnau Mora <arnyminerz@proton.me>
2025-05-26 19:36:11 +02:00
Arnau Mora
a6238a4131 Enable back invoked callback
Signed-off-by: Arnau Mora <arnyminerz@proton.me>
2025-05-26 19:35:27 +02:00
Arnau Mora
bbc7fbfa1e Added missing plugin
Signed-off-by: Arnau Mora <arnyminerz@proton.me>
2025-05-26 19:35:11 +02:00
Arnau Mora
3ba4dfb157 Upgrade Kotlin and KSP
Signed-off-by: Arnau Mora <arnyminerz@proton.me>
2025-05-26 19:35:01 +02:00
Arnau Mora
4544cd9b5c Fixed snapshot dependencies repository
Signed-off-by: Arnau Mora <arnyminerz@proton.me>
2025-05-26 19:34:05 +02:00
Arnau Mora
24026edad0 Added dependencies
Signed-off-by: Arnau Mora <arnyminerz@proton.me>
2025-05-26 19:07:13 +02:00
Michael Biebl
d4b5039297 Use lowercase GroupIDs as a workaround for jitpack.io issues (#1489)
Related discussion at

https://github.com/bitfireAT/davx5-ose/discussions/1236

and

https://github.com/jitpack/jitpack.io/issues/3295#issuecomment-2329134019

For example trying to access:
https://jitpack.io/com/github/bitfireAT/dav4jvm/b87d772e44/ I get:

File not found. Build ok

In comparison under
https://jitpack.io/com/github/bitfireat/dav4jvm/b87d772e44/ I get:

build.log
dav4jvm-b87d772e44-sources.jar
dav4jvm-b87d772e44.jar
dav4jvm-b87d772e44.module
dav4jvm-b87d772e44.pom
dav4jvm-b87d772e44.pom.md5
dav4jvm-b87d772e44.pom.sha1
2025-05-26 13:53:21 +02:00
74 changed files with 1312 additions and 269 deletions

View File

@@ -7,6 +7,7 @@ plugins {
alias(libs.plugins.compose.compiler)
alias(libs.plugins.hilt)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.serialization)
alias(libs.plugins.ksp)
alias(libs.plugins.mikepenz.aboutLibraries)
@@ -14,18 +15,18 @@ plugins {
// Android configuration
android {
compileSdk = 35
compileSdk = 36
defaultConfig {
applicationId = "at.bitfire.davdroid"
versionCode = 404110003
versionName = "4.4.11-rc.2"
versionCode = 404110004
versionName = "4.4.11"
setProperty("archivesBaseName", "davx5-ose-$versionName")
minSdk = 24 // Android 7.0
targetSdk = 35 // Android 15
targetSdk = 36 // Android 16
testInstrumentationRunner = "at.bitfire.davdroid.HiltTestRunner"
}
@@ -106,7 +107,7 @@ android {
localDevices {
create("virtual") {
device = "Pixel 3"
apiLevel = 34
apiLevel = 35
systemImageSource = "aosp-atd"
}
}
@@ -144,6 +145,9 @@ dependencies {
implementation(libs.androidx.lifecycle.runtime.compose)
implementation(libs.androidx.lifecycle.viewmodel.base)
implementation(libs.androidx.lifecycle.viewmodel.compose)
implementation(libs.androidx.lifecycle.viewmodel.navigation3)
implementation(libs.androidx.navigation3.runtime)
implementation(libs.androidx.navigation3.ui)
implementation(libs.androidx.paging)
implementation(libs.androidx.paging.compose)
implementation(libs.androidx.preference)
@@ -154,6 +158,7 @@ dependencies {
implementation(libs.compose.accompanist.permissions)
implementation(platform(libs.compose.bom))
implementation(libs.compose.material3)
implementation(libs.compose.material3.navigation3)
implementation(libs.compose.materialIconsExtended)
debugImplementation(libs.compose.ui.tooling)
implementation(libs.compose.ui.toolingPreview)
@@ -177,6 +182,10 @@ dependencies {
implementation(libs.bitfire.ical4android)
implementation(libs.bitfire.vcard4android)
// Serialization (for navigation)
implementation(libs.kotlinx.serialization.core)
implementation(libs.kotlinx.serialization.json)
// third-party libs
@Suppress("RedundantSuppression")
implementation(libs.dnsjava)

View File

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

View File

@@ -0,0 +1,79 @@
/*
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
*/
package at.bitfire.davdroid.db.migration
import dagger.hilt.android.testing.HiltAndroidTest
import org.junit.Assert.assertEquals
import org.junit.Test
@HiltAndroidTest
class AutoMigration18Test : DatabaseMigrationTest(toVersion = 18) {
@Test
fun testMigration_AllAuthorities() = testMigration(
prepare = { db ->
// Insert service and collection to respect relation constraints
db.execSQL("INSERT INTO service (id, accountName, type) VALUES (?, ?, ?)", arrayOf<Any?>(1, "test", 1))
listOf(1L, 2L, 3L).forEach { id ->
db.execSQL(
"INSERT INTO collection (id, serviceId, url, type, privWriteContent, privUnbind, forceReadOnly, sync) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
arrayOf<Any?>(id, 1, "https://example.com/$id", 1, 1, 1, 0, 1)
)
}
// Insert some syncstats with authorities and lastSync times
val syncstats = listOf(
Entry(1, 1, "com.android.contacts", 1000),
Entry(2, 1, "com.android.calendar", 1000),
Entry(3, 1, "org.dmfs.tasks", 1000),
Entry(4, 1, "org.tasks.opentasks", 2000),
Entry(5, 1, "at.techbee.jtx.provider", 3000), // highest lastSync for collection 1
Entry(6, 1, "unknown.authority", 1000), // ignored
Entry(7, 2, "org.dmfs.tasks", 1000),
Entry(8, 2, "org.tasks.opentasks", 2000), // highest lastSync for collection 2
Entry(9, 3, "org.tasks.opentasks", 1000),
)
syncstats.forEach { (id, collectionId, authority, lastSync) ->
db.execSQL(
"INSERT INTO syncstats (id, collectionId, authority, lastSync) VALUES (?, ?, ?, ?)",
arrayOf<Any?>(id, collectionId, authority, lastSync)
)
}
},
validate = { db ->
db.query("SELECT id, collectionId, dataType FROM syncstats ORDER BY id").use { cursor ->
val found = mutableListOf<Entry>()
db.query("SELECT id, collectionId, dataType FROM syncstats ORDER BY id").use { cursor ->
val idIdx = cursor.getColumnIndex("id")
val colIdx = cursor.getColumnIndex("collectionId")
val typeIdx = cursor.getColumnIndex("dataType")
while (cursor.moveToNext())
found.add(
Entry(cursor.getInt(idIdx), cursor.getLong(colIdx), cursor.getString(typeIdx))
)
}
// Expect one TASKS row per collection (collections 1, 2, 3)
assertEquals(
listOf(
Entry(1, 1, "CONTACTS"),
Entry(2, 1, "EVENTS"),
Entry(5, 1, "TASKS"), // highest lastSync TASK for collection 1 is JTX Board
Entry(8, 2, "TASKS"), // highest lastSync TASK for collection 2
Entry(9, 3, "TASKS"), // only TASK for collection 3
), found
)
}
}
)
data class Entry(
val id: Int,
val collectionId: Long,
val dataType: String? = null,
val lastSync: Long? = null
)
}

View File

@@ -70,7 +70,7 @@ class DavResourceFinderTest {
start()
}
val credentials = Credentials("mock", "12345")
val credentials = Credentials(username = "mock", password = "12345".toCharArray())
client = httpClientBuilder
.authenticate(host = null, credentials = credentials)
.build()

View File

@@ -94,7 +94,6 @@ class JtxSyncManagerTest {
syncManager = jtxSyncManagerFactory.jtxSyncManager(
account = account,
httpClient = httpClientBuilder.build(),
authority = JtxContract.AUTHORITY,
syncResult = SyncResult(),
localCollection = localJtxCollection,
collection = dbCollection,

View File

@@ -494,7 +494,6 @@ class SyncManagerTest {
}
) = syncManagerFactory.create(
account,
"TestAuthority",
httpClientBuilder.build(),
syncResult,
localCollection,

View File

@@ -26,7 +26,6 @@ import org.junit.Assert.assertEquals
class TestSyncManager @AssistedInject constructor(
@Assisted account: Account,
@Assisted authority: String,
@Assisted httpClient: HttpClient,
@Assisted syncResult: SyncResult,
@Assisted localCollection: LocalTestCollection,
@@ -35,7 +34,7 @@ class TestSyncManager @AssistedInject constructor(
): SyncManager<LocalTestResource, LocalTestCollection, DavCollection>(
account,
httpClient,
authority,
SyncDataType.EVENTS,
syncResult,
localCollection,
collection,
@@ -47,7 +46,6 @@ class TestSyncManager @AssistedInject constructor(
interface Factory {
fun create(
account: Account,
authority: String,
httpClient: HttpClient,
syncResult: SyncResult,
localCollection: LocalTestCollection,

View File

@@ -1,3 +1,7 @@
/*
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
*/
package at.bitfire.davdroid.ui.setup
import android.content.Intent
@@ -17,7 +21,7 @@ class LoginActivityTest {
val loginInfo = LoginActivity.loginInfoFromIntent(intent)
assertEquals("https://example.com/nextcloud", loginInfo.baseUri.toString())
assertEquals("user", loginInfo.credentials!!.username)
assertEquals("password", loginInfo.credentials.password)
assertEquals("password", loginInfo.credentials.password?.concatToString())
}
@Test
@@ -30,7 +34,7 @@ class LoginActivityTest {
val loginInfo = LoginActivity.loginInfoFromIntent(intent)
assertEquals("https://example.com:444/nextcloud", loginInfo.baseUri.toString())
assertEquals("user", loginInfo.credentials!!.username)
assertEquals("password", loginInfo.credentials.password)
assertEquals("password", loginInfo.credentials.password?.concatToString())
}
@Test
@@ -39,7 +43,7 @@ class LoginActivityTest {
val loginInfo = LoginActivity.loginInfoFromIntent(intent)
assertEquals("https://example.com/path", loginInfo.baseUri.toString())
assertEquals("user", loginInfo.credentials!!.username)
assertEquals("password", loginInfo.credentials.password)
assertEquals("password", loginInfo.credentials.password?.concatToString())
}
@Test
@@ -48,7 +52,7 @@ class LoginActivityTest {
val loginInfo = LoginActivity.loginInfoFromIntent(intent)
assertEquals("https://example.com:0/path", loginInfo.baseUri.toString())
assertEquals("user", loginInfo.credentials!!.username)
assertEquals("password", loginInfo.credentials.password)
assertEquals("password", loginInfo.credentials.password?.concatToString())
}
@Test
@@ -57,7 +61,7 @@ class LoginActivityTest {
val loginInfo = LoginActivity.loginInfoFromIntent(intent)
assertEquals(null, loginInfo.baseUri)
assertEquals("user@example.com", loginInfo.credentials!!.username)
assertEquals(null, loginInfo.credentials.password)
assertEquals(null, loginInfo.credentials.password?.concatToString())
}
}

View File

@@ -30,8 +30,8 @@ class CredentialsStoreTest {
@Test
fun testSetGetDelete() {
store.setCredentials(0, Credentials(username = "myname", password = "12345"))
assertEquals(Credentials(username = "myname", password = "12345"), store.getCredentials(0))
store.setCredentials(0, Credentials(username = "myname", password = "12345".toCharArray()))
assertEquals(Credentials(username = "myname", password = "12345".toCharArray()), store.getCredentials(0))
store.setCredentials(0, null)
assertNull(store.getCredentials(0))

View File

@@ -1,4 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:installLocation="internalOnly">
@@ -41,6 +45,7 @@
<application
android:name=".App"
android:allowBackup="false"
android:enableOnBackInvokedCallback="true"
android:networkSecurityConfig="@xml/network_security_config"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
@@ -62,7 +67,7 @@
<activity android:name=".ui.intro.IntroActivity" />
<activity
android:name=".ui.AccountsActivity"
android:name=".ui.MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
@@ -73,12 +78,12 @@
<activity
android:name=".ui.AboutActivity"
android:label="@string/navigation_drawer_about"
android:parentActivityName=".ui.AccountsActivity"/>
android:parentActivityName=".ui.MainActivity"/>
<activity
android:name=".ui.AppSettingsActivity"
android:label="@string/app_settings"
android:parentActivityName=".ui.AccountsActivity"
android:parentActivityName=".ui.MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.APPLICATION_PREFERENCES"/>
@@ -106,7 +111,7 @@
<activity
android:name=".ui.setup.LoginActivity"
android:parentActivityName=".ui.AccountsActivity"
android:parentActivityName=".ui.MainActivity"
android:windowSoftInputMode="adjustResize"
android:exported="true">
<intent-filter>
@@ -134,7 +139,7 @@
<activity
android:name=".ui.account.AccountActivity"
android:parentActivityName=".ui.AccountsActivity"
android:parentActivityName=".ui.MainActivity"
android:exported="true">
</activity>
<activity
@@ -156,7 +161,7 @@
<activity
android:name=".ui.webdav.WebdavMountsActivity"
android:exported="true"
android:parentActivityName=".ui.AccountsActivity" />
android:parentActivityName=".ui.MainActivity" />
<activity
android:name=".ui.webdav.AddWebdavMountActivity"
android:parentActivityName=".ui.webdav.WebdavMountsActivity"

View File

@@ -26,12 +26,14 @@ object Constants {
const val MANUAL_PATH_ACCOUNTS_COLLECTIONS = "accounts_collections.html"
const val MANUAL_FRAGMENT_SERVICE_DISCOVERY = "how-does-service-discovery-work"
const val MANUAL_PATH_INTRODUCTION = "introduction.html"
const val MANUAL_FRAGMENT_AUTHENTICATION_METHODS = "authentication-methods"
const val MANUAL_PATH_SETTINGS = "settings.html"
const val MANUAL_FRAGMENT_APP_SETTINGS = "app-wide-settings"
const val MANUAL_FRAGMENT_ACCOUNT_SETTINGS = "account-settings"
const val MANUAL_PATH_WEBDAV_PUSH = "webdav_push.html"
const val MANUAL_PATH_WEBDAV_MOUNTS = "webdav_mounts.html"
val COMMUNITY_URL = "https://github.com/bitfireAT/davx5-ose/discussions".toUri()

View File

@@ -24,7 +24,8 @@ import at.bitfire.davdroid.R
import at.bitfire.davdroid.TextTable
import at.bitfire.davdroid.db.migration.AutoMigration12
import at.bitfire.davdroid.db.migration.AutoMigration16
import at.bitfire.davdroid.ui.AccountsActivity
import at.bitfire.davdroid.db.migration.AutoMigration18
import at.bitfire.davdroid.ui.MainActivity
import at.bitfire.davdroid.ui.NotificationRegistry
import dagger.Module
import dagger.Provides
@@ -42,7 +43,8 @@ import javax.inject.Singleton
SyncStats::class,
WebDavDocument::class,
WebDavMount::class
], exportSchema = true, version = 17, autoMigrations = [
], exportSchema = true, version = 18, autoMigrations = [
AutoMigration(from = 17, to = 18, spec = AutoMigration18::class),
AutoMigration(from = 16, to = 17), // collection: add VAPID key
AutoMigration(from = 15, to = 16, spec = AutoMigration16::class),
AutoMigration(from = 14, to = 15),
@@ -77,7 +79,7 @@ abstract class AppDatabase: RoomDatabase() {
.addCallback(object: Callback() {
override fun onDestructiveMigration(db: SupportSQLiteDatabase) {
notificationRegistry.notifyIfPossible(NotificationRegistry.NOTIFY_DATABASE_CORRUPTED) {
val launcherIntent = Intent(context, AccountsActivity::class.java)
val launcherIntent = Intent(context, MainActivity::class.java)
NotificationCompat.Builder(context, notificationRegistry.CHANNEL_GENERAL)
.setSmallIcon(R.drawable.ic_warning_notify)
.setContentTitle(context.getString(R.string.database_destructive_migration_title))

View File

@@ -8,7 +8,7 @@ import net.openid.appauth.AuthState
data class Credentials(
val username: String? = null,
val password: String? = null,
val password: CharArray? = null,
val certificateAlias: String? = null,
@@ -32,4 +32,27 @@ data class Credentials(
return "Credentials(" + s.joinToString(", ") + ")"
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as Credentials
if (username != other.username) return false
if (!password.contentEquals(other.password)) return false
if (certificateAlias != other.certificateAlias) return false
if (authState != other.authState) return false
return true
}
override fun hashCode(): Int {
var result = username?.hashCode() ?: 0
result = 31 * result + (password?.contentHashCode() ?: 0)
result = 31 * result + (certificateAlias?.hashCode() ?: 0)
result = 31 * result + (authState?.hashCode() ?: 0)
return result
}
}

View File

@@ -14,7 +14,7 @@ import androidx.room.PrimaryKey
ForeignKey(childColumns = arrayOf("collectionId"), entity = Collection::class, parentColumns = arrayOf("id"), onDelete = ForeignKey.CASCADE)
],
indices = [
Index("collectionId", "authority", unique = true),
Index(value = ["collectionId", "dataType"], unique = true)
]
)
data class SyncStats(
@@ -22,7 +22,7 @@ data class SyncStats(
val id: Long,
val collectionId: Long,
val authority: String,
val dataType: String,
val lastSync: Long
)

View File

@@ -0,0 +1,81 @@
/*
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
*/
package at.bitfire.davdroid.db.migration
import android.provider.CalendarContract
import android.provider.ContactsContract
import androidx.room.ProvidedAutoMigrationSpec
import androidx.room.RenameColumn
import androidx.room.migration.AutoMigrationSpec
import androidx.sqlite.db.SupportSQLiteDatabase
import at.bitfire.davdroid.sync.SyncDataType
import at.bitfire.ical4android.TaskProvider
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import dagger.multibindings.IntoSet
import javax.inject.Inject
/**
* Renames syncstats.authority to dataType, and maps values to SyncDataType enum names.
*/
@ProvidedAutoMigrationSpec
@RenameColumn(tableName = "syncstats", fromColumnName = "authority", toColumnName = "dataType")
class AutoMigration18 @Inject constructor() : AutoMigrationSpec {
override fun onPostMigrate(db: SupportSQLiteDatabase) {
// Drop old unique index
db.execSQL("DROP INDEX IF EXISTS index_syncstats_collectionId_authority")
val seen = mutableSetOf<Pair<Long, String>>() // (collectionId, dataType)
db.query(
"SELECT id, collectionId, dataType, lastSync FROM syncstats ORDER BY lastSync DESC"
).use { cursor ->
val idIndex = cursor.getColumnIndex("id")
val collectionIdIndex = cursor.getColumnIndex("collectionId")
val authorityIndex = cursor.getColumnIndex("dataType")
while (cursor.moveToNext()) {
val id = cursor.getLong(idIndex)
val collectionId = cursor.getLong(collectionIdIndex)
val authority = cursor.getString(authorityIndex)
val dataType = when (authority) {
ContactsContract.AUTHORITY -> SyncDataType.CONTACTS.name
CalendarContract.AUTHORITY -> SyncDataType.EVENTS.name
TaskProvider.ProviderName.JtxBoard.authority,
TaskProvider.ProviderName.TasksOrg.authority,
TaskProvider.ProviderName.OpenTasks.authority -> SyncDataType.TASKS.name
else -> {
db.execSQL("DELETE FROM syncstats WHERE id = ?", arrayOf(id))
continue
}
}
val keyValue = collectionId to dataType
if (seen.contains(keyValue)) {
db.execSQL("DELETE FROM syncstats WHERE id = ?", arrayOf(id))
} else {
db.execSQL("UPDATE syncstats SET dataType = ? WHERE id = ?", arrayOf<Any>(dataType, id))
seen.add(keyValue)
}
}
}
// Create new unique index
db.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS index_syncstats_collectionId_dataType ON syncstats (collectionId, dataType)")
}
@Module
@InstallIn(SingletonComponent::class)
abstract class AutoMigrationModule {
@Binds
@IntoSet
abstract fun provide(impl: AutoMigration18): AutoMigrationSpec
}
}

View File

@@ -4,7 +4,7 @@
package at.bitfire.davdroid.network
import android.net.Uri
import androidx.core.net.toUri
import at.bitfire.davdroid.BuildConfig
import at.bitfire.davdroid.db.Credentials
import kotlinx.coroutines.CompletableDeferred
@@ -49,18 +49,18 @@ class GoogleLogin(
URI("https", "apidata.googleusercontent.com", "/caldav/v2/$googleAccount/user", null)
private val serviceConfig = AuthorizationServiceConfiguration(
Uri.parse("https://accounts.google.com/o/oauth2/v2/auth"),
Uri.parse("https://oauth2.googleapis.com/token")
"https://accounts.google.com/o/oauth2/v2/auth".toUri(),
"https://oauth2.googleapis.com/token".toUri()
)
}
fun signIn(email: String, customClientId: String?, locale: String?): AuthorizationRequest {
val builder = AuthorizationRequest.Builder(
GoogleLogin.serviceConfig,
serviceConfig,
customClientId ?: CLIENT_ID,
ResponseTypeValues.CODE,
Uri.parse(BuildConfig.APPLICATION_ID + ":/oauth2/redirect")
(BuildConfig.APPLICATION_ID + ":/oauth2/redirect").toUri()
)
return builder
.setScopes(*SCOPES)

View File

@@ -107,7 +107,12 @@ class HttpClient(
} else if (credentials.username != null && credentials.password != null) {
// basic/digest auth
val authHandler = BasicDigestAuthHandler(UrlUtils.hostToDomain(host), credentials.username, credentials.password, insecurePreemptive = true)
val authHandler = BasicDigestAuthHandler(
domain = UrlUtils.hostToDomain(host),
username = credentials.username,
password = credentials.password,
insecurePreemptive = true
)
authenticationInterceptor = authHandler
authenticator = authHandler
}

View File

@@ -106,7 +106,7 @@ class NextcloudLoginFlow @Inject constructor(
baseUri = URI(serverUrl).resolve(DAV_PATH),
credentials = Credentials(
username = json.getString("loginName"),
password = json.getString("appPassword")
password = json.getString("appPassword").toCharArray()
),
suggestedGroupMethod = GroupMethod.CATEGORIES
)

View File

@@ -5,26 +5,24 @@
package at.bitfire.davdroid.repository
import android.content.Context
import android.content.pm.PackageManager
import at.bitfire.davdroid.db.AppDatabase
import at.bitfire.davdroid.db.SyncStats
import at.bitfire.davdroid.sync.SyncDataType
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import java.text.Collator
import java.util.logging.Logger
import javax.inject.Inject
class DavSyncStatsRepository @Inject constructor(
@ApplicationContext val context: Context,
db: AppDatabase,
private val logger: Logger
db: AppDatabase
) {
private val dao = db.syncStatsDao()
data class LastSynced(
val appName: String,
val dataType: String,
val lastSynced: Long
)
fun getLastSyncedFlow(collectionId: Long): Flow<List<LastSynced>> =
@@ -32,46 +30,21 @@ class DavSyncStatsRepository @Inject constructor(
val collator = Collator.getInstance()
list.map { stats ->
LastSynced(
appName = appNameFromAuthority(stats.authority),
dataType = stats.dataType,
lastSynced = stats.lastSync
)
}.sortedWith { a, b ->
collator.compare(a.appName, b.appName)
collator.compare(a.dataType, b.dataType)
}
}
suspend fun logSyncTime(collectionId: Long, authority: String, lastSync: Long = System.currentTimeMillis()) {
suspend fun logSyncTime(collectionId: Long, dataType: SyncDataType, lastSync: Long = System.currentTimeMillis()) {
dao.insertOrReplace(SyncStats(
id = 0,
collectionId = collectionId,
authority = authority,
dataType = dataType.name,
lastSync = lastSync
))
}
/**
* Tries to find the application name for given authority. Returns the authority if not
* found.
*
* @param authority authority to find the application name for (ie "at.techbee.jtx")
* @return the application name of authority (ie "jtx Board")
*/
private fun appNameFromAuthority(authority: String): String {
val packageManager = context.packageManager
val packageName = packageManager.resolveContentProvider(authority, 0)?.packageName ?: authority
return try {
val appInfo = packageManager.getPackageInfo(packageName, 0).applicationInfo
if (appInfo != null) {
packageManager.getApplicationLabel(appInfo).toString()
} else {
logger.warning("Package name ($packageName) not found for authority: $authority")
authority
}
} catch (e: PackageManager.NameNotFoundException) {
logger.warning("Application name not found for authority: $authority")
authority
}
}
}

View File

@@ -107,7 +107,7 @@ class AccountSettings @AssistedInject constructor(
fun credentials() = Credentials(
accountManager.getUserData(account, KEY_USERNAME),
accountManager.getPassword(account),
accountManager.getPassword(account)?.toCharArray(),
accountManager.getUserData(account, KEY_CERTIFICATE_ALIAS),
@@ -119,7 +119,7 @@ class AccountSettings @AssistedInject constructor(
fun credentials(credentials: Credentials) {
// Basic/Digest auth
accountManager.setAndVerifyUserData(account, KEY_USERNAME, credentials.username)
accountManager.setPassword(account, credentials.password)
accountManager.setPassword(account, credentials.password?.concatToString())
// client certificate
accountManager.setAndVerifyUserData(account, KEY_CERTIFICATE_ALIAS, credentials.certificateAlias)

View File

@@ -104,7 +104,6 @@ class AddressBookSyncer @AssistedInject constructor(
val syncManager = contactsSyncManagerFactory.contactsSyncManager(
account,
httpClient.value,
dataStore.authority,
syncResult,
provider,
addressBook,

View File

@@ -54,7 +54,6 @@ import java.util.logging.Level
class CalendarSyncManager @AssistedInject constructor(
@Assisted account: Account,
@Assisted httpClient: HttpClient,
@Assisted authority: String,
@Assisted syncResult: SyncResult,
@Assisted localCalendar: LocalCalendar,
@Assisted collection: Collection,
@@ -64,7 +63,7 @@ class CalendarSyncManager @AssistedInject constructor(
): SyncManager<LocalEvent, LocalCalendar, DavCalendar>(
account,
httpClient,
authority,
SyncDataType.EVENTS,
syncResult,
localCalendar,
collection,
@@ -77,7 +76,6 @@ class CalendarSyncManager @AssistedInject constructor(
fun calendarSyncManager(
account: Account,
httpClient: HttpClient,
authority: String,
syncResult: SyncResult,
localCalendar: LocalCalendar,
collection: Collection,

View File

@@ -59,7 +59,6 @@ class CalendarSyncer @AssistedInject constructor(
val syncManager = calendarSyncManagerFactory.calendarSyncManager(
account,
httpClient.value,
dataStore.authority,
syncResult,
localCollection,
remoteCollection,

View File

@@ -100,7 +100,6 @@ import kotlin.jvm.optionals.getOrNull
class ContactsSyncManager @AssistedInject constructor(
@Assisted account: Account,
@Assisted httpClient: HttpClient,
@Assisted authority: String,
@Assisted syncResult: SyncResult,
@Assisted val provider: ContentProviderClient,
@Assisted localAddressBook: LocalAddressBook,
@@ -114,7 +113,7 @@ class ContactsSyncManager @AssistedInject constructor(
): SyncManager<LocalAddress, LocalAddressBook, DavAddressBook>(
account,
httpClient,
authority,
SyncDataType.CONTACTS,
syncResult,
localAddressBook,
collection,
@@ -127,7 +126,6 @@ class ContactsSyncManager @AssistedInject constructor(
fun contactsSyncManager(
account: Account,
httpClient: HttpClient,
authority: String,
syncResult: SyncResult,
provider: ContentProviderClient,
localAddressBook: LocalAddressBook,

View File

@@ -43,7 +43,6 @@ import java.util.logging.Level
class JtxSyncManager @AssistedInject constructor(
@Assisted account: Account,
@Assisted httpClient: HttpClient,
@Assisted authority: String,
@Assisted syncResult: SyncResult,
@Assisted localCollection: LocalJtxCollection,
@Assisted collection: Collection,
@@ -52,7 +51,7 @@ class JtxSyncManager @AssistedInject constructor(
): SyncManager<LocalJtxICalObject, LocalJtxCollection, DavCalendar>(
account,
httpClient,
authority,
SyncDataType.TASKS,
syncResult,
localCollection,
collection,
@@ -65,7 +64,6 @@ class JtxSyncManager @AssistedInject constructor(
fun jtxSyncManager(
account: Account,
httpClient: HttpClient,
authority: String,
syncResult: SyncResult,
localCollection: LocalJtxCollection,
collection: Collection,

View File

@@ -72,7 +72,6 @@ class JtxSyncer @AssistedInject constructor(
val syncManager = jtxSyncManagerFactory.jtxSyncManager(
account,
httpClient.value,
dataStore.authority,
syncResult,
localCollection,
remoteCollection,

View File

@@ -54,4 +54,4 @@ enum class SyncDataType {
}
}
}

View File

@@ -68,7 +68,7 @@ import javax.net.ssl.SSLHandshakeException
*
* @param account account to synchronize
* @param httpClient HTTP client to use for network requests, already authenticated with credentials from [account]
* @param authority authority of the content provider the collection shall be synchronized with
* @param dataType data type to synchronize
* @param syncResult receiver for result of the synchronization (will be updated by [performSync])
* @param localCollection local collection to synchronize (interface to content provider)
* @param collection collection info in the database
@@ -77,7 +77,7 @@ import javax.net.ssl.SSLHandshakeException
abstract class SyncManager<ResourceType: LocalResource<*>, out CollectionType: LocalCollection<ResourceType>, RemoteType: DavCollection>(
val account: Account,
val httpClient: HttpClient,
val authority: String,
val dataType: SyncDataType,
val syncResult: SyncResult,
val localCollection: CollectionType,
val collection: Collection,
@@ -141,7 +141,7 @@ abstract class SyncManager<ResourceType: LocalResource<*>, out CollectionType: L
logger.info("No reason to synchronize, aborting")
return@withContext
}
syncStatsRepository.logSyncTime(collection.id, authority)
syncStatsRepository.logSyncTime(collection.id, dataType)
logger.info("Querying server capabilities")
var remoteSyncState = queryCapabilities()
@@ -752,7 +752,7 @@ abstract class SyncManager<ResourceType: LocalResource<*>, out CollectionType: L
}
syncNotificationManager.notifyException(
authority,
dataType,
localCollection.tag,
message,
localCollection,
@@ -764,7 +764,7 @@ abstract class SyncManager<ResourceType: LocalResource<*>, out CollectionType: L
protected fun notifyInvalidResource(e: Throwable, fileName: String) =
syncNotificationManager.notifyInvalidResource(
authority,
dataType,
localCollection.tag,
collection,
e,

View File

@@ -10,12 +10,12 @@ import android.app.TaskStackBuilder
import android.content.ContentUris
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.provider.CalendarContract
import android.provider.ContactsContract
import android.provider.Settings
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.net.toUri
import at.bitfire.dav4jvm.exception.UnauthorizedException
import at.bitfire.davdroid.R
import at.bitfire.davdroid.db.Collection
@@ -104,6 +104,7 @@ class SyncNotificationManager @AssistedInject constructor(
* Tries to inform the user that an exception occurred during synchronization. Includes the affected
* local resource, its collection, the URL, the exception and a user message.
*
* @param syncDataType The type of data which was synced.
* @param notificationTag The tag to use for the notification.
* @param message The message to show to the user.
* @param localCollection The affected local collection.
@@ -112,7 +113,7 @@ class SyncNotificationManager @AssistedInject constructor(
* @param remote The remote URL that caused the exception.
*/
fun notifyException(
authority: String,
syncDataType: SyncDataType,
notificationTag: String,
message: String,
localCollection: LocalCollection<*>,
@@ -129,13 +130,13 @@ class SyncNotificationManager @AssistedInject constructor(
account
)
} else {
contentIntent = buildDebugInfoIntent(authority, e, local, remote)
contentIntent = buildDebugInfoIntent(syncDataType, e, local, remote)
if (local != null)
viewItemAction = buildViewItemActionForLocalResource(local)
}
// to make the PendingIntent unique
contentIntent.data = Uri.parse("davdroid:exception/${e.hashCode()}")
contentIntent.data = "davdroid:exception/${e.hashCode()}".toUri()
val channel: String
val priority: Int
@@ -171,13 +172,14 @@ class SyncNotificationManager @AssistedInject constructor(
* sync has been scheduled, but it still has not run.
* Use [dismissInvalidResource] to dismiss the notification.
*
* @param dataType The type of data which was synced.
* @param notificationTag The tag to use for the notification.
* @param collection The affected collection.
* @param fileName The name of the file containing the invalid resource.
* @param title The title of the notification.
*/
fun notifyInvalidResource(
authority: String,
dataType: SyncDataType,
notificationTag: String,
collection: Collection,
e: Throwable,
@@ -185,7 +187,7 @@ class SyncNotificationManager @AssistedInject constructor(
title: String
) {
notificationRegistry.notifyIfPossible(NotificationRegistry.NOTIFY_INVALID_RESOURCE, tag = notificationTag) {
val intent = buildDebugInfoIntent(authority, e, null, collection.url.resolve(fileName))
val intent = buildDebugInfoIntent(dataType, e, null, collection.url.resolve(fileName))
val builder = NotificationCompat.Builder(context, notificationRegistry.CHANNEL_SYNC_WARNINGS)
builder.setSmallIcon(R.drawable.ic_warning_notify)
@@ -225,14 +227,14 @@ class SyncNotificationManager @AssistedInject constructor(
* Builds intent to go to debug information with the given exception, resource and remote address.
*/
private fun buildDebugInfoIntent(
authority: String,
dataType: SyncDataType,
e: Throwable,
local: LocalResource<*>?,
remote: HttpUrl?
): Intent {
val builder = DebugInfoActivity.IntentBuilder(context)
.withAccount(account)
.withAuthority(authority)
.withSyncDataType(dataType)
.withCause(e)
if (local != null)

View File

@@ -73,7 +73,6 @@ class TaskSyncer @AssistedInject constructor(
val syncManager = tasksSyncManagerFactory.tasksSyncManager(
account,
httpClient.value,
dataStore.authority,
syncResult,
localCollection,
remoteCollection,

View File

@@ -9,9 +9,9 @@ import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.drawable.BitmapDrawable
import android.net.Uri
import androidx.core.app.NotificationCompat
import androidx.core.app.TaskStackBuilder
import androidx.core.net.toUri
import at.bitfire.davdroid.R
import at.bitfire.davdroid.repository.AccountRepository
import at.bitfire.davdroid.resource.LocalDataStore
@@ -122,7 +122,7 @@ class TasksAppManager @Inject constructor(
// couldn't get provider app icon
}
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=${e.provider.packageName}"))
val intent = Intent(Intent.ACTION_VIEW, "market://details?id=${e.provider.packageName}".toUri())
val flags = PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
if (intent.resolveActivity(pm) != null)

View File

@@ -45,7 +45,6 @@ import java.util.logging.Level
class TasksSyncManager @AssistedInject constructor(
@Assisted account: Account,
@Assisted httpClient: HttpClient,
@Assisted authority: String,
@Assisted syncResult: SyncResult,
@Assisted localCollection: LocalTaskList,
@Assisted collection: Collection,
@@ -54,7 +53,7 @@ class TasksSyncManager @AssistedInject constructor(
): SyncManager<LocalTask, LocalTaskList, DavCalendar>(
account,
httpClient,
authority,
SyncDataType.TASKS,
syncResult,
localCollection,
collection,
@@ -67,7 +66,6 @@ class TasksSyncManager @AssistedInject constructor(
fun tasksSyncManager(
account: Account,
httpClient: HttpClient,
authority: String,
syncResult: SyncResult,
localCollection: LocalTaskList,
collection: Collection,

View File

@@ -18,13 +18,14 @@ object SystemAccountUtils {
* @param context operating context
* @param account account to create
* @param userData user data to set
* @param password password to set
*
* @return whether the account has been created
*
* @throws IllegalArgumentException when user data contains non-String values
* @throws IllegalStateException if user data can't be set
*/
fun createAccount(context: Context, account: Account, userData: Bundle, password: String? = null): Boolean {
fun createAccount(context: Context, account: Account, userData: Bundle, password: CharArray? = null): Boolean {
// validate user data
for (key in userData.keySet()) {
userData.get(key)?.let { entry ->
@@ -35,7 +36,7 @@ object SystemAccountUtils {
// create account
val manager = AccountManager.get(context)
if (!manager.addAccountExplicitly(account, password, userData))
if (!manager.addAccountExplicitly(account, password?.concatToString(), userData))
return false
// Android seems to lose the initial user data sometimes, so make sure that the values are set

View File

@@ -1,58 +0,0 @@
/*
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
*/
package at.bitfire.davdroid.ui
import android.content.Intent
import android.os.Bundle
import androidx.activity.compose.setContent
import androidx.appcompat.app.AppCompatActivity
import at.bitfire.davdroid.ui.account.AccountActivity
import at.bitfire.davdroid.ui.intro.IntroActivity
import at.bitfire.davdroid.ui.setup.LoginActivity
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@AndroidEntryPoint
class AccountsActivity: AppCompatActivity() {
@Inject
lateinit var accountsDrawerHandler: AccountsDrawerHandler
private val introActivityLauncher = registerForActivityResult(IntroActivity.Contract) { cancelled ->
if (cancelled)
finish()
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// handle "Sync all" intent from launcher shortcut
val syncAccounts = intent.action == Intent.ACTION_SYNC
setContent {
AccountsScreen(
initialSyncAccounts = syncAccounts,
onShowAppIntro = {
introActivityLauncher.launch(null)
},
accountsDrawerHandler = accountsDrawerHandler,
onAddAccount = {
startActivity(Intent(this, LoginActivity::class.java))
},
onShowAccount = { account ->
val intent = Intent(this, AccountActivity::class.java)
intent.putExtra(AccountActivity.EXTRA_ACCOUNT, account)
startActivity(intent)
},
onManagePermissions = {
startActivity(Intent(this, PermissionsActivity::class.java))
}
)
}
}
}

View File

@@ -54,6 +54,7 @@ import java.util.logging.Logger
class AccountsModel @AssistedInject constructor(
@Assisted private val syncAccountsOnInit: Boolean,
private val accountRepository: AccountRepository,
internal val accountsDrawerHandler: AccountsDrawerHandler,
@ApplicationContext private val context: Context,
private val db: AppDatabase,
introPageFactory: IntroPageFactory,

View File

@@ -7,10 +7,10 @@ package at.bitfire.davdroid.ui
import android.Manifest
import android.accounts.Account
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.provider.Settings
import androidx.activity.compose.BackHandler
import androidx.activity.compose.LocalActivity
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
@@ -70,24 +70,55 @@ import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.core.net.toUri
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import at.bitfire.davdroid.BuildConfig
import at.bitfire.davdroid.R
import at.bitfire.davdroid.ui.account.AccountActivity
import at.bitfire.davdroid.ui.account.AccountProgress
import at.bitfire.davdroid.ui.composable.ActionCard
import at.bitfire.davdroid.ui.composable.ProgressBar
import at.bitfire.davdroid.ui.intro.IntroActivity
import at.bitfire.davdroid.ui.setup.LoginActivity
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.rememberPermissionState
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@Composable
fun AccountsScreen(initialSyncAccounts: Boolean) {
val context = LocalContext.current
val activity = LocalActivity.current
val introActivityLauncher = rememberLauncherForActivityResult(IntroActivity.Contract) { cancelled ->
if (cancelled) activity?.finish()
}
AccountsScreen(
initialSyncAccounts = initialSyncAccounts,
onShowAppIntro = {
introActivityLauncher.launch(null)
},
onAddAccount = {
context.startActivity(Intent(context, LoginActivity::class.java))
},
onShowAccount = { account ->
val intent = Intent(context, AccountActivity::class.java)
intent.putExtra(AccountActivity.EXTRA_ACCOUNT, account)
context.startActivity(intent)
},
onManagePermissions = {
context.startActivity(Intent(context, PermissionsActivity::class.java))
}
)
}
@Composable
fun AccountsScreen(
initialSyncAccounts: Boolean,
onShowAppIntro: () -> Unit,
accountsDrawerHandler: AccountsDrawerHandler,
onAddAccount: () -> Unit,
onShowAccount: (Account) -> Unit,
onManagePermissions: () -> Unit,
@@ -112,7 +143,7 @@ fun AccountsScreen(
}
AccountsScreen(
accountsDrawerHandler = accountsDrawerHandler,
accountsDrawerHandler = model.accountsDrawerHandler,
accounts = accounts,
showSyncAll = showSyncAll,
onSyncAll = { model.syncAllAccounts() },
@@ -148,13 +179,7 @@ fun AccountsScreen(
contactsStorageDisabled: Boolean = false
) {
val scope = rememberCoroutineScope()
val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
BackHandler(drawerState.isOpen) {
scope.launch {
drawerState.close()
}
}
var isRefreshing by remember { mutableStateOf(false) }
LaunchedEffect(isRefreshing) {
@@ -169,7 +194,7 @@ fun AccountsScreen(
ModalNavigationDrawer(
drawerState = drawerState,
drawerContent = {
ModalDrawerSheet {
ModalDrawerSheet(drawerState) {
accountsDrawerHandler.AccountsDrawer(
snackbarHostState = snackbarHostState,
onCloseDrawer = {
@@ -286,7 +311,7 @@ fun AccountsScreen(
onManageDataSaver = {
val intent = Intent(
/* action = */ Settings.ACTION_IGNORE_BACKGROUND_DATA_RESTRICTIONS_SETTINGS,
/* uri = */ Uri.parse("package:${BuildConfig.APPLICATION_ID}")
/* uri = */ "package:${BuildConfig.APPLICATION_ID}".toUri()
)
if (intent.resolveActivity(context.packageManager) != null)
context.startActivity(intent)

View File

@@ -6,12 +6,12 @@ package at.bitfire.davdroid.ui
import android.annotation.SuppressLint
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.provider.Settings
import androidx.activity.compose.setContent
import androidx.appcompat.app.AppCompatActivity
import androidx.core.net.toUri
import at.bitfire.davdroid.BuildConfig
import dagger.hilt.android.AndroidEntryPoint
@@ -31,7 +31,7 @@ class AppSettingsActivity: AppCompatActivity() {
startActivity(
Intent(
Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS,
Uri.parse("package:" + BuildConfig.APPLICATION_ID)
("package:" + BuildConfig.APPLICATION_ID).toUri()
)
)
},

View File

@@ -15,6 +15,7 @@ import androidx.core.content.FileProvider
import androidx.core.content.IntentCompat
import at.bitfire.davdroid.BuildConfig
import at.bitfire.davdroid.R
import at.bitfire.davdroid.sync.SyncDataType
import com.google.common.base.Ascii
import dagger.hilt.android.AndroidEntryPoint
import okhttp3.HttpUrl
@@ -38,8 +39,8 @@ class DebugInfoActivity : AppCompatActivity() {
/** [android.accounts.Account] (as [android.os.Parcelable]) related to problem */
private const val EXTRA_ACCOUNT = "account"
/** sync authority name related to problem */
private const val EXTRA_AUTHORITY = "authority"
/** sync data type related to problem */
private const val EXTRA_SYNC_DATA_TYPE = "syncDataType"
/** serialized [Throwable] that causes the problem */
private const val EXTRA_CAUSE = "cause"
@@ -64,7 +65,7 @@ class DebugInfoActivity : AppCompatActivity() {
setContent {
DebugInfoScreen(
account = IntentCompat.getParcelableExtra(intent, EXTRA_ACCOUNT, Account::class.java),
authority = extras?.getString(EXTRA_AUTHORITY),
syncDataType = extras?.getString(EXTRA_SYNC_DATA_TYPE),
cause = IntentCompat.getParcelableExtra(intent, EXTRA_CAUSE, Throwable::class.java),
localResource = extras?.getString(EXTRA_LOCAL_RESOURCE),
remoteResource = extras?.getString(EXTRA_REMOTE_RESOURCE),
@@ -151,9 +152,9 @@ class DebugInfoActivity : AppCompatActivity() {
return this
}
fun withAuthority(authority: String?): IntentBuilder {
if (authority != null)
intent.putExtra(EXTRA_AUTHORITY, authority)
fun withSyncDataType(dataType: SyncDataType?): IntentBuilder {
if (dataType != null)
intent.putExtra(EXTRA_SYNC_DATA_TYPE, dataType.name)
return this
}

View File

@@ -33,7 +33,6 @@ import androidx.work.WorkManager
import androidx.work.WorkQuery
import at.bitfire.dav4jvm.exception.DavException
import at.bitfire.davdroid.BuildConfig
import at.bitfire.davdroid.sync.account.InvalidAccountException
import at.bitfire.davdroid.R
import at.bitfire.davdroid.TextTable
import at.bitfire.davdroid.db.AppDatabase
@@ -43,6 +42,7 @@ import at.bitfire.davdroid.settings.AccountSettings
import at.bitfire.davdroid.settings.SettingsManager
import at.bitfire.davdroid.sync.SyncDataType
import at.bitfire.davdroid.sync.SyncFrameworkIntegration
import at.bitfire.davdroid.sync.account.InvalidAccountException
import at.bitfire.davdroid.sync.worker.BaseSyncWorker
import at.bitfire.ical4android.TaskProvider
import at.techbee.jtx.JtxContract
@@ -76,7 +76,7 @@ class DebugInfoGenerator @Inject constructor(
operator fun invoke(
syncAccount: Account?,
syncAuthority: String?,
syncDataType: String?,
cause: Throwable?,
localResource: String?,
remoteResource: String?,
@@ -97,12 +97,12 @@ class DebugInfoGenerator @Inject constructor(
}
// continue with most specific information
if (syncAccount != null || syncAuthority != null) {
if (syncAccount != null || syncDataType != null) {
writer.append("SYNCHRONIZATION INFO\n")
if (syncAccount != null)
writer.append("Account: $syncAccount\n")
if (syncAuthority != null)
writer.append("Authority: $syncAuthority\n")
if (syncDataType != null)
writer.append("SyncDataType: $syncDataType\n")
writer.append("\n")
}

View File

@@ -12,6 +12,8 @@ import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import at.bitfire.davdroid.log.LogFileHandler
import at.bitfire.davdroid.ui.DebugInfoModel.Companion.FILE_DEBUG_INFO
import at.bitfire.davdroid.ui.DebugInfoModel.Companion.FILE_LOGS
import com.google.common.io.ByteStreams
import com.google.common.io.Files
import dagger.assisted.Assisted
@@ -38,7 +40,7 @@ class DebugInfoModel @AssistedInject constructor(
data class DebugInfoDetails(
val account: Account?,
val authority: String?,
val syncDataType: String?,
val cause: Throwable?,
val localResource: String?,
val remoteResource: String?,
@@ -100,7 +102,7 @@ class DebugInfoModel @AssistedInject constructor(
)
generateDebugInfo(
syncAccount = details.account,
syncAuthority = details.authority,
syncDataType = details.syncDataType,
cause = details.cause,
localResource = details.localResource,
remoteResource = details.remoteResource,
@@ -116,7 +118,7 @@ class DebugInfoModel @AssistedInject constructor(
*/
private fun generateDebugInfo(
syncAccount: Account?,
syncAuthority: String?,
syncDataType: String?,
cause: Throwable?,
localResource: String?,
remoteResource: String?,
@@ -126,7 +128,7 @@ class DebugInfoModel @AssistedInject constructor(
debugInfoFile.printWriter().use { writer ->
debugInfoGenerator(
syncAccount = syncAccount,
syncAuthority = syncAuthority,
syncDataType = syncDataType,
cause = cause,
localResource = localResource,
remoteResource = remoteResource,

View File

@@ -54,7 +54,7 @@ import java.io.IOException
@Composable
fun DebugInfoScreen(
account: Account?,
authority: String?,
syncDataType: String?,
cause: Throwable?,
localResource: String?,
remoteResource: String?,
@@ -68,7 +68,7 @@ fun DebugInfoScreen(
creationCallback = { factory: DebugInfoModel.Factory ->
factory.createWithDetails(DebugInfoModel.DebugInfoDetails(
account = account,
authority = authority,
syncDataType = syncDataType,
cause = cause,
localResource = localResource,
remoteResource = remoteResource,

View File

@@ -0,0 +1,31 @@
/*
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
*/
package at.bitfire.davdroid.ui
import android.content.Intent
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import at.bitfire.davdroid.ui.navigation.Destination
import at.bitfire.davdroid.ui.navigation.Navigation
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class MainActivity: ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// handle "Sync all" intent from launcher shortcut
val syncAccounts = intent.action == Intent.ACTION_SYNC
setContent {
Navigation(
initialDestination = Destination.Accounts(syncAccounts),
)
}
}
}

View File

@@ -0,0 +1,50 @@
/*
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
*/
package at.bitfire.davdroid.ui
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import at.bitfire.davdroid.ui.navigation.Destination
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withPermit
class NavModel(
initialDestination: Destination,
) : ViewModel() {
private val _backStack = MutableStateFlow(listOf(initialDestination))
val backStack get() = _backStack.asStateFlow()
private val mutex = Semaphore(1)
/**
* Handles back navigation.
* @param amount The number of entries to pop from the end of the backstack, as calculated by the `NavDisplay`'s `sceneStrategy`.
*/
fun popBackStack(amount: Int) {
viewModelScope.launch {
mutex.withPermit {
val backStack = backStack.value.toMutableList().apply {
repeat(amount) { removeAt(lastIndex) }
}
_backStack.emit(backStack)
}
}
}
class Factory(
private val initialDestination: Destination = Destination.Accounts()
): ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return NavModel(initialDestination) as T
}
}
}

View File

@@ -39,6 +39,7 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.core.net.toUri
import androidx.core.text.HtmlCompat
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
@@ -110,8 +111,8 @@ fun TasksCard(
model.selectProvider(provider)
},
installApp = { packageName ->
val uri = Uri.parse("market://details?id=$packageName&referrer=" +
Uri.encode("utm_source=" + BuildConfig.APPLICATION_ID))
val uri = ("market://details?id=$packageName&referrer=" +
Uri.encode("utm_source=" + BuildConfig.APPLICATION_ID)).toUri()
val intent = Intent(Intent.ACTION_VIEW, uri)
if (intent.resolveActivity(context.packageManager) != null)
context.startActivity(intent)

View File

@@ -86,7 +86,7 @@ object UiUtils {
ShortcutInfo.Builder(context, SHORTCUT_SYNC_ALL)
.setIcon(Icon.createWithResource(context, R.drawable.ic_sync_shortcut))
.setShortLabel(context.getString(R.string.accounts_sync_all))
.setIntent(Intent(Intent.ACTION_SYNC, null, context, AccountsActivity::class.java))
.setIntent(Intent(Intent.ACTION_SYNC, null, context, MainActivity::class.java))
.build()
)
} catch(e: Exception) {

View File

@@ -12,7 +12,7 @@ import androidx.activity.compose.setContent
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.IntentCompat
import at.bitfire.davdroid.R
import at.bitfire.davdroid.ui.AccountsActivity
import at.bitfire.davdroid.ui.MainActivity
import dagger.hilt.android.AndroidEntryPoint
import java.util.logging.Logger
import javax.inject.Inject
@@ -35,7 +35,7 @@ class AccountActivity : AppCompatActivity() {
logger.warning("AccountActivity requires EXTRA_ACCOUNT")
// Redirect to accounts overview activity
val intent = Intent(this, AccountsActivity::class.java).apply {
val intent = Intent(this, MainActivity::class.java).apply {
// Create a new root activity, do not allow going back.
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK)
}

View File

@@ -6,7 +6,6 @@
import android.Manifest
import android.accounts.Account
import android.content.Intent
import android.net.Uri
import android.widget.Toast
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.Column
@@ -64,6 +63,7 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.core.net.toUri
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.paging.compose.LazyPagingItems
@@ -131,7 +131,7 @@ fun AccountScreen(
onUpdateCollectionSync = model::setCollectionSync,
onSubscribe = { collection ->
// subscribe
var uri = Uri.parse(collection.source.toString())
var uri = collection.source.toString().toUri()
when {
uri.scheme.equals("http", true) -> uri = uri.buildUpon().scheme("webcal").build()
uri.scheme.equals("https", true) -> uri = uri.buildUpon().scheme("webcals").build()
@@ -405,7 +405,7 @@ fun AccountScreen(
) {
val installIntent = Intent(
Intent.ACTION_VIEW,
Uri.parse("market://details?id=at.bitfire.icsdroid")
"market://details?id=at.bitfire.icsdroid".toUri()
)
if (context.packageManager.resolveActivity(installIntent, 0) != null)
context.startActivity(installIntent)

View File

@@ -549,7 +549,7 @@ fun AuthenticationSettings(
initialValue = null, // Do not show the existing password
passwordField = true,
onValueEntered = { newValue ->
onUpdateCredentials(credentials.copy(password = newValue))
onUpdateCredentials(credentials.copy(password = newValue.toCharArray()))
},
onDismiss = { showPasswordDialog = false }
)
@@ -740,7 +740,7 @@ fun AccountSettingsScreen_Preview() {
onUpdateIgnoreVpns = {},
// Authentication Settings
credentials = Credentials(username = "test", password = "test"),
credentials = Credentials(username = "test", password = "test".toCharArray()),
onUpdateCredentials = {},
isCredentialsUpdateAllowed = true,

View File

@@ -53,6 +53,7 @@ import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import at.bitfire.davdroid.R
import at.bitfire.davdroid.repository.DavSyncStatsRepository
import at.bitfire.davdroid.sync.SyncDataType
import at.bitfire.davdroid.ui.AppTheme
import at.bitfire.davdroid.ui.composable.ExceptionInfoDialog
import at.bitfire.davdroid.ui.composable.ProgressBar
@@ -272,8 +273,14 @@ fun CollectionScreen(
val formatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM)
for (lastSync in lastSynced) {
val dataType = when (lastSync.dataType) {
SyncDataType.EVENTS.name -> stringResource(R.string.collection_datatype_events)
SyncDataType.TASKS.name -> stringResource(R.string.collection_datatype_tasks)
SyncDataType.CONTACTS.name -> stringResource(R.string.collection_datatype_contacts)
else -> lastSync.dataType
}
Text(
text = stringResource(R.string.collection_last_sync, lastSync.appName),
text = stringResource(R.string.collection_last_sync, dataType),
style = MaterialTheme.typography.titleMedium
)
@@ -361,7 +368,7 @@ fun CollectionScreen_Preview() {
owner = "Some One",
lastSynced = listOf(
DavSyncStatsRepository.LastSynced(
appName = "Some Content Provider",
dataType = "Some Sync Data Type",
lastSynced = 1234567890
)
),

View File

@@ -4,7 +4,10 @@
package at.bitfire.davdroid.ui.composable
import android.net.Uri
import androidx.compose.foundation.focusGroup
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
@@ -25,7 +28,11 @@ import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.core.text.HtmlCompat
import at.bitfire.davdroid.Constants
import at.bitfire.davdroid.R
import at.bitfire.davdroid.ui.UiUtils.toAnnotatedString
@Composable
fun PasswordTextField(
@@ -42,33 +49,51 @@ fun PasswordTextField(
) {
var passwordVisible by remember { mutableStateOf(false) }
OutlinedTextField(
value = password,
onValueChange = onPasswordChange,
label = labelText?.let { { Text(it) } },
leadingIcon = leadingIcon,
isError = isError,
singleLine = true,
enabled = enabled,
readOnly = readOnly,
modifier = modifier.focusGroup(),
keyboardOptions = keyboardOptions,
keyboardActions = keyboardActions,
visualTransformation = if (passwordVisible) VisualTransformation.None else PasswordVisualTransformation(),
trailingIcon = {
IconButton(
enabled = enabled,
onClick = { passwordVisible = !passwordVisible }
) {
if (passwordVisible)
Icon(Icons.Default.VisibilityOff, stringResource(R.string.login_password_hide))
else
Icon(Icons.Default.Visibility, stringResource(R.string.login_password_show))
Column {
OutlinedTextField(
value = password,
onValueChange = onPasswordChange,
label = labelText?.let { { Text(it) } },
leadingIcon = leadingIcon,
isError = isError,
singleLine = true,
enabled = enabled,
readOnly = readOnly,
modifier = modifier.focusGroup(),
keyboardOptions = keyboardOptions,
keyboardActions = keyboardActions,
visualTransformation = if (passwordVisible) VisualTransformation.None else PasswordVisualTransformation(),
trailingIcon = {
IconButton(
enabled = enabled,
onClick = { passwordVisible = !passwordVisible }
) {
if (passwordVisible)
Icon(Icons.Default.VisibilityOff, stringResource(R.string.login_password_hide))
else
Icon(Icons.Default.Visibility, stringResource(R.string.login_password_show))
}
}
}
)
)
Text(
modifier = Modifier.padding(vertical = 8.dp),
text = HtmlCompat.fromHtml(
stringResource(
R.string.settings_app_password_hint,
appPasswordHelpUrl().toString()
),
0
).toAnnotatedString()
)
}
}
fun appPasswordHelpUrl(): Uri = Constants.MANUAL_URL.buildUpon()
.appendPath(Constants.MANUAL_PATH_INTRODUCTION)
.fragment(Constants.MANUAL_FRAGMENT_AUTHENTICATION_METHODS)
.build()
@Composable
@Preview
fun PasswordTextField_Sample() {

View File

@@ -7,9 +7,9 @@ package at.bitfire.davdroid.ui.intro
import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.net.Uri
import androidx.activity.result.contract.ActivityResultContract
import androidx.compose.runtime.Composable
import androidx.core.net.toUri
import at.bitfire.davdroid.settings.SettingsManager
import at.bitfire.davdroid.ui.intro.BatteryOptimizationsPageModel.Companion.HINT_AUTOSTART_PERMISSION
import at.bitfire.davdroid.ui.intro.BatteryOptimizationsPageModel.Companion.HINT_BATTERY_OPTIMIZATIONS
@@ -46,7 +46,7 @@ class BatteryOptimizationsPage @Inject constructor(
override fun createIntent(context: Context, input: String): Intent {
return Intent(
android.provider.Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS,
Uri.parse("package:$input")
"package:$input".toUri()
)
}

View File

@@ -0,0 +1,9 @@
/*
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
*/
package at.bitfire.davdroid.ui.navigation
sealed interface Destination {
data class Accounts(val syncAccounts: Boolean = false): Destination
}

View File

@@ -0,0 +1,45 @@
/*
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
*/
package at.bitfire.davdroid.ui.navigation
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator
import androidx.navigation3.runtime.entry
import androidx.navigation3.runtime.entryProvider
import androidx.navigation3.runtime.rememberSavedStateNavEntryDecorator
import androidx.navigation3.ui.NavDisplay
import androidx.navigation3.ui.rememberSceneSetupNavEntryDecorator
import at.bitfire.davdroid.ui.AccountsScreen
import at.bitfire.davdroid.ui.NavModel
@Composable
fun Navigation(
initialDestination: Destination = Destination.Accounts(),
model: NavModel = viewModel(factory = NavModel.Factory(initialDestination)),
) {
val backStack by model.backStack.collectAsState()
NavDisplay(
backStack = backStack,
onBack = model::popBackStack,
entryDecorators = listOf(
// Add the default decorators for managing scenes and saving state
rememberSceneSetupNavEntryDecorator(),
rememberSavedStateNavEntryDecorator(),
// Then add the view model store decorator
rememberViewModelStoreNavEntryDecorator(),
),
entryProvider = entryProvider {
entry<Destination.Accounts> { key ->
AccountsScreen(
initialSyncAccounts = key.syncAccounts,
)
}
},
)
}

View File

@@ -46,7 +46,7 @@ class AdvancedLoginModel @AssistedInject constructor(
baseUri = uri,
credentials = Credentials(
username = username.trimToNull(),
password = password.trimToNull(),
password = password.trimToNull()?.toCharArray(),
certificateAlias = certAlias.trimToNull()
)
)
@@ -60,7 +60,7 @@ class AdvancedLoginModel @AssistedInject constructor(
uiState = uiState.copy(
url = initialLoginInfo.baseUri?.toString()?.removePrefix("https://") ?: "",
username = initialLoginInfo.credentials?.username ?: "",
password = initialLoginInfo.credentials?.password ?: "",
password = initialLoginInfo.credentials?.password?.concatToString() ?: "",
certAlias = initialLoginInfo.credentials?.certificateAlias ?: ""
)
}

View File

@@ -38,7 +38,7 @@ class EmailLoginModel @AssistedInject constructor(
baseUri = uri,
credentials = Credentials(
username = email,
password = password
password = password.toCharArray()
)
)
}
@@ -50,7 +50,7 @@ class EmailLoginModel @AssistedInject constructor(
init {
uiState = uiState.copy(
email = initialLoginInfo.credentials?.username ?: "",
password = initialLoginInfo.credentials?.password ?: ""
password = initialLoginInfo.credentials?.password?.concatToString() ?: ""
)
}

View File

@@ -139,7 +139,7 @@ class LoginActivity @Inject constructor(): AppCompatActivity() {
},
credentials = Credentials(
username = givenUsername,
password = givenPassword
password = givenPassword?.toCharArray()
)
)
}

View File

@@ -46,7 +46,7 @@ class UrlLoginModel @AssistedInject constructor(
baseUri = uri,
credentials = Credentials(
username = username.trimToNull(),
password = password.trimToNull()
password = password.trimToNull()?.toCharArray()
)
)
@@ -59,7 +59,7 @@ class UrlLoginModel @AssistedInject constructor(
uiState = UiState(
url = initialLoginInfo.baseUri?.toString()?.removePrefix("https://") ?: "",
username = initialLoginInfo.credentials?.username ?: "",
password = initialLoginInfo.credentials?.password ?: ""
password = initialLoginInfo.credentials?.password?.concatToString() ?: ""
)
}

View File

@@ -13,6 +13,7 @@ import androidx.lifecycle.viewModelScope
import at.bitfire.davdroid.R
import at.bitfire.davdroid.db.AppDatabase
import at.bitfire.davdroid.db.Credentials
import at.bitfire.davdroid.util.trimToNull
import at.bitfire.davdroid.webdav.WebDavMountRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
@@ -82,8 +83,8 @@ class AddWebdavMountModel @Inject constructor(
val displayName = uiState.displayName
val credentials = Credentials(
username = uiState.username,
password = uiState.password,
username = uiState.username.trimToNull(),
password = uiState.password.trimToNull()?.toCharArray(),
certificateAlias = uiState.certificateAlias
)

View File

@@ -47,7 +47,7 @@ class CredentialsStore @Inject constructor(
return Credentials(
prefs.getString(keyName(mountId, USER_NAME), null),
prefs.getString(keyName(mountId, PASSWORD), null),
prefs.getString(keyName(mountId, PASSWORD), null)?.toCharArray(),
prefs.getString(keyName(mountId, CERTIFICATE_ALIAS), null)
)
}
@@ -57,7 +57,7 @@ class CredentialsStore @Inject constructor(
if (credentials != null)
putBoolean(keyName(mountId, HAS_CREDENTIALS), true)
.putString(keyName(mountId, USER_NAME), credentials.username)
.putString(keyName(mountId, PASSWORD), credentials.password)
.putString(keyName(mountId, PASSWORD), credentials.password?.concatToString())
.putString(keyName(mountId, CERTIFICATE_ALIAS), credentials.certificateAlias)
else
remove(keyName(mountId, HAS_CREDENTIALS))

View File

@@ -413,6 +413,8 @@
<string name="debug_info_logs_caption">Дневници</string>
<string name="debug_info_logs_subtitle">Налични са подробни дневници</string>
<string name="debug_info_logs_view">Преглед</string>
<string name="debug_info_privacy_warning_title">Съобщение за защита на личните данни</string>
<string name="debug_info_privacy_warning_description">Дневниците и информацията за отстраняване на грешки могат да съдържат лична информация. Имайте го предвид, когато ги споделяте публично.</string>
<!--ExceptionInfoFragment-->
<string name="exception">Възникна грешка.</string>
<string name="exception_httpexception">Възникна грешка на HTTP.</string>
@@ -460,5 +462,8 @@
<!--widgets-->
<string name="widget_sync_all">Синхронизирането всичко</string>
<string name="widget_sync_all_accounts">Синхронизиране на всички профили</string>
<string name="widget_labeled_sync_label">Бутон за синхронизиране с етикет</string>
<string name="widget_icon_sync_label">Бутон за синхронизиране с пиктограма</string>
<string name="widget_sync_description">Докоснете, за да бъде извършено ръчно синхронизиране.</string>
<!--cert4android-->
</resources>

View File

@@ -126,7 +126,7 @@
<string name="navigation_drawer_contribute">Einen Beitrag leisten</string>
<string name="navigation_drawer_privacy_policy">Datenschutzerklärung</string>
<string name="account_list_welcome">Willkommen bei DAVx⁵!</string>
<string name="account_list_empty">Verbinden Sie sich mit Ihrem Server und synchronisieren Ihre Kalender und Kontakte .</string>
<string name="account_list_empty">Verbinden Sie sich mit Ihrem Server und synchronisieren Sie Ihre Kalender und Kontakte.</string>
<string name="accounts_sync_all">Alle Konten synchronisieren</string>
<!--Sync warnings-->
<string name="sync_warning_no_notification_permission">Benachrichtigungen deaktiviert. Sie werden nicht über Fehler bei der Synchronisierung informiert.</string>
@@ -204,6 +204,7 @@
<string name="app_settings_unifiedpush_distributor_fcm">FCM (Google Play)</string>
<string name="app_settings_unifiedpush_encrypted">Push-Nachrichten sind immer verschlüsselt.</string>
<!--AccountScreen-->
<string name="account_invalid_account">Konto gibt es nicht</string>
<string name="account_carddav">CardDAV</string>
<string name="account_caldav">CalDAV</string>
<string name="account_webcal">Webcal</string>
@@ -386,7 +387,7 @@
<string name="collection_owner">Besitzer</string>
<string name="collection_push_support">Push-Unterstützung</string>
<string name="collection_push_web_push">Server wirbt mit Push-Unterstützung</string>
<string name="collection_push_subscribed_at">Um %1$s angemeldet, läuft aus um %2$s</string>
<string name="collection_push_subscribed_at">Um %1$s angemeldet, läuft ab %2$s</string>
<string name="collection_last_sync">Letzte Synchronisierung (%s)</string>
<string name="collection_url">Adresse (URL)</string>
<!--debugging and DebugInfoActivity-->
@@ -413,6 +414,8 @@
<string name="debug_info_logs_caption">Protokoll</string>
<string name="debug_info_logs_subtitle">Ausführliches Protokoll verfügbar</string>
<string name="debug_info_logs_view">Logs anzeigen</string>
<string name="debug_info_privacy_warning_title">Datenschutzhinweis</string>
<string name="debug_info_privacy_warning_description">Protokolle und Debug-Informationen können private Daten enthalten. Seien Sie sich dessen bewusst, wenn Sie diese öffentlich weitergeben.</string>
<!--ExceptionInfoFragment-->
<string name="exception">Ein Fehler ist aufgetreten.</string>
<string name="exception_httpexception">Ein HTTP-Fehler ist aufgetreten.</string>
@@ -460,5 +463,8 @@
<!--widgets-->
<string name="widget_sync_all">Alles synchronisieren</string>
<string name="widget_sync_all_accounts">Alle Konten synchronisieren</string>
<string name="widget_labeled_sync_label">Beschriftete Sync-Taste</string>
<string name="widget_icon_sync_label">Sync-Taste-Symbol</string>
<string name="widget_sync_description">Antippen, um die Synchronisierung manuell durchzuführen.</string>
<!--cert4android-->
</resources>

View File

@@ -124,7 +124,7 @@
<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="navigation_drawer_privacy_policy">Privaatsusreeglid</string>
<string name="account_list_welcome">Tere tulemast kasutama rakendust DAVx⁵!</string>
<string name="account_list_empty">Loo ühendus oma serveriga ja hoia kalendrid ning kontaktid sünkroniseerituna.</string>
<string name="accounts_sync_all">Sünkroniseeri kõik kasutajakontod</string>
@@ -204,6 +204,7 @@
<string name="app_settings_unifiedpush_distributor_fcm">FCM (Google Play)</string>
<string name="app_settings_unifiedpush_encrypted">Tõuketeavituste sõnumid on alati krüptitud.</string>
<!--AccountScreen-->
<string name="account_invalid_account">Kasutajakontot pole olemas</string>
<string name="account_carddav">CardDAV</string>
<string name="account_caldav">CalDAV</string>
<string name="account_webcal">Webcal</string>
@@ -233,7 +234,7 @@
<string name="account_install_icsx5">Paigalda ICSx⁵</string>
<!--AddAccountActivity-->
<string name="login_title">Lisa kasutajakonto</string>
<string name="login_privacy_hint"><![CDATA[Kõik andmed liiguvad vaid sinu serveri ja sinu nutiseadme vahel. %1$s ei saada neid mitte kuhugile mujale. Lisateavet leiad <a href="%2$s">meie privaatsuspoliitikast</a>.]]></string>
<string name="login_privacy_hint"><![CDATA[Kõik andmed liiguvad vaid sinu serveri ja sinu nutiseadme vahel. %1$s ei saada neid mitte kuhugile mujale. Lisateavet leiad <a href="%2$s">meie Privaatsusreeglitest</a>.]]></string>
<string name="login_generic_login">Üldine sisselogimine</string>
<string name="login_provider_login">Teenusepakkujakohane sisselogimine</string>
<string name="login_continue">Jätka</string>
@@ -272,7 +273,7 @@
<string name="login_google_account">Google\'i kasutajakonto</string>
<string name="login_google">Logi sisse Google\'i kasutajakontoga</string>
<string name="login_google_client_id">Klienditunnus (kui soovid lisada)</string>
<string name="login_google_client_privacy_policy"><![CDATA[%1$s teisaldab sinu Google\'i kontaktide ja kalendri andmeid vaid sünkroniseerimiseks selles seadmes. Lisateavet leiad meie <a href="%2$s">Privaatsuspoliitikast</a>.]]></string>
<string name="login_google_client_privacy_policy"><![CDATA[%1$s teisaldab sinu Google\'i kontaktide ja kalendri andmeid vaid sünkroniseerimiseks selles seadmes. Lisateavet leiad meie <a href="%2$s">Privaatsusreeglitest</a>.]]></string>
<string name="login_google_client_limited_use"><![CDATA[%1$s järgib <a href="%2$s">Google\'i API teenuste kasutajaandmete poliitikat</a>, sealhulgas piiratud kasutuse nõudeid.]]></string>
<string name="login_oauth_couldnt_obtain_auth_code">Autoriseerimiskoodi saamine polnud võimalik</string>
<string name="login_type_nextcloud">Nextcloud</string>
@@ -413,6 +414,8 @@
<string name="debug_info_logs_caption">Logid</string>
<string name="debug_info_logs_subtitle">Saadaval on üksikasjalikud logid</string>
<string name="debug_info_logs_view">Vaata logisid</string>
<string name="debug_info_privacy_warning_title">Privaatsusteade</string>
<string name="debug_info_privacy_warning_description">Logid ja veaotsingu teave võivad sisaldada privaatset teavet. Nende andmete avalikul jagamisel palun arvesta sellega.</string>
<!--ExceptionInfoFragment-->
<string name="exception">Tekkis viga.</string>
<string name="exception_httpexception">Tekkis http-viga.</string>
@@ -460,5 +463,8 @@
<!--widgets-->
<string name="widget_sync_all">Sünkroniseeri kõik</string>
<string name="widget_sync_all_accounts">Sünkroniseeri kõik kasutajakontod</string>
<string name="widget_labeled_sync_label">Sildiga sünkroniseerimisnupp</string>
<string name="widget_icon_sync_label">Ikooniga sünkroniseerimisnupp</string>
<string name="widget_sync_description">Klõpsi sünkroniseerimise käsitsi käivitamiseks.</string>
<!--cert4android-->
</resources>

View File

@@ -200,6 +200,8 @@
<string name="app_settings_unifiedpush_no_distributor">プッシュのディストリビューターがインストールされていません</string>
<string name="app_settings_unifiedpush_no_endpoint">エンドポイントが設定されていません</string>
<string name="app_settings_unifiedpush_ready">%s 経由でプッシュメッセージを受信できます</string>
<string name="app_settings_unifiedpush_distributor_fcm">FCM (Google Play)</string>
<string name="app_settings_unifiedpush_encrypted">プッシュメッセージは常に暗号化されます。</string>
<!--AccountScreen-->
<string name="account_carddav">CardDAV</string>
<string name="account_caldav">CalDAV</string>
@@ -408,6 +410,8 @@
<string name="debug_info_logs_caption">ログ</string>
<string name="debug_info_logs_subtitle">詳細なログが利用できます</string>
<string name="debug_info_logs_view">ログを表示</string>
<string name="debug_info_privacy_warning_title">プライバシー通知</string>
<string name="debug_info_privacy_warning_description">ログやデバッグ情報はプライベートな情報を含むことがあります。共有する場合には、注意して取り扱ってください。</string>
<!--ExceptionInfoFragment-->
<string name="exception">エラーが発生しました</string>
<string name="exception_httpexception">HTTP エラーが発生しました</string>
@@ -455,5 +459,8 @@
<!--widgets-->
<string name="widget_sync_all">すべて同期</string>
<string name="widget_sync_all_accounts">すべてのアカウントを同期</string>
<string name="widget_labeled_sync_label">同期ボタン (ラベル)</string>
<string name="widget_icon_sync_label">同期ボタン (アイコン)</string>
<string name="widget_sync_description">手動で同期したいときにタップしてください。</string>
<!--cert4android-->
</resources>

View File

@@ -201,7 +201,10 @@
<string name="app_settings_unifiedpush_no_distributor">Geen push distributeur geïnstalleerd</string>
<string name="app_settings_unifiedpush_no_endpoint">Geen eindpunt geconfigureerd</string>
<string name="app_settings_unifiedpush_ready">Klaar om pushberichten te ontvangen via %s</string>
<string name="app_settings_unifiedpush_distributor_fcm">FCM (Google Play)</string>
<string name="app_settings_unifiedpush_encrypted">Pushberichten zijn altijd versleuteld.</string>
<!--AccountScreen-->
<string name="account_invalid_account">Account bestaat niet</string>
<string name="account_carddav">CardDAV</string>
<string name="account_caldav">CalDAV</string>
<string name="account_webcal">Webcal</string>
@@ -411,6 +414,8 @@
<string name="debug_info_logs_caption">Logboeken</string>
<string name="debug_info_logs_subtitle">Uitgebreide logboeken zijn beschikbaar</string>
<string name="debug_info_logs_view">Details bekijken</string>
<string name="debug_info_privacy_warning_title">Privacyverklaring</string>
<string name="debug_info_privacy_warning_description">Logboeken en foutopsporingsgegevens kunnen privé-informatie bevatten. Houd hier rekening mee als u ze openbaar deelt.</string>
<!--ExceptionInfoFragment-->
<string name="exception">Er is een fout opgetreden.</string>
<string name="exception_httpexception">Een HTTP-fout is opgetreden.</string>
@@ -458,5 +463,8 @@
<!--widgets-->
<string name="widget_sync_all">Alles synchroniseren</string>
<string name="widget_sync_all_accounts">Alle accounts synchroniseren</string>
<string name="widget_labeled_sync_label">Gelabelde synchronisatieknop</string>
<string name="widget_icon_sync_label">Pictogram synchronisatieknop</string>
<string name="widget_sync_description">Tik om de synchronisatie handmatig uit te voeren.</string>
<!--cert4android-->
</resources>

View File

@@ -202,7 +202,10 @@
<string name="app_settings_unifiedpush_no_distributor">Nu este instalat un distribuitor push</string>
<string name="app_settings_unifiedpush_no_endpoint">Niciun punct final configurat</string>
<string name="app_settings_unifiedpush_ready">Gata să primească mesaje push peste %s</string>
<string name="app_settings_unifiedpush_distributor_fcm">FCM (Google Play)</string>
<string name="app_settings_unifiedpush_encrypted">Mesajele push sunt întotdeauna criptate.</string>
<!--AccountScreen-->
<string name="account_invalid_account">Contul nu există</string>
<string name="account_carddav">CardDAV</string>
<string name="account_caldav">CalDAV</string>
<string name="account_webcal">Webcal</string>
@@ -414,6 +417,8 @@
<string name="debug_info_logs_caption">Jurnale</string>
<string name="debug_info_logs_subtitle">Jurnalele detaliate sunt disponibile</string>
<string name="debug_info_logs_view">Vezi jurnalele</string>
<string name="debug_info_privacy_warning_title">Notificare de confidențialitate</string>
<string name="debug_info_privacy_warning_description">Jurnalele și informațiile de depanare pot conține informații private. Fii conștient de acest lucru atunci când îl publici.</string>
<!--ExceptionInfoFragment-->
<string name="exception">A avut loc o eroare.</string>
<string name="exception_httpexception">A apărut o eroare HTTP.</string>
@@ -461,5 +466,8 @@
<!--widgets-->
<string name="widget_sync_all">Sincronizează tot</string>
<string name="widget_sync_all_accounts">Sincronizează toate conturile</string>
<string name="widget_labeled_sync_label">Eticheta butonului de sincronizare</string>
<string name="widget_icon_sync_label">Pictograma butonului de sincronizare</string>
<string name="widget_sync_description">Atinge pentru a rula sincronizarea manual.</string>
<!--cert4android-->
</resources>

View File

@@ -206,6 +206,7 @@
<string name="app_settings_unifiedpush_distributor_fcm">FCM (Google Play)</string>
<string name="app_settings_unifiedpush_encrypted">Push-сообщения всегда зашифрованы.</string>
<!--AccountScreen-->
<string name="account_invalid_account">Аккаунт не существует</string>
<string name="account_carddav">CardDAV</string>
<string name="account_caldav">CalDAV</string>
<string name="account_webcal">WebСal</string>
@@ -419,6 +420,8 @@
<string name="debug_info_logs_caption">Логи</string>
<string name="debug_info_logs_subtitle">Доступны подробные логи</string>
<string name="debug_info_logs_view">Просмотр логов</string>
<string name="debug_info_privacy_warning_title"> Предупреждение о конфиденциальности</string>
<string name="debug_info_privacy_warning_description">Журналы и отладочная информация могут содержать конфиденциальную информацию. Пожалуйста, помните об этом при публичном использовании.</string>
<!--ExceptionInfoFragment-->
<string name="exception">Произошла ошибка.</string>
<string name="exception_httpexception">Произошла ошибка HTTP</string>
@@ -466,5 +469,8 @@
<!--widgets-->
<string name="widget_sync_all">Синхронизировать все</string>
<string name="widget_sync_all_accounts">Синхронизировать все аккаунты</string>
<string name="widget_labeled_sync_label">Ярлык кнопки синхронизации</string>
<string name="widget_icon_sync_label">Значок кнопки синхронизации</string>
<string name="widget_sync_description">Нажмите для запуска синхронизации вручную.</string>
<!--cert4android-->
</resources>

View File

@@ -411,6 +411,7 @@
<string name="debug_info_logs_caption">Loggar</string>
<string name="debug_info_logs_subtitle">Utförliga loggar finns tillgängliga</string>
<string name="debug_info_logs_view">Visa loggar</string>
<string name="debug_info_privacy_warning_title">Integritetspolicy</string>
<!--ExceptionInfoFragment-->
<string name="exception">Ett fel har uppstått.</string>
<string name="exception_httpexception">Ett HTTP-fel har uppstått.</string>
@@ -457,5 +458,6 @@
<!--widgets-->
<string name="widget_sync_all">Synkronisera alla</string>
<string name="widget_sync_all_accounts">Synkronisera alla konton</string>
<string name="widget_sync_description">Tryck för att köra synkronisering manuellt.</string>
<!--cert4android-->
</resources>

View File

@@ -203,6 +203,7 @@
<string name="app_settings_unifiedpush_distributor_fcm">FCM (Google Play)</string>
<string name="app_settings_unifiedpush_encrypted">推送消息始终是加密的</string>
<!--AccountScreen-->
<string name="account_invalid_account">账户不存在</string>
<string name="account_carddav">CardDAV</string>
<string name="account_caldav">CalDAV</string>
<string name="account_webcal">Webcal</string>
@@ -410,6 +411,8 @@
<string name="debug_info_logs_caption">日志</string>
<string name="debug_info_logs_subtitle">详细日志可用</string>
<string name="debug_info_logs_view">查看日志</string>
<string name="debug_info_privacy_warning_title">隐私声明</string>
<string name="debug_info_privacy_warning_description">日志和调试信息可能包含私密信息。公开分享时请意识到这一点</string>
<!--ExceptionInfoFragment-->
<string name="exception">出现错误</string>
<string name="exception_httpexception">出现 HTTP 错误</string>
@@ -457,5 +460,8 @@
<!--widgets-->
<string name="widget_sync_all">同步所有</string>
<string name="widget_sync_all_accounts">同步所有账户</string>
<string name="widget_labeled_sync_label">带标签的同步按钮</string>
<string name="widget_icon_sync_label">同步按钮图标</string>
<string name="widget_sync_description">轻按手动运行同步</string>
<!--cert4android-->
</resources>

View File

@@ -367,7 +367,8 @@
<string name="settings_ignore_vpns_off">VPN without underlying validated Internet connection is enough to run synchronization</string>
<string name="settings_authentication">Authentication</string>
<string name="settings_username">User name</string>
<string name="settings_password">Password</string>
<string name="settings_password">Password or app password</string>
<string name="settings_app_password_hint"><![CDATA[You may prefer to use an <a href="%1$s">app password</a>.]]></string>
<string name="settings_new_password">New password</string>
<string name="settings_password_summary">Update the password according to your server.</string>
<string name="settings_certificate_alias">Client certificate</string>
@@ -424,6 +425,9 @@
<string name="create_collection_optional">* optional</string>
<!-- CollectionScreen -->
<string name="collection_datatype_contacts">contacts</string>
<string name="collection_datatype_events">events</string>
<string name="collection_datatype_tasks">tasks</string>
<string name="collection_delete">Delete collection</string>
<string name="collection_delete_warning">This collection (%s) and all its data will be removed permanently, both locally and on the server.</string>
<string name="collection_synchronization">Synchronization</string>

View File

@@ -4,13 +4,12 @@
package at.bitfire.davdroid.di
import at.bitfire.davdroid.ui.intro.OseIntroPageFactory
import at.bitfire.davdroid.ui.AboutActivity
import at.bitfire.davdroid.ui.AccountsDrawerHandler
import at.bitfire.davdroid.ui.OpenSourceLicenseInfoProvider
import at.bitfire.davdroid.ui.OseAccountsDrawerHandler
import at.bitfire.davdroid.ui.intro.IntroPageFactory
import at.bitfire.davdroid.ui.intro.OseIntroPageFactory
import at.bitfire.davdroid.ui.setup.LoginTypesProvider
import at.bitfire.davdroid.ui.setup.StandardLoginTypesProvider
import dagger.Binds
@@ -25,9 +24,6 @@ interface OseModules {
@Module
@InstallIn(ActivityComponent::class)
interface ForActivities {
@Binds
fun accountsDrawerHandler(impl: OseAccountsDrawerHandler): AccountsDrawerHandler
@Binds
fun loginTypesProvider(impl: StandardLoginTypesProvider): LoginTypesProvider
}
@@ -35,6 +31,9 @@ interface OseModules {
@Module
@InstallIn(ViewModelComponent::class)
interface ForViewModels {
@Binds
fun accountsDrawerHandler(impl: OseAccountsDrawerHandler): AccountsDrawerHandler
@Binds
fun appLicenseInfoProvider(impl: OpenSourceLicenseInfoProvider): AboutActivity.AppLicenseInfoProvider

View File

@@ -7,6 +7,7 @@ plugins {
alias(libs.plugins.compose.compiler) apply false
alias(libs.plugins.hilt) apply false
alias(libs.plugins.kotlin.android) apply false
alias(libs.plugins.kotlin.serialization) apply false
alias(libs.plugins.ksp) apply false
alias(libs.plugins.mikepenz.aboutLibraries) apply false

View File

@@ -1,25 +1,28 @@
# Comments apply to next line
[versions]
android-agp = "8.10.0"
android-agp = "8.10.1"
android-desugaring = "2.1.5"
androidx-activityCompose = "1.10.1"
androidx-appcompat = "1.7.0"
androidx-activityCompose = "1.12.0-alpha02"
androidx-appcompat = "1.7.1"
androidx-arch = "2.2.0"
androidx-browser = "1.8.0"
androidx-core = "1.16.0"
androidx-hilt = "1.2.0"
androidx-lifecycle = "2.9.0"
androidx-nav3-core = "1.0.0-alpha03"
androidx-nav3-material = "1.0.0-SNAPSHOT"
androidx-nav3-lifecycle = "1.0.0-alpha01"
androidx-paging = "3.3.6"
androidx-preference = "1.2.1"
androidx-security = "1.1.0-alpha07"
androidx-security = "1.1.0-beta01"
androidx-test-core = "1.6.1"
androidx-test-runner = "1.6.2"
androidx-test-rules = "1.6.1"
androidx-test-junit = "1.2.1"
androidx-work = "2.10.1"
bitfire-cert4android = "b67ba86d31"
bitfire-dav4jvm = "ec6264d427"
bitfire-dav4jvm = "05fb8ecda6"
bitfire-ical4android = "240f756bab"
bitfire-vcard4android = "59eb998f29"
compose-accompanist = "0.37.3"
@@ -29,10 +32,12 @@ glance = "1.1.1"
guava = "33.4.8-android"
hilt = "2.56.2"
# keep in sync with ksp version
kotlin = "2.1.20"
kotlin = "2.1.21"
kotlinx-coroutines = "1.10.2"
kotlinx-serialization = "2.1.21"
kotlinx-serializationCore = "1.8.1"
# see https://github.com/google/ksp/releases for version numbers
ksp = "2.1.20-2.0.0"
ksp = "2.1.21-2.0.1"
mikepenz-aboutLibraries = "12.1.2"
nsk90-kstatemachine = "0.33.0"
mockk = "1.14.2"
@@ -60,6 +65,9 @@ androidx-hilt-work = { module = "androidx.hilt:hilt-work", version.ref = "androi
androidx-lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "androidx-lifecycle" }
androidx-lifecycle-viewmodel-base = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "androidx-lifecycle" }
androidx-lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "androidx-lifecycle" }
androidx-lifecycle-viewmodel-navigation3 = { module = "androidx.lifecycle:lifecycle-viewmodel-navigation3", version.ref = "androidx-nav3-lifecycle" }
androidx-navigation3-runtime = { module = "androidx.navigation3:navigation3-runtime", version.ref = "androidx-nav3-core" }
androidx-navigation3-ui = { module = "androidx.navigation3:navigation3-ui", version.ref = "androidx-nav3-core" }
androidx-paging = { module = "androidx.paging:paging-runtime-ktx", version.ref = "androidx-paging" }
androidx-paging-compose = { module = "androidx.paging:paging-compose", version.ref = "androidx-paging" }
androidx-preference = { module = "androidx.preference:preference-ktx", version.ref = "androidx-preference" }
@@ -70,15 +78,16 @@ androidx-test-rules = { module = "androidx.test:rules", version.ref = "androidx-
androidx-test-junit = { module = "androidx.test.ext:junit", version.ref = "androidx-test-junit" }
androidx-work-base = { module = "androidx.work:work-runtime-ktx", version.ref = "androidx-work" }
androidx-work-testing = { module = "androidx.work:work-testing", version.ref = "androidx-work" }
bitfire-cert4android = { module = "com.github.bitfireAT:cert4android", version.ref = "bitfire-cert4android" }
bitfire-dav4jvm = { module = "com.github.bitfireAT:dav4jvm", version.ref = "bitfire-dav4jvm" }
bitfire-ical4android = { module = "com.github.bitfireAT:ical4android", version.ref = "bitfire-ical4android" }
bitfire-vcard4android = { module = "com.github.bitfireAT:vcard4android", version.ref = "bitfire-vcard4android" }
bitfire-cert4android = { module = "com.github.bitfireat:cert4android", version.ref = "bitfire-cert4android" }
bitfire-dav4jvm = { module = "com.github.bitfireat:dav4jvm", version.ref = "bitfire-dav4jvm" }
bitfire-ical4android = { module = "com.github.bitfireat:ical4android", version.ref = "bitfire-ical4android" }
bitfire-vcard4android = { module = "com.github.bitfireat:vcard4android", version.ref = "bitfire-vcard4android" }
commons-codec = { module = "commons-codec:commons-codec", version.ref = "commons-codec" }
commons-lang = { module = "org.apache.commons:commons-lang3", version.ref = "commons-lang" }
compose-accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "compose-accompanist" }
compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "compose-bom" }
compose-material3 = { group = "androidx.compose.material3", name = "material3" }
compose-material3-navigation3 = { group = "androidx.compose.material3.adaptive", name = "adaptive-navigation3", version.ref = "androidx-nav3-material" }
compose-materialIconsExtended = { module = "androidx.compose.material:material-icons-extended" }
compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling" }
compose-ui-toolingPreview = { module = "androidx.compose.ui:ui-tooling-preview" }
@@ -93,6 +102,8 @@ junit = { module = "junit:junit", version = "4.13.2" }
kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" }
kotlinx-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" }
kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinx-coroutines" }
kotlinx-serialization-core = { module = "org.jetbrains.kotlinx:kotlinx-serialization-core", version.ref = "kotlinx-serializationCore" }
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serializationCore" }
mikepenz-aboutLibraries = { module = "com.mikepenz:aboutlibraries-compose-m3", version.ref = "mikepenz-aboutLibraries" }
mockk = { module = "io.mockk:mockk", version.ref = "mockk" }
mockk-android = { module = "io.mockk:mockk-android", version.ref = "mockk" }
@@ -115,5 +126,6 @@ android-application = { id = "com.android.application", version.ref = "android-a
compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlinx-serialization"}
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
mikepenz-aboutLibraries = { id = "com.mikepenz.aboutlibraries.plugin", version.ref = "mikepenz-aboutLibraries" }

View File

@@ -1,6 +1,10 @@
#
# Copyright <20> All Contributors. See LICENSE and AUTHORS in the root directory for details.
#
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.1-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME

View File

@@ -1,3 +1,7 @@
/*
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
*/
pluginManagement {
repositories {
google()
@@ -16,6 +20,13 @@ dependencyResolutionManagement {
// AppIntro, dav4jvm
maven("https://jitpack.io")
// To use ViewModel and Material 3 with Nav3
// See: https://developer.android.com/guide/navigation/navigation-3/get-started#artifacts
maven {
// View latest build id here: https://androidx.dev/snapshots/builds
url = uri("https://androidx.dev/snapshots/builds/13550935/artifacts/repository")
}
}
}