Compare commits

..

38 Commits

Author SHA1 Message Date
Ricki Hirner
ce56047da3 Version bump to 2.0 2018-07-30 10:15:59 +02:00
Ricki Hirner
3186b89874 Fetch translations from Transifex 2018-07-28 14:34:09 +02:00
Ricki Hirner
1b0557d4b8 Version bump to 2.0-rc1 2018-07-28 14:29:32 +02:00
Ricki Hirner
5efe1dbed0 Vector drawable: fix Android 4.4 compatibility 2018-07-28 14:29:01 +02:00
Ricki Hirner
d5f1074e30 Fetch translations from Transifex 2018-07-27 16:52:57 +02:00
Ricki Hirner
5e32dc7c91 Fix/improve error handling; bump version to 1.12-beta4 2018-07-27 16:49:42 +02:00
Ricki Hirner
1914269811 Version bump to 1.12-beta3 2018-07-22 11:22:34 +02:00
Ricki Hirner
f1d92b0bfe Fetch translations from Transifex 2018-07-22 11:18:50 +02:00
Ricki Hirner
3c76eb79d6 Use Apache Commons ContextedException instead of own class 2018-07-22 11:15:53 +02:00
Ricki Hirner
86e9cb4ace Rewrite startup strings 2018-07-21 11:07:34 +02:00
Ricki Hirner
3c6ff6e6ac Managed: uses managed profiles feature 2018-07-20 11:47:10 +02:00
Ricki Hirner
1082dc367d Version bump to 1.12-beta2 2018-07-17 13:33:15 +02:00
Ricki Hirner
6e935c433c Collection info: allow copying URL to clipboard 2018-07-17 13:23:59 +02:00
Ricki Hirner
4536378ac9 Collection info fragment 2018-07-17 12:47:46 +02:00
Ricki Hirner
042fb6fefa Remove Espresso tests for now 2018-07-17 12:45:45 +02:00
Ricki Hirner
29faa422ba Account activity: show "Select collections to synchronize" hint when no collections are selected 2018-07-16 18:38:04 +02:00
Ricki Hirner
563737b410 Update to okhttp 3.11 2018-07-16 14:00:49 +02:00
Ricki Hirner
384b91ca77 Update build tools; enable parallel sync of address books authority 2018-07-15 17:10:14 +02:00
Ricki Hirner
1ec933128d Fetch translations from Transifex 2018-07-13 15:06:59 +02:00
Ricki Hirner
f7df046af9 New About activity
* new About activity using AboutLibraries library
* include DAVdroid version and other non-personal information in URL when DAVdroid homepage is opened
  (so that we know what DAVdroid versions are used out there and maybe can provide version-specific help)
2018-07-13 14:57:14 +02:00
Ricki Hirner
1622d6d53e New sync logic with XML streaming
* refactored dav4android to use XML streaming
* refactored collection detection
* refactored sync logic
2018-07-12 00:04:51 +02:00
Ricki Hirner
4ba318fa14 Enable strict mode for debugging; use thread for DB changes in AccountActivity 2018-06-27 12:46:28 +02:00
Ricki Hirner
1eecbad457 Change detection error message 2018-06-24 21:33:51 +02:00
Ricki Hirner
4b9aa376b3 Rename address book accounts correctly when renaming accounts; drop unparsable fields from vCards 2018-06-20 12:38:19 +02:00
Ricki Hirner
9d7c72c3ca Managed: move lambda expressions out of parentheses 2018-06-17 16:55:48 +02:00
Ricki Hirner
f41025d4d8 Version bump to 1.11.5 2018-06-17 16:36:54 +02:00
Ricki Hirner
59c765a6e8 move lambda expressions out of parentheses; use CREATOR-named companion objects for Parcelable 2018-06-17 16:34:45 +02:00
Ricki Hirner
6ff069ac57 Fix lateinit null value in resource detection; update Kotlin and gradle 2018-06-16 15:07:25 +02:00
Ricki Hirner
050f86f020 Version bump to 1.11.4.1-mgd1 2018-06-15 13:34:01 +02:00
Ricki Hirner
e45c8ab1eb Managed: Add login introduction message 2018-06-15 13:33:05 +02:00
Ricki Hirner
dd30677334 Trigger a full calendar sync when past event time limit is changed in account settings 2018-06-14 09:20:55 +02:00
Ricki Hirner
822b49cfcf Don't show contacts permission notification when no address book is selected for synchronization 2018-06-13 17:22:43 +02:00
Ricki Hirner
c667c18199 Version bump to 1.11.4.1 2018-06-12 11:08:51 +02:00
Ricki Hirner
33491336af Version bump to 1.11.4.1-beta1 2018-06-12 00:37:31 +02:00
Ricki Hirner
502a40c179 Collection sync: don't reset "present remotely" flag after enumerating resources 2018-06-12 00:35:39 +02:00
Ricki Hirner
c0bb073a57 Version bump to 1.11.4 2018-06-08 11:50:39 +02:00
Ricki Hirner
a5a3fbb969 Fetch translations from Transifex 2018-06-08 11:50:39 +02:00
Ricki Hirner
9c8219108b Update gradle plugin 2018-06-08 11:50:35 +02:00
89 changed files with 1742 additions and 1840 deletions

View File

@@ -11,15 +11,19 @@ apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'org.jetbrains.dokka-android'
ext {
baseVersionName = '2.0'
}
android {
compileSdkVersion 27
buildToolsVersion '27.0.3'
buildToolsVersion '28.0.1'
defaultConfig {
applicationId "at.bitfire.davdroid"
resValue "string", "packageID", applicationId
versionCode 229
versionCode 241
buildConfigField "long", "buildTime", System.currentTimeMillis() + "L"
minSdkVersion 19 // Android 4.4
@@ -33,17 +37,16 @@ android {
}
flavorDimensions "type"
productFlavors {
standard {
dimension "type"
versionName "1.11.4"
versionName baseVersionName
buildConfigField "boolean", "customCerts", "true"
}
managed {
dimension "type"
versionName "1.11.4-mgd"
versionName "$baseVersionName-mgd"
applicationId "com.davdroid.managed"
resValue "string", "packageID", applicationId
@@ -55,20 +58,20 @@ android {
gplay {
dimension "type"
versionName "1.11.4-gplay"
versionName "$baseVersionName-gplay"
buildConfigField "boolean", "customCerts", "true"
}
icloud {
dimension "type"
versionName "1.11.4-icloud"
versionName "$baseVersionName-icloud"
applicationId "at.bitfire.cloudsync"
resValue "string", "packageID", applicationId
}
soldupe {
dimension "type"
versionName "1.11.4-soldupe"
versionName "$baseVersionName-soldupe"
applicationId "com.soldupe.cloudsync"
resValue "string", "packageID", applicationId
@@ -143,11 +146,11 @@ dependencies {
implementation 'com.android.support:cardview-v7:27.1.1'
implementation 'com.android.support:design:27.1.1'
implementation 'com.android.support:preference-v14:27.1.1'
implementation 'com.android.support.test.espresso:espresso-idling-resource:3.0.2'
implementation 'com.github.yukuku:ambilwarna:2.0.1'
implementation 'com.mikepenz:aboutlibraries:6.0.9'
implementation 'com.squareup.okhttp3:logging-interceptor:3.10.0'
implementation 'com.squareup.okhttp3:logging-interceptor:3.11.0'
implementation 'commons-io:commons-io:2.6'
implementation 'dnsjava:dnsjava:2.1.8'
implementation 'org.apache.commons:commons-lang3:3.7'
@@ -156,11 +159,9 @@ dependencies {
// for tests
androidTestImplementation 'com.android.support.test:runner:1.0.2'
androidTestImplementation 'com.android.support.test:rules:1.0.2'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
androidTestImplementation 'com.android.support.test.espresso:espresso-intents:3.0.2'
androidTestImplementation 'junit:junit:4.12'
androidTestImplementation 'com.squareup.okhttp3:mockwebserver:3.10.0'
androidTestImplementation 'com.squareup.okhttp3:mockwebserver:3.11.0'
testImplementation 'junit:junit:4.12'
testImplementation 'com.squareup.okhttp3:mockwebserver:3.10.0'
testImplementation 'com.squareup.okhttp3:mockwebserver:3.11.0'
}

View File

@@ -55,14 +55,15 @@ class CollectionInfoTest {
"</response>" +
"</multistatus>"))
var info: CollectionInfo? = null
DavResource(httpClient.okHttpClient, server.url("/"))
.propfind(0, ResourceType.NAME).use {
val info = CollectionInfo(it)
assertEquals(CollectionInfo.Type.ADDRESS_BOOK, info.type)
assertFalse(info.readOnly)
assertEquals("My Contacts", info.displayName)
assertEquals("My Contacts Description", info.description)
.propfind(0, ResourceType.NAME) { response, _ ->
info = CollectionInfo(response)
}
assertEquals(CollectionInfo.Type.ADDRESS_BOOK, info?.type)
assertFalse(info!!.readOnly)
assertEquals("My Contacts", info?.displayName)
assertEquals("My Contacts Description", info?.description)
// read-only calendar, no display name
server.enqueue(MockResponse()
@@ -80,18 +81,19 @@ class CollectionInfoTest {
"</response>" +
"</multistatus>"))
info = null
DavResource(httpClient.okHttpClient, server.url("/"))
.propfind(0, ResourceType.NAME).use {
val info = CollectionInfo(it)
assertEquals(CollectionInfo.Type.CALENDAR, info.type)
assertTrue(info.readOnly)
assertNull(info.displayName)
assertEquals("My Calendar", info.description)
assertEquals(0xFFFF0000.toInt(), info.color)
assertEquals("tzdata", info.timeZone)
assertTrue(info.supportsVEVENT)
assertTrue(info.supportsVTODO)
.propfind(0, ResourceType.NAME) { response, _ ->
info = CollectionInfo(response)
}
assertEquals(CollectionInfo.Type.CALENDAR, info?.type)
assertTrue(info!!.readOnly)
assertNull(info?.displayName)
assertEquals("My Calendar", info?.description)
assertEquals(0xFFFF0000.toInt(), info?.color)
assertEquals("tzdata", info?.timeZone)
assertTrue(info!!.supportsVEVENT)
assertTrue(info!!.supportsVTODO)
}
@Test

View File

@@ -69,26 +69,24 @@ class DavResourceFinderTest {
@SmallTest
fun testRememberIfAddressBookOrHomeset() {
// recognize home set
var info = ServiceInfo()
DavResource(client.okHttpClient, server.url(PATH_CARDDAV + SUBPATH_PRINCIPAL))
.propfind(0, AddressbookHomeSet.NAME).use {
ServiceInfo().let { info ->
finder.rememberIfAddressBookOrHomeset(it, info)
assertEquals(0, info.collections.size)
assertEquals(1, info.homeSets.size)
assertEquals(server.url("$PATH_CARDDAV$SUBPATH_ADDRESSBOOK_HOMESET/"), info.homeSets.first())
}
.propfind(0, AddressbookHomeSet.NAME) { response, _ ->
finder.scanCardDavResponse(response, info)
}
assertEquals(0, info.collections.size)
assertEquals(1, info.homeSets.size)
assertEquals(server.url("$PATH_CARDDAV$SUBPATH_ADDRESSBOOK_HOMESET/"), info.homeSets.first())
// recognize address book
info = ServiceInfo()
DavResource(client.okHttpClient, server.url(PATH_CARDDAV + SUBPATH_ADDRESSBOOK))
.propfind(0, ResourceType.NAME).use {
ServiceInfo().let { info ->
finder.rememberIfAddressBookOrHomeset(it, info)
assertEquals(1, info.collections.size)
assertEquals(server.url("$PATH_CARDDAV$SUBPATH_ADDRESSBOOK/"), info.collections.keys.first())
assertEquals(0, info.homeSets.size)
}
.propfind(0, ResourceType.NAME) { response, _ ->
finder.scanCardDavResponse(response, info)
}
assertEquals(1, info.collections.size)
assertEquals(server.url("$PATH_CARDDAV$SUBPATH_ADDRESSBOOK/"), info.collections.keys.first())
assertEquals(0, info.homeSets.size)
}
@Test

View File

@@ -44,19 +44,19 @@ class DefaultAccountsDrawerHandler: IAccountsDrawerHandler {
R.id.nav_twitter ->
activity.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("https://twitter.com/davdroidapp")))
R.id.nav_website ->
activity.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(activity.getString(R.string.homepage_url))))
activity.startActivity(Intent(Intent.ACTION_VIEW, App.homepageUrl(activity)))
R.id.nav_manual ->
activity.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(activity.getString(R.string.homepage_url))
activity.startActivity(Intent(Intent.ACTION_VIEW, App.homepageUrl(activity)
.buildUpon().appendEncodedPath("manual/").build()))
R.id.nav_faq ->
activity.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(activity.getString(R.string.homepage_url))
activity.startActivity(Intent(Intent.ACTION_VIEW, App.homepageUrl(activity)
.buildUpon().appendEncodedPath("faq/").build()))
R.id.nav_forums ->
activity.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(activity.getString(R.string.homepage_url))
activity.startActivity(Intent(Intent.ACTION_VIEW, App.homepageUrl(activity)
.buildUpon().appendEncodedPath("forums/").build()))
R.id.nav_donate ->
if (BuildConfig.FLAVOR != App.FLAVOR_GOOGLE_PLAY)
activity.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(activity.getString(R.string.homepage_url))
activity.startActivity(Intent(Intent.ACTION_VIEW, App.homepageUrl(activity)
.buildUpon().appendEncodedPath("donate/").build()))
else ->
return false

View File

@@ -54,10 +54,10 @@ class DefaultLoginCredentialsFragment: Fragment(), CompoundButton.OnCheckedChang
v.urlcert_select_cert.setOnClickListener {
KeyChain.choosePrivateKeyAlias(activity, { alias ->
Handler(Looper.getMainLooper()).post({
Handler(Looper.getMainLooper()).post {
v.urlcert_cert_alias.text = alias
v.urlcert_cert_alias.error = null
})
}
}, null, null, null, -1, view!!.urlcert_cert_alias.text.toString())
}
@@ -125,10 +125,10 @@ class DefaultLoginCredentialsFragment: Fragment(), CompoundButton.OnCheckedChang
var valid = true
val baseUrl = Uri.parse(view.urlpwd_base_url.text.toString())
val uri = validateBaseUrl(baseUrl, false, { message ->
val uri = validateBaseUrl(baseUrl, false) { message ->
view.urlpwd_base_url.error = message
valid = false
})
}
val userName = view.urlpwd_user_name.text.toString()
if (userName.isBlank()) {
@@ -153,10 +153,10 @@ class DefaultLoginCredentialsFragment: Fragment(), CompoundButton.OnCheckedChang
var valid = true
val baseUrl = Uri.parse(view.urlcert_base_url.text.toString())
val uri = validateBaseUrl(baseUrl, true, { message ->
val uri = validateBaseUrl(baseUrl, true) { message ->
view.urlcert_base_url.error = message
valid = false
})
}
val alias = view.urlcert_cert_alias.text.toString()
if (alias.isEmpty()) {

View File

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

Before

Width:  |  Height:  |  Size: 6.6 KiB

After

Width:  |  Height:  |  Size: 6.6 KiB

View File

Before

Width:  |  Height:  |  Size: 8.8 KiB

After

Width:  |  Height:  |  Size: 8.8 KiB

View File

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 45 KiB

View File

Before

Width:  |  Height:  |  Size: 8.8 KiB

After

Width:  |  Height:  |  Size: 8.8 KiB

View File

@@ -8,18 +8,12 @@
<string name="manage_accounts">Gestiona comptes</string>
<string name="please_wait">Esperi si us plau...</string>
<string name="send">Enviar</string>
<string name="homepage_url">https://www.davdroid.com/?pk_campaign=davdroid-app</string>
<string name="beta_feedback_url">https://forums.bitfire.at/category/9/beta-test-discussion?pk_campaign=davdroid-app</string>
<string name="notification_channel_debugging">Depurador</string>
<string name="notification_channel_general">Altres missatges importants</string>
<string name="notification_channel_sync">Sincronització</string>
<string name="notification_channel_sync_io_errors">Xarxa i errors E/S</string>
<string name="notification_channel_sync_status">Estat dels misatges</string>
<!--startup dialogs-->
<string name="startup_autostart_permission">%s permisos d\'inici automàtic</string>
<string name="startup_autostart_permission_message">El microprogramari del dispositiu pot impedir la sincronització automàtica. Hauries de permetre la sincronització automàtica manualment.</string>
<string name="startup_battery_optimization">Optimització de la bateria</string>
<string name="startup_battery_optimization_message">Android pot desactivar/reduir la sincronització de DAVdroid després d\'uns dies. Per impedir això, desactiva l\'optimització de la bateria.</string>
<string name="startup_battery_optimization_disable">Desactiva per DAVdroid</string>
<string name="startup_dont_show_again">No mostrar de nou</string>
<string name="startup_donate">Informació de Codi Obert</string>
@@ -31,7 +25,6 @@
<string name="startup_opentasks_not_installed">OpenTasks no està instal·lada</string>
<string name="startup_opentasks_not_installed_install">Instal·lar OpenTasks</string>
<!--AboutActivity-->
<string name="about_license_terms">Termes de la llicència</string>
<!--global settings-->
<string name="logging_no_external_storage">Emmagatzematge extern no trobat</string>
<!--AccountsActivity-->
@@ -74,7 +67,6 @@
<string name="account_create_new_calendar">Crear nou calendari</string>
<string name="account_install_icsdroid">Instal·lar ICSdroid</string>
<!--AddAccountActivity-->
<string name="login_help_url">https://www.davdroid.com/tested-with/?pk_campaign=davdroid-app</string>
<string name="login_title">Afegir compte</string>
<string name="login_type_email">Entra amb una adreça de correu electrònic</string>
<string name="login_email_address">Correu electrònic</string>
@@ -93,7 +85,6 @@
<string name="login_back">Sortir</string>
<string name="login_create_account">Crear compte</string>
<string name="login_account_name">Nom del compte</string>
<string name="login_view_logs">Veure els registres</string>
<!--AccountSettingsActivity-->
<string name="settings_title">Paràmetres: %s</string>
<string name="settings_authentication">Autentificació</string>

View File

@@ -7,8 +7,6 @@
<string name="please_wait">Chvíli strpení ...</string>
<string name="send">Odeslat</string>
<!--startup dialogs-->
<string name="startup_battery_optimization">Optimalizace využití baterie</string>
<string name="startup_battery_optimization_message">Android může po několika dnech vypnout/prodloužit interval synchronizování DAVdroid. Chcete-li tomuto zabránit, vypněte optimalizaci baterie.</string>
<string name="startup_battery_optimization_disable">Vypnout pro DAVdroid</string>
<string name="startup_dont_show_again">Již nezobrazovat</string>
<string name="startup_donate">Open Source informace</string>
@@ -21,7 +19,6 @@
<string name="startup_opentasks_reinstall_davdroid">Po instalaci OpenTasks musíte PŘEINSTALOVAT DAVdroid a přidat znovu své účty (Android chyba).</string>
<string name="startup_opentasks_not_installed_install">Nainstalovat OpenTasks</string>
<!--AboutActivity-->
<string name="about_license_terms">Licenční podmínky</string>
<string name="about_license_info_no_warranty">Tento program je distribuován BEZ JAKÉKOLIV ZÁRUKY. Je to volně dostupný software a lze jej za určitých podmínek dále distribuovat.</string>
<!--global settings-->
<string name="logging_davdroid_file_logging">DAVdroid logování do souboru</string>
@@ -106,7 +103,6 @@
<string name="login_configuration_detection">Vyhledání konfigurace</string>
<string name="login_querying_server">Chvíli strpení, probíhá dotazování serveru...</string>
<string name="login_no_caldav_carddav">Nelze nalézt službu CalDAV nebo CardDAV.</string>
<string name="login_view_logs">Prohlížet logy</string>
<!--AccountSettingsActivity-->
<string name="settings_title">Nastavení: %s</string>
<string name="settings_authentication">Ověření</string>

View File

@@ -15,10 +15,6 @@
<string name="notification_channel_sync_io_errors">Netværks- og I/O-fejl</string>
<string name="notification_channel_sync_status">Status beskeder</string>
<!--startup dialogs-->
<string name="startup_autostart_permission">%s tilladelse til at starte automatisk</string>
<string name="startup_autostart_permission_message">Enhedens firmware kan forbyde automatisk synkronisering. Du er muligvis nødt til at tillade automatisk synkronisering manuelt.</string>
<string name="startup_battery_optimization">Batteri optimering</string>
<string name="startup_battery_optimization_message">Android kan deaktivere/reducere DAVDroid synkronisering efter et par dage. For at undgå dette, slå batterioptimering fra.</string>
<string name="startup_battery_optimization_disable">Deaktivere DAVdroid</string>
<string name="startup_dont_show_again">Vis ikke igen</string>
<string name="startup_donate">Open-Source information</string>
@@ -33,7 +29,6 @@
<string name="startup_opentasks_reinstall_davdroid">Efter at have installeret OpenTasks, vil du være nødt til at GENINSTALLERE DAVdroid og dine konti igen (en fejl i Android).</string>
<string name="startup_opentasks_not_installed_install">Installere OpenTasks</string>
<!--AboutActivity-->
<string name="about_license_terms">Licensbetingelser</string>
<string name="about_license_info_no_warranty">Dette program leveres ABSOLUT UDEN GARANTI. Det er fri software, og du er velkommen til at videredistribuere det under visse betingelse.</string>
<!--global settings-->
<string name="logging_davdroid_file_logging">DAVdroid fil-logning</string>
@@ -109,7 +104,6 @@
<string name="account_no_webcal_handler_found">Der er ikke fundet noget program der kan håndtere Webcal.</string>
<string name="account_install_icsdroid">Installere ICSdroid</string>
<!--AddAccountActivity-->
<string name="login_help_url">https://www.davdroid.com/tested-with/?pk_campaign=davdroid-app</string>
<string name="login_title">Tilføje konto</string>
<string name="login_type_email">Logge ind med e-post adresse</string>
<string name="login_email_address">E-post adresse</string>
@@ -136,7 +130,6 @@
<string name="login_configuration_detection">Check konfiguration</string>
<string name="login_querying_server">Vent, forespørger serveren...</string>
<string name="login_no_caldav_carddav">Kunne ikke finde CalDAV- eller CardDAV-tjeneste.</string>
<string name="login_view_logs">Vis logs.</string>
<!--AccountSettingsActivity-->
<string name="settings_title">Indstillinger: %s</string>
<string name="settings_authentication">Adgangsgodkendelse</string>

View File

@@ -8,8 +8,6 @@
<string name="manage_accounts">Administrar cuentas</string>
<string name="please_wait">Por favor, espere...</string>
<string name="send">Enviar</string>
<string name="homepage_url">https://www.davdroid.com/?pk_campaign=davdroid-app</string>
<string name="beta_feedback_url">https://forums.bitfire.at/category/9/beta-test-discussion?pk_campaign=davdroid-app</string>
<string name="notification_channel_debugging">Depuración</string>
<string name="notification_channel_general">Otros mensajes importantes</string>
<string name="notification_channel_sync">Sincronización</string>
@@ -17,10 +15,6 @@
<string name="notification_channel_sync_io_errors">Errores de Red y E/S</string>
<string name="notification_channel_sync_status">Mensajes de estado</string>
<!--startup dialogs-->
<string name="startup_autostart_permission">%s permisos de inicio automático</string>
<string name="startup_autostart_permission_message">El firmware del dispositivo podría prevenir sincronización automática. Podría necesitar permitir sincronización manualmente.</string>
<string name="startup_battery_optimization">Optimización de batería</string>
<string name="startup_battery_optimization_message">Android puede desactivar/reducir la sincronización de DAVdroid después de unos días. Para prevenir esto, desactiva la optimización.</string>
<string name="startup_battery_optimization_disable">Apagar para DAVdroid</string>
<string name="startup_dont_show_again">No mostrar de nuevo</string>
<string name="startup_donate">Información de código abierto</string>
@@ -35,7 +29,6 @@
<string name="startup_opentasks_reinstall_davdroid">Tras instalar OpenTasks, tendrás que re-instalar DAVdroid y añadir tus cuentas de nuevo (por un error de Android).</string>
<string name="startup_opentasks_not_installed_install">Instalar OpenTasks</string>
<!--AboutActivity-->
<string name="about_license_terms">Términos de la licencia</string>
<string name="about_license_info_no_warranty">Este programa viene sin NINGÚN TIPO DE GARANTÍA. Es software libre, y cualquier contribución es bienvenida y redistribuida bajo ciertas condiciones.</string>
<!--global settings-->
<string name="logging_davdroid_file_logging">Archivo de registro de DAVdroid</string>
@@ -111,7 +104,6 @@
<string name="account_no_webcal_handler_found">No se encontró aplicación para administrar Webcal</string>
<string name="account_install_icsdroid">Instale ICSdroid</string>
<!--AddAccountActivity-->
<string name="login_help_url">https://www.davdroid.com/tested-with/?pk_campaign=davdroid-app</string>
<string name="login_title">Añadir cuenta</string>
<string name="login_type_email">Acceder con cuenta de correo</string>
<string name="login_email_address">Dirección de correo</string>
@@ -138,7 +130,6 @@
<string name="login_configuration_detection">Detectar configuración</string>
<string name="login_querying_server">Por favor espera, consultando al servidor...</string>
<string name="login_no_caldav_carddav">No se pudo encontrar el servicio CalDAV o CardDAV.</string>
<string name="login_view_logs">Ver registros</string>
<!--AccountSettingsActivity-->
<string name="settings_title">Ajustes: %s</string>
<string name="settings_authentication">Autenticación</string>

View File

@@ -9,8 +9,6 @@
<string name="please_wait">patientez ...</string>
<string name="send">Envoyer</string>
<!--startup dialogs-->
<string name="startup_battery_optimization">Optimisation de la batterie</string>
<string name="startup_battery_optimization_message">Android peut désactiver/réduire la synchronisation de DAVdroid après quelques jours. Pour éviter cela, désactivez l\'optimisation de la batterie.</string>
<string name="startup_battery_optimization_disable">Désactiver pour DAVdroid</string>
<string name="startup_dont_show_again">Ne plus afficher</string>
<string name="startup_donate">Open-Source Information</string>
@@ -23,7 +21,6 @@
<string name="startup_opentasks_reinstall_davdroid">Après l\'installation OpenTasks, vous devez RE-INSTALLER DAVdroid et ajoutez vos comptes à nouveau (bug Android).</string>
<string name="startup_opentasks_not_installed_install">Installer OpenTasks</string>
<!--AboutActivity-->
<string name="about_license_terms">Conditions d\'utilisation</string>
<string name="about_license_info_no_warranty">Ce programme est fourni sans AUCUNE GARANTIE. C\'est un logiciel libre, et vous êtes en droit de le redistribuer sous certaines conditions.</string>
<!--global settings-->
<string name="logging_davdroid_file_logging">DAVdroid fichier de journalisation</string>
@@ -118,7 +115,6 @@
<string name="login_configuration_detection">Détection de la configuration</string>
<string name="login_querying_server">Veuillez patienter, nous interrogeons le serveur ...</string>
<string name="login_no_caldav_carddav">Aucun accès possible au service CalDAV ou CardDAV.</string>
<string name="login_view_logs">Voir infos de débogage</string>
<!--AccountSettingsActivity-->
<string name="settings_title">Paramètres: %s</string>
<string name="settings_authentication">Authentification</string>

View File

@@ -8,8 +8,6 @@
<string name="manage_accounts">Fiókok kezelése</string>
<string name="please_wait">Kérjük, várjon ...</string>
<string name="send">Küldés</string>
<string name="homepage_url">https://www.davdroid.com/?pk_campaign=davdroid-app</string>
<string name="beta_feedback_url">https://forums.bitfire.at/category/9/beta-test-discussion?pk_campaign=davdroid-app</string>
<string name="notification_channel_debugging">Hibakeresés</string>
<string name="notification_channel_general">Egyéb fontos üzenetek</string>
<string name="notification_channel_sync">Szinkronizáció</string>
@@ -17,9 +15,6 @@
<string name="notification_channel_sync_io_errors">Hálózati és I/O hibák</string>
<string name="notification_channel_sync_status">Státuszüzenetek</string>
<!--startup dialogs-->
<string name="startup_autostart_permission_message">Lehetséges,hogy az eszköz nem teszi lehetővé az automatikus szinkronizálást és az automatikus szinkronizálást lehetőségét kézzel kell beállítani.</string>
<string name="startup_battery_optimization">Akkumulátoroptimalizálás </string>
<string name="startup_battery_optimization_message">Az operációs rendszer a DAVdroid szinkronizálást pár nap után leállíthatja vagy visszafoghatja. Ennek elkerülésére kapcsolja ki az akkumulátoroptimalizálást.</string>
<string name="startup_battery_optimization_disable">Kikapcsolás a DAVdroid kapcsán</string>
<string name="startup_dont_show_again">Ne jelenjen meg többet</string>
<string name="startup_donate">A forrás nyíltságával kapcsolatos információk</string>
@@ -34,7 +29,6 @@
<string name="startup_opentasks_reinstall_davdroid">Az OpenTasks telepítését követően újra kell telepíteni a DAVdroit alkalmazást és újra fel kell venni a fiókokat (Android hiba).</string>
<string name="startup_opentasks_not_installed_install">Az OpenTasks telepítése</string>
<!--AboutActivity-->
<string name="about_license_terms">Licencfeltételek</string>
<string name="about_license_info_no_warranty">Ehhez a program SEMMIFÉLE GARANCIA NEM JÁR. Ez a program szabad szoftver, ami a bizonyos feltételek mellett szabadon terjeszthető.</string>
<!--global settings-->
<string name="logging_davdroid_file_logging">DAVdroid fájlalapú naplózás</string>
@@ -110,7 +104,6 @@
<string name="account_no_webcal_handler_found">Nem található Webcal-képes alkalmazás</string>
<string name="account_install_icsdroid">ICSdroid telepítése</string>
<!--AddAccountActivity-->
<string name="login_help_url">https://www.davdroid.com/tested-with/?pk_campaign=davdroid-app</string>
<string name="login_title">Fiók hozzáadása</string>
<string name="login_type_email">Bejelentkezés email cím segítségével</string>
<string name="login_email_address">Email cím:</string>
@@ -137,7 +130,6 @@
<string name="login_configuration_detection">A konfiguráció felderítése</string>
<string name="login_querying_server">Kérjük, várjon, a szerver lekérdezése...</string>
<string name="login_no_caldav_carddav">Nem található CalDAV vagy CardDAV szolgáltatás.</string>
<string name="login_view_logs">Naplóbejegyzések megtekintése</string>
<!--AccountSettingsActivity-->
<string name="settings_title">Beállítások: %s</string>
<string name="settings_authentication">Authentikáció</string>

View File

@@ -14,8 +14,6 @@
<string name="notification_channel_sync_io_errors">Errori di Rete e di I/O</string>
<string name="notification_channel_sync_status">Messaggi di stato</string>
<!--startup dialogs-->
<string name="startup_battery_optimization">Ottimizzazione della batteria</string>
<string name="startup_battery_optimization_message">Android può ridurre o disabilitare la sincronizzazione di DAVdroid dopo alcuni giorni. Per prevenire questo comportamento disabilita l\'ottimizzazione della batteria</string>
<string name="startup_battery_optimization_disable">Disabilita per DAVdroid</string>
<string name="startup_dont_show_again">Non mostrare più</string>
<string name="startup_donate">Informazioni sull\'Open-Source</string>
@@ -30,7 +28,6 @@
<string name="startup_opentasks_reinstall_davdroid">Dopo l\'installazione di OpenTasks è necessario INSTALLARE NUOVAMENTE DAVdroid e aggiungere ancora gli account (per un bug di Android).</string>
<string name="startup_opentasks_not_installed_install">Installa OpenTasks</string>
<!--AboutActivity-->
<string name="about_license_terms">Termini di licenza</string>
<string name="about_license_info_no_warranty">Il programma è distribuito SENZA ALCUNA GARANZIA. È software libero e può essere redistribuito sotto alcune condizioni.</string>
<!--global settings-->
<string name="logging_davdroid_file_logging">Invio del log di DAVdroid su file</string>
@@ -129,7 +126,6 @@
<string name="login_configuration_detection">Rilevazione configurazione</string>
<string name="login_querying_server">Attendere, invio richiesta al server...</string>
<string name="login_no_caldav_carddav">Impossibile trovare servizi CalDAV o CardDAV.</string>
<string name="login_view_logs">Vedi i log</string>
<!--AccountSettingsActivity-->
<string name="settings_title">Impostazioni: %s</string>
<string name="settings_authentication">Autenticazione</string>

View File

@@ -4,29 +4,41 @@
<string name="app_name">DAVdroid</string>
<string name="account_title_address_book">DAVdroid アドレス帳</string>
<string name="address_books_authority_title">アドレス帳</string>
<string name="copied_to_clipboard">クリップボードにコピーしました</string>
<string name="help">ヘルプ</string>
<string name="manage_accounts">アカウントの管理</string>
<string name="please_wait">しばらくお待ちください …</string>
<string name="send">送信</string>
<string name="homepage_url">https://www.davdroid.com/?pk_campaign=davdroid-app</string>
<string name="beta_feedback_url">https://forums.bitfire.at/category/9/beta-test-discussion?pk_campaign=davdroid-app</string>
<string name="notification_channel_debugging">デバッグ中</string>
<string name="notification_channel_general">他の重要なメッセージ</string>
<string name="notification_channel_sync">同期</string>
<string name="notification_channel_sync_errors">同期エラー</string>
<string name="notification_channel_sync_io_errors">ネットワークおよび I/O エラー</string>
<string name="notification_channel_sync_status">ステータスメッセージ</string>
<!--startup dialogs-->
<string name="startup_battery_optimization">バッテリー最適化</string>
<string name="startup_battery_optimization_message">Android は数日後に DAVdroid の同期を無効にする/減らすことがあります。これを防止するには、バッテリー最適化をオフにしてください。</string>
<string name="startup_autostart_permission">自動同期</string>
<string name="startup_autostart_permission_message">%s ファームウェアは自動同期をブロックすることがよくあります。 この場合、Android の設定で自動同期を許可してください。</string>
<string name="startup_battery_optimization">スケジュール同期</string>
<string name="startup_battery_optimization_message">お使いのデバイスは DAVdroid の同期を制限します。 通常の DAVdroid 同期間隔を適用するには、「バッテリ最適化」をオフにしてください。</string>
<string name="startup_battery_optimization_disable">DAVdroid 用にオフにする</string>
<string name="startup_dont_show_again">次回から表示しない</string>
<string name="startup_not_now">後で</string>
<string name="startup_donate">オープンソース情報</string>
<string name="startup_donate_message">あなたがオープンソース ソフトウェア (GPLv3) の DAVdroid を使用していただくことに、私たちは満足しています。 DAVdroid の開発はハードワークで、何千もの作業時間がかかりました。寄付をご検討ください。</string>
<string name="startup_donate_now">寄付ページを表示</string>
<string name="startup_donate_later">たぶん後で</string>
<string name="startup_google_play_accounts_removed">Play ストア DRM バグ情報</string>
<string name="startup_google_play_accounts_removed_message">特定の条件下で、DAVdroid を再起動後またはアップグレードした後、Play ストア DRM によりすべての DAVdroid アカウントがなくなる可能性があります。この問題の影響を受けている場合 (のみ)、Play ストアから「DAVdroid JB 回避策」をインストールしてください。</string>
<string name="startup_more_info">追加情報</string>
<string name="startup_opentasks_not_installed">OpenTasks がインストールされていません</string>
<string name="startup_opentasks_not_installed_message">タスクを同期するために、無料アプリのOpenTasksが必要です。 (連絡先/イベントには必要ありません)</string>
<string name="startup_opentasks_reinstall_davdroid">OpenTasks をインストールした後で、DAVdroidを再インストールして、再度アカウントを追加してください (Android のバグ)。</string>
<string name="startup_opentasks_not_installed_install">OpenTasks をインストール</string>
<!--AboutActivity-->
<string name="about_license_terms">ライセンス規約</string>
<string name="about_libraries">ライブラリー</string>
<string name="about_version">バージョン %1s (%2d)</string>
<string name="about_build_date">コンパイル日時 %s</string>
<string name="about_flavor_info">このバージョンは Google Play での配信にのみ対応です。</string>
<string name="about_license_info_no_warranty">このプログラムは完全に無保証で提供されます。これはフリーソフトウェアで、特定の条件下での再頒布を歓迎します。</string>
<!--global settings-->
<string name="logging_davdroid_file_logging">DAVdroid ファイルログ</string>
@@ -88,6 +100,7 @@
<string name="account_delete">アカウントを削除</string>
<string name="account_delete_confirmation_title">アカウントを削除してもよろしいですか?</string>
<string name="account_delete_confirmation_text">アドレス帳、カレンダー、タスクリストのローカルコピーがすべて削除されます。</string>
<string name="account_select_collections_hint">同期するコレクションを選択</string>
<string name="account_carddav">CardDAV</string>
<string name="account_caldav">CalDAV</string>
<string name="account_webcal">Webcal</string>
@@ -102,7 +115,6 @@
<string name="account_no_webcal_handler_found">Webcal に対応するアプリが見つかりません</string>
<string name="account_install_icsdroid">ICSdroid をインストール</string>
<!--AddAccountActivity-->
<string name="login_help_url">https://www.davdroid.com/tested-with/?pk_campaign=davdroid-app</string>
<string name="login_title">アカウントを追加</string>
<string name="login_type_email">メールアドレスでログイン</string>
<string name="login_email_address">メールアドレス</string>
@@ -129,7 +141,7 @@
<string name="login_configuration_detection">設定の検出</string>
<string name="login_querying_server">しばらくお待ちください。サーバーに問い合わせ中…</string>
<string name="login_no_caldav_carddav">CalDAV または CardDAV サービスが見つかりません。</string>
<string name="login_view_logs">ログを表示</string>
<string name="login_view_logs">詳細を表示</string>
<!--AccountSettingsActivity-->
<string name="settings_title">設定: %s</string>
<string name="settings_authentication">認証</string>
@@ -209,6 +221,9 @@
<string name="delete_collection_confirm_warning">このコレクション (%s) とそのすべてのデータがサーバーから削除されます。</string>
<string name="delete_collection_deleting_collection">コレクションの削除中</string>
<string name="collection_force_read_only">強制的に読み取り専用</string>
<string name="collection_properties">プロパティ</string>
<string name="collection_properties_url">アドレス (URL):</string>
<string name="collection_properties_copy_url">URL をコピー</string>
<!--ExceptionInfoFragment-->
<string name="exception">エラーが発生しました。</string>
<string name="exception_httpexception">HTTP エラーが発生しました。</string>
@@ -224,6 +239,12 @@
<string name="sync_error_permissions_text">追加のアクセス許可が必要です</string>
<string name="sync_error_opentasks_too_old">OpenTasks が古すぎます</string>
<string name="sync_error_opentasks_required_version">必要なバージョン: %1$s (現在 %2$s)</string>
<string name="sync_error_authentication_failed">認証に失敗しました (ログイン情報を確認してください)</string>
<string name="sync_error_io">ネットワークまたは I/O エラー %s</string>
<string name="sync_error_http_dav">HTTP サーバーエラー %s</string>
<string name="sync_error_local_storage">内蔵ストレージエラー %s</string>
<string name="sync_error_retry">再試行</string>
<string name="sync_error_view_item">アイテムを表示</string>
<!--cert4android-->
<string name="certificate_notification_connection_security">DAVdroid: 接続セキュリティ</string>
<string name="trust_certificate_unknown_certificate_found">DAVdroidは、未知の証明書を検出しました。それを信頼しますか?</string>

View File

@@ -8,11 +8,7 @@
<string name="manage_accounts">Behandle kontoer</string>
<string name="please_wait">Vent…</string>
<string name="send">Send</string>
<string name="homepage_url">https://www.davdroid.com/?pk_campaign=davdroid-app</string>
<string name="beta_feedback_url">https://forums.bitfire.at/category/9/beta-test-discussion?pk_campaign=davdroid-app</string>
<!--startup dialogs-->
<string name="startup_battery_optimization">Batterioptimisering</string>
<string name="startup_battery_optimization_message">Det kan hende Android skrur av/reduserer DAVdroid-synkronisering etter et par dager. For å forhindre dette, skru av batterioptimisering.</string>
<string name="startup_battery_optimization_disable">Skru av for DAVdroid</string>
<string name="startup_dont_show_again">Ikke vis igjen</string>
<string name="startup_donate">Friprog-informasjon</string>
@@ -25,7 +21,6 @@
<string name="startup_opentasks_reinstall_davdroid">Etter å ha installert OpenTasks, må du reinstallere Davdroid og legge til kontoene dine igjen (Android-feil).</string>
<string name="startup_opentasks_not_installed_install">Installer OpenTasks</string>
<!--AboutActivity-->
<string name="about_license_terms">Lisensvilkår</string>
<string name="about_license_info_no_warranty">Dette programmet kommer uten NOEN FORM FOR GARANTI. Det er fri programvare, og du er velkommen til å redistribuere det under gitte forhold.</string>
<!--global settings-->
<string name="logging_davdroid_file_logging">DAVDroid fil-logging</string>
@@ -100,7 +95,6 @@
<string name="account_no_webcal_handler_found">Fant ingen programmer med støtte for Webcal</string>
<string name="account_install_icsdroid">Installer ICSdroid</string>
<!--AddAccountActivity-->
<string name="login_help_url">https://www.davdroid.com/tested-with/?pk_campaign=davdroid-app</string>
<string name="login_title">Legg til konto</string>
<string name="login_type_email">Innlogging med e-postadresse</string>
<string name="login_email_address">E-postadresse</string>
@@ -124,7 +118,6 @@
<string name="login_configuration_detection">Oppdagelse av oppsett</string>
<string name="login_querying_server">Vent, spør tjener…</string>
<string name="login_no_caldav_carddav">Fant ikke CalDAV eller CardDAV-tjeneste.</string>
<string name="login_view_logs">Vis logger</string>
<!--AccountSettingsActivity-->
<string name="settings_title">Innstillinger: %s</string>
<string name="settings_authentication">Identitetsbekreftelse</string>

View File

@@ -8,10 +8,7 @@
<string name="manage_accounts">Beheer accounts</string>
<string name="please_wait">Een moment geduld...</string>
<string name="send">Verzenden</string>
<string name="beta_feedback_url">https://forums.bitfire.at/category/9/beta-test-discussion?pk_campaign=davdroid-app</string>
<!--startup dialogs-->
<string name="startup_battery_optimization">Batterij optimalisatie</string>
<string name="startup_battery_optimization_message">Android kan mogelijk de DAVdroid synchronisatie stoppen na een paar dagen. Om dit te voorkomen zet u de batterij optimalisatie uit.</string>
<string name="startup_battery_optimization_disable">DAVdroid afsluiten</string>
<string name="startup_dont_show_again">Niet opnieuw weergeven</string>
<string name="startup_donate">Open-Source informatie</string>
@@ -24,7 +21,6 @@
<string name="startup_opentasks_reinstall_davdroid">Na installatie van OpenTasks dient u DAVdroid opnieuw te installeren en de accounts toe te voegen (Android bug).</string>
<string name="startup_opentasks_not_installed_install">OpenTasks installeren</string>
<!--AboutActivity-->
<string name="about_license_terms">Licentie voorwaarden</string>
<string name="about_license_info_no_warranty">Dit programma kom met ABSOLUUT GEEN GARANTIE. Het is gratis software, en je bent welkom dit te herdistribueren onder bepaalde voorwaarden.</string>
<!--global settings-->
<string name="logging_davdroid_file_logging">DAVDroid bestand loggen</string>
@@ -118,7 +114,6 @@
<string name="login_configuration_detection">Configuratie detectie</string>
<string name="login_querying_server">Even geduld, verzoek naar server...</string>
<string name="login_no_caldav_carddav">Kon geen CalDAV of CardDAV service vinden.</string>
<string name="login_view_logs">Bekijk logs</string>
<!--AccountSettingsActivity-->
<string name="settings_title">Instellingen: %s</string>
<string name="settings_authentication">Authenticatie</string>

View File

@@ -8,12 +8,8 @@
<string name="manage_accounts">Zadządzaj kontami</string>
<string name="please_wait">Proszę czekać</string>
<string name="send">Wyślij</string>
<string name="homepage_url">https://www.davdroid.com/?pk_campaign=davdroid-app</string>
<string name="beta_feedback_url">https://forums.bitfire.at/category/9/beta-test-discussion?pk_campaign=davdroid-app</string>
<string name="notification_channel_debugging">Debugowanie</string>
<!--startup dialogs-->
<string name="startup_battery_optimization">Optymalizacja baterii</string>
<string name="startup_battery_optimization_message">Android może wyłączyć/zmniejszyć synchronizacje DAVdroid po kilku dniach. Aby temu zapobiec należy wyłączyć optymalizację baterii.</string>
<string name="startup_battery_optimization_disable">Wyłącz dla DAVdroid</string>
<string name="startup_dont_show_again">Nie pokazuj ponownie</string>
<string name="startup_donate">Informacje Open-Source</string>
@@ -26,7 +22,6 @@
<string name="startup_opentasks_reinstall_davdroid">Po zainstalowaniu OpenTasks konieczne jest PRZEINSTALOWANIE DAVdroid i ponowne dodanie twoich kont (błąd Androida).</string>
<string name="startup_opentasks_not_installed_install">Zainstaluj OpenTasks</string>
<!--AboutActivity-->
<string name="about_license_terms">Warunki licencji</string>
<string name="about_license_info_no_warranty">Ten program jest ABSOLUTNIE BEZ GWARANCJI. To jest wolne oprogramowanie i mile widziane jest dalsze rozpowszechnianie go pod pewnymi warunkami.</string>
<!--global settings-->
<string name="logging_davdroid_file_logging">Plik logów DAVdroid</string>
@@ -102,7 +97,6 @@
<string name="account_no_webcal_handler_found">Nie znaleziono aplikacji obsługującej Webcal</string>
<string name="account_install_icsdroid">Zainstaluj ICSdroid</string>
<!--AddAccountActivity-->
<string name="login_help_url">https://www.davdroid.com/tested-with/?pk_campaign=davdroid-app</string>
<string name="login_title">Dodaj konto</string>
<string name="login_type_email">Logowanie za pomocą adresu e-mail</string>
<string name="login_email_address">Adres e-mail</string>
@@ -129,7 +123,6 @@
<string name="login_configuration_detection">Wykrywanie konfiguracji</string>
<string name="login_querying_server">Proszę czekać, odpytywanie serwera...</string>
<string name="login_no_caldav_carddav">Nie można znaleźć usługi CalDAV lub CardDAV.</string>
<string name="login_view_logs">Pokaż logi</string>
<!--AccountSettingsActivity-->
<string name="settings_title">Ustawienia: %s</string>
<string name="settings_authentication">Uwierzytelnianie</string>

View File

@@ -4,6 +4,7 @@
<string name="app_name">DAVdroid</string>
<string name="account_title_address_book">Livro de endereços DAVdroid</string>
<string name="address_books_authority_title">Livros de endereços</string>
<string name="copied_to_clipboard">Copiado para a área de transferência</string>
<string name="help">Ajuda</string>
<string name="manage_accounts">Gerenciar contas</string>
<string name="please_wait">Por favor, aguarde...</string>
@@ -15,12 +16,13 @@
<string name="notification_channel_sync_io_errors">Erros de rede e E/S</string>
<string name="notification_channel_sync_status">Mensagens de status</string>
<!--startup dialogs-->
<string name="startup_autostart_permission">%s permissão de início automático</string>
<string name="startup_autostart_permission_message">O firmware do aparelho pode impedir a sincronização automática. Você pode ter que definir a sincronização automática de forma manual.</string>
<string name="startup_battery_optimization">Otimização da bateria</string>
<string name="startup_battery_optimization_message">O Android pode desativar/reduzir a sincronização do DAVdroid depois de alguns dias. Para evitar isso, desligue a otimização da bateria.</string>
<string name="startup_autostart_permission">Sincronização automática</string>
<string name="startup_autostart_permission_message">O firmware%s frequentemente bloqueia a sincronização automática. Nesse caso, ative a sincronização automática nas configurações do seu Android.</string>
<string name="startup_battery_optimization">Sincronização agendada</string>
<string name="startup_battery_optimization_message">Seu aparelho irá restringir a sincronização do DAVdroid. Para forçar a sincronização do DAVdroid em intervalos regulares, desligue a \"otimização da bateria\".</string>
<string name="startup_battery_optimization_disable">Desligar para o DAVdroid</string>
<string name="startup_dont_show_again">Não mostrar novamente</string>
<string name="startup_not_now">Não agora</string>
<string name="startup_donate">Informação sobre Código Aberto</string>
<string name="startup_donate_message">Estamos felizes que você usa o DAVdroid, um software de código aberto (GPLv3). O desenvolvimento do DAVdroid é trabalhoso e consome muitas horas de trabalho. Por esse motivo, considere fazer uma doação.</string>
<string name="startup_donate_now">Mostrar a página de doações</string>
@@ -33,7 +35,10 @@
<string name="startup_opentasks_reinstall_davdroid">Depois da instalação do OpenTasks, torna-se necessário REINSTALAR o DAVdroid e adicionar suas contas novamente (erro do Android).</string>
<string name="startup_opentasks_not_installed_install">Instalar o OpenTasks</string>
<!--AboutActivity-->
<string name="about_license_terms">Termos da Licença</string>
<string name="about_libraries">Bibliotecas</string>
<string name="about_version">Versão %1s (%2d)</string>
<string name="about_build_date">Compilado em %s</string>
<string name="about_flavor_info">Esta versão está disponível apenas para distribuição na Google Play.</string>
<string name="about_license_info_no_warranty">Este programa é distribuído SEM NENHUMA GARANTIA. Ele é software livre e pode ser redistribuído sob algumas condições.</string>
<!--global settings-->
<string name="logging_davdroid_file_logging">Registro do arquivo do DAVdroid</string>
@@ -95,6 +100,7 @@
<string name="account_delete">Excluir conta</string>
<string name="account_delete_confirmation_title">Deseja excluir a conta?</string>
<string name="account_delete_confirmation_text">Todas as cópias locais dos livros de endereços, calendários e listas de tarefas serão excluídas.</string>
<string name="account_select_collections_hint">Selecione as coleções a sincronizar</string>
<string name="account_carddav">CardDAV</string>
<string name="account_caldav">CalDAV</string>
<string name="account_webcal">Webcal</string>
@@ -135,7 +141,7 @@
<string name="login_configuration_detection">Detecção de configuração</string>
<string name="login_querying_server">Aguarde, procurando servidor...</string>
<string name="login_no_caldav_carddav">Não foi possível encontrar o serviço CalDAV ou CardDAV.</string>
<string name="login_view_logs">Exibir registros</string>
<string name="login_view_logs">Mostrar detalhes</string>
<!--AccountSettingsActivity-->
<string name="settings_title">Configurações: %s</string>
<string name="settings_authentication">Autenticação</string>
@@ -216,6 +222,9 @@
<string name="delete_collection_confirm_warning">Esta coleção (%s) e todos os seus dados serão removidos do servidor.</string>
<string name="delete_collection_deleting_collection">Excluindo coleção</string>
<string name="collection_force_read_only">Forçar somente leitura</string>
<string name="collection_properties">Propriedades</string>
<string name="collection_properties_url">Endereço (URL):</string>
<string name="collection_properties_copy_url">Copiar URL</string>
<!--ExceptionInfoFragment-->
<string name="exception">Ocorreu um erro.</string>
<string name="exception_httpexception">Ocorreu um erro de HTTP.</string>

View File

@@ -4,12 +4,11 @@
<string name="app_name">DAVdroid</string>
<string name="account_title_address_book">Адресная книга DAVdroid</string>
<string name="address_books_authority_title">Адресные книги</string>
<string name="copied_to_clipboard">Скопировано в буфер обмена</string>
<string name="help">Помощь</string>
<string name="manage_accounts">Управление аккаунтами</string>
<string name="please_wait">Пожалуйста, подождите …</string>
<string name="send">Отправить</string>
<string name="homepage_url">https://www.davdroid.com/?pk_campaign=davdroid-app</string>
<string name="beta_feedback_url">https://forums.bitfire.at/category/9/beta-test-discussion?pk_campaign=davdroid-app</string>
<string name="notification_channel_debugging">Отладка</string>
<string name="notification_channel_general">Другие важные сообщения</string>
<string name="notification_channel_sync">Синхронизация</string>
@@ -17,12 +16,13 @@
<string name="notification_channel_sync_io_errors">Ошибки сети и ввода/вывода</string>
<string name="notification_channel_sync_status">Сообщения о состоянии</string>
<!--startup dialogs-->
<string name="startup_autostart_permission">Разрешение автозапуска %s</string>
<string name="startup_autostart_permission_message">Прошивка устройства может препятствовать автоматической синхронизации. Возможно, придется разрешить автоматическую синхронизацию вручную.</string>
<string name="startup_battery_optimization">Оптимизация батареи</string>
<string name="startup_battery_optimization_message">Android может ограничить работу DAVdroid в фоновом режиме. Чтобы этого не произошло, необходимо отключить оптимизацию энергопотребления .</string>
<string name="startup_autostart_permission">Автоматическая синхронизация</string>
<string name="startup_autostart_permission_message">%s ПО устройства часто блокирует автоматическую синхронизацию. В этом случае разрешите автоматическую синхронизацию в настройках Android.</string>
<string name="startup_battery_optimization">Синхронизация по расписанию</string>
<string name="startup_battery_optimization_message">Ваше устройство будет блокировать синхронизацию DAVdroid. Чтобы обеспечить регулярные интервалы синхронизации DAVdroid, отключите оптимизацию энергопотребления.</string>
<string name="startup_battery_optimization_disable">Отключить для DAVdroid</string>
<string name="startup_dont_show_again">Не показывать снова</string>
<string name="startup_not_now">Не сейчас</string>
<string name="startup_donate">Open-Source информация</string>
<string name="startup_donate_message">Мы рады, что вы используете DAVdroid, который является программным обеспечением с открытым исходным кодом (GPLv3). Поскольку разработка DAVdroid - тяжелая работа и заняла у нас нас очень много времени, пожалуйста, рассмотрите возможность поддержать проект.</string>
<string name="startup_donate_now">Показать страницу пожертвования</string>
@@ -35,7 +35,10 @@
<string name="startup_opentasks_reinstall_davdroid">После установки OpenTasks необходимо ПЕРЕУСТАНОВИТЬ DAVdroid и повторно добавить ваши аккаунты (проблема Android).</string>
<string name="startup_opentasks_not_installed_install">Установить OpenTasks</string>
<!--AboutActivity-->
<string name="about_license_terms">Условия лицензии</string>
<string name="about_libraries">Библиотеки</string>
<string name="about_version">Версия %1s (%2d)</string>
<string name="about_build_date">Скомпилировано %s</string>
<string name="about_flavor_info">Эта версия распространяется только через Google Play.</string>
<string name="about_license_info_no_warranty">Эта программа поставляется БЕЗ КАКИХ-ЛИБО ГАРАНТИЙ. Это свободное программное обеспечение и вы можете распространять его при соблюдении определенных условий.</string>
<!--global settings-->
<string name="logging_davdroid_file_logging">Файл журнала DAVdroid</string>
@@ -97,6 +100,7 @@
<string name="account_delete">Удалить аккаунт</string>
<string name="account_delete_confirmation_title">Вы действительно хотите удалить аккаунт?</string>
<string name="account_delete_confirmation_text">Все локальные копии адресных книг, календарей и задач будут удалены.</string>
<string name="account_select_collections_hint">Выберите коллекции для синхронизации</string>
<string name="account_carddav">CardDAV</string>
<string name="account_caldav">CalDAV</string>
<string name="account_webcal">WebСal</string>
@@ -111,7 +115,6 @@
<string name="account_no_webcal_handler_found">Не найдено приложение, поддерживающее WebCal</string>
<string name="account_install_icsdroid">Установить ICSdroid</string>
<!--AddAccountActivity-->
<string name="login_help_url">https://www.davdroid.com/tested-with/?pk_campaign=davdroid-app</string>
<string name="login_title">Добавить аккаунт</string>
<string name="login_type_email">Вход с адресом электронной почты</string>
<string name="login_email_address">Адрес электронной почты</string>
@@ -138,7 +141,7 @@
<string name="login_configuration_detection">Обнаружение конфигурации</string>
<string name="login_querying_server">Ожидайте, выполняется запрос к серверу…</string>
<string name="login_no_caldav_carddav">Не удалось найти службу CalDAV или CardDAV.</string>
<string name="login_view_logs">Просмотр логов</string>
<string name="login_view_logs">Показать детали</string>
<!--AccountSettingsActivity-->
<string name="settings_title">Настройки: %s</string>
<string name="settings_authentication">Аутентификация</string>
@@ -221,6 +224,9 @@
<string name="delete_collection_confirm_warning">Эта коллекция (%s) и все ее данные будут удалены с сервера.</string>
<string name="delete_collection_deleting_collection">Удаление коллекции</string>
<string name="collection_force_read_only">Только для чтения</string>
<string name="collection_properties">Свойства</string>
<string name="collection_properties_url">Адрес (URL):</string>
<string name="collection_properties_copy_url">Копировать URL</string>
<!--ExceptionInfoFragment-->
<string name="exception">Произошла ошибка.</string>
<string name="exception_httpexception">Произошла ошибка HTTP</string>

View File

@@ -9,8 +9,6 @@
<string name="please_wait">Сачекајте…</string>
<string name="send">Пошаљи</string>
<!--startup dialogs-->
<string name="startup_battery_optimization">Оптимизација батерије</string>
<string name="startup_battery_optimization_message">Андроид може да искључи/умањи синхронизацију ДАВдроида након неколико дана. Да бисте спречили ово, искључите оптимизацију батерије.</string>
<string name="startup_battery_optimization_disable">Искључи за ДАВдроид</string>
<string name="startup_dont_show_again">Не приказуј поново</string>
<string name="startup_donate">Подаци о отвореном кôду</string>
@@ -23,7 +21,6 @@
<string name="startup_opentasks_reinstall_davdroid">Након инсталирања Отворених задатака, морате поново да инсталирате ДАВдроид и поново додате ваше налоге (због грешке у Андроиду).</string>
<string name="startup_opentasks_not_installed_install">Инсталирај Отворене задатке</string>
<!--AboutActivity-->
<string name="about_license_terms">Услови лиценце</string>
<string name="about_license_info_no_warranty">Овај програм НЕМА НИКАКВЕ ГАРАНЦИЈЕ. Бесплатан је софтвер којег можете слободно да делите под одређеним условима.</string>
<!--global settings-->
<string name="logging_davdroid_file_logging">ДАВдроид евиденција</string>
@@ -110,7 +107,6 @@
<string name="login_configuration_detection">Откривање конфигурације</string>
<string name="login_querying_server">Сачекајте, шаљем упит серверу…</string>
<string name="login_no_caldav_carddav">Не могох да нађем КалДАВ или КардДАВ услугу.</string>
<string name="login_view_logs">Прикажи записе</string>
<!--AccountSettingsActivity-->
<string name="settings_title">Поставке: %s</string>
<string name="settings_authentication">Аутентификација</string>

View File

@@ -18,7 +18,6 @@
<string name="startup_opentasks_reinstall_davdroid">OpenTasks\'i kurduktan sonra, DAVdroid\'i YENİDEN KURMAN ve hesaplarını yeniden eklemen gerek. (Android hatası).</string>
<string name="startup_opentasks_not_installed_install">OpenTasks kur</string>
<!--AboutActivity-->
<string name="about_license_terms">Lisans şartları</string>
<string name="about_license_info_no_warranty">Bu uygulama HİÇ BİR GARANTİ ile gelmemektedir. Bedava bir yazılımdır ve belli koşullar altında dağıtabilirsiniz.</string>
<!--global settings-->
<string name="logging_davdroid_file_logging">DAVdroid dosya jurnallemesi</string>
@@ -87,7 +86,6 @@
<string name="login_configuration_detection">Konfigürasyon keşfi</string>
<string name="login_querying_server">Lütfen bekle, sunucu sorgulanıyor...</string>
<string name="login_no_caldav_carddav">CalDAV veya CardDAV servisi bulunamadı.</string>
<string name="login_view_logs">Jurnallere bak</string>
<!--AccountSettingsActivity-->
<string name="settings_title">Ayarlar: %s</string>
<string name="settings_authentication">Doğrulama</string>

View File

@@ -8,8 +8,6 @@
<string name="manage_accounts">Керування обліковими записами</string>
<string name="please_wait">Будь ласка, зачекайте...</string>
<string name="send">Відправити</string>
<string name="homepage_url">https://www.davdroid.com/?pk_campaign=davdroid-app</string>
<string name="beta_feedback_url">https://forums.bitfire.at/category/9/beta-test-discussion?pk_campaign=davdroid-app</string>
<string name="notification_channel_debugging">Зневадження</string>
<string name="notification_channel_general">Інші важливі повідомлення</string>
<string name="notification_channel_sync">Синхронізація</string>
@@ -17,10 +15,6 @@
<string name="notification_channel_sync_io_errors">Помилка мережі та вводу/виводу</string>
<string name="notification_channel_sync_status">Повідомлення про стан</string>
<!--startup dialogs-->
<string name="startup_autostart_permission">Дозвіл автозапуску %s</string>
<string name="startup_autostart_permission_message">Програмне забезпечення пристрою може запобігати автоматичні синхронізації. Можливо доведеться дозволити автоматичну синхронізацію вручну.</string>
<string name="startup_battery_optimization">Оптимізація енергоспоживання</string>
<string name="startup_battery_optimization_message">Android може вимкнути, чи призупинити синхронізацію DAVdroid через деякий час. Аби запобігти цьому, вимкніть оптимізацію енергоспоживання для додатку.</string>
<string name="startup_battery_optimization_disable">Вимкнути для DAVdroid</string>
<string name="startup_dont_show_again">Не показувати знову</string>
<string name="startup_donate">Інформація Open-Source</string>
@@ -35,7 +29,6 @@
<string name="startup_opentasks_reinstall_davdroid">Після встановлення OpenTasks, необхідно перевстановити DAVdroid та додати облікові записи знову (Вада системи Android).</string>
<string name="startup_opentasks_not_installed_install">Встановити OpenTasks</string>
<!--AboutActivity-->
<string name="about_license_terms">Умови ліцензії</string>
<string name="about_license_info_no_warranty">Цей програмний засіб постачається АБСОЛЮТНО БЕЗ БУДЬ-ЯКИХ ГАРАНТІЙ. Це вільне програмне забезпечення, і ви можете поширювати її, за деякими умовами.</string>
<!--global settings-->
<string name="logging_davdroid_file_logging">Файл звітування DAVdroid</string>
@@ -111,7 +104,6 @@
<string name="account_no_webcal_handler_found">Не знайдено додатку з підтримкою Webcal</string>
<string name="account_install_icsdroid">Встановити ICSdroid</string>
<!--AddAccountActivity-->
<string name="login_help_url">https://www.davdroid.com/tested-with/?pk_campaign=davdroid-app</string>
<string name="login_title">Додати запис</string>
<string name="login_type_email">Увійти за допомогою електронної пошти</string>
<string name="login_email_address">Адреса пошти</string>
@@ -138,7 +130,6 @@
<string name="login_configuration_detection">Виявлення конфігурації</string>
<string name="login_querying_server">Будь ласка, зачекайте, запит до серверу...</string>
<string name="login_no_caldav_carddav">Не вдалося знайти CalDAV чи CardDAV сервіс.</string>
<string name="login_view_logs">Переглянути звіти</string>
<!--AccountSettingsActivity-->
<string name="settings_title">Налаштування: %s</string>
<string name="settings_authentication">Автентифікація</string>

View File

@@ -8,12 +8,8 @@
<string name="manage_accounts">管理账户</string>
<string name="please_wait">请稍等...</string>
<string name="send">发送</string>
<string name="homepage_url">https://www.davdroid.com/?pk_campaign=davdroid-app</string>
<string name="beta_feedback_url">https://forums.bitfire.at/category/9/beta-test-discussion?pk_campaign=davdroid-app</string>
<string name="notification_channel_debugging">调试</string>
<!--startup dialogs-->
<string name="startup_battery_optimization">电池优化</string>
<string name="startup_battery_optimization_message">系统可能会在几天后减少或停用 DAVdroid 同步。为了避免这一情况,请禁用对 DAVdroid 的电池优化。</string>
<string name="startup_battery_optimization_disable">禁用电池优化</string>
<string name="startup_dont_show_again">不再显示</string>
<string name="startup_donate">开源信息</string>
@@ -26,7 +22,6 @@
<string name="startup_opentasks_reinstall_davdroid">安装 OpenTasks 后,由于 Android 的限制,请重新安装 DAVdroid 并重新创建账户。</string>
<string name="startup_opentasks_not_installed_install">安装 OpenTasks</string>
<!--AboutActivity-->
<string name="about_license_terms">许可协议</string>
<string name="about_license_info_no_warranty">本程序不附带任何担保。这是一款自由软件,你可以有条件地传播它。</string>
<!--global settings-->
<string name="logging_davdroid_file_logging">DAVdroid 文件日志</string>
@@ -102,7 +97,6 @@
<string name="account_no_webcal_handler_found">找不到支持 Webcal 的应用</string>
<string name="account_install_icsdroid">安装 ICSdroid</string>
<!--AddAccountActivity-->
<string name="login_help_url">https://www.davdroid.com/tested-with/?pk_campaign=davdroid-app</string>
<string name="login_title">增加账户</string>
<string name="login_type_email">使用邮箱地址登录</string>
<string name="login_email_address">Email 地址</string>
@@ -129,7 +123,6 @@
<string name="login_configuration_detection">正在配置</string>
<string name="login_querying_server">正在与服务器通信,请稍等...</string>
<string name="login_no_caldav_carddav">找不到 CalDAV 或 CardDAV 服务。</string>
<string name="login_view_logs">查看日志</string>
<!--AccountSettingsActivity-->
<string name="settings_title">设置:%s</string>
<string name="settings_authentication">认证</string>

View File

@@ -9,8 +9,6 @@
<string name="please_wait">請稍待 ...</string>
<string name="send">送出</string>
<!--startup dialogs-->
<string name="startup_battery_optimization">電池最佳化</string>
<string name="startup_battery_optimization_message">Android 在數日之後可能會關閉或減少 DAVdroid 的同步。為了避免這發生,請關閉電池最佳化。</string>
<string name="startup_battery_optimization_disable">關閉 DAVdroid 的電池最佳化</string>
<string name="startup_dont_show_again">不要再顯示此訊息</string>
<string name="startup_donate">開源資訊</string>
@@ -23,7 +21,6 @@
<string name="startup_opentasks_reinstall_davdroid">安裝 OpenTasks 後,您必須「重新安裝」 DAVdroid 並且重新加入要同步的帳號 (這是 Android 的設計問題)</string>
<string name="startup_opentasks_not_installed_install">安裝 OpenTasks</string>
<!--AboutActivity-->
<string name="about_license_terms">授權條款</string>
<string name="about_license_info_no_warranty">我們「完全不保證」本程式無瑕疵。這是個自由軟體,歡迎您在符合公用授權條款的情況下任意散布它。</string>
<!--global settings-->
<string name="logging_davdroid_file_logging">DAVdroid 正在記錄除錯訊息</string>
@@ -109,7 +106,6 @@
<string name="login_configuration_detection">設定錯誤</string>
<string name="login_querying_server">請稍待,正在詢問伺服器...</string>
<string name="login_no_caldav_carddav">找不到 CalDAV 或 CardDAV 服務。</string>
<string name="login_view_logs">檢視除錯訊息</string>
<!--AccountSettingsActivity-->
<string name="settings_title">設定: %s</string>
<string name="settings_authentication">登入驗證</string>

View File

@@ -71,7 +71,7 @@
<activity
android:name=".ui.AboutActivity"
android:label="@string/app_name"
android:label="@string/navigation_drawer_about"
android:theme="@style/AppTheme.NoActionBar"
android:parentActivityName=".ui.AccountsActivity"/>

View File

@@ -1,153 +0,0 @@
<h3>Apache License, Version 2.0, January 2004</h3>
<p><a href="http://www.apache.org/licenses/">http://www.apache.org/licenses/</a> </p>
<p>TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION</p>
<p><strong><a name="definitions">1. Definitions</a></strong>.</p>
<p>"License" shall mean the terms and conditions for use, reproduction, and
distribution as defined by Sections 1 through 9 of this document.</p>
<p>"Licensor" shall mean the copyright owner or entity authorized by the
copyright owner that is granting the License.</p>
<p>"Legal Entity" shall mean the union of the acting entity and all other
entities that control, are controlled by, or are under common control with
that entity. For the purposes of this definition, "control" means (i) the
power, direct or indirect, to cause the direction or management of such
entity, whether by contract or otherwise, or (ii) ownership of fifty
percent (50%) or more of the outstanding shares, or (iii) beneficial
ownership of such entity.</p>
<p>"You" (or "Your") shall mean an individual or Legal Entity exercising
permissions granted by this License.</p>
<p>"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation source,
and configuration files.</p>
<p>"Object" form shall mean any form resulting from mechanical transformation
or translation of a Source form, including but not limited to compiled
object code, generated documentation, and conversions to other media types.</p>
<p>"Work" shall mean the work of authorship, whether in Source or Object form,
made available under the License, as indicated by a copyright notice that
is included in or attached to the work (an example is provided in the
Appendix below).</p>
<p>"Derivative Works" shall mean any work, whether in Source or Object form,
that is based on (or derived from) the Work and for which the editorial
revisions, annotations, elaborations, or other modifications represent, as
a whole, an original work of authorship. For the purposes of this License,
Derivative Works shall not include works that remain separable from, or
merely link (or bind by name) to the interfaces of, the Work and Derivative
Works thereof.</p>
<p>"Contribution" shall mean any work of authorship, including the original
version of the Work and any modifications or additions to that Work or
Derivative Works thereof, that is intentionally submitted to Licensor for
inclusion in the Work by the copyright owner or by an individual or Legal
Entity authorized to submit on behalf of the copyright owner. For the
purposes of this definition, "submitted" means any form of electronic,
verbal, or written communication sent to the Licensor or its
representatives, including but not limited to communication on electronic
mailing lists, source code control systems, and issue tracking systems that
are managed by, or on behalf of, the Licensor for the purpose of discussing
and improving the Work, but excluding communication that is conspicuously
marked or otherwise designated in writing by the copyright owner as "Not a
Contribution."</p>
<p>"Contributor" shall mean Licensor and any individual or Legal Entity on
behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.</p>
<p><strong><a name="copyright">2. Grant of Copyright License</a></strong>. Subject to the
terms and conditions of this License, each Contributor hereby grants to You
a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of, publicly
display, publicly perform, sublicense, and distribute the Work and such
Derivative Works in Source or Object form.</p>
<p><strong><a name="patent">3. Grant of Patent License</a></strong>. Subject to the terms
and conditions of this License, each Contributor hereby grants to You a
perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made, use,
offer to sell, sell, import, and otherwise transfer the Work, where such
license applies only to those patent claims licensable by such Contributor
that are necessarily infringed by their Contribution(s) alone or by
combination of their Contribution(s) with the Work to which such
Contribution(s) was submitted. If You institute patent litigation against
any entity (including a cross-claim or counterclaim in a lawsuit) alleging
that the Work or a Contribution incorporated within the Work constitutes
direct or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate as of the
date such litigation is filed.</p>
<p><strong><a name="redistribution">4. Redistribution</a></strong>. You may reproduce and
distribute copies of the Work or Derivative Works thereof in any medium,
with or without modifications, and in Source or Object form, provided that
You meet the following conditions:</p>
<p>a. You must give any other recipients of the Work or Derivative Works a
copy of this License; and</p>
<p>b. You must cause any modified files to carry prominent notices stating
that You changed the files; and</p>
<p>c. You must retain, in the Source form of any Derivative Works that You
distribute, all copyright, patent, trademark, and attribution notices from
the Source form of the Work, excluding those notices that do not pertain to
any part of the Derivative Works; and</p>
<p>d. If the Work includes a "NOTICE" text file as part of its distribution,
then any Derivative Works that You distribute must include a readable copy
of the attribution notices contained within such NOTICE file, excluding
those notices that do not pertain to any part of the Derivative Works, in
at least one of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or documentation,
if provided along with the Derivative Works; or, within a display generated
by the Derivative Works, if and wherever such third-party notices normally
appear. The contents of the NOTICE file are for informational purposes only
and do not modify the License. You may add Your own attribution notices
within Derivative Works that You distribute, alongside or as an addendum to
the NOTICE text from the Work, provided that such additional attribution
notices cannot be construed as modifying the License.
<br/>
<br/>
You may add Your own copyright statement to Your modifications and may
provide additional or different license terms and conditions for use,
reproduction, or distribution of Your modifications, or for any such
Derivative Works as a whole, provided Your use, reproduction, and
distribution of the Work otherwise complies with the conditions stated in
this License.
</p>
<p><strong><a name="contributions">5. Submission of Contributions</a></strong>. Unless You
explicitly state otherwise, any Contribution intentionally submitted for
inclusion in the Work by You to the Licensor shall be under the terms and
conditions of this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify the
terms of any separate license agreement you may have executed with Licensor
regarding such Contributions.</p>
<p><strong><a name="trademarks">6. Trademarks</a></strong>. This License does not grant
permission to use the trade names, trademarks, service marks, or product
names of the Licensor, except as required for reasonable and customary use
in describing the origin of the Work and reproducing the content of the
NOTICE file.</p>
<p><strong><a name="no-warranty">7. Disclaimer of Warranty</a></strong>. Unless required by
applicable law or agreed to in writing, Licensor provides the Work (and
each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including,
without limitation, any warranties or conditions of TITLE,
NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You
are solely responsible for determining the appropriateness of using or
redistributing the Work and assume any risks associated with Your exercise
of permissions under this License.</p>
<p><strong><a name="no-liability">8. Limitation of Liability</a></strong>. In no event and
under no legal theory, whether in tort (including negligence), contract, or
otherwise, unless required by applicable law (such as deliberate and
grossly negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a result
of this License or out of the use or inability to use the Work (including
but not limited to damages for loss of goodwill, work stoppage, computer
failure or malfunction, or any and all other commercial damages or losses),
even if such Contributor has been advised of the possibility of such
damages.</p>
<p><strong><a name="additional">9. Accepting Warranty or Additional Liability</a></strong>.
While redistributing the Work or Derivative Works thereof, You may choose
to offer, and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this License.
However, in accepting such obligations, You may act only on Your own behalf
and on Your sole responsibility, not on behalf of any other Contributor,
and only if You agree to indemnify, defend, and hold each Contributor
harmless for any liability incurred by, or claims asserted against, such
Contributor by reason of your accepting any such warranty or additional
liability.</p>
<p>END OF TERMS AND CONDITIONS</p>

View File

@@ -1,28 +0,0 @@
<h3>BSD License (3-clause)</h3>
<p>Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:</p>
<p>o Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.</p>
<p>o Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.</p>
<p>o Neither the name of Ben Fortuna nor the names of any other contributors
may be used to endorse or promote products derived from this software
without specific prior written permission.</p>
<p>THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.</p>

View File

@@ -1,23 +0,0 @@
<h3>BSD License</h3>
<p>Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:</p>
<p>1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.</p>
<p>2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.</p>
<p>THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.</p>

View File

@@ -14,7 +14,9 @@ import android.content.Intent
import android.content.IntentFilter
import android.graphics.Bitmap
import android.graphics.drawable.BitmapDrawable
import android.net.Uri
import android.os.Build
import android.os.StrictMode
import android.support.v7.app.AppCompatDelegate
import at.bitfire.davdroid.log.Logger
import at.bitfire.davdroid.ui.NotificationUtils
@@ -60,6 +62,13 @@ class App: Application() {
null
}
fun homepageUrl(context: Context) =
Uri.parse(context.getString(R.string.homepage_url)).buildUpon()
.appendQueryParameter("pk_campaign", BuildConfig.APPLICATION_ID)
.appendQueryParameter("pk_kwd", context::class.java.simpleName)
.appendQueryParameter("app-version", BuildConfig.VERSION_NAME)
.build()!!
}
@@ -67,6 +76,23 @@ class App: Application() {
super.onCreate()
Logger.initialize(this)
if (BuildConfig.DEBUG) {
StrictMode.setVmPolicy(StrictMode.VmPolicy.Builder()
.detectActivityLeaks()
.detectFileUriExposure()
.detectLeakedClosableObjects()
.detectLeakedRegistrationObjects()
.detectLeakedSqlLiteObjects()
.penaltyLog()
.build())
// main thread
StrictMode.setThreadPolicy(StrictMode.ThreadPolicy.Builder()
.detectAll()
.penaltyLog()
.build())
}
if (Build.VERSION.SDK_INT <= 21)
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true)

View File

@@ -13,4 +13,18 @@ object Constants {
const val DEFAULT_SYNC_INTERVAL = 4 * 3600L // 4 hours
/**
* Context label for [org.apache.commons.lang3.exception.ContextedException].
* Context value is the [at.bitfire.davdroid.resource.LocalResource]
* which is related to the exception cause.
*/
const val EXCEPTION_CONTEXT_LOCAL_RESOURCE = "localResource"
/**
* Context label for [org.apache.commons.lang3.exception.ContextedException].
* Context value is the [okhttp3.HttpUrl] of the remote resource
* which is related to the exception cause.
*/
const val EXCEPTION_CONTEXT_REMOTE_RESOURCE = "remoteResource"
}

View File

@@ -21,7 +21,7 @@ import android.os.Bundle
import android.support.v4.app.NotificationCompat
import android.support.v4.app.NotificationManagerCompat
import at.bitfire.dav4android.DavResource
import at.bitfire.dav4android.DavResponse
import at.bitfire.dav4android.Response
import at.bitfire.dav4android.UrlUtils
import at.bitfire.dav4android.exception.DavException
import at.bitfire.dav4android.exception.HttpException
@@ -34,8 +34,7 @@ import at.bitfire.davdroid.settings.Settings
import at.bitfire.davdroid.ui.DebugInfoActivity
import at.bitfire.davdroid.ui.NotificationUtils
import okhttp3.HttpUrl
import org.apache.commons.collections4.iterators.IteratorChain
import org.apache.commons.collections4.iterators.SingletonIterator
import okhttp3.OkHttpClient
import java.io.IOException
import java.lang.ref.WeakReference
import java.util.*
@@ -182,64 +181,68 @@ class DavService: Service() {
/**
* Checks if the given URL defines home sets and adds them to the home set list.
* @param dav DavResource to check
*/
@Throws(IOException::class, HttpException::class, DavException::class)
fun queryHomeSets(dav: DavResource, recurse: Boolean = true) {
var response: DavResponse? = null
try {
when (serviceType) {
Services.SERVICE_CARDDAV -> {
response = dav.propfind(0, AddressbookHomeSet.NAME, GroupMembership.NAME)
for ((resource, addressbookHomeSet) in response.searchProperties(AddressbookHomeSet::class.java))
for (href in addressbookHomeSet.hrefs)
resource.url.resolve(href)?.let { homeSets += UrlUtils.withTrailingSlash(it) }
}
Services.SERVICE_CALDAV -> {
response = dav.propfind(0, CalendarHomeSet.NAME, CalendarProxyReadFor.NAME, CalendarProxyWriteFor.NAME, GroupMembership.NAME)
for ((resource, calendarHomeSet) in response.searchProperties(CalendarHomeSet::class.java))
for (href in calendarHomeSet.hrefs)
resource.url.resolve(href)?.let { homeSets.add(UrlUtils.withTrailingSlash(it)) }
}
}
response = requireNotNull(response)
fun queryHomeSets(client: OkHttpClient, url: HttpUrl, recurse: Boolean = true) {
var related = setOf<HttpUrl>()
if (recurse) {
// refresh home sets: calendar-proxy-read/write-for
for ((resource, proxyRead) in response.searchProperties(CalendarProxyReadFor::class.java))
for (href in proxyRead.hrefs) {
Logger.log.fine("Principal is a read-only proxy for $href, checking for home sets")
resource.url.resolve(href)?.let {
queryHomeSets(DavResource(dav.httpClient, it), false)
}
}
for ((resource, proxyWrite) in response.searchProperties(CalendarProxyWriteFor::class.java))
for (href in proxyWrite.hrefs) {
Logger.log.fine("Principal is a read/write proxy for $href, checking for home sets")
resource.url.resolve(href)?.let {
queryHomeSets(DavResource(dav.httpClient, it), false)
}
}
// refresh home sets: direct group memberships
response[GroupMembership::class.java]?.let { groupMembership ->
for (href in groupMembership.hrefs) {
Logger.log.fine("Principal is member of group $href, checking for home sets")
response.url.resolve(href)?.let { url ->
try {
queryHomeSets(DavResource(dav.httpClient, url), false)
} catch (e: HttpException) {
Logger.log.log(Level.WARNING, "Couldn't query member group", e)
} catch (e: DavException) {
Logger.log.log(Level.WARNING, "Couldn't query member group", e)
}
}
fun findRelated(root: HttpUrl, dav: Response) {
// refresh home sets: calendar-proxy-read/write-for
dav[CalendarProxyReadFor::class.java]?.let {
for (href in it.hrefs) {
Logger.log.fine("Principal is a read-only proxy for $href, checking for home sets")
root.resolve(href)?.let {
related += it
}
}
}
dav[CalendarProxyWriteFor::class.java]?.let {
for (href in it.hrefs) {
Logger.log.fine("Principal is a read/write proxy for $href, checking for home sets")
root.resolve(href)?.let {
related += it
}
}
}
// refresh home sets: direct group memberships
dav[GroupMembership::class.java]?.let {
for (href in it.hrefs) {
Logger.log.fine("Principal is member of group $href, checking for home sets")
root.resolve(href)?.let {
related += it
}
}
}
} finally {
response?.close()
}
val dav = DavResource(client, url)
when (serviceType) {
Services.SERVICE_CARDDAV ->
dav.propfind(0, AddressbookHomeSet.NAME, GroupMembership.NAME) { response, _ ->
response[AddressbookHomeSet::class.java]?.let {
for (href in it.hrefs)
dav.location.resolve(href)?.let { homeSets += UrlUtils.withTrailingSlash(it) }
}
if (recurse)
findRelated(dav.location, response)
}
Services.SERVICE_CALDAV -> {
dav.propfind(0, CalendarHomeSet.NAME, CalendarProxyReadFor.NAME, CalendarProxyWriteFor.NAME, GroupMembership.NAME) { response, _ ->
response[CalendarHomeSet::class.java]?.let {
for (href in it.hrefs)
dav.location.resolve(href)?.let { homeSets.add(UrlUtils.withTrailingSlash(it)) }
}
if (recurse)
findRelated(dav.location, response)
}
}
}
for (resource in related)
queryHomeSets(client, resource, false)
}
fun saveHomeSets() {
@@ -276,14 +279,14 @@ class DavService: Service() {
// refresh home set list (from principal)
readPrincipal()?.let { principalUrl ->
Logger.log.fine("Querying principal $principalUrl for home sets")
queryHomeSets(DavResource(httpClient, principalUrl))
queryHomeSets(httpClient, principalUrl)
}
// remember selected collections
val selectedCollections = HashSet<HttpUrl>()
collections.values
.filter { it.selected }
.forEach { (url,_) -> selectedCollections.add(url) }
.forEach { (url, _) -> selectedCollections.add(url) }
// now refresh collections (taken from home sets)
val itHomeSets = homeSets.iterator()
@@ -291,20 +294,18 @@ class DavService: Service() {
val homeSetUrl = itHomeSets.next()
Logger.log.fine("Listing home set $homeSetUrl")
val homeSet = DavResource(httpClient, homeSetUrl)
try {
homeSet.propfind(1, *CollectionInfo.DAV_PROPERTIES).use { response ->
val itCollections = IteratorChain<DavResponse>(response.members.iterator(), response.related.iterator(), SingletonIterator(response))
while (itCollections.hasNext()) {
val member = itCollections.next()
val info = CollectionInfo(member)
info.confirmed = true
Logger.log.log(Level.FINE, "Found collection", info)
DavResource(httpClient, homeSetUrl).propfind(1, *CollectionInfo.DAV_PROPERTIES) { response, _ ->
if (!response.isSuccess())
return@propfind
if ((serviceType == Services.SERVICE_CARDDAV && info.type == CollectionInfo.Type.ADDRESS_BOOK) ||
(serviceType == Services.SERVICE_CALDAV && arrayOf(CollectionInfo.Type.CALENDAR, CollectionInfo.Type.WEBCAL).contains(info.type)))
collections[member.url] = info
}
val info = CollectionInfo(response)
info.confirmed = true
Logger.log.log(Level.FINE, "Found collection", info)
if ((serviceType == Services.SERVICE_CARDDAV && info.type == CollectionInfo.Type.ADDRESS_BOOK) ||
(serviceType == Services.SERVICE_CALDAV && arrayOf(CollectionInfo.Type.CALENDAR, CollectionInfo.Type.WEBCAL).contains(info.type)))
collections[response.href] = info
}
} catch(e: HttpException) {
if (e.code in arrayOf(403, 404, 410))
@@ -319,15 +320,17 @@ class DavService: Service() {
val (url, info) = itCollections.next()
if (!info.confirmed)
try {
val collection = DavResource(httpClient, url)
collection.propfind(0, *CollectionInfo.DAV_PROPERTIES).use { response ->
DavResource(httpClient, url).propfind(0, *CollectionInfo.DAV_PROPERTIES) { response, _ ->
if (!response.isSuccess())
return@propfind
val info = CollectionInfo(response)
info.confirmed = true
// remove unusable collections
if ((serviceType == Services.SERVICE_CARDDAV && info.type != CollectionInfo.Type.ADDRESS_BOOK) ||
(serviceType == Services.SERVICE_CALDAV && !arrayOf(CollectionInfo.Type.CALENDAR, CollectionInfo.Type.WEBCAL).contains(info.type)) ||
(info.type == CollectionInfo.Type.WEBCAL && info.source == null))
(serviceType == Services.SERVICE_CALDAV && !arrayOf(CollectionInfo.Type.CALENDAR, CollectionInfo.Type.WEBCAL).contains(info.type)) ||
(info.type == CollectionInfo.Type.WEBCAL && info.source == null))
itCollections.remove()
}
} catch(e: HttpException) {
@@ -382,4 +385,4 @@ class DavService: Service() {
}
}
}

View File

@@ -48,7 +48,7 @@ class HttpClient private constructor(
/** [OkHttpClient] singleton to build all clients from */
val sharedClient = OkHttpClient.Builder()
// set timeouts
.connectTimeout(30, TimeUnit.SECONDS)
.connectTimeout(15, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.readTimeout(120, TimeUnit.SECONDS)

View File

@@ -11,7 +11,7 @@ package at.bitfire.davdroid.model
import android.content.ContentValues
import android.os.Parcel
import android.os.Parcelable
import at.bitfire.dav4android.DavResponse
import at.bitfire.dav4android.Response
import at.bitfire.dav4android.UrlUtils
import at.bitfire.dav4android.property.*
import at.bitfire.davdroid.model.ServiceDB.Collections
@@ -58,21 +58,7 @@ data class CollectionInfo(
WEBCAL // iCalendar subscription
}
companion object {
val DAV_PROPERTIES = arrayOf(
ResourceType.NAME,
CurrentUserPrivilegeSet.NAME,
DisplayName.NAME,
AddressbookDescription.NAME, SupportedAddressData.NAME,
CalendarDescription.NAME, CalendarColor.NAME, SupportedCalendarComponentSet.NAME,
Source.NAME
)
}
constructor(dav: DavResponse): this(UrlUtils.withTrailingSlash(dav.url)) {
constructor(dav: Response): this(UrlUtils.withTrailingSlash(dav.href)) {
dav[ResourceType::class.java]?.let { type ->
when {
type.types.contains(ResourceType.ADDRESSBOOK) -> this.type = Type.ADDRESS_BOOK
@@ -185,8 +171,8 @@ data class CollectionInfo(
dest.writeString(url.toString())
writeOrNull(id, { dest.writeLong(it) })
writeOrNull(serviceID, { dest.writeLong(it) })
writeOrNull(id) { dest.writeLong(it) }
writeOrNull(serviceID) { dest.writeLong(it) }
dest.writeString(type?.name)
@@ -194,7 +180,7 @@ data class CollectionInfo(
dest.writeByte(if (forceReadOnly) 1 else 0)
dest.writeString(displayName)
dest.writeString(description)
writeOrNull(color, { dest.writeInt(it) })
writeOrNull(color) { dest.writeInt(it) }
dest.writeString(timeZone)
dest.writeByte(if (supportsVEVENT) 1 else 0)
@@ -206,9 +192,17 @@ data class CollectionInfo(
dest.writeByte(if (confirmed) 1 else 0)
}
@Suppress("unused")
@JvmField
val CREATOR = object: Parcelable.Creator<CollectionInfo> {
companion object CREATOR : Parcelable.Creator<CollectionInfo> {
val DAV_PROPERTIES = arrayOf(
ResourceType.NAME,
CurrentUserPrivilegeSet.NAME,
DisplayName.NAME,
AddressbookDescription.NAME, SupportedAddressData.NAME,
CalendarDescription.NAME, CalendarColor.NAME, SupportedCalendarComponentSet.NAME,
Source.NAME
)
override fun createFromParcel(parcel: Parcel): CollectionInfo {
fun<T> readOrNull(parcel: Parcel, read: () -> T): T? {
return if (parcel.readByte() == 0.toByte())
@@ -220,8 +214,8 @@ data class CollectionInfo(
return CollectionInfo(
HttpUrl.parse(parcel.readString())!!,
readOrNull(parcel, { parcel.readLong() }),
readOrNull(parcel, { parcel.readLong() }),
readOrNull(parcel) { parcel.readLong() },
readOrNull(parcel) { parcel.readLong() },
parcel.readString()?.let { Type.valueOf(it) },
@@ -229,7 +223,7 @@ data class CollectionInfo(
parcel.readByte() != 0.toByte(),
parcel.readString(),
parcel.readString(),
readOrNull(parcel, { parcel.readInt() }),
readOrNull(parcel) { parcel.readInt() },
parcel.readString(),
parcel.readByte() != 0.toByte(),

View File

@@ -45,8 +45,8 @@ data class SyncState(
}
}
fun fromSyncToken(token: SyncToken) =
token.token?.let { SyncState(Type.SYNC_TOKEN, it) }
fun fromSyncToken(token: SyncToken, initialSync: Boolean? = null) =
SyncState(Type.SYNC_TOKEN, requireNotNull(token.token), initialSync)
}

View File

@@ -119,20 +119,22 @@ class LocalAddressBook(
get() {
_mainAccount?.let { return it }
val accountManager = AccountManager.get(context)
val name = accountManager.getUserData(account, USER_DATA_MAIN_ACCOUNT_NAME)
val type = accountManager.getUserData(account, USER_DATA_MAIN_ACCOUNT_TYPE)
if (name != null && type != null)
return Account(name, type)
else
throw IllegalStateException("Address book doesn't exist anymore")
AccountManager.get(context).let { accountManager ->
val name = accountManager.getUserData(account, USER_DATA_MAIN_ACCOUNT_NAME)
val type = accountManager.getUserData(account, USER_DATA_MAIN_ACCOUNT_TYPE)
if (name != null && type != null)
return Account(name, type)
else
throw IllegalStateException("Address book doesn't exist anymore")
}
}
set(account) {
val accountManager = AccountManager.get(context)
accountManager.setUserData(account, USER_DATA_MAIN_ACCOUNT_NAME, account.name)
accountManager.setUserData(account, USER_DATA_MAIN_ACCOUNT_TYPE, account.type)
set(newMainAccount) {
AccountManager.get(context).let { accountManager ->
accountManager.setUserData(account, USER_DATA_MAIN_ACCOUNT_NAME, newMainAccount.name)
accountManager.setUserData(account, USER_DATA_MAIN_ACCOUNT_TYPE, newMainAccount.type)
}
_mainAccount = account
_mainAccount = newMainAccount
}
var url: String
@@ -183,19 +185,9 @@ class LocalAddressBook(
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
if (account.name != newAccountName && Build.VERSION.SDK_INT >= 21) {
// no need to re-assign contacts to new account, because they will be deleted by contacts provider in any case
val accountManager = AccountManager.get(context)
val future = accountManager.renameAccount(account, newAccountName, {
try {
// update raw contacts to new account name
provider?.let { provider ->
val values = ContentValues(1)
values.put(RawContacts.ACCOUNT_NAME, newAccountName)
provider.update(syncAdapterURI(RawContacts.CONTENT_URI), values, "${RawContacts.ACCOUNT_NAME}=?", arrayOf(account.name))
}
} catch (e: RemoteException) {
Logger.log.log(Level.WARNING, "Couldn't re-assign contacts to new account name", e)
}
}, null)
val future = accountManager.renameAccount(account, newAccountName, null, null)
account = future.result
}

View File

@@ -110,16 +110,16 @@ class Settings: Service(), Provider.Observer {
}
fun getBoolean(key: String) =
getValue(key, { provider -> provider.getBoolean(key) })
getValue(key) { provider -> provider.getBoolean(key) }
fun getInt(key: String) =
getValue(key, { provider -> provider.getInt(key) })
getValue(key) { provider -> provider.getInt(key) }
fun getLong(key: String) =
getValue(key, { provider -> provider.getLong(key) })
getValue(key) { provider -> provider.getLong(key) }
fun getString(key: String) =
getValue(key, { provider -> provider.getString(key) })
getValue(key) { provider -> provider.getString(key) }
fun isWritable(key: String): Boolean {
@@ -152,16 +152,16 @@ class Settings: Service(), Provider.Observer {
}
fun putBoolean(key: String, value: Boolean?) =
putValue(key, value, { provider -> provider.putBoolean(key, value) })
putValue(key, value) { provider -> provider.putBoolean(key, value) }
fun putInt(key: String, value: Int?) =
putValue(key, value, { provider -> provider.putInt(key, value) })
putValue(key, value) { provider -> provider.putInt(key, value) }
fun putLong(key: String, value: Long?) =
putValue(key, value, { provider -> provider.putLong(key, value) })
putValue(key, value) { provider -> provider.putLong(key, value) }
fun putString(key: String, value: String?) =
putValue(key, value, { provider -> provider.putString(key, value) })
putValue(key, value) { provider -> provider.putString(key, value) }
fun remove(key: String): Boolean {
var deleted = false

View File

@@ -56,16 +56,16 @@ class SharedPreferencesProvider(
}
override fun getBoolean(key: String): Pair<Boolean?, Boolean> =
getValue(key, { preferences -> preferences.getBoolean(key, /* will never be used: */ false) })
getValue(key) { preferences -> preferences.getBoolean(key, /* will never be used: */ false) }
override fun getInt(key: String): Pair<Int?, Boolean> =
getValue(key, { preferences -> preferences.getInt(key, /* will never be used: */ -1) })
getValue(key) { preferences -> preferences.getInt(key, /* will never be used: */ -1) }
override fun getLong(key: String): Pair<Long?, Boolean> =
getValue(key, { preferences -> preferences.getLong(key, /* will never be used: */ -1) })
getValue(key) { preferences -> preferences.getLong(key, /* will never be used: */ -1) }
override fun getString(key: String): Pair<String?, Boolean> =
getValue(key, { preferences -> preferences.getString(key, /* will never be used: */ null) })
getValue(key) { preferences -> preferences.getString(key, /* will never be used: */ null) }
override fun isWritable(key: String) =
@@ -84,16 +84,16 @@ class SharedPreferencesProvider(
}
override fun putBoolean(key: String, value: Boolean?) =
putValue(key, value, { editor, v -> editor.putBoolean(key, v) })
putValue(key, value) { editor, v -> editor.putBoolean(key, v) }
override fun putInt(key: String, value: Int?) =
putValue(key, value, { editor, v -> editor.putInt(key, v) })
putValue(key, value) { editor, v -> editor.putInt(key, v) }
override fun putLong(key: String, value: Long?) =
putValue(key, value, { editor, v -> editor.putLong(key, v) })
putValue(key, value) { editor, v -> editor.putLong(key, v) }
override fun putString(key: String, value: String?) =
putValue(key, value, { editor, v -> editor.putString(key, v) })
putValue(key, value) { editor, v -> editor.putString(key, v) }
override fun remove(key: String): Boolean {
Logger.log.fine("Removing setting $key")

View File

@@ -7,12 +7,16 @@
*/
package at.bitfire.davdroid.syncadapter
import android.Manifest
import android.accounts.Account
import android.accounts.AccountManager
import android.content.*
import android.content.pm.PackageManager
import android.database.DatabaseUtils
import android.os.Build
import android.os.Bundle
import android.provider.ContactsContract
import android.support.v4.content.ContextCompat
import at.bitfire.davdroid.AccountSettings
import at.bitfire.davdroid.R
import at.bitfire.davdroid.log.Logger
@@ -21,26 +25,20 @@ import at.bitfire.davdroid.model.ServiceDB
import at.bitfire.davdroid.model.ServiceDB.Collections
import at.bitfire.davdroid.resource.LocalAddressBook
import at.bitfire.davdroid.settings.ISettings
import at.bitfire.davdroid.ui.AccountActivity
import okhttp3.HttpUrl
import java.util.logging.Level
class AddressBooksSyncAdapterService: SyncAdapterService() {
class AddressBooksSyncAdapterService : SyncAdapterService() {
override fun syncAdapter() = AddressBooksSyncAdapter(this)
class AddressBooksSyncAdapter(
context: Context
): SyncAdapter(context) {
) : SyncAdapter(context) {
override fun sync(settings: ISettings, account: Account, extras: Bundle, authority: String, provider: ContentProviderClient, syncResult: SyncResult) {
val contactsProvider = context.contentResolver.acquireContentProviderClient(ContactsContract.AUTHORITY)
if (contactsProvider == null) {
Logger.log.severe("Couldn't access contacts provider")
syncResult.databaseError = true
return
}
try {
val accountSettings = AccountSettings(context, settings, account)
@@ -51,7 +49,7 @@ class AddressBooksSyncAdapterService: SyncAdapterService() {
if (!extras.containsKey(ContentResolver.SYNC_EXTRAS_MANUAL) && !checkSyncConditions(accountSettings))
return
updateLocalAddressBooks(contactsProvider, account)
updateLocalAddressBooks(provider, account, syncResult)
val accountManager = AccountManager.get(context)
for (addressBookAccount in accountManager.getAccountsByType(context.getString(R.string.account_type_address_book))) {
@@ -61,21 +59,21 @@ class AddressBooksSyncAdapterService: SyncAdapterService() {
syncExtras.putBoolean(ContentResolver.SYNC_EXTRAS_IGNORE_BACKOFF, true)
ContentResolver.requestSync(addressBookAccount, ContactsContract.AUTHORITY, syncExtras)
}
} catch(e: Exception) {
} catch (e: Exception) {
Logger.log.log(Level.SEVERE, "Couldn't sync address books", e)
}
Logger.log.info("Address book sync complete")
}
private fun updateLocalAddressBooks(provider: ContentProviderClient, account: Account) {
private fun updateLocalAddressBooks(provider: ContentProviderClient, account: Account, syncResult: SyncResult) {
ServiceDB.OpenHelper(context).use { dbHelper ->
val db = dbHelper.readableDatabase
fun getService() =
db.query(ServiceDB.Services._TABLE, arrayOf(ServiceDB.Services.ID),
"${ServiceDB.Services.ACCOUNT_NAME}=? AND ${ServiceDB.Services.SERVICE}=?",
arrayOf(account.name, ServiceDB.Services.SERVICE_CARDDAV), null, null, null)?.use { c ->
"${ServiceDB.Services.ACCOUNT_NAME}=? AND ${ServiceDB.Services.SERVICE}=?",
arrayOf(account.name, ServiceDB.Services.SERVICE_CARDDAV), null, null, null)?.use { c ->
if (c.moveToNext())
c.getLong(0)
else
@@ -86,7 +84,7 @@ class AddressBooksSyncAdapterService: SyncAdapterService() {
val collections = mutableMapOf<HttpUrl, CollectionInfo>()
service?.let {
db.query(Collections._TABLE, null,
Collections.SERVICE_ID + "=? AND " + Collections.SYNC, arrayOf(service.toString()), null, null, null)?.use { cursor ->
Collections.SERVICE_ID + "=? AND " + Collections.SYNC, arrayOf(service.toString()), null, null, null)?.use { cursor ->
while (cursor.moveToNext()) {
val values = ContentValues(cursor.columnCount)
DatabaseUtils.cursorRowToContentValues(cursor, values)
@@ -102,30 +100,58 @@ class AddressBooksSyncAdapterService: SyncAdapterService() {
val service = getService()
val remote = remoteAddressBooks(service)
// delete/update local address books
for (addressBook in LocalAddressBook.findAll(context, provider, account)) {
val url = HttpUrl.parse(addressBook.url)!!
val info = remote[url]
if (info == null) {
Logger.log.log(Level.INFO, "Deleting obsolete local address book", url)
addressBook.delete()
if (ContextCompat.checkSelfPermission(context, Manifest.permission.WRITE_CONTACTS) != PackageManager.PERMISSION_GRANTED) {
if (remote.isEmpty()) {
Logger.log.info("No contacts permission, but no address book selected for synchronization")
return
} else {
// remote CollectionInfo found for this local collection, update data
try {
Logger.log.log(Level.FINE, "Updating local address book $url", info)
addressBook.update(info)
} catch(e: Exception) {
Logger.log.log(Level.WARNING, "Couldn't rename address book account", e)
}
// we already have a local address book for this remote collection, don't take into consideration anymore
remote -= url
// no contacts permission, but address books should be synchronized -> show notification
val intent = Intent(context, AccountActivity::class.java)
intent.putExtra(AccountActivity.EXTRA_ACCOUNT, account)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
notifyPermissions(intent)
}
}
// create new local address books
for ((_, info) in remote) {
Logger.log.log(Level.INFO, "Adding local address book", info)
LocalAddressBook.create(context, provider, account, info)
val contactsProvider = context.contentResolver.acquireContentProviderClient(ContactsContract.AUTHORITY)
try {
if (contactsProvider == null) {
Logger.log.severe("Couldn't access contacts provider")
syncResult.databaseError = true
return
}
// delete/update local address books
for (addressBook in LocalAddressBook.findAll(context, contactsProvider, account)) {
val url = HttpUrl.parse(addressBook.url)!!
val info = remote[url]
if (info == null) {
Logger.log.log(Level.INFO, "Deleting obsolete local address book", url)
addressBook.delete()
} else {
// remote CollectionInfo found for this local collection, update data
try {
Logger.log.log(Level.FINE, "Updating local address book $url", info)
addressBook.update(info)
} catch (e: Exception) {
Logger.log.log(Level.WARNING, "Couldn't rename address book account", e)
}
// we already have a local address book for this remote collection, don't take into consideration anymore
remote -= url
}
}
// create new local address books
for ((_, info) in remote) {
Logger.log.log(Level.INFO, "Adding local address book", info)
LocalAddressBook.create(context, contactsProvider, account, info)
}
} finally {
if (Build.VERSION.SDK_INT >= 24)
contactsProvider?.close()
else
contactsProvider?.release()
}
}
}

View File

@@ -1,318 +0,0 @@
/*
* Copyright © Ricki Hirner (bitfire web engineering).
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Public License v3.0
* which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/gpl.html
*/
package at.bitfire.davdroid.syncadapter
import android.accounts.Account
import android.content.ContentResolver
import android.content.Context
import android.content.SyncResult
import android.os.Bundle
import at.bitfire.dav4android.DavCollection
import at.bitfire.dav4android.DavResource
import at.bitfire.dav4android.DavResponse
import at.bitfire.dav4android.exception.ConflictException
import at.bitfire.dav4android.exception.DavException
import at.bitfire.dav4android.exception.HttpException
import at.bitfire.dav4android.exception.PreconditionFailedException
import at.bitfire.dav4android.property.GetCTag
import at.bitfire.dav4android.property.GetETag
import at.bitfire.dav4android.property.SyncToken
import at.bitfire.davdroid.AccountSettings
import at.bitfire.davdroid.HttpClient
import at.bitfire.davdroid.log.Logger
import at.bitfire.davdroid.model.SyncState
import at.bitfire.davdroid.resource.LocalCollection
import at.bitfire.davdroid.resource.LocalResource
import at.bitfire.davdroid.settings.ISettings
import okhttp3.HttpUrl
import okhttp3.RequestBody
import java.util.logging.Level
abstract class BaseDavSyncManager<ResourceType: LocalResource<*>, out CollectionType: LocalCollection<ResourceType>, RemoteType: DavCollection>(
context: Context,
settings: ISettings,
account: Account,
accountSettings: AccountSettings,
extras: Bundle,
authority: String,
syncResult: SyncResult,
localCollection: CollectionType
): SyncManager<ResourceType, CollectionType>(context, settings, account, accountSettings, extras, authority, syncResult, localCollection), AutoCloseable {
companion object {
/**
* How many updates are requested per collection-sync REPORT. We use a rather small number
* because the response has to be parsed and held in memory, which is a spare resource on
* handheld devices.
*/
@Suppress("unused")
const val COLLECTION_SYNC_LIMIT = 1
}
protected val httpClient = HttpClient.Builder(context, settings, accountSettings).build()
protected lateinit var collectionURL: HttpUrl
protected lateinit var davCollection: RemoteType
protected var hasCollectionSync = false
override fun close() {
httpClient.close()
}
override fun prepare() = true
protected fun syncState(dav: DavResponse) =
dav[SyncToken::class.java]?.token?.let {
SyncState(SyncState.Type.SYNC_TOKEN, it)
} ?:
dav[GetCTag::class.java]?.cTag?.let {
SyncState(SyncState.Type.CTAG, it)
}
override fun querySyncState(): SyncState? =
davCollection.propfind(0, GetCTag.NAME, SyncToken.NAME).use {
syncState(it)
}
/**
* Process locally deleted entries (DELETE them on the server as well).
* Checks for thread interruption before each request to allow quick sync cancellation.
*/
override fun processLocallyDeleted(): Boolean {
var numDeleted = 0
// Remove locally deleted entries from server (if they have a name, i.e. if they were uploaded before),
// but only if they don't have changed on the server. Then finally remove them from the local address book.
val localList = localCollection.findDeleted()
for (local in localList)
useLocal(local, {
abortIfCancelled()
val fileName = local.fileName
if (fileName != null) {
Logger.log.info("$fileName has been deleted locally -> deleting from server")
useRemote(DavResource(httpClient.okHttpClient, collectionURL.newBuilder().addPathSegment(fileName).build()), { remote ->
try {
remote.delete(local.eTag)
numDeleted++
} catch (e: HttpException) {
Logger.log.warning("Couldn't delete $fileName from server; ignoring (may be downloaded again)")
}
})
} else
Logger.log.info("Removing local record #${local.id} which has been deleted locally and was never uploaded")
local.delete()
syncResult.stats.numDeletes++
})
Logger.log.info("Removed $numDeleted record(s) from server")
return numDeleted > 0
}
protected abstract fun prepareUpload(resource: ResourceType): RequestBody
/**
* Uploads dirty records to the server, using a PUT request for each record.
* Checks for thread interruption before each request to allow quick sync cancellation.
*/
override fun uploadDirty(): Boolean {
var numUploaded = 0
// upload dirty contacts
for (local in localCollection.findDirty())
useLocal(local, {
abortIfCancelled()
if (local.fileName == null) {
Logger.log.fine("Generating file name/UID for local record #${local.id}")
local.assignNameAndUID()
}
val fileName = local.fileName!!
useRemote(DavResource(httpClient.okHttpClient, collectionURL.newBuilder().addPathSegment(fileName).build()), { remote ->
// generate entity to upload (VCard, iCal, whatever)
val body = prepareUpload(local)
var response: DavResponse? = null
try {
response = if (local.eTag == null) {
Logger.log.info("Uploading new record $fileName")
remote.put(body, null, true)
} else {
Logger.log.info("Uploading locally modified record $fileName")
remote.put(body, local.eTag, false)
}
numUploaded++
} catch(e: ConflictException) {
// we can't interact with the user to resolve the conflict, so we treat 409 like 412
Logger.log.log(Level.INFO, "Edit conflict, ignoring", e)
} catch(e: PreconditionFailedException) {
Logger.log.log(Level.INFO, "Resource has been modified on the server before upload, ignoring", e)
} finally {
response?.close()
}
val newETag = response?.get(GetETag::class.java)
val eTag: String?
if (newETag != null) {
eTag = newETag.eTag
Logger.log.fine("Received new ETag=$eTag after uploading")
} else {
Logger.log.fine("Didn't receive new ETag after uploading, setting to null")
eTag = null
}
local.clearDirty(eTag)
})
})
Logger.log.info("Sent $numUploaded record(s) to server")
return numUploaded > 0
}
override fun syncRequired(state: SyncState?): Boolean {
if (syncAlgorithm() == SyncAlgorithm.PROPFIND_REPORT && extras.containsKey(ContentResolver.SYNC_EXTRAS_MANUAL)) {
Logger.log.info("Manual sync in PROPFIND/REPORT mode, forcing sync")
return true
}
val localState = localCollection.lastSyncState
Logger.log.info("Local sync state = $localState, remote sync state = $state")
return when {
state?.type == SyncState.Type.SYNC_TOKEN -> {
val lastKnownToken = localState?.takeIf { it.type == SyncState.Type.SYNC_TOKEN }?.value
lastKnownToken != state.value
}
state?.type == SyncState.Type.CTAG -> {
val lastKnownCTag = localState?.takeIf { it.type == SyncState.Type.CTAG }?.value
lastKnownCTag != state.value
}
else ->
true
}
}
override fun resetPresentRemotely() {
val number = localCollection.markNotDirty(0)
Logger.log.info("Number of local non-dirty entries: $number")
}
override fun compareLocalRemote(remoteResources: Map<String, DavResponse>): RemoteChanges {
/* check which resources are
1. updated remotely -> update
2. added remotely -> update
3. not present remotely anymore -> ignore (because they will be deleted by deleteObsolete()
*/
val changes = RemoteChanges(null, false)
for ((name, remote) in remoteResources)
useLocal(localCollection.findByName(name), { local ->
if (local == null) {
Logger.log.info("$name has been added remotely")
changes.updated += remote
} else {
val localETag = local.eTag
val remoteETag = remote[GetETag::class.java]?.eTag ?: throw DavException("Server didn't provide ETag")
if (localETag == remoteETag)
Logger.log.fine("$name has not been changed on server (ETag still $remoteETag)")
else {
Logger.log.info("$name has been changed on server (current ETag=$remoteETag, last known ETag=$localETag)")
changes.updated += remote
}
// mark as remotely present, so that this resource won't be deleted at the end
local.updateFlags(LocalResource.FLAG_REMOTELY_PRESENT)
}
})
return changes
}
override fun listRemoteChanges(state: SyncState?): RemoteChanges {
val dav = /*try {
davCollection.reportChanges(
state?.takeIf { state.type == SyncState.Type.SYNC_TOKEN }?.value,
false, COLLECTION_SYNC_LIMIT,
GetETag.NAME)
} catch(e: HttpException) {
if (e.status == 507)*/
// some servers don't like the limit, try again without
davCollection.reportChanges(
state?.takeIf { state.type == SyncState.Type.SYNC_TOKEN }?.value,
false, null,
GetETag.NAME)
/*else
throw e
}*/
dav.use {
val changes = RemoteChanges(dav.syncToken?.let { SyncState.fromSyncToken(it) }, dav.furtherResults)
for (member in dav.members) {
// ignore if resource is existing locally with same ETag
// (happens at initial sync, when resources are already present locally)
var skip = false
localCollection.findByName(member.fileName())?.let { local ->
member[GetETag::class.java]?.eTag?.let { remoteETag ->
if (local.eTag == remoteETag) {
Logger.log.info("${local.fileName} is already available with ETag $remoteETag, skipping update")
skip = true
// mark as remotely present, so that this resource won't be deleted at the end
local.updateFlags(LocalResource.FLAG_REMOTELY_PRESENT)
}
}
}
if (!skip)
changes.updated += member
}
for (member in dav.removedMembers)
changes.deleted += member.fileName()
Logger.log.log(Level.INFO, "Received list of changed/removed resources", changes)
return changes
}
}
override fun deleteNotPresentRemotely() {
val removed = localCollection.removeNotDirtyMarked(0)
Logger.log.info("Removed $removed local resources which are not present on the server anymore")
}
override fun postProcess() {
}
protected fun<T: LocalResource<*>?, R> useLocal(local: T, body: (T) -> R): R {
local?.let { currentLocalResource.push(it) }
val result = body(local)
local?.let { currentLocalResource.pop() }
return result
}
protected fun<T: DavResource, R> useRemote(remote: T, body: (T) -> R): R {
currentRemoteResource.push(remote.location)
val result = body(remote)
currentRemoteResource.pop()
return result
}
protected fun<T> useRemote(remote: DavResponse, body: (DavResponse) -> T): T {
currentRemoteResource.push(remote.url)
val result = body(remote)
currentRemoteResource.pop()
return result
}
protected fun<R> useRemoteCollection(body: (RemoteType) -> R) =
useRemote(davCollection, body)
}

View File

@@ -14,11 +14,14 @@ import android.content.SyncResult
import android.os.Bundle
import at.bitfire.dav4android.DavCalendar
import at.bitfire.dav4android.DavResource
import at.bitfire.dav4android.DavResponse
import at.bitfire.dav4android.DavResponseCallback
import at.bitfire.dav4android.Response
import at.bitfire.dav4android.exception.DavException
import at.bitfire.dav4android.property.*
import at.bitfire.davdroid.AccountSettings
import at.bitfire.davdroid.DavUtils
import at.bitfire.davdroid.log.Logger
import at.bitfire.davdroid.model.SyncState
import at.bitfire.davdroid.resource.LocalCalendar
import at.bitfire.davdroid.resource.LocalEvent
import at.bitfire.davdroid.resource.LocalResource
@@ -45,17 +48,9 @@ class CalendarSyncManager(
authority: String,
syncResult: SyncResult,
localCalendar: LocalCalendar
): BaseDavSyncManager<LocalEvent, LocalCalendar, DavCalendar>(context, settings, account, accountSettings, extras, authority, syncResult, localCalendar) {
companion object {
const val MULTIGET_MAX_RESOURCES = 30
}
): SyncManager<LocalEvent, LocalCalendar, DavCalendar>(context, settings, account, accountSettings, extras, authority, syncResult, localCalendar) {
override fun prepare(): Boolean {
if (!super.prepare())
return false
collectionURL = HttpUrl.parse(localCollection.name ?: return false) ?: return false
davCollection = DavCalendar(httpClient.okHttpClient, collectionURL)
@@ -65,16 +60,21 @@ class CalendarSyncManager(
return true
}
override fun queryCapabilities() =
override fun queryCapabilities(): SyncState? =
useRemoteCollection {
it.propfind(0, SupportedReportSet.NAME, GetCTag.NAME, SyncToken.NAME).use { dav ->
dav[SupportedReportSet::class.java]?.let {
hasCollectionSync = it.reports.contains(SupportedReportSet.SYNC_COLLECTION)
}
Logger.log.info("Server supports Collection Sync: $hasCollectionSync")
var syncState: SyncState? = null
it.propfind(0, SupportedReportSet.NAME, GetCTag.NAME, SyncToken.NAME) { response, relation ->
if (relation == Response.HrefRelation.SELF) {
response[SupportedReportSet::class.java]?.let {
hasCollectionSync = it.reports.contains(SupportedReportSet.SYNC_COLLECTION)
}
syncState(dav)
syncState = syncState(response)
}
}
Logger.log.info("Server supports Collection Sync: $hasCollectionSync")
syncState
}
override fun syncAlgorithm() = if (accountSettings.getTimeRangePastDays() != null || !hasCollectionSync)
@@ -82,7 +82,7 @@ class CalendarSyncManager(
else
SyncAlgorithm.COLLECTION_SYNC
override fun prepareUpload(resource: LocalEvent): RequestBody = useLocal(resource, {
override fun prepareUpload(resource: LocalEvent): RequestBody = useLocal(resource) {
val event = requireNotNull(resource.event)
Logger.log.log(Level.FINE, "Preparing upload of event ${resource.fileName}", event)
@@ -93,9 +93,9 @@ class CalendarSyncManager(
DavCalendar.MIME_ICALENDAR_UTF8,
os.toByteArray()
)
})
}
override fun listAllRemote(): Map<String, DavResponse> {
override fun listAllRemote(callback: DavResponseCallback) {
// calculate time range limits
var limitStart: Date? = null
accountSettings.getTimeRangePastDays()?.let { pastDays ->
@@ -105,68 +105,46 @@ class CalendarSyncManager(
}
return useRemoteCollection { remote ->
// fetch list of remote VEVENTs and build hash table to index file name
Logger.log.info("Querying events since $limitStart")
remote.calendarQuery("VEVENT", limitStart, null).use { dav ->
val result = LinkedHashMap<String, DavResponse>(dav.members.size)
for (iCal in dav.members) {
val fileName = iCal.fileName()
Logger.log.fine("Found remote VEVENT: $fileName")
result[fileName] = iCal
}
result
}
remote.calendarQuery("VEVENT", limitStart, null, callback)
}
}
override fun processRemoteChanges(changes: RemoteChanges) {
for (name in changes.deleted)
localCollection.findByName(name)?.let {
Logger.log.info("Deleting local event $name")
useLocal(it, { local -> local.delete() })
syncResult.stats.numDeletes++
}
override fun downloadRemote(bunch: List<HttpUrl>) {
Logger.log.info("Downloading ${bunch.size} iCalendars: $bunch")
if (bunch.size == 1) {
val remote = bunch.first()
// only one contact, use GET
useRemote(DavResource(httpClient.okHttpClient, remote)) { resource ->
resource.get(DavCalendar.MIME_ICALENDAR.toString()) { response ->
// CalDAV servers MUST return ETag on GET [https://tools.ietf.org/html/rfc4791#section-5.3.4]
val eTag = response.header("ETag")?.let { GetETag(it).eTag }
?: throw DavException("Received CalDAV GET response without ETag")
val toDownload = changes.updated.map { it.url }
Logger.log.info("Downloading ${toDownload.size} resources ($MULTIGET_MAX_RESOURCES at once)")
for (bunch in toDownload.chunked(MULTIGET_MAX_RESOURCES)) {
if (bunch.size == 1)
// only one contact, use GET
useRemote(DavResource(httpClient.okHttpClient, bunch.first()), {
it.get(DavCalendar.MIME_ICALENDAR.toString()).use { dav ->
// CalDAV servers MUST return ETag on GET [https://tools.ietf.org/html/rfc4791#section-5.3.4]
val eTag = dav[GetETag::class.java]?.eTag
?: throw DavException("Received CalDAV GET response without ETag for ${dav.url}")
dav.body?.charStream()?.use { reader ->
processVEvent(dav.fileName(), eTag, reader)
}
}
})
else {
// multiple contacts, use multi-get
useRemoteCollection {
it.multiget(bunch).use { dav ->
// process multiget results
for (remote in dav.members)
useRemote(remote, {
val eTag = remote[GetETag::class.java]?.eTag
?: throw DavException("Received multi-get response without ETag")
val calendarData = remote[CalendarData::class.java]
val iCalendar = calendarData?.iCalendar
?: throw DavException("Received multi-get response without task data")
processVEvent(remote.fileName(), eTag, StringReader(iCalendar))
})
response.body()!!.use {
processVEvent(resource.fileName(), eTag, it.charStream())
}
}
}
} else
// multiple iCalendars, use calendar-multi-get
useRemoteCollection {
it.multiget(bunch) { response, _ ->
useRemote(response) {
val eTag = response[GetETag::class.java]?.eTag
?: throw DavException("Received multi-get response without ETag")
abortIfCancelled()
}
val calendarData = response[CalendarData::class.java]
val iCal = calendarData?.iCalendar
?: throw DavException("Received multi-get response without address data")
processVEvent(DavUtils.lastSegmentOfUrl(response.href), eTag, StringReader(iCal))
}
}
}
}
override fun postProcess() {
}
@@ -185,7 +163,7 @@ class CalendarSyncManager(
val newData = events.first()
// delete local event, if it exists
useLocal(localCollection.findByName(fileName), { local ->
useLocal(localCollection.findByName(fileName)) { local ->
if (local != null) {
Logger.log.info("Updating $fileName in local calendar")
local.eTag = eTag
@@ -193,12 +171,12 @@ class CalendarSyncManager(
syncResult.stats.numUpdates++
} else {
Logger.log.info("Adding $fileName to local calendar")
useLocal(LocalEvent(localCollection, newData, fileName, eTag, LocalResource.FLAG_REMOTELY_PRESENT), {
useLocal(LocalEvent(localCollection, newData, fileName, eTag, LocalResource.FLAG_REMOTELY_PRESENT)) {
it.add()
})
}
syncResult.stats.numInserts++
}
})
}
} else
Logger.log.info("Received VCALENDAR with not exactly one VEVENT with UID and without RECURRENCE-ID; ignoring $fileName")
}

View File

@@ -16,10 +16,12 @@ import android.provider.ContactsContract.Groups
import android.support.v4.app.NotificationCompat
import at.bitfire.dav4android.DavAddressBook
import at.bitfire.dav4android.DavResource
import at.bitfire.dav4android.DavResponse
import at.bitfire.dav4android.DavResponseCallback
import at.bitfire.dav4android.Response
import at.bitfire.dav4android.exception.DavException
import at.bitfire.dav4android.property.*
import at.bitfire.davdroid.AccountSettings
import at.bitfire.davdroid.DavUtils
import at.bitfire.davdroid.HttpClient
import at.bitfire.davdroid.R
import at.bitfire.davdroid.log.Logger
@@ -35,7 +37,6 @@ import okhttp3.HttpUrl
import okhttp3.Request
import okhttp3.RequestBody
import java.io.*
import java.util.*
import java.util.logging.Level
/**
@@ -83,11 +84,9 @@ class ContactsSyncManager(
syncResult: SyncResult,
val provider: ContentProviderClient,
localAddressBook: LocalAddressBook
): BaseDavSyncManager<LocalAddress, LocalAddressBook, DavAddressBook>(context, settings, account, accountSettings, extras, authority, syncResult, localAddressBook) {
): SyncManager<LocalAddress, LocalAddressBook, DavAddressBook>(context, settings, account, accountSettings, extras, authority, syncResult, localAddressBook) {
companion object {
private const val MULTIGET_MAX_RESOURCES = 10
infix fun <T> Set<T>.disjunct(other: Set<T>) = (this - other) union (other - this)
}
@@ -97,11 +96,13 @@ class ContactsSyncManager(
private var hasVCard4 = false
private val groupMethod = accountSettings.getGroupMethod()
/**
* Used to download images which are referenced by URL
*/
private lateinit var resourceDownloader: ResourceDownloader
override fun prepare(): Boolean {
if (!super.prepare())
return false
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
// workaround for Android 7 which sets DIRTY flag when only meta-data is changed
val reallyDirty = localCollection.verifyDirty()
@@ -115,6 +116,8 @@ class ContactsSyncManager(
collectionURL = HttpUrl.parse(localCollection.url) ?: return false
davCollection = DavAddressBook(httpClient.okHttpClient, collectionURL)
resourceDownloader = ResourceDownloader(davCollection.location)
return true
}
@@ -124,19 +127,25 @@ class ContactsSyncManager(
localCollection.includeGroups = groupMethod == GroupMethod.GROUP_VCARDS
return useRemoteCollection {
it.propfind(0, SupportedAddressData.NAME, SupportedReportSet.NAME, GetCTag.NAME, SyncToken.NAME).use { dav ->
dav[SupportedAddressData::class.java]?.let {
hasVCard4 = it.hasVCard4()
}
Logger.log.info("Server supports vCard/4: $hasVCard4")
var syncState: SyncState? = null
it.propfind(0, SupportedAddressData.NAME, SupportedReportSet.NAME, GetCTag.NAME, SyncToken.NAME) { response, relation ->
if (relation == Response.HrefRelation.SELF) {
response[SupportedAddressData::class.java]?.let {
hasVCard4 = it.hasVCard4()
}
dav[SupportedReportSet::class.java]?.let {
hasCollectionSync = it.reports.contains(SupportedReportSet.SYNC_COLLECTION)
}
Logger.log.info("Server supports Collection Sync: $hasCollectionSync")
response[SupportedReportSet::class.java]?.let {
hasCollectionSync = it.reports.contains(SupportedReportSet.SYNC_COLLECTION)
}
syncState(dav)
syncState = syncState(response)
}
}
Logger.log.info("Server supports vCard/4: $hasVCard4")
Logger.log.info("Server supports Collection Sync: $hasCollectionSync")
syncState
}
}
@@ -149,13 +158,13 @@ class ContactsSyncManager(
if (readOnly) {
for (group in localCollection.findDeletedGroups()) {
Logger.log.warning("Restoring locally deleted group (read-only address book!)")
useLocal(group, { it.resetDeleted() })
useLocal(group) { it.resetDeleted() }
numDiscarded++
}
for (contact in localCollection.findDeletedContacts()) {
Logger.log.warning("Restoring locally deleted contact (read-only address book!)")
useLocal(contact, { it.resetDeleted() })
useLocal(contact) { it.resetDeleted() }
numDiscarded++
}
@@ -171,13 +180,13 @@ class ContactsSyncManager(
if (readOnly) {
for (group in localCollection.findDirtyGroups()) {
Logger.log.warning("Resetting locally modified group to ETag=null (read-only address book!)")
useLocal(group, { it.clearDirty(null) })
useLocal(group) { it.clearDirty(null) }
numDiscarded++
}
for (contact in localCollection.findDirtyContacts()) {
Logger.log.warning("Resetting locally modified contact to ETag=null (read-only address book!)")
useLocal(contact, { it.clearDirty(null) })
useLocal(contact) { it.clearDirty(null) }
numDiscarded++
}
@@ -193,16 +202,16 @@ class ContactsSyncManager(
Logger.log.fine("Finally removing group $group")
// useless because Android deletes group memberships as soon as a group is set to DELETED:
// group.markMembersDirty()
useLocal(group, { it.delete() })
useLocal(group) { it.delete() }
}
// groups with DIRTY=1: mark all memberships as dirty, then clean DIRTY flag of group
for (group in localCollection.findDirtyGroups()) {
Logger.log.fine("Marking members of modified group $group as dirty")
useLocal(group, {
useLocal(group) {
it.markMembersDirty()
it.clearDirty(null)
})
}
}
} else {
/* groups as separate VCards: there are group contacts and individual contacts */
@@ -246,7 +255,7 @@ class ContactsSyncManager(
notificationManager.notify("discarded_${account.name}", 0, notification)
}
override fun prepareUpload(resource: LocalAddress): RequestBody = useLocal(resource, {
override fun prepareUpload(resource: LocalAddress): RequestBody = useLocal(resource) {
val contact: Contact
if (resource is LocalContact) {
contact = resource.contact!!
@@ -280,82 +289,45 @@ class ContactsSyncManager(
if (hasVCard4) DavAddressBook.MIME_VCARD4 else DavAddressBook.MIME_VCARD3_UTF8,
os.toByteArray()
)
})
override fun listAllRemote() = useRemoteCollection {
// fetch list of remote VCards and build hash table to index file name
it.propfind(1, ResourceType.NAME, GetETag.NAME).use { dav ->
val result = LinkedHashMap<String, DavResponse>(dav.members.size)
for (vCard in dav.members) {
// ignore member collections
var ignore = false
vCard[ResourceType::class.java]?.let { type ->
if (type.types.contains(ResourceType.COLLECTION))
ignore = true
}
if (ignore)
continue
val fileName = vCard.fileName()
Logger.log.fine("Found remote VCard: $fileName")
result[fileName] = vCard
}
result
}
}
override fun processRemoteChanges(changes: RemoteChanges) {
for (name in changes.deleted)
localCollection.findByName(name)?.let {
Logger.log.info("Deleting local address $name")
useLocal(it, { it.delete() })
syncResult.stats.numDeletes++
override fun listAllRemote(callback: DavResponseCallback) =
useRemoteCollection {
it.propfind(1, ResourceType.NAME, GetETag.NAME, callback = callback)
}
val toDownload = changes.updated.map { it.url }
Logger.log.info("Downloading ${toDownload.size} resources ($MULTIGET_MAX_RESOURCES at once)")
override fun downloadRemote(bunch: List<HttpUrl>) {
Logger.log.info("Downloading ${bunch.size} vCards: $bunch")
if (bunch.size == 1) {
val remote = bunch.first()
// only one contact, use GET
useRemote(DavResource(httpClient.okHttpClient, remote)) { resource ->
resource.get("text/vcard;version=4.0, text/vcard;charset=utf-8;q=0.8, text/vcard;q=0.5") { response ->
// CardDAV servers MUST return ETag on GET [https://tools.ietf.org/html/rfc6352#section-6.3.2.3]
val eTag = response.header("ETag")?.let { GetETag(it).eTag }
?: throw DavException("Received CardDAV GET response without ETag")
// prepare downloader which may be used to download external resource like contact photos
val downloader = ResourceDownloader(collectionURL)
// download new/updated VCards from server
for (bunch in toDownload.chunked(CalendarSyncManager.MULTIGET_MAX_RESOURCES)) {
if (bunch.size == 1)
// only one contact, use GET
useRemote(DavResource(httpClient.okHttpClient, bunch.first()), {
it.get("text/vcard;version=4.0, text/vcard;charset=utf-8;q=0.8, text/vcard;q=0.5").use { dav ->
// CardDAV servers MUST return ETag on GET [https://tools.ietf.org/html/rfc6352#section-6.3.2.3]
val eTag = dav[GetETag::class.java]?.eTag
?: throw DavException("Received CardDAV GET response without ETag for ${dav.url}")
dav.body?.charStream()?.use { reader ->
processVCard(dav.fileName(), eTag, reader, downloader)
}
}
})
else {
// multiple contacts, use multi-get
useRemoteCollection {
it.multiget(bunch, hasVCard4).use { dav ->
// process multi-get results
for (remote in dav.members)
useRemote(remote, {
val eTag = remote[GetETag::class.java]?.eTag
?: throw DavException("Received multi-get response without ETag")
val addressData = remote[AddressData::class.java]
val vCard = addressData?.vCard
?: throw DavException("Received multi-get response without address data")
processVCard(remote.fileName(), eTag, StringReader(vCard), downloader)
})
response.body()!!.use {
processVCard(resource.fileName(), eTag, it.charStream(), resourceDownloader)
}
}
}
} else
// multiple vCards, use addressbook-multi-get
useRemoteCollection {
it.multiget(bunch, hasVCard4) { response, _ ->
useRemote(response) {
val eTag = response[GetETag::class.java]?.eTag
?: throw DavException("Received multi-get response without ETag")
abortIfCancelled()
}
val addressData = response[AddressData::class.java]
val vCard = addressData?.vCard
?: throw DavException("Received multi-get response without address data")
processVCard(DavUtils.lastSegmentOfUrl(response.href), eTag, StringReader(vCard), resourceDownloader)
}
}
}
}
override fun postProcess() {
@@ -393,7 +365,7 @@ class ContactsSyncManager(
}
// update local contact, if it exists
useLocal(localCollection.findByName(fileName), {
useLocal(localCollection.findByName(fileName)) {
var local = it
if (local != null) {
Logger.log.log(Level.INFO, "Updating $fileName in local address book", newData)
@@ -422,16 +394,16 @@ class ContactsSyncManager(
if (local == null) {
if (newData.group) {
Logger.log.log(Level.INFO, "Creating local group", newData)
useLocal(LocalGroup(localCollection, newData, fileName, eTag, LocalResource.FLAG_REMOTELY_PRESENT), {
useLocal(LocalGroup(localCollection, newData, fileName, eTag, LocalResource.FLAG_REMOTELY_PRESENT)) {
it.add()
local = it
})
}
} else {
Logger.log.log(Level.INFO, "Creating local contact", newData)
useLocal(LocalContact(localCollection, newData, fileName, eTag, LocalResource.FLAG_REMOTELY_PRESENT), {
useLocal(LocalContact(localCollection, newData, fileName, eTag, LocalResource.FLAG_REMOTELY_PRESENT)) {
it.add()
local = it
})
}
}
syncResult.stats.numInserts++
}
@@ -453,9 +425,9 @@ class ContactsSyncManager(
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && Build.VERSION.SDK_INT < Build.VERSION_CODES.O)
// workaround for Android 7 which sets DIRTY flag when only meta-data is changed
// workaround for Android 7 which sets DIRTY flag when only meta-data is changed
(local as? LocalContact)?.updateHashCode(null)
})
}
}

View File

@@ -151,7 +151,7 @@ abstract class SyncAdapterService: Service() {
return true
}
private fun notifyPermissions(intent: Intent) {
protected fun notifyPermissions(intent: Intent) {
val notify = NotificationUtils.newBuilder(context, NotificationUtils.CHANNEL_SYNC_ERRORS)
.setSmallIcon(R.drawable.ic_sync_error_notification)
.setContentTitle(context.getString(R.string.sync_error_permissions))

View File

@@ -10,10 +10,7 @@ package at.bitfire.davdroid.syncadapter
import android.accounts.Account
import android.app.PendingIntent
import android.content.ContentUris
import android.content.Context
import android.content.Intent
import android.content.SyncResult
import android.content.*
import android.net.Uri
import android.os.Bundle
import android.os.RemoteException
@@ -21,16 +18,14 @@ import android.provider.CalendarContract
import android.provider.ContactsContract
import android.support.v4.app.NotificationCompat
import android.support.v4.app.NotificationManagerCompat
import at.bitfire.dav4android.DavResponse
import at.bitfire.dav4android.Property
import at.bitfire.dav4android.XmlUtils
import at.bitfire.dav4android.exception.DavException
import at.bitfire.dav4android.exception.HttpException
import at.bitfire.dav4android.exception.ServiceUnavailableException
import at.bitfire.dav4android.exception.UnauthorizedException
import at.bitfire.davdroid.AccountSettings
import at.bitfire.dav4android.*
import at.bitfire.dav4android.exception.*
import at.bitfire.dav4android.property.GetCTag
import at.bitfire.dav4android.property.GetETag
import at.bitfire.dav4android.property.SyncToken
import at.bitfire.davdroid.*
import at.bitfire.davdroid.BuildConfig
import at.bitfire.davdroid.DavService
import at.bitfire.davdroid.Constants
import at.bitfire.davdroid.R
import at.bitfire.davdroid.log.Logger
import at.bitfire.davdroid.model.SyncState
@@ -40,19 +35,24 @@ import at.bitfire.davdroid.ui.AccountSettingsActivity
import at.bitfire.davdroid.ui.DebugInfoActivity
import at.bitfire.davdroid.ui.NotificationUtils
import at.bitfire.ical4android.CalendarStorageException
import at.bitfire.ical4android.MiscUtils
import at.bitfire.ical4android.TaskProvider
import at.bitfire.vcard4android.ContactsStorageException
import okhttp3.HttpUrl
import okhttp3.RequestBody
import org.apache.commons.lang3.exception.ContextedException
import org.dmfs.tasks.contract.TaskContract
import java.io.IOException
import java.io.InterruptedIOException
import java.net.HttpURLConnection
import java.security.cert.CertificateException
import java.util.*
import java.util.concurrent.*
import java.util.concurrent.atomic.AtomicInteger
import java.util.logging.Level
import javax.net.ssl.SSLHandshakeException
abstract class SyncManager<out ResourceType: LocalResource<*>, out CollectionType: LocalCollection<ResourceType>>(
@Suppress("MemberVisibilityCanBePrivate")
abstract class SyncManager<ResourceType: LocalResource<*>, out CollectionType: LocalCollection<ResourceType>, RemoteType: DavCollection>(
val context: Context,
val settings: ISettings,
val account: Account,
@@ -63,8 +63,17 @@ abstract class SyncManager<out ResourceType: LocalResource<*>, out CollectionTyp
val localCollection: CollectionType
): AutoCloseable {
enum class SyncAlgorithm {
PROPFIND_REPORT,
COLLECTION_SYNC
}
companion object {
val MAX_PROCESSING_THREADS = Math.max(Runtime.getRuntime().availableProcessors(), 4)
val MAX_DOWNLOAD_THREADS = Math.max(Runtime.getRuntime().availableProcessors(), 4)
const val MAX_MULTIGET_RESOURCES = 10
fun cancelNotifications(manager: NotificationManagerCompat, authority: String, account: Account) =
manager.cancel(notificationTag(authority, account), NotificationUtils.NOTIFY_SYNC_ERROR)
@@ -81,21 +90,27 @@ abstract class SyncManager<out ResourceType: LocalResource<*>, out CollectionTyp
protected val notificationManager = NotificationManagerCompat.from(context)
protected val notificationTag = notificationTag(authority, mainAccount)
/** Local resource we're currently operating on. Used for error notifications. **/
protected val currentLocalResource = LinkedList<LocalResource<*>>()
/** Remote resource we're currently operating on. Used for error notifications. **/
protected val currentRemoteResource = LinkedList<HttpUrl>()
protected val httpClient = HttpClient.Builder(context, settings, accountSettings).build()
protected lateinit var collectionURL: HttpUrl
protected lateinit var davCollection: RemoteType
protected var hasCollectionSync = false
override fun close() {
httpClient.close()
}
fun performSync() {
// dismiss previous error notifications
notificationManager.cancel(notificationTag, NotificationUtils.NOTIFY_SYNC_ERROR)
try {
unwrapExceptions({
Logger.log.info("Preparing synchronization")
if (!prepare()) {
Logger.log.info("No reason to synchronize, aborting")
return
return@unwrapExceptions
}
abortIfCancelled()
@@ -104,8 +119,7 @@ abstract class SyncManager<out ResourceType: LocalResource<*>, out CollectionTyp
abortIfCancelled()
Logger.log.info("Sending local deletes/updates to server")
val modificationsSent =
processLocallyDeleted() ||
val modificationsSent = processLocallyDeleted() ||
uploadDirty()
abortIfCancelled()
@@ -119,19 +133,14 @@ abstract class SyncManager<out ResourceType: LocalResource<*>, out CollectionTyp
if (modificationsSent)
remoteSyncState = querySyncState()
// list all entries at current sync state (which may be the same as or newer than remoteSyncState)
Logger.log.info("Listing remote entries")
val remote = listAllRemote()
abortIfCancelled()
Logger.log.info("Comparing local/remote entries")
val changes = compareLocalRemote(remote)
Logger.log.info("Processing remote changes")
processRemoteChanges(changes)
// list and process all entries at current sync state (which may be the same as or newer than remoteSyncState)
Logger.log.info("Processing remote entries")
syncRemote { callback ->
listAllRemote(callback)
}
Logger.log.info("Deleting entries which are not present remotely anymore")
deleteNotPresentRemotely()
syncResult.stats.numDeletes += deleteNotPresentRemotely()
Logger.log.info("Post-processing")
postProcess()
@@ -143,51 +152,45 @@ abstract class SyncManager<out ResourceType: LocalResource<*>, out CollectionTyp
var initialSync = false
var syncState = localCollection.lastSyncState?.takeIf { it.type == SyncState.Type.SYNC_TOKEN }
Logger.log.info("Listing changes since $syncState")
var changes: RemoteChanges? = try {
listRemoteChanges(syncState)
} catch(e: HttpException) {
if (e.errors.contains(Property.Name(XmlUtils.NS_WEBDAV, "valid-sync-token"))) {
Logger.log.info("Sync token stale, retrying without sync-token")
syncState = null
listRemoteChanges(null)
} else
throw e
}
if (syncState == null) {
Logger.log.info("Starting initial sync")
initialSync = true
resetPresentRemotely()
}
if (syncState?.initialSync == true) {
} else if (syncState.initialSync == true) {
Logger.log.info("Continuing initial sync")
initialSync = true
}
while (changes != null) {
Logger.log.info("Processing received changes")
processRemoteChanges(changes)
var furtherChanges = false
do {
Logger.log.info("Listing changes since $syncState")
syncRemote { callback ->
try {
val result = listRemoteChanges(syncState, callback)
syncState = SyncState.fromSyncToken(result.first, initialSync)
furtherChanges = result.second
} catch(e: HttpException) {
if (e.errors.any { it.name == Property.Name(XmlUtils.NS_WEBDAV, "valid-sync-token") }) {
Logger.log.info("Sync token invalid, performing initial sync")
initialSync = true
resetPresentRemotely()
// save sync state and keep whether we're in initial sync
syncState = changes.state ?: throw DavException("Received sync-collection without sync-token")
syncState.initialSync = initialSync
Logger.log.log(Level.INFO, "Saving sync state", syncState)
localCollection.lastSyncState = syncState
val result = listRemoteChanges(null, callback)
syncState = SyncState.fromSyncToken(result.first, initialSync)
furtherChanges = result.second
} else
throw e
}
// request next bunch of changes (if available), or exit loop
changes = if (changes.furtherChanges)
listRemoteChanges(syncState)
else {
Logger.log.info("No more changes available on server")
null
Logger.log.log(Level.INFO, "Saving sync state", syncState)
localCollection.lastSyncState = syncState
}
}
Logger.log.info("Server has further changes: $furtherChanges")
} while(furtherChanges)
if (initialSync) {
// initial sync is finished, remove all local resources which have
// not been sent by the server
// initial sync is finished, remove all local resources which have not been listed by server
Logger.log.info("Deleting local resources which are not on server (anymore)")
deleteNotPresentRemotely()
@@ -204,31 +207,36 @@ abstract class SyncManager<out ResourceType: LocalResource<*>, out CollectionTyp
else
Logger.log.info("Remote collection didn't change, no reason to sync")
}
// sync was cancelled: re-throw to SyncAdapterService
catch (e: InterruptedException) { throw e }
catch (e: InterruptedIOException) { throw e }
}, { e, local, remote ->
when (e) {
// sync was cancelled: re-throw to SyncAdapterService
is InterruptedException,
is InterruptedIOException ->
throw e
// specific I/O errors
catch (e: SSLHandshakeException) {
Logger.log.log(Level.WARNING, "SSL handshake failed", e)
// specific I/O errors
is SSLHandshakeException -> {
Logger.log.log(Level.WARNING, "SSL handshake failed", e)
// when a certificate is rejected by cert4android, the cause will be a CertificateException
if (!BuildConfig.customCerts || e.cause !is CertificateException)
notifyException(e)
}
// when a certificate is rejected by cert4android, the cause will be a CertificateException
if (!BuildConfig.customCerts || e.cause !is CertificateException)
notifyException(e, local, remote)
}
// specific HTTP errors
catch (e: ServiceUnavailableException) {
Logger.log.log(Level.WARNING, "Got 503 Service unavailable, trying again later", e)
e.retryAfter?.let { retryAfter ->
// how many seconds to wait? getTime() returns ms, so divide by 1000
syncResult.delayUntil = (retryAfter.time - Date().time) / 1000
// specific HTTP errors
is ServiceUnavailableException -> {
Logger.log.log(Level.WARNING, "Got 503 Service unavailable, trying again later", e)
e.retryAfter?.let { retryAfter ->
// how many seconds to wait? getTime() returns ms, so divide by 1000
syncResult.delayUntil = (retryAfter.time - Date().time) / 1000
}
}
// all others
else ->
notifyException(e, local, remote)
}
}
// all others
catch (e: Throwable) { notifyException(e) }
})
}
@@ -245,41 +253,135 @@ abstract class SyncManager<out ResourceType: LocalResource<*>, out CollectionTyp
protected abstract fun queryCapabilities(): SyncState?
/**
* Queries the remote sync state of the collection.
* Processes locally deleted entries and forwards them to the server (HTTP `DELETE`).
*
* @return sync state (may be null)
* @return whether resources have been deleted from the server
*/
protected abstract fun querySyncState(): SyncState?
protected open fun processLocallyDeleted(): Boolean {
var numDeleted = 0
// Remove locally deleted entries from server (if they have a name, i.e. if they were uploaded before),
// but only if they don't have changed on the server. Then finally remove them from the local address book.
val localList = localCollection.findDeleted()
for (local in localList) {
abortIfCancelled()
useLocal(local) {
val fileName = local.fileName
if (fileName != null) {
Logger.log.info("$fileName has been deleted locally -> deleting from server (ETag ${local.eTag})")
useRemote(DavResource(httpClient.okHttpClient, collectionURL.newBuilder().addPathSegment(fileName).build())) { remote ->
try {
remote.delete(local.eTag) {}
numDeleted++
} catch (e: HttpException) {
Logger.log.warning("Couldn't delete $fileName from server; ignoring (may be downloaded again)")
}
}
} else
Logger.log.info("Removing local record #${local.id} which has been deleted locally and was never uploaded")
local.delete()
syncResult.stats.numDeletes++
}
}
Logger.log.info("Removed $numDeleted record(s) from server")
return numDeleted > 0
}
/**
* Forwards local deletions to the server.
*
* @return whether remote resources have been deleted
*/
protected abstract fun processLocallyDeleted(): Boolean
/**
* Uploads locally modified resources to the server.
* Uploads locally modified resources to the server (HTTP `PUT`).
*
* @return whether resources have been uploaded
*/
protected abstract fun uploadDirty(): Boolean
protected open fun uploadDirty(): Boolean {
var numUploaded = 0
// upload dirty contacts
for (local in localCollection.findDirty())
useLocal(local) {
abortIfCancelled()
if (local.fileName == null) {
Logger.log.fine("Generating file name/UID for local record #${local.id}")
local.assignNameAndUID()
}
val fileName = local.fileName!!
useRemote(DavResource(httpClient.okHttpClient, collectionURL.newBuilder().addPathSegment(fileName).build())) { remote ->
// generate entity to upload (VCard, iCal, whatever)
val body = prepareUpload(local)
var eTag: String? = null
val processETag: (response: okhttp3.Response) -> Unit = {
it.header("ETag")?.let {
eTag = GetETag(it).eTag
}
}
try {
if (local.eTag == null) {
Logger.log.info("Uploading new record $fileName")
remote.put(body, null, true, processETag)
} else {
Logger.log.info("Uploading locally modified record $fileName")
remote.put(body, local.eTag, false, processETag)
}
numUploaded++
} catch(e: ConflictException) {
// we can't interact with the user to resolve the conflict, so we treat 409 like 412
Logger.log.log(Level.INFO, "Edit conflict, ignoring", e)
} catch(e: PreconditionFailedException) {
Logger.log.log(Level.INFO, "Resource has been modified on the server before upload, ignoring", e)
}
if (eTag != null)
Logger.log.fine("Received new ETag=$eTag after uploading")
else
Logger.log.fine("Didn't receive new ETag after uploading, setting to null")
local.clearDirty(eTag)
}
}
Logger.log.info("Sent $numUploaded record(s) to server")
return numUploaded > 0
}
protected abstract fun prepareUpload(resource: ResourceType): RequestBody
/**
* Determines whether a sync is required because there were changes on the server.
* For instance, this method can compare the collection's CTag/sync-token with
* For instance, this method can compare the collection's `CTag`/`sync-token` with
* the last known local value.
*
* When local changes have been uploaded ([processLocallyDeleted] and/or
* [uploadDirty] were true), a sync is always required and this method
* should not be evaluated.
* should *not* be evaluated.
*
* @param state remote sync state to compare local sync state with
*
* @return whether data has been changed on the server = whether running the
* sync algorithm is required
* @return whether data has been changed on the server, i.e. whether running the
* sync algorithm is required
*/
protected abstract fun syncRequired(state: SyncState?): Boolean
protected open fun syncRequired(state: SyncState?): Boolean {
if (syncAlgorithm() == SyncAlgorithm.PROPFIND_REPORT && extras.containsKey(ContentResolver.SYNC_EXTRAS_MANUAL)) {
Logger.log.info("Manual sync in PROPFIND/REPORT mode, forcing sync")
return true
}
val localState = localCollection.lastSyncState
Logger.log.info("Local sync state = $localState, remote sync state = $state")
return when {
state?.type == SyncState.Type.SYNC_TOKEN -> {
val lastKnownToken = localState?.takeIf { it.type == SyncState.Type.SYNC_TOKEN }?.value
lastKnownToken != state.value
}
state?.type == SyncState.Type.CTAG -> {
val lastKnownCTag = localState?.takeIf { it.type == SyncState.Type.CTAG }?.value
lastKnownCTag != state.value
}
else ->
true
}
}
/**
* Determines which sync algorithm to use.
@@ -297,57 +399,161 @@ abstract class SyncManager<out ResourceType: LocalResource<*>, out CollectionTyp
*
* Used together with [deleteNotPresentRemotely].
*/
protected abstract fun resetPresentRemotely()
protected open fun resetPresentRemotely() {
val number = localCollection.markNotDirty(0)
Logger.log.info("Number of local non-dirty entries: $number")
}
/**
* Lists all remote resources which should be taken into account for synchronization.
* Will be used if incremental synchronization is not available.
*
* @return map with resource names (like "mycontact.vcf") as keys and the resources
*/
protected abstract fun listAllRemote(): Map<String, DavResponse>
protected open fun syncRemote(listRemote: (DavResponseCallback) -> Unit) {
// results must be processed in main thread because exceptions must be thrown in main
// thread, so that they can be catched by SyncManager
val results = ConcurrentLinkedQueue<Future<*>>()
// thread-safe sync stats
val nInserted = AtomicInteger()
val nUpdated = AtomicInteger()
val nDeleted = AtomicInteger()
val nSkipped = AtomicInteger()
/**
* Compares local resources which are marked for synchronization and remote resources by file name and ETag.
* Remote resources
* + which are not present locally
* + whose ETag has changed since the last sync (i.e. remote ETag != locally known last remote ETag)
* will be saved as "updated" in the result.
*
* Must mark all found remote resources as "present remotely", so that a later execution of
* [deleteNotPresentRemotely] doesn't (locally) delete any currently available remote resources.
*
* @param remoteResources map of remote resource names and resources
*
* @return List of updated resources on the server. The "deleted" list remains empty. Sync
* state will be null.
*/
protected abstract fun compareLocalRemote(remoteResources: Map<String, DavResponse>): RemoteChanges
// download queue
val toDownload = LinkedBlockingQueue<HttpUrl>()
/**
* Lists remote changes (incremental sync).
*
* Must mark all found remote resources as "present remotely", so that a later execution of
* [deleteNotPresentRemotely] doesn't (locally) delete any currently available remote resources.
*
* @return list of of remote changes together with the sync state after those changes
*/
protected abstract fun listRemoteChanges(state: SyncState?): RemoteChanges
// tasks from this executor create the download tasks (if necessary)
val processor = ThreadPoolExecutor(1, MAX_PROCESSING_THREADS,
10, TimeUnit.SECONDS,
LinkedBlockingQueue(MAX_PROCESSING_THREADS), // accept up to MAX_PROCESSING_THREADS processing tasks
ThreadPoolExecutor.CallerRunsPolicy() // if the queue is full, run task in submitting thread
)
/**
* Processes remote changes:
* + downloads and locally saves remotely updated resources
* + locally deletes remotely deleted resources
*
* Should call [abortIfCancelled] from time to time, for instance
* after downloading a resource.
*
* Must mark downloaded resources as present on server.
*
* @param changes list of remotely updated and deleted resources
*/
protected abstract fun processRemoteChanges(changes: RemoteChanges)
// this executor runs the actual download tasks
val downloader = ThreadPoolExecutor(0, MAX_DOWNLOAD_THREADS,
10, TimeUnit.SECONDS,
LinkedBlockingQueue(MAX_DOWNLOAD_THREADS), // accept up to MAX_DOWNLOAD_THREADS download tasks
ThreadPoolExecutor.CallerRunsPolicy() // if the queue is full, run task in submitting thread
)
fun downloadBunch() {
val bunch = LinkedList<HttpUrl>()
toDownload.drainTo(bunch, MAX_MULTIGET_RESOURCES)
results += downloader.submit {
downloadRemote(bunch)
}
}
listRemote { response, relation ->
// ignore non-members
if (relation != Response.HrefRelation.MEMBER)
return@listRemote
// ignore collections
if (response[at.bitfire.dav4android.property.ResourceType::class.java]?.types?.contains(at.bitfire.dav4android.property.ResourceType.COLLECTION) == true)
return@listRemote
val name = response.hrefName()
if (response.isSuccess()) {
Logger.log.fine("Found remote resource: $name")
results += processor.submit {
useLocal(localCollection.findByName(name)) { local ->
if (local == null) {
Logger.log.info("$name has been added remotely")
toDownload += response.href
nInserted.incrementAndGet()
} else {
val localETag = local.eTag
val remoteETag = response[GetETag::class.java]?.eTag ?: throw DavException("Server didn't provide ETag")
if (localETag == remoteETag) {
Logger.log.info("$name has not been changed on server (ETag still $remoteETag)")
nSkipped.incrementAndGet()
} else {
Logger.log.info("$name has been changed on server (current ETag=$remoteETag, last known ETag=$localETag)")
toDownload += response.href
nUpdated.incrementAndGet()
}
// mark as remotely present, so that this resource won't be deleted at the end
local.updateFlags(LocalResource.FLAG_REMOTELY_PRESENT)
}
}
synchronized(processor) {
if (toDownload.size >= MAX_MULTIGET_RESOURCES)
// download another bunch of MAX_MULTIGET_RESOURCES resources
downloadBunch()
}
}
} else if (response.status?.code == HttpURLConnection.HTTP_NOT_FOUND) {
// collection sync: resource has been deleted on remote server
results += processor.submit {
useLocal(localCollection.findByName(name)) { local ->
Logger.log.info("$name has been deleted on server, deleting locally")
local?.delete()
nDeleted.incrementAndGet()
}
}
}
// check already available results for exceptions so that they don't become too many
checkResults(results)
}
// process remaining responses
processor.shutdown()
processor.awaitTermination(5, TimeUnit.MINUTES)
// download remaining resources
if (toDownload.isNotEmpty())
downloadBunch()
// signal end of queue and wait for download thread
downloader.shutdown()
downloader.awaitTermination(5, TimeUnit.MINUTES)
// check remaining results for exceptions
checkResults(results)
// update sync stats
with(syncResult.stats) {
numInserts += nInserted.get()
numUpdates += nUpdated.get()
numDeletes += nDeleted.get()
numSkippedEntries += nSkipped.get()
}
}
protected abstract fun listAllRemote(callback: DavResponseCallback)
protected open fun listRemoteChanges(syncState: SyncState?, callback: DavResponseCallback): Pair<SyncToken, Boolean> {
var furtherResults = false
val report = davCollection.reportChanges(
syncState?.takeIf { syncState.type == SyncState.Type.SYNC_TOKEN }?.value,
false, null,
GetETag.NAME) { response, relation ->
when (relation) {
Response.HrefRelation.SELF ->
furtherResults = response.status?.code == 507
Response.HrefRelation.MEMBER ->
callback(response, relation)
else ->
Logger.log.fine("Unexpected sync-collection response: $response")
}
}
var syncToken: SyncToken? = null
report.filterIsInstance(SyncToken::class.java).firstOrNull()?.let {
syncToken = it
}
if (syncToken == null)
throw DavException("Received sync-collection response without sync-token")
return Pair(syncToken!!, furtherResults)
}
protected abstract fun downloadRemote(bunch: List<HttpUrl>)
/**
* Locally deletes entries which are
@@ -357,7 +563,11 @@ abstract class SyncManager<out ResourceType: LocalResource<*>, out CollectionTyp
* Used together with [resetPresentRemotely] when a full listing has been received from
* the server to locally delete resources which are not present remotely (anymore).
*/
protected abstract fun deleteNotPresentRemotely()
protected open fun deleteNotPresentRemotely(): Int {
val removed = localCollection.removeNotDirtyMarked(0)
Logger.log.info("Removed $removed local resources which are not present on the server anymore")
return removed
}
/**
* Post-processing of synchronized entries, for instance contact group membership operations.
@@ -365,6 +575,8 @@ abstract class SyncManager<out ResourceType: LocalResource<*>, out CollectionTyp
protected abstract fun postProcess()
// sync helpers
/**
* Throws an [InterruptedException] if the current thread has been interrupted,
* most probably because synchronization was cancelled by the user.
@@ -376,7 +588,27 @@ abstract class SyncManager<out ResourceType: LocalResource<*>, out CollectionTyp
throw InterruptedException("Sync was cancelled")
}
private fun notifyException(e: Throwable) {
protected fun syncState(dav: Response) =
dav[SyncToken::class.java]?.token?.let {
SyncState(SyncState.Type.SYNC_TOKEN, it)
} ?:
dav[GetCTag::class.java]?.cTag?.let {
SyncState(SyncState.Type.CTAG, it)
}
private fun querySyncState(): SyncState? {
var state: SyncState? = null
davCollection.propfind(0, GetCTag.NAME, SyncToken.NAME) { response, relation ->
if (relation == Response.HrefRelation.SELF)
state = syncState(response)
}
return state
}
// exception helpers
private fun notifyException(e: Throwable, local: ResourceType?, remote: HttpUrl?) {
val message: String
when (e) {
@@ -416,20 +648,20 @@ abstract class SyncManager<out ResourceType: LocalResource<*>, out CollectionTyp
} else {
contentIntent = Intent(context, DebugInfoActivity::class.java)
contentIntent.putExtra(DebugInfoActivity.KEY_THROWABLE, e)
contentIntent.putExtra(DebugInfoActivity.KEY_ACCOUNT, account)
contentIntent.putExtra(DebugInfoActivity.KEY_AUTHORITY, authority)
// use current local/remote resource
currentLocalResource.firstOrNull()?.let { local ->
if (local != null) {
// pass local resource info to debug info
contentIntent.putExtra(DebugInfoActivity.KEY_LOCAL_RESOURCE, local.toString())
// generate "view item" action
viewItemAction = buildViewItemAction(local)
}
currentRemoteResource.firstOrNull()?.let { remote ->
if (remote != null)
contentIntent.putExtra(DebugInfoActivity.KEY_REMOTE_RESOURCE, remote.toString())
}
}
// to make the PendingIntent unique
@@ -487,7 +719,7 @@ abstract class SyncManager<out ResourceType: LocalResource<*>, out CollectionTyp
PendingIntent.getService(context, 0, retryIntent, PendingIntent.FLAG_UPDATE_CURRENT))
}
private fun buildViewItemAction(local: LocalResource<*>): NotificationCompat.Action? {
private fun buildViewItemAction(local: ResourceType): NotificationCompat.Action? {
Logger.log.log(Level.FINE, "Adding view action for local resource", local)
val intent = local.id?.let { id ->
when (local) {
@@ -508,20 +740,88 @@ abstract class SyncManager<out ResourceType: LocalResource<*>, out CollectionTyp
null
}
enum class SyncAlgorithm {
PROPFIND_REPORT,
COLLECTION_SYNC
fun checkResults(results: MutableCollection<Future<*>>) {
val iter = results.iterator()
while (iter.hasNext()) {
val result = iter.next()
if (result.isDone) {
try {
result.get()
} catch(e: ExecutionException) {
throw e.cause!!
}
iter.remove()
}
}
}
class RemoteChanges(
val state: SyncState?,
val furtherChanges: Boolean
) {
val deleted = LinkedList<String>()
val updated = LinkedList<DavResponse>()
override fun toString() = MiscUtils.reflectionToString(this)
protected fun<T: ResourceType?, R> useLocal(local: T, body: (T) -> R): R {
try {
return body(local)
} catch (e: ContextedException) {
e.addContextValue(Constants.EXCEPTION_CONTEXT_LOCAL_RESOURCE, local)
throw e
} catch (e: Throwable) {
if (local != null)
throw ContextedException(e).setContextValue(Constants.EXCEPTION_CONTEXT_LOCAL_RESOURCE, local)
else
throw e
}
}
protected fun<T: DavResource, R> useRemote(remote: T, body: (T) -> R): R {
try {
return body(remote)
} catch (e: ContextedException) {
e.addContextValue(Constants.EXCEPTION_CONTEXT_REMOTE_RESOURCE, remote.location)
throw e
} catch(e: Throwable) {
throw ContextedException(e).setContextValue(Constants.EXCEPTION_CONTEXT_REMOTE_RESOURCE, remote.location)
}
}
protected fun<T> useRemote(remote: Response, body: (Response) -> T): T {
try {
return body(remote)
} catch (e: ContextedException) {
e.addContextValue(Constants.EXCEPTION_CONTEXT_REMOTE_RESOURCE, remote.href)
throw e
} catch (e: Throwable) {
throw ContextedException(e).setContextValue(Constants.EXCEPTION_CONTEXT_REMOTE_RESOURCE, remote.href)
}
}
protected fun<R> useRemoteCollection(body: (RemoteType) -> R) =
useRemote(davCollection, body)
private fun unwrapExceptions(body: () -> Unit, handler: (e: Throwable, local: ResourceType?, remote: HttpUrl?) -> Unit) {
var ex: Throwable? = null
try {
body()
} catch(e: Throwable) {
ex = e
}
var local: ResourceType? = null
var remote: HttpUrl? = null
if (ex is ContextedException) {
@Suppress("UNCHECKED_CAST")
// we want the innermost context value, which is the first one
(ex.getFirstContextValue(Constants.EXCEPTION_CONTEXT_LOCAL_RESOURCE) as? ResourceType)?.let {
if (local == null)
local = it
}
(ex.getFirstContextValue(Constants.EXCEPTION_CONTEXT_REMOTE_RESOURCE) as? HttpUrl)?.let {
if (remote == null)
remote = it
}
ex = ex.cause
}
if (ex != null)
handler(ex, local, remote)
}
}

View File

@@ -14,14 +14,17 @@ import android.content.SyncResult
import android.os.Bundle
import at.bitfire.dav4android.DavCalendar
import at.bitfire.dav4android.DavResource
import at.bitfire.dav4android.DavResponse
import at.bitfire.dav4android.DavResponseCallback
import at.bitfire.dav4android.Response
import at.bitfire.dav4android.exception.DavException
import at.bitfire.dav4android.property.CalendarData
import at.bitfire.dav4android.property.GetCTag
import at.bitfire.dav4android.property.GetETag
import at.bitfire.dav4android.property.SyncToken
import at.bitfire.davdroid.AccountSettings
import at.bitfire.davdroid.DavUtils
import at.bitfire.davdroid.log.Logger
import at.bitfire.davdroid.model.SyncState
import at.bitfire.davdroid.resource.LocalResource
import at.bitfire.davdroid.resource.LocalTask
import at.bitfire.davdroid.resource.LocalTaskList
@@ -33,7 +36,6 @@ import okhttp3.RequestBody
import java.io.ByteArrayOutputStream
import java.io.Reader
import java.io.StringReader
import java.util.*
import java.util.logging.Level
/**
@@ -48,19 +50,10 @@ class TasksSyncManager(
authority: String,
syncResult: SyncResult,
localCollection: LocalTaskList
): BaseDavSyncManager<LocalTask, LocalTaskList, DavCalendar>(context, settings, account, accountSettings, extras, authority, syncResult, localCollection) {
companion object {
const val MULTIGET_MAX_RESOURCES = 30
}
): SyncManager<LocalTask, LocalTaskList, DavCalendar>(context, settings, account, accountSettings, extras, authority, syncResult, localCollection) {
override fun prepare(): Boolean {
if (!super.prepare())
return false
val url = localCollection.syncId ?: return false
collectionURL = HttpUrl.parse(url) ?: return false
collectionURL = HttpUrl.parse(localCollection.syncId ?: return false) ?: return false
davCollection = DavCalendar(httpClient.okHttpClient, collectionURL)
return true
@@ -68,14 +61,17 @@ class TasksSyncManager(
override fun queryCapabilities() =
useRemoteCollection {
it.propfind(0, GetCTag.NAME, SyncToken.NAME).use { dav ->
syncState(dav)
var syncState: SyncState? = null
it.propfind(0, GetCTag.NAME, SyncToken.NAME) { response, relation ->
if (relation == Response.HrefRelation.SELF)
syncState = syncState(response)
}
syncState
}
override fun syncAlgorithm() = SyncAlgorithm.PROPFIND_REPORT
override fun prepareUpload(resource: LocalTask): RequestBody = useLocal(resource, {
override fun prepareUpload(resource: LocalTask): RequestBody = useLocal(resource) {
val task = requireNotNull(resource.task)
Logger.log.log(Level.FINE, "Preparing upload of task ${resource.fileName}", task)
@@ -86,68 +82,54 @@ class TasksSyncManager(
DavCalendar.MIME_ICALENDAR_UTF8,
os.toByteArray()
)
})
}
override fun listAllRemote() = useRemoteCollection { remote ->
remote.calendarQuery("VTODO", null, null).use { dav ->
val result = LinkedHashMap<String, DavResponse>(dav.members.size)
for (vCard in dav.members) {
val fileName = vCard.fileName()
Logger.log.fine("Found remote VTODO: $fileName")
result[fileName] = vCard
}
result
override fun listAllRemote(callback: DavResponseCallback) {
useRemoteCollection { remote ->
Logger.log.info("Querying tasks")
remote.calendarQuery("VTODO", null, null, callback)
}
}
override fun processRemoteChanges(changes: RemoteChanges) {
for (name in changes.deleted) {
localCollection.findByName(name)?.let {
Logger.log.info("Deleting local task $name")
useLocal(it, { it.delete() })
syncResult.stats.numDeletes++
}
}
override fun downloadRemote(bunch: List<HttpUrl>) {
Logger.log.info("Downloading ${bunch.size} iCalendars: $bunch")
if (bunch.size == 1) {
val remote = bunch.first()
// only one contact, use GET
useRemote(DavResource(httpClient.okHttpClient, remote)) { resource ->
resource.get(DavCalendar.MIME_ICALENDAR.toString()) { response ->
// CalDAV servers MUST return ETag on GET [https://tools.ietf.org/html/rfc4791#section-5.3.4]
val eTag = response.header("ETag")?.let { GetETag(it).eTag }
?: throw DavException("Received CalDAV GET response without ETag")
val toDownload = changes.updated.map { it.url }
Logger.log.info("Downloading ${toDownload.size} resources ($MULTIGET_MAX_RESOURCES at once)")
for (bunch in toDownload.chunked(MULTIGET_MAX_RESOURCES)) {
if (bunch.size == 1)
// only one contact, use GET
useRemote(DavResource(httpClient.okHttpClient, bunch.first()), { remote ->
remote.get(DavCalendar.MIME_ICALENDAR.toString()).use { dav ->
// CalDAV servers MUST return ETag on GET [https://tools.ietf.org/html/rfc4791#section-5.3.4]
val eTag = dav[GetETag::class.java]?.eTag
?: throw DavException("Received CalDAV GET response without ETag for ${remote.location}")
dav.body?.charStream()?.use { reader ->
processVTodo(remote.fileName(), eTag, reader)
}
response.body()!!.use {
processVTodo(resource.fileName(), eTag, it.charStream())
}
})
else {
// multiple contacts, use multi-get
davCollection.multiget(bunch).use { dav ->
// process multiget results
for (remote in dav.members)
useRemote(remote, {
val eTag = remote[GetETag::class.java]?.eTag
?: throw DavException("Received multi-get response without ETag")
val calendarData = remote[CalendarData::class.java]
val iCalendar = calendarData?.iCalendar
?: throw DavException("Received multi-get response without task data")
processVTodo(remote.fileName(), eTag, StringReader(iCalendar))
})
}
}
} else
// multiple iCalendars, use calendar-multi-get
useRemoteCollection {
it.multiget(bunch) { response, _ ->
useRemote(response) {
val eTag = response[GetETag::class.java]?.eTag
?: throw DavException("Received multi-get response without ETag")
abortIfCancelled()
}
val calendarData = response[CalendarData::class.java]
val iCal = calendarData?.iCalendar
?: throw DavException("Received multi-get response without address data")
processVTodo(DavUtils.lastSegmentOfUrl(response.href), eTag, StringReader(iCal))
}
}
}
}
override fun postProcess() {
}
// helpers
private fun processVTodo(fileName: String, eTag: String, reader: Reader) {
val tasks: List<Task>
try {
@@ -161,7 +143,7 @@ class TasksSyncManager(
val newData = tasks.first()
// update local task, if it exists
useLocal(localCollection.findByName(fileName), { local ->
useLocal(localCollection.findByName(fileName)) { local ->
if (local != null) {
Logger.log.info("Updating $fileName in local task list")
local.eTag = eTag
@@ -169,12 +151,12 @@ class TasksSyncManager(
syncResult.stats.numUpdates++
} else {
Logger.log.info("Adding $fileName to local task list")
useLocal(LocalTask(localCollection, newData, fileName, eTag, LocalResource.FLAG_REMOTELY_PRESENT), {
useLocal(LocalTask(localCollection, newData, fileName, eTag, LocalResource.FLAG_REMOTELY_PRESENT)) {
it.add()
})
}
syncResult.stats.numInserts++
}
})
}
} else
Logger.log.info("Received VCALENDAR with not exactly one VTODO; ignoring $fileName")
}

View File

@@ -8,83 +8,55 @@
package at.bitfire.davdroid.ui
import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.support.v4.app.Fragment
import android.support.v4.app.FragmentManager
import android.support.v4.app.FragmentPagerAdapter
import android.support.v4.app.LoaderManager
import android.support.v4.content.AsyncTaskLoader
import android.support.v4.content.Loader
import android.support.v7.app.AppCompatActivity
import android.text.Html
import android.text.Spanned
import android.text.util.Linkify
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.*
import at.bitfire.davdroid.App
import at.bitfire.davdroid.BuildConfig
import at.bitfire.davdroid.R
import at.bitfire.davdroid.log.Logger
import ezvcard.Ezvcard
import kotlinx.android.synthetic.main.about_component.view.*
import com.mikepenz.aboutlibraries.LibsBuilder
import kotlinx.android.synthetic.main.about_davdroid.*
import kotlinx.android.synthetic.main.activity_about.*
import org.apache.commons.io.IOUtils
import java.io.IOException
import java.text.SimpleDateFormat
import java.util.*
import java.util.logging.Level
class AboutActivity: AppCompatActivity() {
private class ComponentInfo(
val title: String?,
val version: String?,
val website: String,
val copyright: String,
val licenseInfo: Int?,
val licenseTextFile: String?
)
companion object {
private lateinit var components: Array<ComponentInfo>
const val pixelsHtml = "<font color=\"#fff433\">■</font>" +
"<font color=\"#ffffff\">■</font>" +
"<font color=\"#9b59d0\">■</font>" +
"<font color=\"#000000\">■</font>"
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
components = arrayOf(
ComponentInfo(
null, BuildConfig.VERSION_NAME, getString(R.string.homepage_url),
"Ricki Hirner, Bernhard Stockmann (bitfire web engineering)",
null, null
), ComponentInfo(
"AmbilWarna", null, "https://github.com/yukuku/ambilwarna",
"Yuku", R.string.about_license_info_no_warranty, "apache2.html"
), ComponentInfo(
"Apache Commons", null, "http://commons.apache.org/",
"Apache Software Foundation", R.string.about_license_info_no_warranty, "apache2.html"
), ComponentInfo(
"dnsjava", null, "http://dnsjava.org/",
"Brian Wellington", R.string.about_license_info_no_warranty, "bsd.html"
), ComponentInfo(
"ez-vcard", Ezvcard.VERSION, "https://github.com/mangstadt/ez-vcard",
"Michael Angstadt", R.string.about_license_info_no_warranty, "bsd.html"
), ComponentInfo(
"ical4j", at.bitfire.ical4android.Constants.ical4jVersion, "https://ical4j.github.io/",
"Ben Fortuna", R.string.about_license_info_no_warranty, "bsd-3clause.html"
), ComponentInfo(
"OkHttp", at.bitfire.dav4android.Constants.okHttpVersion, "https://square.github.io/okhttp/",
"Square, Inc.", R.string.about_license_info_no_warranty, "apache2.html"
)
)
setContentView(R.layout.activity_about)
setSupportActionBar(toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
viewpager.adapter = TabsAdapter(supportFragmentManager)
tabs.setupWithViewPager(viewpager)
tabs.setupWithViewPager(viewpager, false)
}
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.about_davdroid, menu)
return true
}
fun showWebsite(item: MenuItem) {
val intent = Intent(Intent.ACTION_VIEW, App.homepageUrl(this))
if (intent.resolveActivity(packageManager) != null)
startActivity(intent)
}
@@ -92,118 +64,59 @@ class AboutActivity: AppCompatActivity() {
fm: FragmentManager
): FragmentPagerAdapter(fm) {
override fun getCount() = components.size
override fun getCount() = 2
override fun getPageTitle(position: Int) =
components[position].title ?: getString(R.string.app_name)!!
when (position) {
1 -> getString(R.string.about_libraries)
else -> getString(R.string.app_name)
}!!
override fun getItem(position: Int) = ComponentFragment.instantiate(position)
override fun getItem(position: Int) =
when (position) {
1 -> LibsBuilder()
.withAutoDetect(false)
.withFields(R.string::class.java.fields)
.withLicenseShown(true)
.supportFragment()
else -> DavdroidFragment()
}!!
}
class ComponentFragment: Fragment(), LoaderManager.LoaderCallbacks<Spanned> {
class DavdroidFragment: Fragment() {
companion object {
const val KEY_POSITION = "position"
const val KEY_FILE_NAME = "fileName"
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?) =
inflater.inflate(R.layout.about_davdroid, container, false)!!
fun instantiate(position: Int): ComponentFragment {
val frag = ComponentFragment()
val args = Bundle(1)
args.putInt(KEY_POSITION, position)
frag.arguments = args
return frag
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
app_name.text = getString(R.string.app_name)
app_version.text = getString(R.string.about_version, BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE)
build_time.text = getString(R.string.about_build_date, SimpleDateFormat.getDateInstance().format(BuildConfig.buildTime))
private val licenseFragmentLoader = ServiceLoader.load(ILicenseFragment::class.java)!!.firstOrNull()
pixels.text = Html.fromHtml(pixelsHtml)
if (false /* open-source version */) {
warranty.text = Html.fromHtml(getString(R.string.about_license_info_no_warranty))
license_text.text = Html.fromHtml(getString(R.string.gpl_v3))
} else /* non-ose builds */ {
when (BuildConfig.FLAVOR) {
App.FLAVOR_GOOGLE_PLAY,
App.FLAVOR_ICLOUD,
App.FLAVOR_SOLDUPE ->
warranty.setText(R.string.about_flavor_info)
else ->
warranty.visibility = View.GONE
}
@SuppressLint("SetTextI18n")
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
val info = (activity as AboutActivity).components[arguments!!.getInt(KEY_POSITION)]
val v = inflater.inflate(R.layout.about_component, container, false)
var title = info.title ?: getString(R.string.app_name)
info.version?.let { title += " ${info.version}" }
v.title.text = title
v.website.autoLinkMask = Linkify.WEB_URLS
v.website.text = info.website
v.copyright.text = "© ${info.copyright}"
if (info.licenseInfo == null && info.licenseTextFile == null) {
// No license text, so this must be the app's tab. Show the license fragment here, if available.
licenseFragmentLoader?.let { factory ->
fragmentManager!!.beginTransaction()
.add(R.id.license_fragment, factory.getFragment())
val licenseFragment = ServiceLoader.load(ILicenseFragment::class.java)!!.firstOrNull()
if (savedInstanceState == null && licenseFragment != null)
requireFragmentManager().beginTransaction()
.replace(R.id.license_fragment, licenseFragment.getFragment())
.commit()
}
v.license_terms.visibility = View.GONE
} else {
// show license info
if (info.licenseInfo == null)
v.license_info.visibility = View.GONE
else
v.license_info.setText(info.licenseInfo)
// load and format license text
if (info.licenseTextFile == null) {
v.license_header.visibility = View.GONE
v.license_text.visibility = View.GONE
} else {
val args = Bundle(1)
args.putString(KEY_FILE_NAME, info.licenseTextFile)
loaderManager.initLoader(0, args, this)
}
}
return v
}
override fun onCreateLoader(id: Int, args: Bundle?) =
LicenseLoader(activity!!, args!!.getString(KEY_FILE_NAME))
override fun onLoadFinished(loader: Loader<Spanned>, license: Spanned?) {
view?.let { v ->
v.license_text.autoLinkMask = Linkify.EMAIL_ADDRESSES or Linkify.WEB_URLS
v.license_text.text = license
}
}
override fun onLoaderReset(loader: Loader<Spanned>) {}
}
class LicenseLoader(
context: Context,
val fileName: String
): AsyncTaskLoader<Spanned>(context) {
private var content: Spanned? = null
override fun onStartLoading() {
if (content != null)
deliverResult(content)
else
forceLoad()
}
override fun loadInBackground(): Spanned? {
Logger.log.fine("Loading license file $fileName")
try {
context.resources.assets.open(fileName).use {
content = Html.fromHtml(IOUtils.toString(it, Charsets.UTF_8))
return content
}
} catch(e: IOException) {
Logger.log.log(Level.SEVERE, "Couldn't read license file", e)
return null
}
}
}

View File

@@ -18,12 +18,12 @@ import android.content.pm.PackageManager
import android.database.DatabaseUtils
import android.database.sqlite.SQLiteDatabase
import android.net.Uri
import android.os.*
import android.os.Build
import android.os.Bundle
import android.os.IBinder
import android.provider.CalendarContract
import android.provider.ContactsContract
import android.support.design.widget.Snackbar
import android.support.test.espresso.IdlingRegistry
import android.support.test.espresso.IdlingResource
import android.support.v4.app.ActivityCompat
import android.support.v4.app.DialogFragment
import android.support.v4.content.ContextCompat
@@ -32,7 +32,6 @@ import android.support.v7.app.AppCompatActivity
import android.support.v7.widget.Toolbar
import android.view.*
import android.widget.*
import at.bitfire.davdroid.BuildConfig
import at.bitfire.davdroid.DavService
import at.bitfire.davdroid.R
import at.bitfire.davdroid.log.Logger
@@ -46,6 +45,7 @@ import at.bitfire.ical4android.TaskProvider
import kotlinx.android.synthetic.main.account_caldav_item.view.*
import kotlinx.android.synthetic.main.activity_account.*
import java.util.*
import java.util.concurrent.Executors
import java.util.logging.Level
class AccountActivity: AppCompatActivity(), Toolbar.OnMenuItemClickListener, PopupMenu.OnMenuItemClickListener, LoaderManager.LoaderCallbacks<AccountActivity.AccountInfo> {
@@ -73,7 +73,7 @@ class AccountActivity: AppCompatActivity(), Toolbar.OnMenuItemClickListener, Pop
lateinit var account: Account
private var accountInfo: AccountInfo? = null
private var isActiveIdlingResource: IsActiveIdlingResource? = null
private val dbExecutor = Executors.newSingleThreadExecutor()
override fun onCreate(savedInstanceState: Bundle?) {
@@ -108,18 +108,12 @@ class AccountActivity: AppCompatActivity(), Toolbar.OnMenuItemClickListener, Pop
// load CardDAV/CalDAV collections
loaderManager.initLoader(0, null, this)
// register Espresso idling resource
if (BuildConfig.DEBUG) {
isActiveIdlingResource = IsActiveIdlingResource()
IdlingRegistry.getInstance().register(isActiveIdlingResource)
}
}
override fun onDestroy() {
super.onDestroy()
if (BuildConfig.DEBUG)
IdlingRegistry.getInstance().unregister(isActiveIdlingResource)
dbExecutor.shutdown()
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
@@ -157,9 +151,9 @@ class AccountActivity: AppCompatActivity(), Toolbar.OnMenuItemClickListener, Pop
.setTitle(R.string.account_delete_confirmation_title)
.setMessage(R.string.account_delete_confirmation_text)
.setNegativeButton(android.R.string.no, null)
.setPositiveButton(android.R.string.yes, { _, _ ->
.setPositiveButton(android.R.string.yes) { _, _ ->
deleteAccount()
})
}
.show()
}
else ->
@@ -208,20 +202,24 @@ class AccountActivity: AppCompatActivity(), Toolbar.OnMenuItemClickListener, Pop
val info = adapter.getItem(position)
val nowChecked = !info.selected
OpenHelper(this).use { dbHelper ->
val db = dbHelper.writableDatabase
db.beginTransactionNonExclusive()
dbExecutor.execute {
OpenHelper(this).use { dbHelper ->
val db = dbHelper.writableDatabase
db.beginTransactionNonExclusive()
val values = ContentValues(1)
values.put(Collections.SYNC, if (nowChecked) 1 else 0)
db.update(Collections._TABLE, values, "${Collections.ID}=?", arrayOf(info.id.toString()))
val values = ContentValues(1)
values.put(Collections.SYNC, if (nowChecked) 1 else 0)
db.update(Collections._TABLE, values, "${Collections.ID}=?", arrayOf(info.id.toString()))
db.setTransactionSuccessful()
db.endTransaction()
db.setTransactionSuccessful()
db.endTransaction()
}
info.selected = nowChecked
runOnUiThread {
adapter.notifyDataSetChanged()
}
}
info.selected = nowChecked
adapter.notifyDataSetChanged()
}
private val onActionOverflowListener = { anchor: View, info: CollectionInfo ->
@@ -235,28 +233,32 @@ class AccountActivity: AppCompatActivity(), Toolbar.OnMenuItemClickListener, Pop
isChecked = info.forceReadOnly
}
popup.setOnMenuItemClickListener({ item ->
popup.setOnMenuItemClickListener { item ->
when (item.itemId) {
R.id.force_read_only -> {
val nowChecked = !item.isChecked
OpenHelper(this).use { dbHelper ->
val db = dbHelper.writableDatabase
db.beginTransactionNonExclusive()
dbExecutor.execute {
OpenHelper(this).use { dbHelper ->
val db = dbHelper.writableDatabase
db.beginTransactionNonExclusive()
val values = ContentValues(1)
values.put(Collections.FORCE_READ_ONLY, nowChecked)
db.update(Collections._TABLE, values, "${Collections.ID}=?", arrayOf(info.id.toString()))
val values = ContentValues(1)
values.put(Collections.FORCE_READ_ONLY, nowChecked)
db.update(Collections._TABLE, values, "${Collections.ID}=?", arrayOf(info.id.toString()))
db.setTransactionSuccessful()
db.endTransaction()
reload()
db.setTransactionSuccessful()
db.endTransaction()
reload()
}
}
}
R.id.delete_collection ->
DeleteCollectionFragment.ConfirmDeleteCollectionFragment.newInstance(account, info).show(supportFragmentManager, null)
R.id.properties ->
CollectionInfoFragment.newInstance(info).show(supportFragmentManager, null)
}
true
})
}
popup.show()
// long click was handled
@@ -285,9 +287,9 @@ class AccountActivity: AppCompatActivity(), Toolbar.OnMenuItemClickListener, Pop
val installIntent = Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=at.bitfire.icsdroid"))
if (packageManager.resolveActivity(installIntent, 0) != null)
snack.setAction(R.string.account_install_icsdroid, {
snack.setAction(R.string.account_install_icsdroid) {
startActivity(installIntent)
})
}
snack.show()
}
@@ -331,6 +333,10 @@ class AccountActivity: AppCompatActivity(), Toolbar.OnMenuItemClickListener, Pop
override fun onLoadFinished(loader: Loader<AccountInfo>, info: AccountInfo?) {
accountInfo = info
if (info?.caldav?.collections?.any { it.selected } != true &&
info?.carddav?.collections?.any { it.selected} != true)
select_collections_hint.visibility = View.VISIBLE
carddav.visibility = info?.carddav?.let { carddav ->
carddav_refreshing.visibility = if (carddav.refreshing) View.VISIBLE else View.GONE
@@ -377,12 +383,6 @@ class AccountActivity: AppCompatActivity(), Toolbar.OnMenuItemClickListener, Pop
View.GONE
} ?: View.GONE
// set idle state for UI tests
if (BuildConfig.DEBUG && isActiveIdlingResource!!.isIdleNow)
Handler(Looper.getMainLooper()).post {
isActiveIdlingResource!!.callback?.onTransitionToIdle()
}
// ask for permissions
val requiredPermissions = mutableSetOf<String>()
info?.carddav?.let { carddav ->
@@ -590,11 +590,11 @@ class AccountActivity: AppCompatActivity(), Toolbar.OnMenuItemClickListener, Pop
v.findViewById<ImageView>(R.id.read_only).visibility =
if (info.readOnly || info.forceReadOnly) View.VISIBLE else View.GONE
v.findViewById<ImageView>(R.id.action_overflow).setOnClickListener({ view ->
v.findViewById<ImageView>(R.id.action_overflow).setOnClickListener { view ->
(context as? AccountActivity)?.let {
it.onActionOverflowListener(view, info)
}
})
}
return v
}
@@ -644,11 +644,11 @@ class AccountActivity: AppCompatActivity(), Toolbar.OnMenuItemClickListener, Pop
if (info.type == CollectionInfo.Type.WEBCAL)
overflow.visibility = View.GONE
else
overflow.setOnClickListener({ view ->
overflow.setOnClickListener { view ->
(context as? AccountActivity)?.let {
it.onActionOverflowListener(view, info)
}
})
}
return v
}
@@ -700,31 +700,29 @@ class AccountActivity: AppCompatActivity(), Toolbar.OnMenuItemClickListener, Pop
ContentResolver.cancelSync(addrBookAccount, null)
// update account name references in database
OpenHelper(activity!!).use { dbHelper ->
OpenHelper(requireActivity()).use { dbHelper ->
ServiceDB.onRenameAccount(dbHelper.writableDatabase, oldAccount.name, newName)
}
// update main account of address book accounts
try {
for (addrBookAccount in accountManager.getAccountsByType(getString(R.string.account_type_address_book))) {
val provider = activity!!.contentResolver.acquireContentProviderClient(ContactsContract.AUTHORITY)
try {
if (provider != null) {
val addressBook = LocalAddressBook(activity!!, addrBookAccount, provider)
if (oldAccount == addressBook.mainAccount)
addressBook.mainAccount = Account(newName, oldAccount.type)
}
} finally {
if (Build.VERSION.SDK_INT >= 24)
provider?.close()
else
provider?.release()
if (ContextCompat.checkSelfPermission(requireActivity(), Manifest.permission.WRITE_CONTACTS) == PackageManager.PERMISSION_GRANTED)
try {
requireActivity().contentResolver.acquireContentProviderClient(ContactsContract.AUTHORITY)?.let { provider ->
for (addrBookAccount in accountManager.getAccountsByType(getString(R.string.account_type_address_book)))
try {
val addressBook = LocalAddressBook(requireActivity(), addrBookAccount, provider)
if (oldAccount == addressBook.mainAccount)
addressBook.mainAccount = Account(newName, oldAccount.type)
} finally {
if (Build.VERSION.SDK_INT >= 24)
provider.close()
else
provider.release()
}
}
} catch(e: Exception) {
Logger.log.log(Level.SEVERE, "Couldn't update address book accounts", e)
}
} catch(e: Exception) {
Logger.log.log(Level.SEVERE, "Couldn't update address book accounts", e)
}
// calendar provider doesn't allow changing account_name of Events
// (all events will have to be downloaded again)
@@ -741,7 +739,7 @@ class AccountActivity: AppCompatActivity(), Toolbar.OnMenuItemClickListener, Pop
}, null)
activity!!.finish()
})
.setNegativeButton(android.R.string.cancel, { _, _ -> })
.setNegativeButton(android.R.string.cancel) { _, _ -> }
.create()
}
}
@@ -777,25 +775,4 @@ class AccountActivity: AppCompatActivity(), Toolbar.OnMenuItemClickListener, Pop
Snackbar.make(findViewById(R.id.parent), R.string.account_synchronizing_now, Snackbar.LENGTH_LONG).show()
}
/**
* For Espresso tests. Is idle when the CalDAV/CardDAV cards are either invisible or
* there's no more progress bar.
*/
inner class IsActiveIdlingResource: IdlingResource {
var callback: IdlingResource.ResourceCallback? = null
override fun getName() = "CalDAV/CardDAV activity (progress bar)"
override fun registerIdleTransitionCallback(callback: IdlingResource.ResourceCallback) {
this.callback = callback
}
override fun isIdleNow() =
(carddav.visibility == View.GONE || carddav_refreshing.visibility == View.GONE) &&
(caldav.visibility == View.GONE || caldav_refreshing.visibility == View.GONE)
}
}

View File

@@ -35,7 +35,9 @@ import at.bitfire.davdroid.AccountSettings
import at.bitfire.davdroid.InvalidAccountException
import at.bitfire.davdroid.R
import at.bitfire.davdroid.model.Credentials
import at.bitfire.davdroid.resource.LocalCalendar
import at.bitfire.davdroid.settings.ISettings
import at.bitfire.ical4android.AndroidCalendar
import at.bitfire.ical4android.TaskProvider
import at.bitfire.vcard4android.GroupMethod
import org.apache.commons.lang3.StringUtils
@@ -128,9 +130,9 @@ class AccountSettingsActivity: AppCompatActivity() {
prefCertAlias.setOnPreferenceClickListener {
KeyChain.choosePrivateKeyAlias(activity, { alias ->
accountSettings.credentials(Credentials(certificateAlias = alias))
Handler(Looper.getMainLooper()).post({
Handler(Looper.getMainLooper()).post {
loaderManager.restartLoader(0, arguments, this)
})
}
}, null, null, null, -1, credentials.certificateAlias)
true
}
@@ -238,7 +240,7 @@ class AccountSettingsActivity: AppCompatActivity() {
.setIcon(R.drawable.ic_error_dark)
.setTitle(R.string.settings_contact_group_method_change)
.setMessage(R.string.settings_contact_group_method_change_reload_contacts)
.setPositiveButton(android.R.string.ok, { _, _ ->
.setPositiveButton(android.R.string.ok) { _, _ ->
// change group method
accountSettings.setGroupMethod(GroupMethod.valueOf(groupMethod as String))
loaderManager.restartLoader(0, arguments, this)
@@ -247,7 +249,7 @@ class AccountSettingsActivity: AppCompatActivity() {
val args = Bundle(1)
args.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true)
ContentResolver.requestSync(account, getString(R.string.address_books_authority), args)
})
}
.setNegativeButton(android.R.string.cancel, null)
.show()
false
@@ -276,6 +278,23 @@ class AccountSettingsActivity: AppCompatActivity() {
-1
}
accountSettings.setTimeRangePastDays(if (days < 0) null else days)
// reset sync state of all calendars in this account to trigger a full sync
if (ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.WRITE_CALENDAR) == PackageManager.PERMISSION_GRANTED) {
requireContext().contentResolver.acquireContentProviderClient(CalendarContract.AUTHORITY)?.let { provider ->
try {
AndroidCalendar.find(account, provider, LocalCalendar.Factory, null, null).forEach { calendar ->
calendar.lastSyncState = null
}
} finally {
if (Build.VERSION.SDK_INT >= 24)
provider.close()
else
provider.release()
}
}
}
loaderManager.restartLoader(0, arguments, this)
false
}
@@ -312,10 +331,10 @@ class AccountSettingsActivity: AppCompatActivity() {
.setTitle(R.string.settings_event_colors)
.setMessage(R.string.settings_event_colors_off_confirm)
.setNegativeButton(android.R.string.cancel, null)
.setPositiveButton(android.R.string.ok, { _, _ ->
.setPositiveButton(android.R.string.ok) { _, _ ->
accountSettings.setEventColors(false)
loaderManager.restartLoader(0, arguments, this)
})
}
.show()
false
}

View File

@@ -50,9 +50,9 @@ class AccountsActivity: AppCompatActivity(), NavigationView.OnNavigationItemSele
setSupportActionBar(toolbar)
fab.setOnClickListener({
fab.setOnClickListener {
startActivity(Intent(this, LoginActivity::class.java))
})
}
val toggle = ActionBarDrawerToggle(
this, drawer_layout, toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close)
@@ -121,9 +121,9 @@ class AccountsActivity: AppCompatActivity(), NavigationView.OnNavigationItemSele
if (!ContentResolver.getMasterSyncAutomatically()) {
val snackbar = Snackbar
.make(findViewById(R.id.coordinator), R.string.accounts_global_sync_disabled, Snackbar.LENGTH_INDEFINITE)
.setAction(R.string.accounts_global_sync_enable, {
ContentResolver.setMasterSyncAutomatically(true)
})
.setAction(R.string.accounts_global_sync_enable) {
ContentResolver.setMasterSyncAutomatically(true)
}
syncStatusSnackbar = snackbar
snackbar.show()
}

View File

@@ -0,0 +1,61 @@
/*
* Copyright © Ricki Hirner (bitfire web engineering).
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Public License v3.0
* which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/gpl.html
*/
package at.bitfire.davdroid.ui
import android.annotation.SuppressLint
import android.app.Dialog
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.net.Uri
import android.os.Bundle
import android.support.v4.app.DialogFragment
import android.support.v7.app.AlertDialog
import android.widget.Toast
import at.bitfire.davdroid.R
import at.bitfire.davdroid.model.CollectionInfo
import kotlinx.android.synthetic.main.collection_properties.view.*
class CollectionInfoFragment : DialogFragment() {
companion object {
private const val ARGS_INFO = "info"
fun newInstance(info: CollectionInfo): CollectionInfoFragment {
val frag = CollectionInfoFragment()
val args = Bundle(1)
args.putParcelable(ARGS_INFO, info)
frag.arguments = args
return frag
}
}
@SuppressLint("InflateParams")
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val info = arguments!![ARGS_INFO] as CollectionInfo
val view = requireActivity().layoutInflater.inflate(R.layout.collection_properties, null)
view.url.text = info.url.toString()
view.url_copy.setOnClickListener {
val clipboard = requireActivity().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
val text = ClipData.newPlainText(info.displayName, info.url.toString())
clipboard.primaryClip = text
Toast.makeText(requireActivity(), R.string.copied_to_clipboard, Toast.LENGTH_LONG).show()
}
return AlertDialog.Builder(requireActivity())
.setTitle(info.displayName)
.setView(view)
.create()
}
}

View File

@@ -192,7 +192,7 @@ class CreateCollectionFragment: DialogFragment(), LoaderManager.LoaderCallbacks<
val collection = DavResource(httpClient.okHttpClient, info.url)
// create collection on remote server
collection.mkCol(writer.toString())
collection.mkCol(writer.toString()) {}
// now insert collection into database:
ServiceDB.OpenHelper(context).use { dbHelper ->

View File

@@ -91,7 +91,7 @@ class DeleteCollectionFragment: DialogFragment(), LoaderManager.LoaderCallbacks<
val collection = DavResource(httpClient.okHttpClient, collectionInfo.url)
// delete collection from server
collection.delete(null)
collection.delete(null) {}
// delete collection locally
ServiceDB.OpenHelper(context).use { dbHelper ->
@@ -133,14 +133,14 @@ class DeleteCollectionFragment: DialogFragment(), LoaderManager.LoaderCallbacks<
return AlertDialog.Builder(activity!!)
.setTitle(R.string.delete_collection_confirm_title)
.setMessage(getString(R.string.delete_collection_confirm_warning, name))
.setPositiveButton(android.R.string.yes, { _, _ ->
.setPositiveButton(android.R.string.yes) { _, _ ->
val frag = DeleteCollectionFragment()
frag.arguments = arguments
frag.show(fragmentManager, null)
})
.setNegativeButton(android.R.string.no, { _, _ ->
}
.setNegativeButton(android.R.string.no) { _, _ ->
dismiss()
})
}
.create()
}
}

View File

@@ -49,13 +49,13 @@ class ExceptionInfoFragment: DialogFragment() {
.setIcon(R.drawable.ic_error_dark)
.setTitle(title)
.setMessage(exception::class.java.name + "\n" + exception.localizedMessage)
.setNegativeButton(R.string.exception_show_details, { _, _ ->
.setNegativeButton(R.string.exception_show_details) { _, _ ->
val intent = Intent(activity, DebugInfoActivity::class.java)
intent.putExtra(DebugInfoActivity.KEY_THROWABLE, exception)
account?.let { intent.putExtra(DebugInfoActivity.KEY_ACCOUNT, it) }
startActivity(intent)
})
.setPositiveButton(android.R.string.ok, { _, _ -> })
}
.setPositiveButton(android.R.string.ok) { _, _ -> }
.create()
isCancelable = false
return dialog

View File

@@ -30,7 +30,7 @@ import at.bitfire.davdroid.R
import at.bitfire.davdroid.log.Logger
import at.bitfire.davdroid.resource.LocalTaskList
import at.bitfire.davdroid.settings.ISettings
import org.apache.commons.lang3.StringUtils
import org.apache.commons.lang3.text.WordUtils
import java.util.*
import java.util.logging.Level
@@ -130,18 +130,18 @@ class StartupDialogFragment: DialogFragment(), LoaderManager.LoaderCallbacks<ISe
Mode.AUTOSTART_PERMISSIONS ->
AlertDialog.Builder(activity)
.setIcon(R.drawable.ic_error_dark)
.setTitle(getString(R.string.startup_autostart_permission, StringUtils.capitalize(Build.MANUFACTURER)))
.setMessage(R.string.startup_autostart_permission_message)
.setPositiveButton(R.string.startup_more_info, { _, _ ->
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.homepage_url)).buildUpon()
.appendPath("faq").appendPath("automatic-synchronization-is-not-run-as-expected").build())
.setTitle(R.string.startup_autostart_permission)
.setMessage(getString(R.string.startup_autostart_permission_message, WordUtils.capitalize(Build.MANUFACTURER.toLowerCase())))
.setPositiveButton(R.string.startup_more_info) { _, _ ->
val intent = Intent(Intent.ACTION_VIEW, App.homepageUrl(requireActivity()).buildUpon()
.appendPath("faq").appendEncodedPath("automatic-synchronization-is-not-run-as-expected/").build())
if (intent.resolveActivity(activity.packageManager) != null)
activity.startActivity(intent)
})
.setNeutralButton(android.R.string.ok, { _, _ -> })
.setNegativeButton(R.string.startup_dont_show_again, { _, _ ->
}
.setNeutralButton(R.string.startup_not_now) { _, _ -> }
.setNegativeButton(R.string.startup_dont_show_again) { _, _ ->
settings?.putBoolean(HINT_AUTOSTART_PERMISSIONS, false)
})
}
.create()
Mode.BATTERY_OPTIMIZATIONS ->
@@ -149,16 +149,16 @@ class StartupDialogFragment: DialogFragment(), LoaderManager.LoaderCallbacks<ISe
.setIcon(R.drawable.ic_info_dark)
.setTitle(R.string.startup_battery_optimization)
.setMessage(R.string.startup_battery_optimization_message)
.setPositiveButton(R.string.startup_battery_optimization_disable, @TargetApi(Build.VERSION_CODES.M) { _, _ ->
.setPositiveButton(R.string.startup_battery_optimization_disable) @TargetApi(Build.VERSION_CODES.M) { _, _ ->
val intent = Intent(android.provider.Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS,
Uri.parse("package:" + BuildConfig.APPLICATION_ID))
if (intent.resolveActivity(activity.packageManager) != null)
activity.startActivity(intent)
})
.setNeutralButton(android.R.string.ok, { _, _ -> })
.setNegativeButton(R.string.startup_dont_show_again, { _: DialogInterface, _: Int ->
Uri.parse("package:" + BuildConfig.APPLICATION_ID))
if (intent.resolveActivity(activity.packageManager) != null)
activity.startActivity(intent)
}
.setNeutralButton(R.string.startup_not_now) { _, _ -> }
.setNegativeButton(R.string.startup_dont_show_again) { _: DialogInterface, _: Int ->
settings?.putBoolean(HINT_BATTERY_OPTIMIZATIONS, false)
})
}
.create()
Mode.GOOGLE_PLAY_ACCOUNTS_REMOVED -> {
@@ -172,16 +172,16 @@ class StartupDialogFragment: DialogFragment(), LoaderManager.LoaderCallbacks<ISe
.setIcon(icon)
.setTitle(R.string.startup_google_play_accounts_removed)
.setMessage(R.string.startup_google_play_accounts_removed_message)
.setPositiveButton(R.string.startup_more_info, { _, _ ->
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.homepage_url)).buildUpon()
.appendPath("faq").appendPath("accounts-gone-after-reboot-or-update").build())
.setPositiveButton(R.string.startup_more_info) { _, _ ->
val intent = Intent(Intent.ACTION_VIEW, App.homepageUrl(requireActivity()).buildUpon()
.appendPath("faq").appendEncodedPath("accounts-gone-after-reboot-or-update/").build())
if (intent.resolveActivity(activity.packageManager) != null)
activity.startActivity(intent)
})
.setNeutralButton(android.R.string.ok, { _, _ -> })
.setNegativeButton(R.string.startup_dont_show_again, { _, _ ->
}
.setNeutralButton(R.string.startup_not_now) { _, _ -> }
.setNegativeButton(R.string.startup_dont_show_again) { _, _ ->
settings?.putBoolean(HINT_GOOGLE_PLAY_ACCOUNTS_REMOVED, false)
})
}
.create()
}
@@ -193,17 +193,17 @@ class StartupDialogFragment: DialogFragment(), LoaderManager.LoaderCallbacks<ISe
.setIcon(R.drawable.ic_playlist_add_check_dark)
.setTitle(R.string.startup_opentasks_not_installed)
.setMessage(builder.toString())
.setPositiveButton(R.string.startup_opentasks_not_installed_install, { _, _ ->
.setPositiveButton(R.string.startup_opentasks_not_installed_install) { _, _ ->
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=org.dmfs.tasks"))
if (intent.resolveActivity(activity.packageManager) != null)
activity.startActivity(intent)
else
Logger.log.warning("No market app available, can't install OpenTasks")
})
.setNeutralButton(android.R.string.ok, { _, _ -> })
.setNegativeButton(R.string.startup_dont_show_again, { _: DialogInterface, _: Int ->
}
.setNeutralButton(R.string.startup_not_now) { _, _ -> }
.setNegativeButton(R.string.startup_dont_show_again) { _: DialogInterface, _: Int ->
settings?.putBoolean(HINT_OPENTASKS_NOT_INSTALLED, false)
})
}
.create()
}
@@ -212,17 +212,16 @@ class StartupDialogFragment: DialogFragment(), LoaderManager.LoaderCallbacks<ISe
.setIcon(R.mipmap.ic_launcher)
.setTitle(R.string.startup_donate)
.setMessage(R.string.startup_donate_message)
.setPositiveButton(R.string.startup_donate_now, { _, _ ->
val uri = Uri.parse(getString(R.string.homepage_url))
.buildUpon()
.setPositiveButton(R.string.startup_donate_now) { _, _ ->
val uri = App.homepageUrl(requireActivity()).buildUpon()
.appendEncodedPath("donate/")
.build()
startActivity(Intent(Intent.ACTION_VIEW, uri))
settings?.putLong(SETTING_NEXT_DONATION_POPUP, System.currentTimeMillis() + 30 * 86400000L) // 30 days
})
.setNegativeButton(R.string.startup_donate_later, { _, _ ->
}
.setNegativeButton(R.string.startup_donate_later) { _, _ ->
settings?.putLong(SETTING_NEXT_DONATION_POPUP, System.currentTimeMillis() + 14 * 86400000L) // 14 days
})
}
.create()
}

View File

@@ -58,9 +58,9 @@ class AccountDetailsFragment: Fragment(), LoaderManager.LoaderCallbacks<CreateSe
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
val v = inflater.inflate(R.layout.login_account_details, container, false)
v.back.setOnClickListener({ _ ->
v.back.setOnClickListener { _ ->
requireFragmentManager().popBackStack()
})
}
val args = requireNotNull(arguments)
val config = args.getParcelable(KEY_CONFIG) as DavResourceFinder.Configuration
@@ -76,7 +76,7 @@ class AccountDetailsFragment: Fragment(), LoaderManager.LoaderCallbacks<CreateSe
v.contact_group_method.isEnabled = false
}
v.create_account.setOnClickListener({ _ ->
v.create_account.setOnClickListener { _ ->
val name = v.account_name.text.toString()
if (name.isEmpty())
v.account_name.error = getString(R.string.login_account_name_required)
@@ -87,7 +87,7 @@ class AccountDetailsFragment: Fragment(), LoaderManager.LoaderCallbacks<CreateSe
} else
Snackbar.make(v, R.string.login_account_not_created, Snackbar.LENGTH_LONG).show()
}
})
}
loaderManager.initLoader(0, null, this)
@@ -172,7 +172,7 @@ class AccountDetailsFragment: Fragment(), LoaderManager.LoaderCallbacks<CreateSe
refreshIntent.putExtra(DavService.EXTRA_DAV_SERVICE_ID, id)
requireActivity().startService(refreshIntent)
// calendar sync is automatically enabled by isAlwaysSyncable="true" in res/xml/sync_contacts.xml
// calendar sync is automatically enabled by isAlwaysSyncable="true" in res/xml/sync_calendars.xml
accountSettings.setSyncInterval(CalendarContract.AUTHORITY, Constants.DEFAULT_SYNC_INTERVAL)
// enable task sync if OpenTasks is installed

View File

@@ -11,7 +11,7 @@ import android.content.Context
import android.os.Parcel
import android.os.Parcelable
import at.bitfire.dav4android.DavResource
import at.bitfire.dav4android.DavResponse
import at.bitfire.dav4android.Response
import at.bitfire.dav4android.UrlUtils
import at.bitfire.dav4android.exception.DavException
import at.bitfire.dav4android.exception.HttpException
@@ -127,11 +127,10 @@ class DavResourceFinder(
}
}
if (config.principal != null && service == Service.CALDAV) {
if (config.principal != null && service == Service.CALDAV)
// query email address (CalDAV scheduling: calendar-user-address-set)
val davPrincipal = DavResource(httpClient.okHttpClient, config.principal!!, log)
try {
davPrincipal.propfind(0, CalendarUserAddressSet.NAME).use { response ->
DavResource(httpClient.okHttpClient, config.principal!!, log).propfind(0, CalendarUserAddressSet.NAME) { response, _ ->
response[CalendarUserAddressSet::class.java]?.let { addressSet ->
for (href in addressSet.hrefs)
try {
@@ -146,7 +145,6 @@ class DavResourceFinder(
} catch(e: Exception) {
log.log(Level.WARNING, "Couldn't query user email address", e)
}
}
// return config or null if config doesn't contain useful information
val serviceAvailable = config.principal != null || config.homeSets.isNotEmpty() || config.collections.isNotEmpty()
@@ -159,54 +157,27 @@ class DavResourceFinder(
private fun checkUserGivenURL(baseURL: HttpUrl, service: Service, config: Configuration.ServiceInfo) {
log.info("Checking user-given URL: $baseURL")
var principal: HttpUrl? = null
val davBase = DavResource(httpClient.okHttpClient, baseURL, log)
try {
val davBase = DavResource(httpClient.okHttpClient, baseURL, log)
lateinit var response: DavResponse
try {
when (service) {
Service.CARDDAV -> {
response = davBase.propfind(0,
ResourceType.NAME, DisplayName.NAME, AddressbookDescription.NAME,
AddressbookHomeSet.NAME,
CurrentUserPrincipal.NAME
)
rememberIfAddressBookOrHomeset(response, config)
}
Service.CALDAV -> {
response = davBase.propfind(0,
ResourceType.NAME, DisplayName.NAME, CalendarColor.NAME, CalendarDescription.NAME, CalendarTimezone.NAME, CurrentUserPrivilegeSet.NAME, SupportedCalendarComponentSet.NAME,
CalendarHomeSet.NAME,
CurrentUserPrincipal.NAME
)
rememberIfCalendarOrHomeset(response, config)
when (service) {
Service.CARDDAV -> {
davBase.propfind(0,
ResourceType.NAME, DisplayName.NAME, AddressbookDescription.NAME,
AddressbookHomeSet.NAME,
CurrentUserPrincipal.NAME
) { response, _ ->
scanCardDavResponse(response, config)
}
}
// check for current-user-principal
response.searchProperty(CurrentUserPrincipal::class.java)?.let { (dav, currentUserPrincipal) ->
currentUserPrincipal.href?.let {
principal = dav.url.resolve(it)
Service.CALDAV -> {
davBase.propfind(0,
ResourceType.NAME, DisplayName.NAME, CalendarColor.NAME, CalendarDescription.NAME, CalendarTimezone.NAME, CurrentUserPrivilegeSet.NAME, SupportedCalendarComponentSet.NAME,
CalendarHomeSet.NAME,
CurrentUserPrincipal.NAME
) { response, _ ->
scanCalDavResponse(response, config)
}
}
// check for resource type "principal"
if (principal == null)
for ((dav, resourceType) in response.searchProperties(ResourceType::class.java)) {
if (resourceType.types.contains(ResourceType.PRINCIPAL)) {
principal = dav.url
break
}
}
} finally {
response.close()
}
// If a principal has been detected successfully, ensure that it provides the required service.
principal?.let {
if (providesService(it, service))
config.principal = it
}
} catch(e: Exception) {
log.log(Level.FINE, "PROPFIND/OPTIONS on user-given URL failed", e)
@@ -214,72 +185,111 @@ class DavResourceFinder(
}
/**
* If [dav] references an address book or an address book home set, it will added to
* config.collections or config.homesets. URLs will be stored with trailing "/".
* @param dav resource whose properties are evaluated
* @param config structure where the address book (collection) and/or home set is stored into (if found)
* If [dav] references an address book, an address book home set, and/or a princiapl,
* it will added to, config.collections, config.homesets and/or config.principal.
* URLs will be stored with trailing "/".
*
* @param dav response whose properties are evaluated
* @param config structure where the results are stored into
*/
fun rememberIfAddressBookOrHomeset(dav: DavResponse, config: Configuration.ServiceInfo) {
// Is there an address book?
for ((addressBook, resourceType) in dav.searchProperties(ResourceType::class.java)) {
if (resourceType.types.contains(ResourceType.ADDRESSBOOK)) {
val info = CollectionInfo(addressBook)
fun scanCardDavResponse(dav: Response, config: Configuration.ServiceInfo) {
var principal: HttpUrl? = null
// check for current-user-principal
dav[CurrentUserPrincipal::class.java]?.href?.let {
principal = dav.requestedUrl.resolve(it)
}
// Is it an address book and/or principal?
dav[ResourceType::class.java]?.let {
if (it.types.contains(ResourceType.ADDRESSBOOK)) {
val info = CollectionInfo(dav)
log.info("Found address book at ${info.url}")
config.collections[info.url] = info
}
if (it.types.contains(ResourceType.PRINCIPAL))
principal = dav.href
}
// Is there an addressbook-home-set?
for ((dav, homeSet) in dav.searchProperties(AddressbookHomeSet::class.java)) {
// Is it an addressbook-home-set?
dav[AddressbookHomeSet::class.java]?.let { homeSet ->
for (href in homeSet.hrefs) {
dav.url.resolve(href)?.let {
dav.requestedUrl.resolve(href)?.let {
val location = UrlUtils.withTrailingSlash(it)
log.info("Found address book home-set at $location")
config.homeSets += location
}
}
}
principal?.let {
if (providesService(it, Service.CARDDAV))
config.principal = principal
}
}
private fun rememberIfCalendarOrHomeset(dav: DavResponse, config: Configuration.ServiceInfo) {
// Is the collection a calendar collection?
for ((calendar, resourceType) in dav.searchProperties(ResourceType::class.java)) {
if (resourceType.types.contains(ResourceType.CALENDAR)) {
val info = CollectionInfo(calendar)
/**
* If [dav] references an address book, an address book home set, and/or a princiapl,
* it will added to, config.collections, config.homesets and/or config.principal.
* URLs will be stored with trailing "/".
*
* @param dav response whose properties are evaluated
* @param config structure where the results are stored into
*/
fun scanCalDavResponse(dav: Response, config: Configuration.ServiceInfo) {
var principal: HttpUrl? = null
// check for current-user-principal
dav[CurrentUserPrincipal::class.java]?.href?.let {
principal = dav.requestedUrl.resolve(it)
}
// Is it a calendar book and/or principal?
dav[ResourceType::class.java]?.let {
if (it.types.contains(ResourceType.CALENDAR)) {
val info = CollectionInfo(dav)
log.info("Found calendar at ${info.url}")
config.collections[info.url] = info
}
if (it.types.contains(ResourceType.PRINCIPAL))
principal = dav.href
}
// Is there an calendar-home-set?
for ((dav, homeSet) in dav.searchProperties(CalendarHomeSet::class.java)) {
// Is it an calendar-home-set?
dav[CalendarHomeSet::class.java]?.let { homeSet ->
for (href in homeSet.hrefs) {
dav.url.resolve(href)?.let {
dav.requestedUrl.resolve(href)?.let {
val location = UrlUtils.withTrailingSlash(it)
log.info("Found calendar home-set at $location")
log.info("Found calendar book home-set at $location")
config.homeSets += location
}
}
}
principal?.let {
if (providesService(it, Service.CALDAV))
config.principal = principal
}
}
@Throws(IOException::class)
fun providesService(url: HttpUrl, service: Service): Boolean {
val davPrincipal = DavResource(httpClient.okHttpClient, url, log)
var provided = false
try {
davPrincipal.options().use {
val capabilities = it.capabilities
DavResource(httpClient.okHttpClient, url, log).options { capabilities, _ ->
if ((service == Service.CARDDAV && capabilities.contains("addressbook")) ||
(service == Service.CALDAV && capabilities.contains("calendar-access")))
return true
provided = true
}
} catch(e: Exception) {
log.log(Level.SEVERE, "Couldn't detect services on $url", e)
if (e !is HttpException && e !is DavException)
throw e
}
return false
return provided
}
@@ -346,31 +356,28 @@ class DavResourceFinder(
/**
* Queries a given URL for current-user-principal
*
* @param url URL to query with PROPFIND (Depth: 0)
* @param service required service (may be null, in which case no service check is done)
* @return current-user-principal URL that provides required service, or null if none
*/
@Throws(IOException::class, HttpException::class, DavException::class)
fun getCurrentUserPrincipal(url: HttpUrl, service: Service?): HttpUrl? {
val dav = DavResource(httpClient.okHttpClient, url, log)
dav.propfind(0, CurrentUserPrincipal.NAME).use {
it.searchProperty(CurrentUserPrincipal::class.java)?.let { (dav, currentUserPrincipal) ->
currentUserPrincipal.href?.let { href ->
dav.url.resolve(href)?.let { principal ->
log.info("Found current-user-principal: $principal")
var principal: HttpUrl? = null
DavResource(httpClient.okHttpClient, url, log).propfind(0, CurrentUserPrincipal.NAME) { response, _ ->
response[CurrentUserPrincipal::class.java]?.href?.let { href ->
response.requestedUrl.resolve(href)?.let {
log.info("Found current-user-principal: $it")
// service check
if (service != null && !providesService(principal, service)) {
log.info("$principal doesn't provide required $service service")
return null
}
return principal
}
// service check
if (service != null && !providesService(it, service))
log.info("$it doesn't provide required $service service")
else
principal = it
}
}
}
return null
return principal
}
@@ -429,9 +436,8 @@ class DavResourceFinder(
dest.writeString(logs)
}
@Suppress("unused")
@JvmField
val CREATOR = object: Parcelable.Creator<Configuration> {
companion object CREATOR : Parcelable.Creator<Configuration> {
override fun createFromParcel(source: Parcel): Configuration {
fun readCollections(): MutableMap<HttpUrl, CollectionInfo> {

View File

@@ -104,14 +104,14 @@ class DetectConfigurationFragment: DialogFragment(), LoaderManager.LoaderCallbac
.setTitle(R.string.login_configuration_detection)
.setIcon(R.drawable.ic_error_dark)
.setMessage(R.string.login_no_caldav_carddav)
.setNeutralButton(R.string.login_view_logs, { _, _ ->
.setNeutralButton(R.string.login_view_logs) { _, _ ->
val intent = Intent(activity, DebugInfoActivity::class.java)
intent.putExtra(DebugInfoActivity.KEY_LOGS, arguments!!.getString(KEY_LOGS))
startActivity(intent)
})
.setPositiveButton(android.R.string.ok, { _, _ ->
}
.setPositiveButton(android.R.string.ok) { _, _ ->
// dismiss
})
}
.create()!!
}

View File

@@ -14,6 +14,7 @@ import android.os.Bundle
import android.support.v7.app.AppCompatActivity
import android.view.Menu
import android.view.MenuItem
import at.bitfire.davdroid.App
import at.bitfire.davdroid.R
import java.util.*
@@ -62,7 +63,9 @@ class LoginActivity: AppCompatActivity() {
fun showHelp(item: MenuItem) {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.login_help_url))))
startActivity(Intent(Intent.ACTION_VIEW, App.homepageUrl(this).buildUpon()
.appendEncodedPath("tested-with/")
.build()))
}
}

View File

@@ -28,18 +28,16 @@ data class LoginInfo(
dest.writeSerializable(credentials)
}
companion object {
@JvmField
val CREATOR = object: Parcelable.Creator<LoginInfo> {
override fun createFromParcel(source: Parcel) =
LoginInfo(
source.readSerializable() as URI,
source.readSerializable() as Credentials
)
companion object CREATOR : Parcelable.Creator<LoginInfo> {
override fun newArray(size: Int) = arrayOfNulls<LoginInfo>(size)
}
override fun createFromParcel(source: Parcel) =
LoginInfo(
source.readSerializable() as URI,
source.readSerializable() as Credentials
)
override fun newArray(size: Int) = arrayOfNulls<LoginInfo>(size)
}

View File

@@ -0,0 +1,13 @@
<!--
~ Copyright © Ricki Hirner (bitfire web engineering).
~ All rights reserved. This program and the accompanying materials
~ are made available under the terms of the GNU Public License v3.0
~ which accompanies this distribution, and is available at
~ http://www.gnu.org/licenses/gpl.html
-->
<vector android:alpha="0.54" android:height="24dp"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M16,1L4,1c-1.1,0 -2,0.9 -2,2v14h2L4,3h12L16,1zM19,5L8,5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h11c1.1,0 2,-0.9 2,-2L21,7c0,-1.1 -0.9,-2 -2,-2zM19,21L8,21L8,7h11v14z"/>
</vector>

View File

@@ -0,0 +1,5 @@
<vector android:height="24dp"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@color/actionBarButton" android:pathData="M10,20v-6h4v6h5v-8h3L12,3 2,12h3v8z"/>
</vector>

View File

@@ -0,0 +1,18 @@
<!--
~ Copyright © Ricki Hirner (bitfire web engineering).
~ All rights reserved. This program and the accompanying materials
~ are made available under the terms of the GNU Public License v3.0
~ which accompanies this distribution, and is available at
~ http://www.gnu.org/licenses/gpl.html
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:alpha="0.54"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M9,11.24L9,7.5C9,6.12 10.12,5 11.5,5S14,6.12 14,7.5v3.74c1.21,-0.81 2,-2.18 2,-3.74C16,5.01 13.99,3 11.5,3S7,5.01 7,7.5c0,1.56 0.79,2.93 2,3.74zM18.84,15.87l-4.54,-2.26c-0.17,-0.07 -0.35,-0.11 -0.54,-0.11L13,13.5v-6c0,-0.83 -0.67,-1.5 -1.5,-1.5S10,6.67 10,7.5v10.74l-3.43,-0.72c-0.08,-0.01 -0.15,-0.03 -0.24,-0.03 -0.31,0 -0.59,0.13 -0.79,0.33l-0.79,0.8 4.94,4.94c0.27,0.27 0.65,0.44 1.06,0.44h6.79c0.75,0 1.33,-0.55 1.44,-1.28l0.75,-5.27c0.01,-0.07 0.02,-0.14 0.02,-0.2 0,-0.62 -0.38,-1.16 -0.91,-1.38z"/>
</vector>

View File

@@ -0,0 +1,11 @@
<!--
~ Copyright © Ricki Hirner (bitfire web engineering).
~ All rights reserved. This program and the accompanying materials
~ are made available under the terms of the GNU Public License v3.0
~ which accompanies this distribution, and is available at
~ http://www.gnu.org/licenses/gpl.html
-->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/ic_touch_app_dark"/>
</layer-list>

View File

@@ -1,85 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright © Ricki Hirner (bitfire web engineering).
~ All rights reserved. This program and the accompanying materials
~ are made available under the terms of the GNU Public License v3.0
~ which accompanies this distribution, and is available at
~ http://www.gnu.org/licenses/gpl.html
-->
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="@dimen/activity_margin">
<TextView
android:id="@+id/title"
tools:text="DAVdroid 1.0"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:textAppearance="?android:attr/textAppearanceLarge"
android:layout_gravity="center_horizontal"/>
<TextView
android:id="@+id/website"
tools:text="https://davdroid.bitfire.at"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:layout_gravity="center_horizontal"/>
<TextView
android:id="@+id/copyright"
tools:text="© author, company"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceMedium"
android:layout_marginBottom="16dp"/>
<LinearLayout
android:id="@+id/license_terms"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:orientation="vertical">
<TextView
android:id="@+id/license_info"
tools:text="This program comes with ABSOLUTELY NO WARRANTY. It is free software, and you are welcome to redistribute it under certain conditions."
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="32dp"/>
<TextView
android:id="@+id/license_header"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/TextView.Heading"
android:text="@string/about_license_terms"
android:layout_marginBottom="16dp"/>
<TextView
android:id="@+id/license_text"
android:text="@string/please_wait"
tools:text="(Full license text)"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceSmall"/>
</LinearLayout>
<FrameLayout
android:id="@+id/license_fragment"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</LinearLayout>
</ScrollView>

View File

@@ -0,0 +1,88 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright © Ricki Hirner (bitfire web engineering).
~ All rights reserved. This program and the accompanying materials
~ are made available under the terms of the GNU Public License v3.0
~ which accompanies this distribution, and is available at
~ http://www.gnu.org/licenses/gpl.html
-->
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_horizontal">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="@dimen/activity_margin"
android:orientation="vertical"
android:gravity="center_horizontal">
<ImageView
android:layout_width="128dp"
android:layout_height="128dp"
android:src="@mipmap/ic_launcher"
android:layout_marginBottom="16dp"/>
<TextView
android:id="@+id/app_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAlignment="center"
android:textAppearance="?android:attr/textAppearanceLarge"
android:text="DAVdroid"
tools:ignore="HardcodedText" />
<TextView
android:id="@+id/app_version"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAlignment="center"
android:text="@string/about_version" />
<TextView
android:id="@+id/build_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:textAlignment="center"
android:text="@string/about_build_date" />
<TextView
android:id="@+id/copyright"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:textAppearance="?android:attr/textAppearanceMedium"
android:text="@string/about_copyright" />
<TextView
android:id="@+id/warranty"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"/>
<TextView
android:id="@+id/pixels"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:textAlignment="center" />
<FrameLayout
android:id="@+id/license_fragment"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/license_text"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</FrameLayout>
</LinearLayout>
</ScrollView>

View File

@@ -17,7 +17,20 @@
<LinearLayout android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="@dimen/activity_margin">
android:padding="@dimen/activity_margin"
android:animateLayoutChanges="true">
<TextView
android:id="@+id/select_collections_hint"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:drawableLeft="@drawable/ic_touch_app_dark_compat"
android:drawableStart="@drawable/ic_touch_app_dark_compat"
android:drawablePadding="8dp"
android:gravity="center_vertical"
android:visibility="gone"
android:text="@string/account_select_collections_hint"/>
<android.support.v7.widget.CardView
android:id="@+id/carddav"

View File

@@ -0,0 +1,56 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright © Ricki Hirner (bitfire web engineering).
~ All rights reserved. This program and the accompanying materials
~ are made available under the terms of the GNU Public License v3.0
~ which accompanies this distribution, and is available at
~ http://www.gnu.org/licenses/gpl.html
-->
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="10dp">
<TextView
android:id="@+id/url_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="4dp"
android:text="@string/collection_properties_url"
android:labelFor="@id/url" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/url"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:fontFamily="monospace"
android:textSize="12sp"/>
<ImageButton
android:id="@+id/url_copy"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:padding="1sp"
android:background="@android:color/transparent"
android:layout_marginLeft="4dp"
app:srcCompat="@drawable/ic_content_copy_dark"
android:contentDescription="@string/collection_properties_copy_url" />
</LinearLayout>
</LinearLayout>
</ScrollView>

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright © Ricki Hirner (bitfire web engineering).
~ All rights reserved. This program and the accompanying materials
~ are made available under the terms of the GNU Public License v3.0
~ which accompanies this distribution, and is available at
~ http://www.gnu.org/licenses/gpl.html
-->
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:icon="@drawable/ic_home_action"
android:title="@string/navigation_drawer_website"
app:showAsAction="ifRoom"
android:onClick="showWebsite" />
</menu>

View File

@@ -13,7 +13,10 @@
android:checkable="true"
android:title="@string/collection_force_read_only"/>
<item android:id="@+id/properties"
android:title="@string/collection_properties"/>
<item android:id="@+id/delete_collection"
android:title="@string/delete_collection"/>
android:title="@string/delete_collection"/>
</menu>

View File

@@ -8,8 +8,6 @@
<string name="manage_accounts">إدارة الحسابات</string>
<string name="please_wait">يرجى الانتظار...</string>
<string name="send">إرسال</string>
<string name="homepage_url">https://www.davdroid.com/?pk_campaign=davdroid-app</string>
<string name="beta_feedback_url">https://forums.bitfire.at/category/9/beta-test-discussion?pk_campaign=davdroid-app</string>
<string name="notification_channel_debugging">تصحيح العلل</string>
<string name="notification_channel_general">رسائل هامة أخرى</string>
<string name="notification_channel_sync">مزامنة</string>
@@ -17,10 +15,6 @@
<string name="notification_channel_sync_io_errors">أخطاء الشبكة و عمليات الإدخال/الإخراج</string>
<string name="notification_channel_sync_status">رسائل الحالة</string>
<!--startup dialogs-->
<string name="startup_autostart_permission">إذن البدء التلقائي %s</string>
<string name="startup_autostart_permission_message">البرنامج الثابت للجهاز قد يمنع المزامنة التلقائية. قد يتطلب الأمر منك السماح بالمزامنة التلقائية يدوياً.</string>
<string name="startup_battery_optimization">تحسين البطارية</string>
<string name="startup_battery_optimization_message">قد يعطّل نظام آندرويد مزامنة DAVdroid أو يقللها بعد عدة أيام. قم بتعطيل تحسين البطارية لمنع حدوث ذلك.</string>
<string name="startup_battery_optimization_disable">التعطيل لـ DAVdroid</string>
<string name="startup_dont_show_again">لاتعرضه مرة أخرى</string>
<string name="startup_donate">معلومات المصدر المفتوح</string>
@@ -35,7 +29,6 @@
<string name="startup_opentasks_reinstall_davdroid">بعد تثبيت OpenTasks، لابد لك من إعادة تثبيت DAVdroid وإضافة حساباتك مجدداً (علة في آندرويد).</string>
<string name="startup_opentasks_not_installed_install">تثبيت OpenTasks</string>
<!--AboutActivity-->
<string name="about_license_terms">أحكام الترخيص</string>
<string name="about_license_info_no_warranty">يقدَّم هذا البرنامج دون أدنى مسؤولية. إنه برنامج حر، وندعوك لإعادة توزيعه حسب أحكام محددة.</string>
<!--global settings-->
<string name="logging_davdroid_file_logging">تسجيل ملفات DAVdroid</string>
@@ -111,7 +104,6 @@
<string name="account_no_webcal_handler_found">لم نجِد تطبيقاً قادراً على استخدام Webcal</string>
<string name="account_install_icsdroid">تثبيت ICSdroid</string>
<!--AddAccountActivity-->
<string name="login_help_url">https://www.davdroid.com/tested-with/?pk_campaign=davdroid-app</string>
<string name="login_title">إضافة حساب</string>
<string name="login_type_email">تسجيل الدخول بعنوان البريد</string>
<string name="login_email_address">عنوان البريد الإلكتروني</string>
@@ -138,7 +130,6 @@
<string name="login_configuration_detection">اكتشاف الضبط</string>
<string name="login_querying_server">يجري استعلام الخادم ... يرجى الانتظار</string>
<string name="login_no_caldav_carddav">لم نجِد خدمة CalDAV أو CardDAV.</string>
<string name="login_view_logs">عرض السجلات</string>
<!--AccountSettingsActivity-->
<string name="settings_title">%s الإعدادات:</string>
<string name="settings_authentication">التصديق</string>

View File

@@ -4,12 +4,11 @@
<string name="app_name">DAVdroid</string>
<string name="account_title_address_book">DAVdroid-Adressbuch</string>
<string name="address_books_authority_title">Adressbücher</string>
<string name="copied_to_clipboard">In Zwischenablage kopiert</string>
<string name="help">Hilfe</string>
<string name="manage_accounts">Konten verwalten</string>
<string name="please_wait">Bitte warten …</string>
<string name="send">Senden</string>
<string name="homepage_url">https://www.davdroid.com/?pk_campaign=davdroid-app</string>
<string name="beta_feedback_url">https://forums.bitfire.at/category/9/beta-test-discussion?pk_campaign=davdroid-app</string>
<string name="notification_channel_debugging">Fehlersuche</string>
<string name="notification_channel_general">Andere wichtige Mitteilungen</string>
<string name="notification_channel_sync">Synchronisierung</string>
@@ -17,12 +16,13 @@
<string name="notification_channel_sync_io_errors">Netzwerk- und E/A-Fehler</string>
<string name="notification_channel_sync_status">Status-Mitteilungen</string>
<!--startup dialogs-->
<string name="startup_autostart_permission">%s Auto-Start-Berechtigung</string>
<string name="startup_autostart_permission_message">Automatische Synchronisierung wird möglicherweise von Ihrer Geräte-Firmware verhindert. In diesem Fall müssen Sie die automatische Synchronisierung manuell erlauben.</string>
<string name="startup_battery_optimization">Akku-Leistungsoptimierung</string>
<string name="startup_battery_optimization_message">Android kann die DAVdroid-Synchronisierung in ein paar Tagen reduzieren/deaktivieren. Um dies zu verhindern, muss die Akku-Leistungsoptimierung deaktiviert werden.</string>
<string name="startup_autostart_permission">Automatische Synchronisierung</string>
<string name="startup_autostart_permission_message">%s-Firmware verhindert oftmals das automatische Synchronisieren. In diesem Fall müssen Sie das automatische Synchronisieren in den Android-Einstellung erlauben.</string>
<string name="startup_battery_optimization">Geplante Synchronisierung</string>
<string name="startup_battery_optimization_message">Ihr Gerät wird die DAVdroid- Synchronisierung einschränken. Damit regelmäßig synchronisiert werden kann, muss die »Akku-Leistungsoptimierung« deaktiviert werden.</string>
<string name="startup_battery_optimization_disable">Für DAVdroid deaktivieren</string>
<string name="startup_dont_show_again">Nicht mehr anzeigen</string>
<string name="startup_not_now">Später</string>
<string name="startup_donate">Open-Source-Information</string>
<string name="startup_donate_message">Es freut uns, dass Sie DAVdroid und damit Open-Source-Software (GPLv3) verwenden. Da in DAVdroid tausende Stunden harter Arbeit stecken und es immer noch weiter entwickelt wird, gibt es die Möglichkeit, uns zu spenden.</string>
<string name="startup_donate_now">Spendenseite anzeigen</string>
@@ -35,7 +35,10 @@
<string name="startup_opentasks_reinstall_davdroid">Nach der Installation von OpenTasks muss DAVdroid NEU INSTALLIERT und die Konten neu hinzugefügt werden (Android-Bug).</string>
<string name="startup_opentasks_not_installed_install">OpenTasks installieren</string>
<!--AboutActivity-->
<string name="about_license_terms">Lizenzbedingungen</string>
<string name="about_libraries">Bibliotheken</string>
<string name="about_version">Version %1s (%2d)</string>
<string name="about_build_date">Erstellt am %s</string>
<string name="about_flavor_info">Diese Version ist nur zur Verteilung über Google Play bestimmt.</string>
<string name="about_license_info_no_warranty">Dieses Programm wird OHNE JEDE GEWÄHRLEISTUNG bereitgestellt. Es ist freie Software Sie können es also unter bestimmten Bedingungen weiterverbreiten.</string>
<!--global settings-->
<string name="logging_davdroid_file_logging">DAVdroid Datei-Protokollierung</string>
@@ -97,6 +100,7 @@
<string name="account_delete">Konto löschen</string>
<string name="account_delete_confirmation_title">Konto wirklich löschen?</string>
<string name="account_delete_confirmation_text">Alle Adressbücher, Kalender und Aufgabenlisten werden vom Gerät (nicht am Server) gelöscht.</string>
<string name="account_select_collections_hint">Ordner zur Synchronisierung auswählen</string>
<string name="account_carddav">CardDAV</string>
<string name="account_caldav">CalDAV</string>
<string name="account_webcal">Webcal</string>
@@ -111,7 +115,6 @@
<string name="account_no_webcal_handler_found">Keine Webcal-App gefunden</string>
<string name="account_install_icsdroid">ICSdroid installieren</string>
<!--AddAccountActivity-->
<string name="login_help_url">https://www.davdroid.com/tested-with/?pk_campaign=davdroid-app</string>
<string name="login_title">Konto hinzufügen</string>
<string name="login_type_email">Mit Email-Adresse anmelden</string>
<string name="login_email_address">Email-Adresse</string>
@@ -138,7 +141,7 @@
<string name="login_configuration_detection">Ressourcen-Erkennung</string>
<string name="login_querying_server">Server-Informationen werden abgerufen. Bitte warten …</string>
<string name="login_no_caldav_carddav">Es konnte weder ein CalDAV- noch ein CardDAV-Dienst gefunden werden.</string>
<string name="login_view_logs">Protokoll anzeigen</string>
<string name="login_view_logs">Details anzeigen</string>
<!--AccountSettingsActivity-->
<string name="settings_title">Einstellungen: %s</string>
<string name="settings_authentication">Anmeldeinformationen</string>
@@ -219,6 +222,9 @@
<string name="delete_collection_confirm_warning">Dieser Ordner (%s) wird samt allen Inhalten vom Server gelöscht.</string>
<string name="delete_collection_deleting_collection">Ordner wird gelöscht</string>
<string name="collection_force_read_only">Schreibschutz erzwingen</string>
<string name="collection_properties">Eigenschaften</string>
<string name="collection_properties_url">Adresse (URL):</string>
<string name="collection_properties_copy_url">URL kopieren</string>
<!--ExceptionInfoFragment-->
<string name="exception">Ein Fehler ist aufgetreten.</string>
<string name="exception_httpexception">Ein HTTP-Fehler ist aufgetreten.</string>

View File

@@ -1,4 +1,72 @@
<h3 style="text-align: center;">GNU GENERAL PUBLIC LICENSE</h3>
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright © Ricki Hirner (bitfire web engineering).
~ All rights reserved. This program and the accompanying materials
~ are made available under the terms of the GNU Public License v3.0
~ which accompanies this distribution, and is available at
~ http://www.gnu.org/licenses/gpl.html
-->
<resources>
<string name="define_ambilwarna" translatable="false" />
<string name="library_ambilwarna_author" translatable="false">Randy Sugianto</string>
<string name="library_ambilwarna_authorWebsite">https://github.com/yukuku</string>
<string name="library_ambilwarna_libraryName" translatable="false">AmbilWarna</string>
<string name="library_ambilwarna_libraryDescription">This is a small library for your application to enable the users to select an arbitrary color.</string>
<string name="library_ambilwarna_libraryWebsite" translatable="false">https://github.com/yukuku/ambilwarna</string>
<string name="library_ambilwarna_isOpenSource" translatable="false">true</string>
<string name="library_ambilwarna_licenseId">apache_2_0</string>
<string name="define_commons" translatable="false" />
<string name="library_commons_author" translatable="false">Apache Software Foundation</string>
<string name="library_commons_authorWebsite">https://www.apache.org/</string>
<string name="library_commons_libraryName" translatable="false">Apache Commons</string>
<string name="library_commons_libraryDescription">Apache Commons is an Apache project focused on all aspects of reusable Java components.</string>
<string name="library_commons_libraryWebsite" translatable="false">https://commons.apache.org/components.html</string>
<string name="library_commons_isOpenSource" translatable="false">true</string>
<string name="library_commons_licenseId">apache_2_0</string>
<string name="define_dnsjava" translatable="false" />
<string name="library_dnsjava_author" translatable="false">Brian Wellington</string>
<string name="library_dnsjava_authorWebsite">http://www.xbill.org/~bwelling/</string>
<string name="library_dnsjava_libraryName" translatable="false">dnsjava</string>
<string name="library_dnsjava_libraryDescription">dnsjava is an implementation of DNS in Java.</string>
<string name="library_dnsjava_libraryWebsite" translatable="false">http://dnsjava.org/</string>
<string name="library_dnsjava_isOpenSource" translatable="false">true</string>
<string name="library_dnsjava_licenseId">bsd_3</string>
<string name="define_ezvcard" translatable="false" />
<string name="library_ezvcard_author" translatable="false">Michael Angstadt</string>
<string name="library_ezvcard_authorWebsite" translatable="false">http://mikeangstadt.name/</string>
<string name="library_ezvcard_libraryName" translatable="false">ez-vcard</string>
<string name="library_ezvcard_libraryDescription">ez-vcard is a vCard library written in Java.</string>
<string name="library_ezvcard_libraryWebsite" translatable="false">https://github.com/mangstadt/ez-vcard</string>
<string name="library_ezvcard_isOpenSource" translatable="false">true</string>
<string name="library_ezvcard_licenseId">bsd_2</string>
<string name="define_ical4j" translatable="false" />
<string name="library_ical4j_author" translatable="false">Ben Fortuna</string>
<string name="library_ical4j_authorWebsite">http://basepatterns.org/</string>
<string name="library_ical4j_libraryName" translatable="false">ical4j</string>
<string name="library_ical4j_libraryDescription">iCal4j is a Java API that provides support for the iCalendar specification as defined in RFC2445.</string>
<string name="library_ical4j_libraryWebsite" translatable="false">https://ical4j.github.io/</string>
<string name="library_ical4j_isOpenSource" translatable="false">true</string>
<string name="library_ical4j_licenseId">bsd_3</string>
<string name="define_okhttp" translatable="false" />
<string name="library_okhttp_author" translatable="false">Square, Inc.</string>
<string name="library_okhttp_authorWebsite">https://squareup.com/</string>
<string name="library_okhttp_libraryName" translatable="false">okhttp</string>
<string name="library_okhttp_libraryDescription">An HTTP+HTTP/2 client for Android and Java applications that\s efficient by default.</string>
<string name="library_okhttp_libraryWebsite" translatable="false">http://square.github.io/okhttp/</string>
<string name="library_okhttp_isOpenSource" translatable="false">true</string>
<string name="library_okhttp_licenseId">apache_2_0</string>
<!-- license texts -->
<string name="gpl_v3"><![CDATA[
<h3 style="text-align: center;">GNU GENERAL PUBLIC LICENSE</h3>
<p style="text-align: center;">Version 3, 29 June 2007</p>
<p>Copyright &copy; 2007 Free Software Foundation, Inc.
@@ -42,16 +110,16 @@ know their rights.</p>
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.</p>
<p>For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
<p>For the developers\' and authors\' protection, the GPL clearly explains
that there is no warranty for this free software. For both users\' and
authors\' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.</p>
<p>Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
protecting users\' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
@@ -77,7 +145,7 @@ modification follow.</p>
<p>&ldquo;Copyright&rdquo; also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.</p>
<p>&ldquo;The Program&rdquo; refers to any copyrightable work licensed under this
License. Each licensee is addressed as &ldquo;you&rdquo;. &ldquo;Licensees&rdquo; and
&ldquo;recipients&rdquo; may be individuals or organizations.</p>
@@ -135,7 +203,7 @@ produce the work, or an object code interpreter used to run it.</p>
<p>The &ldquo;Corresponding Source&rdquo; for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
control those activities. However, it does not include the work\'s
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
@@ -177,7 +245,7 @@ your copyrighted material outside their relationship with you.</p>
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.</p>
<h4><a name="section3"></a>3. Protecting Users' Legal Rights From Anti-Circumvention Law.</h4>
<h4><a name="section3"></a>3. Protecting Users\' Legal Rights From Anti-Circumvention Law.</h4>
<p>No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
@@ -189,13 +257,13 @@ measures.</p>
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
modification of the work as a means of enforcing, against the work\'s
users, your or third parties\' legal rights to forbid circumvention of
technological measures.</p>
<h4><a name="section4"></a>4. Conveying Verbatim Copies.</h4>
<p>You may convey verbatim copies of the Program's source code as you
<p>You may convey verbatim copies of the Program\'s source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
@@ -240,7 +308,7 @@ works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
&ldquo;aggregate&rdquo; if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
used to limit the access or legal rights of the compilation\'s users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.</p>
@@ -462,7 +530,7 @@ organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
licenses to the work the party\'s predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.</p>
@@ -479,9 +547,9 @@ sale, or importing the Program or any portion of it.</p>
<p>A &ldquo;contributor&rdquo; is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's &ldquo;contributor version&rdquo;.</p>
work thus licensed is called the contributor\'s &ldquo;contributor version&rdquo;.</p>
<p>A contributor's &ldquo;essential patent claims&rdquo; are all patent claims
<p>A contributor\'s &ldquo;essential patent claims&rdquo; are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
@@ -492,7 +560,7 @@ patent sublicenses in a manner consistent with the requirements of
this License.</p>
<p>Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
patent license under the contributor\'s essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.</p>
@@ -513,10 +581,10 @@ patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. &ldquo;Knowingly relying&rdquo; means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
covered work in a country, or your recipient\'s use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.</p>
<p>If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
@@ -544,7 +612,7 @@ or that patent license was granted, prior to 28 March 2007.</p>
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.</p>
<h4><a name="section12"></a>12. No Surrender of Others' Freedom.</h4>
<h4><a name="section12"></a>12. No Surrender of Others\' Freedom.</h4>
<p>If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
@@ -584,7 +652,7 @@ GNU General Public License, you may choose any version ever published
by the Free Software Foundation.</p>
<p>If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
versions of the GNU General Public License can be used, that proxy\'s
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.</p>
@@ -626,3 +694,6 @@ Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.</p>
<p>END OF TERMS AND CONDITIONS</p>
]]></string>
</resources>

View File

@@ -11,17 +11,18 @@
<!-- common strings -->
<string name="app_name">DAVdroid</string>
<string name="account_type">bitfire.at.davdroid</string>
<string name="account_type_address_book">at.bitfire.davdroid.address_book</string>
<string name="account_type" translatable="false">bitfire.at.davdroid</string>
<string name="account_type_address_book" translatable="false">at.bitfire.davdroid.address_book</string>
<string name="account_title_address_book">DAVdroid Address book</string>
<string name="address_books_authority">at.bitfire.davdroid.addressbooks</string>
<string name="address_books_authority" translatable="false">at.bitfire.davdroid.addressbooks</string>
<string name="address_books_authority_title">Address books</string>
<string name="copied_to_clipboard">Copied to clipboard</string>
<string name="help">Help</string>
<string name="manage_accounts">Manage accounts</string>
<string name="please_wait">Please wait …</string>
<string name="send">Send</string>
<string name="homepage_url">https://www.davdroid.com/?pk_campaign=davdroid-app</string>
<string name="beta_feedback_url">https://forums.bitfire.at/category/9/beta-test-discussion?pk_campaign=davdroid-app</string>
<string name="homepage_url" translatable="false">https://www.davdroid.com/</string>
<string name="beta_feedback_url" translatable="false">https://forums.bitfire.at/category/9/beta-test-discussion?pk_campaign=davdroid-app</string>
<string name="notification_channel_debugging">Debugging</string>
<string name="notification_channel_general">Other important messages</string>
@@ -31,12 +32,13 @@
<string name="notification_channel_sync_status">Status messages</string>
<!-- startup dialogs -->
<string name="startup_autostart_permission">%s auto-start permission</string>
<string name="startup_autostart_permission_message">Device firmware may prevent automatic synchronization. You may have to allow automatic synchronization manually.</string>
<string name="startup_battery_optimization">Battery Optimization</string>
<string name="startup_battery_optimization_message">Android may disable/reduce DAVdroid synchronization after a few days. To prevent this, turn off battery optimization.</string>
<string name="startup_autostart_permission">Automatic synchronization</string>
<string name="startup_autostart_permission_message">%s firmware often blocks automatic synchronization. In this case, allow automatic synchronization in your Android settings.</string>
<string name="startup_battery_optimization">Scheduled synchronization</string>
<string name="startup_battery_optimization_message">Your device will restrain DAVdroid synchronization. To enforce regular DAVdroid sync intervals, turn off \"battery optimization\".</string>
<string name="startup_battery_optimization_disable">Turn off for DAVdroid</string>
<string name="startup_dont_show_again">Don\'t show again</string>
<string name="startup_not_now">Not now</string>
<string name="startup_donate">Open-Source Information</string>
<string name="startup_donate_message">We\'re happy that you use DAVdroid, which is open-source software (GPLv3). Because developing DAVdroid is hard work and took us thousands of working hours, please consider a donation.</string>
<string name="startup_donate_now">Show donation page</string>
@@ -50,7 +52,11 @@
<string name="startup_opentasks_not_installed_install">Install OpenTasks</string>
<!-- AboutActivity -->
<string name="about_license_terms">License terms</string>
<string name="about_libraries">Libraries</string>
<string name="about_version">Version %1s (%2d)</string>
<string name="about_build_date">Compiled on %s</string>
<string name="about_copyright" translatable="false">© Ricki Hirner, Bernhard Stockmann (bitfire web engineering)</string>
<string name="about_flavor_info">This version is only eligible for distribution over Google Play.</string>
<string name="about_license_info_no_warranty">This program comes with ABSOLUTELY NO WARRANTY. It is free software, and you are welcome to redistribute it under certain conditions.</string>
<!-- global settings -->
@@ -117,6 +123,7 @@
<string name="account_delete">Delete account</string>
<string name="account_delete_confirmation_title">Really delete account?</string>
<string name="account_delete_confirmation_text">All local copies of address books, calendars and task lists will be deleted.</string>
<string name="account_select_collections_hint">Select collections to synchronize</string>
<string name="account_carddav">CardDAV</string>
<string name="account_caldav">CalDAV</string>
<string name="account_webcal">Webcal</string>
@@ -132,7 +139,6 @@
<string name="account_install_icsdroid">Install ICSdroid</string>
<!-- AddAccountActivity -->
<string name="login_help_url">https://www.davdroid.com/tested-with/?pk_campaign=davdroid-app</string>
<string name="login_title">Add account</string>
<string name="login_type_email">Login with email address</string>
<string name="login_email_address">Email address</string>
@@ -160,7 +166,7 @@
<string name="login_configuration_detection">Configuration detection</string>
<string name="login_querying_server">Please wait, querying server…</string>
<string name="login_no_caldav_carddav">Couldn\'t find CalDAV or CardDAV service.</string>
<string name="login_view_logs">View logs</string>
<string name="login_view_logs">Show details</string>
<!-- AccountSettingsActivity -->
<string name="settings_title">Settings: %s</string>
@@ -252,6 +258,9 @@
<string name="delete_collection_confirm_warning">This collection (%s) and all its data will be removed from the server.</string>
<string name="delete_collection_deleting_collection">Deleting collection</string>
<string name="collection_force_read_only">Force read-only</string>
<string name="collection_properties">Properties</string>
<string name="collection_properties_url">Address (URL):</string>
<string name="collection_properties_copy_url">Copy URL</string>
<!-- ExceptionInfoFragment -->
<string name="exception">An error has occurred.</string>

View File

@@ -10,4 +10,5 @@
android:accountType="@string/account_type"
android:contentAuthority="@string/address_books_authority"
android:isAlwaysSyncable="true"
android:allowParallelSyncs="true"
android:supportsUploading="false" />

View File

@@ -9,7 +9,5 @@
<sync-adapter xmlns:android="http://schemas.android.com/apk/res/android"
android:accountType="@string/account_type"
android:contentAuthority="com.android.calendar"
android:allowParallelSyncs="true"
android:supportsUploading="true"
android:isAlwaysSyncable="true"
android:userVisible="true" />
android:isAlwaysSyncable="true" />

View File

@@ -9,6 +9,5 @@
<sync-adapter xmlns:android="http://schemas.android.com/apk/res/android"
android:accountType="@string/account_type_address_book"
android:contentAuthority="com.android.contacts"
android:allowParallelSyncs="true"
android:supportsUploading="true"
android:isAlwaysSyncable="true" />

View File

@@ -9,6 +9,4 @@
<sync-adapter xmlns:android="http://schemas.android.com/apk/res/android"
android:accountType="@string/account_type"
android:contentAuthority="org.dmfs.tasks"
android:allowParallelSyncs="true"
android:supportsUploading="true"
android:userVisible="true" />
android:supportsUploading="true" />

View File

@@ -9,7 +9,7 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext.dokka_version = '0.9.16'
ext.kotlin_version = '1.2.41'
ext.kotlin_version = '1.2.51'
repositories {
jcenter()

View File

@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-4.8-all.zip