Compare commits
38 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ce56047da3 | ||
|
|
3186b89874 | ||
|
|
1b0557d4b8 | ||
|
|
5efe1dbed0 | ||
|
|
d5f1074e30 | ||
|
|
5e32dc7c91 | ||
|
|
1914269811 | ||
|
|
f1d92b0bfe | ||
|
|
3c76eb79d6 | ||
|
|
86e9cb4ace | ||
|
|
3c6ff6e6ac | ||
|
|
1082dc367d | ||
|
|
6e935c433c | ||
|
|
4536378ac9 | ||
|
|
042fb6fefa | ||
|
|
29faa422ba | ||
|
|
563737b410 | ||
|
|
384b91ca77 | ||
|
|
1ec933128d | ||
|
|
f7df046af9 | ||
|
|
1622d6d53e | ||
|
|
4ba318fa14 | ||
|
|
1eecbad457 | ||
|
|
4b9aa376b3 | ||
|
|
9d7c72c3ca | ||
|
|
f41025d4d8 | ||
|
|
59c765a6e8 | ||
|
|
6ff069ac57 | ||
|
|
050f86f020 | ||
|
|
e45c8ab1eb | ||
|
|
dd30677334 | ||
|
|
822b49cfcf | ||
|
|
c667c18199 | ||
|
|
33491336af | ||
|
|
502a40c179 | ||
|
|
c0bb073a57 | ||
|
|
a5a3fbb969 | ||
|
|
9c8219108b |
@@ -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'
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 6.6 KiB After Width: | Height: | Size: 6.6 KiB |
|
Before Width: | Height: | Size: 8.8 KiB After Width: | Height: | Size: 8.8 KiB |
|
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 45 KiB |
|
Before Width: | Height: | Size: 8.8 KiB After Width: | Height: | Size: 8.8 KiB |
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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)
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 ->
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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()!!
|
||||
|
||||
}
|
||||
|
||||
@@ -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()))
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
}
|
||||
|
||||
|
||||
13
app/src/main/res/drawable/ic_content_copy_dark.xml
Normal 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>
|
||||
5
app/src/main/res/drawable/ic_home_action.xml
Normal 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>
|
||||
18
app/src/main/res/drawable/ic_touch_app_dark.xml
Normal 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>
|
||||
11
app/src/main/res/drawable/ic_touch_app_dark_compat.xml
Normal 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>
|
||||
@@ -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>
|
||||
88
app/src/main/res/layout/about_davdroid.xml
Normal 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>
|
||||
@@ -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"
|
||||
|
||||
56
app/src/main/res/layout/collection_properties.xml
Normal 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>
|
||||
19
app/src/main/res/menu/about_davdroid.xml
Normal 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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 © 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>“Copyright” also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.</p>
|
||||
|
||||
|
||||
<p>“The Program” refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as “you”. “Licensees” and
|
||||
“recipients” 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 “Corresponding Source” 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
|
||||
“aggregate” 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 “contributor” 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 “contributor version”.</p>
|
||||
work thus licensed is called the contributor\'s “contributor version”.</p>
|
||||
|
||||
<p>A contributor's “essential patent claims” are all patent claims
|
||||
<p>A contributor\'s “essential patent claims” 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. “Knowingly relying” 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>
|
||||
@@ -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>
|
||||
|
||||
@@ -10,4 +10,5 @@
|
||||
android:accountType="@string/account_type"
|
||||
android:contentAuthority="@string/address_books_authority"
|
||||
android:isAlwaysSyncable="true"
|
||||
android:allowParallelSyncs="true"
|
||||
android:supportsUploading="false" />
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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()
|
||||
|
||||
2
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -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
|
||||
|
||||