mirror of
https://github.com/bitfireAT/davx5-ose.git
synced 2026-02-27 12:28:10 -05:00
Compare commits
25 Commits
arnau/keys
...
nav3-migra
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e9fb031d0a | ||
|
|
d1c3548ccc | ||
|
|
762095c7ce | ||
|
|
fa50fe4c30 | ||
|
|
ba4d3b2fd1 | ||
|
|
0fed85fdc3 | ||
|
|
6fbaea9487 | ||
|
|
fc2bc8aa47 | ||
|
|
0321e4ab8f | ||
|
|
711543c5f1 | ||
|
|
5c485834e9 | ||
|
|
f349f1fec8 | ||
|
|
e6413506cb | ||
|
|
d9b36a0e34 | ||
|
|
514623c0f2 | ||
|
|
9978850594 | ||
|
|
e1f5b2e3c1 | ||
|
|
ad0cdb5c0c | ||
|
|
de9d58bc20 | ||
|
|
a6238a4131 | ||
|
|
bbc7fbfa1e | ||
|
|
3ba4dfb157 | ||
|
|
4544cd9b5c | ||
|
|
24026edad0 | ||
|
|
d4b5039297 |
@@ -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)
|
||||
|
||||
648
app/schemas/at.bitfire.davdroid.db.AppDatabase/18.json
Normal file
648
app/schemas/at.bitfire.davdroid.db.AppDatabase/18.json
Normal 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')"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -94,7 +94,6 @@ class JtxSyncManagerTest {
|
||||
syncManager = jtxSyncManagerFactory.jtxSyncManager(
|
||||
account = account,
|
||||
httpClient = httpClientBuilder.build(),
|
||||
authority = JtxContract.AUTHORITY,
|
||||
syncResult = SyncResult(),
|
||||
localCollection = localJtxCollection,
|
||||
collection = dbCollection,
|
||||
|
||||
@@ -494,7 +494,6 @@ class SyncManagerTest {
|
||||
}
|
||||
) = syncManagerFactory.create(
|
||||
account,
|
||||
"TestAuthority",
|
||||
httpClientBuilder.build(),
|
||||
syncResult,
|
||||
localCollection,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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))
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -104,7 +104,6 @@ class AddressBookSyncer @AssistedInject constructor(
|
||||
val syncManager = contactsSyncManagerFactory.contactsSyncManager(
|
||||
account,
|
||||
httpClient.value,
|
||||
dataStore.authority,
|
||||
syncResult,
|
||||
provider,
|
||||
addressBook,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -59,7 +59,6 @@ class CalendarSyncer @AssistedInject constructor(
|
||||
val syncManager = calendarSyncManagerFactory.calendarSyncManager(
|
||||
account,
|
||||
httpClient.value,
|
||||
dataStore.authority,
|
||||
syncResult,
|
||||
localCollection,
|
||||
remoteCollection,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -72,7 +72,6 @@ class JtxSyncer @AssistedInject constructor(
|
||||
val syncManager = jtxSyncManagerFactory.jtxSyncManager(
|
||||
account,
|
||||
httpClient.value,
|
||||
dataStore.authority,
|
||||
syncResult,
|
||||
localCollection,
|
||||
remoteCollection,
|
||||
|
||||
@@ -54,4 +54,4 @@ enum class SyncDataType {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -73,7 +73,6 @@ class TaskSyncer @AssistedInject constructor(
|
||||
val syncManager = tasksSyncManagerFactory.tasksSyncManager(
|
||||
account,
|
||||
httpClient.value,
|
||||
dataStore.authority,
|
||||
syncResult,
|
||||
localCollection,
|
||||
remoteCollection,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
)
|
||||
)
|
||||
},
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
31
app/src/main/kotlin/at/bitfire/davdroid/ui/MainActivity.kt
Normal file
31
app/src/main/kotlin/at/bitfire/davdroid/ui/MainActivity.kt
Normal 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),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
50
app/src/main/kotlin/at/bitfire/davdroid/ui/NavModel.kt
Normal file
50
app/src/main/kotlin/at/bitfire/davdroid/ui/NavModel.kt
Normal 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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
|
||||
@@ -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
|
||||
)
|
||||
),
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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()
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -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 ?: ""
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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() ?: ""
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -139,7 +139,7 @@ class LoginActivity @Inject constructor(): AppCompatActivity() {
|
||||
},
|
||||
credentials = Credentials(
|
||||
username = givenUsername,
|
||||
password = givenPassword
|
||||
password = givenPassword?.toCharArray()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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() ?: ""
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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" }
|
||||
|
||||
6
gradle/wrapper/gradle-wrapper.properties
vendored
6
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -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
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user