Compare commits
203 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 | ||
|
|
f85e435303 | ||
|
|
4e4d3b60aa | ||
|
|
2848f0c466 | ||
|
|
49512be453 | ||
|
|
4cae527723 | ||
|
|
0baf10c962 | ||
|
|
a8f0c8ea24 | ||
|
|
baa044d61e | ||
|
|
6352113c1e | ||
|
|
99c1fd94c5 | ||
|
|
4e3e281892 | ||
|
|
5445f2ab72 | ||
|
|
c089c3d369 | ||
|
|
6fef958d5f | ||
|
|
6d34def40c | ||
|
|
ed06106c23 | ||
|
|
866841afc0 | ||
|
|
8a76167eca | ||
|
|
8587100853 | ||
|
|
fff7677703 | ||
|
|
a7aded904d | ||
|
|
a76cc5a805 | ||
|
|
9ec5bd51f5 | ||
|
|
9c15749257 | ||
|
|
faced361d8 | ||
|
|
b67e42b91a | ||
|
|
1012dbfe4b | ||
|
|
22bd34ce60 | ||
|
|
330c2bd49d | ||
|
|
bf2287550c | ||
|
|
95033a20fd | ||
|
|
f8dec15c97 | ||
|
|
3ea1512f95 | ||
|
|
a546823cb9 | ||
|
|
9582e07944 | ||
|
|
a1b0427bfc | ||
|
|
4cf4cecf0a | ||
|
|
842648d602 | ||
|
|
4fdcc077f4 | ||
|
|
c0f0f8a83c | ||
|
|
c586bab08b | ||
|
|
e81ae958aa | ||
|
|
051530fa7d | ||
|
|
628937e109 | ||
|
|
cd3662ce43 | ||
|
|
d694b480c4 | ||
|
|
afc02d5ab5 | ||
|
|
171cda098a | ||
|
|
19b660333f | ||
|
|
e6419ccefc | ||
|
|
2a783bef3a | ||
|
|
34c08b299c | ||
|
|
f2d9221239 | ||
|
|
32651978cc | ||
|
|
437a055c81 | ||
|
|
65ef5cd1d9 | ||
|
|
cac1339b61 | ||
|
|
d6c11b7f39 | ||
|
|
4cfb0af588 | ||
|
|
b2ad46e41c | ||
|
|
b36731705a | ||
|
|
4df8aba2ac | ||
|
|
5e0ed389c2 | ||
|
|
486c7db99c | ||
|
|
58556447f5 | ||
|
|
afd614fa19 | ||
|
|
3a69f66ba8 | ||
|
|
edadc4e260 | ||
|
|
0db859f3db | ||
|
|
a3a3cf8259 | ||
|
|
c065c48702 | ||
|
|
0f5f2a3331 | ||
|
|
d7bf4f95a5 | ||
|
|
8c82d21ecc | ||
|
|
619012e54e | ||
|
|
87ab0ca05b | ||
|
|
6f7f35abcc | ||
|
|
3c6f6145f0 | ||
|
|
1fb90762e0 | ||
|
|
3fe1d428a3 | ||
|
|
c47394b021 | ||
|
|
faa49350c9 | ||
|
|
511dde66ad | ||
|
|
c43f016dc7 | ||
|
|
c45e4a1797 | ||
|
|
3a734c9e68 | ||
|
|
e0e4a026e6 | ||
|
|
6f6182c0ce | ||
|
|
4158ec9aee | ||
|
|
c49333998f | ||
|
|
bc4f4b5dfd | ||
|
|
dbd5bde458 | ||
|
|
be4c680497 | ||
|
|
d7c5ed23b7 | ||
|
|
25a328a3c4 | ||
|
|
dd3d95bdb9 | ||
|
|
0a47935430 | ||
|
|
2a83f98f0a | ||
|
|
2224a6d2ae | ||
|
|
30968f8ee3 | ||
|
|
7f9be9a8da | ||
|
|
a516800f45 | ||
|
|
0c92b02d73 | ||
|
|
95354096ab | ||
|
|
d8a54f823b | ||
|
|
ceedd218ca | ||
|
|
1627770103 | ||
|
|
82b1da5f0d | ||
|
|
5e70b97942 | ||
|
|
43e216642b | ||
|
|
4d17bc673f | ||
|
|
cf24bfa965 | ||
|
|
3d746e7019 | ||
|
|
9a30207316 | ||
|
|
dcb38e89a3 | ||
|
|
2aed1ee97d | ||
|
|
0a87a15822 | ||
|
|
0bca883d76 | ||
|
|
e4c282cd99 | ||
|
|
fbc733c16e | ||
|
|
60d3473f0f | ||
|
|
0d6d2fcff9 | ||
|
|
6d4fbd419d | ||
|
|
d163cbb882 | ||
|
|
47c1b26953 | ||
|
|
7ac36a64a2 | ||
|
|
dcd2250349 | ||
|
|
6dc4a2d01e | ||
|
|
b044eab80e | ||
|
|
ad8a264dce | ||
|
|
f9912456af | ||
|
|
abcaaf8605 | ||
|
|
47534adad0 | ||
|
|
502c0db95b | ||
|
|
8d81798931 | ||
|
|
14563b7ab6 | ||
|
|
c322ecb774 | ||
|
|
167317b04f | ||
|
|
621c16a100 | ||
|
|
9ceb2423e7 | ||
|
|
95c4a96ae3 | ||
|
|
045e6ceea9 | ||
|
|
512f21ecc3 | ||
|
|
8a90227d19 | ||
|
|
da5e812ca2 | ||
|
|
2c14bd13c8 | ||
|
|
918fb1bd48 | ||
|
|
f6e1c915da | ||
|
|
eb7ffbee57 | ||
|
|
68bc301b7b | ||
|
|
5d18bfcae9 | ||
|
|
8072516b9a | ||
|
|
5ed9c66a0a | ||
|
|
01fba4cd31 | ||
|
|
a8ab6a1b8d | ||
|
|
f20d193087 | ||
|
|
4472d43044 | ||
|
|
4a201b81ae | ||
|
|
d54ea0edad | ||
|
|
909cb96e5f | ||
|
|
8f7d377f12 | ||
|
|
2a01489dcc | ||
|
|
93ba407a3f | ||
|
|
0e4b2820f5 | ||
|
|
826578ed89 |
38
README.md
@@ -1,38 +0,0 @@
|
||||
|
||||
[](https://gitlab.com/bitfireAT/davdroid/commits/master)
|
||||
|
||||
|
||||
DAVdroid
|
||||
========
|
||||
|
||||
Please see the [DAVdroid Web site](https://davdroid.bitfire.at) for
|
||||
comprehensive information about DAVdroid.
|
||||
|
||||
DAVdroid is licensed under the [GPLv3 License](LICENSE).
|
||||
|
||||
News and updates: [@davdroidapp](https://twitter.com/davdroidapp) on Twitter /
|
||||
[davdroid-announce](https://davdroid.bitfire.at/download/newsletter/) mailing list
|
||||
|
||||
Help and discussion: [DAVdroid forums](https://davdroid.bitfire.at/forums)
|
||||
|
||||
**If you want to support DAVdroid, please consider [donating to DAVdroid](https://davdroid.bitfire.at/donate/)
|
||||
or [purchasing it](https://davdroid.bitfire.at/download/).**
|
||||
|
||||
Parts of DAVdroid have been outsourced into these libraries:
|
||||
|
||||
* [cert4android](https://gitlab.com/bitfireAT/cert4android) – custom certificate management
|
||||
* [dav4android](https://gitlab.com/bitfireAT/dav4android) – WebDAV/CalDav/CardDAV framework
|
||||
* [ical4android](https://gitlab.com/bitfireAT/ical4android) – iCalendar processing and Calendar Provider access
|
||||
* [vcard4android](https://gitlab.com/bitfireAT/vcard4android) – VCard processing and Contacts Provider access
|
||||
|
||||
|
||||
USED THIRD-PARTY LIBRARIES
|
||||
==========================
|
||||
|
||||
Those libraries are used by DAVdroid (alphabetically):
|
||||
|
||||
* [dnsjava](http://www.xbill.org/dnsjava/) – [BSD License](http://www.xbill.org/dnsjava/dnsjava-current/LICENSE)
|
||||
* [ez-vcard](https://code.google.com/p/ez-vcard/) – [New BSD License](http://opensource.org/licenses/BSD-3-Clause)
|
||||
* [iCal4j](http://ical4j.sourceforge.net/) – [New BSD License](http://sourceforge.net/p/ical4j/ical4j/ci/default/tree/LICENSE)
|
||||
* [okhttp](https://square.github.io/okhttp/) – [Apache License, Version 2.0](https://square.github.io/okhttp/#license)
|
||||
* [Project Lombok](http://projectlombok.org/) – [MIT License](http://opensource.org/licenses/mit-license.php)
|
||||
104
app/build.gradle
@@ -9,53 +9,73 @@
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlin-android-extensions'
|
||||
apply plugin: 'org.jetbrains.dokka-android'
|
||||
|
||||
ext {
|
||||
baseVersionName = '2.0'
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion 26
|
||||
buildToolsVersion '26.0.1'
|
||||
compileSdkVersion 27
|
||||
buildToolsVersion '28.0.1'
|
||||
|
||||
defaultConfig {
|
||||
applicationId "at.bitfire.davdroid"
|
||||
resValue "string", "packageID", applicationId
|
||||
|
||||
versionCode 182
|
||||
versionCode 241
|
||||
buildConfigField "long", "buildTime", System.currentTimeMillis() + "L"
|
||||
|
||||
minSdkVersion 16
|
||||
targetSdkVersion 25
|
||||
minSdkVersion 19 // Android 4.4
|
||||
targetSdkVersion 27 // Android 8.1
|
||||
|
||||
buildConfigField "boolean", "customCerts", "false"
|
||||
buildConfigField "boolean", "customCertsUI", "true"
|
||||
|
||||
// when using this, make sure that notification icons are real bitmaps
|
||||
vectorDrawables.useSupportLibrary = true
|
||||
}
|
||||
|
||||
flavorDimensions "type"
|
||||
productFlavors {
|
||||
standard {
|
||||
versionName "1.9"
|
||||
dimension "type"
|
||||
versionName baseVersionName
|
||||
|
||||
buildConfigField "boolean", "customCerts", "true"
|
||||
}
|
||||
managed {
|
||||
dimension "type"
|
||||
versionName "$baseVersionName-mgd"
|
||||
|
||||
applicationId "com.davdroid.managed"
|
||||
resValue "string", "packageID", applicationId
|
||||
minSdkVersion 21
|
||||
|
||||
versionName "1.9"
|
||||
buildConfigField "boolean", "customCerts", "true"
|
||||
buildConfigField "boolean", "customCertsUI", "false"
|
||||
minSdkVersion 21
|
||||
}
|
||||
|
||||
gplay {
|
||||
versionName "1.9-gplay"
|
||||
dimension "type"
|
||||
versionName "$baseVersionName-gplay"
|
||||
|
||||
buildConfigField "boolean", "customCerts", "true"
|
||||
}
|
||||
icloud {
|
||||
dimension "type"
|
||||
versionName "$baseVersionName-icloud"
|
||||
|
||||
applicationId "at.bitfire.cloudsync"
|
||||
resValue "string", "packageID", applicationId
|
||||
|
||||
versionName "1.9-cloud"
|
||||
}
|
||||
soldupe {
|
||||
dimension "type"
|
||||
versionName "$baseVersionName-soldupe"
|
||||
|
||||
applicationId "com.soldupe.cloudsync"
|
||||
resValue "string", "packageID", applicationId
|
||||
minSdkVersion 21
|
||||
|
||||
versionName "1.9-soldupe"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,6 +84,7 @@ android {
|
||||
standard.res.srcDirs = [ "src/davdroid/res" ]
|
||||
gplay.java.srcDirs = [ "src/gplay/java", "src/davdroid/java" ]
|
||||
gplay.res.srcDirs = [ "src/gplay/res", "src/davdroid/res" ]
|
||||
androidTest.java.srcDirs = [ "src/androidTest/java", "src/espressoTest/java" ]
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
@@ -96,13 +117,9 @@ android {
|
||||
|
||||
lintOptions {
|
||||
disable 'GoogleAppIndexingWarning' // we don't need Google indexing, thanks
|
||||
disable 'GradleDependency'
|
||||
disable 'IconColors'
|
||||
disable 'IconLauncherShape'
|
||||
disable 'IconMissingDensityFolder'
|
||||
disable 'ImpliedQuantity', 'MissingQuantity'
|
||||
disable 'ImpliedQuantity', 'MissingQuantity' // quantities from Transifex may vary
|
||||
disable 'MissingTranslation', 'ExtraTranslation' // translations from Transifex are not always up to date
|
||||
disable 'Recycle' // doesn't understand Lombok's @Cleanup
|
||||
disable "OnClick" // doesn't recognize Kotlin onClick methods
|
||||
disable 'RtlEnabled'
|
||||
disable 'RtlHardcoded'
|
||||
disable 'Typos'
|
||||
@@ -118,38 +135,33 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compile project(':cert4android')
|
||||
compile project(':dav4android')
|
||||
compile project(':ical4android')
|
||||
compile project(':vcard4android')
|
||||
implementation project(':cert4android')
|
||||
implementation project(':dav4android')
|
||||
implementation project(':ical4android')
|
||||
implementation project(':vcard4android')
|
||||
|
||||
compile "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||
|
||||
compile 'com.android.support:appcompat-v7:26.0.1'
|
||||
compile 'com.android.support:cardview-v7:26.0.1'
|
||||
compile 'com.android.support:design:26.0.1'
|
||||
compile 'com.android.support:preference-v14:26.0.1'
|
||||
implementation 'com.android.support:appcompat-v7:27.1.1'
|
||||
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'
|
||||
|
||||
compile 'com.github.yukuku:ambilwarna:2.0.1'
|
||||
implementation 'com.github.yukuku:ambilwarna:2.0.1'
|
||||
implementation 'com.mikepenz:aboutlibraries:6.0.9'
|
||||
|
||||
compile 'com.squareup.okhttp3:logging-interceptor:3.9.0'
|
||||
compile 'commons-io:commons-io:2.5'
|
||||
compile 'dnsjava:dnsjava:2.1.8'
|
||||
compile 'org.apache.commons:commons-lang3:3.6'
|
||||
compile 'org.apache.commons:commons-collections4:4.1'
|
||||
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'
|
||||
implementation 'org.apache.commons:commons-collections4:4.1'
|
||||
|
||||
// for tests
|
||||
//noinspection GradleDynamicVersion
|
||||
androidTestCompile('com.android.support.test:runner:+') {
|
||||
exclude group: 'com.android.support', module: 'support-annotations'
|
||||
}
|
||||
//noinspection GradleDynamicVersion
|
||||
androidTestCompile('com.android.support.test:rules:+') {
|
||||
exclude group: 'com.android.support', module: 'support-annotations'
|
||||
}
|
||||
androidTestCompile 'junit:junit:4.12'
|
||||
androidTestCompile 'com.squareup.okhttp3:mockwebserver:3.9.0'
|
||||
androidTestImplementation 'com.android.support.test:runner:1.0.2'
|
||||
androidTestImplementation 'com.android.support.test:rules:1.0.2'
|
||||
androidTestImplementation 'junit:junit:4.12'
|
||||
androidTestImplementation 'com.squareup.okhttp3:mockwebserver:3.11.0'
|
||||
|
||||
testCompile 'junit:junit:4.12'
|
||||
testCompile 'com.squareup.okhttp3:mockwebserver:3.9.0'
|
||||
testImplementation 'junit:junit:4.12'
|
||||
testImplementation 'com.squareup.okhttp3:mockwebserver:3.11.0'
|
||||
}
|
||||
|
||||
@@ -19,11 +19,12 @@
|
||||
-dontwarn ezvcard.io.json.** # JSON serializer (for jCards) not used
|
||||
-dontwarn freemarker.** # freemarker templating library (for creating hCards) not used
|
||||
-dontwarn org.jsoup.** # jsoup library (for hCard parsing) not used
|
||||
-keep class ezvcard.property.** { *; } # keep all VCard properties (created at runtime)
|
||||
-keep class ezvcard.property.** { *; } # keep all vCard properties (created at runtime)
|
||||
|
||||
# ical4j: ignore unused dynamic libraries
|
||||
-dontwarn aQute.**
|
||||
-dontwarn groovy.** # Groovy-based ContentBuilder not used
|
||||
-dontwarn javax.cache.** # no JCache support in Android
|
||||
-dontwarn net.fortuna.ical4j.model.**
|
||||
-dontwarn org.codehaus.groovy.**
|
||||
-dontwarn org.apache.log4j.** # ignore warnings from log4j dependency
|
||||
@@ -34,6 +35,7 @@
|
||||
-dontwarn okio.**
|
||||
-dontwarn javax.annotation.Nullable
|
||||
-dontwarn javax.annotation.ParametersAreNonnullByDefault
|
||||
-dontwarn org.conscrypt.**
|
||||
|
||||
# dnsjava
|
||||
-dontwarn sun.net.spi.nameservice.** # not available on Android
|
||||
|
||||
1
app/src/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
espressoTest
|
||||
@@ -0,0 +1,128 @@
|
||||
/*
|
||||
* 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
|
||||
|
||||
import android.support.test.InstrumentationRegistry.getInstrumentation
|
||||
import at.bitfire.cert4android.CustomCertManager
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.mockwebserver.MockWebServer
|
||||
import org.apache.commons.io.IOUtils
|
||||
import org.apache.commons.lang3.ArrayUtils.contains
|
||||
import org.junit.After
|
||||
import org.junit.Assert.*
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import java.net.Socket
|
||||
import java.security.KeyFactory
|
||||
import java.security.KeyStore
|
||||
import java.security.Principal
|
||||
import java.security.cert.CertificateFactory
|
||||
import java.security.cert.X509Certificate
|
||||
import java.security.spec.PKCS8EncodedKeySpec
|
||||
import javax.net.ssl.SSLSocket
|
||||
import javax.net.ssl.TrustManagerFactory
|
||||
import javax.net.ssl.X509ExtendedKeyManager
|
||||
import javax.net.ssl.X509TrustManager
|
||||
|
||||
class CustomTlsSocketFactoryTest {
|
||||
|
||||
private lateinit var certMgr: CustomCertManager
|
||||
private lateinit var factory: CustomTlsSocketFactory
|
||||
private val server = MockWebServer()
|
||||
|
||||
@Before
|
||||
fun startServer() {
|
||||
certMgr = CustomCertManager(getInstrumentation().context, false, true)
|
||||
factory = CustomTlsSocketFactory(null, certMgr)
|
||||
server.start()
|
||||
}
|
||||
|
||||
@After
|
||||
fun stopServer() {
|
||||
server.shutdown()
|
||||
certMgr.close()
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun testSendClientCertificate() {
|
||||
var public: X509Certificate? = null
|
||||
javaClass.classLoader.getResourceAsStream("sample.crt").use {
|
||||
public = CertificateFactory.getInstance("X509").generateCertificate(it) as? X509Certificate
|
||||
}
|
||||
assertNotNull(public)
|
||||
|
||||
val keyFactory = KeyFactory.getInstance("RSA")
|
||||
val private = keyFactory.generatePrivate(PKCS8EncodedKeySpec(readResource("sample.key")))
|
||||
assertNotNull(private)
|
||||
|
||||
val keyStore = KeyStore.getInstance("AndroidKeyStore")
|
||||
keyStore.load(null)
|
||||
val alias = "sample"
|
||||
keyStore.setKeyEntry(alias, private, null, arrayOf(public))
|
||||
assertTrue(keyStore.containsAlias(alias))
|
||||
|
||||
val trustManagerFactory = TrustManagerFactory.getInstance("X509")
|
||||
trustManagerFactory.init(null as KeyStore?)
|
||||
val trustManager = trustManagerFactory.trustManagers.first() as X509TrustManager
|
||||
|
||||
val factory = CustomTlsSocketFactory(object: X509ExtendedKeyManager() {
|
||||
override fun getServerAliases(p0: String?, p1: Array<out Principal>?): Array<String>? = null
|
||||
override fun chooseServerAlias(p0: String?, p1: Array<out Principal>?, p2: Socket?) = null
|
||||
|
||||
override fun getClientAliases(p0: String?, p1: Array<out Principal>?) =
|
||||
arrayOf(alias)
|
||||
|
||||
override fun chooseClientAlias(p0: Array<out String>?, p1: Array<out Principal>?, p2: Socket?) =
|
||||
alias
|
||||
|
||||
override fun getCertificateChain(forAlias: String?) =
|
||||
arrayOf(public).takeIf { forAlias == alias }
|
||||
|
||||
override fun getPrivateKey(forAlias: String?) =
|
||||
private.takeIf { forAlias == alias }
|
||||
}, trustManager)
|
||||
|
||||
/* known client cert test URLs (thanks!):
|
||||
* - https://prod.idrix.eu/secure/
|
||||
* - https://server.cryptomix.com/secure/
|
||||
*/
|
||||
val client = OkHttpClient.Builder()
|
||||
.sslSocketFactory(factory, trustManager)
|
||||
.build()
|
||||
client.newCall(Request.Builder()
|
||||
.get()
|
||||
.url("https://prod.idrix.eu/secure/")
|
||||
.build()).execute().use { response ->
|
||||
assertTrue(response.isSuccessful)
|
||||
assertTrue(response.body()!!.string().contains("CN=User Cert,O=Internet Widgits Pty Ltd,ST=Some-State,C=CA"))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testUpgradeTLS() {
|
||||
val s = factory.createSocket(server.hostName, server.port)
|
||||
assertTrue(s is SSLSocket)
|
||||
val ssl = s as SSLSocket
|
||||
|
||||
assertFalse(contains(ssl.enabledProtocols, "SSLv3"))
|
||||
assertTrue(contains(ssl.enabledProtocols, "TLSv1"))
|
||||
assertTrue(contains(ssl.enabledProtocols, "TLSv1.1"))
|
||||
assertTrue(contains(ssl.enabledProtocols, "TLSv1.2"))
|
||||
}
|
||||
|
||||
|
||||
private fun readResource(name: String): ByteArray {
|
||||
this.javaClass.classLoader.getResourceAsStream(name).use {
|
||||
return IOUtils.toByteArray(it)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,63 +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;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.Socket;
|
||||
|
||||
import javax.net.ssl.SSLSocket;
|
||||
import javax.net.ssl.TrustManagerFactory;
|
||||
import javax.net.ssl.X509TrustManager;
|
||||
|
||||
import at.bitfire.cert4android.CustomCertManager;
|
||||
import okhttp3.mockwebserver.MockWebServer;
|
||||
|
||||
import static android.support.test.InstrumentationRegistry.getInstrumentation;
|
||||
import static android.support.test.InstrumentationRegistry.getTargetContext;
|
||||
import static junit.framework.TestCase.assertFalse;
|
||||
import static org.apache.commons.lang3.ArrayUtils.contains;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class SSLSocketFactoryCompatTest {
|
||||
|
||||
CustomCertManager certMgr;
|
||||
SSLSocketFactoryCompat factory;
|
||||
MockWebServer server = new MockWebServer();
|
||||
|
||||
@Before
|
||||
public void startServer() throws Exception {
|
||||
certMgr = new CustomCertManager(getInstrumentation().getContext(), true, null);
|
||||
factory = new SSLSocketFactoryCompat(certMgr);
|
||||
server.start();
|
||||
}
|
||||
|
||||
@After
|
||||
public void stopServer() throws Exception {
|
||||
server.shutdown();
|
||||
certMgr.close();
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testUpgradeTLS() throws IOException {
|
||||
Socket s = factory.createSocket(server.getHostName(), server.getPort());
|
||||
assertTrue(s instanceof SSLSocket);
|
||||
|
||||
SSLSocket ssl = (SSLSocket)s;
|
||||
assertFalse(contains(ssl.getEnabledProtocols(), "SSLv3"));
|
||||
assertTrue(contains(ssl.getEnabledProtocols(), "TLSv1"));
|
||||
assertTrue(contains(ssl.getEnabledProtocols(), "TLSv1.1"));
|
||||
assertTrue(contains(ssl.getEnabledProtocols(), "TLSv1.2"));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,133 +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.model;
|
||||
|
||||
import android.content.ContentValues;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import at.bitfire.dav4android.DavResource;
|
||||
import at.bitfire.dav4android.exception.DavException;
|
||||
import at.bitfire.dav4android.exception.HttpException;
|
||||
import at.bitfire.dav4android.property.ResourceType;
|
||||
import at.bitfire.davdroid.HttpClient;
|
||||
import at.bitfire.davdroid.model.ServiceDB.Collections;
|
||||
import okhttp3.mockwebserver.MockResponse;
|
||||
import okhttp3.mockwebserver.MockWebServer;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class CollectionInfoTest {
|
||||
|
||||
HttpClient httpClient;
|
||||
MockWebServer server = new MockWebServer();
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
httpClient = new HttpClient.Builder().build();
|
||||
}
|
||||
|
||||
@After
|
||||
public void shutDown() {
|
||||
httpClient.close();
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testFromDavResource() throws IOException, HttpException, DavException {
|
||||
// r/w address book
|
||||
server.enqueue(new MockResponse()
|
||||
.setResponseCode(207)
|
||||
.setBody("<multistatus xmlns='DAV:' xmlns:CARD='urn:ietf:params:xml:ns:carddav'>" +
|
||||
"<response>" +
|
||||
" <href>/</href>" +
|
||||
" <propstat><prop>" +
|
||||
" <resourcetype><collection/><CARD:addressbook/></resourcetype>" +
|
||||
" <displayname>My Contacts</displayname>" +
|
||||
" <CARD:addressbook-description>My Contacts Description</CARD:addressbook-description>" +
|
||||
" </prop></propstat>" +
|
||||
"</response>" +
|
||||
"</multistatus>"));
|
||||
|
||||
DavResource dav = new DavResource(httpClient.getOkHttpClient(), server.url("/"));
|
||||
dav.propfind(0, ResourceType.NAME);
|
||||
CollectionInfo info = new CollectionInfo(dav);
|
||||
assertEquals(CollectionInfo.Type.ADDRESS_BOOK, info.getType());
|
||||
assertFalse(info.getReadOnly());
|
||||
assertEquals("My Contacts", info.getDisplayName());
|
||||
assertEquals("My Contacts Description", info.getDescription());
|
||||
|
||||
// read-only calendar, no display name
|
||||
server.enqueue(new MockResponse()
|
||||
.setResponseCode(207)
|
||||
.setBody("<multistatus xmlns='DAV:' xmlns:CAL='urn:ietf:params:xml:ns:caldav' xmlns:ICAL='http://apple.com/ns/ical/'>" +
|
||||
"<response>" +
|
||||
" <href>/</href>" +
|
||||
" <propstat><prop>" +
|
||||
" <resourcetype><collection/><CAL:calendar/></resourcetype>" +
|
||||
" <current-user-privilege-set><privilege><read/></privilege></current-user-privilege-set>" +
|
||||
" <CAL:calendar-description>My Calendar</CAL:calendar-description>" +
|
||||
" <CAL:calendar-timezone>tzdata</CAL:calendar-timezone>" +
|
||||
" <ICAL:calendar-color>#ff0000</ICAL:calendar-color>" +
|
||||
" </prop></propstat>" +
|
||||
"</response>" +
|
||||
"</multistatus>"));
|
||||
|
||||
dav = new DavResource(httpClient.getOkHttpClient(), server.url("/"));
|
||||
dav.propfind(0, ResourceType.NAME);
|
||||
info = new CollectionInfo(dav);
|
||||
assertEquals(CollectionInfo.Type.CALENDAR, info.getType());
|
||||
assertTrue(info.getReadOnly());
|
||||
assertNull(info.getDisplayName());
|
||||
assertEquals("My Calendar", info.getDescription());
|
||||
assertEquals(0xFFFF0000, (int)info.getColor());
|
||||
assertEquals("tzdata", info.getTimeZone());
|
||||
assertTrue(info.getSupportsVEVENT());
|
||||
assertTrue(info.getSupportsVTODO());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFromDB() {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(Collections.ID, 1);
|
||||
values.put(Collections.SERVICE_ID, 1);
|
||||
values.put(Collections.TYPE, CollectionInfo.Type.CALENDAR.name());
|
||||
values.put(Collections.URL, "http://example.com");
|
||||
values.put(Collections.READ_ONLY, 1);
|
||||
values.put(Collections.DISPLAY_NAME, "display name");
|
||||
values.put(Collections.DESCRIPTION, "description");
|
||||
values.put(Collections.COLOR, 0xFFFF0000);
|
||||
values.put(Collections.TIME_ZONE, "tzdata");
|
||||
values.put(Collections.SUPPORTS_VEVENT, 1);
|
||||
values.put(Collections.SUPPORTS_VTODO, 1);
|
||||
values.put(Collections.SYNC, 1);
|
||||
|
||||
CollectionInfo info = new CollectionInfo(values);
|
||||
assertEquals(CollectionInfo.Type.CALENDAR, info.getType());
|
||||
assertEquals(1, (long)info.getId());
|
||||
assertEquals(1, (long)info.getServiceID());
|
||||
assertEquals("http://example.com", info.getUrl());
|
||||
assertTrue(info.getReadOnly());
|
||||
assertEquals("display name", info.getDisplayName());
|
||||
assertEquals("description", info.getDescription());
|
||||
assertEquals(0xFFFF0000, (int)info.getColor());
|
||||
assertEquals("tzdata", info.getTimeZone());
|
||||
assertTrue(info.getSupportsVEVENT());
|
||||
assertTrue(info.getSupportsVTODO());
|
||||
assertTrue(info.getSelected());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
/*
|
||||
* 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.model
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.support.test.filters.SmallTest
|
||||
import at.bitfire.dav4android.DavResource
|
||||
import at.bitfire.dav4android.property.ResourceType
|
||||
import at.bitfire.davdroid.HttpClient
|
||||
import at.bitfire.davdroid.model.ServiceDB.Collections
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.mockwebserver.MockResponse
|
||||
import okhttp3.mockwebserver.MockWebServer
|
||||
import org.junit.After
|
||||
import org.junit.Assert.*
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
|
||||
class CollectionInfoTest {
|
||||
|
||||
private lateinit var httpClient: HttpClient
|
||||
private val server = MockWebServer()
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
httpClient = HttpClient.Builder().build()
|
||||
}
|
||||
|
||||
@After
|
||||
fun shutDown() {
|
||||
httpClient.close()
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
@SmallTest
|
||||
fun testFromDavResource() {
|
||||
// r/w address book
|
||||
server.enqueue(MockResponse()
|
||||
.setResponseCode(207)
|
||||
.setBody("<multistatus xmlns='DAV:' xmlns:CARD='urn:ietf:params:xml:ns:carddav'>" +
|
||||
"<response>" +
|
||||
" <href>/</href>" +
|
||||
" <propstat><prop>" +
|
||||
" <resourcetype><collection/><CARD:addressbook/></resourcetype>" +
|
||||
" <displayname>My Contacts</displayname>" +
|
||||
" <CARD:addressbook-description>My Contacts Description</CARD:addressbook-description>" +
|
||||
" </prop></propstat>" +
|
||||
"</response>" +
|
||||
"</multistatus>"))
|
||||
|
||||
var info: CollectionInfo? = null
|
||||
DavResource(httpClient.okHttpClient, server.url("/"))
|
||||
.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()
|
||||
.setResponseCode(207)
|
||||
.setBody("<multistatus xmlns='DAV:' xmlns:CAL='urn:ietf:params:xml:ns:caldav' xmlns:ICAL='http://apple.com/ns/ical/'>" +
|
||||
"<response>" +
|
||||
" <href>/</href>" +
|
||||
" <propstat><prop>" +
|
||||
" <resourcetype><collection/><CAL:calendar/></resourcetype>" +
|
||||
" <current-user-privilege-set><privilege><read/></privilege></current-user-privilege-set>" +
|
||||
" <CAL:calendar-description>My Calendar</CAL:calendar-description>" +
|
||||
" <CAL:calendar-timezone>tzdata</CAL:calendar-timezone>" +
|
||||
" <ICAL:calendar-color>#ff0000</ICAL:calendar-color>" +
|
||||
" </prop></propstat>" +
|
||||
"</response>" +
|
||||
"</multistatus>"))
|
||||
|
||||
info = null
|
||||
DavResource(httpClient.okHttpClient, server.url("/"))
|
||||
.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
|
||||
fun testFromDB() {
|
||||
val values = ContentValues()
|
||||
values.put(Collections.ID, 1)
|
||||
values.put(Collections.SERVICE_ID, 1)
|
||||
values.put(Collections.TYPE, CollectionInfo.Type.CALENDAR.name)
|
||||
values.put(Collections.URL, "http://example.com")
|
||||
values.put(Collections.READ_ONLY, 1)
|
||||
values.put(Collections.DISPLAY_NAME, "display name")
|
||||
values.put(Collections.DESCRIPTION, "description")
|
||||
values.put(Collections.COLOR, 0xFFFF0000)
|
||||
values.put(Collections.TIME_ZONE, "tzdata")
|
||||
values.put(Collections.SUPPORTS_VEVENT, 1)
|
||||
values.put(Collections.SUPPORTS_VTODO, 1)
|
||||
values.put(Collections.SYNC, 1)
|
||||
|
||||
val info = CollectionInfo(values)
|
||||
assertEquals(CollectionInfo.Type.CALENDAR, info.type)
|
||||
assertEquals(1.toLong(), info.id)
|
||||
assertEquals(1.toLong(), info.serviceID)
|
||||
assertEquals(HttpUrl.parse("http://example.com/"), info.url)
|
||||
assertTrue(info.readOnly)
|
||||
assertEquals("display name", info.displayName)
|
||||
assertEquals("description", info.description)
|
||||
assertEquals(0xFFFF0000.toInt(), info.color)
|
||||
assertEquals("tzdata", info.timeZone)
|
||||
assertTrue(info.supportsVEVENT)
|
||||
assertTrue(info.supportsVTODO)
|
||||
assertTrue(info.selected)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -8,11 +8,11 @@
|
||||
|
||||
package at.bitfire.davdroid.settings
|
||||
|
||||
import android.content.ServiceConnection
|
||||
import android.support.test.InstrumentationRegistry
|
||||
import android.support.test.InstrumentationRegistry.getTargetContext
|
||||
import at.bitfire.davdroid.App
|
||||
import junit.framework.Assert.*
|
||||
import junit.framework.Assert.assertFalse
|
||||
import junit.framework.Assert.assertTrue
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
|
||||
@@ -1,200 +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.ui.setup;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
|
||||
import at.bitfire.dav4android.DavResource;
|
||||
import at.bitfire.dav4android.exception.DavException;
|
||||
import at.bitfire.dav4android.exception.HttpException;
|
||||
import at.bitfire.dav4android.property.AddressbookHomeSet;
|
||||
import at.bitfire.dav4android.property.ResourceType;
|
||||
import at.bitfire.davdroid.HttpClient;
|
||||
import at.bitfire.davdroid.log.Logger;
|
||||
import at.bitfire.davdroid.ui.setup.DavResourceFinder.Configuration.ServiceInfo;
|
||||
import okhttp3.mockwebserver.Dispatcher;
|
||||
import okhttp3.mockwebserver.MockResponse;
|
||||
import okhttp3.mockwebserver.MockWebServer;
|
||||
import okhttp3.mockwebserver.RecordedRequest;
|
||||
|
||||
import static android.support.test.InstrumentationRegistry.getTargetContext;
|
||||
import static junit.framework.TestCase.assertFalse;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class DavResourceFinderTest {
|
||||
|
||||
MockWebServer server = new MockWebServer();
|
||||
|
||||
DavResourceFinder finder;
|
||||
HttpClient client;
|
||||
LoginCredentials credentials;
|
||||
|
||||
private static final String
|
||||
PATH_NO_DAV = "/nodav",
|
||||
|
||||
PATH_CALDAV = "/caldav",
|
||||
PATH_CARDDAV = "/carddav",
|
||||
PATH_CALDAV_AND_CARDDAV = "/both-caldav-carddav",
|
||||
|
||||
SUBPATH_PRINCIPAL = "/principal",
|
||||
SUBPATH_ADDRESSBOOK_HOMESET = "/addressbooks",
|
||||
SUBPATH_ADDRESSBOOK = "/addressbooks/private-contacts";
|
||||
|
||||
@Before
|
||||
public void initServerAndClient() throws Exception {
|
||||
server.setDispatcher(new TestDispatcher());
|
||||
server.start();
|
||||
|
||||
credentials = new LoginCredentials(URI.create("/"), "mock", "12345");
|
||||
finder = new DavResourceFinder(getTargetContext(), credentials);
|
||||
|
||||
client = new HttpClient.Builder()
|
||||
.addAuthentication(null, credentials.getUserName(), credentials.getPassword())
|
||||
.build();
|
||||
}
|
||||
|
||||
@After
|
||||
public void stopServer() throws Exception {
|
||||
server.shutdown();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRememberIfAddressBookOrHomeset() throws IOException, HttpException, DavException {
|
||||
ServiceInfo info;
|
||||
|
||||
// before dav.propfind(), no info is available
|
||||
DavResource dav = new DavResource(client.getOkHttpClient(), server.url(PATH_CARDDAV + SUBPATH_PRINCIPAL));
|
||||
finder.rememberIfAddressBookOrHomeset(dav, info = new ServiceInfo());
|
||||
assertEquals(0, info.getCollections().size());
|
||||
assertEquals(0, info.getHomeSets().size());
|
||||
|
||||
// recognize home set
|
||||
dav.propfind(0, AddressbookHomeSet.NAME);
|
||||
finder.rememberIfAddressBookOrHomeset(dav, info = new ServiceInfo());
|
||||
assertEquals(0, info.getCollections().size());
|
||||
assertEquals(1, info.getHomeSets().size());
|
||||
assertEquals(server.url(PATH_CARDDAV + SUBPATH_ADDRESSBOOK_HOMESET + "/").uri(), info.getHomeSets().iterator().next());
|
||||
|
||||
// recognize address book
|
||||
dav = new DavResource(client.getOkHttpClient(), server.url(PATH_CARDDAV + SUBPATH_ADDRESSBOOK));
|
||||
dav.propfind(0, ResourceType.NAME);
|
||||
finder.rememberIfAddressBookOrHomeset(dav, info = new ServiceInfo());
|
||||
assertEquals(1, info.getCollections().size());
|
||||
assertEquals(server.url(PATH_CARDDAV + SUBPATH_ADDRESSBOOK + "/").uri(), info.getCollections().keySet().iterator().next());
|
||||
assertEquals(0, info.getHomeSets().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testProvidesService() throws IOException {
|
||||
assertFalse(finder.providesService(server.url(PATH_NO_DAV), DavResourceFinder.Service.CALDAV));
|
||||
assertFalse(finder.providesService(server.url(PATH_NO_DAV), DavResourceFinder.Service.CARDDAV));
|
||||
|
||||
assertTrue(finder.providesService(server.url(PATH_CALDAV), DavResourceFinder.Service.CALDAV));
|
||||
assertFalse(finder.providesService(server.url(PATH_CALDAV), DavResourceFinder.Service.CARDDAV));
|
||||
|
||||
assertTrue(finder.providesService(server.url(PATH_CARDDAV), DavResourceFinder.Service.CARDDAV));
|
||||
assertFalse(finder.providesService(server.url(PATH_CARDDAV), DavResourceFinder.Service.CALDAV));
|
||||
|
||||
assertTrue(finder.providesService(server.url(PATH_CALDAV_AND_CARDDAV), DavResourceFinder.Service.CALDAV));
|
||||
assertTrue(finder.providesService(server.url(PATH_CALDAV_AND_CARDDAV), DavResourceFinder.Service.CARDDAV));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetCurrentUserPrincipal() throws IOException, HttpException, DavException {
|
||||
assertNull(finder.getCurrentUserPrincipal(server.url(PATH_NO_DAV), DavResourceFinder.Service.CALDAV));
|
||||
assertNull(finder.getCurrentUserPrincipal(server.url(PATH_NO_DAV), DavResourceFinder.Service.CARDDAV));
|
||||
|
||||
assertEquals(
|
||||
server.url(PATH_CALDAV + SUBPATH_PRINCIPAL).uri(),
|
||||
finder.getCurrentUserPrincipal(server.url(PATH_CALDAV), DavResourceFinder.Service.CALDAV)
|
||||
);
|
||||
assertNull(finder.getCurrentUserPrincipal(server.url(PATH_CALDAV), DavResourceFinder.Service.CARDDAV));
|
||||
|
||||
assertEquals(
|
||||
server.url(PATH_CARDDAV + SUBPATH_PRINCIPAL).uri(),
|
||||
finder.getCurrentUserPrincipal(server.url(PATH_CARDDAV), DavResourceFinder.Service.CARDDAV)
|
||||
);
|
||||
assertNull(finder.getCurrentUserPrincipal(server.url(PATH_CARDDAV), DavResourceFinder.Service.CALDAV));
|
||||
}
|
||||
|
||||
|
||||
// mock server
|
||||
|
||||
public class TestDispatcher extends Dispatcher {
|
||||
|
||||
@Override
|
||||
public MockResponse dispatch(RecordedRequest rq) throws InterruptedException {
|
||||
if (!checkAuth(rq)) {
|
||||
MockResponse authenticate = new MockResponse().setResponseCode(401);
|
||||
authenticate.setHeader("WWW-Authenticate", "Basic realm=\"test\"");
|
||||
return authenticate;
|
||||
}
|
||||
|
||||
String path = rq.getPath();
|
||||
|
||||
if ("OPTIONS".equalsIgnoreCase(rq.getMethod())) {
|
||||
String dav = null;
|
||||
if (path.startsWith(PATH_CALDAV))
|
||||
dav = "calendar-access";
|
||||
else if (path.startsWith(PATH_CARDDAV))
|
||||
dav = "addressbook";
|
||||
else if (path.startsWith(PATH_CALDAV_AND_CARDDAV))
|
||||
dav = "calendar-access, addressbook";
|
||||
MockResponse response = new MockResponse().setResponseCode(200);
|
||||
if (dav != null)
|
||||
response.addHeader("DAV", dav);
|
||||
return response;
|
||||
|
||||
} else if ("PROPFIND".equalsIgnoreCase(rq.getMethod())) {
|
||||
String props = null;
|
||||
switch (path) {
|
||||
case PATH_CALDAV:
|
||||
case PATH_CARDDAV:
|
||||
props = "<current-user-principal><href>" + path + SUBPATH_PRINCIPAL + "</href></current-user-principal>";
|
||||
break;
|
||||
|
||||
case PATH_CARDDAV + SUBPATH_PRINCIPAL:
|
||||
props = "<CARD:addressbook-home-set>" +
|
||||
" <href>" + PATH_CARDDAV + SUBPATH_ADDRESSBOOK_HOMESET + "</href>" +
|
||||
"</CARD:addressbook-home-set>";
|
||||
break;
|
||||
case PATH_CARDDAV + SUBPATH_ADDRESSBOOK:
|
||||
props = "<resourcetype>" +
|
||||
" <collection/>" +
|
||||
" <CARD:addressbook/>" +
|
||||
"</resourcetype>";
|
||||
break;
|
||||
}
|
||||
Logger.log.info("Sending props: " + props);
|
||||
return new MockResponse()
|
||||
.setResponseCode(207)
|
||||
.setBody("<multistatus xmlns='DAV:' xmlns:CARD='urn:ietf:params:xml:ns:carddav'>" +
|
||||
"<response>" +
|
||||
" <href>" + rq.getPath() + "</href>" +
|
||||
" <propstat><prop>" + props + "</prop></propstat>" +
|
||||
"</response>" +
|
||||
"</multistatus>");
|
||||
}
|
||||
|
||||
return new MockResponse().setResponseCode(404);
|
||||
}
|
||||
|
||||
private boolean checkAuth(RecordedRequest rq) {
|
||||
return "Basic bW9jazoxMjM0NQ==".equals(rq.getHeader("Authorization"));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
/*
|
||||
* 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.setup
|
||||
|
||||
import android.support.test.InstrumentationRegistry.getTargetContext
|
||||
import android.support.test.filters.SmallTest
|
||||
import at.bitfire.dav4android.DavResource
|
||||
import at.bitfire.dav4android.property.AddressbookHomeSet
|
||||
import at.bitfire.dav4android.property.ResourceType
|
||||
import at.bitfire.davdroid.HttpClient
|
||||
import at.bitfire.davdroid.log.Logger
|
||||
import at.bitfire.davdroid.model.Credentials
|
||||
import at.bitfire.davdroid.ui.setup.DavResourceFinder.Configuration.ServiceInfo
|
||||
import okhttp3.mockwebserver.Dispatcher
|
||||
import okhttp3.mockwebserver.MockResponse
|
||||
import okhttp3.mockwebserver.MockWebServer
|
||||
import okhttp3.mockwebserver.RecordedRequest
|
||||
import org.junit.After
|
||||
import org.junit.Assert.*
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import java.net.URI
|
||||
|
||||
class DavResourceFinderTest {
|
||||
|
||||
companion object {
|
||||
private const val PATH_NO_DAV = "/nodav"
|
||||
private const val PATH_CALDAV = "/caldav"
|
||||
private const val PATH_CARDDAV = "/carddav"
|
||||
private const val PATH_CALDAV_AND_CARDDAV = "/both-caldav-carddav"
|
||||
|
||||
private const val SUBPATH_PRINCIPAL = "/principal"
|
||||
private const val SUBPATH_ADDRESSBOOK_HOMESET = "/addressbooks"
|
||||
private const val SUBPATH_ADDRESSBOOK = "/addressbooks/private-contacts"
|
||||
}
|
||||
|
||||
val server = MockWebServer()
|
||||
|
||||
lateinit var finder: DavResourceFinder
|
||||
lateinit var client: HttpClient
|
||||
lateinit var loginInfo: LoginInfo
|
||||
|
||||
@Before
|
||||
fun initServerAndClient() {
|
||||
server.setDispatcher(TestDispatcher())
|
||||
server.start()
|
||||
|
||||
loginInfo = LoginInfo(URI.create("/"), Credentials("mock", "12345"))
|
||||
finder = DavResourceFinder(getTargetContext(), loginInfo)
|
||||
|
||||
client = HttpClient.Builder()
|
||||
.addAuthentication(null, loginInfo.credentials)
|
||||
.build()
|
||||
}
|
||||
|
||||
@After
|
||||
fun stopServer() {
|
||||
server.shutdown()
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
@SmallTest
|
||||
fun testRememberIfAddressBookOrHomeset() {
|
||||
// recognize home set
|
||||
var info = ServiceInfo()
|
||||
DavResource(client.okHttpClient, server.url(PATH_CARDDAV + SUBPATH_PRINCIPAL))
|
||||
.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) { 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
|
||||
fun testProvidesService() {
|
||||
assertFalse(finder.providesService(server.url(PATH_NO_DAV), DavResourceFinder.Service.CALDAV))
|
||||
assertFalse(finder.providesService(server.url(PATH_NO_DAV), DavResourceFinder.Service.CARDDAV))
|
||||
|
||||
assertTrue(finder.providesService(server.url(PATH_CALDAV), DavResourceFinder.Service.CALDAV))
|
||||
assertFalse(finder.providesService(server.url(PATH_CALDAV), DavResourceFinder.Service.CARDDAV))
|
||||
|
||||
assertTrue(finder.providesService(server.url(PATH_CARDDAV), DavResourceFinder.Service.CARDDAV))
|
||||
assertFalse(finder.providesService(server.url(PATH_CARDDAV), DavResourceFinder.Service.CALDAV))
|
||||
|
||||
assertTrue(finder.providesService(server.url(PATH_CALDAV_AND_CARDDAV), DavResourceFinder.Service.CALDAV))
|
||||
assertTrue(finder.providesService(server.url(PATH_CALDAV_AND_CARDDAV), DavResourceFinder.Service.CARDDAV))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testGetCurrentUserPrincipal() {
|
||||
assertNull(finder.getCurrentUserPrincipal(server.url(PATH_NO_DAV), DavResourceFinder.Service.CALDAV))
|
||||
assertNull(finder.getCurrentUserPrincipal(server.url(PATH_NO_DAV), DavResourceFinder.Service.CARDDAV))
|
||||
|
||||
assertEquals(
|
||||
server.url(PATH_CALDAV + SUBPATH_PRINCIPAL),
|
||||
finder.getCurrentUserPrincipal(server.url(PATH_CALDAV), DavResourceFinder.Service.CALDAV)
|
||||
)
|
||||
assertNull(finder.getCurrentUserPrincipal(server.url(PATH_CALDAV), DavResourceFinder.Service.CARDDAV))
|
||||
|
||||
assertEquals(
|
||||
server.url(PATH_CARDDAV + SUBPATH_PRINCIPAL),
|
||||
finder.getCurrentUserPrincipal(server.url(PATH_CARDDAV), DavResourceFinder.Service.CARDDAV)
|
||||
)
|
||||
assertNull(finder.getCurrentUserPrincipal(server.url(PATH_CARDDAV), DavResourceFinder.Service.CALDAV))
|
||||
}
|
||||
|
||||
|
||||
// mock server
|
||||
|
||||
class TestDispatcher: Dispatcher() {
|
||||
|
||||
override fun dispatch(rq: RecordedRequest): MockResponse {
|
||||
if (!checkAuth(rq)) {
|
||||
val authenticate = MockResponse().setResponseCode(401)
|
||||
authenticate.setHeader("WWW-Authenticate", "Basic realm=\"test\"")
|
||||
return authenticate
|
||||
}
|
||||
|
||||
val path = rq.path
|
||||
|
||||
if (rq.method.equals("OPTIONS", true)) {
|
||||
val dav = when {
|
||||
path.startsWith(PATH_CALDAV) -> "calendar-access"
|
||||
path.startsWith(PATH_CARDDAV) -> "addressbook"
|
||||
path.startsWith(PATH_CALDAV_AND_CARDDAV) -> "calendar-access, addressbook"
|
||||
else -> null
|
||||
}
|
||||
val response = MockResponse().setResponseCode(200)
|
||||
if (dav != null)
|
||||
response.addHeader("DAV", dav)
|
||||
return response
|
||||
} else if (rq.method.equals("PROPFIND", true)) {
|
||||
val props: String?
|
||||
when (path) {
|
||||
PATH_CALDAV,
|
||||
PATH_CARDDAV ->
|
||||
props = "<current-user-principal><href>$path$SUBPATH_PRINCIPAL</href></current-user-principal>"
|
||||
|
||||
PATH_CARDDAV + SUBPATH_PRINCIPAL ->
|
||||
props = "<CARD:addressbook-home-set>" +
|
||||
" <href>$PATH_CARDDAV$SUBPATH_ADDRESSBOOK_HOMESET</href>" +
|
||||
"</CARD:addressbook-home-set>"
|
||||
|
||||
PATH_CARDDAV + SUBPATH_ADDRESSBOOK ->
|
||||
props = "<resourcetype>" +
|
||||
" <collection/>" +
|
||||
" <CARD:addressbook/>" +
|
||||
"</resourcetype>"
|
||||
|
||||
else -> props = null
|
||||
}
|
||||
Logger.log.info("Sending props: $props")
|
||||
return MockResponse()
|
||||
.setResponseCode(207)
|
||||
.setBody("<multistatus xmlns='DAV:' xmlns:CARD='urn:ietf:params:xml:ns:carddav'>" +
|
||||
"<response>" +
|
||||
" <href>${rq.path}</href>" +
|
||||
" <propstat><prop>$props</prop></propstat>" +
|
||||
"</response>" +
|
||||
"</multistatus>")
|
||||
}
|
||||
|
||||
return MockResponse().setResponseCode(404)
|
||||
}
|
||||
|
||||
private fun checkAuth(rq: RecordedRequest) =
|
||||
rq.getHeader("Authorization") == "Basic bW9jazoxMjM0NQ=="
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
24
app/src/androidTest/resources/sample.crt
Normal file
@@ -0,0 +1,24 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIEEzCCAfsCAQEwDQYJKoZIhvcNAQEFBQAwRjELMAkGA1UEBhMCQ0ExEzARBgNV
|
||||
BAgMClNvbWUtU3RhdGUxEDAOBgNVBAoMB0NBIENlcnQxEDAOBgNVBAMMB0NBIENl
|
||||
cnQwHhcNMTgwMTEzMjAyOTI5WhcNMTkwMTEzMjAyOTI5WjBZMQswCQYDVQQGEwJD
|
||||
QTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0
|
||||
cyBQdHkgTHRkMRIwEAYDVQQDDAlVc2VyIENlcnQwggEiMA0GCSqGSIb3DQEBAQUA
|
||||
A4IBDwAwggEKAoIBAQDqOyHAeG4psE/f6i/eTfwbhn6j7WaFXxZiSOWwpQZmzRrx
|
||||
MrfkABJCk0X7KNgCaJcmBkG9G1Ri4HfKrxvJFswMXknlq+0ulGBk7oDnZM+pihuX
|
||||
3D9VCWMMkCqYhLCGADj2zB2mkX4LpcMRi6XoOetKURE/vcIy7rSLAtJM6ZRdftfh
|
||||
2ZxnautS1Tyujh9Au3NI/+Of80tT/nA+oBJQeT1fB/ga1OQlZP5kjSaA7IPiIbTz
|
||||
QBO+r898MvqK/lwsvOYnWAp7TY03z+vPfCs0zjijZEl9Wrl0hW6o5db5kU1v5bcr
|
||||
p87hxFJsGD2HIr2y6kvYfL2hn+h9iANyYdRnUgapAgMBAAEwDQYJKoZIhvcNAQEF
|
||||
BQADggIBAHANsiJITedXPyp89lVMEmGY3zKtOqgQ3tqjvjlNt2sdPnj7wmZbmrNd
|
||||
sa90S/UwOn8PzEFOVxYy1BPlljlEjtjmc4OHMcm4P4Zv36uawHilmK8V+zT59gCK
|
||||
ftB5FP2TLFUFi2X9o8J06d0xJRE77uewN155NV4RmPuP4b/tMmeixoQppHqLqEr5
|
||||
lgEUnt3Mh1ctmeFQFJR6lJ01hlB0gdpVHIhzrVLTO3uo8ePLJTmxP6tyKl/HXj9F
|
||||
mpVsKb1kriKwbkGczfw99OUZeUVbTwQOR07r0SrG71B7IuDvxIORnhQc1OUjt7ob
|
||||
wjdaZauAHxpGBRu+hw9Yqaxchk9Gldy1nEjGyyVCD0FU5taXbl8PhBWEDc4U9tI+
|
||||
xVNmPpsSuCsbz3Mjd1YIVRGL99vLrKsQcj+TNM+jJKKRKes3ihl+l/0FwG6UuO7L
|
||||
EvjlUg5hOtYi1D7xuYyMjroGBGh7swYMt6w4eCDbcjzcCkaCi0H2pScM/rLBpDjS
|
||||
LIoGCvZ1LBdi933/iOj1/8dxGZwY6fEgcyiD2n0xAgYIniLWjEZXOMdIK5FNTNga
|
||||
Tswanvp+6Noa4oIu/hl/LXvPMsouaWfSEbRe0Dshi3GpLj3YtEHoN9DHB8bn7jy5
|
||||
34By81GT41m5kq3hWP//x9kSHYSADpbovCbKbElU1qSt6vTVR4nq
|
||||
-----END CERTIFICATE-----
|
||||
BIN
app/src/androidTest/resources/sample.key
Normal file
@@ -20,6 +20,11 @@ import at.bitfire.davdroid.settings.ISettings
|
||||
|
||||
class DefaultAccountsDrawerHandler: IAccountsDrawerHandler {
|
||||
|
||||
companion object {
|
||||
private const val BETA_FEEDBACK_URI = "mailto:support@davdroid.com?subject=${BuildConfig.APPLICATION_ID}/${BuildConfig.VERSION_NAME} feedback (${BuildConfig.VERSION_CODE})"
|
||||
}
|
||||
|
||||
|
||||
override fun onSettingsChanged(settings: ISettings?, menu: Menu) {
|
||||
if (BuildConfig.VERSION_NAME.contains("-beta") || BuildConfig.VERSION_NAME.contains("-rc"))
|
||||
menu.findItem(R.id.nav_beta_feedback).isVisible = true
|
||||
@@ -31,20 +36,27 @@ class DefaultAccountsDrawerHandler: IAccountsDrawerHandler {
|
||||
activity.startActivity(Intent(activity, AboutActivity::class.java))
|
||||
R.id.nav_app_settings ->
|
||||
activity.startActivity(Intent(activity, AppSettingsActivity::class.java))
|
||||
R.id.nav_beta_feedback ->
|
||||
activity.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(activity.getString(R.string.beta_feedback_url))))
|
||||
R.id.nav_beta_feedback -> {
|
||||
val intent = Intent(Intent.ACTION_SENDTO, Uri.parse(BETA_FEEDBACK_URI))
|
||||
if (activity.packageManager.resolveActivity(intent, 0) != null)
|
||||
activity.startActivity(intent)
|
||||
}
|
||||
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, App.homepageUrl(activity)
|
||||
.buildUpon().appendEncodedPath("manual/").build()))
|
||||
R.id.nav_faq ->
|
||||
activity.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(activity.getString(R.string.navigation_drawer_faq_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
|
||||
@@ -53,4 +65,4 @@ class DefaultAccountsDrawerHandler: IAccountsDrawerHandler {
|
||||
return true
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -8,9 +8,12 @@
|
||||
|
||||
package at.bitfire.davdroid.ui.setup
|
||||
|
||||
import android.app.Fragment
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.security.KeyChain
|
||||
import android.support.v4.app.Fragment
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
@@ -37,10 +40,10 @@ class DefaultLoginCredentialsFragment: Fragment(), CompoundButton.OnCheckedChang
|
||||
val password = it.getStringExtra(LoginActivity.EXTRA_PASSWORD)
|
||||
|
||||
if (url != null) {
|
||||
v.login_type_url.isChecked = true
|
||||
v.base_url.setText(url)
|
||||
v.user_name.setText(username)
|
||||
v.url_password.setText(password)
|
||||
v.login_type_urlpwd.isChecked = true
|
||||
v.urlpwd_base_url.setText(url)
|
||||
v.urlpwd_user_name.setText(username)
|
||||
v.urlpwd_password.setText(password)
|
||||
} else {
|
||||
v.login_type_email.isChecked = true
|
||||
v.email_address.setText(username)
|
||||
@@ -49,112 +52,155 @@ class DefaultLoginCredentialsFragment: Fragment(), CompoundButton.OnCheckedChang
|
||||
}
|
||||
}
|
||||
|
||||
v.login.setOnClickListener({ _ ->
|
||||
validateLoginData()?.let { credentials ->
|
||||
DetectConfigurationFragment.newInstance(credentials).show(fragmentManager, null)
|
||||
v.urlcert_select_cert.setOnClickListener {
|
||||
KeyChain.choosePrivateKeyAlias(activity, { alias ->
|
||||
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())
|
||||
}
|
||||
|
||||
v.login.setOnClickListener {
|
||||
validateLoginData()?.let { info ->
|
||||
DetectConfigurationFragment.newInstance(info).show(fragmentManager, null)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// initialize to Login by email
|
||||
onCheckedChanged(v)
|
||||
|
||||
v.login_type_email.setOnCheckedChangeListener(this)
|
||||
v.login_type_url.setOnCheckedChangeListener(this)
|
||||
v.login_type_urlpwd.setOnCheckedChangeListener(this)
|
||||
v.login_type_urlcert.setOnCheckedChangeListener(this)
|
||||
|
||||
return v
|
||||
}
|
||||
|
||||
override fun onCheckedChanged(buttonView: CompoundButton, isChecked: Boolean) {
|
||||
onCheckedChanged(view)
|
||||
onCheckedChanged(view!!)
|
||||
}
|
||||
|
||||
private fun onCheckedChanged(v: View) {
|
||||
val loginByEmail = !v.login_type_url.isChecked
|
||||
v.login_type_email_details.visibility = if (loginByEmail) View.VISIBLE else View.GONE
|
||||
v.login_type_url_details.visibility = if (loginByEmail) View.GONE else View.VISIBLE
|
||||
(if (loginByEmail) v.email_address else v.base_url).requestFocus()
|
||||
v.login_type_email_details.visibility = if (v.login_type_email.isChecked) View.VISIBLE else View.GONE
|
||||
v.login_type_urlpwd_details.visibility = if (v.login_type_urlpwd.isChecked) View.VISIBLE else View.GONE
|
||||
v.login_type_urlcert_details.visibility = if (v.login_type_urlcert.isChecked) View.VISIBLE else View.GONE
|
||||
}
|
||||
|
||||
private fun validateLoginData(): LoginCredentials? {
|
||||
if (view.login_type_email.isChecked) {
|
||||
var uri: URI? = null
|
||||
var valid = true
|
||||
private fun validateLoginData(): LoginInfo? {
|
||||
val view = requireNotNull(view)
|
||||
when {
|
||||
// Login with email address
|
||||
view.login_type_email.isChecked -> {
|
||||
var uri: URI? = null
|
||||
var valid = true
|
||||
|
||||
val email = view.email_address.text.toString()
|
||||
if (!email.matches(Regex(".+@.+"))) {
|
||||
view.email_address.error = getString(R.string.login_email_address_error)
|
||||
valid = false
|
||||
} else
|
||||
try {
|
||||
uri = URI("mailto", email, null)
|
||||
} catch(e: URISyntaxException) {
|
||||
view.email_address.error = e.localizedMessage
|
||||
valid = false
|
||||
}
|
||||
|
||||
val password = view.email_password.getText().toString()
|
||||
if (password.isEmpty()) {
|
||||
view.email_password.setError(getString(R.string.login_password_required))
|
||||
valid = false
|
||||
}
|
||||
|
||||
return if (valid && uri != null)
|
||||
LoginCredentials(uri, email, password)
|
||||
else
|
||||
null
|
||||
|
||||
} else if (view.login_type_url.isChecked) {
|
||||
var uri: URI? = null
|
||||
var valid = true
|
||||
|
||||
val baseUrl = Uri.parse(view.base_url.text.toString())
|
||||
val scheme = baseUrl.scheme
|
||||
if (scheme.equals("http", true) || scheme.equals("https", true)) {
|
||||
var host = baseUrl.host
|
||||
if (host.isNullOrBlank()) {
|
||||
view.base_url.error = getString(R.string.login_url_host_name_required)
|
||||
val email = view.email_address.text.toString()
|
||||
if (!email.matches(Regex(".+@.+"))) {
|
||||
view.email_address.error = getString(R.string.login_email_address_error)
|
||||
valid = false
|
||||
} else
|
||||
try {
|
||||
host = IDN.toASCII(host)
|
||||
} catch(e: IllegalArgumentException) {
|
||||
Constants.log.log(Level.WARNING, "Host name not conforming to RFC 3490", e)
|
||||
uri = URI("mailto", email, null)
|
||||
} catch (e: URISyntaxException) {
|
||||
view.email_address.error = e.localizedMessage
|
||||
valid = false
|
||||
}
|
||||
|
||||
val path = baseUrl.encodedPath
|
||||
val port = baseUrl.port
|
||||
try {
|
||||
uri = URI(baseUrl.scheme, null, host, port, path, null, null)
|
||||
} catch(e: URISyntaxException) {
|
||||
view.base_url.error = e.localizedMessage
|
||||
val password = view.email_password.text.toString()
|
||||
if (password.isEmpty()) {
|
||||
view.email_password.error = getString(R.string.login_password_required)
|
||||
valid = false
|
||||
}
|
||||
} else {
|
||||
view.base_url.error = getString(R.string.login_url_must_be_http_or_https)
|
||||
valid = false
|
||||
|
||||
return if (valid && uri != null)
|
||||
LoginInfo(uri, email, password)
|
||||
else
|
||||
null
|
||||
|
||||
}
|
||||
|
||||
val userName = view.user_name.text.toString()
|
||||
if (userName.isBlank()) {
|
||||
view.user_name.error = getString(R.string.login_user_name_required)
|
||||
valid = false
|
||||
// Login with URL and user name
|
||||
view.login_type_urlpwd.isChecked -> {
|
||||
var valid = true
|
||||
|
||||
val baseUrl = Uri.parse(view.urlpwd_base_url.text.toString())
|
||||
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()) {
|
||||
view.urlpwd_user_name.error = getString(R.string.login_user_name_required)
|
||||
valid = false
|
||||
}
|
||||
|
||||
val password = view.urlpwd_password.text.toString()
|
||||
if (password.isEmpty()) {
|
||||
view.urlpwd_password.error = getString(R.string.login_password_required)
|
||||
valid = false
|
||||
}
|
||||
|
||||
return if (valid && uri != null)
|
||||
LoginInfo(uri, userName, password)
|
||||
else
|
||||
null
|
||||
}
|
||||
|
||||
val password = view.url_password.getText().toString()
|
||||
if (password.isEmpty()) {
|
||||
view.url_password.setError(getString(R.string.login_password_required))
|
||||
valid = false
|
||||
}
|
||||
// Login with URL and client certificate
|
||||
view.login_type_urlcert.isChecked -> {
|
||||
var valid = true
|
||||
|
||||
return if (valid && uri != null)
|
||||
LoginCredentials(uri, userName, password)
|
||||
else
|
||||
null
|
||||
val baseUrl = Uri.parse(view.urlcert_base_url.text.toString())
|
||||
val uri = validateBaseUrl(baseUrl, true) { message ->
|
||||
view.urlcert_base_url.error = message
|
||||
valid = false
|
||||
}
|
||||
|
||||
val alias = view.urlcert_cert_alias.text.toString()
|
||||
if (alias.isEmpty()) {
|
||||
view.urlcert_cert_alias.error = ""
|
||||
valid = false
|
||||
}
|
||||
|
||||
if (valid && uri != null)
|
||||
return LoginInfo(uri, certificateAlias = alias)
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
private fun validateBaseUrl(baseUrl: Uri, httpsRequired: Boolean, reportError: (String) -> Unit): URI? {
|
||||
var uri: URI? = null
|
||||
val scheme = baseUrl.scheme
|
||||
if ((!httpsRequired && scheme.equals("http", true)) || scheme.equals("https", true)) {
|
||||
var host = baseUrl.host
|
||||
if (host.isNullOrBlank())
|
||||
reportError(getString(R.string.login_url_host_name_required))
|
||||
else
|
||||
try {
|
||||
host = IDN.toASCII(host)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
Constants.log.log(Level.WARNING, "Host name not conforming to RFC 3490", e)
|
||||
}
|
||||
|
||||
val path = baseUrl.encodedPath
|
||||
val port = baseUrl.port
|
||||
try {
|
||||
uri = URI(baseUrl.scheme, null, host, port, path, null, null)
|
||||
} catch (e: URISyntaxException) {
|
||||
reportError(e.localizedMessage)
|
||||
}
|
||||
} else
|
||||
reportError(getString(if (httpsRequired)
|
||||
R.string.login_url_must_be_https
|
||||
else
|
||||
R.string.login_url_must_be_http_or_https))
|
||||
return uri
|
||||
}
|
||||
|
||||
|
||||
class Factory: ILoginCredentialsFragment {
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ Copyright © 2013 – 2016 Ricki Hirner (bitfire web engineering).
|
||||
~ 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
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
@@ -38,61 +39,141 @@
|
||||
android:id="@+id/login_type_email"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:checked="true"
|
||||
android:text="@string/login_type_email"
|
||||
android:paddingLeft="14dp" tools:ignore="RtlSymmetry"
|
||||
style="@style/login_type_headline"/>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/login_type_email_details"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingBottom="16dp"
|
||||
android:orientation="vertical">
|
||||
|
||||
<EditText
|
||||
android:id="@+id/email_address"
|
||||
<android.support.design.widget.TextInputLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
<android.support.design.widget.TextInputEditText
|
||||
android:id="@+id/email_address"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/login_email_address"
|
||||
android:inputType="textEmailAddress"/>
|
||||
</android.support.design.widget.TextInputLayout>
|
||||
<android.support.design.widget.TextInputLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/login_email_address"
|
||||
android:inputType="textEmailAddress"/>
|
||||
<at.bitfire.davdroid.ui.widget.EditPassword
|
||||
android:id="@+id/email_password"
|
||||
android:hint="@string/login_password"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"/>
|
||||
app:passwordToggleEnabled="true">
|
||||
<android.support.design.widget.TextInputEditText
|
||||
android:id="@+id/email_password"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/login_password"
|
||||
android:fontFamily="monospace"
|
||||
android:inputType="textPassword"/>
|
||||
</android.support.design.widget.TextInputLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<RadioButton
|
||||
android:id="@+id/login_type_url"
|
||||
android:id="@+id/login_type_urlpwd"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/login_type_url"
|
||||
android:layout_marginTop="16dp"
|
||||
android:paddingLeft="14dp" tools:ignore="RtlSymmetry"
|
||||
style="@style/login_type_headline"/>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/login_type_url_details"
|
||||
android:id="@+id/login_type_urlpwd_details"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingBottom="16dp"
|
||||
android:orientation="vertical">
|
||||
|
||||
<EditText
|
||||
android:id="@+id/base_url"
|
||||
<android.support.design.widget.TextInputLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
<android.support.design.widget.TextInputEditText
|
||||
android:id="@+id/urlpwd_base_url"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/login_base_url"
|
||||
android:inputType="textUri"/>
|
||||
</android.support.design.widget.TextInputLayout>
|
||||
<android.support.design.widget.TextInputLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
<android.support.design.widget.TextInputEditText
|
||||
android:id="@+id/urlpwd_user_name"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/login_user_name"
|
||||
android:inputType="textEmailAddress"/>
|
||||
</android.support.design.widget.TextInputLayout>
|
||||
<android.support.design.widget.TextInputLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/login_base_url"
|
||||
android:inputType="textUri"/>
|
||||
<EditText
|
||||
android:id="@+id/user_name"
|
||||
app:passwordToggleEnabled="true">
|
||||
<android.support.design.widget.TextInputEditText
|
||||
android:id="@+id/urlpwd_password"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:fontFamily="monospace"
|
||||
android:inputType="textPassword"
|
||||
android:hint="@string/login_password"/>
|
||||
</android.support.design.widget.TextInputLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<RadioButton
|
||||
android:id="@+id/login_type_urlcert"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/login_type_url_certificate"
|
||||
android:layout_marginTop="16dp"
|
||||
android:paddingLeft="14dp" tools:ignore="RtlSymmetry"
|
||||
style="@style/login_type_headline"/>
|
||||
<LinearLayout
|
||||
android:id="@+id/login_type_urlcert_details"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingBottom="16dp"
|
||||
android:orientation="vertical">
|
||||
|
||||
<android.support.design.widget.TextInputLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/login_user_name"
|
||||
android:inputType="textEmailAddress"/>
|
||||
<at.bitfire.davdroid.ui.widget.EditPassword
|
||||
android:id="@+id/url_password"
|
||||
android:layout_height="wrap_content">
|
||||
<android.support.design.widget.TextInputEditText
|
||||
android:id="@+id/urlcert_base_url"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/login_base_url"
|
||||
android:inputType="textUri"/>
|
||||
</android.support.design.widget.TextInputLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/login_password"/>
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/urlcert_cert_alias"
|
||||
android:layout_width="0dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingLeft="3dp"
|
||||
android:paddingRight="3dp"
|
||||
style="@style/Base.TextAppearance.AppCompat.Body1"
|
||||
android:textSize="16sp"/>
|
||||
|
||||
<Button
|
||||
android:id="@+id/urlcert_select_cert"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
style="@style/Widget.AppCompat.Button.Borderless.Colored"
|
||||
android:text="@string/login_select_certificate"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
6
app/src/davdroid/res/mipmap-anydpi-v26/ic_launcher.xml
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/primaryColor" />
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
||||
|
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 |
BIN
app/src/davdroid/res/mipmap/ic_launcher_foreground.png
Normal file
|
After Width: | Height: | Size: 8.8 KiB |
@@ -2,19 +2,110 @@
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<!--common strings-->
|
||||
<string name="app_name">DAVdroid</string>
|
||||
<string name="account_title_address_book">DAVdroid Llibreta d’Adreces</string>
|
||||
<string name="address_books_authority_title">Llibreta d’adreces</string>
|
||||
<string name="help">Ajuda</string>
|
||||
<string name="manage_accounts">Gestiona comptes</string>
|
||||
<string name="please_wait">Esperi si us plau...</string>
|
||||
<string name="send">Enviar</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_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>
|
||||
<string name="startup_donate_message">Som feliços de que utilitzis DAVdroid, el qual és programari lliure (GPLv3). Desenvolupar DAVdroid ens porte moltes hores i molta feina, si us plau, considera fer una donació.</string>
|
||||
<string name="startup_donate_now">Mostra la pàgina de donació</string>
|
||||
<string name="startup_donate_later">Més tard</string>
|
||||
<string name="startup_google_play_accounts_removed">Informació d\'errors DRM a Play Store</string>
|
||||
<string name="startup_more_info">Més informació</string>
|
||||
<string name="startup_opentasks_not_installed">OpenTasks no està instal·lada</string>
|
||||
<string name="startup_opentasks_not_installed_install">Instal·lar OpenTasks</string>
|
||||
<!--AboutActivity-->
|
||||
<!--global settings-->
|
||||
<string name="logging_no_external_storage">Emmagatzematge extern no trobat</string>
|
||||
<!--AccountsActivity-->
|
||||
<string name="navigation_drawer_about">Quan a / Llicència</string>
|
||||
<string name="navigation_drawer_beta_feedback">Retroacció de la Beta</string>
|
||||
<string name="navigation_drawer_settings">Paràmetres</string>
|
||||
<string name="navigation_drawer_news_updates">Novetats & Actualitzacions</string>
|
||||
<string name="navigation_drawer_external_links">Enllaços externs</string>
|
||||
<string name="navigation_drawer_website">Pàgina web</string>
|
||||
<string name="navigation_drawer_manual">Manual</string>
|
||||
<string name="navigation_drawer_faq">Preguntes Freqüents</string>
|
||||
<string name="navigation_drawer_forums">Ajuda / Fòrums</string>
|
||||
<string name="navigation_drawer_donate">Fer donació</string>
|
||||
<string name="accounts_global_sync_enable">Activar</string>
|
||||
<!--DavService-->
|
||||
<!--AppSettingsActivity-->
|
||||
<string name="app_settings">Paràmetres</string>
|
||||
<string name="app_settings_user_interface">Interfície d’usuari</string>
|
||||
<string name="app_settings_reset_hints">Reiniciar pistes</string>
|
||||
<string name="app_settings_security">Seguretat</string>
|
||||
<string name="app_settings_debug">Depurador</string>
|
||||
<!--AccountActivity-->
|
||||
<!--PermissionsActivity-->
|
||||
<string name="account_synchronize_now">Sincronitza ara</string>
|
||||
<string name="account_synchronizing_now">Sincronitzant ara</string>
|
||||
<string name="account_settings">Paràmetres del compte</string>
|
||||
<string name="account_rename">Canvia el nom al compte</string>
|
||||
<string name="account_rename_rename">Canvia el nom</string>
|
||||
<string name="account_delete">Esborra el compte</string>
|
||||
<string name="account_delete_confirmation_title">Segur que vols esborrar el compte?</string>
|
||||
<string name="account_carddav">CardDAV</string>
|
||||
<string name="account_caldav">CalDAV</string>
|
||||
<string name="account_webcal">Webcal</string>
|
||||
<string name="account_synchronize_this_collection">Sincronitzar aquesta col·lecció</string>
|
||||
<string name="account_read_only">Només de lectura</string>
|
||||
<string name="account_calendar">Calendari</string>
|
||||
<string name="account_task_list">Llista de tasques</string>
|
||||
<string name="account_refresh_address_book_list">Actualitza la llista de llibretes d’adreces</string>
|
||||
<string name="account_create_new_address_book">Crear nova llibreta d’adreces</string>
|
||||
<string name="account_refresh_calendar_list">Actualitzar llista de calendaris</string>
|
||||
<string name="account_create_new_calendar">Crear nou calendari</string>
|
||||
<string name="account_install_icsdroid">Instal·lar ICSdroid</string>
|
||||
<!--AddAccountActivity-->
|
||||
<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>
|
||||
<string name="login_email_address_error">Es requereix un correu electrònic vàlid</string>
|
||||
<string name="login_password">Contrasenya</string>
|
||||
<string name="login_password_required">Es requereix contrasenya</string>
|
||||
<string name="login_type_url">Entra amb una URL i un nom d\'usuari</string>
|
||||
<string name="login_url_must_be_http_or_https">La URL hauria de començar amb http(s)://</string>
|
||||
<string name="login_url_must_be_https">La URL hauria de començar amb https://</string>
|
||||
<string name="login_url_host_name_required">Es requereix nom d\'amfitrió</string>
|
||||
<string name="login_user_name">Nom d\'usuari/a</string>
|
||||
<string name="login_user_name_required">Es requereix nom d\'usuari/a</string>
|
||||
<string name="login_base_url">URL base</string>
|
||||
<string name="login_select_certificate">Selecciona certificat</string>
|
||||
<string name="login_login">Entrada</string>
|
||||
<string name="login_back">Sortir</string>
|
||||
<string name="login_create_account">Crear compte</string>
|
||||
<string name="login_account_name">Nom del compte</string>
|
||||
<!--AccountSettingsActivity-->
|
||||
<string name="settings_title">Paràmetres: %s</string>
|
||||
<string name="settings_authentication">Autentificació</string>
|
||||
<string name="settings_username">Nom d\'usuari/a</string>
|
||||
<string name="settings_enter_username">Escriu el nom d\'usuari/a:</string>
|
||||
<string name="settings_password">Contrasenya</string>
|
||||
<string name="settings_enter_password">Escriu la teva contrasenya</string>
|
||||
<string name="settings_sync">Sincronització</string>
|
||||
<string name="settings_sync_summary_manually">Només a mà</string>
|
||||
<string-array name="settings_sync_interval_names">
|
||||
<item>Només a mà</item>
|
||||
<item>Cada 15 minuts</item>
|
||||
<item>Cada 30 minuts</item>
|
||||
<item>Cada hora</item>
|
||||
<item>Cada 2 hores</item>
|
||||
<item>Cada 4 hores</item>
|
||||
<item>Una vegada al dia</item>
|
||||
</string-array>
|
||||
<string name="settings_sync_wifi_only">Sincronitzar només amb WI-FI</string>
|
||||
<string name="settings_carddav">CardDAV</string>
|
||||
<string name="settings_caldav">CalDAV</string>
|
||||
<!--collection management-->
|
||||
<!--ExceptionInfoFragment-->
|
||||
<!--sync adapters and DebugInfoActivity-->
|
||||
|
||||
@@ -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>
|
||||
@@ -17,13 +15,10 @@
|
||||
<string name="startup_donate_later">Možná později</string>
|
||||
<string name="startup_google_play_accounts_removed">Informace o chybě DRM Obchodu Play</string>
|
||||
<string name="startup_google_play_accounts_removed_message">Za určitých podmínek může dojít po restartu nebo aktualizaci aplikace DAVdroid k vymazání účtů kvůli chybě DRM Obchodu Play. Pokud jste postiženi touto chybou (ale pouze v tomto případě), nainstalujte prosím z Obchodu Play aplikaci \"DAVdroid JB Workaround\".</string>
|
||||
<string name="startup_google_play_accounts_removed_more_info">Více informací</string>
|
||||
<string name="startup_opentasks_not_installed">OpenTasks není nainstalován</string>
|
||||
<string name="startup_opentasks_not_installed_message">Aplikace OpenTasks není dostupná, proto nebude DAVdroid moci synchronizovat seznam úkolů.</string>
|
||||
<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>
|
||||
@@ -84,17 +79,6 @@
|
||||
<string name="account_create_new_address_book">Vytvořit nový adresář</string>
|
||||
<string name="account_refresh_calendar_list">Obnovit seznam kalendářů</string>
|
||||
<string name="account_create_new_calendar">Vytvořit nový kalendář</string>
|
||||
<!--PermissionsActivity-->
|
||||
<string name="permissions_title">DAVdroid oprávnění</string>
|
||||
<string name="permissions_calendar">Oprávnění pro kalendáře</string>
|
||||
<string name="permissions_calendar_details">Pro synchronizaci CalDAV událostí s místním kalendářem potřebuje DAVdroid oprávnění přistupovat ke kalendářům.</string>
|
||||
<string name="permissions_calendar_request">Vyžádat oprávnění kalendáře</string>
|
||||
<string name="permissions_contacts">Oprávnění pro kontakty</string>
|
||||
<string name="permissions_contacts_details">Pro synchronizaci CardDAV adresářů s místními kontakty potřebuje DAVdroid oprávnění přistupovat ke kontaktům.</string>
|
||||
<string name="permissions_contacts_request">Vyžádat oprávnění kontaktů</string>
|
||||
<string name="permissions_opentasks">Oprávnění pro OpenTasks</string>
|
||||
<string name="permissions_opentasks_details">Pro synchronizaci CalDAV událostí s místním seznamem úkolů potřebuje DAVdroid oprávnění přistupovat k OpenTasks.</string>
|
||||
<string name="permissions_opentasks_request">Vyžádat oprávnění OpenTasks</string>
|
||||
<!--AddAccountActivity-->
|
||||
<string name="login_title">Přidat účet</string>
|
||||
<string name="login_type_email">Přihlášení s emailovou adresou</string>
|
||||
@@ -119,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>
|
||||
@@ -153,6 +136,7 @@
|
||||
<plurals name="settings_sync_time_range_past_days">
|
||||
<item quantity="one">Ignorovat události starší než 1 den</item>
|
||||
<item quantity="few">Ignorovat události starší než %d dny</item>
|
||||
<item quantity="many">Ignorovat události starší než %d dnů</item>
|
||||
<item quantity="other">Ignorovat události starší než %d dnů</item>
|
||||
</plurals>
|
||||
<string name="settings_sync_time_range_past_message">Události z minulosti starší než vyznačený počet dnů budou ignorovány (lze zadat 0). Ponechte prázdné pro synchronizaci všech událostí.</string>
|
||||
@@ -189,27 +173,6 @@
|
||||
<string name="debug_info_title">Ladící informace</string>
|
||||
<string name="sync_error_permissions">DAVdroid oprávnění</string>
|
||||
<string name="sync_error_permissions_text">Vyžadována dodatečná oprávnění</string>
|
||||
<string name="sync_error_calendar">Synchronizace kalendáře selhala (%s)</string>
|
||||
<string name="sync_error_contacts">Synchronizace adresáře selhala (%s)</string>
|
||||
<string name="sync_error_tasks">Synchronizace úkolu selhala (%s)</string>
|
||||
<string name="sync_error">Chyba při %s</string>
|
||||
<string name="sync_error_http_dav">Chyba serveru při %s</string>
|
||||
<string name="sync_error_local_storage">Chyba databáze při %s</string>
|
||||
<string-array name="sync_error_phases">
|
||||
<item>příprava synchronizace</item>
|
||||
<item>dotazování možností</item>
|
||||
<item>zpracovávání místně smazaných záznamů</item>
|
||||
<item>příprava vytvořených/upravených záznamů</item>
|
||||
<item>nahrávání vytvořených/upravených záznamů</item>
|
||||
<item>kontrola stavu synchronizace</item>
|
||||
<item>výpis místních záznamů</item>
|
||||
<item>výpis vzdálených záznamů</item>
|
||||
<item>porovnání místních/vzdálených záznamů</item>
|
||||
<item>stahování vzdálených záznamů</item>
|
||||
<item>uzavírání procesu</item>
|
||||
<item>ukládání stavu synchronizace</item>
|
||||
</string-array>
|
||||
<string name="sync_error_unauthorized">Chybné uživatelské jméno/heslo</string>
|
||||
<!--cert4android-->
|
||||
<string name="certificate_notification_connection_security">DAVdroid: Zabezpečení připojení</string>
|
||||
<string name="trust_certificate_unknown_certificate_found">DAVdroid nalezl neznámý certifikát. Chcete mu důvěřovat?</string>
|
||||
|
||||
@@ -2,58 +2,64 @@
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<!--common strings-->
|
||||
<string name="app_name">DAVdroid</string>
|
||||
<string name="account_title_address_book">DAVdroid Addressebog</string>
|
||||
<string name="account_title_address_book">DAVdroid adressebog</string>
|
||||
<string name="address_books_authority_title">Adressebøger</string>
|
||||
<string name="help">Hjælp</string>
|
||||
<string name="manage_accounts">Administrer konti</string>
|
||||
<string name="please_wait">Vent venligst ...</string>
|
||||
<string name="manage_accounts">Administrere konti</string>
|
||||
<string name="please_wait">Vent...</string>
|
||||
<string name="send">Send</string>
|
||||
<string name="notification_channel_debugging">Fejlsøgning</string>
|
||||
<string name="notification_channel_general">Andre vigtige beskeder</string>
|
||||
<string name="notification_channel_sync">Synkronisering</string>
|
||||
<string name="notification_channel_sync_errors">Synkroniserings-fejl</string>
|
||||
<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_battery_optimization">Batterioptimering</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">Deaktiver DAVdroid</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>
|
||||
<string name="startup_donate">Open-Source information</string>
|
||||
<string name="startup_donate_message">Det glæder os, at du bruger DAVdroid, som er open source-software (GPLv3). Det er hårdt arbejde at udvikle DAVdroid og taget tusindvis af arbejdstimer, så overvej at donere til projektet.</string>
|
||||
<string name="startup_donate_now">Vis donationsside</string>
|
||||
<string name="startup_donate_later">Måske senere</string>
|
||||
<string name="startup_google_play_accounts_removed">Play Store DRM-fejl: Information</string>
|
||||
<string name="startup_google_play_accounts_removed">Play Store DRM fejl information</string>
|
||||
<string name="startup_google_play_accounts_removed_message">Under visse tekniske omstændigheder kan DRM fra Play Store bevirke, at alle DAVdroid-konti er væk efter en genstart eller opgradering af DAVdroid. Hvis du er udsat for dette problem (og ellers ikke), opfordres du til at installere DAVDroid JB Workaround\" fra Play Store.</string>
|
||||
<string name="startup_google_play_accounts_removed_more_info">Yderligere oplysninger</string>
|
||||
<string name="startup_more_info">Mere information</string>
|
||||
<string name="startup_opentasks_not_installed">OpenTasks ikke installeret</string>
|
||||
<string name="startup_opentasks_not_installed_message">OpenTasks er ikke til rådighedm så DAVdroid vil ikke kunne synkronisere opgavelister.</string>
|
||||
<string name="startup_opentasks_not_installed_message">Du er nødt til at have den gratis app OpenTasks for at kunne synkronisere opgaver. (Ikke påkrævet for kontakter/begivenheder.)</string>
|
||||
<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">Installer OpenTasks</string>
|
||||
<string name="startup_opentasks_not_installed_install">Installere OpenTasks</string>
|
||||
<!--AboutActivity-->
|
||||
<string name="about_license_terms">Licensforhold</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>
|
||||
<string name="logging_to_external_storage">Logger til eksternt datalager: %s</string>
|
||||
<string name="logging_couldnt_create_file">Kunne ikke oprette ekstern logfil: %s</string>
|
||||
<string name="logging_no_external_storage">Eksternt lager ikke funder</string>
|
||||
<string name="logging_no_external_storage">Eksternt lager ikke fundet</string>
|
||||
<!--AccountsActivity-->
|
||||
<string name="navigation_drawer_open">Åbn navigationsvindue</string>
|
||||
<string name="navigation_drawer_close">Luk navigationsvindue</string>
|
||||
<string name="navigation_drawer_subtitle">CalDAV/CardDAV Sync-adapter</string>
|
||||
<string name="navigation_drawer_about">Om / Licens</string>
|
||||
<string name="navigation_drawer_settings">Indstillinger</string>
|
||||
<string name="navigation_drawer_subtitle">CalDAV/CardDAV synkroniseringsadapter</string>
|
||||
<string name="navigation_drawer_about">Om / licens</string>
|
||||
<string name="navigation_drawer_beta_feedback">Beta tilbagemelding</string>
|
||||
<string name="navigation_drawer_settings">Opsætning</string>
|
||||
<string name="navigation_drawer_news_updates">Nyheder & opdateringer</string>
|
||||
<string name="navigation_drawer_external_links">Eksterne links</string>
|
||||
<string name="navigation_drawer_website">Hjemmeside</string>
|
||||
<string name="navigation_drawer_faq">FAQ</string>
|
||||
<string name="navigation_drawer_external_links">Eksterne henvisninger</string>
|
||||
<string name="navigation_drawer_website">Netsted</string>
|
||||
<string name="navigation_drawer_manual">Manual</string>
|
||||
<string name="navigation_drawer_faq">OSS</string>
|
||||
<string name="navigation_drawer_forums">Hjælp / fora</string>
|
||||
<string name="navigation_drawer_donate">Donation</string>
|
||||
<string name="account_list_empty">Velkommen til DAVdroid!\n\nDu kan nu tilføje en CaDAV/CardDAV-konto.</string>
|
||||
<string name="account_list_empty">Velkommen til DAVdroid!\n\nDu kan nu tilføje en CalDAV/CardDAV konto.</string>
|
||||
<string name="accounts_global_sync_disabled">Automatisk synkronisering på tværs af systemet er deaktiveret</string>
|
||||
<string name="accounts_global_sync_enable">Aktiver</string>
|
||||
<string name="accounts_global_sync_enable">Aktivere</string>
|
||||
<!--DavService-->
|
||||
<string name="dav_service_refresh_failed">Registrering af tjeneste kunne ikke foretages</string>
|
||||
<string name="dav_service_refresh_couldnt_refresh">Kunne opdatere liste over sæt</string>
|
||||
<string name="dav_service_refresh_couldnt_refresh">Kunne ikke opdatere samling liste</string>
|
||||
<!--AppSettingsActivity-->
|
||||
<string name="app_settings">Indstillinger</string>
|
||||
<string name="app_settings_user_interface">Brugerflade</string>
|
||||
<string name="app_settings_reset_hints">Nulstil vejledende popups</string>
|
||||
<string name="app_settings_reset_hints_summary">Genaktiverer hjælp, som er blevet lukket tidligere</string>
|
||||
<string name="app_settings_reset_hints">Nulstil vejledende pop op</string>
|
||||
<string name="app_settings_reset_hints_summary">Genaktivere tidligere lukket pop op</string>
|
||||
<string name="app_settings_reset_hints_success">Al vejledning vil blive vist igen</string>
|
||||
<string name="app_settings_connection">Forbindelse</string>
|
||||
<string name="app_settings_override_proxy">Tilsidesæt proxyindstillinger</string>
|
||||
@@ -68,79 +74,93 @@
|
||||
<string name="app_settings_reset_certificates">Nulstil (ikke-)betroede certifikater</string>
|
||||
<string name="app_settings_reset_certificates_summary">Nulstiller tilliden til brugerdefinerede certifikater</string>
|
||||
<string name="app_settings_reset_certificates_success">Alle brugerdefinerede certifikater er blevet rydet</string>
|
||||
<string name="app_settings_debug">Debugging</string>
|
||||
<string name="app_settings_debug">Fejlsøgning</string>
|
||||
<string name="app_settings_log_to_external_storage">Log til ekstern fil</string>
|
||||
<string name="app_settings_log_to_external_storage_on">Logger til eksternt lager (hvis muligt)</string>
|
||||
<string name="app_settings_log_to_external_storage_off">Ekstern logning deaktiveret</string>
|
||||
<string name="app_settings_show_debug_info">Vis debug-info</string>
|
||||
<string name="app_settings_show_debug_info">Vis fejlsøgnings information</string>
|
||||
<string name="app_settings_show_debug_info_details">Vis/del software og opsætningsoplysninger</string>
|
||||
<!--AccountActivity-->
|
||||
<string name="account_synchronize_now">Synkroniser nu</string>
|
||||
<string name="account_synchronizing_now">Synkroniserer nu</string>
|
||||
<string name="account_synchronize_now">Synkronisere</string>
|
||||
<string name="account_synchronizing_now">Synkroniserer</string>
|
||||
<string name="account_settings">Opsætning af konti</string>
|
||||
<string name="account_rename">Omdøb konto</string>
|
||||
<string name="account_rename">Omdøbe konto</string>
|
||||
<string name="account_rename_new_name">Lokaldata der ikke er gemt kan gå tabt. Eftersynkronisering er krævet efter omdøbning. Nyt kontonavn: </string>
|
||||
<string name="account_rename_rename">Omdøb</string>
|
||||
<string name="account_delete">Slet konto</string>
|
||||
<string name="account_delete_confirmation_title">Ønsker du at slette konto?</string>
|
||||
<string name="account_rename_rename">Omdøbe</string>
|
||||
<string name="account_delete">Slette konto</string>
|
||||
<string name="account_delete_confirmation_title">Slette konto?</string>
|
||||
<string name="account_delete_confirmation_text">Alle lokale kopier af addessebøger, kalendere og opgavelister vil blive slettet.</string>
|
||||
<string name="account_refresh_address_book_list">Opdater adressebogslister</string>
|
||||
<string name="account_create_new_address_book">Opret ny adressebog</string>
|
||||
<string name="account_refresh_calendar_list">Opdater kalenderliste</string>
|
||||
<string name="account_create_new_calendar">Opret ny kalender</string>
|
||||
<!--PermissionsActivity-->
|
||||
<string name="permissions_title">DAVdroid adgangsrettigheder</string>
|
||||
<string name="permissions_calendar">Kalenderadgange</string>
|
||||
<string name="permissions_calendar_details">For at synkronisere CalDAV-begivenheder med dine lokale kalendere, skal DAVdroid have adgang til dine kalendere.</string>
|
||||
<string name="permissions_calendar_request">Anmod om kalenderadgang</string>
|
||||
<string name="permissions_contacts">Kontakter: Adgangsrettigheder</string>
|
||||
<string name="permissions_contacts_details">DAVdroid er nødt til at have adgang til dine kontakter, hvis CardDAV-adressebøger skal kunne synkronisere med dine kontakter.</string>
|
||||
<string name="permissions_contacts_request">Anmod om adgang til kontakter</string>
|
||||
<string name="permissions_opentasks">OpenTasks: Adgangsrettigheder</string>
|
||||
<string name="permissions_opentasks_details">DAVdroid er nødt til at have adgang til OpenTasks, hvis CalDAV-opgaver skal kunne synkronisere med dine lokale opgavelister.</string>
|
||||
<string name="permissions_opentasks_request">Anmod om adgang til OpenTasks</string>
|
||||
<string name="account_carddav">CardDAV</string>
|
||||
<string name="account_caldav">CalDAV</string>
|
||||
<string name="account_webcal">Webcal</string>
|
||||
<string name="account_synchronize_this_collection">synkronisere samling</string>
|
||||
<string name="account_read_only">skrivebeskyttet</string>
|
||||
<string name="account_calendar">kalender</string>
|
||||
<string name="account_task_list">opgave liste</string>
|
||||
<string name="account_refresh_address_book_list">Opdatere adressebogslister</string>
|
||||
<string name="account_create_new_address_book">Oprette ny adressebog</string>
|
||||
<string name="account_refresh_calendar_list">Opdatere kalenderliste</string>
|
||||
<string name="account_create_new_calendar">Oprette ny kalender</string>
|
||||
<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_title">Tilføj konto</string>
|
||||
<string name="login_type_email">Log ind med emailadresse</string>
|
||||
<string name="login_email_address">Emailadresse</string>
|
||||
<string name="login_email_address_error">Gyldig emailadresse påkrævet</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>
|
||||
<string name="login_email_address_error">Gyldig e-post adresse påkrævet</string>
|
||||
<string name="login_password">Adgangskode</string>
|
||||
<string name="login_password_required">Adgangskode påkrævet</string>
|
||||
<string name="login_type_url">Log ind med URL og brugernavn</string>
|
||||
<string name="login_url_must_be_http_or_https">URL skal begynde med http(s)://</string>
|
||||
<string name="login_type_url">Logge ind med URL og brugernavn</string>
|
||||
<string name="login_url_must_be_http_or_https">URL skal starte med http(s)://</string>
|
||||
<string name="login_url_must_be_https">URL skal starte med https://</string>
|
||||
<string name="login_url_host_name_required">Værtsnavn påkrævet</string>
|
||||
<string name="login_user_name">Brugernavn</string>
|
||||
<string name="login_user_name_required">Brugernavn påkrævet</string>
|
||||
<string name="login_base_url">Basis-URL</string>
|
||||
<string name="login_login">Login</string>
|
||||
<string name="login_base_url">Basis URL</string>
|
||||
<string name="login_type_url_certificate">Logge ind med URL og klientcertifikat</string>
|
||||
<string name="login_select_certificate">Vælge certifikat</string>
|
||||
<string name="login_login">Logge ind</string>
|
||||
<string name="login_back">Tilbage</string>
|
||||
<string name="login_create_account">Opret konto</string>
|
||||
<string name="login_create_account">Oprette konto</string>
|
||||
<string name="login_account_name">Kontonavn</string>
|
||||
<string name="login_account_name_info">Brug emailadressen som kontonavn, for Android vil bruge kontonavnet til ORGANIZER-feltet for aktiviteter, som du opretter. Du kan ikke have to konti med samme navn.</string>
|
||||
<string name="login_account_contact_group_method">Gruppering af kontakter:</string>
|
||||
<string name="login_account_name_required">Kontonavn påkrævet</string>
|
||||
<string name="login_account_not_created">Konto kunne ikke oprettes</string>
|
||||
<string name="login_configuration_detection">Check konfiguration</string>
|
||||
<string name="login_querying_server">Vent, forespørger hos serveren...</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>
|
||||
<string name="settings_username">Brugernavn</string>
|
||||
<string name="settings_enter_username">Indtast brugernavn:</string>
|
||||
<string name="settings_enter_username">Indtaste brugernavn:</string>
|
||||
<string name="settings_password">Adgangskode</string>
|
||||
<string name="settings_password_summary">Opdater adgangskoden, så den svarer til din server.</string>
|
||||
<string name="settings_enter_password">Indtast adgangskode:</string>
|
||||
<string name="settings_certificate_alias">Alias for klientcertifikat</string>
|
||||
<string name="settings_sync">Synkronisering</string>
|
||||
<string name="settings_sync_interval_contacts">Synkroniseringsinterval for kontakter</string>
|
||||
<string name="settings_sync_summary_manually">Kun manuelt</string>
|
||||
<string name="settings_sync_summary_periodically" tools:ignore="PluralsCandidate">Hver %d minutter + øjeblikkeligt ved lokale ændringer</string>
|
||||
<string name="settings_sync_interval_calendars">Synkroniseringsinterval for kalender</string>
|
||||
<string name="settings_sync_interval_tasks">Synkroniseringsinterval for opgaver</string>
|
||||
<string-array name="settings_sync_interval_names">
|
||||
<item>Kun manuelt</item>
|
||||
<item>Hvert 15. minut</item>
|
||||
<item>Hver halve time</item>
|
||||
<item>Hver time</item>
|
||||
<item>Hver 2. time</item>
|
||||
<item>Hver 4. time</item>
|
||||
<item>En gang om dagen</item>
|
||||
</string-array>
|
||||
<string name="settings_sync_wifi_only">Synkroniser kun over WiFi</string>
|
||||
<string name="settings_sync_wifi_only_on">Synkronisering er begrænset til WiFi-forbindelser</string>
|
||||
<string name="settings_sync_wifi_only_off">Forbindelsestypen har ingen betydning</string>
|
||||
<string name="settings_sync_wifi_only_ssids">WiFi SSID-begrænsning</string>
|
||||
<string name="settings_sync_wifi_only_ssids_on">Synkroniserer kun over %s</string>
|
||||
<string name="settings_sync_wifi_only_ssids_off">Alle WiFi-forbindelser vil blive anvendt</string>
|
||||
<string name="settings_sync_wifi_only_ssids_message">Kommaseparerede navne (SSID\'er) over tilladte WiFi-netværk (efterlad blank for at bruge alle)</string>
|
||||
<string name="settings_carddav">CardDAV</string>
|
||||
<string name="settings_contact_group_method">Gruppering af kontakter</string>
|
||||
<string-array name="settings_contact_group_method_values">
|
||||
@@ -151,6 +171,8 @@
|
||||
<item>Grupper er særskilte VCards</item>
|
||||
<item>Grupper er kategorier pr. kontakt</item>
|
||||
</string-array>
|
||||
<string name="settings_contact_group_method_change">Skift grupperingsmetode</string>
|
||||
<string name="settings_contact_group_method_change_reload_contacts">Dette kræver, at alle kontakter genindlæses. Lokale ændringer, der ikke er gemt, vil blive slettet.</string>
|
||||
<string name="settings_caldav">CalDAV</string>
|
||||
<string name="settings_sync_time_range_past">Tidsafgrænsning for tidligere begivenheder</string>
|
||||
<string name="settings_sync_time_range_past_none">Alle begivenheder vil blive synkroniseret</string>
|
||||
@@ -162,6 +184,10 @@
|
||||
<string name="settings_manage_calendar_colors">Administrer farver for kalender</string>
|
||||
<string name="settings_manage_calendar_colors_on">Kalenderfarver administreres af DAVdroid</string>
|
||||
<string name="settings_manage_calendar_colors_off">Kalenderfarver sættes ikke fra DAVdroid</string>
|
||||
<string name="settings_event_colors">Farver for begivenheder</string>
|
||||
<string name="settings_event_colors_on">Synkroniser farver for begivenheder</string>
|
||||
<string name="settings_event_colors_off">Synkroniser ikke farver for begivenheder</string>
|
||||
<string name="settings_event_colors_off_confirm">Fjernes farver for begivenheder kan det fjerne farver, der allerede er synkroniseret.</string>
|
||||
<!--collection management-->
|
||||
<string name="create_addressbook">Opret adressebog</string>
|
||||
<string name="create_addressbook_display_name_hint">Min adressebog</string>
|
||||
@@ -183,6 +209,7 @@
|
||||
<string name="delete_collection_confirm_title">Er du sikker?</string>
|
||||
<string name="delete_collection_confirm_warning">DAV-sættet (%s) og dets data vil blive fjernet fra serveren.</string>
|
||||
<string name="delete_collection_deleting_collection">Sletter CalDAV-sæt</string>
|
||||
<string name="collection_force_read_only">Sæt skrivebeskyttet</string>
|
||||
<!--ExceptionInfoFragment-->
|
||||
<string name="exception">Der er opstået en fejl.</string>
|
||||
<string name="exception_httpexception">Der er opstået en HTTP-fejl.</string>
|
||||
@@ -190,29 +217,21 @@
|
||||
<string name="exception_show_details">Vis detaljer</string>
|
||||
<!--sync adapters and DebugInfoActivity-->
|
||||
<string name="debug_info_title">Debug-info</string>
|
||||
<string name="sync_contacts_read_only_address_book">Skrivebeskyttet adressebog</string>
|
||||
<plurals name="sync_contacts_local_contact_changes_discarded">
|
||||
<item quantity="one">Lokal ændring slettet</item>
|
||||
<item quantity="other">%d lokale ændringer slettet</item>
|
||||
</plurals>
|
||||
<string name="sync_error_permissions">DAVdroid-rettigheder</string>
|
||||
<string name="sync_error_permissions_text">Yderligere adgang påkrævet</string>
|
||||
<string name="sync_error_calendar">Synkronisering af kalenderen lykkedes ikke (%s)</string>
|
||||
<string name="sync_error_contacts">Synkronisering af adressebogen lykkedes ikke (%s)</string>
|
||||
<string name="sync_error_tasks">Synkronisering af opgaver lykkedes ikke (%s)</string>
|
||||
<string name="sync_error">Fejl under %s</string>
|
||||
<string name="sync_error_http_dav">Serverfejl under %s</string>
|
||||
<string name="sync_error_local_storage">Databasefejl under %s</string>
|
||||
<string-array name="sync_error_phases">
|
||||
<item>forbereder synkronisering</item>
|
||||
<item>checker understøttelse</item>
|
||||
<item>behandler poster, der er slettet lokalt</item>
|
||||
<item>behandler poster, der er blevet oprettet/redigeret</item>
|
||||
<item>uploader poster, der er blevet oprettet/redigeret</item>
|
||||
<item>checker synkroniseringsstatus</item>
|
||||
<item>laver liste af lokale poster</item>
|
||||
<item>laver lister af poster på server</item>
|
||||
<item>sammenligner poster lokalt og på server</item>
|
||||
<item>downloader poster på server</item>
|
||||
<item>efterbehandler</item>
|
||||
<item>gemmer synkroniseringsstatus</item>
|
||||
</string-array>
|
||||
<string name="sync_error_unauthorized">Fejl i brugernavn/adgangskode</string>
|
||||
<string name="sync_error_opentasks_too_old">OpenTasks-version for gammel</string>
|
||||
<string name="sync_error_opentasks_required_version">Påkrævet version: %1$s (aktuelt%2$s)</string>
|
||||
<string name="sync_error_authentication_failed">Login mislykkedes (check loginoplysninger)</string>
|
||||
<string name="sync_error_io">Netværks- eller I/O-fejl - %s</string>
|
||||
<string name="sync_error_http_dav">HTTP-serverfejl - %s</string>
|
||||
<string name="sync_error_local_storage">Lokal lagringsfejl - %s</string>
|
||||
<string name="sync_error_retry">Gentag</string>
|
||||
<string name="sync_error_view_item">Vis element</string>
|
||||
<!--cert4android-->
|
||||
<string name="certificate_notification_connection_security">DAVdroid: Forbindelsessikkerhed</string>
|
||||
<string name="trust_certificate_unknown_certificate_found">DAVdroid er stødt på et ukendt certifikat. Vil du stole på det? </string>
|
||||
|
||||
@@ -2,13 +2,19 @@
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<!--common strings-->
|
||||
<string name="app_name">DAVdroid</string>
|
||||
<string name="account_title_address_book">Agenda DAVdroid</string>
|
||||
<string name="address_books_authority_title">Agendas</string>
|
||||
<string name="help">Ayuda</string>
|
||||
<string name="manage_accounts">Administrar cuentas</string>
|
||||
<string name="please_wait">Por favor, espere...</string>
|
||||
<string name="send">Enviar</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>
|
||||
<string name="notification_channel_sync_errors">Errores de sincronización</string>
|
||||
<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_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>
|
||||
@@ -17,13 +23,12 @@
|
||||
<string name="startup_donate_later">Quizás luego</string>
|
||||
<string name="startup_google_play_accounts_removed">Información de error de DRM de Play Store</string>
|
||||
<string name="startup_google_play_accounts_removed_message">Bajo ciertas condiciones, el DRM de Play Store puede causar que todas las cuentas de DAVdroid se desconfiguren tras un reinicio o una actualización de DAVdroid. Si esto le afecta (y sólo en ese caso), por favor, instale \"DAVdroid JB Workaround\" desde Play Store.</string>
|
||||
<string name="startup_google_play_accounts_removed_more_info">Más información</string>
|
||||
<string name="startup_more_info">Más información</string>
|
||||
<string name="startup_opentasks_not_installed">OpenTasks no está instalado</string>
|
||||
<string name="startup_opentasks_not_installed_message">La aplicación OpenTasks no está disponible. DAVdroid no podrá sincronizar listas de tareas.</string>
|
||||
<string name="startup_opentasks_not_installed_message">Para sincronizar tareas, la aplicación libre OpenTasks es requerida. (No necesaria para contactos/eventos.)</string>
|
||||
<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>
|
||||
@@ -35,13 +40,18 @@
|
||||
<string name="navigation_drawer_close">Cerrar panel de navegación</string>
|
||||
<string name="navigation_drawer_subtitle">Adaptador de sincronización CalDAV/CardDAV</string>
|
||||
<string name="navigation_drawer_about">Acerca de / Licencia</string>
|
||||
<string name="navigation_drawer_beta_feedback">Retroalimentación Beta</string>
|
||||
<string name="navigation_drawer_settings">Ajustes</string>
|
||||
<string name="navigation_drawer_news_updates">Noticias y actualizaciones</string>
|
||||
<string name="navigation_drawer_external_links">Enlaces externos</string>
|
||||
<string name="navigation_drawer_website">Sitio web</string>
|
||||
<string name="navigation_drawer_manual">Manual</string>
|
||||
<string name="navigation_drawer_faq">Preguntas frequentes</string>
|
||||
<string name="navigation_drawer_forums">Ayuda / Foros</string>
|
||||
<string name="navigation_drawer_donate">Donar</string>
|
||||
<string name="account_list_empty">Bienvenido a DAVdroid!\n\nAhora puedes añadir una cuenta CalDAV/CardDAV.</string>
|
||||
<string name="accounts_global_sync_disabled">Sincronización automática del sistema completo está deshabilitada</string>
|
||||
<string name="accounts_global_sync_enable">Activar</string>
|
||||
<!--DavService-->
|
||||
<string name="dav_service_refresh_failed">Falló la detección del servicio</string>
|
||||
<string name="dav_service_refresh_couldnt_refresh">No se pudo refrescar lista de colección</string>
|
||||
@@ -80,21 +90,19 @@
|
||||
<string name="account_delete">Eliminar cuenta</string>
|
||||
<string name="account_delete_confirmation_title">¿Seguro que deseas eliminar la cuenta?</string>
|
||||
<string name="account_delete_confirmation_text">Todas las copias locales de tus contactos, calendarios y tareas serán eliminadas.</string>
|
||||
<string name="account_carddav">CardDAV</string>
|
||||
<string name="account_caldav">CalDAV</string>
|
||||
<string name="account_webcal">Webcal</string>
|
||||
<string name="account_synchronize_this_collection">sincronizar ésta colección</string>
|
||||
<string name="account_read_only">solo lectura</string>
|
||||
<string name="account_calendar">calendario</string>
|
||||
<string name="account_task_list">lista de tareas</string>
|
||||
<string name="account_refresh_address_book_list">Refrescar contactos</string>
|
||||
<string name="account_create_new_address_book">Crear nueva lista de contactos</string>
|
||||
<string name="account_create_new_address_book">Crear nueva agenda</string>
|
||||
<string name="account_refresh_calendar_list">Refrescar calendario</string>
|
||||
<string name="account_create_new_calendar">Crear nuevo calendario</string>
|
||||
<!--PermissionsActivity-->
|
||||
<string name="permissions_title">Permisos de DAVdroid</string>
|
||||
<string name="permissions_calendar">Permisos de calendario</string>
|
||||
<string name="permissions_calendar_details">Para sincronizar eventos CalDAV con tus calendarios locales, DAVdroid necesita acceder a los mismos.</string>
|
||||
<string name="permissions_calendar_request">Solicitar permisos sobre calendario</string>
|
||||
<string name="permissions_contacts">Permisos de contactos</string>
|
||||
<string name="permissions_contacts_details">Para sincronizar libretas de contactos CadDAV con tus contactos locales, DAVdroid necesita acceder a los mismos.</string>
|
||||
<string name="permissions_contacts_request">Solicitar permisos sobre contactos</string>
|
||||
<string name="permissions_opentasks">Permisos de OpenTasks</string>
|
||||
<string name="permissions_opentasks_details">Para sincronizar listas de tareas CalDAV con tus listas de tareas locales, DAVdroid necesita acceder a OpenTasks.</string>
|
||||
<string name="permissions_opentasks_request">Solicitar permisos sobre OpenTasks</string>
|
||||
<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_title">Añadir cuenta</string>
|
||||
<string name="login_type_email">Acceder con cuenta de correo</string>
|
||||
@@ -104,10 +112,13 @@
|
||||
<string name="login_password_required">Contraseña requerida</string>
|
||||
<string name="login_type_url">Acceder con URL y nombre de usuario</string>
|
||||
<string name="login_url_must_be_http_or_https">La URL debe comenzar con http(s)://</string>
|
||||
<string name="login_url_must_be_https">El URL debe comenzar con https://</string>
|
||||
<string name="login_url_host_name_required">Nombre de servidor requerido</string>
|
||||
<string name="login_user_name">Nombre de usuario</string>
|
||||
<string name="login_user_name_required">Nombre de usuario requerido</string>
|
||||
<string name="login_base_url">URL base</string>
|
||||
<string name="login_type_url_certificate">Iniciar sesión con URL y certificado del cliente</string>
|
||||
<string name="login_select_certificate">Seleccionar un certificado</string>
|
||||
<string name="login_login">Registrar</string>
|
||||
<string name="login_back">Volver</string>
|
||||
<string name="login_create_account">Crear cuenta</string>
|
||||
@@ -119,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>
|
||||
@@ -128,6 +138,7 @@
|
||||
<string name="settings_password">Contraseña</string>
|
||||
<string name="settings_password_summary">Actualiza la contraseña de acuerdo a tu servidor.</string>
|
||||
<string name="settings_enter_password">Introduce tu contraseña:</string>
|
||||
<string name="settings_certificate_alias">Alias del certificado cliente</string>
|
||||
<string name="settings_sync">Sincronización</string>
|
||||
<string name="settings_sync_interval_contacts">Intervalo de sincronización de contactos</string>
|
||||
<string name="settings_sync_summary_manually">Solo manualmente</string>
|
||||
@@ -137,6 +148,10 @@
|
||||
<string name="settings_sync_wifi_only">Sincronizar sólo sobre WiFi</string>
|
||||
<string name="settings_sync_wifi_only_on">La sincronización está restringida a conexiones WiFi</string>
|
||||
<string name="settings_sync_wifi_only_off">Tipo de conexión no tenido en cuenta</string>
|
||||
<string name="settings_sync_wifi_only_ssids">Restricción WiFi SSID</string>
|
||||
<string name="settings_sync_wifi_only_ssids_on">Solo se sincronizará a través de %s</string>
|
||||
<string name="settings_sync_wifi_only_ssids_off">Todas las conexiones WiFi serán usadas</string>
|
||||
<string name="settings_sync_wifi_only_ssids_message">Nombres separados por comas (SSIDs) de redes WiFi permitidas (deje vacío para todas)</string>
|
||||
<string name="settings_carddav">CardDAV</string>
|
||||
<string name="settings_contact_group_method">Método de contacto de grupo</string>
|
||||
<string-array name="settings_contact_group_method_values">
|
||||
@@ -147,6 +162,8 @@
|
||||
<item>Los groups tienen VCards separadas</item>
|
||||
<item>Los groups tienen una categoría por contacto</item>
|
||||
</string-array>
|
||||
<string name="settings_contact_group_method_change">Cambie método de grupo</string>
|
||||
<string name="settings_contact_group_method_change_reload_contacts">Esto requiere recargar todos los contactos. Cambios locales sin guardar serán descartados.</string>
|
||||
<string name="settings_caldav">CalDAV</string>
|
||||
<string name="settings_sync_time_range_past">Límite de tiempo de eventos pasados</string>
|
||||
<string name="settings_sync_time_range_past_none">Todos los eventos serán sincronizados</string>
|
||||
@@ -158,8 +175,12 @@
|
||||
<string name="settings_manage_calendar_colors">Colores de calendario</string>
|
||||
<string name="settings_manage_calendar_colors_on">Los colores de los calendarios son administrados por DAVdroid</string>
|
||||
<string name="settings_manage_calendar_colors_off">Los colores de los calendarios no son establecidos por DAVdroid</string>
|
||||
<string name="settings_event_colors">Soporte de colores en eventos</string>
|
||||
<string name="settings_event_colors_on">Sincronizar colores de eventos</string>
|
||||
<string name="settings_event_colors_off">No sincronizar colores de eventos</string>
|
||||
<string name="settings_event_colors_off_confirm">Desactivar colores de eventos podría remover colores de eventos ya sincronizados.</string>
|
||||
<!--collection management-->
|
||||
<string name="create_addressbook">Crear nueva lista de contactos</string>
|
||||
<string name="create_addressbook">Crear nueva agenda</string>
|
||||
<string name="create_addressbook_display_name_hint">Agendas</string>
|
||||
<string name="create_calendar">Crear colección CalDAV</string>
|
||||
<string name="create_calendar_display_name_hint">Mi calendario</string>
|
||||
@@ -179,6 +200,7 @@
|
||||
<string name="delete_collection_confirm_title">¿Estás seguro/a?</string>
|
||||
<string name="delete_collection_confirm_warning">Esta colección (%s) y toda su información será eliminada del servidor.</string>
|
||||
<string name="delete_collection_deleting_collection">Eliminando colección</string>
|
||||
<string name="collection_force_read_only">Forzar solo-lectura</string>
|
||||
<!--ExceptionInfoFragment-->
|
||||
<string name="exception">Ocurrió un error.</string>
|
||||
<string name="exception_httpexception">Ha ocurrido un error HTTP.</string>
|
||||
@@ -186,29 +208,21 @@
|
||||
<string name="exception_show_details">Mostrar detalles</string>
|
||||
<!--sync adapters and DebugInfoActivity-->
|
||||
<string name="debug_info_title">Información de depuración</string>
|
||||
<string name="sync_contacts_read_only_address_book">Libro de direcciones solo-lectura</string>
|
||||
<plurals name="sync_contacts_local_contact_changes_discarded">
|
||||
<item quantity="one">Cambio de contacto local descartado</item>
|
||||
<item quantity="other">%d cambios de contactos locales descartados</item>
|
||||
</plurals>
|
||||
<string name="sync_error_permissions">Permisos de DAVdroid</string>
|
||||
<string name="sync_error_permissions_text">Permisos adicionales requeridos</string>
|
||||
<string name="sync_error_calendar">La sincronización de calendario falló (%s)</string>
|
||||
<string name="sync_error_contacts">La sincronización de agenda falló (%s)</string>
|
||||
<string name="sync_error_tasks">La sincronización de tareas falló (%s)</string>
|
||||
<string name="sync_error">Error al %s</string>
|
||||
<string name="sync_error_http_dav">Error de servidor al %s</string>
|
||||
<string name="sync_error_local_storage">Error de base de datos al %s</string>
|
||||
<string-array name="sync_error_phases">
|
||||
<item>preparando sincronización</item>
|
||||
<item>buscando capacidades</item>
|
||||
<item>procesando entradas borradas localmente</item>
|
||||
<item>preparando entradas creadas/modificadas</item>
|
||||
<item>cargando entradas creadas/modificadas</item>
|
||||
<item>comprobando estado de sincronización</item>
|
||||
<item>enumerando entradas locales</item>
|
||||
<item>enumerando entradas remotas</item>
|
||||
<item>comparando entradas locales/remotas</item>
|
||||
<item>descargando entradas remotas</item>
|
||||
<item>post-procesando</item>
|
||||
<item>guardando estado de sincronización</item>
|
||||
</string-array>
|
||||
<string name="sync_error_unauthorized">Nombre de usuario/contraseña erróneo</string>
|
||||
<string name="sync_error_opentasks_too_old">OpenTasks muy viejo</string>
|
||||
<string name="sync_error_opentasks_required_version">Versión requerida: %1$s (actual %2$s)</string>
|
||||
<string name="sync_error_authentication_failed">Falló la autenticación (revise credenciales de inicio de sesión)</string>
|
||||
<string name="sync_error_io">Error de red o E/S – %s</string>
|
||||
<string name="sync_error_http_dav">Error de servidor – %s</string>
|
||||
<string name="sync_error_local_storage">Error de almacenamiento local – %s</string>
|
||||
<string name="sync_error_retry">Reintentar</string>
|
||||
<string name="sync_error_view_item">Ver item</string>
|
||||
<!--cert4android-->
|
||||
<string name="certificate_notification_connection_security">DAVdroid: Seguridad de conexión</string>
|
||||
<string name="trust_certificate_unknown_certificate_found">DAVdroid ha encontrado un certificado desconocido. ¿Quieres que sea válido?</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>
|
||||
@@ -19,13 +17,10 @@
|
||||
<string name="startup_donate_later">Plus tard</string>
|
||||
<string name="startup_google_play_accounts_removed">Erreur information Play Store DRM</string>
|
||||
<string name="startup_google_play_accounts_removed_message">Dans certaines conditions, Play Store DRM peut provoquer la disparition de tous les comptes DAVdroid après un redémarrage ou après la mise à niveau de DAVdroid. Si vous êtes concerné par ce problème (et seulement alors), s\'il vous plaît installer \"DAVdroid JB Solution\" du Play Store.</string>
|
||||
<string name="startup_google_play_accounts_removed_more_info">Plus d\'informations</string>
|
||||
<string name="startup_opentasks_not_installed">L\'application OpenTasks n\'est pas installée</string>
|
||||
<string name="startup_opentasks_not_installed_message">L\'application OpenTasks n\'est pas disponible, donc DAVdroid ne pourra pas synchroniser des listes de tâches.</string>
|
||||
<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>
|
||||
@@ -88,25 +83,15 @@
|
||||
<string name="account_carddav">CardDAV (les carnets d\'adresse) </string>
|
||||
<string name="account_caldav">CalDAV (les agendas) </string>
|
||||
<string name="account_webcal">WebCal (les anciens agenda)</string>
|
||||
<string name="account_calendar">calendrier</string>
|
||||
<string name="account_task_list">liste de tâche</string>
|
||||
<string name="account_refresh_address_book_list">Actualiser le carnet d\'adresses</string>
|
||||
<string name="account_create_new_address_book">Créer un nouveau carnet d\'adresses</string>
|
||||
<string name="account_refresh_calendar_list">Actualiser le calendrier</string>
|
||||
<string name="account_create_new_calendar">Créer un nouveau calendrier</string>
|
||||
<string name="account_no_webcal_handler_found">Aucune application compatible WebCal</string>
|
||||
<string name="account_install_icsdroid">Installer ICSdroid</string>
|
||||
<!--PermissionsActivity-->
|
||||
<string name="permissions_title">Autorisations DAVdroid</string>
|
||||
<string name="permissions_calendar">Autorisations calendrier</string>
|
||||
<string name="permissions_calendar_details">Pour synchroniser les événements CalDAV avec vos calendriers locaux, DAVdroid a besoin d\'accéder à vos calendriers.</string>
|
||||
<string name="permissions_calendar_request">Demande d\'autorisations d\'accéder au calendrier</string>
|
||||
<string name="permissions_contacts">Autorisations d\'accès aux contacts</string>
|
||||
<string name="permissions_contacts_details">Pour synchroniser les carnets d\'adresses de CardDAV avec votre carnet d\'adresses local, DAVdroid a besoin d\'accéder à vos contacts.</string>
|
||||
<string name="permissions_contacts_request">Demande d\'autorisations d\'accéder aux contacts</string>
|
||||
<string name="permissions_opentasks">Autorisations OpenTasks</string>
|
||||
<string name="permissions_opentasks_details">Pour synchroniser les tâches de CalDAV avec vos listes de tâches locales, DAVdroid a besoin d\'accéder à OpenTasks.</string>
|
||||
<string name="permissions_opentasks_request">Demande d\'autorisations d\'accéder à OpenTasks</string>
|
||||
<!--AddAccountActivity-->
|
||||
<string name="login_help_url">https://davdroid.bitfire.at/configuration/?pk_campaign=davdroid-app</string>
|
||||
<string name="login_title">Ajouter un compte</string>
|
||||
<string name="login_type_email">Connexion avec une adresse email</string>
|
||||
<string name="login_email_address">Adresse mail</string>
|
||||
@@ -130,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>
|
||||
@@ -157,7 +141,7 @@
|
||||
<string name="settings_sync_wifi_only">Synchronisation en Wifi seulement</string>
|
||||
<string name="settings_sync_wifi_only_on">La synchronisation est limitée aux connexions WiFi</string>
|
||||
<string name="settings_sync_wifi_only_off">Le type de connexion n\'est pas pris en charge</string>
|
||||
<string name="settings_sync_wifi_only_ssids">Restrictions concernant le nom de réseau WiFi (SSID)</string>
|
||||
<string name="settings_sync_wifi_only_ssids">Restriction WiFi SSID</string>
|
||||
<string name="settings_sync_wifi_only_ssids_on">Synchronisation possible seulement en %s</string>
|
||||
<string name="settings_sync_wifi_only_ssids_off">Toutes les connexions WiFi seront utilisées</string>
|
||||
<string name="settings_sync_wifi_only_ssids_message">Liste des points d\'accès WiFi (SSID) autorisés, séparés par des virgules. (Laissez vide pour tous)</string>
|
||||
@@ -216,27 +200,6 @@
|
||||
<string name="debug_info_title">Infos de débogage</string>
|
||||
<string name="sync_error_permissions">Autorisations DAVdroid</string>
|
||||
<string name="sync_error_permissions_text">Autorisations supplémentaires demandées</string>
|
||||
<string name="sync_error_calendar">Échec de la synchronisation du calendrier (%s)</string>
|
||||
<string name="sync_error_contacts">Échec de la synchronisation du carnet d\'adresse (%s)</string>
|
||||
<string name="sync_error_tasks">Échec de la synchronisation (%s)</string>
|
||||
<string name="sync_error">Erreur durant %s</string>
|
||||
<string name="sync_error_http_dav">Erreur de serveur durant %s</string>
|
||||
<string name="sync_error_local_storage">Erreur de base de donnée durant %s</string>
|
||||
<string-array name="sync_error_phases">
|
||||
<item>prépare la synchronisation</item>
|
||||
<item>demande les autorisations</item>
|
||||
<item>procède à la suppression des entrées locales</item>
|
||||
<item>prépare les entrées créées/modifiées</item>
|
||||
<item>envoi les entrées créées/modifiées</item>
|
||||
<item>vérifie l\'état de la synchronisation</item>
|
||||
<item>liste les entrées locales</item>
|
||||
<item>liste les entrées distantes</item>
|
||||
<item>compare les entrées locales/distantes</item>
|
||||
<item>télécharge les entrées distantes</item>
|
||||
<item>post-traitement</item>
|
||||
<item>enregistre l\'état de la synchronisation</item>
|
||||
</string-array>
|
||||
<string name="sync_error_unauthorized">Nom d\'utilisateur ou mot de passe erroné</string>
|
||||
<!--cert4android-->
|
||||
<string name="certificate_notification_connection_security">DAVdroid : Sécurité de la connexion</string>
|
||||
<string name="trust_certificate_unknown_certificate_found">DAVdroid a rencontré un certificat inconnu. Voulez-vous lui faire confiance?</string>
|
||||
|
||||
@@ -8,10 +8,13 @@
|
||||
<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://davdroid.bitfire.at/?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>
|
||||
<string name="notification_channel_sync_errors">Szinkronizációs hibák</string>
|
||||
<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_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>
|
||||
@@ -20,13 +23,12 @@
|
||||
<string name="startup_donate_later">Talán később</string>
|
||||
<string name="startup_google_play_accounts_removed">Play Áruház DRM hibával kapcsolatos információ</string>
|
||||
<string name="startup_google_play_accounts_removed_message">Bizonyos körülmények között a Play Áruház DRM okozhatja azt, hogy az eszköz újraindítását vagy a DAVdroid frissítését követően a DAVdroid fiókok eltűnnek. Amennyiben (és csak amennyiben) érinti Önt ez a probléma, telepítse a \"DAVdroid JB Workaround\" alkalmazást Play Áruházból.</string>
|
||||
<string name="startup_google_play_accounts_removed_more_info">További információk</string>
|
||||
<string name="startup_more_info">További információk</string>
|
||||
<string name="startup_opentasks_not_installed">Az OpenTasks nincs telepítve</string>
|
||||
<string name="startup_opentasks_not_installed_message">Az OpenTasks alkalmazás nincs telepítve, így a DAVdroid nem lesz képes szinkronizálni feladatlistákat.</string>
|
||||
<string name="startup_opentasks_not_installed_message">A feladatok szinkronizálásához az ingyenes OpenTasks alkalmazásra van szükség. (A névjegyek és események szinkronizálásához erre nincs szükség.)</string>
|
||||
<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>
|
||||
@@ -38,12 +40,13 @@
|
||||
<string name="navigation_drawer_close">Navigációs fiók lezárása</string>
|
||||
<string name="navigation_drawer_subtitle">CalDAV/CardDAV szinkronizációs adapter</string>
|
||||
<string name="navigation_drawer_about">Névjegy / Licenc</string>
|
||||
<string name="navigation_drawer_beta_feedback">Tesztelői visszajelzés</string>
|
||||
<string name="navigation_drawer_settings">Beállítások</string>
|
||||
<string name="navigation_drawer_news_updates">Hírek és frissítések</string>
|
||||
<string name="navigation_drawer_external_links">Weblapok</string>
|
||||
<string name="navigation_drawer_website">Honlap</string>
|
||||
<string name="navigation_drawer_manual">Kézikönyv</string>
|
||||
<string name="navigation_drawer_faq">GYIK</string>
|
||||
<string name="navigation_drawer_faq_url">https://davdroid.bitfire.at/faq/?pk_campaign=davdroid-app</string>
|
||||
<string name="navigation_drawer_forums">Segítség / Fórumok</string>
|
||||
<string name="navigation_drawer_donate">Támogatás</string>
|
||||
<string name="account_list_empty">Üdvözöljük a DAVdroid felhasználók között!\n\nMost már felvehet CalDAV/CardDav fiókokat.</string>
|
||||
@@ -90,25 +93,17 @@
|
||||
<string name="account_carddav">CardDAV</string>
|
||||
<string name="account_caldav">CalDAV</string>
|
||||
<string name="account_webcal">Webcal</string>
|
||||
<string name="account_synchronize_this_collection">a gyűjtemény szinkronizálása</string>
|
||||
<string name="account_read_only">csak olvasható</string>
|
||||
<string name="account_calendar">naptár</string>
|
||||
<string name="account_task_list">feladatlista</string>
|
||||
<string name="account_refresh_address_book_list">Címjegyzék-lista frissítése</string>
|
||||
<string name="account_create_new_address_book">Új címjegyzék létrehozása</string>
|
||||
<string name="account_refresh_calendar_list">Naptárlista frissítése</string>
|
||||
<string name="account_create_new_calendar">Új naptár létrehozása</string>
|
||||
<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>
|
||||
<!--PermissionsActivity-->
|
||||
<string name="permissions_title">DAVdroid engedélyek </string>
|
||||
<string name="permissions_calendar">Naptárengedély</string>
|
||||
<string name="permissions_calendar_details">A CalDAV naptárak és a helyi naptárak szinkronizálásához a DAVdroid naptárhozzáférést igényel.</string>
|
||||
<string name="permissions_calendar_request">Naptárhozzáférés igénylése</string>
|
||||
<string name="permissions_contacts">Névjegyengedélyek</string>
|
||||
<string name="permissions_contacts_details">A CardDAV címlisták és a helyi címlisták szinkronizálásához a névjegyhozzáférést igényel.</string>
|
||||
<string name="permissions_contacts_request">Névjegyengedélyek igénylése</string>
|
||||
<string name="permissions_opentasks">OpenTasks engedélyek</string>
|
||||
<string name="permissions_opentasks_details">A CalDAV feladatlisták és a helyi feladatlisták szinkronizálásához a DAVdroid OpenTasks hozzáférést igényel.</string>
|
||||
<string name="permissions_opentasks_request">OpenTasks engedélyek igénylése</string>
|
||||
<!--AddAccountActivity-->
|
||||
<string name="login_help_url">https://davdroid.bitfire.at/configuration/?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>
|
||||
@@ -117,10 +112,13 @@
|
||||
<string name="login_password_required">A jelszó megadása szükséges</string>
|
||||
<string name="login_type_url">Bejelentkezés URL és felhasználónév segítségével</string>
|
||||
<string name="login_url_must_be_http_or_https">Az URL elején szerepeljen http(s)://</string>
|
||||
<string name="login_url_must_be_https">Az URL elején szerepeljen https://</string>
|
||||
<string name="login_url_host_name_required">A szervernév megadása feltétlenül szükséges</string>
|
||||
<string name="login_user_name">Felhasználónév</string>
|
||||
<string name="login_user_name_required">A felhasználónév megadása feltétlenül szükséges</string>
|
||||
<string name="login_base_url">URL-törzs</string>
|
||||
<string name="login_type_url_certificate">Bejelentkezés URL és tanúsítvány segítségével</string>
|
||||
<string name="login_select_certificate">Tanúsítvány kiválasztása</string>
|
||||
<string name="login_login">Bejelentkezés</string>
|
||||
<string name="login_back">Vissza</string>
|
||||
<string name="login_create_account">Fiók létrehozása</string>
|
||||
@@ -132,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>
|
||||
@@ -141,6 +138,7 @@
|
||||
<string name="settings_password">Jelszó</string>
|
||||
<string name="settings_password_summary">Adja meg a szerveren érvényes új jelszót.</string>
|
||||
<string name="settings_enter_password">Adja meg a jelszót:</string>
|
||||
<string name="settings_certificate_alias">Tanúsítvány alternatív megnevezése</string>
|
||||
<string name="settings_sync">Szinkronizálás</string>
|
||||
<string name="settings_sync_interval_contacts">Névjegyszinkronizálás sűrűsége</string>
|
||||
<string name="settings_sync_summary_manually">Manuális</string>
|
||||
@@ -173,6 +171,8 @@
|
||||
<item>Minden csoport egy különálló VCard</item>
|
||||
<item>A csoportok a kapcsolatonkéni kategóriák</item>
|
||||
</string-array>
|
||||
<string name="settings_contact_group_method_change">A csoportok kezelésének megváltoztatása</string>
|
||||
<string name="settings_contact_group_method_change_reload_contacts">Az összes névjegyet újra be kell olvasni. Az el nem mentett helyi változások törlődni fognak.</string>
|
||||
<string name="settings_caldav">CalDAV</string>
|
||||
<string name="settings_sync_time_range_past">Múltbéli események időkorlátja</string>
|
||||
<string name="settings_sync_time_range_past_none">Minden esemény szinkronizálása</string>
|
||||
@@ -209,6 +209,7 @@
|
||||
<string name="delete_collection_confirm_title">Biztos?</string>
|
||||
<string name="delete_collection_confirm_warning">A gyűjtemény (%s) és a hozzá tartozó adatok törölve lesznek a szerverről.</string>
|
||||
<string name="delete_collection_deleting_collection">Gyűjtemény törlése</string>
|
||||
<string name="collection_force_read_only">Csak olvashatóvá tétel</string>
|
||||
<!--ExceptionInfoFragment-->
|
||||
<string name="exception">Hiba történt.</string>
|
||||
<string name="exception_httpexception">HTTP hiba történt.</string>
|
||||
@@ -216,29 +217,21 @@
|
||||
<string name="exception_show_details">Részletek megjelenítése</string>
|
||||
<!--sync adapters and DebugInfoActivity-->
|
||||
<string name="debug_info_title">Hibakeresési információ</string>
|
||||
<string name="sync_contacts_read_only_address_book">Csak olvasható címjegyzék</string>
|
||||
<plurals name="sync_contacts_local_contact_changes_discarded">
|
||||
<item quantity="one">Helyi névjegyváltozás elvetve</item>
|
||||
<item quantity="other">%d helyi névjegyváltozás elvetve</item>
|
||||
</plurals>
|
||||
<string name="sync_error_permissions">DAVdroid engedélyek </string>
|
||||
<string name="sync_error_permissions_text">További engedélyek szükségesek</string>
|
||||
<string name="sync_error_calendar">A naptár szinkronizálása nem sikerült (%s)</string>
|
||||
<string name="sync_error_contacts">A címjegyzék szinkronizálása nem sikerült (%s)</string>
|
||||
<string name="sync_error_tasks">A feladatok szinkronizálása nem sikerült (%s)</string>
|
||||
<string name="sync_error">Hiba az alábbi művelet közben: %s</string>
|
||||
<string name="sync_error_http_dav">Szerver oldali hiba az alábbi művelet közben: %s</string>
|
||||
<string name="sync_error_local_storage">Adatbázishiba az alábbi művelet közben: %s</string>
|
||||
<string-array name="sync_error_phases">
|
||||
<item>szinkronizáció előkészítése </item>
|
||||
<item>szerver képességeinek lekérdezése</item>
|
||||
<item>a helyben törölt bejegyzések feldolgozása</item>
|
||||
<item>az új vagy módosított bejegyzések gyűjtése </item>
|
||||
<item>az új vagy módosított bejegyzések feltöltése</item>
|
||||
<item>szinkronizációs állapot ellenőrzése</item>
|
||||
<item>helyi bejegyzések listázása</item>
|
||||
<item>távoli bejegyzések listázása</item>
|
||||
<item>helyi és távoli bejegyzések összehasonlítása</item>
|
||||
<item>távoli bejegyzések letöltése</item>
|
||||
<item>utófeldolgozás</item>
|
||||
<item>szinkronizációs állapot mentése</item>
|
||||
</string-array>
|
||||
<string name="sync_error_unauthorized">A felhasználónév vagy jelszó hibás</string>
|
||||
<string name="sync_error_opentasks_too_old">Az OpenTask verzió nem megfelelő</string>
|
||||
<string name="sync_error_opentasks_required_version">Szükséges verzió: %1$s (jelenlegi verzió: %2$s)</string>
|
||||
<string name="sync_error_authentication_failed">Authentikáció nem sikerült (ellenőrizze a hitelesítéshez megadott adatokat)</string>
|
||||
<string name="sync_error_io">Hálózati vagy I/O hiba – %s</string>
|
||||
<string name="sync_error_http_dav">HTTP szerver oldali hiba - %s</string>
|
||||
<string name="sync_error_local_storage">Tárhelyhiba - %s</string>
|
||||
<string name="sync_error_retry">Újbóli próbálkozás</string>
|
||||
<string name="sync_error_view_item">Elem megtekintése</string>
|
||||
<!--cert4android-->
|
||||
<string name="certificate_notification_connection_security">DAVdroid: kapcsolatbiztonság</string>
|
||||
<string name="trust_certificate_unknown_certificate_found">Egy eddig ismeretlen tanúsítvány érkezett. Megbízhatónak kívánja elfogadni?</string>
|
||||
|
||||
@@ -2,15 +2,18 @@
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<!--common strings-->
|
||||
<string name="app_name">DAVdroid</string>
|
||||
<string name="account_title_address_book">Rubrica degli indirizzi DAVdroid</string>
|
||||
<string name="address_books_authority_title">Rubriche degli indirizzi</string>
|
||||
<string name="account_title_address_book">Rubrica DAVdroid</string>
|
||||
<string name="address_books_authority_title">Rubriche</string>
|
||||
<string name="help">Aiuto</string>
|
||||
<string name="manage_accounts">Gestione account</string>
|
||||
<string name="please_wait">Attendere prego …</string>
|
||||
<string name="send">Invia</string>
|
||||
<string name="notification_channel_general">Altri messaggi importanti</string>
|
||||
<string name="notification_channel_sync">Sincronizzazione</string>
|
||||
<string name="notification_channel_sync_errors">Errori di sincronizzazione</string>
|
||||
<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">Ottimizazione 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>
|
||||
@@ -19,13 +22,12 @@
|
||||
<string name="startup_donate_later">Più tardi</string>
|
||||
<string name="startup_google_play_accounts_removed">Informazioni sul bug del DRM di Play Store</string>
|
||||
<string name="startup_google_play_accounts_removed_message">In alcune condizioni il DRM di Play Store può causare la perdita di tutti gli account DAVdroid dopo un riavvio o dopo un aggiornamento di DAVdroid. Se verificate questo problema installate successivamente \"DAVdroid JB Workaround\" da Play Store.</string>
|
||||
<string name="startup_google_play_accounts_removed_more_info">Maggiori informazioni</string>
|
||||
<string name="startup_more_info">Più informazioni</string>
|
||||
<string name="startup_opentasks_not_installed">OpenTasks non installata</string>
|
||||
<string name="startup_opentasks_not_installed_message">L\'applicazione OpenTasks non è installata: di conseguenza DAVdroid non potrà sincronizzare l\'elenco delle attività.</string>
|
||||
<string name="startup_opentasks_not_installed_message">Per sincronizzare le attività è richiesta l\'app gratuita OpenTasks. (Non richiesta per contatti/eventi.)</string>
|
||||
<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>
|
||||
@@ -41,7 +43,9 @@
|
||||
<string name="navigation_drawer_news_updates">Notizie & aggiornamenti</string>
|
||||
<string name="navigation_drawer_external_links">Link esterni</string>
|
||||
<string name="navigation_drawer_website">Sito web</string>
|
||||
<string name="navigation_drawer_manual">Manuale</string>
|
||||
<string name="navigation_drawer_faq">Domande Frequenti</string>
|
||||
<string name="navigation_drawer_forums">Aiuto / Forum</string>
|
||||
<string name="navigation_drawer_donate">Donazione</string>
|
||||
<string name="account_list_empty">Benvenuto a DAVdroid!\n\nÈ ora possibile aggiungere account CalDAV/CardDAV.</string>
|
||||
<string name="accounts_global_sync_disabled">La sincronizzazione automatica dell\'intero sistema è disabilitata</string>
|
||||
@@ -87,24 +91,15 @@
|
||||
<string name="account_carddav">CardDAV</string>
|
||||
<string name="account_caldav">CalDAV</string>
|
||||
<string name="account_webcal">Webcal</string>
|
||||
<string name="account_read_only">sola lettura</string>
|
||||
<string name="account_calendar">calendario</string>
|
||||
<string name="account_task_list">elenco attività</string>
|
||||
<string name="account_refresh_address_book_list">Aggiorna elenco degli indirizzari</string>
|
||||
<string name="account_create_new_address_book">Crea un nuovo indirizzario</string>
|
||||
<string name="account_refresh_calendar_list">Aggiorna lista calendari</string>
|
||||
<string name="account_create_new_calendar">Crea nuovo calendario</string>
|
||||
<string name="account_install_icsdroid">Installa ICSdroid</string>
|
||||
<!--PermissionsActivity-->
|
||||
<string name="permissions_title">Permessi DAVdroid</string>
|
||||
<string name="permissions_calendar">Permessi calendario</string>
|
||||
<string name="permissions_calendar_details">Per sincronizzare gli eventi CalDAV con i calendari locali DAVdroid deve avere l\'accesso ai tuoi calendari.</string>
|
||||
<string name="permissions_calendar_request">Richiesta autorizzazione al calendario</string>
|
||||
<string name="permissions_contacts">Permessi Contatti</string>
|
||||
<string name="permissions_contacts_details">Per sincronizzare l\'indirizzario CardDAV con i contatti locali DAVdroid deve avere l\'accesso ai tuoi contatti.</string>
|
||||
<string name="permissions_contacts_request">Richiesta autorizzazione ai contatti</string>
|
||||
<string name="permissions_opentasks">Permessi OpenTasks</string>
|
||||
<string name="permissions_opentasks_details">Per sincronizzazione l\'elenco attività di CalDAV con l\'elenco locale DAVdroid deve avere l\'accesso ad OpenTasks.</string>
|
||||
<string name="permissions_opentasks_request">Richiesta autorizzazione ad OpenTasks</string>
|
||||
<!--AddAccountActivity-->
|
||||
<string name="login_help_url">https://davdroid.bitfire.at/configuration/?pk_campaign=davdroid-app</string>
|
||||
<string name="login_title">Aggiungi account</string>
|
||||
<string name="login_type_email">Accedi con indirizzo email</string>
|
||||
<string name="login_email_address">Indirizzo email</string>
|
||||
@@ -113,10 +108,13 @@
|
||||
<string name="login_password_required">Password richiesta</string>
|
||||
<string name="login_type_url">Accedi con URL e nome utente</string>
|
||||
<string name="login_url_must_be_http_or_https">L\'URL deve iniziare con http(s)://</string>
|
||||
<string name="login_url_must_be_https">L\'URL deve iniziare con https://</string>
|
||||
<string name="login_url_host_name_required">Nome host richiesto</string>
|
||||
<string name="login_user_name">Nome utente</string>
|
||||
<string name="login_user_name_required">Nome utente richiesto</string>
|
||||
<string name="login_base_url">Base URL</string>
|
||||
<string name="login_type_url_certificate">Accedi con URL e certificato client</string>
|
||||
<string name="login_select_certificate">Seleziona certificato</string>
|
||||
<string name="login_login">Login</string>
|
||||
<string name="login_back">Indietro</string>
|
||||
<string name="login_create_account">Crea account</string>
|
||||
@@ -128,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>
|
||||
@@ -155,6 +152,7 @@
|
||||
<string name="settings_sync_wifi_only">Sincr. solo tramite WiFi</string>
|
||||
<string name="settings_sync_wifi_only_on">La sincronizzazione è limitata alle connessioni WiFi</string>
|
||||
<string name="settings_sync_wifi_only_off">Il tipo di connessione non è preso in considerazione</string>
|
||||
<string name="settings_sync_wifi_only_ssids">Restrizione SSID WiFi</string>
|
||||
<string name="settings_sync_wifi_only_ssids_message">Nomi (SSID) delle reti WiFi autorizzate separati da virgola (lascia vuoto per autorizzarle tutte)</string>
|
||||
<string name="settings_carddav">CardDAV</string>
|
||||
<string name="settings_contact_group_method">Organizzazione dei gruppi di contatto</string>
|
||||
@@ -177,9 +175,12 @@
|
||||
<string name="settings_manage_calendar_colors">Cambia il colore del calendario</string>
|
||||
<string name="settings_manage_calendar_colors_on">I colori dei calendari sono gestiti da DAVdroid</string>
|
||||
<string name="settings_manage_calendar_colors_off">I colori dei calendari non sono gestiti da DAVdroid</string>
|
||||
<string name="settings_event_colors">Supporto colore dell\'evento</string>
|
||||
<string name="settings_event_colors_on">Sincronizza colori eventi</string>
|
||||
<string name="settings_event_colors_off">Non sincronizza colori eventi</string>
|
||||
<!--collection management-->
|
||||
<string name="create_addressbook">Crea indirizzario</string>
|
||||
<string name="create_addressbook_display_name_hint">Il mio indirizzario</string>
|
||||
<string name="create_addressbook">Crea rubrica</string>
|
||||
<string name="create_addressbook_display_name_hint">La mia rubrica</string>
|
||||
<string name="create_calendar">Crea raccolta CalDAV</string>
|
||||
<string name="create_calendar_display_name_hint">Mio calendario</string>
|
||||
<string name="create_calendar_time_zone">Fuso orario:</string>
|
||||
@@ -198,6 +199,7 @@
|
||||
<string name="delete_collection_confirm_title">Sei sicuro?</string>
|
||||
<string name="delete_collection_confirm_warning">Questa raccolta (%s) e tutti i suoi dati saranno rimossi dal server.</string>
|
||||
<string name="delete_collection_deleting_collection">Cancellazione della raccolta</string>
|
||||
<string name="collection_force_read_only">Forza sola lettura</string>
|
||||
<!--ExceptionInfoFragment-->
|
||||
<string name="exception">Si è verificato un errore.</string>
|
||||
<string name="exception_httpexception">Si è verificato un errore HTTP.</string>
|
||||
@@ -205,29 +207,12 @@
|
||||
<string name="exception_show_details">Mostra dettagli</string>
|
||||
<!--sync adapters and DebugInfoActivity-->
|
||||
<string name="debug_info_title">Informazioni di debug</string>
|
||||
<string name="sync_contacts_read_only_address_book">Rubrica in sola lettura</string>
|
||||
<string name="sync_error_permissions">Autorizzazioni DAVdroid</string>
|
||||
<string name="sync_error_permissions_text">Autorizzazioni addizionali richieste</string>
|
||||
<string name="sync_error_calendar">Sincronizzazione del calendario fallita (%s)</string>
|
||||
<string name="sync_error_contacts">Sincronizzazione dell\'indirizzario fallita (%s)</string>
|
||||
<string name="sync_error_tasks">Sincronizzazione delle attività fallita (%s)</string>
|
||||
<string name="sync_error">Errore nel %s</string>
|
||||
<string name="sync_error_http_dav">Errore del server nel %s</string>
|
||||
<string name="sync_error_local_storage">Errore del database nel %s</string>
|
||||
<string-array name="sync_error_phases">
|
||||
<item>inizio sincronizzazione</item>
|
||||
<item>controllo caratteristiche del server</item>
|
||||
<item>elaborazione voci cancellate in locale</item>
|
||||
<item>elaborazione voci create o modificate</item>
|
||||
<item>invio voci create o modificate</item>
|
||||
<item>controllo stato della sincronizzazione</item>
|
||||
<item>elenco voci locali</item>
|
||||
<item>elenco voci remote</item>
|
||||
<item>confronto voci locali e remote</item>
|
||||
<item>download voci remote</item>
|
||||
<item>post-processing</item>
|
||||
<item>salvataggio stato della sincronizzazione</item>
|
||||
</string-array>
|
||||
<string name="sync_error_unauthorized">Nome utente o password errati</string>
|
||||
<string name="sync_error_opentasks_too_old">OpenTasks troppo vecchia</string>
|
||||
<string name="sync_error_opentasks_required_version">Versione richiesta: %1$s (attualmente %2$s)</string>
|
||||
<string name="sync_error_authentication_failed">Autenticazione fallita (controlla credenziali login)</string>
|
||||
<!--cert4android-->
|
||||
<string name="certificate_notification_connection_security">DAVdroid: sicurezza della connessione</string>
|
||||
<string name="trust_certificate_unknown_certificate_found">DAVdroid ha trovato un certificato sconosciuto. Ritenerlo affidabile?</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://davdroid.bitfire.at/?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_google_play_accounts_removed_more_info">追加情報</string>
|
||||
<string name="startup_more_info">追加情報</string>
|
||||
<string name="startup_opentasks_not_installed">OpenTasks がインストールされていません</string>
|
||||
<string name="startup_opentasks_not_installed_message">OpenTasks アプリが利用できないため、DAVdroid はタスクリストを同期することができません。</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>
|
||||
@@ -38,12 +50,13 @@
|
||||
<string name="navigation_drawer_close">ナビゲーションドロワーを閉じる</string>
|
||||
<string name="navigation_drawer_subtitle">CalDAV/CardDAV 同期アダプター</string>
|
||||
<string name="navigation_drawer_about">アプリについて / ライセンス</string>
|
||||
<string name="navigation_drawer_beta_feedback">ベータフィードバック</string>
|
||||
<string name="navigation_drawer_settings">設定</string>
|
||||
<string name="navigation_drawer_news_updates">ニュース & 更新</string>
|
||||
<string name="navigation_drawer_external_links">外部リンク</string>
|
||||
<string name="navigation_drawer_website">Web サイト</string>
|
||||
<string name="navigation_drawer_manual">手動</string>
|
||||
<string name="navigation_drawer_faq">FAQ</string>
|
||||
<string name="navigation_drawer_faq_url">https://davdroid.bitfire.at/faq/?pk_campaign=davdroid-app</string>
|
||||
<string name="navigation_drawer_forums">ヘルプ / フォーラム</string>
|
||||
<string name="navigation_drawer_donate">寄付</string>
|
||||
<string name="account_list_empty">DAVdroid にようこそ!\n\nCalDAV/CardDAV アカウントを追加できるようになりました。</string>
|
||||
@@ -87,29 +100,21 @@
|
||||
<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>
|
||||
<string name="account_synchronize_this_collection">このコレクションを同期</string>
|
||||
<string name="account_read_only">読み取り専用</string>
|
||||
<string name="account_calendar">カレンダー</string>
|
||||
<string name="account_task_list">タスクリスト</string>
|
||||
<string name="account_refresh_address_book_list">アドレス帳リストを更新</string>
|
||||
<string name="account_create_new_address_book">新しいアドレス帳を作成</string>
|
||||
<string name="account_refresh_calendar_list">カレンダーリストを更新</string>
|
||||
<string name="account_create_new_calendar">新しいカレンダーを作成</string>
|
||||
<string name="account_no_webcal_handler_found">Webcal に対応するアプリが見つかりません</string>
|
||||
<string name="account_install_icsdroid">ICSdroid をインストール</string>
|
||||
<string name="account_read_only_address_book_selected">読み取り専用のアドレス帳 – ローカルの変更は破棄されます!</string>
|
||||
<!--PermissionsActivity-->
|
||||
<string name="permissions_title">DAVdroid アクセス許可</string>
|
||||
<string name="permissions_calendar">カレンダー アクセス許可</string>
|
||||
<string name="permissions_calendar_details">ローカルのカレンダーと CalDAV イベントを同期するため、DAVdroid がカレンダーにアクセスする必要があります。</string>
|
||||
<string name="permissions_calendar_request">カレンダー アクセス許可の要求</string>
|
||||
<string name="permissions_contacts">連絡先アクセス許可</string>
|
||||
<string name="permissions_contacts_details">ローカルの連絡先と CalDAV アドレス帳を同期するため、DAVdroid が連絡先にアクセスする必要があります。</string>
|
||||
<string name="permissions_contacts_request">連絡先アクセス許可の要求</string>
|
||||
<string name="permissions_opentasks">OpenTasks アクセス許可</string>
|
||||
<string name="permissions_opentasks_details">ローカルのタスクリストと CalDAV タスクを同期するため、DAVdroid が OpenTasks にアクセスする必要があります。</string>
|
||||
<string name="permissions_opentasks_request">OpenTasks アクセス許可の要求</string>
|
||||
<!--AddAccountActivity-->
|
||||
<string name="login_help_url">https://davdroid.bitfire.at/configuration/?pk_campaign=davdroid-app</string>
|
||||
<string name="login_title">アカウントを追加</string>
|
||||
<string name="login_type_email">メールアドレスでログイン</string>
|
||||
<string name="login_email_address">メールアドレス</string>
|
||||
@@ -118,10 +123,13 @@
|
||||
<string name="login_password_required">パスワードが必要です</string>
|
||||
<string name="login_type_url">URL とユーザー名でログイン</string>
|
||||
<string name="login_url_must_be_http_or_https">URL は http(s):// で始まる必要があります</string>
|
||||
<string name="login_url_must_be_https">URL は https:// で始まる必要があります</string>
|
||||
<string name="login_url_host_name_required">ホスト名が必要です</string>
|
||||
<string name="login_user_name">ユーザー名</string>
|
||||
<string name="login_user_name_required">ユーザー名が必要です</string>
|
||||
<string name="login_base_url">ベース URL</string>
|
||||
<string name="login_type_url_certificate">URL とクライアント証明書でログイン</string>
|
||||
<string name="login_select_certificate">証明書を選択</string>
|
||||
<string name="login_login">ログイン</string>
|
||||
<string name="login_back">戻る</string>
|
||||
<string name="login_create_account">アカウントを作成</string>
|
||||
@@ -133,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>
|
||||
@@ -142,6 +150,7 @@
|
||||
<string name="settings_password">パスワード</string>
|
||||
<string name="settings_password_summary">ご利用のサーバーに従ってパスワードを更新します。</string>
|
||||
<string name="settings_enter_password">パスワードを入力:</string>
|
||||
<string name="settings_certificate_alias">クライアント証明書の別名</string>
|
||||
<string name="settings_sync">同期</string>
|
||||
<string name="settings_sync_interval_contacts">連絡先同期間隔</string>
|
||||
<string name="settings_sync_summary_manually">手動のみ</string>
|
||||
@@ -174,6 +183,8 @@
|
||||
<item>グループは個別の VCards</item>
|
||||
<item>グループは連絡先カテゴリーごと</item>
|
||||
</string-array>
|
||||
<string name="settings_contact_group_method_change">グループ方法を変更</string>
|
||||
<string name="settings_contact_group_method_change_reload_contacts">これはすべての連絡先を再読み込みする必要があります。 保存していないローカルの変更は破棄されます。</string>
|
||||
<string name="settings_caldav">CalDAV</string>
|
||||
<string name="settings_sync_time_range_past">過去イベントの時間限度</string>
|
||||
<string name="settings_sync_time_range_past_none">すべてのイベントが同期されます</string>
|
||||
@@ -209,6 +220,10 @@
|
||||
<string name="delete_collection_confirm_title">よろしいですか?</string>
|
||||
<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>
|
||||
@@ -222,27 +237,14 @@
|
||||
</plurals>
|
||||
<string name="sync_error_permissions">DAVdroid アクセス許可</string>
|
||||
<string name="sync_error_permissions_text">追加のアクセス許可が必要です</string>
|
||||
<string name="sync_error_calendar">カレンダーの同期に失敗しました (%s)</string>
|
||||
<string name="sync_error_contacts">アドレス帳の同期に失敗しました (%s)</string>
|
||||
<string name="sync_error_tasks">タスクの同期に失敗しました (%s)</string>
|
||||
<string name="sync_error">%s 時にエラー</string>
|
||||
<string name="sync_error_http_dav">%s 時にサーバーエラー</string>
|
||||
<string name="sync_error_local_storage">%s 時にデータベースエラー</string>
|
||||
<string-array name="sync_error_phases">
|
||||
<item>同期の準備中</item>
|
||||
<item>機能の問い合わせ中</item>
|
||||
<item>ローカルで削除されたエントリーの処理中</item>
|
||||
<item>作成済/更新済エントリーの準備中</item>
|
||||
<item>作成済/更新済エントリーのアップロード中</item>
|
||||
<item>同期の状態を確認中</item>
|
||||
<item>ローカルのエントリーをリスト中</item>
|
||||
<item>リモートのエントリーをリスト中</item>
|
||||
<item>ローカル/リモートのエントリーを比較中</item>
|
||||
<item>リモートのエントリーをダウンロード中</item>
|
||||
<item>後処理中</item>
|
||||
<item>同期の状態を保存中</item>
|
||||
</string-array>
|
||||
<string name="sync_error_unauthorized">ユーザー名/パスワードが間違っています</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,10 +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://davdroid.bitfire.at/?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>
|
||||
@@ -20,13 +17,10 @@
|
||||
<string name="startup_donate_later">Kanskje senere</string>
|
||||
<string name="startup_google_play_accounts_removed">DRM-feilinformasjon på Play-butikken</string>
|
||||
<string name="startup_google_play_accounts_removed_message">Under gitte forhold vil DRM fra Play-butikken forårsake at alle DAVdroid-kontoer forsvinner etter omstart eller etter oppgradering av DAVdroid. Hvis du rammes av dette problemet (og bare da), installer \"DAVdroid JB Workaround\" fra Play-butikken.</string>
|
||||
<string name="startup_google_play_accounts_removed_more_info">Mer informasjon</string>
|
||||
<string name="startup_opentasks_not_installed">OpenTasks er ikke installert</string>
|
||||
<string name="startup_opentasks_not_installed_message">OpenTasks-programmet er ikke tilgjengelig, så DAVdroid kan ikke synkronisere gjøremålslister.</string>
|
||||
<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>
|
||||
@@ -44,7 +38,6 @@
|
||||
<string name="navigation_drawer_external_links">Eksterne lenker</string>
|
||||
<string name="navigation_drawer_website">Nettside</string>
|
||||
<string name="navigation_drawer_faq">O-S-S</string>
|
||||
<string name="navigation_drawer_faq_url">https://davdroid.bitfire.at/faq/?pk_campaign=davdroid-app</string>
|
||||
<string name="navigation_drawer_forums">Hjelp / Forum</string>
|
||||
<string name="navigation_drawer_donate">Doner</string>
|
||||
<string name="account_list_empty">Velkommen til DAVdroid.\n\nDu kan legge til en CalDAV/CardDAV-konto nå.</string>
|
||||
@@ -91,26 +84,17 @@
|
||||
<string name="account_carddav">CardDAV</string>
|
||||
<string name="account_caldav">CalDAV</string>
|
||||
<string name="account_webcal">Webcal</string>
|
||||
<string name="account_synchronize_this_collection">synkroniser denne samlingen</string>
|
||||
<string name="account_read_only">kun lesbar</string>
|
||||
<string name="account_calendar">kalender</string>
|
||||
<string name="account_task_list">oppgaveliste</string>
|
||||
<string name="account_refresh_address_book_list">Gjenoppfrisk adressebokliste</string>
|
||||
<string name="account_create_new_address_book">Opprett ny adressebok</string>
|
||||
<string name="account_refresh_calendar_list">Gjenoppfrisk kalenderliste</string>
|
||||
<string name="account_create_new_calendar">Opprett ny kalender</string>
|
||||
<string name="account_no_webcal_handler_found">Fant ingen programmer med støtte for Webcal</string>
|
||||
<string name="account_install_icsdroid">Installer ICSdroid</string>
|
||||
<string name="account_read_only_address_book_selected">Adressebok uten skrivemuligheter - lokale endringer vil bli forkastet!</string>
|
||||
<!--PermissionsActivity-->
|
||||
<string name="permissions_title">DAVdroid-tilgang</string>
|
||||
<string name="permissions_calendar">Kalender-tilgang</string>
|
||||
<string name="permissions_calendar_details">For å synkronisere CalDAV-hendelser med dine lokale kalendere må DAVdroid ha tilgang til kalenderne dine.</string>
|
||||
<string name="permissions_calendar_request">Forespør kalender-tilganger</string>
|
||||
<string name="permissions_contacts">Kontakt-tilgang</string>
|
||||
<string name="permissions_contacts_details">For å synkronisere CardDAV-adressebøker med dine lokale kontakter må DAVdroid ha tilgang til dine kontakter.</string>
|
||||
<string name="permissions_contacts_request">Forespør kontakt-tilgang</string>
|
||||
<string name="permissions_opentasks">OpenTasks-tilganger</string>
|
||||
<string name="permissions_opentasks_details">For å synkronisere CalDAV-gjøremål med din lokale gjøremålslister må DAVdroid ha tilgang til OpenTasks.</string>
|
||||
<string name="permissions_opentasks_request">Forespør OpenTasks-tilganger</string>
|
||||
<!--AddAccountActivity-->
|
||||
<string name="login_help_url">https://davdroid.bitfire.at/configuration/?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>
|
||||
@@ -134,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>
|
||||
@@ -175,6 +158,8 @@
|
||||
<item>Grupper er egne vKort</item>
|
||||
<item>Grupper er kategorier per kontakt</item>
|
||||
</string-array>
|
||||
<string name="settings_contact_group_method_change">Endre gruppemetode</string>
|
||||
<string name="settings_contact_group_method_change_reload_contacts">Dette krever ny innlasting av alle kontakter. Ulagrede lokale endringer vil bli forkastet.</string>
|
||||
<string name="settings_caldav">CalDAV</string>
|
||||
<string name="settings_sync_time_range_past">Tidsgrense for tidligere hendelser</string>
|
||||
<string name="settings_sync_time_range_past_none">Alle gjøremål vil bli synkronisert</string>
|
||||
@@ -211,6 +196,7 @@
|
||||
<string name="delete_collection_confirm_title">Er du sikker?</string>
|
||||
<string name="delete_collection_confirm_warning">Denne samlingen (%s) og all dens data vil bli fjernet fra tjeneren.</string>
|
||||
<string name="delete_collection_deleting_collection">Sletter samling</string>
|
||||
<string name="collection_force_read_only">Tving kun lesbar</string>
|
||||
<!--ExceptionInfoFragment-->
|
||||
<string name="exception">En feil har inntruffet</string>
|
||||
<string name="exception_httpexception">En HTTP-feil har inntruffet.</string>
|
||||
@@ -225,27 +211,6 @@
|
||||
</plurals>
|
||||
<string name="sync_error_permissions">DAVdroid-tilganger</string>
|
||||
<string name="sync_error_permissions_text">Ytterligere tilganger kreves</string>
|
||||
<string name="sync_error_calendar">Kalendersynkronisering feilet (%s)</string>
|
||||
<string name="sync_error_contacts">Adresseboksynkronisering mislyktes (%s)</string>
|
||||
<string name="sync_error_tasks">Gjøremålssynkronisering mislyktes (%s)</string>
|
||||
<string name="sync_error">Feil under %s</string>
|
||||
<string name="sync_error_http_dav">Tjener feil under %s</string>
|
||||
<string name="sync_error_local_storage">Databasefeil under %s</string>
|
||||
<string-array name="sync_error_phases">
|
||||
<item>forbereder synkronisering</item>
|
||||
<item>spør om muligheter</item>
|
||||
<item>behandler lokalt slettede oppføringer</item>
|
||||
<item>forbereder opprettede/endrede oppføringer</item>
|
||||
<item>laster opp opprettede/modifiserte oppføringer</item>
|
||||
<item>sjekker synkroniseringstilstand</item>
|
||||
<item>listefører lokale oppføringer</item>
|
||||
<item>listefører oppføringer annensteds hen</item>
|
||||
<item>sammenlign lokale/oppføringer annensteds hen</item>
|
||||
<item>laster ned oppføringer annensteds fra</item>
|
||||
<item>etterbehandling</item>
|
||||
<item>lagrer synkroniseringstilstand</item>
|
||||
</string-array>
|
||||
<string name="sync_error_unauthorized">Brukernavn-/passord feil</string>
|
||||
<!--cert4android-->
|
||||
<string name="certificate_notification_connection_security">DAVdroid: Tilkoblingssikkerhet</string>
|
||||
<string name="trust_certificate_unknown_certificate_found">DAVdroid har støtt på et ukjent sertifikat. Har du tiltro til det?</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="homepage_url">https://davdroid.bitfire.at/?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>
|
||||
@@ -20,13 +17,10 @@
|
||||
<string name="startup_donate_later">Misschien later</string>
|
||||
<string name="startup_google_play_accounts_removed">Play Store DRM fout-informatie</string>
|
||||
<string name="startup_google_play_accounts_removed_message">Onder bepaalde omstandigheden, kan Play Store DRM ervoor zorgen dat accounts kwijt zijn na een herstart of na een DAVdroid update. Als dit probleem zich bij je voordoet (en alleen dan), Installeer dan \"DAVdroid JB Workaround\" vanuit de Play Store</string>
|
||||
<string name="startup_google_play_accounts_removed_more_info">Meer informatie</string>
|
||||
<string name="startup_opentasks_not_installed">OpenTasks niet geinstalleerd</string>
|
||||
<string name="startup_opentasks_not_installed_message">De OpenTasks app is niet beschikbaar, Hierdoor is het voor DAVdroid niet mogelijk om uw taken te synchroniseren.</string>
|
||||
<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>
|
||||
@@ -38,12 +32,13 @@
|
||||
<string name="navigation_drawer_close">Sluit navigatie drawer</string>
|
||||
<string name="navigation_drawer_subtitle">CalDAV/CardDav Sync adapter</string>
|
||||
<string name="navigation_drawer_about">Over / Licentie</string>
|
||||
<string name="navigation_drawer_beta_feedback">Beta terugkoppeling</string>
|
||||
<string name="navigation_drawer_settings">Instellingen</string>
|
||||
<string name="navigation_drawer_news_updates">Nieuws & updates</string>
|
||||
<string name="navigation_drawer_external_links">Externe links</string>
|
||||
<string name="navigation_drawer_website">Website</string>
|
||||
<string name="navigation_drawer_faq">FAQ</string>
|
||||
<string name="navigation_drawer_faq_url">https://davdroid.bitfire.at/faq/?pk_campaign=davdroid-app</string>
|
||||
<string name="navigation_drawer_forums">Help / Forums</string>
|
||||
<string name="navigation_drawer_donate">Doneren</string>
|
||||
<string name="account_list_empty">Welkom bij DAVdroid!\n\nJe kunt nu een CalDAV/CardDAv account toevoegen.</string>
|
||||
<string name="accounts_global_sync_disabled">Systeembrede automatische synchronisatie is uitgeschakeld</string>
|
||||
@@ -86,23 +81,16 @@
|
||||
<string name="account_delete">Account verwijderen</string>
|
||||
<string name="account_delete_confirmation_title">Account echt verwijderen?</string>
|
||||
<string name="account_delete_confirmation_text">Alle lokale kopieën van adresboeken, agenda\'s en taken worden verwijderd.</string>
|
||||
<string name="account_carddav">CardDAV</string>
|
||||
<string name="account_caldav">CalDAV</string>
|
||||
<string name="account_webcal">Webcal</string>
|
||||
<string name="account_refresh_address_book_list">Adresboeken vernieuwen</string>
|
||||
<string name="account_create_new_address_book">Maak een nieuw adresboek</string>
|
||||
<string name="account_refresh_calendar_list">Agenda\'s vernieuwen</string>
|
||||
<string name="account_create_new_calendar">Maak een nieuwe agenda</string>
|
||||
<!--PermissionsActivity-->
|
||||
<string name="permissions_title">DAVdroid rechten</string>
|
||||
<string name="permissions_calendar">Agenda rechten</string>
|
||||
<string name="permissions_calendar_details">Om CalDAV afspraken te synchroniseren met u agenda dient DAVdroid toegang te verkrijgen. </string>
|
||||
<string name="permissions_calendar_request">Agenda rechten verkrijgen</string>
|
||||
<string name="permissions_contacts">Contact rechten</string>
|
||||
<string name="permissions_contacts_details">Om CalDAV afspraken te synchroniseren met u contacten dient DAVdroid toegang te verkrijgen. </string>
|
||||
<string name="permissions_contacts_request">Contacten rechten verkrijgen</string>
|
||||
<string name="permissions_opentasks">OpenTasks rechten</string>
|
||||
<string name="permissions_opentasks_details">Om CalDAV taken te synchroniseren met uw local takenlijst dient DAVdroid toegang te hebben tot OpenTasks</string>
|
||||
<string name="permissions_opentasks_request">OpenTasks rechten verkrijgen</string>
|
||||
<string name="account_no_webcal_handler_found">Geen mogelijke Webcal app gevonden</string>
|
||||
<string name="account_install_icsdroid">ICSdroid installeren</string>
|
||||
<!--AddAccountActivity-->
|
||||
<string name="login_help_url">https://davdroid.bitfire.at/configuration/?pk_campaign=davdroid-app</string>
|
||||
<string name="login_title">Account toevoegen</string>
|
||||
<string name="login_type_email">Inloggen met e-mailadres</string>
|
||||
<string name="login_email_address">Email adres</string>
|
||||
@@ -126,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>
|
||||
@@ -144,6 +131,9 @@
|
||||
<string name="settings_sync_wifi_only">Sync alleen tijdens WiFi</string>
|
||||
<string name="settings_sync_wifi_only_on">Synchronisatie is voorbehouden tijdens WiFi verbindingen</string>
|
||||
<string name="settings_sync_wifi_only_off">Verbinding type is niet overwogen</string>
|
||||
<string name="settings_sync_wifi_only_ssids">WiFi SSID beperking</string>
|
||||
<string name="settings_sync_wifi_only_ssids_on">Zal alleen synchroniseren over %s</string>
|
||||
<string name="settings_sync_wifi_only_ssids_off">Alle WiFI verbindingen zullen worden gebruikt</string>
|
||||
<string name="settings_carddav">CardDAV</string>
|
||||
<string name="settings_contact_group_method">Contact groep methode</string>
|
||||
<string-array name="settings_contact_group_method_values">
|
||||
@@ -165,6 +155,9 @@
|
||||
<string name="settings_manage_calendar_colors">Agenda kleuren beheren</string>
|
||||
<string name="settings_manage_calendar_colors_on">Agenda kleuren worden door DAVdroid beheerd.</string>
|
||||
<string name="settings_manage_calendar_colors_off">Agenda kleuren worden niet door DAVdroid ingesteld</string>
|
||||
<string name="settings_event_colors">Evenement kleur ondersteuning</string>
|
||||
<string name="settings_event_colors_on">Evenement kleuren synchroniseren</string>
|
||||
<string name="settings_event_colors_off">Evenement kleuren niet synchroniseren</string>
|
||||
<!--collection management-->
|
||||
<string name="create_addressbook">Maak adresboek</string>
|
||||
<string name="create_addressbook_display_name_hint">Mijn adresboek</string>
|
||||
@@ -193,29 +186,9 @@
|
||||
<string name="exception_show_details">Toon details</string>
|
||||
<!--sync adapters and DebugInfoActivity-->
|
||||
<string name="debug_info_title">Debug informatie</string>
|
||||
<string name="sync_contacts_read_only_address_book">Alleen-lezen adresboek</string>
|
||||
<string name="sync_error_permissions">DAVdroid rechten</string>
|
||||
<string name="sync_error_permissions_text">Aanvullende rechten vereist</string>
|
||||
<string name="sync_error_calendar">Agenda synchronisatie is mislukt (%s)</string>
|
||||
<string name="sync_error_contacts">Adresboek synchronisatie is mislukt (%s)</string>
|
||||
<string name="sync_error_tasks">Taak synchronisatie is mislukt (%s)</string>
|
||||
<string name="sync_error">Fout tijdens %s</string>
|
||||
<string name="sync_error_http_dav">Serverfout tijdens %s</string>
|
||||
<string name="sync_error_local_storage">Database fout tijdens %s</string>
|
||||
<string-array name="sync_error_phases">
|
||||
<item>synchronisatie voorbereiden</item>
|
||||
<item>querying mogelijkheden</item>
|
||||
<item>verwerken van lokaal verwijderde data</item>
|
||||
<item>voorberteiding maken/wijzigen data</item>
|
||||
<item>uploaden maken/bewerken data</item>
|
||||
<item>controleren syngronisatie voortgang</item>
|
||||
<item>lijst lokale data</item>
|
||||
<item>lijst remote data</item>
|
||||
<item>vergelijken lokale/remote data</item>
|
||||
<item>downloaden remote data</item>
|
||||
<item>nabewerking</item>
|
||||
<item>opslaan sync voortgang</item>
|
||||
</string-array>
|
||||
<string name="sync_error_unauthorized">Gebruikersnaam/wachtwoord onjuist</string>
|
||||
<!--cert4android-->
|
||||
<string name="certificate_notification_connection_security">DAVdroid: Verbinding beveiliging</string>
|
||||
<string name="trust_certificate_unknown_certificate_found">Davdroid is benaderd door een onbekend certificaat. Vertrouwd u dit?</string>
|
||||
|
||||
@@ -8,9 +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="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>
|
||||
@@ -19,13 +18,10 @@
|
||||
<string name="startup_donate_later">Może później</string>
|
||||
<string name="startup_google_play_accounts_removed">Informacje o błędzie DRM Sklepu Play</string>
|
||||
<string name="startup_google_play_accounts_removed_message">Pod pewnymi warunkami, DRM Sklepu Play może powodować, że wszystkie konta DAVdroid mogą zostać usunięte po uruchomieniu lub po uaktualnieniu DAVdroid. Jeśli jesteś dotknięty tym problemem (i tylko wtedy) należy zainstalować \"DAVdroid JB Obejście\" ze Sklepu Play.</string>
|
||||
<string name="startup_google_play_accounts_removed_more_info">Więcej informacji</string>
|
||||
<string name="startup_opentasks_not_installed">OpenTasks nie jest zainstalowany</string>
|
||||
<string name="startup_opentasks_not_installed_message">Aplikacja Open Tasks nie jest dostępna, więc DAVdroid nie będzie mógł synchronizować listy zadań. </string>
|
||||
<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>
|
||||
@@ -37,11 +33,13 @@
|
||||
<string name="navigation_drawer_close">Zamknij menu nawigacji</string>
|
||||
<string name="navigation_drawer_subtitle">Adapter synchronizacji CalDAV/CardDAV</string>
|
||||
<string name="navigation_drawer_about">O DAVdroid / Licencja</string>
|
||||
<string name="navigation_drawer_beta_feedback">Przekaż opinię</string>
|
||||
<string name="navigation_drawer_settings">Ustawienia</string>
|
||||
<string name="navigation_drawer_news_updates">Nowości & aktualizacje</string>
|
||||
<string name="navigation_drawer_external_links">Zewnętrzne odnośniki</string>
|
||||
<string name="navigation_drawer_website">Strona WWW</string>
|
||||
<string name="navigation_drawer_faq">FQA</string>
|
||||
<string name="navigation_drawer_manual">Ręcznie</string>
|
||||
<string name="navigation_drawer_faq">Pytania i odpowiedzi</string>
|
||||
<string name="navigation_drawer_forums">Pomoc / Forum</string>
|
||||
<string name="navigation_drawer_donate">Dotacja</string>
|
||||
<string name="account_list_empty">Witamy w DAVdroid!\n\nMożesz teraz dodać konto CalDAV/CardDAV.</string>
|
||||
@@ -60,8 +58,8 @@
|
||||
<string name="app_settings_override_proxy">Nadpisz ustawienia proxy</string>
|
||||
<string name="app_settings_override_proxy_on">Użyj niestandardowych ustawień proxy </string>
|
||||
<string name="app_settings_override_proxy_off">Użyj systemowych ustawień proxy</string>
|
||||
<string name="app_settings_override_proxy_host">Nazwa hosta HTTP proxy</string>
|
||||
<string name="app_settings_override_proxy_port">Port HTTP proxy</string>
|
||||
<string name="app_settings_override_proxy_host">Nazwa hosta proxy HTTP</string>
|
||||
<string name="app_settings_override_proxy_port">Port proxy HTTP</string>
|
||||
<string name="app_settings_security">Bezpieczeństwo</string>
|
||||
<string name="app_settings_distrust_system_certs">Usuń certyfikaty systemowe</string>
|
||||
<string name="app_settings_distrust_system_certs_on">CA systemowe i użytkownika nie zostaną dodane</string>
|
||||
@@ -70,11 +68,11 @@
|
||||
<string name="app_settings_reset_certificates_summary">Zresetuj wszystkie niestandardowe certyfikaty.</string>
|
||||
<string name="app_settings_reset_certificates_success">Wszystkie niestandardowe certyfikaty zostały wyczyszczone</string>
|
||||
<string name="app_settings_debug">Debugowanie</string>
|
||||
<string name="app_settings_log_to_external_storage">Loguj to zewnętrznego pliku</string>
|
||||
<string name="app_settings_log_to_external_storage">Loguj do zewnętrznego pliku</string>
|
||||
<string name="app_settings_log_to_external_storage_on">Logowanie do zewnętrznej pamięci (jeśli jest dostępna)</string>
|
||||
<string name="app_settings_log_to_external_storage_off">Logowanie do zewnętrznego pliku jest niedostępne</string>
|
||||
<string name="app_settings_show_debug_info">Pokaż informacje do debug\'owania</string>
|
||||
<string name="app_settings_show_debug_info_details">Przeglądaj/udostępnij oprogramowanie i szczegóły konfiguracji </string>
|
||||
<string name="app_settings_log_to_external_storage_off">Logowanie do zewnętrznego pliku jest wyłączone</string>
|
||||
<string name="app_settings_show_debug_info">Pokaż informacje do debugowania</string>
|
||||
<string name="app_settings_show_debug_info_details">Przejrzyj lub udostępnij informacje o programie i jego konfiguracji</string>
|
||||
<!--AccountActivity-->
|
||||
<string name="account_synchronize_now">Synchronizuj teraz</string>
|
||||
<string name="account_synchronizing_now">Synchronizcja w toku</string>
|
||||
@@ -88,37 +86,32 @@
|
||||
<string name="account_carddav">CardDAV</string>
|
||||
<string name="account_caldav">CalDAV</string>
|
||||
<string name="account_webcal">Webcal</string>
|
||||
<string name="account_refresh_address_book_list">Odśwież list książek adresowych</string>
|
||||
<string name="account_synchronize_this_collection">Synchronizuj kolekcję</string>
|
||||
<string name="account_read_only">tylko do odczytu</string>
|
||||
<string name="account_calendar">kalendarz</string>
|
||||
<string name="account_task_list">lista zadań</string>
|
||||
<string name="account_refresh_address_book_list">Odśwież listę książek adresowych</string>
|
||||
<string name="account_create_new_address_book">Stwórz nową książkę adresową</string>
|
||||
<string name="account_refresh_calendar_list">Odśwież liste kalendarzy</string>
|
||||
<string name="account_refresh_calendar_list">Odśwież listę kalendarzy</string>
|
||||
<string name="account_create_new_calendar">Stwórz nowy kalendarz</string>
|
||||
<string name="account_no_webcal_handler_found">Nie znaleziono aplikacji obsługującej Webcal</string>
|
||||
<string name="account_install_icsdroid">Zainstaluj ICSdroid</string>
|
||||
<string name="account_read_only_address_book_selected">Książka adresowa tylko do odczytu - lokalne zmiany zostaną usunięte</string>
|
||||
<!--PermissionsActivity-->
|
||||
<string name="permissions_title">Uprawnienia DAVdroid</string>
|
||||
<string name="permissions_calendar">Uprawnienia kalendarza</string>
|
||||
<string name="permissions_calendar_details">Aby synchronizować wydarzenia CalDav z lokalnymi kalendarzami, DAVdroid potrzebuje dostępu do twoich kalendarzy.</string>
|
||||
<string name="permissions_calendar_request">Zezwól na uprawnienia kalendarza</string>
|
||||
<string name="permissions_contacts">Uprawnienia kontaktów</string>
|
||||
<string name="permissions_contacts_details">Aby synchronizować książki adresowe CardDAV z lokalnymi kontaktami, DAVdroid potrzebuje dostępu do twoich kontaktów.</string>
|
||||
<string name="permissions_contacts_request">Zezwól na uprawnienia kontaktów</string>
|
||||
<string name="permissions_opentasks">Uprawnienia OpenTasks</string>
|
||||
<string name="permissions_opentasks_details">Aby synchronizować zadania CalDav z lokalnymi listami zadań, DAVdroid potrzebuje dostępu do OpenTasks.</string>
|
||||
<string name="permissions_opentasks_request">Zezwól na uprawnienia OpenTasks</string>
|
||||
<!--AddAccountActivity-->
|
||||
<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>
|
||||
<string name="login_email_address_error">Wymanagny poprawny adres e-mail</string>
|
||||
<string name="login_email_address_error">Wymagany poprawny adres e-mail</string>
|
||||
<string name="login_password">Hasło</string>
|
||||
<string name="login_password_required">Wymagane hasło</string>
|
||||
<string name="login_type_url">Logowanie za pomocą adresu URL i nazwy użytkownika</string>
|
||||
<string name="login_url_must_be_http_or_https">URL musi zaczynać się z http(s)://</string>
|
||||
<string name="login_url_must_be_http_or_https">URL musi zaczynać się od http(s)://</string>
|
||||
<string name="login_url_must_be_https">Adres URL musi zaczynać się od https://</string>
|
||||
<string name="login_url_host_name_required">Wymagana nazwa hosta</string>
|
||||
<string name="login_user_name">Nazwa użytkownika</string>
|
||||
<string name="login_user_name_required">Wymagana nazwa użtkonika</string>
|
||||
<string name="login_user_name_required">Wymagana nazwa użytkownika</string>
|
||||
<string name="login_base_url">Podstawowy URL</string>
|
||||
<string name="login_type_url_certificate">Logowanie za pomocą adresu URL i certyfikatu klienta</string>
|
||||
<string name="login_select_certificate">Wybierz certyfikat</string>
|
||||
<string name="login_login">Zaloguj</string>
|
||||
<string name="login_back">Wróć</string>
|
||||
<string name="login_create_account">Stwórz konto</string>
|
||||
@@ -130,32 +123,32 @@
|
||||
<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>
|
||||
<string name="settings_username">Nazwa użytkownika</string>
|
||||
<string name="settings_enter_username">Wpisz nazwe użytkownika:</string>
|
||||
<string name="settings_enter_username">Wpisz nazwę użytkownika:</string>
|
||||
<string name="settings_password">Hasło</string>
|
||||
<string name="settings_password_summary">Zaktualizuj hasło zgodnie z serwerem.</string>
|
||||
<string name="settings_enter_password">Wpisz hasło:</string>
|
||||
<string name="settings_certificate_alias">Alias certyfikatu klienta</string>
|
||||
<string name="settings_sync">Synchronizacja</string>
|
||||
<string name="settings_sync_interval_contacts">Okres synchronizacji kontktów</string>
|
||||
<string name="settings_sync_interval_contacts">Częstotliwość synchronizacji kontaktów</string>
|
||||
<string name="settings_sync_summary_manually">Tylko ręcznie</string>
|
||||
<string name="settings_sync_summary_periodically" tools:ignore="PluralsCandidate">Co %d minut oraz natychmiast przy zmianach lokalnych</string>
|
||||
<string name="settings_sync_interval_calendars">Okres synchronizacji kalendarzy</string>
|
||||
<string name="settings_sync_interval_tasks">Okres synchronizacji list zadań</string>
|
||||
<string name="settings_sync_interval_calendars">Częstotliwość synchronizacji kalendarzy</string>
|
||||
<string name="settings_sync_interval_tasks">Częstotliwość synchronizacji list zadań</string>
|
||||
<string-array name="settings_sync_interval_names">
|
||||
<item>Tylko ręcznie</item>
|
||||
<item>Co 15 minut</item>
|
||||
<item>Co 30 minut</item>
|
||||
<item>Co godzinę</item>
|
||||
<item>Co 2 godzinny</item>
|
||||
<item>Co 4 godzinny</item>
|
||||
<item>Co 2 godziny</item>
|
||||
<item>Co 4 godziny</item>
|
||||
<item>Raz dziennie</item>
|
||||
</string-array>
|
||||
<string name="settings_sync_wifi_only">Synchronizuj tylko przez WiFi</string>
|
||||
<string name="settings_sync_wifi_only_on">Synchronizacja jest ograniczony do połączeń WiFi</string>
|
||||
<string name="settings_sync_wifi_only_on">Synchronizacja jest ograniczona do połączeń WiFi</string>
|
||||
<string name="settings_sync_wifi_only_off">Rodzaj połączenia nie jest brany pod uwagę</string>
|
||||
<string name="settings_sync_wifi_only_ssids">Ograniczenia WiFi SSID</string>
|
||||
<string name="settings_sync_wifi_only_ssids_on">Będzie synchronizować tylko w %s</string>
|
||||
@@ -167,9 +160,11 @@
|
||||
<item>GROUP_VCARDS</item>
|
||||
<item>CATEGORIES</item>
|
||||
</string-array>
|
||||
<string name="settings_contact_group_method_change">Zmień metodę grupową</string>
|
||||
<string name="settings_contact_group_method_change_reload_contacts">Wymaga to ponownego pobrania wszystkich kontaktów. Niezapisane zmiany z tego telefonu zostaną usunięte.</string>
|
||||
<string name="settings_caldav">CalDAV</string>
|
||||
<string name="settings_sync_time_range_past">Limit czasowy przeszłych wydarzeń</string>
|
||||
<string name="settings_sync_time_range_past_none">Wszystkie wydarzenia zostaną synchronizowane</string>
|
||||
<string name="settings_sync_time_range_past_none">Wszystkie wydarzenia zostaną zsynchronizowane</string>
|
||||
<plurals name="settings_sync_time_range_past_days">
|
||||
<item quantity="one">Wydarzenia starsze niż jeden dzień zostaną zignorowane.</item>
|
||||
<item quantity="few">Wydarzenia starsze niż %d dni zostaną zignorowane.</item>
|
||||
@@ -181,13 +176,13 @@
|
||||
<string name="settings_manage_calendar_colors_on">Kolory kalendarza są zarządzane przez DAVdroid</string>
|
||||
<string name="settings_manage_calendar_colors_off">Kolory kalendarze nie są ustawiane przez DAVdroid</string>
|
||||
<string name="settings_event_colors">Obsługa kolorów wydarzeń</string>
|
||||
<string name="settings_event_colors_on">Synchronizuj kolorów zdarzeń</string>
|
||||
<string name="settings_event_colors_on">Synchronizuj kolory zdarzeń</string>
|
||||
<string name="settings_event_colors_off">Nie synchronizuj kolorów zdarzeń</string>
|
||||
<string name="settings_event_colors_off_confirm">Wyłączenie kolorów zdarzeń może usunąć już zsynchronizowane kolory zdarzeń.</string>
|
||||
<!--collection management-->
|
||||
<string name="create_addressbook">Stwórz książkę adresową</string>
|
||||
<string name="create_addressbook_display_name_hint">Moja książka adresowa</string>
|
||||
<string name="create_calendar">Stwórz kolekcje CalDAV</string>
|
||||
<string name="create_calendar">Stwórz kolekcję CalDAV</string>
|
||||
<string name="create_calendar_display_name_hint">Mój kalendarz</string>
|
||||
<string name="create_calendar_time_zone">Strefa czasowa:</string>
|
||||
<string name="create_calendar_type">Typ kolekcji:</string>
|
||||
@@ -198,13 +193,14 @@
|
||||
<string name="create_collection_creating">Tworzenie kolekcji</string>
|
||||
<string name="create_collection_display_name">Nazwa wyświetlana (tytuł) kolekcji:</string>
|
||||
<string name="create_collection_display_name_required">Tytuł jest wymagany</string>
|
||||
<string name="create_collection_description">Opis (opcjoalnie)</string>
|
||||
<string name="create_collection_description">Opis (opcjonalnie)</string>
|
||||
<string name="create_collection_home_set">Ustaw początek:</string>
|
||||
<string name="create_collection_create">Stwórz</string>
|
||||
<string name="delete_collection">Usuń kolekcje</string>
|
||||
<string name="delete_collection_confirm_title">Jesteś pewien?</string>
|
||||
<string name="delete_collection">Usuń kolekcję</string>
|
||||
<string name="delete_collection_confirm_title">Czy jesteś pewien?</string>
|
||||
<string name="delete_collection_confirm_warning">Kolekcja (%s) i jej wszystkie dane zostaną usunięte z serwera.</string>
|
||||
<string name="delete_collection_deleting_collection">Usuwanie kolekcji</string>
|
||||
<string name="collection_force_read_only">Wymuś tylko do odczytu</string>
|
||||
<!--ExceptionInfoFragment-->
|
||||
<string name="exception">Wystąpił błąd.</string>
|
||||
<string name="exception_httpexception">Wystąpił błąd HTTP.</string>
|
||||
@@ -221,27 +217,8 @@
|
||||
</plurals>
|
||||
<string name="sync_error_permissions">Uprawnienia DAVdroid</string>
|
||||
<string name="sync_error_permissions_text">Wymagane dodatkowe uprawnienia</string>
|
||||
<string name="sync_error_calendar">Synchronizacja kalendarza nie powiodała się (%s)</string>
|
||||
<string name="sync_error_contacts">Synchronizacja książki adresowej nie powiodała się (%s)</string>
|
||||
<string name="sync_error_tasks">Synchronizacja zadań nie powiodała się (%s)</string>
|
||||
<string name="sync_error">Błąd podczas %s</string>
|
||||
<string name="sync_error_http_dav">Błąd servera podczas %s</string>
|
||||
<string name="sync_error_local_storage">Bład bazy danych podczas %s</string>
|
||||
<string-array name="sync_error_phases">
|
||||
<item>przygotowanie synchronizacji</item>
|
||||
<item>odpytywanie możliwości</item>
|
||||
<item>przetwarzanie lokalnie usuniętych wpisów</item>
|
||||
<item>przygotowanie stworzonych/zmodyfikowanych wpisów</item>
|
||||
<item>wysyłanie stworzonych/zmodyfikowanych wpisów</item>
|
||||
<item>sprawdzanie stanu synchronizacji</item>
|
||||
<item>listowanie lokalnych wpisów</item>
|
||||
<item>listowanie zdalnych wpisów</item>
|
||||
<item>porównywanie lokalnych/zdalnych wpisów</item>
|
||||
<item>pobieranie zdalnych wpisów</item>
|
||||
<item>przetwarzanie końcowe</item>
|
||||
<item>zapisywanie stanu synchronizacji</item>
|
||||
</string-array>
|
||||
<string name="sync_error_unauthorized">Błędna nazwa użytkownika lub hasło</string>
|
||||
<string name="sync_error_opentasks_too_old">OpenTask jest niekompatybilny</string>
|
||||
<string name="sync_error_opentasks_required_version">Wymagana wersja: %1$s (obecna %2$s)</string>
|
||||
<!--cert4android-->
|
||||
<string name="certificate_notification_connection_security">DAVdroid: Bezpieczeństwo połączenia</string>
|
||||
<string name="trust_certificate_unknown_certificate_found">DAVdroid napotkał nieznany certyfikat. Czy chcesz go dodać?</string>
|
||||
|
||||
@@ -4,28 +4,41 @@
|
||||
<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>
|
||||
<string name="send">Enviar</string>
|
||||
<string name="notification_channel_debugging">Depuração</string>
|
||||
<string name="notification_channel_general">Outras mensagens importantes</string>
|
||||
<string name="notification_channel_sync">Sincronização</string>
|
||||
<string name="notification_channel_sync_errors">Erros de sincronização</string>
|
||||
<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_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>
|
||||
<string name="startup_donate_later">Talvez depois</string>
|
||||
<string name="startup_google_play_accounts_removed">Informação sobre o erro de DRM da Play Store</string>
|
||||
<string name="startup_google_play_accounts_removed_message">Sob certas condições, o DRM da Play Store pode fazer com que todas as contas DAVdroid sejam perdidas depois de uma reinicialização ou atualização do DAVdroid. Se você for afetado por esse problema, instale o \"DAVdroid JB Workaround\" a partir da Play Store.</string>
|
||||
<string name="startup_google_play_accounts_removed_more_info">Mais informações</string>
|
||||
<string name="startup_more_info">Mais informações</string>
|
||||
<string name="startup_opentasks_not_installed">O OpenTasks não está instalado</string>
|
||||
<string name="startup_opentasks_not_installed_message">O aplicativo OpenTasks não está disponível, não sendo possível sincronizar as listas de tarefas pelo DAVdroid.</string>
|
||||
<string name="startup_opentasks_not_installed_message">Para sincronizar tarefas é necessário instalar o aplicativo livre OpenTasks. (Não é necessário para contatos/eventos)</string>
|
||||
<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>
|
||||
@@ -37,10 +50,12 @@
|
||||
<string name="navigation_drawer_close">Fechar gaveta de navegação</string>
|
||||
<string name="navigation_drawer_subtitle">Sincronização de CalDAV/CardDAV</string>
|
||||
<string name="navigation_drawer_about">Sobre / Licença</string>
|
||||
<string name="navigation_drawer_beta_feedback">Comentários sobre a versão beta</string>
|
||||
<string name="navigation_drawer_settings">Configurações</string>
|
||||
<string name="navigation_drawer_news_updates">Novidades e atualizações</string>
|
||||
<string name="navigation_drawer_external_links">Links externos</string>
|
||||
<string name="navigation_drawer_website">Site na Web</string>
|
||||
<string name="navigation_drawer_manual">Manual</string>
|
||||
<string name="navigation_drawer_faq">Perguntas fequentes</string>
|
||||
<string name="navigation_drawer_forums">Ajuda / Fóruns</string>
|
||||
<string name="navigation_drawer_donate">Doações</string>
|
||||
@@ -85,29 +100,21 @@
|
||||
<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>
|
||||
<string name="account_synchronize_this_collection">sincronizar esta coleção</string>
|
||||
<string name="account_read_only">Somente leitura</string>
|
||||
<string name="account_calendar">calendário</string>
|
||||
<string name="account_task_list">lista de tarefas</string>
|
||||
<string name="account_refresh_address_book_list">Atualizar lista de livros de endereços</string>
|
||||
<string name="account_create_new_address_book">Criar novo livro de endereços</string>
|
||||
<string name="account_refresh_calendar_list">Atualizar lista de calendários</string>
|
||||
<string name="account_create_new_calendar">Criar novo calendário</string>
|
||||
<string name="account_no_webcal_handler_found">Não foi encontrado um aplicativo capaz de lidar com Webcal</string>
|
||||
<string name="account_install_icsdroid">Instalar ICSdroid</string>
|
||||
<string name="account_read_only_address_book_selected">Livro de endereços somente leitura – as alterações locais serão descartadas!</string>
|
||||
<!--PermissionsActivity-->
|
||||
<string name="permissions_title">Permissões do DAVdroid</string>
|
||||
<string name="permissions_calendar">Permissões do calendário</string>
|
||||
<string name="permissions_calendar_details">Para sincronizar os eventos CalDAV com seus calendários locais, o DAVdroid precisa acessar seus calendários.</string>
|
||||
<string name="permissions_calendar_request">Solicitar permissão do calendário</string>
|
||||
<string name="permissions_contacts">Permissões dos contados</string>
|
||||
<string name="permissions_contacts_details">Para sincronizar livros de endereços CardDAV com seus contatos locais, o DAVdroid precisa acessar seus contatos.</string>
|
||||
<string name="permissions_contacts_request">Solicitar permissão dos contatos</string>
|
||||
<string name="permissions_opentasks">Permissões do OpenTasks</string>
|
||||
<string name="permissions_opentasks_details">Para sincronizar tarefas CalDAV com suas listas de tarefas locais, o DAVdroid precisa acessar o OpenTasks.</string>
|
||||
<string name="permissions_opentasks_request">Solicitar permissão do OpenTasks</string>
|
||||
<!--AddAccountActivity-->
|
||||
<string name="login_help_url">https://davdroid.bitfire.at/configuration/?pk_campaign=davdroid-app</string>
|
||||
<string name="login_title">Adicionar conta</string>
|
||||
<string name="login_type_email">Autenticação com endereço de e-mail</string>
|
||||
<string name="login_email_address">Endereço de e-mail</string>
|
||||
@@ -116,10 +123,13 @@
|
||||
<string name="login_password_required">É necessário uma senha</string>
|
||||
<string name="login_type_url">Autenticação com usuário e URL</string>
|
||||
<string name="login_url_must_be_http_or_https">A URL deve começar com http(s)://</string>
|
||||
<string name="login_url_must_be_https">A URL deve começar com https://</string>
|
||||
<string name="login_url_host_name_required">É necessário um nome de máquina</string>
|
||||
<string name="login_user_name">Usuário</string>
|
||||
<string name="login_user_name_required">É necessário um nome de usuário</string>
|
||||
<string name="login_base_url">URL base</string>
|
||||
<string name="login_type_url_certificate">Autenticação com URL e certificado do cliente</string>
|
||||
<string name="login_select_certificate">Selecionar certificado</string>
|
||||
<string name="login_login">Autenticar</string>
|
||||
<string name="login_back">Voltar</string>
|
||||
<string name="login_create_account">Criar conta</string>
|
||||
@@ -131,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>
|
||||
@@ -140,6 +150,7 @@
|
||||
<string name="settings_password">Senha</string>
|
||||
<string name="settings_password_summary">Atualize a senha de acordo com seu servidor</string>
|
||||
<string name="settings_enter_password">Digite sua senha:</string>
|
||||
<string name="settings_certificate_alias">Nome do certificado do cliente</string>
|
||||
<string name="settings_sync">Sincronização</string>
|
||||
<string name="settings_sync_interval_contacts">Intervalo sinc. de contatos</string>
|
||||
<string name="settings_sync_summary_manually">Apenas manualmente</string>
|
||||
@@ -172,6 +183,8 @@
|
||||
<item>Grupos são VCards separados</item>
|
||||
<item>Grupos são categorias por contato</item>
|
||||
</string-array>
|
||||
<string name="settings_contact_group_method_change">Alterar a forma de agrupamento</string>
|
||||
<string name="settings_contact_group_method_change_reload_contacts">É necessário recarregar todos os contatos. As alterações locais que não foram salvas serão descartadas.</string>
|
||||
<string name="settings_caldav">CalDAV</string>
|
||||
<string name="settings_sync_time_range_past">Limite de tempo para eventos passados</string>
|
||||
<string name="settings_sync_time_range_past_none">Todos os eventos serão sincronizados</string>
|
||||
@@ -208,6 +221,10 @@
|
||||
<string name="delete_collection_confirm_title">Tem certeza?</string>
|
||||
<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>
|
||||
@@ -222,27 +239,14 @@
|
||||
</plurals>
|
||||
<string name="sync_error_permissions">Permissões do DAVdroid</string>
|
||||
<string name="sync_error_permissions_text">É necessário permissões adicionais</string>
|
||||
<string name="sync_error_calendar">Falha na sincronização do calendário (%s)</string>
|
||||
<string name="sync_error_contacts">Falha na sincronização do livro de endereços (%s)</string>
|
||||
<string name="sync_error_tasks">Falha na sincronização das tarefas (%s)</string>
|
||||
<string name="sync_error">Erro ao %s</string>
|
||||
<string name="sync_error_http_dav">Erro do servidor ao %s</string>
|
||||
<string name="sync_error_local_storage">Erro do banco de dados ao %s</string>
|
||||
<string-array name="sync_error_phases">
|
||||
<item>preparando sincronização</item>
|
||||
<item>procurando habilidades</item>
|
||||
<item>processando os itens excluídos localmente</item>
|
||||
<item>preparando os itens criados/modificados</item>
|
||||
<item>enviando os itens criados/modificados</item>
|
||||
<item>verificando o estado da sincronização</item>
|
||||
<item>listando os itens locais</item>
|
||||
<item>listando os itens remotos</item>
|
||||
<item>comparando os itens locais/remotos</item>
|
||||
<item>baixando os itens remotos</item>
|
||||
<item>pós-processamento</item>
|
||||
<item>salvando o estado da sincronização</item>
|
||||
</string-array>
|
||||
<string name="sync_error_unauthorized">Usuário/senha incorreto</string>
|
||||
<string name="sync_error_opentasks_too_old">A versão do OpenTasks é muito antiga</string>
|
||||
<string name="sync_error_opentasks_required_version">Versão necessária: %1$s (atual %2$s)</string>
|
||||
<string name="sync_error_authentication_failed">Falha de autenticação (verifique as credenciais)</string>
|
||||
<string name="sync_error_io">Erro de rede ou E/S – %s</string>
|
||||
<string name="sync_error_http_dav">Erro no servidor HTTP – %s</string>
|
||||
<string name="sync_error_local_storage">Erro de armazenamento local – %s</string>
|
||||
<string name="sync_error_retry">Repetir</string>
|
||||
<string name="sync_error_view_item">Ver item</string>
|
||||
<!--cert4android-->
|
||||
<string name="certificate_notification_connection_security">DAVdroid: Segurança da conexão</string>
|
||||
<string name="trust_certificate_unknown_certificate_found">O DAVdroid encontrou um certificado desconhecido. Deseja torná-lo confiável?</string>
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
<!--DavService-->
|
||||
<!--AppSettingsActivity-->
|
||||
<!--AccountActivity-->
|
||||
<!--PermissionsActivity-->
|
||||
<!--AddAccountActivity-->
|
||||
<string name="login_type_email">Login com seu enderço de email</string>
|
||||
<string name="login_type_url">Login com URL e nome do usuário</string>
|
||||
|
||||
@@ -2,56 +2,76 @@
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<!--common strings-->
|
||||
<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="please_wait">Пожалуйста, подождите …</string>
|
||||
<string name="send">Отправить</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">Ошибки сети и ввода/вывода</string>
|
||||
<string name="notification_channel_sync_status">Сообщения о состоянии</string>
|
||||
<!--startup dialogs-->
|
||||
<string name="startup_battery_optimization">Оптимизация батареи</string>
|
||||
<string name="startup_battery_optimization_message">Андроид может отключить/уменьшить синхронизацию 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_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>
|
||||
<string name="startup_donate_message">Мы рады, что вы используете DAVdroid, который является программным обеспечением с открытым исходным кодом (GPLv3). Поскольку разработка DAVdroid - тяжелая работа и заняла у нас нас очень много времени, пожалуйста, рассмотрите возможность поддержать проект.</string>
|
||||
<string name="startup_donate_now">Показать страницу пожертвования</string>
|
||||
<string name="startup_donate_later">Возможно, позже</string>
|
||||
<string name="startup_google_play_accounts_removed">Информация об ошибке в Play Store DRM</string>
|
||||
<string name="startup_google_play_accounts_removed_message">При определённых условиях Play Store DRM может стать причиной потери всех DAVdroid аккаунтов после перезагрузки устройства или после обновления DAVdroid. Если Вы столкнулись с этой проблемой (и только в этом случае), установите \"DAVdroid JB Workaround\" из Play Store.</string>
|
||||
<string name="startup_google_play_accounts_removed_more_info">Дополнительно</string>
|
||||
<string name="startup_google_play_accounts_removed_message">При определенных условиях Play Store DRM может стать причиной потери всех DAVdroid аккаунтов после перезагрузки устройства или после обновления DAVdroid. Если Вы столкнулись с этой проблемой (и только в этом случае), установите \"DAVdroid JB Workaround\" из Play Store.</string>
|
||||
<string name="startup_more_info">Дополнительная информация</string>
|
||||
<string name="startup_opentasks_not_installed">OpenTasks не установлен</string>
|
||||
<string name="startup_opentasks_not_installed_message">OpenTasks недоступен, DAVdroid не сможет синхронизировать список задач</string>
|
||||
<string name="startup_opentasks_reinstall_davdroid">После установки OpenTasks необходимо ПЕРЕУСТАНОВИТЬ DAVdroid и добавить Ваши аккаунты ещё раз (ошибка Android).</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_license_info_no_warranty">Эта программа поставляется АБСОЛЮТНО БЕЗ КАКИХ-ЛИБО ГАРАНТИЙ. Это свободная программа, и вы приглашаетесь повторно распространять ее при определенных условиях.</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>
|
||||
<string name="logging_davdroid_file_logging">Файл журнала DAVdroid</string>
|
||||
<string name="logging_to_external_storage">Сохранение логов во внешнем хранилище: %s</string>
|
||||
<string name="logging_couldnt_create_file">Не удалось создать внешний лог файл: %s</string>
|
||||
<string name="logging_couldnt_create_file">Не удалось создать внешний файл журнала: %s</string>
|
||||
<string name="logging_no_external_storage">Внешнее хранилище не найдено</string>
|
||||
<!--AccountsActivity-->
|
||||
<string name="navigation_drawer_open">Открыть панель навигации</string>
|
||||
<string name="navigation_drawer_close">Закрыть панель навигации</string>
|
||||
<string name="navigation_drawer_subtitle">Адаптер синхронизации CalDAV/CardDAV</string>
|
||||
<string name="navigation_drawer_about">О программе / Лицензия</string>
|
||||
<string name="navigation_drawer_beta_feedback">Отзыв о бета-тестировании</string>
|
||||
<string name="navigation_drawer_settings">Настройки</string>
|
||||
<string name="navigation_drawer_news_updates">Новости и обновления</string>
|
||||
<string name="navigation_drawer_news_updates">Новости & обновления</string>
|
||||
<string name="navigation_drawer_external_links">Внешние ссылки</string>
|
||||
<string name="navigation_drawer_website">Веб сайт</string>
|
||||
<string name="navigation_drawer_faq">ЧАВО</string>
|
||||
<string name="navigation_drawer_website">Веб-сайт</string>
|
||||
<string name="navigation_drawer_manual">Руководство</string>
|
||||
<string name="navigation_drawer_faq">FAQ</string>
|
||||
<string name="navigation_drawer_forums">Помощь / Форумы</string>
|
||||
<string name="navigation_drawer_donate">Пожертвовать</string>
|
||||
<string name="account_list_empty">Вас приветствует DAVdroid\n\nМожете добавить CalDAV/CardDAV аккаунт сейчас.</string>
|
||||
<string name="account_list_empty">Добро пожаловать в DAVdroid\n\nТеперь вы можете добавить аккаунт CalDAV/CardDAV.</string>
|
||||
<string name="accounts_global_sync_disabled">Синхронизация отключена на уровне устройства</string>
|
||||
<string name="accounts_global_sync_enable">Включить</string>
|
||||
<!--DavService-->
|
||||
<string name="dav_service_refresh_failed">Не удалось обнаружить сервисы</string>
|
||||
<string name="dav_service_refresh_couldnt_refresh">Невозможно обновить список коллекций</string>
|
||||
<string name="dav_service_refresh_failed">Сбой при обнаружении службы</string>
|
||||
<string name="dav_service_refresh_couldnt_refresh">Не удалось обновить список коллекций</string>
|
||||
<!--AppSettingsActivity-->
|
||||
<string name="app_settings">Настройки</string>
|
||||
<string name="app_settings_user_interface">Интерфейс пользователя</string>
|
||||
<string name="app_settings_reset_hints">Включить подсказки</string>
|
||||
<string name="app_settings_reset_hints_summary">Включить подсказки, которые были отключены ранее</string>
|
||||
<string name="app_settings_reset_hints_success">Все подсказки будут показаны снова</string>
|
||||
<string name="app_settings_connection">Соединение</string>
|
||||
<string name="app_settings_connection">Подключение</string>
|
||||
<string name="app_settings_override_proxy">Переопределить настройки прокси-сервера</string>
|
||||
<string name="app_settings_override_proxy_on">Использовать пользовательские настройки прокси-сервера</string>
|
||||
<string name="app_settings_override_proxy_off">Использовать системные настройки прокси-сервера</string>
|
||||
@@ -63,89 +83,111 @@
|
||||
<string name="app_settings_distrust_system_certs_off">Доверять системным и добавленным пользователем CA (рекомендуется)</string>
|
||||
<string name="app_settings_reset_certificates">Сброс (не)доверенных сертификатов</string>
|
||||
<string name="app_settings_reset_certificates_summary">Отменить доверие ко всем пользовательским сертификатам</string>
|
||||
<string name="app_settings_reset_certificates_success">Все пользовательские сертификаты были очищены</string>
|
||||
<string name="app_settings_reset_certificates_success">Все пользовательские сертификаты были удалены</string>
|
||||
<string name="app_settings_debug">Отладка</string>
|
||||
<string name="app_settings_log_to_external_storage">Сохранять лог во внешний файл</string>
|
||||
<string name="app_settings_log_to_external_storage_on">Сохранение логов во внешнем хранилище (если доступно)</string>
|
||||
<string name="app_settings_log_to_external_storage_off">Сохранение логов во внешний файл отключено</string>
|
||||
<string name="app_settings_show_debug_info">Отладочная информация</string>
|
||||
<string name="app_settings_show_debug_info">Показать отладочную информацию</string>
|
||||
<string name="app_settings_show_debug_info_details">Просмотреть/поделиться программой и настройками</string>
|
||||
<!--AccountActivity-->
|
||||
<string name="account_synchronize_now">Синхронизировать</string>
|
||||
<string name="account_synchronizing_now">Синхронизация</string>
|
||||
<string name="account_settings">Настройки аккаунта</string>
|
||||
<string name="account_rename">Переименовать аккаунт</string>
|
||||
<string name="account_rename_new_name">Несохранённые локальные данные могут быть потеряны. Требуется выполнить синхронизацию после переименования. Новое имя аккаунта:</string>
|
||||
<string name="account_rename_new_name">Несохраненные локальные данные могут быть потеряны. Необходима повторная синхронизация после переименования. Новое имя аккаунта:</string>
|
||||
<string name="account_rename_rename">Переименовать</string>
|
||||
<string name="account_delete">Удалить аккаунт</string>
|
||||
<string name="account_delete_confirmation_title">Вы действительно хотите удалить аккаунт?</string>
|
||||
<string name="account_delete_confirmation_text">Все локальные копии контактов, календарей и задач будут удалены.</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>
|
||||
<string name="account_synchronize_this_collection">синхронизировать эту коллекцию</string>
|
||||
<string name="account_read_only">только для чтения</string>
|
||||
<string name="account_calendar">календарь</string>
|
||||
<string name="account_task_list">список задач</string>
|
||||
<string name="account_refresh_address_book_list">Обновить список адресных книг</string>
|
||||
<string name="account_create_new_address_book">Создать новую адресную книгу</string>
|
||||
<string name="account_refresh_calendar_list">Обновить календарь</string>
|
||||
<string name="account_refresh_calendar_list">Обновить список календарей</string>
|
||||
<string name="account_create_new_calendar">Создать новый календарь</string>
|
||||
<!--PermissionsActivity-->
|
||||
<string name="permissions_title">Разрешения DAVdroid</string>
|
||||
<string name="permissions_calendar">Разрешения для календаря</string>
|
||||
<string name="permissions_calendar_details">Для синхронизации CalDAV событий с Вашими локальными календарями DAVdroid должен иметь доступ к Вашим календарям.</string>
|
||||
<string name="permissions_calendar_request">Запрос разрешений для календаря</string>
|
||||
<string name="permissions_contacts">Разрешения для контактов</string>
|
||||
<string name="permissions_contacts_details">Для синхронизации CardDAV адресных книг с Вашими локальными контактами DAVdroid должен иметь доступ к Вашим контактам.</string>
|
||||
<string name="permissions_contacts_request">Запрос разрешений для контактов</string>
|
||||
<string name="permissions_opentasks">Разрешения OpenTasks</string>
|
||||
<string name="permissions_opentasks_details">Для синхронизации CalDAV задач с Вашими локальными списками задач DAVdroid должен иметь доступ к OpenTasks.</string>
|
||||
<string name="permissions_opentasks_request">Запрос разрешений для OpenTasks</string>
|
||||
<string name="account_no_webcal_handler_found">Не найдено приложение, поддерживающее WebCal</string>
|
||||
<string name="account_install_icsdroid">Установить ICSdroid</string>
|
||||
<!--AddAccountActivity-->
|
||||
<string name="login_title">Добавить аккаунт</string>
|
||||
<string name="login_type_email">Вход по адресу электронной почты</string>
|
||||
<string name="login_type_email">Вход с адресом электронной почты</string>
|
||||
<string name="login_email_address">Адрес электронной почты</string>
|
||||
<string name="login_email_address_error">Требуется действующий адрес электронной почты</string>
|
||||
<string name="login_password">Пароль</string>
|
||||
<string name="login_password_required">Требуется пароль</string>
|
||||
<string name="login_type_url">Вход через URL и имя пользователя</string>
|
||||
<string name="login_type_url">Вход с URL и именем пользователя</string>
|
||||
<string name="login_url_must_be_http_or_https">URL должен начинаться с http(s)://</string>
|
||||
<string name="login_url_must_be_https">URL-адрес должен начинаться с https://</string>
|
||||
<string name="login_url_host_name_required">Требуется имя хоста</string>
|
||||
<string name="login_user_name">Имя</string>
|
||||
<string name="login_user_name_required">Требуется имя</string>
|
||||
<string name="login_user_name">Имя пользователя</string>
|
||||
<string name="login_user_name_required">Требуется Имя пользователя</string>
|
||||
<string name="login_base_url">Базовый URL</string>
|
||||
<string name="login_login">Логин</string>
|
||||
<string name="login_type_url_certificate">Войти с URL-адресом и сертификатом клиента</string>
|
||||
<string name="login_select_certificate">Выберите сертификат</string>
|
||||
<string name="login_login">Вход</string>
|
||||
<string name="login_back">Назад</string>
|
||||
<string name="login_create_account">Создать аккаунт</string>
|
||||
<string name="login_account_name">Имя аккаунта</string>
|
||||
<string name="login_account_name_info">Используйте Ваш адрес адрес электронной почты в качестве имени аккаунта, так как Android будет использовать имя аккаунта в поле ORGANIZER для событий, которые Вы создаёте. Вы не можете иметь два аккаунта с одинаковыми именами.</string>
|
||||
<string name="login_account_name_info">Используйте ваш адрес адрес электронной почты в качестве имени аккаунта, поскольку Android будет использовать имя аккаунта в поле ORGANIZER для событий, которые вы создаете. У вас не может быть двух аккаунтов с тем же именем.</string>
|
||||
<string name="login_account_contact_group_method">Метод группировки контактов:</string>
|
||||
<string name="login_account_name_required">Требуется имя аккаунта</string>
|
||||
<string name="login_account_not_created">Аккаунт не может быть создан</string>
|
||||
<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_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>
|
||||
<string name="settings_username">Имя</string>
|
||||
<string name="settings_username">Имя пользователя</string>
|
||||
<string name="settings_enter_username">Введите имя пользователя:</string>
|
||||
<string name="settings_password">Пароль</string>
|
||||
<string name="settings_password_summary">Обновить пароль в соответствии с вашим сервером.</string>
|
||||
<string name="settings_password_summary">Обновить пароль</string>
|
||||
<string name="settings_enter_password">Введите свой пароль:</string>
|
||||
<string name="settings_certificate_alias">Псевдоним сертификата клиента</string>
|
||||
<string name="settings_sync">Синхронизация</string>
|
||||
<string name="settings_sync_interval_contacts">Интервал синхронизации контактов</string>
|
||||
<string name="settings_sync_summary_manually">Вручную</string>
|
||||
<string name="settings_sync_summary_periodically" tools:ignore="PluralsCandidate">Каждые %d минут и немедленно при локальных изменениях</string>
|
||||
<string name="settings_sync_interval_calendars">Интервал синхронизации календарей</string>
|
||||
<string name="settings_sync_interval_tasks">Интервал синхронизации задач</string>
|
||||
<string-array name="settings_sync_interval_names">
|
||||
<item>Только вручную</item>
|
||||
<item>Каждые 15 минут</item>
|
||||
<item>Каждые 30 минут</item>
|
||||
<item>Каждый час</item>
|
||||
<item>Каждые 2 часа</item>
|
||||
<item>Каждые 4 часа</item>
|
||||
<item>Один раз в день</item>
|
||||
</string-array>
|
||||
<string name="settings_sync_wifi_only">Синхронизировать только через WiFi</string>
|
||||
<string name="settings_sync_wifi_only_on">Разрешить синхронизацию только через WiFi</string>
|
||||
<string name="settings_sync_wifi_only_off">Не учитывать тип соединения</string>
|
||||
<string name="settings_sync_wifi_only_ssids">Ограничение WiFi SSID</string>
|
||||
<string name="settings_sync_wifi_only_ssids_on">Будет синхронизироваться только %s</string>
|
||||
<string name="settings_sync_wifi_only_ssids_off">Все соединения WiFi будут использоваться</string>
|
||||
<string name="settings_sync_wifi_only_ssids_message">Названия (SSID), разделенные запятыми разрешенных сетей Wi-Fi (оставьте пустым для всех)</string>
|
||||
<string name="settings_carddav">CardDAV</string>
|
||||
<string name="settings_contact_group_method">Метод группировки контактов</string>
|
||||
<string-array name="settings_contact_group_method_entries">
|
||||
<item>Группы как отдельные vCards</item>
|
||||
<item>Группы как категории внутри контакта</item>
|
||||
<string-array name="settings_contact_group_method_values">
|
||||
<item>GROUP_VCARDS</item>
|
||||
<item>CATEGORIES</item>
|
||||
</string-array>
|
||||
<string-array name="settings_contact_group_method_entries">
|
||||
<item>Группы являются отдельными vCards</item>
|
||||
<item>Группы являются категориями контактов</item>
|
||||
</string-array>
|
||||
<string name="settings_contact_group_method_change">Изменить метод группировки</string>
|
||||
<string name="settings_contact_group_method_change_reload_contacts">Вам потребуется перезагрузить все контакты. Несохраненные локальные изменения будут отброшены.</string>
|
||||
<string name="settings_caldav">CalDAV</string>
|
||||
<string name="settings_sync_time_range_past">Интервал синхронизации</string>
|
||||
<string name="settings_sync_time_range_past_none">Все события будут синхронизированы</string>
|
||||
<string name="settings_sync_time_range_past">Ограничение по времени прошедшего события</string>
|
||||
<string name="settings_sync_time_range_past_none">Все события будут синхронизироваться</string>
|
||||
<plurals name="settings_sync_time_range_past_days">
|
||||
<item quantity="one">События старше одного дня будут игнорироваться</item>
|
||||
<item quantity="few">События старше %d дней будут игнорироваться</item>
|
||||
@@ -154,60 +196,62 @@
|
||||
</plurals>
|
||||
<string name="settings_sync_time_range_past_message">События старше указанного количества дней будут игнорироваться (может быть 0). Оставьте пустым для синхронизации всех событий.</string>
|
||||
<string name="settings_manage_calendar_colors">Управление цветами календаря</string>
|
||||
<string name="settings_manage_calendar_colors_on">Цвета календаря управляются DAVdroid</string>
|
||||
<string name="settings_manage_calendar_colors_off">Цвета календаря не управляются DAVdroid</string>
|
||||
<string name="settings_manage_calendar_colors_on">Цвета календаря устанавливаются DAVdroid</string>
|
||||
<string name="settings_manage_calendar_colors_off">Цвета календаря не устанавливаются DAVdroid</string>
|
||||
<string name="settings_event_colors">Поддержка цвета событий</string>
|
||||
<string name="settings_event_colors_on">Синхронизация цветов событий</string>
|
||||
<string name="settings_event_colors_off">Не синхронизировать цвета событий</string>
|
||||
<string name="settings_event_colors_off_confirm">Отключение цветов событий может удалить уже синхронизированные цвета событий.</string>
|
||||
<!--collection management-->
|
||||
<string name="create_addressbook">Создать адресную книгу</string>
|
||||
<string name="create_addressbook_display_name_hint">Моя Адресная книга</string>
|
||||
<string name="create_calendar">Создать CalDAV коллекцию</string>
|
||||
<string name="create_calendar_display_name_hint">Мой Календарь</string>
|
||||
<string name="create_addressbook_display_name_hint">Моя адресная книга</string>
|
||||
<string name="create_calendar">Создать коллекцию CalDAV</string>
|
||||
<string name="create_calendar_display_name_hint">Мой календарь</string>
|
||||
<string name="create_calendar_time_zone">Часовой пояс:</string>
|
||||
<string name="create_calendar_type">Тип коллекции:</string>
|
||||
<string name="create_calendar_type_only_events">Календарь (только события)</string>
|
||||
<string name="create_calendar_type_only_tasks">Список задач (только задачи)</string>
|
||||
<string name="create_calendar_type_events_and_tasks">Совмещённый (события и задачи)</string>
|
||||
<string name="create_calendar_type_events_and_tasks">Совмещенный (события и задачи)</string>
|
||||
<string name="create_collection_color">Установить цвет коллекции</string>
|
||||
<string name="create_collection_creating">Создание коллекции</string>
|
||||
<string name="create_collection_display_name">Отображаемое имя (название) этой коллекции:</string>
|
||||
<string name="create_collection_display_name_required">Требуется название</string>
|
||||
<string name="create_collection_description">Описание (необязательно):</string>
|
||||
<string name="create_collection_home_set">Главная папка:</string>
|
||||
<string name="create_collection_home_set">Адрес каталога:</string>
|
||||
<string name="create_collection_create">Создать</string>
|
||||
<string name="delete_collection">Удалить коллекцию</string>
|
||||
<string name="delete_collection_confirm_title">Вы уверены?</string>
|
||||
<string name="delete_collection_confirm_warning">Коллекция (%s) и все её данные будут удалены с сервера.</string>
|
||||
<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>
|
||||
<string name="exception_ioexception">Произошла ошибка ввода/вывода.</string>
|
||||
<string name="exception_show_details">Подробнее</string>
|
||||
<string name="exception_show_details">Показать детали</string>
|
||||
<!--sync adapters and DebugInfoActivity-->
|
||||
<string name="debug_info_title">Отладочная информация</string>
|
||||
<string name="sync_contacts_read_only_address_book">Адресная книга только для чтения</string>
|
||||
<plurals name="sync_contacts_local_contact_changes_discarded">
|
||||
<item quantity="one">Локальное изменение контакта отменено</item>
|
||||
<item quantity="few">%d локальных изменений контакта отменены</item>
|
||||
<item quantity="many">%d локальных изменений контакта отменены</item>
|
||||
<item quantity="other">%d локальных изменений контакта отменены</item>
|
||||
</plurals>
|
||||
<string name="sync_error_permissions">Разрешения DAVdroid</string>
|
||||
<string name="sync_error_permissions_text">Требуются дополнительные разрешения</string>
|
||||
<string name="sync_error_calendar">Ошибка при синхронизации Календаря (%s)</string>
|
||||
<string name="sync_error_contacts">Ошибка при синхронизации Контактов (%s)</string>
|
||||
<string name="sync_error_tasks">Ошибка при синхронизации Задачи (%s)</string>
|
||||
<string name="sync_error">Ошибка %s</string>
|
||||
<string name="sync_error_http_dav">Ошибка сервера %s</string>
|
||||
<string name="sync_error_local_storage">Ошибка базы данных %s</string>
|
||||
<string-array name="sync_error_phases">
|
||||
<item>подготовка синхронизации</item>
|
||||
<item>запрос поддерживаемых возможностей</item>
|
||||
<item>обработка локально удалённых записей</item>
|
||||
<item>подготовка созданных/изменённых записей</item>
|
||||
<item>загрузка созданных/изменённых записей</item>
|
||||
<item>проверка статуса синхронизации</item>
|
||||
<item>просмотр локальных записей</item>
|
||||
<item>просмотр серверных записей</item>
|
||||
<item>сравнение локальных/серверных записей</item>
|
||||
<item>загрузка серверных записей</item>
|
||||
<item>пост-обработка</item>
|
||||
<item>сохранение статуса синхронизации</item>
|
||||
</string-array>
|
||||
<string name="sync_error_unauthorized">Имя пользователя/пароль неверные</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">Ошибка сети или ввода/вывода – %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="certificate_notification_connection_security">DAVdroid: безопасность подключения</string>
|
||||
<string name="trust_certificate_unknown_certificate_found">DAVdroid обнаружил неизвестный сертификат. Вы хотите доверять ему?</string>
|
||||
</resources>
|
||||
|
||||
@@ -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>
|
||||
@@ -19,13 +17,10 @@
|
||||
<string name="startup_donate_later">Можда касније</string>
|
||||
<string name="startup_google_play_accounts_removed">Грешка ДРМ-а Плеј продавнице</string>
|
||||
<string name="startup_google_play_accounts_removed_message">Под одређеним околностима ДРМ Плеј продавнице може да узрокује да сви налози ДАВдроида нестане након рестарта или ажурирања ДАВдроида. Ако и само ако имате овај проблем, инсталирајте „DAVdroid JB Workaround“ са Плеј продавнице.</string>
|
||||
<string name="startup_google_play_accounts_removed_more_info">Још информација</string>
|
||||
<string name="startup_opentasks_not_installed">Отворени задаци нису инсталирани</string>
|
||||
<string name="startup_opentasks_not_installed_message">Отворени задаци нису доступни па ДАВдроид неће моћи да синхронизује листе задатака. </string>
|
||||
<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>
|
||||
@@ -88,17 +83,6 @@
|
||||
<string name="account_create_new_address_book">Направи нови адресар</string>
|
||||
<string name="account_refresh_calendar_list">Освежи списак календара</string>
|
||||
<string name="account_create_new_calendar">Направи нови календар</string>
|
||||
<!--PermissionsActivity-->
|
||||
<string name="permissions_title">ДАВдроид дозволе</string>
|
||||
<string name="permissions_calendar">Дозволе за календар</string>
|
||||
<string name="permissions_calendar_details">Да би синхронизовао КалДАВ догађаје са вашим локалним календарима, ДАВдроиду треба приступ вашим календарима.</string>
|
||||
<string name="permissions_calendar_request">Захтевај дозволе за календар</string>
|
||||
<string name="permissions_contacts">Дозволе за контакте</string>
|
||||
<string name="permissions_contacts_details">Да би синхронизовао КардДАВ адресаре са вашим локалним контактима, ДАВдроиду треба приступ вашим контактима.</string>
|
||||
<string name="permissions_contacts_request">Захтевај дозволе за контакте</string>
|
||||
<string name="permissions_opentasks">Дозволе за задатке</string>
|
||||
<string name="permissions_opentasks_details">Да би синхронизовао КалДАВ задатке са вашим локалним листама задатака, ДАВдроиду треба приступ Задацима.</string>
|
||||
<string name="permissions_opentasks_request">Захтевај дозволе за задатке</string>
|
||||
<!--AddAccountActivity-->
|
||||
<string name="login_title">Додај налог</string>
|
||||
<string name="login_type_email">Пријавите се адресом е-поште</string>
|
||||
@@ -123,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>
|
||||
@@ -193,27 +176,6 @@
|
||||
<string name="debug_info_title">Подаци за исправљање грешака</string>
|
||||
<string name="sync_error_permissions">ДАВдроид дозволе</string>
|
||||
<string name="sync_error_permissions_text">Потребне су додатне доволе</string>
|
||||
<string name="sync_error_calendar">Синхронизација календара није успела (%s)</string>
|
||||
<string name="sync_error_contacts">Синхронизација адресара није успела (%s)</string>
|
||||
<string name="sync_error_tasks">Синхронизација задатака није успела (%s)</string>
|
||||
<string name="sync_error">Грешка током %s</string>
|
||||
<string name="sync_error_http_dav">Грешка сервера током %s</string>
|
||||
<string name="sync_error_local_storage">Грешка базе података током %s</string>
|
||||
<string-array name="sync_error_phases">
|
||||
<item>припремам синхронизацију</item>
|
||||
<item>проверавам могућности</item>
|
||||
<item>обрађујем локално обрисане уносе</item>
|
||||
<item>припремам направљене/измењене уносе</item>
|
||||
<item>отпремам направљене/измењене уносе</item>
|
||||
<item>проверавам стање синхронизације</item>
|
||||
<item>излиставам локалне уносе</item>
|
||||
<item>излиставам удаљене уносе</item>
|
||||
<item>упоређујем локалне/удаљене уносе</item>
|
||||
<item>преузимам удаљене уносе</item>
|
||||
<item>додатна обрада</item>
|
||||
<item>уписујем стање синхронизације</item>
|
||||
</string-array>
|
||||
<string name="sync_error_unauthorized">Корисничко име или лозинка погрешни</string>
|
||||
<!--cert4android-->
|
||||
<string name="certificate_notification_connection_security">ДАВдроид: Безбедност везе</string>
|
||||
<string name="trust_certificate_unknown_certificate_found">ДАВдроид је наишао на непознат сертификат. Желите ли да се поуздате у њега?</string>
|
||||
|
||||
@@ -14,13 +14,10 @@
|
||||
<string name="startup_donate_later">Belki sonra</string>
|
||||
<string name="startup_google_play_accounts_removed">Play Store DRM hata bilgisi</string>
|
||||
<string name="startup_google_play_accounts_removed_message">Bazı durumlarda Play Store DRM\'i, cihazı yeniden başlatınca veya DAVdroid\'i yükseltince DAVdroid hesaplarının yokolmasına sebep olabiliyor. Bu sorundan etkileniyorsan (ve sadece bu durumda), lütfen Play Store\'daki \"DAVdroid JB Workaround\" uygulamasını kur.</string>
|
||||
<string name="startup_google_play_accounts_removed_more_info">Daha fazla bilgi</string>
|
||||
<string name="startup_opentasks_not_installed">OpenTasks kurulu değil</string>
|
||||
<string name="startup_opentasks_not_installed_message">OpenTasks uygulaması yok, dolayısıyla DAVdroid iş listelerini senkronize edemeyecektir.</string>
|
||||
<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>
|
||||
@@ -66,18 +63,6 @@
|
||||
<string name="account_create_new_address_book">Yeni rehber oluştur</string>
|
||||
<string name="account_refresh_calendar_list">Takvim listesini yenile</string>
|
||||
<string name="account_create_new_calendar">Yeni takvim oluştur</string>
|
||||
<!--PermissionsActivity-->
|
||||
<string name="permissions_title">DAVdroid izinleri</string>
|
||||
<string name="permissions_calendar">Takvim izinleri</string>
|
||||
<string name="permissions_calendar_details">Takvim (sadece olaylar)
|
||||
</string>
|
||||
<string name="permissions_calendar_request">Takvim izinleri iste</string>
|
||||
<string name="permissions_contacts">Kişiler izinleri</string>
|
||||
<string name="permissions_contacts_details">CardDAV rehberlerinin cihazınızdaki kişilerinizle senkronize edebilmek için, DAVdroid cihazınızdaki kişilerinize erişime ihtiyacı vardır.</string>
|
||||
<string name="permissions_contacts_request">Kişiler izinleri iste</string>
|
||||
<string name="permissions_opentasks">OpenTasks izinleri</string>
|
||||
<string name="permissions_opentasks_details">CalDav iş listelerinizi yerel iş listelerinizle senkronize edebilmek için DAVdroid\'in OpenTasks\'e erişime ihtiyacı vardır.</string>
|
||||
<string name="permissions_opentasks_request">OpenTasks izinleri iste</string>
|
||||
<!--AddAccountActivity-->
|
||||
<string name="login_title">Hesap ekle</string>
|
||||
<string name="login_type_email">Eposta adresi ile giriş yap</string>
|
||||
@@ -101,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>
|
||||
@@ -123,6 +107,7 @@
|
||||
<string name="settings_sync_time_range_past">Geçmiş olay zaman sınırı</string>
|
||||
<string name="settings_sync_time_range_past_none">Tüm olaylar senkronize edilecek</string>
|
||||
<plurals name="settings_sync_time_range_past_days">
|
||||
<item quantity="one">%d günden daha eski olaylar göz ardı edilecektir</item>
|
||||
<item quantity="other">%d günden daha eski olaylar göz ardı edilecektir</item>
|
||||
</plurals>
|
||||
<string name="settings_sync_time_range_past_message">Bu sayıdan daha eski olan olaylar yok sayılacaktır (0 olabilir). Tüm olayları senkronize etmek için boş bırak.</string>
|
||||
@@ -160,12 +145,5 @@
|
||||
<string name="debug_info_title">Hata ayıklama bilgisi</string>
|
||||
<string name="sync_error_permissions">DAVdroid izinleri</string>
|
||||
<string name="sync_error_permissions_text">Ek izinler zorunludur</string>
|
||||
<string name="sync_error_calendar">Takvim senkronizasyonu başarısız (%s)</string>
|
||||
<string name="sync_error_contacts">Rehber senkronizasyonu başarısız (%s)</string>
|
||||
<string name="sync_error_tasks">İş senkronizasyonu başarısız (%s)</string>
|
||||
<string name="sync_error">%s yaparken hata</string>
|
||||
<string name="sync_error_http_dav">%s yaparken sunucu hatası</string>
|
||||
<string name="sync_error_local_storage">%s yaparken veritabanı hatası</string>
|
||||
<string name="sync_error_unauthorized">Kullanıcı adı/parola yanlış</string>
|
||||
<!--cert4android-->
|
||||
</resources>
|
||||
|
||||
@@ -8,9 +8,13 @@
|
||||
<string name="manage_accounts">Керування обліковими записами</string>
|
||||
<string name="please_wait">Будь ласка, зачекайте...</string>
|
||||
<string name="send">Відправити</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">Помилка мережі та вводу/виводу</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_battery_optimization_disable">Вимкнути для DAVdroid</string>
|
||||
<string name="startup_dont_show_again">Не показувати знову</string>
|
||||
<string name="startup_donate">Інформація Open-Source</string>
|
||||
@@ -19,13 +23,12 @@
|
||||
<string name="startup_donate_later">Можливо пізніше</string>
|
||||
<string name="startup_google_play_accounts_removed">Інформація про ваду в Play Store DRM</string>
|
||||
<string name="startup_google_play_accounts_removed_message">При деяких обставинах Play Store DRM може стати причиною втрати всіх облікових записів DAVdroid після перезавантаження пристрою чи оновлення DAVdroid. Якщо ви зіткнулися із цим (і лише у цьому випадку), будь ласка, встановіть \"DAVdroid JB Workaround\" з Play Store.</string>
|
||||
<string name="startup_google_play_accounts_removed_more_info">Детальніше</string>
|
||||
<string name="startup_more_info">Додаткова інформація</string>
|
||||
<string name="startup_opentasks_not_installed">OpenTasks не встановлено</string>
|
||||
<string name="startup_opentasks_not_installed_message">Додаток OpenTasks не встановлено, ото ж DAVdroid не зможе синхронізувати завдання.</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_license_info_no_warranty">Цей програмний засіб постачається АБСОЛЮТНО БЕЗ БУДЬ-ЯКИХ ГАРАНТІЙ. Це вільне програмне забезпечення, і ви можете поширювати її, за деякими умовами.</string>
|
||||
<!--global settings-->
|
||||
<string name="logging_davdroid_file_logging">Файл звітування DAVdroid</string>
|
||||
@@ -37,11 +40,14 @@
|
||||
<string name="navigation_drawer_close">Закрити панель навігації</string>
|
||||
<string name="navigation_drawer_subtitle">Адаптер синхронізації CalDAV/CardDAV</string>
|
||||
<string name="navigation_drawer_about">Про / Ліцензія</string>
|
||||
<string name="navigation_drawer_beta_feedback">Beta відгук</string>
|
||||
<string name="navigation_drawer_settings">Налаштування</string>
|
||||
<string name="navigation_drawer_news_updates">Новини та оновлення</string>
|
||||
<string name="navigation_drawer_external_links">Зовнішні посилання</string>
|
||||
<string name="navigation_drawer_website">Веб сайт</string>
|
||||
<string name="navigation_drawer_manual">Посібник</string>
|
||||
<string name="navigation_drawer_faq">Питання/Відповіді</string>
|
||||
<string name="navigation_drawer_forums">Допомога / Форуми</string>
|
||||
<string name="navigation_drawer_donate">Підтримка</string>
|
||||
<string name="account_list_empty">Вітаємо у DAVdroid!\n\nТепер можете додавати облікові записи CalDAV/CardDAV.</string>
|
||||
<string name="accounts_global_sync_disabled">Автоматичну синхронізацію вимкнено зі сторони системи</string>
|
||||
@@ -84,21 +90,19 @@
|
||||
<string name="account_delete">Видалити запис</string>
|
||||
<string name="account_delete_confirmation_title">Дійсно видалити обліковий запис?</string>
|
||||
<string name="account_delete_confirmation_text">Всі локальні копії адресних книг, календарів та завдань будуть вилучені.</string>
|
||||
<string name="account_carddav">CardDAV</string>
|
||||
<string name="account_caldav">CalDAV</string>
|
||||
<string name="account_webcal">Webcal</string>
|
||||
<string name="account_synchronize_this_collection">синхронізувати дану колекцію</string>
|
||||
<string name="account_read_only">лише читання</string>
|
||||
<string name="account_calendar">календар</string>
|
||||
<string name="account_task_list">перелік завдань</string>
|
||||
<string name="account_refresh_address_book_list">Оновити перелік адресних книг</string>
|
||||
<string name="account_create_new_address_book">Створити нову адресну книгу</string>
|
||||
<string name="account_refresh_calendar_list">Оновити перелік каленарів</string>
|
||||
<string name="account_create_new_calendar">Створити новий календар</string>
|
||||
<!--PermissionsActivity-->
|
||||
<string name="permissions_title">Дозволи DAVdroid</string>
|
||||
<string name="permissions_calendar">Дозволи календаря</string>
|
||||
<string name="permissions_calendar_details">Для синхронізації CalDAV подій з локальним календарем необхідний дозвіл до Ваших календарів.</string>
|
||||
<string name="permissions_calendar_request">Запит дозволів календаря</string>
|
||||
<string name="permissions_contacts">Дозволи контактів</string>
|
||||
<string name="permissions_contacts_details">Для синхронізації адресної книги CalDAV з локальними контактами необхідний дозвіл до Ваших контактів.</string>
|
||||
<string name="permissions_contacts_request">Запит дозволів контактів</string>
|
||||
<string name="permissions_opentasks">Дозволи OpenTasks</string>
|
||||
<string name="permissions_opentasks_details">Для синхронізації CalDAV завдань з локальним списком завдань необхідний дозвіл до OpenTasks.</string>
|
||||
<string name="permissions_opentasks_request">Запит дозволів OpenTasks</string>
|
||||
<string name="account_no_webcal_handler_found">Не знайдено додатку з підтримкою Webcal</string>
|
||||
<string name="account_install_icsdroid">Встановити ICSdroid</string>
|
||||
<!--AddAccountActivity-->
|
||||
<string name="login_title">Додати запис</string>
|
||||
<string name="login_type_email">Увійти за допомогою електронної пошти</string>
|
||||
@@ -108,10 +112,13 @@
|
||||
<string name="login_password_required">Потребує пароль</string>
|
||||
<string name="login_type_url">Увійти за допомогою URL та імені користувача</string>
|
||||
<string name="login_url_must_be_http_or_https">URL адреса повинна починатися з http(s)://</string>
|
||||
<string name="login_url_must_be_https">Посилання повинно починатися з https://</string>
|
||||
<string name="login_url_host_name_required">Потребує назву хосту</string>
|
||||
<string name="login_user_name">Ім\'я користувача</string>
|
||||
<string name="login_user_name_required">Потребує ім\'я користувача</string>
|
||||
<string name="login_base_url">Базовий URL</string>
|
||||
<string name="login_type_url_certificate">Вхід по посиланню та сертифікату клієнта</string>
|
||||
<string name="login_select_certificate">Обрати сертифікат</string>
|
||||
<string name="login_login">Увійти</string>
|
||||
<string name="login_back">Назад</string>
|
||||
<string name="login_create_account">Створити запис</string>
|
||||
@@ -123,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>
|
||||
@@ -132,33 +138,58 @@
|
||||
<string name="settings_password">Пароль</string>
|
||||
<string name="settings_password_summary">Оновити пароль, згідно налаштувань Вашого сервера.</string>
|
||||
<string name="settings_enter_password">Введіть Ваш пароль:</string>
|
||||
<string name="settings_certificate_alias">Прив\'язка сертифікату клієнта</string>
|
||||
<string name="settings_sync">Синхронізація</string>
|
||||
<string name="settings_sync_interval_contacts">Інтервал синхронізації контактів</string>
|
||||
<string name="settings_sync_summary_manually">Лише вручну</string>
|
||||
<string name="settings_sync_summary_periodically" tools:ignore="PluralsCandidate">Кожних %d хвилин, а також негайно при внесенні локальних змін</string>
|
||||
<string name="settings_sync_interval_calendars">Інтервал синхронізації календарів</string>
|
||||
<string name="settings_sync_interval_tasks">Інтервал синхронізації завдань</string>
|
||||
<string-array name="settings_sync_interval_names">
|
||||
<item>Вручну</item>
|
||||
<item>Кожні 15 хвилин</item>
|
||||
<item>Кожні 30 хвилин</item>
|
||||
<item>Щогодинно</item>
|
||||
<item>Кожні 2 години</item>
|
||||
<item>Кожні 4 години</item>
|
||||
<item>Щоденно</item>
|
||||
</string-array>
|
||||
<string name="settings_sync_wifi_only">Синхронізувати лише через Wi-Fi</string>
|
||||
<string name="settings_sync_wifi_only_on">Виконувати синхронізацію лише через Wi-Fi</string>
|
||||
<string name="settings_sync_wifi_only_off">Не враховувати тип з\'єднання</string>
|
||||
<string name="settings_sync_wifi_only_ssids">Обмеження WiFi SSID</string>
|
||||
<string name="settings_sync_wifi_only_ssids_on">Синхронізувати лише через %s</string>
|
||||
<string name="settings_sync_wifi_only_ssids_off">Може використовуватись всі Wi-Fi з\'єднання</string>
|
||||
<string name="settings_sync_wifi_only_ssids_message">Назви (SSID) дозволених Wi-Fi мереж, розділені комами (залиште порожнім для всіх)</string>
|
||||
<string name="settings_carddav">CardDAV</string>
|
||||
<string name="settings_contact_group_method">Метод групування контактів</string>
|
||||
<string-array name="settings_contact_group_method_values">
|
||||
<item>GROUP_VCARDS</item>
|
||||
<item>CATEGORIES</item>
|
||||
</string-array>
|
||||
<string-array name="settings_contact_group_method_entries">
|
||||
<item>Групи як окремі VCard</item>
|
||||
<item>Групи як категорії в середині контактів</item>
|
||||
</string-array>
|
||||
<string name="settings_contact_group_method_change">Змінити метод групування</string>
|
||||
<string name="settings_contact_group_method_change_reload_contacts">Це потребує перезавантаження всіх контактів. Незбережені локальні зміни буде відхилено.</string>
|
||||
<string name="settings_caldav">CalDAV</string>
|
||||
<string name="settings_sync_time_range_past">Інтервал синхронізації</string>
|
||||
<string name="settings_sync_time_range_past_none">Всі події будуть синхронізовані</string>
|
||||
<plurals name="settings_sync_time_range_past_days">
|
||||
<item quantity="one">Події старші одного дня будуть проігноровані</item>
|
||||
<item quantity="few">Події старші %d днів будуть проігноровані</item>
|
||||
<item quantity="many">Події старші %d днів будуть проігноровані</item>
|
||||
<item quantity="other">Події старші %d днів будуть проігноровані</item>
|
||||
</plurals>
|
||||
<string name="settings_sync_time_range_past_message">Події старші вказаного часу будуть проігноровані (може бути 0). Залиште порожнім, аби синхронізувати всі події.</string>
|
||||
<string name="settings_manage_calendar_colors">Керування кольорами календаря</string>
|
||||
<string name="settings_manage_calendar_colors_on">Кольори календаря керуються DAVdroid</string>
|
||||
<string name="settings_manage_calendar_colors_off">Кольори календаря не керуються DAVdroid</string>
|
||||
<string name="settings_event_colors">Підтримка кольорів подій</string>
|
||||
<string name="settings_event_colors_on">Синхронізувати кольори подій</string>
|
||||
<string name="settings_event_colors_off">Не синхронізувати кольори подій</string>
|
||||
<string name="settings_event_colors_off_confirm">Вимикання кольорів подій може видалити вже синхронізовані кольори подій.</string>
|
||||
<!--collection management-->
|
||||
<string name="create_addressbook">Створити адресну книгу</string>
|
||||
<string name="create_addressbook_display_name_hint">Моя адресна книга</string>
|
||||
@@ -180,6 +211,7 @@
|
||||
<string name="delete_collection_confirm_title">Ви впевнені?</string>
|
||||
<string name="delete_collection_confirm_warning">Колекція (%s) та всі пов\'язані данні будуть вилучені з даного серверу.</string>
|
||||
<string name="delete_collection_deleting_collection">Видалення колекції</string>
|
||||
<string name="collection_force_read_only">Примусово лише читання</string>
|
||||
<!--ExceptionInfoFragment-->
|
||||
<string name="exception">Трапилась помилка.</string>
|
||||
<string name="exception_httpexception">Трапилась помилка HTTP.</string>
|
||||
@@ -187,29 +219,23 @@
|
||||
<string name="exception_show_details">Показати подробиці</string>
|
||||
<!--sync adapters and DebugInfoActivity-->
|
||||
<string name="debug_info_title">Інформація зневадження</string>
|
||||
<string name="sync_contacts_read_only_address_book">Адресна книга лише для читання</string>
|
||||
<plurals name="sync_contacts_local_contact_changes_discarded">
|
||||
<item quantity="one">Локальну зміну контакту відхилено</item>
|
||||
<item quantity="few">%d локальні зміни контакту відхилено</item>
|
||||
<item quantity="many">%d локальних змін контакту відхилено</item>
|
||||
<item quantity="other">%d локальних змін контакту відхилено</item>
|
||||
</plurals>
|
||||
<string name="sync_error_permissions">Дозволи DAVdroid</string>
|
||||
<string name="sync_error_permissions_text">Потребує додаткові дозволи</string>
|
||||
<string name="sync_error_calendar">Помилка синхронізації календаря (%s)</string>
|
||||
<string name="sync_error_contacts">Помилка синхронізації адресної книги (%s)</string>
|
||||
<string name="sync_error_tasks">Помилка синхронізації завдань (%s)</string>
|
||||
<string name="sync_error">Помилка під час %s</string>
|
||||
<string name="sync_error_http_dav">Помилка серверу під час %s</string>
|
||||
<string name="sync_error_local_storage">Помилка серверу під час %s</string>
|
||||
<string-array name="sync_error_phases">
|
||||
<item>підготовка синхронізації</item>
|
||||
<item>опитування можливостей</item>
|
||||
<item>обробка локально вилучених записів</item>
|
||||
<item>підготовка створених/змінених записів</item>
|
||||
<item>вивантаження створених/змінених записів</item>
|
||||
<item>перевірка стану синхронізації</item>
|
||||
<item>перелік локальних записів</item>
|
||||
<item>перелік віддалених записів</item>
|
||||
<item>порівняння локальних/віддалених записів</item>
|
||||
<item>завантаження віддалених записів</item>
|
||||
<item>після обробка</item>
|
||||
<item>збереження стану синхронізації</item>
|
||||
</string-array>
|
||||
<string name="sync_error_unauthorized">Невірне ім\'я користувача чи пароль</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">Помилка мережі та вводу/виводу — %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>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!--
|
||||
~ Copyright © 2013 – 2016 Ricki Hirner (bitfire web engineering).
|
||||
~ 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
|
||||
@@ -8,11 +8,10 @@
|
||||
|
||||
<resources>
|
||||
|
||||
<style name="AppTheme.NoActionBar">
|
||||
<item name="windowActionBar">false</item>
|
||||
<item name="windowNoTitle">true</item>
|
||||
<item name="android:windowDrawsSystemBarBackgrounds">true</item>
|
||||
<item name="android:statusBarColor">@android:color/transparent</item>
|
||||
<style name="AppThemeExt" parent="AppTheme">
|
||||
<item name="android:windowActivityTransitions">true</item>
|
||||
<item name="android:windowEnterTransition">@android:transition/slide_right</item>
|
||||
<item name="android:windowExitTransition">@android:transition/slide_left</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
||||
|
||||
@@ -8,10 +8,8 @@
|
||||
<string name="manage_accounts">管理账户</string>
|
||||
<string name="please_wait">请稍等...</string>
|
||||
<string name="send">发送</string>
|
||||
<string name="homepage_url">https://davdroid.bitfire.at/?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>
|
||||
@@ -20,13 +18,10 @@
|
||||
<string name="startup_donate_later">稍后提示</string>
|
||||
<string name="startup_google_play_accounts_removed">Play 商店 DRM 问题提醒</string>
|
||||
<string name="startup_google_play_accounts_removed_message">在某些情况下,Play 商店的 DRM 可能会导致所有 DAVdroid 账户在设备重启或升级 DAVdroid 后消失。如果你遇到了该问题,请从 Play 商店安装“DAVdroid JB Workaround”,否则请不要安装修复程序。</string>
|
||||
<string name="startup_google_play_accounts_removed_more_info">更多信息</string>
|
||||
<string name="startup_opentasks_not_installed">OpenTasks 未安装</string>
|
||||
<string name="startup_opentasks_not_installed_message">未安装 OpenTasks 应用,故 DAVdroid 无法同步任务列表。</string>
|
||||
<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>
|
||||
@@ -38,15 +33,16 @@
|
||||
<string name="navigation_drawer_close">关闭导航抽屉</string>
|
||||
<string name="navigation_drawer_subtitle">CalDAV/CardDAV 同步器</string>
|
||||
<string name="navigation_drawer_about">关于 / 许可</string>
|
||||
<string name="navigation_drawer_beta_feedback">测试版反馈</string>
|
||||
<string name="navigation_drawer_settings">设置</string>
|
||||
<string name="navigation_drawer_news_updates">最新消息</string>
|
||||
<string name="navigation_drawer_external_links">外部链接</string>
|
||||
<string name="navigation_drawer_website">应用网站</string>
|
||||
<string name="navigation_drawer_manual">手动</string>
|
||||
<string name="navigation_drawer_faq">常见问题</string>
|
||||
<string name="navigation_drawer_faq_url">https://davdroid.bitfire.at/faq/?pk_campaign=davdroid-app</string>
|
||||
<string name="navigation_drawer_forums">帮助 / 论坛</string>
|
||||
<string name="navigation_drawer_donate">捐助</string>
|
||||
<string name="account_list_empty">欢迎使用 DAVdroid!\n\n现在你可以增加 CalDAV/CardDAV 账户。</string>
|
||||
<string name="account_list_empty">欢迎使用 DAVdroid!\n\n请开始增加 CalDAV/CardDAV 账户。</string>
|
||||
<string name="accounts_global_sync_disabled">系统全局自动同步已禁用</string>
|
||||
<string name="accounts_global_sync_enable">启用</string>
|
||||
<!--DavService-->
|
||||
@@ -90,25 +86,17 @@
|
||||
<string name="account_carddav">CardDAV</string>
|
||||
<string name="account_caldav">CalDAV</string>
|
||||
<string name="account_webcal">Webcal</string>
|
||||
<string name="account_synchronize_this_collection">同步该集合</string>
|
||||
<string name="account_read_only">只读</string>
|
||||
<string name="account_calendar">日历</string>
|
||||
<string name="account_task_list">任务列表</string>
|
||||
<string name="account_refresh_address_book_list">刷新通讯录列表</string>
|
||||
<string name="account_create_new_address_book">创建通讯录</string>
|
||||
<string name="account_refresh_calendar_list">刷新日历列表</string>
|
||||
<string name="account_create_new_calendar">创建日历</string>
|
||||
<string name="account_no_webcal_handler_found">找不到支持 Webcal 的应用</string>
|
||||
<string name="account_install_icsdroid">安装 ICSdroid</string>
|
||||
<!--PermissionsActivity-->
|
||||
<string name="permissions_title">DAVdroid 权限</string>
|
||||
<string name="permissions_calendar">日历权限</string>
|
||||
<string name="permissions_calendar_details">要把 CalDAV 事件与本地日历同步,DAVdroid 需要日历权限。</string>
|
||||
<string name="permissions_calendar_request">请求日历权限</string>
|
||||
<string name="permissions_contacts">通讯录权限</string>
|
||||
<string name="permissions_contacts_details">要把 CardDAV 通讯录与本地通讯录同步,DAVdroid 需要通讯录权限。</string>
|
||||
<string name="permissions_contacts_request">请求通讯录权限</string>
|
||||
<string name="permissions_opentasks">OpenTasks 权限</string>
|
||||
<string name="permissions_opentasks_details">要把 CalDAV 任务与本地任务列表同步,DAVdroid 需要访问 OpenTasks。</string>
|
||||
<string name="permissions_opentasks_request">请求 OpenTasks 权限</string>
|
||||
<!--AddAccountActivity-->
|
||||
<string name="login_help_url">https://davdroid.bitfire.at/configuration/?pk_campaign=davdroid-app</string>
|
||||
<string name="login_title">增加账户</string>
|
||||
<string name="login_type_email">使用邮箱地址登录</string>
|
||||
<string name="login_email_address">Email 地址</string>
|
||||
@@ -116,11 +104,14 @@
|
||||
<string name="login_password">密码</string>
|
||||
<string name="login_password_required">请输入密码</string>
|
||||
<string name="login_type_url">使用 URL 和用户名登录</string>
|
||||
<string name="login_url_must_be_http_or_https">URL 必须以 http(s):// 为开头</string>
|
||||
<string name="login_url_must_be_http_or_https">URL 需以 http(s):// 为开头</string>
|
||||
<string name="login_url_must_be_https">URL 需以 https:// 为开头</string>
|
||||
<string name="login_url_host_name_required">请输入主机名</string>
|
||||
<string name="login_user_name">用户名</string>
|
||||
<string name="login_user_name_required">请输入用户名</string>
|
||||
<string name="login_base_url">根地址</string>
|
||||
<string name="login_type_url_certificate">使用 URL 和客户端证书登录</string>
|
||||
<string name="login_select_certificate">选择证书</string>
|
||||
<string name="login_login">登录</string>
|
||||
<string name="login_back">返回</string>
|
||||
<string name="login_create_account">创建账户</string>
|
||||
@@ -132,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>
|
||||
@@ -141,6 +131,7 @@
|
||||
<string name="settings_password">密码</string>
|
||||
<string name="settings_password_summary">修改服务器密码</string>
|
||||
<string name="settings_enter_password">输入密码</string>
|
||||
<string name="settings_certificate_alias">客户端证书别名</string>
|
||||
<string name="settings_sync">同步</string>
|
||||
<string name="settings_sync_interval_contacts">通讯录自动同步间隔</string>
|
||||
<string name="settings_sync_summary_manually">手动同步</string>
|
||||
@@ -170,9 +161,11 @@
|
||||
<item>CATEGORIES</item>
|
||||
</string-array>
|
||||
<string-array name="settings_contact_group_method_entries">
|
||||
<item>分为不同的 VCards</item>
|
||||
<item>每个联系人的分类</item>
|
||||
<item>按 VCard 文件分组</item>
|
||||
<item>按联系人分类分组</item>
|
||||
</string-array>
|
||||
<string name="settings_contact_group_method_change">修改分组方式</string>
|
||||
<string name="settings_contact_group_method_change_reload_contacts">本次操作需要重新载入所有联系人,未同步的本地修改会被撤销。</string>
|
||||
<string name="settings_caldav">CalDAV</string>
|
||||
<string name="settings_sync_time_range_past">旧日程时间限制</string>
|
||||
<string name="settings_sync_time_range_past_none">同步所有日程</string>
|
||||
@@ -208,6 +201,7 @@
|
||||
<string name="delete_collection_confirm_title">你确定吗?</string>
|
||||
<string name="delete_collection_confirm_warning">这个集合 %s 及其所有数据将会从服务器删除。</string>
|
||||
<string name="delete_collection_deleting_collection">正在删除集合</string>
|
||||
<string name="collection_force_read_only">强制只读</string>
|
||||
<!--ExceptionInfoFragment-->
|
||||
<string name="exception">出现错误</string>
|
||||
<string name="exception_httpexception">出现 HTTP 错误</string>
|
||||
@@ -215,29 +209,14 @@
|
||||
<string name="exception_show_details">显示详情</string>
|
||||
<!--sync adapters and DebugInfoActivity-->
|
||||
<string name="debug_info_title">调试信息</string>
|
||||
<string name="sync_contacts_read_only_address_book">只读通讯录</string>
|
||||
<plurals name="sync_contacts_local_contact_changes_discarded">
|
||||
<item quantity="other">%d 本地联系人修改被撤销</item>
|
||||
</plurals>
|
||||
<string name="sync_error_permissions">DAVdroid 权限</string>
|
||||
<string name="sync_error_permissions_text">需要额外权限</string>
|
||||
<string name="sync_error_calendar">日历同步失败(%s)</string>
|
||||
<string name="sync_error_contacts">通讯录同步失败(%s)</string>
|
||||
<string name="sync_error_tasks">任务同步失败(%s)</string>
|
||||
<string name="sync_error">%s时错误</string>
|
||||
<string name="sync_error_http_dav">%s时服务器错误</string>
|
||||
<string name="sync_error_local_storage">%s时数据库错误</string>
|
||||
<string-array name="sync_error_phases">
|
||||
<item>准备同步</item>
|
||||
<item>请求功能列表</item>
|
||||
<item>处理本地删除项目</item>
|
||||
<item>准备创建/修改项目</item>
|
||||
<item>上传创建/修改项目</item>
|
||||
<item>检查同步状态</item>
|
||||
<item>检查本地数据</item>
|
||||
<item>检查远程数据</item>
|
||||
<item>比较本地和远程数据</item>
|
||||
<item>下载远程数据</item>
|
||||
<item>预处理</item>
|
||||
<item>保存同步状态</item>
|
||||
</string-array>
|
||||
<string name="sync_error_unauthorized">用户名或密码错误</string>
|
||||
<string name="sync_error_opentasks_too_old">OpenTasks 版本太旧</string>
|
||||
<string name="sync_error_opentasks_required_version">最低版本 %1$s (当前 %2$s)</string>
|
||||
<!--cert4android-->
|
||||
<string name="certificate_notification_connection_security">DAVdroid: 连接安全性</string>
|
||||
<string name="trust_certificate_unknown_certificate_found">DAVdroid 遇到了未知证书。你是否要信任该证书?</string>
|
||||
|
||||
@@ -8,10 +8,7 @@
|
||||
<string name="manage_accounts">管理帳號</string>
|
||||
<string name="please_wait">請稍待 ...</string>
|
||||
<string name="send">送出</string>
|
||||
<string name="homepage_url">https://davdroid.bitfire.at/?pk_campaign=davdroid-app</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>
|
||||
@@ -20,13 +17,10 @@
|
||||
<string name="startup_donate_later">下次再說</string>
|
||||
<string name="startup_google_play_accounts_removed">Play商店數位權利管理錯誤訊息</string>
|
||||
<string name="startup_google_play_accounts_removed_message">在某些情況下,Play商店 的數位權利管理可能導致在重開機後或更新 DAVdroid 後,DAVdroid 全部帳號消失。如果您遇到此問題 (且只有在遇到此問題時),請到 Play商店 安裝 \"DAVdroid JB Workaround\"。</string>
|
||||
<string name="startup_google_play_accounts_removed_more_info">更多資訊</string>
|
||||
<string name="startup_opentasks_not_installed">OpenTasks 未安裝</string>
|
||||
<string name="startup_opentasks_not_installed_message">OpenTasks 這個 app 不存在您的裝置上,所以 DAVdroid 無法同步工作清單。</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_license_info_no_warranty">我們「完全不保證」本程式無瑕疵。這是個自由軟體,歡迎您在符合公用授權條款的情況下任意散布它。</string>
|
||||
<!--global settings-->
|
||||
<string name="logging_davdroid_file_logging">DAVdroid 正在記錄除錯訊息</string>
|
||||
@@ -43,7 +37,6 @@
|
||||
<string name="navigation_drawer_external_links">外部連結</string>
|
||||
<string name="navigation_drawer_website">我們的網站</string>
|
||||
<string name="navigation_drawer_faq">常見問答</string>
|
||||
<string name="navigation_drawer_faq_url">https://davdroid.bitfire.at/faq/?pk_campaign=davdroid-app</string>
|
||||
<string name="navigation_drawer_donate">贊助我們</string>
|
||||
<string name="account_list_empty">歡迎使用 DAVdroid!\n\n您現在可以新增 CalDAV/CardDAV 帳號</string>
|
||||
<string name="accounts_global_sync_enable">啟用</string>
|
||||
@@ -89,19 +82,7 @@
|
||||
<string name="account_create_new_address_book">建立新的通訊錄</string>
|
||||
<string name="account_refresh_calendar_list">刷新行事曆清單</string>
|
||||
<string name="account_create_new_calendar">建立新的行事曆</string>
|
||||
<!--PermissionsActivity-->
|
||||
<string name="permissions_title">DAVdroid 權限</string>
|
||||
<string name="permissions_calendar">行事曆權限</string>
|
||||
<string name="permissions_calendar_details">為了將 CalDAV 行事曆與您裝置上的行事曆同步,DAVdroid 需要存取行事曆的權限。</string>
|
||||
<string name="permissions_calendar_request">要求行事曆權限</string>
|
||||
<string name="permissions_contacts">通訊錄權限</string>
|
||||
<string name="permissions_contacts_details">為了將 CardDAV 通訊錄與您裝置上的通訊錄同步,DAVdroid 需要存取通訊錄的權限。</string>
|
||||
<string name="permissions_contacts_request">要求通訊錄權限</string>
|
||||
<string name="permissions_opentasks">OpenTasks 權限</string>
|
||||
<string name="permissions_opentasks_details">為了將 CalDAV 工作清單與您裝置上的工作清單同步,DAVdroid 需要存取 OpenTasks 的權限。</string>
|
||||
<string name="permissions_opentasks_request">要求 OpenTasks 權限</string>
|
||||
<!--AddAccountActivity-->
|
||||
<string name="login_help_url">https://davdroid.bitfire.at/configuration/?pk_campaign=davdroid-app</string>
|
||||
<string name="login_title">新增帳號</string>
|
||||
<string name="login_type_email">用 Email 地址登入</string>
|
||||
<string name="login_email_address">Email 地址</string>
|
||||
@@ -125,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>
|
||||
@@ -193,27 +173,6 @@
|
||||
<string name="debug_info_title">除錯訊息</string>
|
||||
<string name="sync_error_permissions">DAVdroid 權限</string>
|
||||
<string name="sync_error_permissions_text">需要額外的權限</string>
|
||||
<string name="sync_error_calendar">行事曆同步失敗 (%s)</string>
|
||||
<string name="sync_error_contacts">通訊錄同步失敗 (%s)</string>
|
||||
<string name="sync_error_tasks">工作清單同步失敗 (%s)</string>
|
||||
<string name="sync_error">在 %s 發生錯誤</string>
|
||||
<string name="sync_error_http_dav">在 %s 發生伺服器錯誤</string>
|
||||
<string name="sync_error_local_storage">在 %s 發生資料庫錯誤</string>
|
||||
<string-array name="sync_error_phases">
|
||||
<item>準備同步</item>
|
||||
<item>詢問能力</item>
|
||||
<item>處理裝置上的項目刪除</item>
|
||||
<item>準備新建 / 修改項目</item>
|
||||
<item>上傳新建 / 修改的項目</item>
|
||||
<item>檢查同步狀態</item>
|
||||
<item>列出裝置上項目</item>
|
||||
<item>列出伺服器上項目</item>
|
||||
<item>比對裝置上 / 伺服器上項目</item>
|
||||
<item>從伺服器下載項目</item>
|
||||
<item>傳輸後處理中</item>
|
||||
<item>儲存同步狀態</item>
|
||||
</string-array>
|
||||
<string name="sync_error_unauthorized">使用者帳號 / 密碼錯誤</string>
|
||||
<!--cert4android-->
|
||||
<string name="certificate_notification_connection_security">DAVdroid: 連線安全性</string>
|
||||
<string name="trust_certificate_unknown_certificate_found">DAVdroid 發現未知的憑證,您要信任它嗎?</string>
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest package="at.bitfire.davdroid"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<uses-permission android:name="com.android.vending.CHECK_LICENSE" />
|
||||
|
||||
</manifest>
|
||||
@@ -42,6 +42,10 @@
|
||||
android:id="@+id/nav_website"
|
||||
android:icon="@drawable/ic_home_dark"
|
||||
android:title="@string/navigation_drawer_website"/>
|
||||
<item
|
||||
android:id="@+id/nav_manual"
|
||||
android:icon="@drawable/ic_info_dark"
|
||||
android:title="@string/navigation_drawer_manual"/>
|
||||
<item
|
||||
android:id="@+id/nav_faq"
|
||||
android:icon="@drawable/ic_help_dark"
|
||||
|
||||
@@ -40,6 +40,10 @@
|
||||
<uses-permission android:name="android.permission.READ_CALENDAR"/>
|
||||
<uses-permission android:name="android.permission.WRITE_CALENDAR"/>
|
||||
|
||||
<!-- android.permission-group.LOCATION -->
|
||||
<!-- required since Android 8.1 to get the WiFi name (for "sync in Wifi only" feature) -->
|
||||
<uses-permission-sdk-23 android:name="android.permission.ACCESS_COARSE_LOCATION"/>
|
||||
|
||||
<!-- ical4android declares task access permissions -->
|
||||
|
||||
<application
|
||||
@@ -49,26 +53,12 @@
|
||||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/AppTheme"
|
||||
android:theme="@style/AppThemeExt"
|
||||
tools:ignore="UnusedAttribute">
|
||||
|
||||
<service android:name=".DavService"/>
|
||||
<service android:name=".settings.Settings"/>
|
||||
|
||||
<receiver android:name=".AccountsChangedReceiver">
|
||||
<intent-filter>
|
||||
<action android:name="android.accounts.LOGIN_ACCOUNTS_CHANGED"/>
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<receiver android:name=".PackageChangedReceiver">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.PACKAGE_ADDED"/>
|
||||
<action android:name="android.intent.action.PACKAGE_FULLY_REMOVED"/>
|
||||
<data android:scheme="package"/>
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<activity
|
||||
android:name=".ui.AccountsActivity"
|
||||
android:label="@string/app_name"
|
||||
@@ -81,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"/>
|
||||
|
||||
@@ -90,10 +80,6 @@
|
||||
android:label="@string/app_settings"
|
||||
android:parentActivityName=".ui.AccountsActivity"/>
|
||||
|
||||
<activity android:name=".ui.PermissionsActivity"
|
||||
android:label="@string/permissions_title"
|
||||
android:parentActivityName=".ui.AccountsActivity"/>
|
||||
|
||||
<activity
|
||||
android:name=".ui.setup.LoginActivity"
|
||||
android:label="@string/login_title"
|
||||
|
||||
@@ -4,6 +4,8 @@ import at.bitfire.davdroid.settings.ISettingsObserver;
|
||||
|
||||
interface ISettings {
|
||||
|
||||
void forceReload();
|
||||
|
||||
boolean has(String key);
|
||||
|
||||
boolean getBoolean(String key, boolean defaultValue);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
<h3>The MIT License (MIT)</h3>
|
||||
|
||||
<p>Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:</p>
|
||||
|
||||
<p>The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.</p>
|
||||
|
||||
<p>THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.</p>
|
||||
@@ -9,10 +9,8 @@ package at.bitfire.davdroid
|
||||
|
||||
import android.accounts.Account
|
||||
import android.accounts.AccountManager
|
||||
import android.content.ContentResolver
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.*
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Parcel
|
||||
@@ -21,9 +19,11 @@ import android.provider.CalendarContract
|
||||
import android.provider.ContactsContract
|
||||
import at.bitfire.davdroid.log.Logger
|
||||
import at.bitfire.davdroid.model.CollectionInfo
|
||||
import at.bitfire.davdroid.model.Credentials
|
||||
import at.bitfire.davdroid.model.ServiceDB
|
||||
import at.bitfire.davdroid.model.ServiceDB.*
|
||||
import at.bitfire.davdroid.model.ServiceDB.Collections
|
||||
import at.bitfire.davdroid.model.SyncState
|
||||
import at.bitfire.davdroid.resource.LocalAddressBook
|
||||
import at.bitfire.davdroid.resource.LocalCalendar
|
||||
import at.bitfire.davdroid.resource.LocalTaskList
|
||||
@@ -31,14 +31,21 @@ import at.bitfire.davdroid.settings.ISettings
|
||||
import at.bitfire.ical4android.AndroidCalendar
|
||||
import at.bitfire.ical4android.AndroidTaskList
|
||||
import at.bitfire.ical4android.CalendarStorageException
|
||||
import at.bitfire.ical4android.TaskProvider
|
||||
import at.bitfire.vcard4android.ContactsStorageException
|
||||
import at.bitfire.vcard4android.GroupMethod
|
||||
import okhttp3.HttpUrl
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import org.dmfs.tasks.contract.TaskContract
|
||||
import java.util.*
|
||||
import java.util.logging.Level
|
||||
|
||||
class AccountSettings @Throws(InvalidAccountException::class) constructor(
|
||||
/**
|
||||
* Manages settings of an account.
|
||||
*
|
||||
* @throws InvalidAccountException on construction when the account doesn't exist (anymore)
|
||||
*/
|
||||
class AccountSettings(
|
||||
val context: Context,
|
||||
val settings: ISettings,
|
||||
val account: Account
|
||||
@@ -46,45 +53,52 @@ class AccountSettings @Throws(InvalidAccountException::class) constructor(
|
||||
|
||||
companion object {
|
||||
|
||||
val CURRENT_VERSION = 7
|
||||
val KEY_SETTINGS_VERSION = "version"
|
||||
const val CURRENT_VERSION = 8
|
||||
const val KEY_SETTINGS_VERSION = "version"
|
||||
|
||||
val KEY_USERNAME = "user_name"
|
||||
const val KEY_USERNAME = "user_name"
|
||||
const val KEY_CERTIFICATE_ALIAS = "certificate_alias"
|
||||
|
||||
val KEY_WIFI_ONLY = "wifi_only" // sync on WiFi only (default: false)
|
||||
val KEY_WIFI_ONLY_SSIDS = "wifi_only_ssids" // restrict sync to specific WiFi SSIDs
|
||||
const val KEY_WIFI_ONLY = "wifi_only" // sync on WiFi only (default: false)
|
||||
const val KEY_WIFI_ONLY_SSIDS = "wifi_only_ssids" // restrict sync to specific WiFi SSIDs
|
||||
|
||||
/** Time range limitation to the past [in days]
|
||||
value = null default value (DEFAULT_TIME_RANGE_PAST_DAYS)
|
||||
< 0 (-1) no limit
|
||||
>= 0 entries more than n days in the past won't be synchronized
|
||||
*/
|
||||
val KEY_TIME_RANGE_PAST_DAYS = "time_range_past_days"
|
||||
val DEFAULT_TIME_RANGE_PAST_DAYS = 90
|
||||
const val KEY_TIME_RANGE_PAST_DAYS = "time_range_past_days"
|
||||
const val DEFAULT_TIME_RANGE_PAST_DAYS = 90
|
||||
|
||||
/* Whether DAVdroid sets the local calendar color to the value from service DB at every sync
|
||||
value = null (not existing) true (default)
|
||||
"0" false */
|
||||
val KEY_MANAGE_CALENDAR_COLORS = "manage_calendar_colors"
|
||||
const val KEY_MANAGE_CALENDAR_COLORS = "manage_calendar_colors"
|
||||
|
||||
/* Whether DAVdroid populates and uses CalendarContract.Colors
|
||||
value = null (not existing) false (default)
|
||||
"1" true */
|
||||
val KEY_EVENT_COLORS = "event_colors"
|
||||
const val KEY_EVENT_COLORS = "event_colors"
|
||||
|
||||
/** Contact group method:
|
||||
value = null (not existing) groups as separate VCards (default)
|
||||
"CATEGORIES" groups are per-contact CATEGORIES
|
||||
*/
|
||||
val KEY_CONTACT_GROUP_METHOD = "contact_group_method"
|
||||
const val KEY_CONTACT_GROUP_METHOD = "contact_group_method"
|
||||
|
||||
@JvmField
|
||||
val SYNC_INTERVAL_MANUALLY = -1L
|
||||
const val SYNC_INTERVAL_MANUALLY = -1L
|
||||
|
||||
fun initialUserData(userName: String): Bundle {
|
||||
fun initialUserData(credentials: Credentials): Bundle {
|
||||
val bundle = Bundle(2)
|
||||
bundle.putString(KEY_SETTINGS_VERSION, CURRENT_VERSION.toString())
|
||||
bundle.putString(KEY_USERNAME, userName)
|
||||
|
||||
when (credentials.type) {
|
||||
Credentials.Type.UsernamePassword ->
|
||||
bundle.putString(KEY_USERNAME, credentials.userName)
|
||||
Credentials.Type.ClientCertificate ->
|
||||
bundle.putString(KEY_CERTIFICATE_ALIAS, credentials.certificateAlias)
|
||||
}
|
||||
|
||||
return bundle
|
||||
}
|
||||
|
||||
@@ -111,11 +125,17 @@ class AccountSettings @Throws(InvalidAccountException::class) constructor(
|
||||
|
||||
// authentication settings
|
||||
|
||||
fun username(): String? = accountManager.getUserData(account, KEY_USERNAME)
|
||||
fun username(userName: String) = accountManager.setUserData(account, KEY_USERNAME, userName)
|
||||
fun credentials() = Credentials(
|
||||
accountManager.getUserData(account, KEY_USERNAME),
|
||||
accountManager.getPassword(account),
|
||||
accountManager.getUserData(account, KEY_CERTIFICATE_ALIAS)
|
||||
)
|
||||
|
||||
fun password(): String? = accountManager.getPassword(account)
|
||||
fun password(password: String) = accountManager.setPassword(account, password)
|
||||
fun credentials(credentials: Credentials) {
|
||||
accountManager.setUserData(account, KEY_USERNAME, credentials.userName)
|
||||
accountManager.setPassword(account, credentials.password)
|
||||
accountManager.setUserData(account, KEY_CERTIFICATE_ALIAS, credentials.certificateAlias)
|
||||
}
|
||||
|
||||
|
||||
// sync. settings
|
||||
@@ -158,11 +178,11 @@ class AccountSettings @Throws(InvalidAccountException::class) constructor(
|
||||
|
||||
fun getTimeRangePastDays(): Int? {
|
||||
val strDays = accountManager.getUserData(account, KEY_TIME_RANGE_PAST_DAYS)
|
||||
if (strDays != null) {
|
||||
return if (strDays != null) {
|
||||
val days = Integer.valueOf(strDays)
|
||||
return if (days < 0) null else days
|
||||
if (days < 0) null else days
|
||||
} else
|
||||
return DEFAULT_TIME_RANGE_PAST_DAYS
|
||||
DEFAULT_TIME_RANGE_PAST_DAYS
|
||||
}
|
||||
|
||||
fun setTimeRangePastDays(days: Int?) =
|
||||
@@ -210,6 +230,8 @@ class AccountSettings @Throws(InvalidAccountException::class) constructor(
|
||||
try {
|
||||
val updateProc = this::class.java.getDeclaredMethod("update_${fromVersion}_$toVersion")
|
||||
updateProc.invoke(this)
|
||||
|
||||
Logger.log.info("Account version update successful")
|
||||
accountManager.setUserData(account, KEY_SETTINGS_VERSION, toVersion.toString())
|
||||
} catch (e: Exception) {
|
||||
Logger.log.log(Level.SEVERE, "Couldn't update account settings", e)
|
||||
@@ -217,6 +239,37 @@ class AccountSettings @Throws(InvalidAccountException::class) constructor(
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
/**
|
||||
* There is a mistake in this method. [TaskContract.Tasks.SYNC_VERSION] is used to store the
|
||||
* SEQUENCE and should not be used for the eTag.
|
||||
*/
|
||||
private fun update_7_8() {
|
||||
TaskProvider.acquire(context, TaskProvider.ProviderName.OpenTasks)?.let { provider ->
|
||||
// ETag is now in sync_version instead of sync1
|
||||
// UID is now in _uid instead of sync2
|
||||
provider.client.query(TaskProvider.syncAdapterUri(provider.tasksUri(), account),
|
||||
arrayOf(TaskContract.Tasks._ID, TaskContract.Tasks.SYNC1, TaskContract.Tasks.SYNC2),
|
||||
"${TaskContract.Tasks.ACCOUNT_TYPE}=? AND ${TaskContract.Tasks.ACCOUNT_NAME}=?",
|
||||
arrayOf(account.type, account.name), null).use { cursor ->
|
||||
while (cursor.moveToNext()) {
|
||||
val id = cursor.getLong(0)
|
||||
val eTag = cursor.getString(1)
|
||||
val uid = cursor.getString(2)
|
||||
val values = ContentValues(4)
|
||||
values.put(TaskContract.Tasks._UID, uid)
|
||||
values.put(TaskContract.Tasks.SYNC_VERSION, eTag)
|
||||
values.putNull(TaskContract.Tasks.SYNC1)
|
||||
values.putNull(TaskContract.Tasks.SYNC2)
|
||||
Logger.log.log(Level.FINER, "Updating task $id", values)
|
||||
provider.client.update(
|
||||
TaskProvider.syncAdapterUri(ContentUris.withAppendedId(provider.tasksUri(), id), account),
|
||||
values, null, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
private fun update_6_7() {
|
||||
// add calendar colors
|
||||
@@ -238,6 +291,7 @@ class AccountSettings @Throws(InvalidAccountException::class) constructor(
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
@SuppressLint("ParcelClassLoader")
|
||||
private fun update_5_6() {
|
||||
context.contentResolver.acquireContentProviderClient(ContactsContract.AUTHORITY)?.let { provider ->
|
||||
val parcel = Parcel.obtain()
|
||||
@@ -255,7 +309,7 @@ class AccountSettings @Throws(InvalidAccountException::class) constructor(
|
||||
parcel.unmarshall(raw, 0, raw.size)
|
||||
parcel.setDataPosition(0)
|
||||
val params = parcel.readBundle()
|
||||
val url = params.getString("url")
|
||||
val url = params.getString("url")?.let { HttpUrl.parse(it) }
|
||||
if (url == null)
|
||||
Logger.log.info("No address book URL, ignoring account")
|
||||
else {
|
||||
@@ -265,7 +319,7 @@ class AccountSettings @Throws(InvalidAccountException::class) constructor(
|
||||
info.displayName = account.name
|
||||
Logger.log.log(Level.INFO, "Creating new address book account", url)
|
||||
val addressBookAccount = Account(LocalAddressBook.accountName(account, info), context.getString(R.string.account_type_address_book))
|
||||
if (!accountManager.addAccountExplicitly(addressBookAccount, null, LocalAddressBook.initialUserData(account, info.url)))
|
||||
if (!accountManager.addAccountExplicitly(addressBookAccount, null, LocalAddressBook.initialUserData(account, info.url.toString())))
|
||||
throw ContactsStorageException("Couldn't create address book account")
|
||||
|
||||
// move contacts to new address book
|
||||
@@ -332,7 +386,7 @@ class AccountSettings @Throws(InvalidAccountException::class) constructor(
|
||||
context.contentResolver.acquireContentProviderClient(ContactsContract.AUTHORITY)?.let { client ->
|
||||
try {
|
||||
val addrBook = LocalAddressBook(context, account, client)
|
||||
val url = addrBook.getURL()
|
||||
val url = addrBook.url
|
||||
Logger.log.fine("Migrating address book $url")
|
||||
|
||||
// insert CardDAV service
|
||||
@@ -391,7 +445,7 @@ class AccountSettings @Throws(InvalidAccountException::class) constructor(
|
||||
}
|
||||
}
|
||||
|
||||
AndroidTaskList.acquireTaskProvider(context.contentResolver)?.use { provider ->
|
||||
AndroidTaskList.acquireTaskProvider(context)?.use { provider ->
|
||||
try {
|
||||
val taskLists = AndroidTaskList.find(account, provider, LocalTaskList.Factory, null, null)
|
||||
for (taskList in taskLists)
|
||||
@@ -445,6 +499,7 @@ class AccountSettings @Throws(InvalidAccountException::class) constructor(
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
@SuppressLint("Recycle")
|
||||
private fun update_1_2() {
|
||||
/* - KEY_ADDRESSBOOK_URL ("addressbook_url"),
|
||||
- KEY_ADDRESSBOOK_CTAG ("addressbook_ctag"),
|
||||
@@ -462,16 +517,16 @@ class AccountSettings @Throws(InvalidAccountException::class) constructor(
|
||||
// until now, ContactsContract.Settings.UNGROUPED_VISIBLE was not set explicitly
|
||||
val values = ContentValues()
|
||||
values.put(ContactsContract.Settings.UNGROUPED_VISIBLE, 1)
|
||||
addr.updateSettings(values)
|
||||
addr.settings = values
|
||||
|
||||
val url = accountManager.getUserData(account, "addressbook_url")
|
||||
if (!url.isNullOrEmpty())
|
||||
addr.setURL(url)
|
||||
addr.url = url
|
||||
accountManager.setUserData(account, "addressbook_url", null)
|
||||
|
||||
val cTag = accountManager.getUserData (account, "addressbook_ctag")
|
||||
if (!cTag.isNullOrEmpty())
|
||||
addr.setCTag(cTag)
|
||||
addr.lastSyncState = SyncState(SyncState.Type.CTAG, cTag)
|
||||
accountManager.setUserData(account, "addressbook_ctag", null)
|
||||
} finally {
|
||||
if (Build.VERSION.SDK_INT >= 24)
|
||||
|
||||
@@ -1,48 +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
|
||||
|
||||
import android.accounts.AccountManager
|
||||
import android.accounts.OnAccountsUpdateListener
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import java.util.*
|
||||
|
||||
class AccountsChangedReceiver: BroadcastReceiver() {
|
||||
|
||||
companion object {
|
||||
|
||||
private val listeners = LinkedList<OnAccountsUpdateListener>()
|
||||
|
||||
@JvmStatic
|
||||
fun registerListener(listener: OnAccountsUpdateListener, callImmediately: Boolean) {
|
||||
listeners += listener
|
||||
if (callImmediately)
|
||||
listener.onAccountsUpdated(null)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun unregisterListener(listener: OnAccountsUpdateListener) {
|
||||
listeners -= listener
|
||||
}
|
||||
}
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
if (intent.action == AccountManager.LOGIN_ACCOUNTS_CHANGED_ACTION) {
|
||||
val serviceIntent = Intent(context, DavService::class.java)
|
||||
serviceIntent.action = DavService.ACTION_ACCOUNTS_UPDATED
|
||||
context.startService(serviceIntent)
|
||||
|
||||
for (listener in listeners)
|
||||
listener.onAccountsUpdated(null)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -10,39 +10,46 @@ package at.bitfire.davdroid
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
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
|
||||
import kotlin.concurrent.thread
|
||||
|
||||
class App: Application() {
|
||||
|
||||
companion object {
|
||||
|
||||
@JvmField val FLAVOR_GOOGLE_PLAY = "gplay"
|
||||
@JvmField val FLAVOR_ICLOUD = "icloud"
|
||||
@JvmField val FLAVOR_MANAGED = "managed"
|
||||
@JvmField val FLAVOR_SOLDUPE = "soldupe"
|
||||
@JvmField val FLAVOR_STANDARD = "standard"
|
||||
const val FLAVOR_GOOGLE_PLAY = "gplay"
|
||||
const val FLAVOR_ICLOUD = "icloud"
|
||||
const val FLAVOR_MANAGED = "managed"
|
||||
const val FLAVOR_SOLDUPE = "soldupe"
|
||||
const val FLAVOR_STANDARD = "standard"
|
||||
|
||||
val ORGANIZATION = "organization"
|
||||
val ORGANIZATION_LOGO_URL = "logo_url"
|
||||
const val ORGANIZATION = "organization"
|
||||
const val ORGANIZATION_LOGO_URL = "logo_url"
|
||||
|
||||
val SUPPORT_HOMEPAGE = "support_homepage_url"
|
||||
val SUPPORT_PHONE = "support_phone_number"
|
||||
val SUPPORT_EMAIL = "support_email_address"
|
||||
const val SUPPORT_HOMEPAGE = "support_homepage_url"
|
||||
const val SUPPORT_PHONE = "support_phone_number"
|
||||
const val SUPPORT_EMAIL = "support_email_address"
|
||||
|
||||
val MAX_ACCOUNTS = "max_accounts"
|
||||
const val MAX_ACCOUNTS = "max_accounts"
|
||||
|
||||
@JvmField val DISTRUST_SYSTEM_CERTIFICATES = "distrust_system_certs"
|
||||
@JvmField val OVERRIDE_PROXY = "override_proxy"
|
||||
@JvmField val OVERRIDE_PROXY_HOST = "override_proxy_host"
|
||||
@JvmField val OVERRIDE_PROXY_PORT = "override_proxy_port"
|
||||
const val DISTRUST_SYSTEM_CERTIFICATES = "distrust_system_certs"
|
||||
const val OVERRIDE_PROXY = "override_proxy"
|
||||
const val OVERRIDE_PROXY_HOST = "override_proxy_host"
|
||||
const val OVERRIDE_PROXY_PORT = "override_proxy_port"
|
||||
|
||||
@JvmField val OVERRIDE_PROXY_HOST_DEFAULT = "localhost"
|
||||
@JvmField val OVERRIDE_PROXY_PORT_DEFAULT = 8118
|
||||
const val OVERRIDE_PROXY_HOST_DEFAULT = "localhost"
|
||||
const val OVERRIDE_PROXY_PORT_DEFAULT = 8118
|
||||
|
||||
|
||||
@JvmStatic
|
||||
fun getLauncherBitmap(context: Context): Bitmap? {
|
||||
val drawableLogo = if (android.os.Build.VERSION.SDK_INT >= 21)
|
||||
context.getDrawable(R.mipmap.ic_launcher)
|
||||
@@ -55,12 +62,54 @@ 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()!!
|
||||
|
||||
}
|
||||
|
||||
|
||||
override fun onCreate() {
|
||||
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)
|
||||
|
||||
NotificationUtils.createChannels(this)
|
||||
|
||||
// don't block UI for some background checks
|
||||
thread {
|
||||
// watch installed/removed apps
|
||||
val tasksFilter = IntentFilter()
|
||||
tasksFilter.addAction(Intent.ACTION_PACKAGE_ADDED)
|
||||
tasksFilter.addAction(Intent.ACTION_PACKAGE_FULLY_REMOVED)
|
||||
tasksFilter.addDataScheme("package")
|
||||
registerReceiver(PackageChangedReceiver(), tasksFilter)
|
||||
|
||||
// check whether a tasks app is currently installed
|
||||
PackageChangedReceiver.updateTaskSync(this)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -5,20 +5,26 @@
|
||||
* which accompanies this distribution, and is available at
|
||||
* http://www.gnu.org/licenses/gpl.html
|
||||
*/
|
||||
package at.bitfire.davdroid;
|
||||
package at.bitfire.davdroid
|
||||
|
||||
object Constants {
|
||||
|
||||
// notification IDs
|
||||
@JvmField val NOTIFICATION_EXTERNAL_FILE_LOGGING = 1
|
||||
@JvmField val NOTIFICATION_REFRESH_COLLECTIONS = 2
|
||||
@JvmField val NOTIFICATION_CONTACTS_SYNC = 10
|
||||
@JvmField val NOTIFICATION_CALENDAR_SYNC = 11
|
||||
@JvmField val NOTIFICATION_TASK_SYNC = 12
|
||||
@JvmField val NOTIFICATION_PERMISSIONS = 20
|
||||
@JvmField val NOTIFICATION_SUBSCRIPTION = 21
|
||||
const val DAVDROID_GREEN_RGBA = 0xFF8bc34a.toInt()
|
||||
|
||||
@JvmField
|
||||
val DEFAULT_SYNC_INTERVAL = 4 * 3600L // 4 hours
|
||||
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"
|
||||
|
||||
}
|
||||
|
||||
@@ -15,12 +15,17 @@ import java.net.InetAddress
|
||||
import java.net.Socket
|
||||
import java.security.GeneralSecurityException
|
||||
import java.util.*
|
||||
import javax.net.ssl.SSLContext
|
||||
import javax.net.ssl.SSLSocket
|
||||
import javax.net.ssl.SSLSocketFactory
|
||||
import javax.net.ssl.X509TrustManager
|
||||
import javax.net.ssl.*
|
||||
|
||||
class SSLSocketFactoryCompat(trustManager: X509TrustManager): SSLSocketFactory() {
|
||||
/**
|
||||
* Custom TLS socket factory with support for
|
||||
* - enabling/disabling algorithms depending on the Android version,
|
||||
* - client certificate authentication
|
||||
*/
|
||||
class CustomTlsSocketFactory(
|
||||
keyManager: KeyManager?,
|
||||
trustManager: X509TrustManager
|
||||
): SSLSocketFactory() {
|
||||
|
||||
private var delegate: SSLSocketFactory
|
||||
|
||||
@@ -40,9 +45,8 @@ class SSLSocketFactoryCompat(trustManager: X509TrustManager): SSLSocketFactory()
|
||||
cipherSuites = null
|
||||
Logger.log.fine("Using device default TLS protocols/ciphers")
|
||||
} else {
|
||||
val socket = SSLSocketFactory.getDefault().createSocket() as SSLSocket?
|
||||
try {
|
||||
socket?.let {
|
||||
(SSLSocketFactory.getDefault().createSocket() as? SSLSocket)?.use { socket ->
|
||||
try {
|
||||
/* set reasonable protocol versions */
|
||||
// - enable all supported protocols (enables TLSv1.1 and TLSv1.2 on Android <5.0)
|
||||
// - remove all SSL versions (especially SSLv3) because they're insecure now
|
||||
@@ -53,7 +57,7 @@ class SSLSocketFactoryCompat(trustManager: X509TrustManager): SSLSocketFactory()
|
||||
protocols = _protocols.toTypedArray()
|
||||
|
||||
/* set up reasonable cipher suites */
|
||||
val knownCiphers = arrayOf<String>(
|
||||
val knownCiphers = arrayOf(
|
||||
// TLS 1.2
|
||||
"TLS_RSA_WITH_AES_256_GCM_SHA384",
|
||||
"TLS_RSA_WITH_AES_128_GCM_SHA256",
|
||||
@@ -92,11 +96,9 @@ class SSLSocketFactoryCompat(trustManager: X509TrustManager): SSLSocketFactory()
|
||||
|
||||
Logger.log.info("Enabling (only) these TLS ciphers: " + _cipherSuites.joinToString(", "))
|
||||
cipherSuites = _cipherSuites.toTypedArray()
|
||||
} catch (e: IOException) {
|
||||
Logger.log.severe("Couldn't determine default TLS settings")
|
||||
}
|
||||
} catch(e: IOException) {
|
||||
Logger.log.severe("Couldn't determine default TLS settings")
|
||||
} finally {
|
||||
socket?.close() // doesn't implement Closeable on all supported Android versions
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -106,7 +108,10 @@ class SSLSocketFactoryCompat(trustManager: X509TrustManager): SSLSocketFactory()
|
||||
init {
|
||||
try {
|
||||
val sslContext = SSLContext.getInstance("TLS")
|
||||
sslContext.init(null, arrayOf(trustManager), null)
|
||||
sslContext.init(
|
||||
if (keyManager != null) arrayOf(keyManager) else null,
|
||||
arrayOf(trustManager),
|
||||
null)
|
||||
delegate = sslContext.socketFactory
|
||||
} catch (e: GeneralSecurityException) {
|
||||
throw IllegalStateException() // system has no TLS
|
||||
@@ -9,18 +9,19 @@
|
||||
package at.bitfire.davdroid
|
||||
|
||||
import android.accounts.Account
|
||||
import android.accounts.AccountManager
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.PendingIntent
|
||||
import android.app.Service
|
||||
import android.content.ContentResolver
|
||||
import android.content.ContentValues
|
||||
import android.content.Intent
|
||||
import android.database.DatabaseUtils
|
||||
import android.database.sqlite.SQLiteDatabase
|
||||
import android.os.Binder
|
||||
import android.os.Bundle
|
||||
import android.support.v4.app.NotificationCompat
|
||||
import android.support.v4.app.NotificationManagerCompat
|
||||
import android.support.v7.app.NotificationCompat
|
||||
import at.bitfire.dav4android.DavResource
|
||||
import at.bitfire.dav4android.Response
|
||||
import at.bitfire.dav4android.UrlUtils
|
||||
import at.bitfire.dav4android.exception.DavException
|
||||
import at.bitfire.dav4android.exception.HttpException
|
||||
@@ -29,12 +30,11 @@ import at.bitfire.davdroid.log.Logger
|
||||
import at.bitfire.davdroid.model.CollectionInfo
|
||||
import at.bitfire.davdroid.model.ServiceDB.*
|
||||
import at.bitfire.davdroid.model.ServiceDB.Collections
|
||||
import at.bitfire.davdroid.resource.LocalAddressBook
|
||||
import at.bitfire.davdroid.settings.Settings
|
||||
import at.bitfire.davdroid.ui.DebugInfoActivity
|
||||
import at.bitfire.vcard4android.ContactsStorageException
|
||||
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.*
|
||||
@@ -44,9 +44,14 @@ import kotlin.concurrent.thread
|
||||
class DavService: Service() {
|
||||
|
||||
companion object {
|
||||
@JvmField val ACTION_ACCOUNTS_UPDATED = "accountsUpdated"
|
||||
@JvmField val ACTION_REFRESH_COLLECTIONS = "refreshCollections"
|
||||
@JvmField val EXTRA_DAV_SERVICE_ID = "davServiceID"
|
||||
const val ACTION_REFRESH_COLLECTIONS = "refreshCollections"
|
||||
const val EXTRA_DAV_SERVICE_ID = "davServiceID"
|
||||
|
||||
/** Initialize a forced synchronization. Expects intent data
|
||||
to be an URI of this format:
|
||||
contents://<authority>/<account.type>/<account name>
|
||||
**/
|
||||
const val ACTION_FORCE_SYNC = "forceSync"
|
||||
}
|
||||
|
||||
private val runningRefresh = HashSet<Long>()
|
||||
@@ -58,13 +63,20 @@ class DavService: Service() {
|
||||
val id = intent.getLongExtra(EXTRA_DAV_SERVICE_ID, -1)
|
||||
|
||||
when (intent.action) {
|
||||
ACTION_ACCOUNTS_UPDATED ->
|
||||
cleanupAccounts()
|
||||
ACTION_REFRESH_COLLECTIONS ->
|
||||
if (runningRefresh.add(id)) {
|
||||
thread { refreshCollections(id) }
|
||||
refreshingStatusListeners.forEach { it.get()?.onDavRefreshStatusChanged(id, true) }
|
||||
}
|
||||
|
||||
ACTION_FORCE_SYNC -> {
|
||||
val authority = intent.data.authority
|
||||
val account = Account(
|
||||
intent.data.pathSegments[1],
|
||||
intent.data.pathSegments[0]
|
||||
)
|
||||
forceSync(authority, account)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,42 +121,15 @@ class DavService: Service() {
|
||||
which actually do the work
|
||||
*/
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
fun cleanupAccounts() {
|
||||
Logger.log.info("Cleaning up orphaned accounts")
|
||||
|
||||
OpenHelper(this).use { dbHelper ->
|
||||
val db = dbHelper.writableDatabase
|
||||
|
||||
val sqlAccountNames = LinkedList<String>()
|
||||
val accountNames = HashSet<String>()
|
||||
val accountManager = AccountManager.get(this)
|
||||
for (account in accountManager.getAccountsByType(getString(R.string.account_type))) {
|
||||
sqlAccountNames.add(DatabaseUtils.sqlEscapeString(account.name))
|
||||
accountNames += account.name
|
||||
}
|
||||
|
||||
// delete orphaned address book accounts
|
||||
accountManager.getAccountsByType(getString(R.string.account_type_address_book))
|
||||
.map { LocalAddressBook(this, it, null) }
|
||||
.forEach {
|
||||
try {
|
||||
if (!accountNames.contains(it.getMainAccount().name))
|
||||
it.delete()
|
||||
} catch(e: ContactsStorageException) {
|
||||
Logger.log.log(Level.SEVERE, "Couldn't get address book main account", e)
|
||||
}
|
||||
}
|
||||
|
||||
// delete orphaned services in DB
|
||||
if (sqlAccountNames.isEmpty())
|
||||
db.delete(Services._TABLE, null, null)
|
||||
else
|
||||
db.delete(Services._TABLE, "${Services.ACCOUNT_NAME} NOT IN (${sqlAccountNames.joinToString(",")})", null)
|
||||
}
|
||||
private fun forceSync(authority: String, account: Account) {
|
||||
Logger.log.info("Forcing $authority synchronization of $account")
|
||||
val extras = Bundle(2)
|
||||
extras.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true) // manual sync
|
||||
extras.putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, true) // run immediately (don't queue)
|
||||
ContentResolver.requestSync(account, authority, extras)
|
||||
}
|
||||
|
||||
fun refreshCollections(service: Long) {
|
||||
private fun refreshCollections(service: Long) {
|
||||
OpenHelper(this@DavService).use { dbHelper ->
|
||||
val db = dbHelper.writableDatabase
|
||||
|
||||
@@ -196,24 +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) {
|
||||
when (serviceType) {
|
||||
Services.SERVICE_CARDDAV -> {
|
||||
dav.propfind(0, AddressbookHomeSet.NAME, GroupMembership.NAME)
|
||||
for ((resource, addressbookHomeSet) in dav.findProperties(AddressbookHomeSet.NAME) as List<Pair<DavResource, AddressbookHomeSet>>)
|
||||
for (href in addressbookHomeSet.hrefs)
|
||||
resource.location.resolve(href)?.let { homeSets += UrlUtils.withTrailingSlash(it) }
|
||||
fun queryHomeSets(client: OkHttpClient, url: HttpUrl, recurse: Boolean = true) {
|
||||
var related = setOf<HttpUrl>()
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
Services.SERVICE_CALDAV -> {
|
||||
dav.propfind(0, CalendarHomeSet.NAME, CalendarProxyReadFor.NAME, CalendarProxyWriteFor.NAME, GroupMembership.NAME)
|
||||
for ((resource, calendarHomeSet) in dav.findProperties(CalendarHomeSet.NAME) as List<Pair<DavResource, CalendarHomeSet>>)
|
||||
for (href in calendarHomeSet.hrefs)
|
||||
resource.location.resolve(href)?.let { homeSets.add(UrlUtils.withTrailingSlash(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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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() {
|
||||
@@ -240,108 +269,84 @@ class DavService: Service() {
|
||||
try {
|
||||
Logger.log.info("Refreshing $serviceType collections of service #$service")
|
||||
|
||||
// create authenticating OkHttpClient (credentials taken from account settings)
|
||||
HttpClient.Builder(this@DavService, account)
|
||||
.setForeground(true)
|
||||
.build().use { client ->
|
||||
val httpClient = client.okHttpClient
|
||||
Settings.getInstance(this)?.use { settings ->
|
||||
// create authenticating OkHttpClient (credentials taken from account settings)
|
||||
HttpClient.Builder(this, settings, AccountSettings(this, settings, account))
|
||||
.setForeground(true)
|
||||
.build().use { client ->
|
||||
val httpClient = client.okHttpClient
|
||||
|
||||
// refresh home set list (from principal)
|
||||
readPrincipal()?.let { principalUrl ->
|
||||
Logger.log.fine("Querying principal $principalUrl for home sets")
|
||||
val principal = DavResource(httpClient, principalUrl)
|
||||
queryHomeSets(principal)
|
||||
|
||||
// refresh home sets: calendar-proxy-read/write-for
|
||||
for ((resource, proxyRead) in principal.findProperties(CalendarProxyReadFor.NAME) as List<Pair<DavResource, CalendarProxyReadFor>>)
|
||||
for (href in proxyRead.hrefs) {
|
||||
Logger.log.fine("Principal is a read-only proxy for $href, checking for home sets")
|
||||
resource.location.resolve(href)?.let { queryHomeSets(DavResource(httpClient, it)) }
|
||||
}
|
||||
for ((resource, proxyWrite) in principal.findProperties(CalendarProxyWriteFor.NAME) as List<Pair<DavResource, CalendarProxyWriteFor>>)
|
||||
for (href in proxyWrite.hrefs) {
|
||||
Logger.log.fine("Principal is a read/write proxy for $href, checking for home sets")
|
||||
resource.location.resolve(href)?.let { queryHomeSets(DavResource(httpClient, it)) }
|
||||
}
|
||||
|
||||
// refresh home sets: direct group memberships
|
||||
(principal.properties[GroupMembership.NAME] as GroupMembership?)?.let { groupMembership ->
|
||||
for (href in groupMembership.hrefs) {
|
||||
Logger.log.fine("Principal is member of group $href, checking for home sets")
|
||||
principal.location.resolve(href)?.let { url ->
|
||||
try {
|
||||
queryHomeSets(DavResource(httpClient, url))
|
||||
} 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
// refresh home set list (from principal)
|
||||
readPrincipal()?.let { principalUrl ->
|
||||
Logger.log.fine("Querying principal $principalUrl for home sets")
|
||||
queryHomeSets(httpClient, principalUrl)
|
||||
}
|
||||
}
|
||||
|
||||
// remember selected collections
|
||||
val selectedCollections = HashSet<HttpUrl>()
|
||||
collections.values
|
||||
.filter { it.selected }
|
||||
.forEach { (url,_) -> HttpUrl.parse(url)?.let { selectedCollections.add(it) } }
|
||||
// remember selected collections
|
||||
val selectedCollections = HashSet<HttpUrl>()
|
||||
collections.values
|
||||
.filter { it.selected }
|
||||
.forEach { (url, _) -> selectedCollections.add(url) }
|
||||
|
||||
// now refresh collections (taken from home sets)
|
||||
val itHomeSets = homeSets.iterator()
|
||||
while (itHomeSets.hasNext()) {
|
||||
val homeSetUrl = itHomeSets.next()
|
||||
Logger.log.fine("Listing home set $homeSetUrl")
|
||||
// now refresh collections (taken from home sets)
|
||||
val itHomeSets = homeSets.iterator()
|
||||
while (itHomeSets.hasNext()) {
|
||||
val homeSetUrl = itHomeSets.next()
|
||||
Logger.log.fine("Listing home set $homeSetUrl")
|
||||
|
||||
val homeSet = DavResource(httpClient, homeSetUrl)
|
||||
try {
|
||||
homeSet.propfind(1, *CollectionInfo.DAV_PROPERTIES)
|
||||
val itCollections = IteratorChain<DavResource>(homeSet.members.iterator(), SingletonIterator(homeSet))
|
||||
while (itCollections.hasNext()) {
|
||||
val member = itCollections.next()
|
||||
val info = CollectionInfo(member)
|
||||
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[member.location] = info
|
||||
}
|
||||
} catch(e: HttpException) {
|
||||
if (e.status in arrayOf(403, 404, 410))
|
||||
// delete home set only if it was not accessible (40x)
|
||||
itHomeSets.remove()
|
||||
}
|
||||
}
|
||||
|
||||
// check/refresh unconfirmed collections
|
||||
val itCollections = collections.entries.iterator()
|
||||
while (itCollections.hasNext()) {
|
||||
val (url, info) = itCollections.next()
|
||||
if (!info.confirmed)
|
||||
try {
|
||||
val collection = DavResource(httpClient, url)
|
||||
collection.propfind(0, *CollectionInfo.DAV_PROPERTIES)
|
||||
val info = CollectionInfo(collection)
|
||||
info.confirmed = true
|
||||
DavResource(httpClient, homeSetUrl).propfind(1, *CollectionInfo.DAV_PROPERTIES) { response, _ ->
|
||||
if (!response.isSuccess())
|
||||
return@propfind
|
||||
|
||||
// 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))
|
||||
itCollections.remove()
|
||||
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.status in arrayOf(403, 404, 410))
|
||||
// delete collection only if it was not accessible (40x)
|
||||
itCollections.remove()
|
||||
else
|
||||
throw e
|
||||
if (e.code in arrayOf(403, 404, 410))
|
||||
// delete home set only if it was not accessible (40x)
|
||||
itHomeSets.remove()
|
||||
}
|
||||
}
|
||||
|
||||
// check/refresh unconfirmed collections
|
||||
val itCollections = collections.entries.iterator()
|
||||
while (itCollections.hasNext()) {
|
||||
val (url, info) = itCollections.next()
|
||||
if (!info.confirmed)
|
||||
try {
|
||||
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))
|
||||
itCollections.remove()
|
||||
}
|
||||
} catch(e: HttpException) {
|
||||
if (e.code in arrayOf(403, 404, 410))
|
||||
// delete collection only if it was not accessible (40x)
|
||||
itCollections.remove()
|
||||
else
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
// restore selections
|
||||
for (url in selectedCollections)
|
||||
collections[url]?.let { it.selected = true }
|
||||
}
|
||||
|
||||
// restore selections
|
||||
for (url in selectedCollections)
|
||||
collections[url]?.let { it.selected = true }
|
||||
}
|
||||
|
||||
db.beginTransactionNonExclusive()
|
||||
@@ -358,19 +363,20 @@ class DavService: Service() {
|
||||
} catch(e: Exception) {
|
||||
Logger.log.log(Level.SEVERE, "Couldn't refresh collection list", e)
|
||||
|
||||
val debugIntent = Intent(this@DavService, DebugInfoActivity::class.java)
|
||||
val debugIntent = Intent(this, DebugInfoActivity::class.java)
|
||||
debugIntent.putExtra(DebugInfoActivity.KEY_THROWABLE, e)
|
||||
debugIntent.putExtra(DebugInfoActivity.KEY_ACCOUNT, account)
|
||||
|
||||
val nm = NotificationManagerCompat.from(this@DavService)
|
||||
val notify = NotificationCompat.Builder(this@DavService)
|
||||
.setSmallIcon(R.drawable.ic_error_light)
|
||||
.setLargeIcon(App.getLauncherBitmap(this@DavService))
|
||||
val notify = NotificationUtils.newBuilder(this)
|
||||
.setSmallIcon(R.drawable.ic_sync_error_notification)
|
||||
.setContentTitle(getString(R.string.dav_service_refresh_failed))
|
||||
.setContentText(getString(R.string.dav_service_refresh_couldnt_refresh))
|
||||
.setContentIntent(PendingIntent.getActivity(this@DavService, 0, debugIntent, PendingIntent.FLAG_UPDATE_CURRENT))
|
||||
.setContentIntent(PendingIntent.getActivity(this, 0, debugIntent, PendingIntent.FLAG_UPDATE_CURRENT))
|
||||
.setSubText(account.name)
|
||||
.setCategory(NotificationCompat.CATEGORY_ERROR)
|
||||
.build()
|
||||
nm.notify(Constants.NOTIFICATION_REFRESH_COLLECTIONS, notify)
|
||||
NotificationManagerCompat.from(this)
|
||||
.notify(service.toString(), NotificationUtils.NOTIFY_REFRESH_COLLECTIONS, notify)
|
||||
} finally {
|
||||
runningRefresh.remove(service)
|
||||
refreshingStatusListeners.forEach { it.get()?.onDavRefreshStatusChanged(service, false) }
|
||||
@@ -379,4 +385,4 @@ class DavService: Service() {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -6,33 +6,77 @@
|
||||
* http://www.gnu.org/licenses/gpl.html
|
||||
*/
|
||||
|
||||
package at.bitfire.davdroid;
|
||||
package at.bitfire.davdroid
|
||||
|
||||
import android.annotation.TargetApi
|
||||
import android.content.Context
|
||||
import android.net.ConnectivityManager
|
||||
import android.os.Build
|
||||
import at.bitfire.davdroid.log.Logger
|
||||
import okhttp3.HttpUrl
|
||||
import org.xbill.DNS.*
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Some WebDAV and related network utility methods
|
||||
*/
|
||||
object DavUtils {
|
||||
|
||||
@JvmStatic
|
||||
fun ARGBtoCalDAVColor(colorWithAlpha: Int): String {
|
||||
val alpha = (colorWithAlpha shr 24) and 0xFF
|
||||
val color = colorWithAlpha and 0xFFFFFF
|
||||
return String.format("#%06X%02X", color, alpha)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun lastSegmentOfUrl(url: String): String {
|
||||
val httpUrl = HttpUrl.parse(url) ?: throw IllegalArgumentException("url not parsable")
|
||||
|
||||
fun lastSegmentOfUrl(url: HttpUrl): String {
|
||||
// the list returned by HttpUrl.pathSegments() is unmodifiable, so we have to create a copy
|
||||
val segments = LinkedList<String>(httpUrl.pathSegments())
|
||||
Collections.reverse(segments)
|
||||
val segments = LinkedList<String>(url.pathSegments())
|
||||
segments.reverse()
|
||||
|
||||
for (segment in segments)
|
||||
if (segment.isNotEmpty())
|
||||
return segment
|
||||
return segments.firstOrNull { it.isNotEmpty() } ?: "/"
|
||||
}
|
||||
|
||||
return "/"
|
||||
fun prepareLookup(context: Context, lookup: Lookup) {
|
||||
@TargetApi(Build.VERSION_CODES.O)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
/* Since Android 8, the system properties net.dns1, net.dns2, ... are not available anymore.
|
||||
The current version of dnsjava relies on these properties to find the default name servers,
|
||||
so we have to add the servers explicitly (fortunately, there's an Android API to
|
||||
get the active DNS servers). */
|
||||
val connectivity = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
||||
val activeLink = connectivity.getLinkProperties(connectivity.activeNetwork)
|
||||
val simpleResolvers = activeLink.dnsServers.map {
|
||||
Logger.log.fine("Using DNS server ${it.hostAddress}")
|
||||
val resolver = SimpleResolver()
|
||||
resolver.setAddress(it)
|
||||
resolver
|
||||
}
|
||||
val resolver = ExtendedResolver(simpleResolvers.toTypedArray())
|
||||
lookup.setResolver(resolver)
|
||||
}
|
||||
}
|
||||
|
||||
fun selectSRVRecord(records: Array<Record>?): SRVRecord? {
|
||||
val srvRecords = records?.filterIsInstance(SRVRecord::class.java)
|
||||
srvRecords?.let {
|
||||
if (it.size > 1)
|
||||
Logger.log.warning("Multiple SRV records not supported yet; using first one")
|
||||
return it.firstOrNull()
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun pathsFromTXTRecords(records: Array<Record>?): List<String> {
|
||||
val paths = LinkedList<String>()
|
||||
records?.filterIsInstance(TXTRecord::class.java)?.forEach { txt ->
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
for (segment in txt.strings as List<String>)
|
||||
if (segment.startsWith("path=")) {
|
||||
paths.add(segment.substring(5))
|
||||
break
|
||||
}
|
||||
}
|
||||
return paths
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -6,62 +6,77 @@
|
||||
* http://www.gnu.org/licenses/gpl.html
|
||||
*/
|
||||
|
||||
package at.bitfire.davdroid;
|
||||
package at.bitfire.davdroid
|
||||
|
||||
import android.accounts.Account
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.security.KeyChain
|
||||
import at.bitfire.cert4android.CustomCertManager
|
||||
import at.bitfire.dav4android.BasicDigestAuthHandler
|
||||
import at.bitfire.dav4android.Constants
|
||||
import at.bitfire.dav4android.UrlUtils
|
||||
import at.bitfire.davdroid.log.Logger
|
||||
import at.bitfire.davdroid.settings.Settings
|
||||
import at.bitfire.davdroid.model.Credentials
|
||||
import at.bitfire.davdroid.settings.ISettings
|
||||
import okhttp3.Cache
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Response
|
||||
import okhttp3.internal.tls.OkHostnameVerifier
|
||||
import okhttp3.logging.HttpLoggingInterceptor
|
||||
import java.io.Closeable
|
||||
import java.io.File
|
||||
import java.net.InetSocketAddress
|
||||
import java.net.Proxy
|
||||
import java.net.Socket
|
||||
import java.security.KeyStore
|
||||
import java.security.Principal
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.logging.Level
|
||||
import javax.net.ssl.KeyManager
|
||||
import javax.net.ssl.TrustManagerFactory
|
||||
import javax.net.ssl.X509ExtendedKeyManager
|
||||
import javax.net.ssl.X509TrustManager
|
||||
|
||||
class HttpClient private constructor(
|
||||
val okHttpClient: OkHttpClient,
|
||||
private val certManager: CustomCertManager?
|
||||
): Closeable {
|
||||
): AutoCloseable {
|
||||
|
||||
companion object {
|
||||
/** [OkHttpClient] singleton to build all clients from */
|
||||
val sharedClient = OkHttpClient.Builder()
|
||||
// set timeouts
|
||||
.connectTimeout(15, TimeUnit.SECONDS)
|
||||
.writeTimeout(30, TimeUnit.SECONDS)
|
||||
.readTimeout(120, TimeUnit.SECONDS)
|
||||
|
||||
// don't allow redirects by default, because it would break PROPFIND handling
|
||||
.followRedirects(false)
|
||||
|
||||
// add User-Agent to every request
|
||||
.addNetworkInterceptor(UserAgentInterceptor)
|
||||
|
||||
.build()!!
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
certManager?.close()
|
||||
}
|
||||
|
||||
|
||||
class Builder @JvmOverloads constructor(
|
||||
class Builder(
|
||||
val context: Context? = null,
|
||||
account: Account? = null,
|
||||
val settings: ISettings? = null,
|
||||
accountSettings: AccountSettings? = null,
|
||||
logger: java.util.logging.Logger = Logger.log
|
||||
val logger: java.util.logging.Logger = Logger.log
|
||||
) {
|
||||
var certManager: CustomCertManager? = null
|
||||
private val orig = OkHttpClient.Builder()
|
||||
private var certManager: CustomCertManager? = null
|
||||
private var certificateAlias: String? = null
|
||||
|
||||
private val orig = sharedClient.newBuilder()
|
||||
|
||||
init {
|
||||
// set timeouts
|
||||
orig.connectTimeout(30, TimeUnit.SECONDS)
|
||||
orig.writeTimeout(30, TimeUnit.SECONDS)
|
||||
orig.readTimeout(120, TimeUnit.SECONDS)
|
||||
|
||||
// don't allow redirects by default, because it would break PROPFIND handling
|
||||
orig.followRedirects(false)
|
||||
|
||||
// add User-Agent to every request
|
||||
orig.addNetworkInterceptor(UserAgentInterceptor)
|
||||
|
||||
// add cookie store for non-persistent cookies (some services like Horde use cookies for session tracking)
|
||||
orig.cookieJar(MemoryCookieStore())
|
||||
|
||||
@@ -74,41 +89,37 @@ class HttpClient private constructor(
|
||||
orig.addInterceptor(loggingInterceptor)
|
||||
}
|
||||
|
||||
context?.let {
|
||||
Settings.getInstance(context)?.use { settings ->
|
||||
// custom proxy support
|
||||
try {
|
||||
if (settings.getBoolean(App.OVERRIDE_PROXY, false)) {
|
||||
val address = InetSocketAddress(
|
||||
settings.getString(App.OVERRIDE_PROXY_HOST, App.OVERRIDE_PROXY_HOST_DEFAULT),
|
||||
settings.getInt(App.OVERRIDE_PROXY_PORT, App.OVERRIDE_PROXY_PORT_DEFAULT)
|
||||
)
|
||||
settings?.let {
|
||||
// custom proxy support
|
||||
try {
|
||||
if (settings.getBoolean(App.OVERRIDE_PROXY, false)) {
|
||||
val address = InetSocketAddress(
|
||||
settings.getString(App.OVERRIDE_PROXY_HOST, App.OVERRIDE_PROXY_HOST_DEFAULT),
|
||||
settings.getInt(App.OVERRIDE_PROXY_PORT, App.OVERRIDE_PROXY_PORT_DEFAULT)
|
||||
)
|
||||
|
||||
val proxy = Proxy(Proxy.Type.HTTP, address)
|
||||
orig.proxy(proxy)
|
||||
Logger.log.log(Level.INFO, "Using proxy", proxy)
|
||||
}
|
||||
} catch(e: Exception) {
|
||||
Logger.log.log(Level.SEVERE, "Can't set proxy, ignoring", e)
|
||||
val proxy = Proxy(Proxy.Type.HTTP, address)
|
||||
orig.proxy(proxy)
|
||||
Logger.log.log(Level.INFO, "Using proxy", proxy)
|
||||
}
|
||||
} catch(e: Exception) {
|
||||
Logger.log.log(Level.SEVERE, "Can't set proxy, ignoring", e)
|
||||
}
|
||||
|
||||
context?.let {
|
||||
if (BuildConfig.customCerts)
|
||||
customCertManager(CustomCertManager(context, !settings.getBoolean(App.DISTRUST_SYSTEM_CERTIFICATES, false)))
|
||||
customCertManager(CustomCertManager(context, BuildConfig.customCertsUI, !settings.getBoolean(App.DISTRUST_SYSTEM_CERTIFICATES, false)))
|
||||
|
||||
// use account settings for authentication
|
||||
val accountSettings = accountSettings ?: account?.let { AccountSettings(context, settings, it) }
|
||||
accountSettings?.let {
|
||||
val userName = accountSettings.username()
|
||||
val password = accountSettings.password()
|
||||
if (userName != null && password != null)
|
||||
addAuthentication(null, userName, password)
|
||||
addAuthentication(null, it.credentials())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
constructor(context: Context, host: String?, username: String, password: String): this(context) {
|
||||
addAuthentication(host, username, password)
|
||||
constructor(context: Context, host: String?, credentials: Credentials): this(context) {
|
||||
addAuthentication(host, credentials)
|
||||
}
|
||||
|
||||
fun withDiskCache(): Builder {
|
||||
@@ -132,22 +143,77 @@ class HttpClient private constructor(
|
||||
|
||||
fun customCertManager(manager: CustomCertManager) {
|
||||
certManager = manager
|
||||
orig.sslSocketFactory(SSLSocketFactoryCompat(manager), manager)
|
||||
orig.hostnameVerifier(manager.hostnameVerifier(OkHostnameVerifier.INSTANCE))
|
||||
}
|
||||
|
||||
fun setForeground(foreground: Boolean): Builder {
|
||||
certManager?.appInForeground = foreground
|
||||
return this
|
||||
}
|
||||
|
||||
fun addAuthentication(host: String?, username: String, password: String): Builder {
|
||||
val authHandler = BasicDigestAuthHandler(UrlUtils.hostToDomain(host), username, password)
|
||||
orig .addNetworkInterceptor(authHandler)
|
||||
.authenticator(authHandler)
|
||||
fun addAuthentication(host: String?, credentials: Credentials): Builder {
|
||||
when (credentials.type) {
|
||||
Credentials.Type.UsernamePassword -> {
|
||||
val authHandler = BasicDigestAuthHandler(UrlUtils.hostToDomain(host), credentials.userName!!, credentials.password!!)
|
||||
orig .addNetworkInterceptor(authHandler)
|
||||
.authenticator(authHandler)
|
||||
}
|
||||
Credentials.Type.ClientCertificate -> {
|
||||
certificateAlias = credentials.certificateAlias
|
||||
}
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
fun build() = HttpClient(orig.build(), certManager)
|
||||
fun build(): HttpClient {
|
||||
val trustManager = certManager ?: {
|
||||
val factory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
|
||||
factory.init(null as KeyStore?)
|
||||
factory.trustManagers.first() as X509TrustManager
|
||||
}()
|
||||
|
||||
val hostnameVerifier = certManager?.hostnameVerifier(OkHostnameVerifier.INSTANCE)
|
||||
?: OkHostnameVerifier.INSTANCE
|
||||
|
||||
var keyManager: KeyManager? = null
|
||||
try {
|
||||
certificateAlias?.let { alias ->
|
||||
// get client certificate and private key
|
||||
val certs = KeyChain.getCertificateChain(context, alias) ?: return@let
|
||||
val key = KeyChain.getPrivateKey(context, alias) ?: return@let
|
||||
logger.fine("Using client certificate $alias for authentication (chain length: ${certs.size})")
|
||||
|
||||
// create Android KeyStore (performs key operations without revealing secret data to DAVdroid)
|
||||
val keyStore = KeyStore.getInstance("AndroidKeyStore")
|
||||
keyStore.load(null)
|
||||
|
||||
// create KeyManager
|
||||
keyManager = object: X509ExtendedKeyManager() {
|
||||
override fun getServerAliases(p0: String?, p1: Array<out Principal>?): Array<String>? = null
|
||||
override fun chooseServerAlias(p0: String?, p1: Array<out Principal>?, p2: Socket?) = null
|
||||
|
||||
override fun getClientAliases(p0: String?, p1: Array<out Principal>?) =
|
||||
arrayOf(alias)
|
||||
|
||||
override fun chooseClientAlias(p0: Array<out String>?, p1: Array<out Principal>?, p2: Socket?) =
|
||||
alias
|
||||
|
||||
override fun getCertificateChain(forAlias: String?) =
|
||||
certs.takeIf { forAlias == alias }
|
||||
|
||||
override fun getPrivateKey(forAlias: String?) =
|
||||
key.takeIf { forAlias == alias }
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logger.log(Level.SEVERE, "Couldn't set up client certificate authentication", e)
|
||||
}
|
||||
|
||||
orig.sslSocketFactory(CustomTlsSocketFactory(keyManager, trustManager), trustManager)
|
||||
orig.hostnameVerifier(hostnameVerifier)
|
||||
|
||||
return HttpClient(orig.build(), certManager)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -158,8 +224,11 @@ class HttpClient private constructor(
|
||||
App.FLAVOR_SOLDUPE -> "Soldupe Sync"
|
||||
else -> "DAVdroid"
|
||||
}
|
||||
private val userAgentDate = SimpleDateFormat("yyyy/MM/dd", Locale.US).format(Date(BuildConfig.buildTime))
|
||||
private val userAgent = "$productName/${BuildConfig.VERSION_NAME} ($userAgentDate; dav4android; okhttp3) Android/${Build.VERSION.RELEASE}"
|
||||
|
||||
// use Locale.US because numbers may be encoded as non-ASCII characters in other locales
|
||||
private val userAgentDateFormat = SimpleDateFormat("yyyy/MM/dd", Locale.US)
|
||||
private val userAgentDate = userAgentDateFormat.format(Date(BuildConfig.buildTime))
|
||||
private val userAgent = "$productName/${BuildConfig.VERSION_NAME} ($userAgentDate; dav4android; okhttp/${Constants.okHttpVersion}) Android/${Build.VERSION.RELEASE}"
|
||||
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
val locale = Locale.getDefault()
|
||||
|
||||
@@ -27,7 +27,7 @@ class MemoryCookieStore: CookieJar {
|
||||
* This ensures that cookies can be overwritten. [RFC 6265 5.3 Storage Model]
|
||||
* Not thread-safe!
|
||||
*/
|
||||
val storage = MultiKeyMap.multiKeyMap(HashedMap<MultiKey<out String>, Cookie>())!!
|
||||
private val storage = MultiKeyMap.multiKeyMap(HashedMap<MultiKey<out String>, Cookie>())!!
|
||||
|
||||
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) {
|
||||
synchronized(storage) {
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
package at.bitfire.davdroid
|
||||
|
||||
import android.accounts.Account
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
@@ -23,19 +22,11 @@ import at.bitfire.ical4android.TaskProvider
|
||||
|
||||
class PackageChangedReceiver: BroadcastReceiver() {
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
if (intent.action == Intent.ACTION_PACKAGE_ADDED || intent.action == Intent.ACTION_PACKAGE_FULLY_REMOVED)
|
||||
updateTaskSync(context)
|
||||
}
|
||||
|
||||
|
||||
companion object {
|
||||
|
||||
@JvmStatic
|
||||
fun updateTaskSync(context: Context) {
|
||||
val tasksInstalled = LocalTaskList.tasksProviderAvailable(context)
|
||||
Logger.log.info("Package (un)installed; OpenTasks provider now available = $tasksInstalled")
|
||||
Logger.log.info("Tasks provider available = $tasksInstalled")
|
||||
|
||||
// check all accounts and (de)activate OpenTasks if a CalDAV service is defined
|
||||
ServiceDB.OpenHelper(context).use { dbHelper ->
|
||||
@@ -50,7 +41,7 @@ class PackageChangedReceiver: BroadcastReceiver() {
|
||||
if (ContentResolver.getIsSyncable(account, TaskProvider.ProviderName.OpenTasks.authority) <= 0) {
|
||||
ContentResolver.setIsSyncable(account, TaskProvider.ProviderName.OpenTasks.authority, 1)
|
||||
ContentResolver.setSyncAutomatically(account, TaskProvider.ProviderName.OpenTasks.authority, true)
|
||||
ContentResolver.addPeriodicSync(account, TaskProvider.ProviderName.OpenTasks.authority, Bundle(), Constants.DEFAULT_SYNC_INTERVAL.toLong())
|
||||
ContentResolver.addPeriodicSync(account, TaskProvider.ProviderName.OpenTasks.authority, Bundle(), Constants.DEFAULT_SYNC_INTERVAL)
|
||||
}
|
||||
} else
|
||||
ContentResolver.setIsSyncable(account, TaskProvider.ProviderName.OpenTasks.authority, 0)
|
||||
@@ -62,4 +53,9 @@ class PackageChangedReceiver: BroadcastReceiver() {
|
||||
|
||||
}
|
||||
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
updateTaskSync(context)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ import java.util.logging.LogRecord
|
||||
|
||||
object LogcatHandler: Handler() {
|
||||
|
||||
private val MAX_LINE_LENGTH = 3000
|
||||
private const val MAX_LINE_LENGTH = 3000
|
||||
|
||||
init {
|
||||
formatter = PlainTextFormatter.LOGCAT
|
||||
|
||||
@@ -17,10 +17,9 @@ import android.preference.PreferenceManager
|
||||
import android.support.v4.app.NotificationCompat
|
||||
import android.support.v4.app.NotificationManagerCompat
|
||||
import android.util.Log
|
||||
import at.bitfire.davdroid.App
|
||||
import at.bitfire.davdroid.Constants
|
||||
import at.bitfire.davdroid.R
|
||||
import at.bitfire.davdroid.ui.AppSettingsActivity
|
||||
import at.bitfire.davdroid.ui.NotificationUtils
|
||||
import org.apache.commons.lang3.time.DateFormatUtils
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
@@ -29,28 +28,24 @@ import java.util.logging.Level
|
||||
|
||||
object Logger {
|
||||
|
||||
val LOG_TO_EXTERNAL_STORAGE = "log_to_external_storage"
|
||||
const val LOG_TO_EXTERNAL_STORAGE = "log_to_external_storage"
|
||||
|
||||
@JvmField
|
||||
val log = java.util.logging.Logger.getLogger("davdroid")!!
|
||||
|
||||
|
||||
lateinit var context: Context
|
||||
lateinit var preferences: SharedPreferences
|
||||
private lateinit var preferences: SharedPreferences
|
||||
|
||||
fun initialize(context: Context) {
|
||||
this.context = context
|
||||
|
||||
preferences = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
preferences.registerOnSharedPreferenceChangeListener { _, s ->
|
||||
if (s == LOG_TO_EXTERNAL_STORAGE)
|
||||
reinitialize()
|
||||
reinitialize(context.applicationContext)
|
||||
}
|
||||
|
||||
reinitialize()
|
||||
reinitialize(context.applicationContext)
|
||||
}
|
||||
|
||||
private fun reinitialize() {
|
||||
private fun reinitialize(context: Context) {
|
||||
val logToFile = preferences.getBoolean(LOG_TO_EXTERNAL_STORAGE, false)
|
||||
val logVerbose = logToFile || Log.isLoggable(log.name, Log.DEBUG)
|
||||
|
||||
@@ -68,9 +63,8 @@ object Logger {
|
||||
val nm = NotificationManagerCompat.from(context)
|
||||
// log to external file according to preferences
|
||||
if (logToFile) {
|
||||
val builder = NotificationCompat.Builder(context)
|
||||
builder .setSmallIcon(R.drawable.ic_sd_storage_light)
|
||||
.setLargeIcon(App.getLauncherBitmap(context))
|
||||
val builder = NotificationUtils.newBuilder(context, NotificationUtils.CHANNEL_DEBUG)
|
||||
builder .setSmallIcon(R.drawable.ic_sd_storage_notification)
|
||||
.setContentTitle(context.getString(R.string.logging_davdroid_file_logging))
|
||||
.setLocalOnly(true)
|
||||
|
||||
@@ -91,22 +85,22 @@ object Logger {
|
||||
.setCategory(NotificationCompat.CATEGORY_STATUS)
|
||||
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||
.setContentIntent(PendingIntent.getActivity(context, 0, prefIntent, PendingIntent.FLAG_UPDATE_CURRENT))
|
||||
.setStyle(NotificationCompat.BigTextStyle()
|
||||
.bigText(context.getString(R.string.logging_to_external_storage, dir.path)))
|
||||
.setStyle(NotificationCompat.BigTextStyle().bigText(context.getString(R.string.logging_to_external_storage, dir.path)))
|
||||
.setOngoing(true)
|
||||
|
||||
} catch(e: IOException) {
|
||||
log.log(Level.SEVERE, "Couldn't create external log file", e)
|
||||
|
||||
builder .setContentText(context.getString(R.string.logging_couldnt_create_file, e.localizedMessage))
|
||||
val message = context.getString(R.string.logging_couldnt_create_file, e.localizedMessage)
|
||||
builder .setContentText(message)
|
||||
.setStyle(NotificationCompat.BigTextStyle().bigText(message))
|
||||
.setCategory(NotificationCompat.CATEGORY_ERROR)
|
||||
}
|
||||
else
|
||||
builder.setContentText(context.getString(R.string.logging_no_external_storage))
|
||||
|
||||
nm.notify(Constants.NOTIFICATION_EXTERNAL_FILE_LOGGING, builder.build())
|
||||
nm.notify(NotificationUtils.NOTIFY_EXTERNAL_FILE_LOGGING, builder.build())
|
||||
} else
|
||||
nm.cancel(Constants.NOTIFICATION_EXTERNAL_FILE_LOGGING)
|
||||
nm.cancel(NotificationUtils.NOTIFY_EXTERNAL_FILE_LOGGING)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -19,10 +19,10 @@ class PlainTextFormatter private constructor(
|
||||
): Formatter() {
|
||||
|
||||
companion object {
|
||||
@JvmField val LOGCAT = PlainTextFormatter(true)
|
||||
@JvmField val DEFAULT = PlainTextFormatter(false)
|
||||
val LOGCAT = PlainTextFormatter(true)
|
||||
val DEFAULT = PlainTextFormatter(false)
|
||||
|
||||
val MAX_MESSAGE_LENGTH = 20000
|
||||
const val MAX_MESSAGE_LENGTH = 20000
|
||||
}
|
||||
|
||||
override fun format(r: LogRecord): String {
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
|
||||
package at.bitfire.davdroid.log
|
||||
|
||||
import java.util.logging.Handler;
|
||||
import java.util.logging.LogRecord;
|
||||
import java.util.logging.Handler
|
||||
import java.util.logging.LogRecord
|
||||
|
||||
class StringHandler: Handler() {
|
||||
|
||||
|
||||
@@ -9,13 +9,25 @@
|
||||
package at.bitfire.davdroid.model
|
||||
|
||||
import android.content.ContentValues
|
||||
import at.bitfire.dav4android.DavResource
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import at.bitfire.dav4android.Response
|
||||
import at.bitfire.dav4android.UrlUtils
|
||||
import at.bitfire.dav4android.property.*
|
||||
import at.bitfire.davdroid.model.ServiceDB.Collections
|
||||
import java.io.Serializable
|
||||
import okhttp3.HttpUrl
|
||||
|
||||
data class CollectionInfo @JvmOverloads constructor(
|
||||
val url: String,
|
||||
/**
|
||||
* Represents a WebDAV collection.
|
||||
*
|
||||
* @constructor always appends a trailing slash to the URL
|
||||
*/
|
||||
data class CollectionInfo(
|
||||
|
||||
/**
|
||||
* URL of the collection (including trailing slash)
|
||||
*/
|
||||
val url: HttpUrl,
|
||||
|
||||
var id: Long? = null,
|
||||
var serviceID: Long? = null,
|
||||
@@ -23,6 +35,7 @@ data class CollectionInfo @JvmOverloads constructor(
|
||||
var type: Type? = null,
|
||||
|
||||
var readOnly: Boolean = false,
|
||||
var forceReadOnly: Boolean = false,
|
||||
var displayName: String? = null,
|
||||
var description: String? = null,
|
||||
var color: Int? = null,
|
||||
@@ -37,7 +50,7 @@ data class CollectionInfo @JvmOverloads constructor(
|
||||
|
||||
// non-persistent properties
|
||||
var confirmed: Boolean = false
|
||||
): Serializable {
|
||||
): Parcelable {
|
||||
|
||||
enum class Type {
|
||||
ADDRESS_BOOK,
|
||||
@@ -45,23 +58,8 @@ data class CollectionInfo @JvmOverloads constructor(
|
||||
WEBCAL // iCalendar subscription
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@JvmField
|
||||
val DAV_PROPERTIES = arrayOf(
|
||||
ResourceType.NAME,
|
||||
CurrentUserPrivilegeSet.NAME,
|
||||
DisplayName.NAME,
|
||||
AddressbookDescription.NAME, SupportedAddressData.NAME,
|
||||
CalendarDescription.NAME, CalendarColor.NAME, SupportedCalendarComponentSet.NAME,
|
||||
Source.NAME
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
|
||||
constructor(dav: DavResource): this(dav.location.toString()) {
|
||||
(dav.properties[ResourceType.NAME] as ResourceType?)?.let { type ->
|
||||
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
|
||||
type.types.contains(ResourceType.CALENDAR) -> this.type = Type.CALENDAR
|
||||
@@ -69,40 +67,41 @@ data class CollectionInfo @JvmOverloads constructor(
|
||||
}
|
||||
}
|
||||
|
||||
(dav.properties[CurrentUserPrivilegeSet.NAME] as CurrentUserPrivilegeSet?)?.let { privilegeSet ->
|
||||
dav[CurrentUserPrivilegeSet::class.java]?.let { privilegeSet ->
|
||||
readOnly = !privilegeSet.mayWriteContent
|
||||
}
|
||||
|
||||
(dav.properties[DisplayName.NAME] as DisplayName?)?.let {
|
||||
dav[DisplayName::class.java]?.let {
|
||||
if (!it.displayName.isNullOrEmpty())
|
||||
displayName = it.displayName
|
||||
}
|
||||
|
||||
when (type) {
|
||||
Type.ADDRESS_BOOK -> {
|
||||
(dav.properties[AddressbookDescription.NAME] as AddressbookDescription?)?.let { description = it.description }
|
||||
dav[AddressbookDescription::class.java]?.let { description = it.description }
|
||||
}
|
||||
Type.CALENDAR -> {
|
||||
(dav.properties[CalendarDescription.NAME] as CalendarDescription?)?.let { description = it.description }
|
||||
(dav.properties[CalendarColor.NAME] as CalendarColor?)?.let { color = it.color }
|
||||
(dav.properties[CalendarTimezone.NAME] as CalendarTimezone?)?.let { timeZone = it.vTimeZone }
|
||||
Type.CALENDAR, Type.WEBCAL -> {
|
||||
dav[CalendarDescription::class.java]?.let { description = it.description }
|
||||
dav[CalendarColor::class.java]?.let { color = it.color }
|
||||
dav[CalendarTimezone::class.java]?.let { timeZone = it.vTimeZone }
|
||||
|
||||
supportsVEVENT = true
|
||||
supportsVTODO = true
|
||||
(dav.properties[SupportedCalendarComponentSet.NAME] as SupportedCalendarComponentSet?)?.let {
|
||||
supportsVEVENT = it.supportsEvents
|
||||
supportsVTODO = it.supportsTasks
|
||||
if (type == Type.CALENDAR) {
|
||||
supportsVEVENT = true
|
||||
supportsVTODO = true
|
||||
dav[SupportedCalendarComponentSet::class.java]?.let {
|
||||
supportsVEVENT = it.supportsEvents
|
||||
supportsVTODO = it.supportsTasks
|
||||
}
|
||||
} else { // Type.WEBCAL
|
||||
dav[Source::class.java]?.let { source = it.hrefs.firstOrNull() }
|
||||
supportsVEVENT = true
|
||||
}
|
||||
}
|
||||
Type.WEBCAL -> {
|
||||
(dav.properties[Source.NAME] as Source?)?.let { source = it.hrefs.firstOrNull() }
|
||||
supportsVEVENT = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
constructor(values: ContentValues): this(values.getAsString(Collections.URL)) {
|
||||
constructor(values: ContentValues): this(UrlUtils.withTrailingSlash(HttpUrl.parse(values.getAsString(Collections.URL))!!)) {
|
||||
id = values.getAsLong(Collections.ID)
|
||||
serviceID = values.getAsLong(Collections.SERVICE_ID)
|
||||
type = try {
|
||||
@@ -112,6 +111,7 @@ data class CollectionInfo @JvmOverloads constructor(
|
||||
}
|
||||
|
||||
readOnly = values.getAsInteger(Collections.READ_ONLY) != 0
|
||||
forceReadOnly = values.getAsInteger(Collections.FORCE_READ_ONLY) != 0
|
||||
displayName = values.getAsString(Collections.DISPLAY_NAME)
|
||||
description = values.getAsString(Collections.DESCRIPTION)
|
||||
|
||||
@@ -131,8 +131,9 @@ data class CollectionInfo @JvmOverloads constructor(
|
||||
// Collections.SERVICE_ID is never changed
|
||||
type?.let { values.put(Collections.TYPE, it.name) }
|
||||
|
||||
values.put(Collections.URL, url)
|
||||
values.put(Collections.URL, url.toString())
|
||||
values.put(Collections.READ_ONLY, if (readOnly) 1 else 0)
|
||||
values.put(Collections.FORCE_READ_ONLY, if (forceReadOnly) 1 else 0)
|
||||
values.put(Collections.DISPLAY_NAME, displayName)
|
||||
values.put(Collections.DESCRIPTION, description)
|
||||
values.put(Collections.COLOR, color)
|
||||
@@ -156,4 +157,87 @@ data class CollectionInfo @JvmOverloads constructor(
|
||||
(i != 0)
|
||||
}
|
||||
|
||||
|
||||
override fun describeContents(): Int = 0
|
||||
override fun writeToParcel(dest: Parcel, flags: Int) {
|
||||
fun<T> writeOrNull(value: T?, write: (T) -> Unit) {
|
||||
if (value == null)
|
||||
dest.writeByte(0)
|
||||
else {
|
||||
dest.writeByte(1)
|
||||
write(value)
|
||||
}
|
||||
}
|
||||
|
||||
dest.writeString(url.toString())
|
||||
|
||||
writeOrNull(id) { dest.writeLong(it) }
|
||||
writeOrNull(serviceID) { dest.writeLong(it) }
|
||||
|
||||
dest.writeString(type?.name)
|
||||
|
||||
dest.writeByte(if (readOnly) 1 else 0)
|
||||
dest.writeByte(if (forceReadOnly) 1 else 0)
|
||||
dest.writeString(displayName)
|
||||
dest.writeString(description)
|
||||
writeOrNull(color) { dest.writeInt(it) }
|
||||
|
||||
dest.writeString(timeZone)
|
||||
dest.writeByte(if (supportsVEVENT) 1 else 0)
|
||||
dest.writeByte(if (supportsVTODO) 1 else 0)
|
||||
dest.writeByte(if (selected) 1 else 0)
|
||||
|
||||
dest.writeString(source)
|
||||
|
||||
dest.writeByte(if (confirmed) 1 else 0)
|
||||
}
|
||||
|
||||
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())
|
||||
null
|
||||
else
|
||||
read()
|
||||
}
|
||||
|
||||
return CollectionInfo(
|
||||
HttpUrl.parse(parcel.readString())!!,
|
||||
|
||||
readOrNull(parcel) { parcel.readLong() },
|
||||
readOrNull(parcel) { parcel.readLong() },
|
||||
|
||||
parcel.readString()?.let { Type.valueOf(it) },
|
||||
|
||||
parcel.readByte() != 0.toByte(),
|
||||
parcel.readByte() != 0.toByte(),
|
||||
parcel.readString(),
|
||||
parcel.readString(),
|
||||
readOrNull(parcel) { parcel.readInt() },
|
||||
|
||||
parcel.readString(),
|
||||
parcel.readByte() != 0.toByte(),
|
||||
parcel.readByte() != 0.toByte(),
|
||||
parcel.readByte() != 0.toByte(),
|
||||
|
||||
parcel.readString(),
|
||||
|
||||
parcel.readByte() != 0.toByte()
|
||||
)
|
||||
}
|
||||
|
||||
override fun newArray(size: Int) = arrayOfNulls<CollectionInfo>(size)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
40
app/src/main/java/at/bitfire/davdroid/model/Credentials.kt
Normal file
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* 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.model
|
||||
|
||||
import java.io.Serializable
|
||||
|
||||
class Credentials(
|
||||
val userName: String? = null,
|
||||
val password: String? = null,
|
||||
val certificateAlias: String? = null
|
||||
): Serializable {
|
||||
|
||||
enum class Type {
|
||||
UsernamePassword,
|
||||
ClientCertificate
|
||||
}
|
||||
|
||||
val type: Type
|
||||
|
||||
init {
|
||||
type = when {
|
||||
!certificateAlias.isNullOrEmpty() ->
|
||||
Type.ClientCertificate
|
||||
!userName.isNullOrEmpty() && !password.isNullOrEmpty() ->
|
||||
Type.UsernamePassword
|
||||
else ->
|
||||
throw IllegalArgumentException("Either username/password or certificate alias must be set")
|
||||
}
|
||||
}
|
||||
|
||||
override fun toString() =
|
||||
"Credentials(type=$type, userName=$userName, certificateAlias=$certificateAlias)"
|
||||
|
||||
}
|
||||
@@ -17,56 +17,49 @@ import android.preference.PreferenceManager
|
||||
import at.bitfire.davdroid.App
|
||||
import at.bitfire.davdroid.log.Logger
|
||||
import at.bitfire.davdroid.ui.StartupDialogFragment
|
||||
import java.io.Closeable
|
||||
import java.util.logging.Level
|
||||
|
||||
class ServiceDB {
|
||||
|
||||
/*object Settings {
|
||||
@JvmField val _TABLE = "settings"
|
||||
@JvmField val NAME = "setting"
|
||||
@JvmField val VALUE = "value"
|
||||
}*/
|
||||
|
||||
object Services {
|
||||
@JvmField val _TABLE = "services"
|
||||
@JvmField val ID = "_id"
|
||||
@JvmField val ACCOUNT_NAME = "accountName"
|
||||
@JvmField val SERVICE = "service"
|
||||
@JvmField val PRINCIPAL = "principal"
|
||||
const val _TABLE = "services"
|
||||
const val ID = "_id"
|
||||
const val ACCOUNT_NAME = "accountName"
|
||||
const val SERVICE = "service"
|
||||
const val PRINCIPAL = "principal"
|
||||
|
||||
// allowed values for SERVICE column
|
||||
@JvmField val SERVICE_CALDAV = "caldav"
|
||||
@JvmField val SERVICE_CARDDAV = "carddav"
|
||||
const val SERVICE_CALDAV = "caldav"
|
||||
const val SERVICE_CARDDAV = "carddav"
|
||||
}
|
||||
|
||||
object HomeSets {
|
||||
@JvmField val _TABLE = "homesets"
|
||||
@JvmField val ID = "_id"
|
||||
@JvmField val SERVICE_ID = "serviceID"
|
||||
@JvmField val URL = "url"
|
||||
const val _TABLE = "homesets"
|
||||
const val ID = "_id"
|
||||
const val SERVICE_ID = "serviceID"
|
||||
const val URL = "url"
|
||||
}
|
||||
|
||||
object Collections {
|
||||
@JvmField val _TABLE = "collections"
|
||||
@JvmField val ID = "_id"
|
||||
@JvmField val TYPE = "type"
|
||||
@JvmField val SERVICE_ID = "serviceID"
|
||||
@JvmField val URL = "url"
|
||||
@JvmField val READ_ONLY = "readOnly"
|
||||
@JvmField val DISPLAY_NAME = "displayName"
|
||||
@JvmField val DESCRIPTION = "description"
|
||||
@JvmField val COLOR = "color"
|
||||
@JvmField val TIME_ZONE = "timezone"
|
||||
@JvmField val SUPPORTS_VEVENT = "supportsVEVENT"
|
||||
@JvmField val SUPPORTS_VTODO = "supportsVTODO"
|
||||
@JvmField val SOURCE = "source"
|
||||
@JvmField val SYNC = "sync"
|
||||
const val _TABLE = "collections"
|
||||
const val ID = "_id"
|
||||
const val TYPE = "type"
|
||||
const val SERVICE_ID = "serviceID"
|
||||
const val URL = "url"
|
||||
const val READ_ONLY = "readOnly"
|
||||
const val FORCE_READ_ONLY = "forceReadOnly"
|
||||
const val DISPLAY_NAME = "displayName"
|
||||
const val DESCRIPTION = "description"
|
||||
const val COLOR = "color"
|
||||
const val TIME_ZONE = "timezone"
|
||||
const val SUPPORTS_VEVENT = "supportsVEVENT"
|
||||
const val SUPPORTS_VTODO = "supportsVTODO"
|
||||
const val SOURCE = "source"
|
||||
const val SYNC = "sync"
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@JvmStatic
|
||||
fun onRenameAccount(db: SQLiteDatabase, oldName: String, newName: String) {
|
||||
val values = ContentValues(1)
|
||||
values.put(Services.ACCOUNT_NAME, newName)
|
||||
@@ -78,11 +71,11 @@ class ServiceDB {
|
||||
|
||||
class OpenHelper(
|
||||
val context: Context
|
||||
): SQLiteOpenHelper(context, DATABASE_NAME, null, DATABASE_VERSION), Closeable {
|
||||
): SQLiteOpenHelper(context, DATABASE_NAME, null, DATABASE_VERSION), AutoCloseable {
|
||||
|
||||
companion object {
|
||||
val DATABASE_NAME = "services.db"
|
||||
val DATABASE_VERSION = 3
|
||||
const val DATABASE_NAME = "services.db"
|
||||
const val DATABASE_VERSION = 4
|
||||
}
|
||||
|
||||
override fun onConfigure(db: SQLiteDatabase) {
|
||||
@@ -112,6 +105,7 @@ class ServiceDB {
|
||||
"${Collections.TYPE} TEXT NOT NULL," +
|
||||
"${Collections.URL} TEXT NOT NULL," +
|
||||
"${Collections.READ_ONLY} INTEGER DEFAULT 0 NOT NULL," +
|
||||
"${Collections.FORCE_READ_ONLY} INTEGER DEFAULT 0 NOT NULL," +
|
||||
"${Collections.DISPLAY_NAME} TEXT NULL," +
|
||||
"${Collections.DESCRIPTION} TEXT NULL," +
|
||||
"${Collections.COLOR} INTEGER NULL," +
|
||||
@@ -125,7 +119,7 @@ class ServiceDB {
|
||||
|
||||
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
|
||||
for (upgradeFrom in oldVersion until newVersion) {
|
||||
val upgradeTo = oldVersion + 1
|
||||
val upgradeTo = upgradeFrom + 1
|
||||
Logger.log.info("Upgrading database from version $upgradeFrom to $upgradeTo")
|
||||
try {
|
||||
val upgradeProc = this::class.java.getDeclaredMethod("upgrade_${upgradeFrom}_$upgradeTo", SQLiteDatabase::class.java)
|
||||
@@ -136,6 +130,11 @@ class ServiceDB {
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
private fun upgrade_3_4(db: SQLiteDatabase) {
|
||||
db.execSQL("ALTER TABLE ${Collections._TABLE} ADD COLUMN ${Collections.FORCE_READ_ONLY} INTEGER DEFAULT 0 NOT NULL")
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
private fun upgrade_2_3(db: SQLiteDatabase) {
|
||||
val edit = PreferenceManager.getDefaultSharedPreferences(context).edit()
|
||||
@@ -186,7 +185,7 @@ class ServiceDB {
|
||||
// print columns
|
||||
val cols = cursor.columnCount
|
||||
sb.append("\t| ")
|
||||
for (i in 0 .. cols-1)
|
||||
for (i in 0 until cols)
|
||||
sb .append(" ")
|
||||
.append(cursor.getColumnName(i))
|
||||
.append(" |")
|
||||
@@ -195,7 +194,7 @@ class ServiceDB {
|
||||
// print rows
|
||||
while (cursor.moveToNext()) {
|
||||
sb.append("\t| ")
|
||||
for (i in 0 .. cols-1) {
|
||||
for (i in 0 until cols) {
|
||||
sb.append(" ")
|
||||
try {
|
||||
val value = cursor.getString(i)
|
||||
@@ -221,4 +220,4 @@ class ServiceDB {
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
63
app/src/main/java/at/bitfire/davdroid/model/SyncState.kt
Normal file
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* 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.model
|
||||
|
||||
import at.bitfire.dav4android.property.SyncToken
|
||||
import org.json.JSONException
|
||||
import org.json.JSONObject
|
||||
|
||||
data class SyncState(
|
||||
val type: Type,
|
||||
val value: String,
|
||||
|
||||
/**
|
||||
* Whether this sync state occurred during an initial sync as described
|
||||
* in RFC 6578, which means the initial sync is not complete yet.
|
||||
*/
|
||||
var initialSync: Boolean? = null
|
||||
) {
|
||||
|
||||
companion object {
|
||||
|
||||
private const val KEY_TYPE = "type"
|
||||
private const val KEY_VALUE = "value"
|
||||
private const val KEY_INITIAL_SYNC = "initialSync"
|
||||
|
||||
fun fromString(s: String?): SyncState? {
|
||||
if (s == null)
|
||||
return null
|
||||
|
||||
return try {
|
||||
val json = JSONObject(s)
|
||||
SyncState(
|
||||
Type.valueOf(json.getString(KEY_TYPE)),
|
||||
json.getString(KEY_VALUE),
|
||||
try { json.getBoolean(KEY_INITIAL_SYNC) } catch(e: JSONException) { null }
|
||||
)
|
||||
} catch (e: JSONException) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun fromSyncToken(token: SyncToken, initialSync: Boolean? = null) =
|
||||
SyncState(Type.SYNC_TOKEN, requireNotNull(token.token), initialSync)
|
||||
|
||||
}
|
||||
|
||||
enum class Type { CTAG, SYNC_TOKEN }
|
||||
|
||||
override fun toString(): String {
|
||||
val json = JSONObject()
|
||||
json.put(KEY_TYPE, type.name)
|
||||
json.put(KEY_VALUE, value)
|
||||
initialSync?.let { json.put(KEY_INITIAL_SYNC, it) }
|
||||
return json.toString()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -6,23 +6,16 @@
|
||||
* http://www.gnu.org/licenses/gpl.html
|
||||
*/
|
||||
|
||||
package at.bitfire.davdroid.model;
|
||||
package at.bitfire.davdroid.model
|
||||
|
||||
import android.provider.ContactsContract.RawContacts;
|
||||
import android.provider.ContactsContract.RawContacts
|
||||
|
||||
object UnknownProperties {
|
||||
|
||||
@JvmField
|
||||
val CONTENT_ITEM_TYPE = "x.davdroid/unknown-properties"
|
||||
const val CONTENT_ITEM_TYPE = "x.davdroid/unknown-properties"
|
||||
|
||||
|
||||
@JvmField
|
||||
val MIMETYPE = RawContacts.Data.MIMETYPE
|
||||
|
||||
@JvmField
|
||||
val RAW_CONTACT_ID = RawContacts.Data.RAW_CONTACT_ID
|
||||
|
||||
@JvmField
|
||||
val UNKNOWN_PROPERTIES = RawContacts.Data.DATA1
|
||||
const val MIMETYPE = RawContacts.Data.MIMETYPE
|
||||
const val RAW_CONTACT_ID = RawContacts.Data.RAW_CONTACT_ID
|
||||
const val UNKNOWN_PROPERTIES = RawContacts.Data.DATA1
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* 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.resource
|
||||
|
||||
import at.bitfire.vcard4android.Contact
|
||||
|
||||
interface LocalAddress: LocalResource<Contact> {
|
||||
|
||||
fun resetDeleted()
|
||||
|
||||
}
|
||||
@@ -9,6 +9,7 @@ package at.bitfire.davdroid.resource
|
||||
|
||||
import android.accounts.Account
|
||||
import android.accounts.AccountManager
|
||||
import android.annotation.TargetApi
|
||||
import android.content.*
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
@@ -22,70 +23,66 @@ import at.bitfire.davdroid.DavUtils
|
||||
import at.bitfire.davdroid.R
|
||||
import at.bitfire.davdroid.log.Logger
|
||||
import at.bitfire.davdroid.model.CollectionInfo
|
||||
import at.bitfire.davdroid.model.SyncState
|
||||
import at.bitfire.vcard4android.*
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.FileNotFoundException
|
||||
import java.util.*
|
||||
import java.util.logging.Level
|
||||
|
||||
/**
|
||||
* A local address book. Requires an own Android account, because Android manages contacts per
|
||||
* account and there is no such thing as "address books". So, DAVdroid creates a "DAVdroid
|
||||
* address book" account for every CardDAV address book. These accounts are bound to a
|
||||
* DAVdroid main account.
|
||||
*/
|
||||
class LocalAddressBook(
|
||||
private val context: Context,
|
||||
account: Account,
|
||||
provider: ContentProviderClient?
|
||||
): AndroidAddressBook<LocalContact, LocalGroup>(account, provider, LocalContact.Factory, LocalGroup.Factory), LocalCollection<LocalResource> {
|
||||
): AndroidAddressBook<LocalContact, LocalGroup>(account, provider, LocalContact.Factory, LocalGroup.Factory), LocalCollection<LocalAddress> {
|
||||
|
||||
companion object {
|
||||
|
||||
val USER_DATA_MAIN_ACCOUNT_TYPE = "real_account_type"
|
||||
val USER_DATA_MAIN_ACCOUNT_NAME = "real_account_name"
|
||||
val USER_DATA_URL = "url"
|
||||
val USER_DATA_READ_ONLY = "read_only"
|
||||
val USER_DATA_CTAG = "ctag"
|
||||
const val USER_DATA_MAIN_ACCOUNT_TYPE = "real_account_type"
|
||||
const val USER_DATA_MAIN_ACCOUNT_NAME = "real_account_name"
|
||||
const val USER_DATA_URL = "url"
|
||||
const val USER_DATA_READ_ONLY = "read_only"
|
||||
|
||||
@JvmStatic
|
||||
@Throws(ContactsStorageException::class)
|
||||
fun create(context: Context, provider: ContentProviderClient, mainAccount: Account, info: CollectionInfo): LocalAddressBook {
|
||||
val accountManager = AccountManager.get(context)
|
||||
|
||||
val account = Account(accountName(mainAccount, info), context.getString(R.string.account_type_address_book))
|
||||
if (!accountManager.addAccountExplicitly(account, null, initialUserData(mainAccount, info.url)))
|
||||
throw ContactsStorageException("Couldn't create address book account")
|
||||
if (!accountManager.addAccountExplicitly(account, null, initialUserData(mainAccount, info.url.toString())))
|
||||
throw IllegalStateException("Couldn't create address book account")
|
||||
|
||||
val addressBook = LocalAddressBook(context, account, provider)
|
||||
ContentResolver.setSyncAutomatically(account, ContactsContract.AUTHORITY, true)
|
||||
|
||||
// initialize Contacts Provider Settings
|
||||
val values = ContentValues(2)
|
||||
values.put(ContactsContract.Settings.SHOULD_SYNC, 1)
|
||||
values.put(ContactsContract.Settings.UNGROUPED_VISIBLE, 1)
|
||||
addressBook.settings = values
|
||||
|
||||
return addressBook
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
@Throws(ContactsStorageException::class)
|
||||
fun find(context: Context, provider: ContentProviderClient, mainAccount: Account?): List<LocalAddressBook> {
|
||||
val accountManager = AccountManager.get(context)
|
||||
fun findAll(context: Context, provider: ContentProviderClient, mainAccount: Account?) = AccountManager.get(context)
|
||||
.getAccountsByType(context.getString(R.string.account_type_address_book))
|
||||
.map { LocalAddressBook(context, it, provider) }
|
||||
.filter { mainAccount == null || it.mainAccount == mainAccount }
|
||||
.toList()
|
||||
|
||||
val result = LinkedList<LocalAddressBook>()
|
||||
accountManager.getAccountsByType(context.getString(R.string.account_type_address_book))
|
||||
.map { LocalAddressBook(context, it, provider) }
|
||||
.filter { mainAccount == null || it.getMainAccount() == mainAccount }
|
||||
.forEach { result += it }
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun accountName(mainAccount: Account, info: CollectionInfo): String {
|
||||
val baos = ByteArrayOutputStream()
|
||||
baos.write(info.url.hashCode())
|
||||
val hash = Base64.encodeToString(baos.toByteArray(), Base64.NO_WRAP or Base64.NO_PADDING)
|
||||
|
||||
val sb = StringBuilder(if (info.displayName.isNullOrEmpty()) DavUtils.lastSegmentOfUrl(info.url) else info.displayName)
|
||||
sb .append(" (")
|
||||
.append(mainAccount.name)
|
||||
.append(" ")
|
||||
.append(hash)
|
||||
.append(")")
|
||||
sb.append(" (${mainAccount.name} $hash)")
|
||||
return sb.toString()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun initialUserData(mainAccount: Account, url: String): Bundle {
|
||||
val bundle = Bundle(3)
|
||||
bundle.putString(USER_DATA_MAIN_ACCOUNT_NAME, mainAccount.name)
|
||||
@@ -94,180 +91,212 @@ class LocalAddressBook(
|
||||
return bundle
|
||||
}
|
||||
|
||||
fun mainAccount(context: Context, account: Account): Account =
|
||||
if (account.type == context.getString(R.string.account_type_address_book)) {
|
||||
val manager = AccountManager.get(context)
|
||||
Account(
|
||||
manager.getUserData(account, USER_DATA_MAIN_ACCOUNT_NAME),
|
||||
manager.getUserData(account, USER_DATA_MAIN_ACCOUNT_TYPE)
|
||||
)
|
||||
} else
|
||||
account
|
||||
|
||||
}
|
||||
|
||||
override val title = account.name!!
|
||||
|
||||
/**
|
||||
* Whether contact groups (LocalGroup resources) are included in query results for
|
||||
* {@link #getAll()}, {@link #getDeleted()}, {@link #getDirty()} and
|
||||
* {@link #getWithoutFileName()}.
|
||||
* Whether contact groups ([LocalGroup]) are included in query results
|
||||
* and are affected by updates/deletes on generic members.
|
||||
*
|
||||
* For instance, if this option is disabled, [findDirty] will find only dirty [LocalContact]s,
|
||||
* but if it is enabled, [findDirty] will find dirty [LocalContact]s and [LocalGroup]s.
|
||||
*/
|
||||
var includeGroups = true
|
||||
|
||||
private var _mainAccount: Account? = null
|
||||
var mainAccount: Account
|
||||
get() {
|
||||
_mainAccount?.let { return it }
|
||||
|
||||
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(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 = newMainAccount
|
||||
}
|
||||
|
||||
var url: String
|
||||
get() = AccountManager.get(context).getUserData(account, USER_DATA_URL)
|
||||
?: throw IllegalStateException("Address book has no URL")
|
||||
set(url) = AccountManager.get(context).setUserData(account, USER_DATA_URL, url)
|
||||
|
||||
var readOnly: Boolean
|
||||
get() = AccountManager.get(context).getUserData(account, USER_DATA_READ_ONLY) != null
|
||||
set(readOnly) = AccountManager.get(context).setUserData(account, USER_DATA_READ_ONLY, if (readOnly) "1" else null)
|
||||
|
||||
override var lastSyncState: SyncState?
|
||||
get() = syncState?.let { SyncState.fromString(String(it)) }
|
||||
set(state) {
|
||||
syncState = state?.toString()?.toByteArray()
|
||||
}
|
||||
|
||||
|
||||
/* operations on the collection (address book) itself */
|
||||
|
||||
@Throws(ContactsStorageException::class)
|
||||
override fun markNotDirty(flags: Int): Int {
|
||||
val values = ContentValues(1)
|
||||
values.put(LocalContact.COLUMN_FLAGS, flags)
|
||||
var number = provider!!.update(rawContactsSyncUri(), values, "${RawContacts.DIRTY}=0", null)
|
||||
|
||||
if (includeGroups) {
|
||||
values.clear()
|
||||
values.put(LocalGroup.COLUMN_FLAGS, flags)
|
||||
number += provider.update(groupsSyncUri(), values, "${Groups.DIRTY}=0", null)
|
||||
}
|
||||
|
||||
return number
|
||||
}
|
||||
|
||||
override fun removeNotDirtyMarked(flags: Int): Int {
|
||||
var number = provider!!.delete(rawContactsSyncUri(),
|
||||
"${RawContacts.DIRTY}=0 AND ${LocalContact.COLUMN_FLAGS}=?", arrayOf(flags.toString()))
|
||||
|
||||
if (includeGroups)
|
||||
number += provider.delete(groupsSyncUri(),
|
||||
"${Groups.DIRTY}=0 AND ${LocalGroup.COLUMN_FLAGS}=?", arrayOf(flags.toString()))
|
||||
|
||||
return number
|
||||
}
|
||||
|
||||
fun update(info: CollectionInfo) {
|
||||
val newAccountName = accountName(getMainAccount(), info)
|
||||
val newAccountName = accountName(mainAccount, info)
|
||||
|
||||
@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
|
||||
}
|
||||
|
||||
Constants.log.info("Address book read-only? = ${info.readOnly}")
|
||||
setReadOnly(info.readOnly)
|
||||
readOnly = info.readOnly || info.forceReadOnly
|
||||
|
||||
// make sure it will still be synchronized when contacts are updated
|
||||
ContentResolver.setSyncAutomatically(account, ContactsContract.AUTHORITY, true)
|
||||
}
|
||||
|
||||
@Throws(ContactsStorageException::class)
|
||||
fun delete() {
|
||||
val accountManager = AccountManager.get(context)
|
||||
try {
|
||||
if (Build.VERSION.SDK_INT >= 22)
|
||||
accountManager.removeAccount(account, null, null, null)
|
||||
else
|
||||
@Suppress("deprecation")
|
||||
accountManager.removeAccount(account, null, null)
|
||||
} catch(e: Exception) {
|
||||
throw ContactsStorageException("Couldn't remove address book", e)
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= 22)
|
||||
accountManager.removeAccount(account, null, null, null)
|
||||
else
|
||||
accountManager.removeAccount(account, null, null)
|
||||
}
|
||||
|
||||
|
||||
/* operations on members (contacts/groups) */
|
||||
|
||||
@Throws(ContactsStorageException::class, FileNotFoundException::class)
|
||||
fun findContactByUID(uid: String): LocalContact {
|
||||
val contacts = queryContacts("${AndroidContact.COLUMN_UID}=?", arrayOf(uid))
|
||||
if (contacts.isEmpty())
|
||||
throw FileNotFoundException()
|
||||
return contacts.first()
|
||||
override fun findByName(name: String): LocalAddress? {
|
||||
val result = queryContacts("${AndroidContact.COLUMN_FILENAME}=?", arrayOf(name)).firstOrNull()
|
||||
return if (includeGroups)
|
||||
result ?: queryGroups("${AndroidGroup.COLUMN_FILENAME}=?", arrayOf(name)).firstOrNull()
|
||||
else
|
||||
result
|
||||
}
|
||||
|
||||
@Throws(ContactsStorageException::class)
|
||||
override fun getAll(): List<LocalResource> {
|
||||
val all = LinkedList<LocalResource>()
|
||||
all.addAll(queryContacts(null, null))
|
||||
if (includeGroups)
|
||||
all.addAll(queryGroups(null, null))
|
||||
return all
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of local contacts/groups which have been deleted locally. (DELETED != 0).
|
||||
* @throws RemoteException on content provider errors
|
||||
*/
|
||||
@Throws(ContactsStorageException::class)
|
||||
override fun getDeleted(): List<LocalResource> {
|
||||
val deleted = LinkedList<LocalResource>()
|
||||
deleted.addAll(getDeletedContacts())
|
||||
if (includeGroups)
|
||||
deleted.addAll(getDeletedGroups())
|
||||
return deleted
|
||||
override fun findDeleted() =
|
||||
if (includeGroups)
|
||||
findDeletedContacts() + findDeletedGroups()
|
||||
else
|
||||
findDeletedContacts()
|
||||
|
||||
fun findDeletedContacts() = queryContacts("${RawContacts.DELETED}!=0", null)
|
||||
fun findDeletedGroups() = queryGroups("${Groups.DELETED}!=0", null)
|
||||
|
||||
/**
|
||||
* Returns an array of local contacts/groups which have been changed locally (DIRTY != 0).
|
||||
* @throws RemoteException on content provider errors
|
||||
*/
|
||||
override fun findDirty() =
|
||||
if (includeGroups)
|
||||
findDirtyContacts() + findDirtyGroups()
|
||||
else
|
||||
findDirtyContacts()
|
||||
|
||||
fun findDirtyContacts() = queryContacts("${RawContacts.DIRTY}!=0", null)
|
||||
fun findDirtyGroups() = queryGroups("${Groups.DIRTY}!=0", null)
|
||||
|
||||
private fun queryContactsGroups(whereContacts: String?, whereArgsContacts: Array<String>?, whereGroups: String?, whereArgsGroups: Array<String>?): List<LocalAddress> {
|
||||
val contacts = queryContacts(whereContacts, whereArgsContacts)
|
||||
return if (includeGroups)
|
||||
contacts + queryGroups(whereGroups, whereArgsGroups)
|
||||
else
|
||||
contacts
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Queries all contacts with DIRTY flag and checks whether their data checksum has changed, i.e.
|
||||
* if they're "really dirty" (= data has changed, not only metadata, which is not hashed).
|
||||
* The DIRTY flag is removed from contacts which are not "really dirty", i.e. from contacts
|
||||
* whose contact data checksum has not changed.
|
||||
* @return number of "really dirty" contacts
|
||||
* @throws RemoteException on content provider errors
|
||||
*/
|
||||
@Throws(ContactsStorageException::class)
|
||||
fun verifyDirty(): Int {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N || Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
|
||||
throw IllegalStateException("verifyDirty() should not be called on Android != 7")
|
||||
|
||||
var reallyDirty = 0
|
||||
for (contact in getDirtyContacts())
|
||||
try {
|
||||
val lastHash = contact.getLastHashCode()
|
||||
val currentHash = contact.dataHashCode()
|
||||
if (lastHash == currentHash) {
|
||||
// hash is code still the same, contact is not "really dirty" (only metadata been have changed)
|
||||
Logger.log.log(Level.FINE, "Contact data hash has not changed, resetting dirty flag", contact)
|
||||
contact.resetDirty()
|
||||
} else {
|
||||
Logger.log.log(Level.FINE, "Contact data has changed from hash $lastHash to $currentHash", contact)
|
||||
reallyDirty++
|
||||
}
|
||||
} catch(e: FileNotFoundException) {
|
||||
throw ContactsStorageException("Couldn't calculate hash code", e)
|
||||
for (contact in findDirtyContacts()) {
|
||||
val lastHash = contact.getLastHashCode()
|
||||
val currentHash = contact.dataHashCode()
|
||||
if (lastHash == currentHash) {
|
||||
// hash is code still the same, contact is not "really dirty" (only metadata been have changed)
|
||||
Logger.log.log(Level.FINE, "Contact data hash has not changed, resetting dirty flag", contact)
|
||||
contact.resetDirty()
|
||||
} else {
|
||||
Logger.log.log(Level.FINE, "Contact data has changed from hash $lastHash to $currentHash", contact)
|
||||
reallyDirty++
|
||||
}
|
||||
}
|
||||
|
||||
if (includeGroups)
|
||||
reallyDirty += getDirtyGroups().size
|
||||
reallyDirty += findDirtyGroups().size
|
||||
|
||||
return reallyDirty
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of local contacts/groups which have been changed locally (DIRTY != 0).
|
||||
*/
|
||||
@Throws(ContactsStorageException::class)
|
||||
override fun getDirty(): List<LocalResource> {
|
||||
val dirty = LinkedList<LocalResource>()
|
||||
dirty.addAll(getDirtyContacts())
|
||||
if (includeGroups)
|
||||
dirty.addAll(getDirtyGroups())
|
||||
return dirty
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of local contacts which don't have a file name yet.
|
||||
*/
|
||||
@Throws(ContactsStorageException::class)
|
||||
override fun getWithoutFileName(): List<LocalResource> {
|
||||
val nameless = LinkedList<LocalResource>()
|
||||
nameless.addAll(queryContacts("${AndroidContact.COLUMN_FILENAME} IS NULL", null))
|
||||
if (includeGroups)
|
||||
nameless.addAll(queryGroups("${AndroidGroup.COLUMN_FILENAME} IS NULL", null))
|
||||
return nameless
|
||||
}
|
||||
|
||||
@Throws(ContactsStorageException::class)
|
||||
fun getDeletedContacts() = queryContacts("${RawContacts.DELETED} != 0", null)
|
||||
|
||||
@Throws(ContactsStorageException::class)
|
||||
fun getDirtyContacts() = queryContacts("${RawContacts.DIRTY} != 0", null)
|
||||
|
||||
@Throws(ContactsStorageException::class)
|
||||
fun getDeletedGroups() = queryGroups("${Groups.DELETED} != 0", null)
|
||||
|
||||
@Throws(ContactsStorageException::class)
|
||||
fun getDirtyGroups() = queryGroups("${Groups.DIRTY} != 0", null)
|
||||
|
||||
@Throws(ContactsStorageException::class)
|
||||
fun getByGroupMembership(groupID: Long): List<LocalContact> {
|
||||
try {
|
||||
val ids = HashSet<Long>()
|
||||
|
||||
provider!!.query(syncAdapterURI(ContactsContract.Data.CONTENT_URI),
|
||||
arrayOf(RawContacts.Data.RAW_CONTACT_ID),
|
||||
"(${GroupMembership.MIMETYPE}=? AND ${GroupMembership.GROUP_ROW_ID}=?) OR (${CachedGroupMembership.MIMETYPE}=? AND ${CachedGroupMembership.GROUP_ID}=?)",
|
||||
arrayOf(GroupMembership.CONTENT_ITEM_TYPE, groupID.toString(), CachedGroupMembership.CONTENT_ITEM_TYPE, groupID.toString()),
|
||||
null)?.use { cursor ->
|
||||
while (cursor.moveToNext())
|
||||
ids += cursor.getLong(0)
|
||||
}
|
||||
|
||||
return ids.map { id -> LocalContact(this, id, null, null) }
|
||||
} catch (e: RemoteException) {
|
||||
throw ContactsStorageException("Couldn't query contacts", e)
|
||||
val ids = HashSet<Long>()
|
||||
provider!!.query(syncAdapterURI(ContactsContract.Data.CONTENT_URI),
|
||||
arrayOf(RawContacts.Data.RAW_CONTACT_ID),
|
||||
"(${GroupMembership.MIMETYPE}=? AND ${GroupMembership.GROUP_ROW_ID}=?) OR (${CachedGroupMembership.MIMETYPE}=? AND ${CachedGroupMembership.GROUP_ID}=?)",
|
||||
arrayOf(GroupMembership.CONTENT_ITEM_TYPE, groupID.toString(), CachedGroupMembership.CONTENT_ITEM_TYPE, groupID.toString()),
|
||||
null)?.use { cursor ->
|
||||
while (cursor.moveToNext())
|
||||
ids += cursor.getLong(0)
|
||||
}
|
||||
|
||||
return ids.map { findContactByID(it) }
|
||||
}
|
||||
|
||||
|
||||
@@ -276,29 +305,23 @@ class LocalAddressBook(
|
||||
/**
|
||||
* Finds the first group with the given title. If there is no group with this
|
||||
* title, a new group is created.
|
||||
* @param title title of the group to look for
|
||||
* @return id of the group with given title
|
||||
* @throws ContactsStorageException on contact provider errors
|
||||
* @param title title of the group to look for
|
||||
* @return id of the group with given title
|
||||
* @throws RemoteException on content provider errors
|
||||
*/
|
||||
@Throws(ContactsStorageException::class)
|
||||
fun findOrCreateGroup(title: String): Long {
|
||||
try {
|
||||
provider!!.query(syncAdapterURI(Groups.CONTENT_URI), arrayOf(Groups._ID),
|
||||
"${Groups.TITLE}=?", arrayOf(title), null)?.use { cursor ->
|
||||
if (cursor.moveToNext())
|
||||
return cursor.getLong(0)
|
||||
}
|
||||
|
||||
val values = ContentValues(1)
|
||||
values.put(Groups.TITLE, title)
|
||||
val uri = provider.insert(syncAdapterURI(Groups.CONTENT_URI), values)
|
||||
return ContentUris.parseId(uri)
|
||||
} catch(e: RemoteException) {
|
||||
throw ContactsStorageException("Couldn't find local contact group", e)
|
||||
provider!!.query(syncAdapterURI(Groups.CONTENT_URI), arrayOf(Groups._ID),
|
||||
"${Groups.TITLE}=?", arrayOf(title), null)?.use { cursor ->
|
||||
if (cursor.moveToNext())
|
||||
return cursor.getLong(0)
|
||||
}
|
||||
|
||||
val values = ContentValues(1)
|
||||
values.put(Groups.TITLE, title)
|
||||
val uri = provider.insert(syncAdapterURI(Groups.CONTENT_URI), values)
|
||||
return ContentUris.parseId(uri)
|
||||
}
|
||||
|
||||
@Throws(ContactsStorageException::class)
|
||||
fun removeEmptyGroups() {
|
||||
// find groups without members
|
||||
/** should be done using {@link Groups.SUMMARY_COUNT}, but it's not implemented in Android yet */
|
||||
@@ -308,52 +331,4 @@ class LocalAddressBook(
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(ContactsStorageException::class)
|
||||
fun removeGroups() {
|
||||
try {
|
||||
provider!!.delete(syncAdapterURI(Groups.CONTENT_URI), null, null)
|
||||
} catch(e: RemoteException) {
|
||||
throw ContactsStorageException("Couldn't remove all groups", e)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// SETTINGS
|
||||
|
||||
@Throws(ContactsStorageException::class)
|
||||
fun getMainAccount(): Account {
|
||||
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 ContactsStorageException("Address book doesn't exist anymore")
|
||||
}
|
||||
|
||||
fun setMainAccount(mainAccount: Account) {
|
||||
val accountManager = AccountManager.get(context)
|
||||
accountManager.setUserData(account, USER_DATA_MAIN_ACCOUNT_NAME, mainAccount.name)
|
||||
accountManager.setUserData(account, USER_DATA_MAIN_ACCOUNT_TYPE, mainAccount.type)
|
||||
}
|
||||
|
||||
@Throws(ContactsStorageException::class)
|
||||
fun getURL() =
|
||||
AccountManager.get(context).getUserData(account, USER_DATA_URL) ?: throw ContactsStorageException("Address book has no URL")
|
||||
|
||||
fun setURL(url: String) =
|
||||
AccountManager.get(context).setUserData(account, USER_DATA_URL, url)
|
||||
|
||||
fun getReadOnly() =
|
||||
AccountManager.get(context).getUserData(account, USER_DATA_READ_ONLY) != null
|
||||
|
||||
fun setReadOnly(readOnly: Boolean) =
|
||||
AccountManager.get(context).setUserData(account, USER_DATA_READ_ONLY, if (readOnly) "1" else null)
|
||||
|
||||
override fun getCTag(): String? =
|
||||
AccountManager.get(context).getUserData(account, USER_DATA_CTAG)
|
||||
|
||||
override fun setCTag(cTag: String?) =
|
||||
AccountManager.get(context).setUserData(account, USER_DATA_CTAG, cTag)
|
||||
|
||||
}
|
||||
}
|
||||
@@ -14,14 +14,17 @@ import android.content.ContentProviderOperation
|
||||
import android.content.ContentUris
|
||||
import android.content.ContentValues
|
||||
import android.net.Uri
|
||||
import android.os.RemoteException
|
||||
import android.provider.CalendarContract
|
||||
import android.provider.CalendarContract.*
|
||||
import at.bitfire.davdroid.Constants
|
||||
import at.bitfire.davdroid.DavUtils
|
||||
import at.bitfire.davdroid.log.Logger
|
||||
import at.bitfire.davdroid.model.CollectionInfo
|
||||
import at.bitfire.ical4android.*
|
||||
import java.io.FileNotFoundException
|
||||
import at.bitfire.davdroid.model.SyncState
|
||||
import at.bitfire.ical4android.AndroidCalendar
|
||||
import at.bitfire.ical4android.AndroidCalendarFactory
|
||||
import at.bitfire.ical4android.BatchOperation
|
||||
import at.bitfire.ical4android.DateUtils
|
||||
import java.util.*
|
||||
import java.util.logging.Level
|
||||
|
||||
@@ -33,18 +36,8 @@ class LocalCalendar private constructor(
|
||||
|
||||
companion object {
|
||||
|
||||
val defaultColor = 0xFF8bc34a.toInt() // light green 500
|
||||
private const val COLUMN_SYNC_STATE = Calendars.CAL_SYNC1
|
||||
|
||||
val COLUMN_CTAG = Calendars.CAL_SYNC1
|
||||
|
||||
val BASE_INFO_COLUMNS = arrayOf(
|
||||
Events._ID,
|
||||
Events._SYNC_ID,
|
||||
LocalEvent.COLUMN_ETAG
|
||||
)
|
||||
|
||||
|
||||
@Throws(CalendarStorageException::class)
|
||||
fun create(account: Account, provider: ContentProviderClient, info: CollectionInfo): Uri {
|
||||
val values = valuesFromCollectionInfo(info, true)
|
||||
|
||||
@@ -56,20 +49,18 @@ class LocalCalendar private constructor(
|
||||
// flag as visible & synchronizable at creation, might be changed by user at any time
|
||||
values.put(Calendars.VISIBLE, 1)
|
||||
values.put(Calendars.SYNC_EVENTS, 1)
|
||||
val uri = create(account, provider, values)
|
||||
|
||||
return uri
|
||||
return create(account, provider, values)
|
||||
}
|
||||
|
||||
private fun valuesFromCollectionInfo(info: CollectionInfo, withColor: Boolean): ContentValues {
|
||||
val values = ContentValues()
|
||||
values.put(Calendars.NAME, info.url)
|
||||
values.put(Calendars.NAME, info.url.toString())
|
||||
values.put(Calendars.CALENDAR_DISPLAY_NAME, if (info.displayName.isNullOrBlank()) DavUtils.lastSegmentOfUrl(info.url) else info.displayName)
|
||||
|
||||
if (withColor)
|
||||
values.put(Calendars.CALENDAR_COLOR, info.color ?: defaultColor)
|
||||
values.put(Calendars.CALENDAR_COLOR, info.color ?: Constants.DAVDROID_GREEN_RGBA)
|
||||
|
||||
if (info.readOnly)
|
||||
if (info.readOnly || info.forceReadOnly)
|
||||
values.put(Calendars.CALENDAR_ACCESS_LEVEL, Calendars.CAL_ACCESS_READ)
|
||||
else {
|
||||
values.put(Calendars.CALENDAR_ACCESS_LEVEL, Calendars.CAL_ACCESS_OWNER)
|
||||
@@ -92,31 +83,33 @@ class LocalCalendar private constructor(
|
||||
values.put(Calendars.ALLOWED_ATTENDEE_TYPES, "${CalendarContract.Attendees.TYPE_OPTIONAL},${CalendarContract.Attendees.TYPE_REQUIRED},${CalendarContract.Attendees.TYPE_RESOURCE}")
|
||||
return values
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override val title: String
|
||||
get() = displayName ?: id.toString()
|
||||
|
||||
override fun eventBaseInfoColumns() = BASE_INFO_COLUMNS
|
||||
override var lastSyncState: SyncState?
|
||||
get() = provider.query(calendarSyncURI(), arrayOf(COLUMN_SYNC_STATE), null, null, null)?.let { cursor ->
|
||||
if (cursor.moveToNext())
|
||||
return SyncState.fromString(cursor.getString(0))
|
||||
else
|
||||
null
|
||||
}
|
||||
set(state) {
|
||||
val values = ContentValues(1)
|
||||
values.put(COLUMN_SYNC_STATE, state.toString())
|
||||
provider.update(calendarSyncURI(), values, null, null)
|
||||
}
|
||||
|
||||
|
||||
@Throws(CalendarStorageException::class)
|
||||
fun update(info: CollectionInfo, updateColor: Boolean) =
|
||||
update(valuesFromCollectionInfo(info, updateColor))
|
||||
|
||||
@Throws(CalendarStorageException::class)
|
||||
override fun getAll(): List<LocalEvent> =
|
||||
queryEvents("${Events.ORIGINAL_ID} IS NULL", null)
|
||||
|
||||
@Throws(CalendarStorageException::class)
|
||||
override fun getDeleted() =
|
||||
override fun findDeleted() =
|
||||
queryEvents("${Events.DELETED}!=0 AND ${Events.ORIGINAL_ID} IS NULL", null)
|
||||
|
||||
@Throws(CalendarStorageException::class)
|
||||
override fun getWithoutFileName() =
|
||||
queryEvents("${Events._SYNC_ID} IS NULL AND ${Events.ORIGINAL_ID} IS NULL", null)
|
||||
|
||||
@Throws(FileNotFoundException::class, CalendarStorageException::class)
|
||||
override fun getDirty(): List<LocalEvent> {
|
||||
override fun findDirty(): List<LocalEvent> {
|
||||
val dirty = LinkedList<LocalEvent>()
|
||||
|
||||
// get dirty events which are required to have an increased SEQUENCE value
|
||||
@@ -133,50 +126,46 @@ class LocalCalendar private constructor(
|
||||
return dirty
|
||||
}
|
||||
|
||||
@Throws(CalendarStorageException::class)
|
||||
override fun getCTag(): String? =
|
||||
try {
|
||||
provider.query(calendarSyncURI(), arrayOf(COLUMN_CTAG), null, null, null)?.use { cursor ->
|
||||
if (cursor.moveToNext())
|
||||
return cursor.getString(0)
|
||||
}
|
||||
null
|
||||
} catch (e: RemoteException) {
|
||||
throw CalendarStorageException("Couldn't read local (last known) CTag", e)
|
||||
}
|
||||
override fun findByName(name: String) =
|
||||
queryEvents("${Events._SYNC_ID}=?", arrayOf(name)).firstOrNull()
|
||||
|
||||
@Throws(CalendarStorageException::class)
|
||||
override fun setCTag(cTag: String?) {
|
||||
try {
|
||||
val values = ContentValues(1)
|
||||
values.put(COLUMN_CTAG, cTag)
|
||||
provider.update(calendarSyncURI(), values, null, null)
|
||||
} catch (e: RemoteException) {
|
||||
throw CalendarStorageException("Couldn't write local (last known) CTag", e)
|
||||
}
|
||||
|
||||
override fun markNotDirty(flags: Int): Int {
|
||||
val values = ContentValues(1)
|
||||
values.put(LocalEvent.COLUMN_FLAGS, flags)
|
||||
return provider.update(eventsSyncURI(), values,
|
||||
"${Events.CALENDAR_ID}=? AND ${Events.DIRTY}=0 AND ${Events.ORIGINAL_ID} IS NULL",
|
||||
arrayOf(id.toString()))
|
||||
}
|
||||
|
||||
@Throws(CalendarStorageException::class)
|
||||
override fun removeNotDirtyMarked(flags: Int) =
|
||||
provider.delete(eventsSyncURI(),
|
||||
"${Events.CALENDAR_ID}=? AND ${Events.DIRTY}=0 AND ${Events.ORIGINAL_ID} IS NULL AND ${LocalEvent.COLUMN_FLAGS}=?",
|
||||
arrayOf(id.toString(), flags.toString()))
|
||||
|
||||
|
||||
fun processDirtyExceptions() {
|
||||
try {
|
||||
// process deleted exceptions
|
||||
Logger.log.info("Processing deleted exceptions")
|
||||
provider.query(
|
||||
syncAdapterURI(Events.CONTENT_URI),
|
||||
arrayOf(Events._ID, Events.ORIGINAL_ID, LocalEvent.COLUMN_SEQUENCE),
|
||||
"${Events.DELETED}!=0 AND ${Events.ORIGINAL_ID} IS NOT NULL", null, null)?.use { cursor ->
|
||||
while (cursor.moveToNext()) {
|
||||
Logger.log.fine("Found deleted exception, removing; then re-scheduling original event")
|
||||
val id = cursor.getLong(0) // can't be null (by definition)
|
||||
val originalID = cursor.getLong(1) // can't be null (by query)
|
||||
// process deleted exceptions
|
||||
Logger.log.info("Processing deleted exceptions")
|
||||
provider.query(
|
||||
syncAdapterURI(Events.CONTENT_URI),
|
||||
arrayOf(Events._ID, Events.ORIGINAL_ID, LocalEvent.COLUMN_SEQUENCE),
|
||||
"${Events.CALENDAR_ID}=? AND ${Events.DELETED}!=0 AND ${Events.ORIGINAL_ID} IS NOT NULL",
|
||||
arrayOf(id.toString()), null)?.use { cursor ->
|
||||
while (cursor.moveToNext()) {
|
||||
Logger.log.fine("Found deleted exception, removing and re-scheduling original event (if available)")
|
||||
val id = cursor.getLong(0) // can't be null (by definition)
|
||||
val originalID = cursor.getLong(1) // can't be null (by query)
|
||||
|
||||
val batch = BatchOperation(provider)
|
||||
val batch = BatchOperation(provider)
|
||||
|
||||
// get original event's SEQUENCE
|
||||
provider.query(
|
||||
syncAdapterURI(ContentUris.withAppendedId(Events.CONTENT_URI, originalID)),
|
||||
arrayOf(LocalEvent.COLUMN_SEQUENCE),
|
||||
null, null, null)?.use { cursor2 ->
|
||||
// get original event's SEQUENCE
|
||||
provider.query(
|
||||
syncAdapterURI(ContentUris.withAppendedId(Events.CONTENT_URI, originalID)),
|
||||
arrayOf(LocalEvent.COLUMN_SEQUENCE),
|
||||
null, null, null)?.use { cursor2 ->
|
||||
if (cursor2.moveToNext()) {
|
||||
// original event is available
|
||||
val originalSequence = if (cursor2.isNull(0)) 0 else cursor2.getInt(0)
|
||||
|
||||
// re-schedule original event and set it to DIRTY
|
||||
@@ -186,44 +175,43 @@ class LocalCalendar private constructor(
|
||||
.withValue(Events.DIRTY, 1)
|
||||
))
|
||||
}
|
||||
|
||||
// remove exception
|
||||
batch.enqueue(BatchOperation.Operation(
|
||||
ContentProviderOperation.newDelete(syncAdapterURI(ContentUris.withAppendedId(Events.CONTENT_URI, id)))
|
||||
))
|
||||
batch.commit()
|
||||
}
|
||||
}
|
||||
|
||||
// process dirty exceptions
|
||||
Logger.log.info("Processing dirty exceptions")
|
||||
provider.query(
|
||||
syncAdapterURI(Events.CONTENT_URI),
|
||||
arrayOf(Events._ID, Events.ORIGINAL_ID, LocalEvent.COLUMN_SEQUENCE),
|
||||
"${Events.DIRTY}!=0 AND ${Events.ORIGINAL_ID} IS NOT NULL", null, null)?.use { cursor ->
|
||||
while (cursor.moveToNext()) {
|
||||
Logger.log.fine("Found dirty exception, increasing SEQUENCE to re-schedule")
|
||||
val id = cursor.getLong(0) // can't be null (by definition)
|
||||
val originalID = cursor.getLong(1) // can't be null (by query)
|
||||
val sequence = if (cursor.isNull(2)) 0 else cursor.getInt(2)
|
||||
|
||||
val batch = BatchOperation(provider)
|
||||
// original event to DIRTY
|
||||
batch.enqueue(BatchOperation.Operation (
|
||||
ContentProviderOperation.newUpdate(syncAdapterURI(ContentUris.withAppendedId(Events.CONTENT_URI, originalID)))
|
||||
.withValue(Events.DIRTY, 1)
|
||||
))
|
||||
// increase SEQUENCE and set DIRTY to 0
|
||||
batch.enqueue(BatchOperation.Operation (
|
||||
ContentProviderOperation.newUpdate(syncAdapterURI(ContentUris.withAppendedId(Events.CONTENT_URI, id)))
|
||||
.withValue(LocalEvent.COLUMN_SEQUENCE, sequence + 1)
|
||||
.withValue(Events.DIRTY, 0)
|
||||
))
|
||||
batch.commit()
|
||||
}
|
||||
// completely remove deleted exception
|
||||
batch.enqueue(BatchOperation.Operation(
|
||||
ContentProviderOperation.newDelete(syncAdapterURI(ContentUris.withAppendedId(Events.CONTENT_URI, id)))
|
||||
))
|
||||
batch.commit()
|
||||
}
|
||||
}
|
||||
|
||||
// process dirty exceptions
|
||||
Logger.log.info("Processing dirty exceptions")
|
||||
provider.query(
|
||||
syncAdapterURI(Events.CONTENT_URI),
|
||||
arrayOf(Events._ID, Events.ORIGINAL_ID, LocalEvent.COLUMN_SEQUENCE),
|
||||
"${Events.CALENDAR_ID}=? AND ${Events.DIRTY}!=0 AND ${Events.ORIGINAL_ID} IS NOT NULL",
|
||||
arrayOf(id.toString()), null)?.use { cursor ->
|
||||
while (cursor.moveToNext()) {
|
||||
Logger.log.fine("Found dirty exception, increasing SEQUENCE to re-schedule")
|
||||
val id = cursor.getLong(0) // can't be null (by definition)
|
||||
val originalID = cursor.getLong(1) // can't be null (by query)
|
||||
val sequence = if (cursor.isNull(2)) 0 else cursor.getInt(2)
|
||||
|
||||
val batch = BatchOperation(provider)
|
||||
// original event to DIRTY
|
||||
batch.enqueue(BatchOperation.Operation (
|
||||
ContentProviderOperation.newUpdate(syncAdapterURI(ContentUris.withAppendedId(Events.CONTENT_URI, originalID)))
|
||||
.withValue(Events.DIRTY, 1)
|
||||
))
|
||||
// increase SEQUENCE and set DIRTY to 0
|
||||
batch.enqueue(BatchOperation.Operation (
|
||||
ContentProviderOperation.newUpdate(syncAdapterURI(ContentUris.withAppendedId(Events.CONTENT_URI, id)))
|
||||
.withValue(LocalEvent.COLUMN_SEQUENCE, sequence + 1)
|
||||
.withValue(Events.DIRTY, 0)
|
||||
))
|
||||
batch.commit()
|
||||
}
|
||||
} catch (e: RemoteException) {
|
||||
throw CalendarStorageException("Couldn't process locally modified exception", e)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -235,4 +223,4 @@ class LocalCalendar private constructor(
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -8,28 +8,31 @@
|
||||
|
||||
package at.bitfire.davdroid.resource
|
||||
|
||||
import at.bitfire.ical4android.CalendarStorageException
|
||||
import at.bitfire.vcard4android.ContactsStorageException
|
||||
import java.io.FileNotFoundException
|
||||
import at.bitfire.davdroid.model.SyncState
|
||||
|
||||
interface LocalCollection<out T: LocalResource> {
|
||||
interface LocalCollection<out T: LocalResource<*>> {
|
||||
|
||||
@Throws(CalendarStorageException::class, ContactsStorageException::class)
|
||||
fun getDeleted(): List<T>
|
||||
/** collection title (used for user notifications etc.) **/
|
||||
val title: String
|
||||
|
||||
@Throws(CalendarStorageException::class, ContactsStorageException::class)
|
||||
fun getWithoutFileName(): List<T>
|
||||
var lastSyncState: SyncState?
|
||||
|
||||
@Throws(CalendarStorageException::class, ContactsStorageException::class, FileNotFoundException::class)
|
||||
fun getDirty(): List<T>
|
||||
fun findDeleted(): List<T>
|
||||
fun findDirty(): List<T>
|
||||
|
||||
@Throws(CalendarStorageException::class, ContactsStorageException::class)
|
||||
fun getAll(): List<T>
|
||||
fun findByName(name: String): T?
|
||||
|
||||
@Throws(CalendarStorageException::class, ContactsStorageException::class)
|
||||
fun getCTag(): String?
|
||||
|
||||
@Throws(CalendarStorageException::class, ContactsStorageException::class)
|
||||
fun setCTag(cTag: String?)
|
||||
/**
|
||||
* Marks all entries which are not dirty with the given flags only.
|
||||
* @return number of marked entries
|
||||
**/
|
||||
fun markNotDirty(flags: Int): Int
|
||||
|
||||
/**
|
||||
* Removes all entries with are not dirty and are marked with exactly the given flags.
|
||||
* @return number of removed entries
|
||||
*/
|
||||
fun removeNotDirtyMarked(flags: Int): Int
|
||||
|
||||
}
|
||||
|
||||
@@ -23,129 +23,110 @@ import ezvcard.Ezvcard
|
||||
import java.io.FileNotFoundException
|
||||
import java.util.*
|
||||
|
||||
class LocalContact: AndroidContact, LocalResource {
|
||||
class LocalContact: AndroidContact, LocalAddress {
|
||||
|
||||
companion object {
|
||||
|
||||
init {
|
||||
Contact.productID = "+//IDN bitfire.at//DAVdroid/" + BuildConfig.VERSION_NAME + " ez-vcard/" + Ezvcard.VERSION
|
||||
}
|
||||
|
||||
val COLUMN_HASHCODE = ContactsContract.RawContacts.SYNC3
|
||||
|
||||
const val COLUMN_FLAGS = ContactsContract.RawContacts.SYNC4
|
||||
const val COLUMN_HASHCODE = ContactsContract.RawContacts.SYNC3
|
||||
}
|
||||
|
||||
private val cachedGroupMemberships = HashSet<Long>()
|
||||
private val groupMemberships = HashSet<Long>()
|
||||
|
||||
override var flags: Int = 0
|
||||
|
||||
constructor(addressBook: AndroidAddressBook<LocalContact,*>, id: Long, fileName: String?, eTag: String?):
|
||||
super(addressBook, id, fileName, eTag)
|
||||
|
||||
constructor(addressBook: AndroidAddressBook<LocalContact,*>, contact: Contact, fileName: String?, eTag: String?):
|
||||
super(addressBook, contact, fileName, eTag)
|
||||
|
||||
|
||||
@Throws(ContactsStorageException::class)
|
||||
fun resetDeleted() {
|
||||
val values = ContentValues(1)
|
||||
values.put(ContactsContract.RawContacts.DELETED, 0)
|
||||
try {
|
||||
addressBook.provider!!.update(rawContactSyncURI(), values, null, null)
|
||||
} catch(e: RemoteException) {
|
||||
throw ContactsStorageException("Couldn't clear deleted flag", e)
|
||||
}
|
||||
constructor(addressBook: AndroidAddressBook<LocalContact,*>, values: ContentValues)
|
||||
: super(addressBook, values) {
|
||||
flags = values.getAsInteger(COLUMN_FLAGS) ?: 0
|
||||
}
|
||||
|
||||
constructor(addressBook: AndroidAddressBook<LocalContact,*>, contact: Contact, fileName: String?, eTag: String?, flags: Int)
|
||||
: super(addressBook, contact, fileName, eTag) {
|
||||
this.flags = flags
|
||||
}
|
||||
|
||||
|
||||
override fun assignNameAndUID() {
|
||||
val uid = UUID.randomUUID().toString()
|
||||
val newFileName = "$uid.vcf"
|
||||
|
||||
val values = ContentValues(2)
|
||||
values.put(COLUMN_FILENAME, newFileName)
|
||||
values.put(COLUMN_UID, uid)
|
||||
addressBook.provider!!.update(rawContactSyncURI(), values, null, null)
|
||||
|
||||
fileName = newFileName
|
||||
}
|
||||
|
||||
override fun clearDirty(eTag: String?) {
|
||||
val values = ContentValues(3)
|
||||
values.put(COLUMN_ETAG, eTag)
|
||||
values.put(ContactsContract.RawContacts.DIRTY, 0)
|
||||
|
||||
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 hashCode = dataHashCode()
|
||||
values.put(COLUMN_HASHCODE, hashCode)
|
||||
Logger.log.finer("Clearing dirty flag with eTag = $eTag, contact hash = $hashCode")
|
||||
}
|
||||
|
||||
addressBook.provider!!.update(rawContactSyncURI(), values, null, null)
|
||||
|
||||
this.eTag = eTag
|
||||
}
|
||||
|
||||
override fun resetDeleted() {
|
||||
val values = ContentValues(1)
|
||||
values.put(ContactsContract.Groups.DELETED, 0)
|
||||
addressBook.provider!!.update(rawContactSyncURI(), values, null, null)
|
||||
}
|
||||
|
||||
@Throws(ContactsStorageException::class)
|
||||
fun resetDirty() {
|
||||
val values = ContentValues(1)
|
||||
values.put(ContactsContract.RawContacts.DIRTY, 0)
|
||||
try {
|
||||
addressBook.provider!!.update(rawContactSyncURI(), values, null, null)
|
||||
} catch(e: RemoteException) {
|
||||
throw ContactsStorageException("Couldn't clear dirty flag", e)
|
||||
}
|
||||
addressBook.provider!!.update(rawContactSyncURI(), values, null, null)
|
||||
}
|
||||
|
||||
@Throws(ContactsStorageException::class)
|
||||
override fun clearDirty(eTag: String?) {
|
||||
try {
|
||||
val values = ContentValues(3)
|
||||
values.put(COLUMN_ETAG, eTag)
|
||||
values.put(ContactsContract.RawContacts.DIRTY, 0)
|
||||
override fun updateFlags(flags: Int) {
|
||||
val values = ContentValues(1)
|
||||
values.put(LocalContact.COLUMN_FLAGS, flags)
|
||||
addressBook.provider!!.update(rawContactSyncURI(), values, null, null)
|
||||
|
||||
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 hashCode = dataHashCode()
|
||||
values.put(COLUMN_HASHCODE, hashCode)
|
||||
Logger.log.finer("Clearing dirty flag with eTag = $eTag, contact hash = $hashCode")
|
||||
}
|
||||
|
||||
addressBook.provider!!.update(rawContactSyncURI(), values, null, null)
|
||||
|
||||
this.eTag = eTag
|
||||
} catch(e: Exception) {
|
||||
throw ContactsStorageException("Couldn't clear dirty flag", e)
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(ContactsStorageException::class)
|
||||
override fun prepareForUpload() {
|
||||
try {
|
||||
val uid = UUID.randomUUID().toString()
|
||||
val newFileName = uid + ".vcf"
|
||||
|
||||
val values = ContentValues(2)
|
||||
values.put(COLUMN_FILENAME, newFileName)
|
||||
values.put(COLUMN_UID, uid)
|
||||
addressBook.provider!!.update(rawContactSyncURI(), values, null, null)
|
||||
|
||||
fileName = newFileName
|
||||
} catch(e: RemoteException) {
|
||||
throw ContactsStorageException("Couldn't update UID", e)
|
||||
}
|
||||
this.flags = flags
|
||||
}
|
||||
|
||||
|
||||
@Throws(ContactsStorageException::class)
|
||||
override fun populateData(mimeType: String, row: ContentValues) {
|
||||
when (mimeType) {
|
||||
CachedGroupMembership.CONTENT_ITEM_TYPE ->
|
||||
cachedGroupMemberships.add(row.getAsLong(CachedGroupMembership.GROUP_ID))
|
||||
cachedGroupMemberships += row.getAsLong(CachedGroupMembership.GROUP_ID)
|
||||
GroupMembership.CONTENT_ITEM_TYPE ->
|
||||
groupMemberships.add(row.getAsLong(GroupMembership.GROUP_ROW_ID))
|
||||
groupMemberships += row.getAsLong(GroupMembership.GROUP_ROW_ID)
|
||||
UnknownProperties.CONTENT_ITEM_TYPE ->
|
||||
try {
|
||||
contact!!.unknownProperties = row.getAsString(UnknownProperties.UNKNOWN_PROPERTIES)
|
||||
} catch(e: FileNotFoundException) {
|
||||
throw ContactsStorageException("Couldn't fetch data rows", e)
|
||||
}
|
||||
contact!!.unknownProperties = row.getAsString(UnknownProperties.UNKNOWN_PROPERTIES)
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(ContactsStorageException::class)
|
||||
override fun insertDataRows(batch: BatchOperation) {
|
||||
super.insertDataRows(batch)
|
||||
|
||||
try {
|
||||
contact!!.unknownProperties?.let { unknownProperties ->
|
||||
val op: BatchOperation.Operation
|
||||
val builder = ContentProviderOperation.newInsert(dataSyncURI())
|
||||
if (id == null)
|
||||
op = BatchOperation.Operation(builder, UnknownProperties.RAW_CONTACT_ID, 0)
|
||||
else {
|
||||
op = BatchOperation.Operation(builder)
|
||||
builder.withValue(UnknownProperties.RAW_CONTACT_ID, id)
|
||||
}
|
||||
builder .withValue(UnknownProperties.MIMETYPE, UnknownProperties.CONTENT_ITEM_TYPE)
|
||||
.withValue(UnknownProperties.UNKNOWN_PROPERTIES, unknownProperties)
|
||||
batch.enqueue(op)
|
||||
contact!!.unknownProperties?.let { unknownProperties ->
|
||||
val op: BatchOperation.Operation
|
||||
val builder = ContentProviderOperation.newInsert(dataSyncURI())
|
||||
if (id == null)
|
||||
op = BatchOperation.Operation(builder, UnknownProperties.RAW_CONTACT_ID, 0)
|
||||
else {
|
||||
op = BatchOperation.Operation(builder)
|
||||
builder.withValue(UnknownProperties.RAW_CONTACT_ID, id)
|
||||
}
|
||||
} catch(e: FileNotFoundException) {
|
||||
throw ContactsStorageException("Couldn't insert data rows", e)
|
||||
builder .withValue(UnknownProperties.MIMETYPE, UnknownProperties.CONTENT_ITEM_TYPE)
|
||||
.withValue(UnknownProperties.UNKNOWN_PROPERTIES, unknownProperties)
|
||||
batch.enqueue(op)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -154,7 +135,6 @@ class LocalContact: AndroidContact, LocalResource {
|
||||
* Attention: re-reads {@link #contact} from the database, discarding all changes in memory
|
||||
* @return hash code of contact data (including group memberships)
|
||||
*/
|
||||
@Throws(FileNotFoundException::class, ContactsStorageException::class)
|
||||
internal fun dataHashCode(): Int {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N || Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
|
||||
throw IllegalStateException("dataHashCode() should not be called on Android != 7")
|
||||
@@ -169,44 +149,34 @@ class LocalContact: AndroidContact, LocalResource {
|
||||
return dataHash xor groupHash
|
||||
}
|
||||
|
||||
@Throws(ContactsStorageException::class)
|
||||
fun updateHashCode(batch: BatchOperation?) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N || Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
|
||||
throw IllegalStateException("updateHashCode() should not be called on Android != 7")
|
||||
|
||||
val values = ContentValues(1)
|
||||
try {
|
||||
val hashCode = dataHashCode()
|
||||
Logger.log.fine("Storing contact hash = $hashCode")
|
||||
values.put(COLUMN_HASHCODE, hashCode)
|
||||
val hashCode = dataHashCode()
|
||||
Logger.log.fine("Storing contact hash = $hashCode")
|
||||
values.put(COLUMN_HASHCODE, hashCode)
|
||||
|
||||
if (batch == null)
|
||||
addressBook.provider!!.update(rawContactSyncURI(), values, null, null)
|
||||
else {
|
||||
val builder = ContentProviderOperation
|
||||
.newUpdate(rawContactSyncURI())
|
||||
.withValues(values)
|
||||
batch.enqueue(BatchOperation.Operation(builder))
|
||||
}
|
||||
} catch(e: Exception) {
|
||||
throw ContactsStorageException("Couldn't store contact checksum", e)
|
||||
if (batch == null)
|
||||
addressBook.provider!!.update(rawContactSyncURI(), values, null, null)
|
||||
else {
|
||||
val builder = ContentProviderOperation
|
||||
.newUpdate(rawContactSyncURI())
|
||||
.withValues(values)
|
||||
batch.enqueue(BatchOperation.Operation(builder))
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(ContactsStorageException::class)
|
||||
fun getLastHashCode(): Int {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N || Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
|
||||
throw IllegalStateException("getLastHashCode() should not be called on Android != 7")
|
||||
|
||||
try {
|
||||
addressBook.provider!!.query(rawContactSyncURI(), arrayOf(COLUMN_HASHCODE), null, null, null)?.use { c ->
|
||||
if (c.moveToNext() && !c.isNull(0))
|
||||
return c.getInt(0)
|
||||
}
|
||||
return 0
|
||||
} catch(e: RemoteException) {
|
||||
throw ContactsStorageException("Could't read last hash code", e)
|
||||
addressBook.provider!!.query(rawContactSyncURI(), arrayOf(COLUMN_HASHCODE), null, null, null)?.use { c ->
|
||||
if (c.moveToNext() && !c.isNull(0))
|
||||
return c.getInt(0)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
|
||||
@@ -217,7 +187,7 @@ class LocalContact: AndroidContact, LocalResource {
|
||||
.withValue(GroupMembership.RAW_CONTACT_ID, id)
|
||||
.withValue(GroupMembership.GROUP_ROW_ID, groupID)
|
||||
))
|
||||
groupMemberships.add(groupID)
|
||||
groupMemberships += groupID
|
||||
|
||||
batch.enqueue(BatchOperation.Operation(
|
||||
ContentProviderOperation.newInsert(dataSyncURI())
|
||||
@@ -226,7 +196,7 @@ class LocalContact: AndroidContact, LocalResource {
|
||||
.withValue(CachedGroupMembership.GROUP_ID, groupID)
|
||||
.withYieldAllowed(true)
|
||||
))
|
||||
cachedGroupMemberships.add(groupID)
|
||||
cachedGroupMemberships += groupID
|
||||
}
|
||||
|
||||
fun removeGroupMemberships(batch: BatchOperation) {
|
||||
@@ -247,10 +217,9 @@ class LocalContact: AndroidContact, LocalResource {
|
||||
* Cached memberships are kept in sync with memberships by DAVdroid and are used to determine
|
||||
* whether a membership has been deleted/added when a raw contact is dirty.
|
||||
* @return set of {@link GroupMembership#GROUP_ROW_ID} (may be empty)
|
||||
* @throws ContactsStorageException on contact provider errors
|
||||
* @throws FileNotFoundException if the current contact can't be found
|
||||
* @throws FileNotFoundException if the current contact can't be found
|
||||
* @throws RemoteException on contacts provider errors
|
||||
*/
|
||||
@Throws(FileNotFoundException::class, ContactsStorageException::class)
|
||||
fun getCachedGroupMemberships(): Set<Long> {
|
||||
contact
|
||||
return cachedGroupMemberships
|
||||
@@ -259,26 +228,26 @@ class LocalContact: AndroidContact, LocalResource {
|
||||
/**
|
||||
* Returns the IDs of all groups the contact is member of.
|
||||
* @return set of {@link GroupMembership#GROUP_ROW_ID}s (may be empty)
|
||||
* @throws ContactsStorageException on contact provider errors
|
||||
* @throws FileNotFoundException if the current contact can't be found
|
||||
* @throws FileNotFoundException if the current contact can't be found
|
||||
* @throws RemoteException on contacts provider errors
|
||||
*/
|
||||
@Throws(FileNotFoundException::class, ContactsStorageException::class)
|
||||
fun getGroupMemberships(): Set<Long> {
|
||||
contact
|
||||
return groupMemberships
|
||||
}
|
||||
|
||||
|
||||
// data rows
|
||||
override fun buildContact(builder: ContentProviderOperation.Builder, update: Boolean) {
|
||||
builder.withValue(COLUMN_FLAGS, flags)
|
||||
super.buildContact(builder, update)
|
||||
}
|
||||
|
||||
// factory
|
||||
|
||||
object Factory: AndroidContactFactory<LocalContact> {
|
||||
|
||||
override fun newInstance(addressBook: AndroidAddressBook<LocalContact, *>, id: Long, fileName: String?, eTag: String?) =
|
||||
LocalContact(addressBook, id, fileName, eTag)
|
||||
|
||||
override fun newInstance(addressBook: AndroidAddressBook<LocalContact, *>, contact: Contact, fileName: String?, eTag: String?) =
|
||||
LocalContact(addressBook, contact, fileName, eTag)
|
||||
|
||||
override fun fromProvider(addressBook: AndroidAddressBook<LocalContact, *>, values: ContentValues) =
|
||||
LocalContact(addressBook, values)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -10,68 +10,59 @@ package at.bitfire.davdroid.resource
|
||||
|
||||
import android.content.ContentProviderOperation
|
||||
import android.content.ContentValues
|
||||
import android.os.Build
|
||||
import android.provider.CalendarContract
|
||||
import android.provider.CalendarContract.Events
|
||||
import at.bitfire.davdroid.BuildConfig
|
||||
import at.bitfire.ical4android.*
|
||||
import net.fortuna.ical4j.model.property.ProdId
|
||||
import java.io.FileNotFoundException
|
||||
import java.util.*
|
||||
|
||||
class LocalEvent: AndroidEvent, LocalResource {
|
||||
class LocalEvent: AndroidEvent, LocalResource<Event> {
|
||||
|
||||
companion object {
|
||||
init {
|
||||
iCalendar.prodId = ProdId("+//IDN bitfire.at//DAVdroid/" + BuildConfig.VERSION_NAME + " ical4j/2.x")
|
||||
ICalendar.prodId = ProdId("+//IDN bitfire.at//DAVdroid/" + BuildConfig.VERSION_NAME + " ical4j/" + Constants.ical4jVersion)
|
||||
}
|
||||
|
||||
val COLUMN_ETAG = CalendarContract.Events.SYNC_DATA1
|
||||
val COLUMN_UID = if (Build.VERSION.SDK_INT >= 17) Events.UID_2445 else Events.SYNC_DATA2
|
||||
val COLUMN_SEQUENCE = CalendarContract.Events.SYNC_DATA3
|
||||
const val COLUMN_ETAG = CalendarContract.Events.SYNC_DATA1
|
||||
const val COLUMN_FLAGS = CalendarContract.Events.SYNC_DATA2
|
||||
const val COLUMN_SEQUENCE = CalendarContract.Events.SYNC_DATA3
|
||||
}
|
||||
|
||||
override var fileName: String? = null
|
||||
private set
|
||||
|
||||
override var eTag: String? = null
|
||||
|
||||
override var flags: Int = 0
|
||||
private set
|
||||
|
||||
var weAreOrganizer = true
|
||||
|
||||
|
||||
constructor(calendar: AndroidCalendar<*>, event: Event, fileName: String?, eTag: String?): super(calendar, event) {
|
||||
constructor(calendar: AndroidCalendar<*>, event: Event, fileName: String?, eTag: String?, flags: Int): super(calendar, event) {
|
||||
this.fileName = fileName
|
||||
this.eTag = eTag
|
||||
this.flags = flags
|
||||
}
|
||||
|
||||
private constructor(calendar: AndroidCalendar<*>, id: Long, baseInfo: ContentValues?): super(calendar, id, baseInfo) {
|
||||
baseInfo?.let {
|
||||
fileName = it.getAsString(Events._SYNC_ID)
|
||||
eTag = it.getAsString(COLUMN_ETAG)
|
||||
}
|
||||
private constructor(calendar: AndroidCalendar<*>, values: ContentValues): super(calendar, values) {
|
||||
fileName = values.getAsString(Events._SYNC_ID)
|
||||
eTag = values.getAsString(COLUMN_ETAG)
|
||||
flags = values.getAsInteger(COLUMN_FLAGS) ?: 0
|
||||
}
|
||||
|
||||
|
||||
/* process LocalEvent-specific fields */
|
||||
|
||||
@Throws(FileNotFoundException::class, CalendarStorageException::class)
|
||||
override fun populateEvent(row: ContentValues) {
|
||||
super.populateEvent(row)
|
||||
val event = requireNotNull(event)
|
||||
|
||||
fileName = row.getAsString(Events._SYNC_ID)
|
||||
eTag = row.getAsString(COLUMN_ETAG)
|
||||
event.uid = row.getAsString(COLUMN_UID)
|
||||
|
||||
event.uid = row.getAsString(Events.UID_2445)
|
||||
event.sequence = row.getAsInteger(COLUMN_SEQUENCE)
|
||||
if (Build.VERSION.SDK_INT >= 17) {
|
||||
val isOrganizer = row.getAsInteger(Events.IS_ORGANIZER)
|
||||
weAreOrganizer = isOrganizer != null && isOrganizer != 0
|
||||
} else {
|
||||
val organizer = row.getAsString(Events.ORGANIZER)
|
||||
weAreOrganizer = organizer == null || organizer == calendar.account.name
|
||||
}
|
||||
|
||||
val isOrganizer = row.getAsInteger(Events.IS_ORGANIZER)
|
||||
weAreOrganizer = isOrganizer != null && isOrganizer != 0
|
||||
}
|
||||
|
||||
@Throws(FileNotFoundException::class, CalendarStorageException::class)
|
||||
override fun buildEvent(recurrence: Event?, builder: ContentProviderOperation.Builder) {
|
||||
super.buildEvent(recurrence, builder)
|
||||
val event = requireNotNull(event)
|
||||
@@ -79,10 +70,11 @@ class LocalEvent: AndroidEvent, LocalResource {
|
||||
val buildException = recurrence != null
|
||||
val eventToBuild = recurrence ?: event
|
||||
|
||||
builder .withValue(COLUMN_UID, event.uid)
|
||||
builder .withValue(Events.UID_2445, event.uid)
|
||||
.withValue(COLUMN_SEQUENCE, eventToBuild.sequence)
|
||||
.withValue(CalendarContract.Events.DIRTY, 0)
|
||||
.withValue(CalendarContract.Events.DELETED, 0)
|
||||
.withValue(LocalEvent.COLUMN_FLAGS, flags)
|
||||
|
||||
if (buildException)
|
||||
builder .withValue(Events.ORIGINAL_SYNC_ID, fileName)
|
||||
@@ -92,57 +84,48 @@ class LocalEvent: AndroidEvent, LocalResource {
|
||||
}
|
||||
|
||||
|
||||
/* custom queries */
|
||||
|
||||
@Throws(CalendarStorageException::class)
|
||||
override fun prepareForUpload() {
|
||||
try {
|
||||
var uid: String? = null
|
||||
calendar.provider.query(eventSyncURI(), arrayOf(COLUMN_UID), null, null, null)?.use { cursor ->
|
||||
if (cursor.moveToNext())
|
||||
uid = cursor.getString(0)
|
||||
}
|
||||
if (uid == null)
|
||||
uid = UUID.randomUUID().toString()
|
||||
|
||||
val newFileName = "$uid.ics"
|
||||
|
||||
val values = ContentValues(2)
|
||||
values.put(Events._SYNC_ID, newFileName)
|
||||
values.put(COLUMN_UID, uid)
|
||||
calendar.provider.update(eventSyncURI(), values, null, null)
|
||||
|
||||
fileName = newFileName
|
||||
event!!.uid = uid
|
||||
|
||||
} catch(e: Exception) {
|
||||
throw CalendarStorageException("Couldn't update UID", e)
|
||||
override fun assignNameAndUID() {
|
||||
var uid: String? = null
|
||||
calendar.provider.query(eventSyncURI(), arrayOf(Events.UID_2445), null, null, null)?.use { cursor ->
|
||||
if (cursor.moveToNext())
|
||||
uid = cursor.getString(0)
|
||||
}
|
||||
if (uid == null)
|
||||
uid = UUID.randomUUID().toString()
|
||||
|
||||
val newFileName = "$uid.ics"
|
||||
|
||||
val values = ContentValues(2)
|
||||
values.put(Events._SYNC_ID, newFileName)
|
||||
values.put(Events.UID_2445, uid)
|
||||
calendar.provider.update(eventSyncURI(), values, null, null)
|
||||
|
||||
fileName = newFileName
|
||||
event!!.uid = uid
|
||||
}
|
||||
|
||||
@Throws(CalendarStorageException::class)
|
||||
override fun clearDirty(eTag: String?) {
|
||||
try {
|
||||
val values = ContentValues(2)
|
||||
values.put(CalendarContract.Events.DIRTY, 0)
|
||||
values.put(COLUMN_ETAG, eTag)
|
||||
values.put(COLUMN_SEQUENCE, event!!.sequence);
|
||||
calendar.provider.update(eventSyncURI(), values, null, null)
|
||||
val values = ContentValues(2)
|
||||
values.put(CalendarContract.Events.DIRTY, 0)
|
||||
values.put(COLUMN_ETAG, eTag)
|
||||
values.put(COLUMN_SEQUENCE, event!!.sequence)
|
||||
calendar.provider.update(eventSyncURI(), values, null, null)
|
||||
|
||||
this.eTag = eTag
|
||||
} catch (e: Exception) {
|
||||
throw CalendarStorageException("Couldn't update UID", e)
|
||||
}
|
||||
this.eTag = eTag
|
||||
}
|
||||
|
||||
override fun updateFlags(flags: Int) {
|
||||
val values = ContentValues(1)
|
||||
values.put(COLUMN_FLAGS, flags)
|
||||
calendar.provider.update(eventSyncURI(), values, null, null)
|
||||
|
||||
this.flags = flags
|
||||
}
|
||||
|
||||
|
||||
object Factory: AndroidEventFactory<LocalEvent> {
|
||||
|
||||
override fun newInstance(calendar: AndroidCalendar<*>, id: Long, baseInfo: ContentValues?) =
|
||||
LocalEvent(calendar, id, baseInfo)
|
||||
|
||||
override fun newInstance(calendar: AndroidCalendar<*>, event: Event) =
|
||||
LocalEvent(calendar, event, null, null)
|
||||
|
||||
override fun fromProvider(calendar: AndroidCalendar<*>, values: ContentValues) =
|
||||
LocalEvent(calendar, values)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ package at.bitfire.davdroid.resource
|
||||
import android.content.ContentProviderOperation
|
||||
import android.content.ContentUris
|
||||
import android.content.ContentValues
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Parcel
|
||||
import android.os.RemoteException
|
||||
@@ -21,116 +22,126 @@ import android.provider.ContactsContract.RawContacts
|
||||
import android.provider.ContactsContract.RawContacts.Data
|
||||
import at.bitfire.dav4android.Constants
|
||||
import at.bitfire.vcard4android.*
|
||||
import java.io.FileNotFoundException
|
||||
import java.util.*
|
||||
import java.util.logging.Level
|
||||
|
||||
class LocalGroup: AndroidGroup, LocalResource {
|
||||
class LocalGroup: AndroidGroup, LocalAddress {
|
||||
|
||||
companion object {
|
||||
|
||||
const val COLUMN_FLAGS = Groups.SYNC4
|
||||
|
||||
/** marshaled list of member UIDs, as sent by server */
|
||||
val COLUMN_PENDING_MEMBERS = Groups.SYNC3
|
||||
const val COLUMN_PENDING_MEMBERS = Groups.SYNC3
|
||||
|
||||
/**
|
||||
* Processes all groups with non-null {@link #COLUMN_PENDING_MEMBERS}: the pending memberships
|
||||
* are (if possible) applied, keeping cached memberships in sync.
|
||||
* @param addressBook address book to take groups from
|
||||
* @throws ContactsStorageException on contact provider errors
|
||||
*/
|
||||
@JvmStatic
|
||||
@Throws(ContactsStorageException::class)
|
||||
fun applyPendingMemberships(addressBook: LocalAddressBook) {
|
||||
try {
|
||||
addressBook.provider!!.query(
|
||||
addressBook.syncAdapterURI(Groups.CONTENT_URI),
|
||||
arrayOf(Groups._ID, COLUMN_PENDING_MEMBERS),
|
||||
"$COLUMN_PENDING_MEMBERS IS NOT NULL", null,
|
||||
null
|
||||
)?.use { cursor ->
|
||||
val batch = BatchOperation(addressBook.provider)
|
||||
while (cursor.moveToNext()) {
|
||||
val id = cursor.getLong(0)
|
||||
Constants.log.fine("Assigning members to group $id")
|
||||
addressBook.provider!!.query(
|
||||
addressBook.groupsSyncUri(),
|
||||
arrayOf(Groups._ID, COLUMN_PENDING_MEMBERS),
|
||||
"$COLUMN_PENDING_MEMBERS IS NOT NULL", null,
|
||||
null
|
||||
)?.use { cursor ->
|
||||
val batch = BatchOperation(addressBook.provider)
|
||||
while (cursor.moveToNext()) {
|
||||
val id = cursor.getLong(0)
|
||||
Constants.log.fine("Assigning members to group $id")
|
||||
|
||||
// required for workaround for Android 7 which sets DIRTY flag when only meta-data is changed
|
||||
val changeContactIDs = HashSet<Long>()
|
||||
// required for workaround for Android 7 which sets DIRTY flag when only meta-data is changed
|
||||
val changeContactIDs = HashSet<Long>()
|
||||
|
||||
// delete all memberships and cached memberships for this group
|
||||
for (contact in addressBook.getByGroupMembership(id)) {
|
||||
contact.removeGroupMemberships(batch)
|
||||
changeContactIDs.add(contact.id!!)
|
||||
}
|
||||
|
||||
// extract list of member UIDs
|
||||
val members = LinkedList<String>()
|
||||
val raw = cursor.getBlob(1)
|
||||
val parcel = Parcel.obtain()
|
||||
try {
|
||||
parcel.unmarshall(raw, 0, raw.size)
|
||||
parcel.setDataPosition(0)
|
||||
parcel.readStringList(members)
|
||||
} finally {
|
||||
parcel.recycle()
|
||||
}
|
||||
|
||||
// insert memberships
|
||||
for (uid in members) {
|
||||
Constants.log.fine("Assigning member: $uid")
|
||||
try {
|
||||
val member = addressBook.findContactByUID(uid)
|
||||
member.addToGroup(batch, id)
|
||||
changeContactIDs.add(member.id!!)
|
||||
} catch(e: FileNotFoundException) {
|
||||
Constants.log.log(Level.WARNING, "Group member not found: $uid", e)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
changeContactIDs
|
||||
.map { LocalContact(addressBook, it, null, null) }
|
||||
.forEach { it.updateHashCode(batch) }
|
||||
|
||||
// remove pending memberships
|
||||
batch.enqueue(BatchOperation.Operation(
|
||||
ContentProviderOperation.newUpdate(addressBook.syncAdapterURI(ContentUris.withAppendedId(Groups.CONTENT_URI, id)))
|
||||
.withValue(COLUMN_PENDING_MEMBERS, null)
|
||||
.withYieldAllowed(true)
|
||||
))
|
||||
|
||||
batch.commit()
|
||||
// delete all memberships and cached memberships for this group
|
||||
for (contact in addressBook.getByGroupMembership(id)) {
|
||||
contact.removeGroupMemberships(batch)
|
||||
changeContactIDs += contact.id!!
|
||||
}
|
||||
|
||||
// extract list of member UIDs
|
||||
val members = LinkedList<String>()
|
||||
val raw = cursor.getBlob(1)
|
||||
val parcel = Parcel.obtain()
|
||||
try {
|
||||
parcel.unmarshall(raw, 0, raw.size)
|
||||
parcel.setDataPosition(0)
|
||||
parcel.readStringList(members)
|
||||
} finally {
|
||||
parcel.recycle()
|
||||
}
|
||||
|
||||
// insert memberships
|
||||
for (uid in members) {
|
||||
Constants.log.fine("Assigning member: $uid")
|
||||
addressBook.findContactByUID(uid)?.let { member ->
|
||||
member.addToGroup(batch, id)
|
||||
changeContactIDs += member.id!!
|
||||
} ?: Constants.log.warning("Group member not found: $uid")
|
||||
}
|
||||
|
||||
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
|
||||
changeContactIDs
|
||||
.map { addressBook.findContactByID(it) }
|
||||
.forEach { it.updateHashCode(batch) }
|
||||
|
||||
// remove pending memberships
|
||||
batch.enqueue(BatchOperation.Operation(
|
||||
ContentProviderOperation.newUpdate(addressBook.syncAdapterURI(ContentUris.withAppendedId(Groups.CONTENT_URI, id)))
|
||||
.withValue(COLUMN_PENDING_MEMBERS, null)
|
||||
.withYieldAllowed(true)
|
||||
))
|
||||
|
||||
batch.commit()
|
||||
}
|
||||
} catch(e: RemoteException) {
|
||||
throw ContactsStorageException("Couldn't get pending memberships", e)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
constructor(addressBook: AndroidAddressBook<out AndroidContact, LocalGroup>, id: Long, fileName: String?, eTag: String?):
|
||||
super(addressBook, id, fileName, eTag)
|
||||
|
||||
constructor(addressBook: AndroidAddressBook<out AndroidContact, LocalGroup>, contact: Contact, fileName: String?, eTag: String?):
|
||||
super(addressBook, contact, fileName, eTag)
|
||||
override var flags: Int = 0
|
||||
|
||||
|
||||
@Throws(ContactsStorageException::class)
|
||||
fun resetDeleted() {
|
||||
val uri = ContentUris.withAppendedId(addressBook.syncAdapterURI(ContactsContract.Groups.CONTENT_URI), requireNotNull(id))
|
||||
|
||||
val values = ContentValues(1)
|
||||
values.put(ContactsContract.Groups.DELETED, 0)
|
||||
try {
|
||||
addressBook.provider!!.update(uri, values, null, null)
|
||||
} catch(e: RemoteException) {
|
||||
throw ContactsStorageException("Couldn't clear deleted flag", e)
|
||||
}
|
||||
constructor(addressBook: AndroidAddressBook<out AndroidContact, LocalGroup>, values: ContentValues)
|
||||
: super(addressBook, values) {
|
||||
flags = values.getAsInteger(COLUMN_FLAGS) ?: 0
|
||||
}
|
||||
|
||||
constructor(addressBook: AndroidAddressBook<out AndroidContact, LocalGroup>, contact: Contact, fileName: String?, eTag: String?, flags: Int)
|
||||
: super(addressBook, contact, fileName, eTag) {
|
||||
this.flags = flags
|
||||
}
|
||||
|
||||
|
||||
override fun contentValues(): ContentValues {
|
||||
val values = super.contentValues()
|
||||
values.put(COLUMN_FLAGS, flags)
|
||||
|
||||
val members = Parcel.obtain()
|
||||
try {
|
||||
members.writeStringList(contact!!.members)
|
||||
values.put(COLUMN_PENDING_MEMBERS, members.marshall())
|
||||
} finally {
|
||||
members.recycle()
|
||||
}
|
||||
return values
|
||||
}
|
||||
|
||||
|
||||
override fun assignNameAndUID() {
|
||||
val uid = UUID.randomUUID().toString()
|
||||
val newFileName = "$uid.vcf"
|
||||
|
||||
val values = ContentValues(2)
|
||||
values.put(COLUMN_FILENAME, newFileName)
|
||||
values.put(COLUMN_UID, uid)
|
||||
update(values)
|
||||
|
||||
fileName = newFileName
|
||||
}
|
||||
|
||||
@Throws(ContactsStorageException::class)
|
||||
override fun clearDirty(eTag: String?) {
|
||||
val id = requireNotNull(id)
|
||||
|
||||
@@ -165,38 +176,9 @@ class LocalGroup: AndroidGroup, LocalResource {
|
||||
batch.commit()
|
||||
}
|
||||
|
||||
@Throws(ContactsStorageException::class)
|
||||
override fun prepareForUpload() {
|
||||
val uid = UUID.randomUUID().toString()
|
||||
val newFileName = "$uid.vcf"
|
||||
|
||||
val values = ContentValues(2)
|
||||
values.put(COLUMN_FILENAME, newFileName)
|
||||
values.put(COLUMN_UID, uid)
|
||||
update(values)
|
||||
|
||||
fileName = newFileName
|
||||
}
|
||||
|
||||
@Throws(FileNotFoundException::class, ContactsStorageException::class)
|
||||
override fun contentValues(): ContentValues {
|
||||
val values = super.contentValues()
|
||||
|
||||
val members = Parcel.obtain()
|
||||
try {
|
||||
members.writeStringList(contact!!.members)
|
||||
values.put(COLUMN_PENDING_MEMBERS, members.marshall())
|
||||
} finally {
|
||||
members.recycle()
|
||||
}
|
||||
return values
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Marks all members of the current group as dirty.
|
||||
*/
|
||||
@Throws(ContactsStorageException::class)
|
||||
fun markMembersDirty() {
|
||||
val batch = BatchOperation(addressBook.provider!!)
|
||||
|
||||
@@ -210,31 +192,45 @@ class LocalGroup: AndroidGroup, LocalResource {
|
||||
batch.commit()
|
||||
}
|
||||
|
||||
override fun resetDeleted() {
|
||||
val values = ContentValues(1)
|
||||
values.put(Groups.DELETED, 0)
|
||||
addressBook.provider!!.update(groupSyncUri(), values, null, null)
|
||||
}
|
||||
|
||||
override fun updateFlags(flags: Int) {
|
||||
val values = ContentValues(1)
|
||||
values.put(LocalGroup.COLUMN_FLAGS, flags)
|
||||
addressBook.provider!!.update(groupSyncUri(), values, null, null)
|
||||
|
||||
this.flags = flags
|
||||
}
|
||||
|
||||
|
||||
// helpers
|
||||
|
||||
private fun groupSyncUri(): Uri {
|
||||
val id = requireNotNull(id)
|
||||
return ContentUris.withAppendedId(addressBook.groupsSyncUri(), id)
|
||||
}
|
||||
|
||||
/**
|
||||
* Lists all members of this group.
|
||||
* @return list of all members' raw contact IDs
|
||||
* @throws ContactsStorageException on contact provider errorst
|
||||
* @throws RemoteException on contact provider errors
|
||||
*/
|
||||
@Throws(ContactsStorageException::class)
|
||||
internal fun getMembers(): List<Long> {
|
||||
val id = requireNotNull(id)
|
||||
val members = LinkedList<Long>()
|
||||
try {
|
||||
addressBook.provider!!.query(
|
||||
addressBook.syncAdapterURI(ContactsContract.Data.CONTENT_URI),
|
||||
arrayOf(Data.RAW_CONTACT_ID),
|
||||
"${GroupMembership.MIMETYPE}=? AND ${GroupMembership.GROUP_ROW_ID}=?",
|
||||
arrayOf(GroupMembership.CONTENT_ITEM_TYPE, id.toString()),
|
||||
null
|
||||
)?.use { cursor ->
|
||||
while (cursor.moveToNext())
|
||||
members.add(cursor.getLong(0))
|
||||
}
|
||||
} catch(e: RemoteException) {
|
||||
throw ContactsStorageException("Couldn't list group members", e)
|
||||
addressBook.provider!!.query(
|
||||
addressBook.syncAdapterURI(ContactsContract.Data.CONTENT_URI),
|
||||
arrayOf(Data.RAW_CONTACT_ID),
|
||||
"${GroupMembership.MIMETYPE}=? AND ${GroupMembership.GROUP_ROW_ID}=?",
|
||||
arrayOf(GroupMembership.CONTENT_ITEM_TYPE, id.toString()),
|
||||
null
|
||||
)?.use { cursor ->
|
||||
while (cursor.moveToNext())
|
||||
members += cursor.getLong(0)
|
||||
}
|
||||
return members
|
||||
}
|
||||
@@ -243,13 +239,8 @@ class LocalGroup: AndroidGroup, LocalResource {
|
||||
// factory
|
||||
|
||||
object Factory: AndroidGroupFactory<LocalGroup> {
|
||||
|
||||
override fun newInstance(addressBook: AndroidAddressBook<out AndroidContact, LocalGroup>, id: Long, fileName: String?, eTag: String?) =
|
||||
LocalGroup(addressBook, id, fileName, eTag)
|
||||
|
||||
override fun newInstance(addressBook: AndroidAddressBook<out AndroidContact, LocalGroup>, contact: Contact, fileName: String?, eTag: String?) =
|
||||
LocalGroup(addressBook, contact, fileName, eTag)
|
||||
|
||||
override fun fromProvider(addressBook: AndroidAddressBook<out AndroidContact, LocalGroup>, values: ContentValues) =
|
||||
LocalGroup(addressBook, values)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -8,23 +8,50 @@
|
||||
|
||||
package at.bitfire.davdroid.resource
|
||||
|
||||
import at.bitfire.ical4android.CalendarStorageException
|
||||
import at.bitfire.vcard4android.ContactsStorageException
|
||||
import android.net.Uri
|
||||
|
||||
interface LocalResource {
|
||||
interface LocalResource<in TData: Any> {
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Resource is present on remote server. This flag is used to identify resources
|
||||
* which are not present on the remote server anymore and can be deleted at the end
|
||||
* of the synchronization.
|
||||
*/
|
||||
const val FLAG_REMOTELY_PRESENT = 1
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Unique ID which identifies the resource in the local storage. May be null if the
|
||||
* resource has not been saved yet.
|
||||
*/
|
||||
val id: Long?
|
||||
|
||||
var fileName: String?
|
||||
val fileName: String?
|
||||
var eTag: String?
|
||||
val flags: Int
|
||||
|
||||
@Throws(CalendarStorageException::class, ContactsStorageException::class)
|
||||
fun assignNameAndUID()
|
||||
fun clearDirty(eTag: String?)
|
||||
fun updateFlags(flags: Int)
|
||||
|
||||
/**
|
||||
* Adds the data object to the content provider and ensures that the dirty flag is clear.
|
||||
* @return content URI of the created row (e.g. event URI)
|
||||
*/
|
||||
fun add(): Uri
|
||||
|
||||
/**
|
||||
* Updates the data object in the content provider and ensures that the dirty flag is clear.
|
||||
* @return content URI of the updated row (e.g. event URI)
|
||||
*/
|
||||
fun update(data: TData): Uri
|
||||
|
||||
/**
|
||||
* Deletes the data object from the content provider.
|
||||
* @return number of affected rows
|
||||
*/
|
||||
fun delete(): Int
|
||||
|
||||
@Throws(CalendarStorageException::class, ContactsStorageException::class)
|
||||
fun prepareForUpload()
|
||||
|
||||
@Throws(CalendarStorageException::class, ContactsStorageException::class)
|
||||
fun clearDirty(eTag: String?)
|
||||
|
||||
}
|
||||
@@ -10,108 +10,93 @@ package at.bitfire.davdroid.resource
|
||||
|
||||
import android.content.ContentProviderOperation
|
||||
import android.content.ContentValues
|
||||
import android.provider.CalendarContract.Events
|
||||
import at.bitfire.ical4android.*
|
||||
import org.dmfs.provider.tasks.TaskContract.Tasks
|
||||
import java.io.FileNotFoundException
|
||||
import java.text.ParseException
|
||||
import at.bitfire.ical4android.AndroidTask
|
||||
import at.bitfire.ical4android.AndroidTaskFactory
|
||||
import at.bitfire.ical4android.AndroidTaskList
|
||||
import at.bitfire.ical4android.Task
|
||||
import org.dmfs.tasks.contract.TaskContract.Tasks
|
||||
import java.util.*
|
||||
|
||||
class LocalTask: AndroidTask, LocalResource {
|
||||
class LocalTask: AndroidTask, LocalResource<Task> {
|
||||
|
||||
companion object {
|
||||
val COLUMN_ETAG = Tasks.SYNC1
|
||||
val COLUMN_UID = Tasks.SYNC2
|
||||
val COLUMN_SEQUENCE = Tasks.SYNC3
|
||||
const val COLUMN_ETAG = Tasks.SYNC1
|
||||
const val COLUMN_FLAGS = Tasks.SYNC2
|
||||
}
|
||||
|
||||
override var fileName: String? = null
|
||||
override var eTag: String? = null
|
||||
|
||||
constructor(taskList: AndroidTaskList<*>, task: Task, fileName: String?, eTag: String?): super(taskList, task) {
|
||||
override var flags = 0
|
||||
private set
|
||||
|
||||
|
||||
constructor(taskList: AndroidTaskList<*>, task: Task, fileName: String?, eTag: String?, flags: Int)
|
||||
: super(taskList, task) {
|
||||
this.fileName = fileName
|
||||
this.eTag = eTag
|
||||
this.flags = flags
|
||||
}
|
||||
|
||||
private constructor(taskList: AndroidTaskList<*>, id: Long, baseInfo: ContentValues?): super(taskList, id) {
|
||||
baseInfo?.let {
|
||||
fileName = it.getAsString(Events._SYNC_ID)
|
||||
eTag = it.getAsString(COLUMN_ETAG)
|
||||
}
|
||||
private constructor(taskList: AndroidTaskList<*>, values: ContentValues): super(taskList) {
|
||||
id = values.getAsLong(Tasks._ID)
|
||||
fileName = values.getAsString(Tasks._SYNC_ID)
|
||||
eTag = values.getAsString(COLUMN_ETAG)
|
||||
flags = values.getAsInteger(COLUMN_FLAGS) ?: 0
|
||||
}
|
||||
|
||||
|
||||
/* process LocalTask-specific fields */
|
||||
|
||||
@Throws(ParseException::class)
|
||||
override fun populateTask(values: ContentValues) {
|
||||
super.populateTask(values)
|
||||
|
||||
fileName = values.getAsString(Events._SYNC_ID)
|
||||
eTag = values.getAsString(COLUMN_ETAG)
|
||||
|
||||
val task = requireNotNull(task)
|
||||
task.uid = values.getAsString(COLUMN_UID)
|
||||
task.sequence = values.getAsInteger(COLUMN_SEQUENCE)
|
||||
}
|
||||
|
||||
@Throws(FileNotFoundException::class, CalendarStorageException::class)
|
||||
override fun buildTask(builder: ContentProviderOperation.Builder, update: Boolean) {
|
||||
super.buildTask(builder, update)
|
||||
val task = requireNotNull(task)
|
||||
|
||||
builder .withValue(Tasks._SYNC_ID, fileName)
|
||||
.withValue(COLUMN_UID, task.uid)
|
||||
.withValue(COLUMN_SEQUENCE, task.sequence)
|
||||
.withValue(COLUMN_ETAG, eTag)
|
||||
.withValue(COLUMN_FLAGS, flags)
|
||||
}
|
||||
|
||||
|
||||
/* custom queries */
|
||||
|
||||
@Throws(CalendarStorageException::class)
|
||||
override fun prepareForUpload() {
|
||||
try {
|
||||
val uid = UUID.randomUUID().toString()
|
||||
val newFileName = uid + ".ics"
|
||||
override fun assignNameAndUID() {
|
||||
val uid = UUID.randomUUID().toString()
|
||||
val newFileName = "$uid.ics"
|
||||
|
||||
val values = ContentValues(2)
|
||||
values.put(Tasks._SYNC_ID, newFileName)
|
||||
values.put(COLUMN_UID, uid)
|
||||
taskList.provider.client.update(taskSyncURI(), values, null, null)
|
||||
val values = ContentValues(2)
|
||||
values.put(Tasks._SYNC_ID, newFileName)
|
||||
values.put(Tasks._UID, uid)
|
||||
taskList.provider.client.update(taskSyncURI(), values, null, null)
|
||||
|
||||
fileName = newFileName
|
||||
fileName = newFileName
|
||||
|
||||
task!!.uid = uid
|
||||
} catch (e: Exception) {
|
||||
throw CalendarStorageException("Couldn't update UID", e)
|
||||
}
|
||||
task!!.uid = uid
|
||||
}
|
||||
|
||||
@Throws(CalendarStorageException::class)
|
||||
override fun clearDirty(eTag: String?) {
|
||||
try {
|
||||
val values = ContentValues(2)
|
||||
values.put(Tasks._DIRTY, 0)
|
||||
values.put(COLUMN_ETAG, eTag)
|
||||
val values = ContentValues(3)
|
||||
values.put(Tasks._DIRTY, 0)
|
||||
values.put(COLUMN_ETAG, eTag)
|
||||
values.put(Tasks.SYNC_VERSION, task!!.sequence)
|
||||
taskList.provider.client.update(taskSyncURI(), values, null, null)
|
||||
|
||||
values.put(COLUMN_SEQUENCE, task!!.sequence)
|
||||
this.eTag = eTag
|
||||
}
|
||||
|
||||
override fun updateFlags(flags: Int) {
|
||||
if (id != null) {
|
||||
val values = ContentValues(1)
|
||||
values.put(COLUMN_FLAGS, flags)
|
||||
taskList.provider.client.update(taskSyncURI(), values, null, null)
|
||||
|
||||
this.eTag = eTag
|
||||
} catch (e: Exception) {
|
||||
throw CalendarStorageException("Couldn't update _DIRTY/ETag/SEQUENCE", e)
|
||||
}
|
||||
|
||||
this.flags = flags
|
||||
}
|
||||
|
||||
|
||||
object Factory: AndroidTaskFactory<LocalTask> {
|
||||
|
||||
override fun newInstance(calendar: AndroidTaskList<*>, id: Long, baseInfo: ContentValues?) =
|
||||
LocalTask(calendar, id, baseInfo)
|
||||
|
||||
override fun newInstance(calendar: AndroidTaskList<*>, task: Task) =
|
||||
LocalTask(calendar, task, null, null)
|
||||
|
||||
override fun fromProvider(taskList: AndroidTaskList<*>, values: ContentValues) =
|
||||
LocalTask(taskList, values)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,15 +15,17 @@ import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import at.bitfire.davdroid.Constants
|
||||
import at.bitfire.davdroid.DavUtils
|
||||
import at.bitfire.davdroid.log.Logger
|
||||
import at.bitfire.davdroid.model.CollectionInfo
|
||||
import at.bitfire.davdroid.model.SyncState
|
||||
import at.bitfire.ical4android.AndroidTaskList
|
||||
import at.bitfire.ical4android.AndroidTaskListFactory
|
||||
import at.bitfire.ical4android.CalendarStorageException
|
||||
import at.bitfire.ical4android.TaskProvider
|
||||
import org.dmfs.provider.tasks.TaskContract.TaskLists
|
||||
import org.dmfs.provider.tasks.TaskContract.Tasks
|
||||
import java.io.FileNotFoundException
|
||||
import org.dmfs.tasks.contract.TaskContract.TaskLists
|
||||
import org.dmfs.tasks.contract.TaskContract.Tasks
|
||||
import java.util.logging.Level
|
||||
|
||||
class LocalTaskList private constructor(
|
||||
account: Account,
|
||||
@@ -33,29 +35,20 @@ class LocalTaskList private constructor(
|
||||
|
||||
companion object {
|
||||
|
||||
val defaultColor = 0xFFC3EA6E.toInt() // "DAVdroid green"
|
||||
|
||||
val COLUMN_CTAG = TaskLists.SYNC_VERSION
|
||||
|
||||
val BASE_INFO_COLUMNS = arrayOf(
|
||||
Tasks._ID,
|
||||
Tasks._SYNC_ID,
|
||||
LocalTask.COLUMN_ETAG
|
||||
)
|
||||
|
||||
@JvmStatic
|
||||
fun tasksProviderAvailable(context: Context): Boolean {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
|
||||
return context.packageManager.resolveContentProvider(TaskProvider.ProviderName.OpenTasks.authority, 0) != null
|
||||
else {
|
||||
val provider = TaskProvider.acquire(context.contentResolver, TaskProvider.ProviderName.OpenTasks)
|
||||
provider?.use { return true }
|
||||
else
|
||||
try {
|
||||
TaskProvider.acquire(context, TaskProvider.ProviderName.OpenTasks)?.use {
|
||||
return true
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
// couldn't acquire task provider
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
@Throws(CalendarStorageException::class)
|
||||
fun create(account: Account, provider: TaskProvider, info: CollectionInfo): Uri {
|
||||
val values = valuesFromCollectionInfo(info, true)
|
||||
values.put(TaskLists.OWNER, account.name)
|
||||
@@ -64,7 +57,6 @@ class LocalTaskList private constructor(
|
||||
return create(account, provider, values)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
@Throws(Exception::class)
|
||||
fun onRenameAccount(resolver: ContentResolver, oldName: String, newName: String) {
|
||||
var client: ContentProviderClient? = null
|
||||
@@ -85,38 +77,49 @@ class LocalTaskList private constructor(
|
||||
|
||||
private fun valuesFromCollectionInfo(info: CollectionInfo, withColor: Boolean): ContentValues {
|
||||
val values = ContentValues(3)
|
||||
values.put(TaskLists._SYNC_ID, info.url)
|
||||
values.put(TaskLists._SYNC_ID, info.url.toString())
|
||||
values.put(TaskLists.LIST_NAME, if (info.displayName.isNullOrBlank()) DavUtils.lastSegmentOfUrl(info.url) else info.displayName)
|
||||
|
||||
if (withColor)
|
||||
values.put(TaskLists.LIST_COLOR, info.color ?: defaultColor)
|
||||
values.put(TaskLists.LIST_COLOR, info.color ?: Constants.DAVDROID_GREEN_RGBA)
|
||||
|
||||
return values
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override val title: String
|
||||
get() = name ?: id.toString()
|
||||
|
||||
override fun taskBaseInfoColumns() = BASE_INFO_COLUMNS
|
||||
override var lastSyncState: SyncState?
|
||||
get() {
|
||||
try {
|
||||
provider.client.query(taskListSyncUri(), arrayOf(TaskLists.SYNC_VERSION),
|
||||
null, null, null)?.use { cursor ->
|
||||
if (cursor.moveToNext())
|
||||
cursor.getString(0)?.let {
|
||||
return SyncState.fromString(it)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Logger.log.log(Level.WARNING, "Couldn't read sync state", e)
|
||||
}
|
||||
return null
|
||||
}
|
||||
set(state) {
|
||||
val values = ContentValues(1)
|
||||
values.put(TaskLists.SYNC_VERSION, state?.toString())
|
||||
provider.client.update(taskListSyncUri(), values, null, null)
|
||||
}
|
||||
|
||||
|
||||
@Throws(CalendarStorageException::class)
|
||||
fun update(info: CollectionInfo, updateColor: Boolean) {
|
||||
update(valuesFromCollectionInfo(info, updateColor));
|
||||
}
|
||||
fun update(info: CollectionInfo, updateColor: Boolean) =
|
||||
update(valuesFromCollectionInfo(info, updateColor))
|
||||
|
||||
|
||||
@Throws(CalendarStorageException::class)
|
||||
override fun getAll() = queryTasks(null, null)
|
||||
override fun findDeleted() = queryTasks("${Tasks._DELETED}!=0", null)
|
||||
|
||||
@Throws(CalendarStorageException::class)
|
||||
override fun getDeleted() = queryTasks("${Tasks._DELETED}!=0", null)
|
||||
|
||||
@Throws(CalendarStorageException::class)
|
||||
override fun getWithoutFileName() = queryTasks("${Tasks._SYNC_ID} IS NULL", null)
|
||||
|
||||
@Throws(FileNotFoundException::class, CalendarStorageException::class)
|
||||
override fun getDirty(): List<LocalTask> {
|
||||
override fun findDirty(): List<LocalTask> {
|
||||
val tasks = queryTasks("${Tasks._DIRTY}!=0", null)
|
||||
for (localTask in tasks) {
|
||||
val task = requireNotNull(localTask.task)
|
||||
@@ -129,30 +132,23 @@ class LocalTaskList private constructor(
|
||||
return tasks
|
||||
}
|
||||
|
||||
override fun findByName(name: String) =
|
||||
queryTasks("${Tasks._SYNC_ID}=?", arrayOf(name)).firstOrNull()
|
||||
|
||||
@Throws(CalendarStorageException::class)
|
||||
override fun getCTag(): String? =
|
||||
try {
|
||||
provider.client.query(taskListSyncUri(), arrayOf(COLUMN_CTAG), null, null, null)?.use { cursor ->
|
||||
if (cursor.moveToNext())
|
||||
return cursor.getString(0)
|
||||
}
|
||||
null
|
||||
} catch(e: Exception) {
|
||||
throw CalendarStorageException("Couldn't read local (last known) CTag", e)
|
||||
}
|
||||
|
||||
@Throws(CalendarStorageException::class)
|
||||
override fun setCTag(cTag: String?) {
|
||||
try {
|
||||
val values = ContentValues(1)
|
||||
values.put(COLUMN_CTAG, cTag)
|
||||
provider.client.update(taskListSyncUri(), values, null, null)
|
||||
} catch (e: Exception) {
|
||||
throw CalendarStorageException("Couldn't write local (last known) CTag", e)
|
||||
}
|
||||
override fun markNotDirty(flags: Int): Int {
|
||||
val values = ContentValues(1)
|
||||
values.put(LocalTask.COLUMN_FLAGS, flags)
|
||||
return provider.client.update(tasksSyncUri(), values,
|
||||
"${Tasks.LIST_ID}=? AND ${Tasks._DIRTY}=0",
|
||||
arrayOf(id.toString()))
|
||||
}
|
||||
|
||||
override fun removeNotDirtyMarked(flags: Int) =
|
||||
provider.client.delete(tasksSyncUri(),
|
||||
"${Tasks.LIST_ID}=? AND ${Tasks._DIRTY}=0 AND ${LocalTask.COLUMN_FLAGS}=?",
|
||||
arrayOf(id.toString(), flags.toString()))
|
||||
|
||||
|
||||
object Factory: AndroidTaskListFactory<LocalTaskList> {
|
||||
|
||||
@@ -161,4 +157,4 @@ class LocalTaskList private constructor(
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -14,18 +14,18 @@ open class DefaultsProvider(
|
||||
private val allowOverride: Boolean = true
|
||||
): Provider {
|
||||
|
||||
open val booleanDefaults = mapOf<String, Boolean>(
|
||||
open val booleanDefaults = mapOf(
|
||||
Pair(App.DISTRUST_SYSTEM_CERTIFICATES, false),
|
||||
Pair(App.OVERRIDE_PROXY, false)
|
||||
)
|
||||
|
||||
open val intDefaults = mapOf<String, Int>(
|
||||
open val intDefaults = mapOf(
|
||||
Pair(App.OVERRIDE_PROXY_PORT, App.OVERRIDE_PROXY_PORT_DEFAULT)
|
||||
)
|
||||
|
||||
open val longDefaults = mapOf<String, Long>()
|
||||
|
||||
open val stringDefaults = mapOf<String, String>(
|
||||
open val stringDefaults = mapOf(
|
||||
Pair(App.OVERRIDE_PROXY_HOST, App.OVERRIDE_PROXY_HOST_DEFAULT)
|
||||
)
|
||||
|
||||
@@ -33,6 +33,9 @@ open class DefaultsProvider(
|
||||
override fun close() {
|
||||
}
|
||||
|
||||
override fun forceReload() {
|
||||
}
|
||||
|
||||
|
||||
private fun hasKey(key: String) =
|
||||
booleanDefaults.containsKey(key) ||
|
||||
|
||||
@@ -12,6 +12,8 @@ import java.io.Closeable
|
||||
|
||||
interface Provider: Closeable {
|
||||
|
||||
fun forceReload()
|
||||
|
||||
fun has(key: String): Pair<Boolean, Boolean>
|
||||
|
||||
fun getBoolean(key: String): Pair<Boolean?, Boolean>
|
||||
|
||||
@@ -19,7 +19,6 @@ import android.os.Handler
|
||||
import android.os.IBinder
|
||||
import android.os.Looper
|
||||
import at.bitfire.davdroid.log.Logger
|
||||
import java.io.Closeable
|
||||
import java.lang.ref.WeakReference
|
||||
import java.util.*
|
||||
import java.util.logging.Level
|
||||
@@ -60,6 +59,10 @@ class Settings: Service(), Provider.Observer {
|
||||
}
|
||||
|
||||
|
||||
fun forceReload() {
|
||||
providers.forEach { it.forceReload() }
|
||||
}
|
||||
|
||||
override fun onReload() {
|
||||
observers.forEach {
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
@@ -72,16 +75,19 @@ class Settings: Service(), Provider.Observer {
|
||||
private fun has(key: String): Boolean {
|
||||
Logger.log.fine("Looking for setting $key")
|
||||
var result = false
|
||||
for (provider in providers) {
|
||||
val (value, further) = provider.has(key)
|
||||
Logger.log.finer("${provider::class.java.simpleName}: has $key = $value, continue: $further")
|
||||
if (value) {
|
||||
result = true
|
||||
break
|
||||
for (provider in providers)
|
||||
try {
|
||||
val (value, further) = provider.has(key)
|
||||
Logger.log.finer("${provider::class.java.simpleName}: has $key = $value, continue: $further")
|
||||
if (value) {
|
||||
result = true
|
||||
break
|
||||
}
|
||||
if (!further)
|
||||
break
|
||||
} catch(e: Exception) {
|
||||
Logger.log.log(Level.SEVERE, "Couldn't look up setting in $provider", e)
|
||||
}
|
||||
if (!further)
|
||||
break
|
||||
}
|
||||
Logger.log.fine("Looking for setting $key -> $result")
|
||||
return result
|
||||
}
|
||||
@@ -89,28 +95,31 @@ class Settings: Service(), Provider.Observer {
|
||||
private fun<T> getValue(key: String, reader: (Provider) -> Pair<T?, Boolean>): T? {
|
||||
Logger.log.fine("Looking up setting $key")
|
||||
var result: T? = null
|
||||
for (provider in providers) {
|
||||
val (value, further) = reader(provider)
|
||||
Logger.log.finer("${provider::class.java.simpleName}: value = $value, continue: $further")
|
||||
value?.let { result = it }
|
||||
if (!further)
|
||||
break
|
||||
}
|
||||
for (provider in providers)
|
||||
try {
|
||||
val (value, further) = reader(provider)
|
||||
Logger.log.finer("${provider::class.java.simpleName}: value = $value, continue: $further")
|
||||
value?.let { result = it }
|
||||
if (!further)
|
||||
break
|
||||
} catch(e: Exception) {
|
||||
Logger.log.log(Level.SEVERE, "Couldn't read setting from $provider", e)
|
||||
}
|
||||
Logger.log.fine("Looked up setting $key -> $result")
|
||||
return result
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -130,7 +139,12 @@ class Settings: Service(), Provider.Observer {
|
||||
val (writable, further) = provider.isWritable(key)
|
||||
Logger.log.finer("${provider::class.java.simpleName}: writable = $writable, continue: $further")
|
||||
if (writable)
|
||||
return writer(provider)
|
||||
return try {
|
||||
writer(provider)
|
||||
} catch (e: Exception) {
|
||||
Logger.log.log(Level.SEVERE, "Couldn't write setting to $provider", e)
|
||||
false
|
||||
}
|
||||
if (!further)
|
||||
return false
|
||||
}
|
||||
@@ -138,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
|
||||
@@ -158,6 +172,9 @@ class Settings: Service(), Provider.Observer {
|
||||
|
||||
val binder = object: ISettings.Stub() {
|
||||
|
||||
override fun forceReload() =
|
||||
this@Settings.forceReload()
|
||||
|
||||
override fun has(key: String) =
|
||||
this@Settings.has(key)
|
||||
|
||||
@@ -207,14 +224,16 @@ class Settings: Service(), Provider.Observer {
|
||||
class Stub(
|
||||
delegate: ISettings,
|
||||
private val context: Context,
|
||||
private val serviceConn: ServiceConnection
|
||||
): ISettings by delegate, Closeable {
|
||||
private val serviceConn: ServiceConnection?
|
||||
): ISettings by delegate, AutoCloseable {
|
||||
|
||||
override fun close() {
|
||||
try {
|
||||
serviceConn.let { context.unbindService(it) }
|
||||
} catch(e: Exception) {
|
||||
Logger.log.log(Level.SEVERE, "Couldn't unbind Settings service", e)
|
||||
serviceConn?.let {
|
||||
try {
|
||||
context.unbindService(it)
|
||||
} catch (e: Exception) {
|
||||
Logger.log.log(Level.SEVERE, "Couldn't unbind Settings service", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -223,6 +242,9 @@ class Settings: Service(), Provider.Observer {
|
||||
companion object {
|
||||
|
||||
fun getInstance(context: Context): Stub? {
|
||||
if (context is Settings)
|
||||
return Stub(context.binder, context, null)
|
||||
|
||||
if (Looper.getMainLooper().thread == Thread.currentThread())
|
||||
throw IllegalStateException("Must not be called from main thread")
|
||||
|
||||
|
||||
@@ -20,8 +20,8 @@ class SharedPreferencesProvider(
|
||||
): Provider {
|
||||
|
||||
companion object {
|
||||
private val META_VERSION = "version"
|
||||
private val CURRENT_VERSION = 0
|
||||
private const val META_VERSION = "version"
|
||||
private const val CURRENT_VERSION = 0
|
||||
}
|
||||
|
||||
private val preferences = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
@@ -39,6 +39,9 @@ class SharedPreferencesProvider(
|
||||
override fun close() {
|
||||
}
|
||||
|
||||
override fun forceReload() {
|
||||
}
|
||||
|
||||
|
||||
override fun has(key: String) =
|
||||
Pair(preferences.contains(key), true)
|
||||
@@ -53,44 +56,44 @@ 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) =
|
||||
Pair(true, true)
|
||||
|
||||
private fun<T> putValue(key: String, value: T?, writer: (SharedPreferences.Editor, T) -> Unit): Boolean {
|
||||
if (value == null)
|
||||
return remove(key)
|
||||
return if (value == null)
|
||||
remove(key)
|
||||
else {
|
||||
Logger.log.fine("Writing setting $key = $value")
|
||||
val edit = preferences.edit()
|
||||
writer(edit, value)
|
||||
edit.apply()
|
||||
return true
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
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,28 +7,92 @@
|
||||
*/
|
||||
package at.bitfire.davdroid.syncadapter
|
||||
|
||||
import android.accounts.AbstractAccountAuthenticator
|
||||
import android.accounts.Account
|
||||
import android.accounts.AccountAuthenticatorResponse
|
||||
import android.accounts.AccountManager
|
||||
import android.accounts.*
|
||||
import android.app.Service
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.database.DatabaseUtils
|
||||
import android.os.Bundle
|
||||
import at.bitfire.davdroid.R
|
||||
import at.bitfire.davdroid.log.Logger
|
||||
import at.bitfire.davdroid.model.ServiceDB
|
||||
import at.bitfire.davdroid.resource.LocalAddressBook
|
||||
import at.bitfire.davdroid.ui.setup.LoginActivity
|
||||
import java.util.*
|
||||
import java.util.logging.Level
|
||||
|
||||
class AccountAuthenticatorService: Service() {
|
||||
|
||||
/**
|
||||
* Account authenticator for the main DAVdroid account type.
|
||||
*
|
||||
* Gets started when a DAVdroid account is removed, too, so it also watches for account removals
|
||||
* and contains the corresponding cleanup code.
|
||||
*/
|
||||
class AccountAuthenticatorService: Service(), OnAccountsUpdateListener {
|
||||
|
||||
companion object {
|
||||
|
||||
fun cleanupAccounts(context: Context) {
|
||||
Logger.log.info("Cleaning up orphaned accounts")
|
||||
|
||||
ServiceDB.OpenHelper(context).use { dbHelper ->
|
||||
val db = dbHelper.writableDatabase
|
||||
|
||||
val sqlAccountNames = LinkedList<String>()
|
||||
val accountNames = HashSet<String>()
|
||||
val accountManager = AccountManager.get(context)
|
||||
for (account in accountManager.getAccountsByType(context.getString(R.string.account_type))) {
|
||||
sqlAccountNames.add(DatabaseUtils.sqlEscapeString(account.name))
|
||||
accountNames += account.name
|
||||
}
|
||||
|
||||
// delete orphaned address book accounts
|
||||
accountManager.getAccountsByType(context.getString(R.string.account_type_address_book))
|
||||
.map { LocalAddressBook(context, it, null) }
|
||||
.forEach {
|
||||
try {
|
||||
if (!accountNames.contains(it.mainAccount.name))
|
||||
it.delete()
|
||||
} catch(e: Exception) {
|
||||
Logger.log.log(Level.SEVERE, "Couldn't delete address book account", e)
|
||||
}
|
||||
}
|
||||
|
||||
// delete orphaned services in DB
|
||||
if (sqlAccountNames.isEmpty())
|
||||
db.delete(ServiceDB.Services._TABLE, null, null)
|
||||
else
|
||||
db.delete(ServiceDB.Services._TABLE, "${ServiceDB.Services.ACCOUNT_NAME} NOT IN (${sqlAccountNames.joinToString(",")})", null)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
private lateinit var accountManager: AccountManager
|
||||
private lateinit var accountAuthenticator: AccountAuthenticator
|
||||
|
||||
override fun onCreate() {
|
||||
accountManager = AccountManager.get(this)
|
||||
accountManager.addOnAccountsUpdatedListener(this, null, true)
|
||||
|
||||
accountAuthenticator = AccountAuthenticator(this)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
accountManager.removeOnAccountsUpdatedListener(this)
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent?) =
|
||||
accountAuthenticator.iBinder.takeIf { intent?.action == android.accounts.AccountManager.ACTION_AUTHENTICATOR_INTENT }
|
||||
|
||||
|
||||
override fun onAccountsUpdated(accounts: Array<out Account>?) {
|
||||
cleanupAccounts(this)
|
||||
}
|
||||
|
||||
|
||||
private class AccountAuthenticator(
|
||||
val context: Context
|
||||
): AbstractAccountAuthenticator(context) {
|
||||
|
||||
@@ -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.vcard4android.ContactsStorageException
|
||||
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)
|
||||
|
||||
|
||||
protected class AddressBooksSyncAdapter(
|
||||
class AddressBooksSyncAdapter(
|
||||
context: Context
|
||||
): SyncAdapter(context) {
|
||||
|
||||
override fun sync(settings: ISettings, account: Account, extras: Bundle, authority: String, addressBooksProvider: 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
|
||||
}
|
||||
) : SyncAdapter(context) {
|
||||
|
||||
override fun sync(settings: ISettings, account: Account, extras: Bundle, authority: String, provider: ContentProviderClient, syncResult: SyncResult) {
|
||||
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,32 +59,32 @@ 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
|
||||
null
|
||||
}
|
||||
|
||||
fun remoteAddressBooks(service: Long?): MutableMap<String, CollectionInfo> {
|
||||
val collections = mutableMapOf<String, CollectionInfo>()
|
||||
fun remoteAddressBooks(service: Long?): MutableMap<HttpUrl, CollectionInfo> {
|
||||
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.find(context, provider, account)) {
|
||||
val url = addressBook.getURL()
|
||||
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: ContactsStorageException) {
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,28 +9,27 @@
|
||||
package at.bitfire.davdroid.syncadapter
|
||||
|
||||
import android.accounts.Account
|
||||
import android.content.ContentProviderClient
|
||||
import android.content.Context
|
||||
import android.content.SyncResult
|
||||
import android.os.Bundle
|
||||
import at.bitfire.dav4android.DavCalendar
|
||||
import at.bitfire.dav4android.DavResource
|
||||
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.*
|
||||
import at.bitfire.davdroid.AccountSettings
|
||||
import at.bitfire.davdroid.Constants
|
||||
import at.bitfire.davdroid.R
|
||||
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
|
||||
import at.bitfire.davdroid.settings.ISettings
|
||||
import at.bitfire.ical4android.Event
|
||||
import at.bitfire.ical4android.InvalidCalendarException
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.RequestBody
|
||||
import org.apache.commons.collections4.ListUtils
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.Reader
|
||||
import java.io.StringReader
|
||||
@@ -38,141 +37,119 @@ import java.util.*
|
||||
import java.util.logging.Level
|
||||
|
||||
/**
|
||||
* Synchronization manager for CalDAV collections; handles events ({@code VEVENT}).
|
||||
* Synchronization manager for CalDAV collections; handles events (VEVENT)
|
||||
*/
|
||||
class CalendarSyncManager(
|
||||
context: Context,
|
||||
settings: ISettings,
|
||||
account: Account,
|
||||
settings: AccountSettings,
|
||||
accountSettings: AccountSettings,
|
||||
extras: Bundle,
|
||||
authority: String,
|
||||
syncResult: SyncResult,
|
||||
val provider: ContentProviderClient,
|
||||
val localCalendar: LocalCalendar
|
||||
): SyncManager(context, account, settings, extras, authority, syncResult, "calendar/${localCalendar.id}") {
|
||||
|
||||
val MAX_MULTIGET = 20
|
||||
|
||||
init {
|
||||
localCollection = localCalendar
|
||||
}
|
||||
|
||||
override fun notificationId() = Constants.NOTIFICATION_CALENDAR_SYNC
|
||||
|
||||
override fun getSyncErrorTitle() = context.getString(R.string.sync_error_calendar, account.name)!!
|
||||
|
||||
localCalendar: LocalCalendar
|
||||
): SyncManager<LocalEvent, LocalCalendar, DavCalendar>(context, settings, account, accountSettings, extras, authority, syncResult, localCalendar) {
|
||||
|
||||
override fun prepare(): Boolean {
|
||||
collectionURL = HttpUrl.parse(localCalendar.name ?: return false) ?: return false
|
||||
collectionURL = HttpUrl.parse(localCollection.name ?: return false) ?: return false
|
||||
davCollection = DavCalendar(httpClient.okHttpClient, collectionURL)
|
||||
|
||||
// if there are dirty exceptions for events, mark their master events as dirty, too
|
||||
localCollection.processDirtyExceptions()
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun queryCapabilities() {
|
||||
davCollection.propfind(0, GetCTag.NAME)
|
||||
override fun queryCapabilities(): SyncState? =
|
||||
useRemoteCollection {
|
||||
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 = syncState(response)
|
||||
}
|
||||
}
|
||||
|
||||
Logger.log.info("Server supports Collection Sync: $hasCollectionSync")
|
||||
syncState
|
||||
}
|
||||
|
||||
override fun syncAlgorithm() = if (accountSettings.getTimeRangePastDays() != null || !hasCollectionSync)
|
||||
SyncAlgorithm.PROPFIND_REPORT
|
||||
else
|
||||
SyncAlgorithm.COLLECTION_SYNC
|
||||
|
||||
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)
|
||||
|
||||
val os = ByteArrayOutputStream()
|
||||
event.write(os)
|
||||
|
||||
RequestBody.create(
|
||||
DavCalendar.MIME_ICALENDAR_UTF8,
|
||||
os.toByteArray()
|
||||
)
|
||||
}
|
||||
|
||||
override fun prepareDirty() {
|
||||
super.prepareDirty()
|
||||
localCalendar.processDirtyExceptions()
|
||||
}
|
||||
|
||||
override fun prepareUpload(resource: LocalResource): RequestBody {
|
||||
if (resource is LocalEvent) {
|
||||
val event = requireNotNull(resource.event)
|
||||
Logger.log.log(Level.FINE, "Preparing upload of event ${resource.fileName}", event)
|
||||
|
||||
val os = ByteArrayOutputStream()
|
||||
event.write(os)
|
||||
|
||||
return RequestBody.create(
|
||||
DavCalendar.MIME_ICALENDAR,
|
||||
os.toByteArray()
|
||||
)
|
||||
} else
|
||||
throw IllegalArgumentException("resource must be a LocalEvent")
|
||||
}
|
||||
|
||||
override fun listRemote() {
|
||||
override fun listAllRemote(callback: DavResponseCallback) {
|
||||
// calculate time range limits
|
||||
var limitStart: Date? = null
|
||||
settings.getTimeRangePastDays()?.let { pastDays ->
|
||||
accountSettings.getTimeRangePastDays()?.let { pastDays ->
|
||||
val calendar = Calendar.getInstance()
|
||||
calendar.add(Calendar.DAY_OF_MONTH, -pastDays)
|
||||
limitStart = calendar.time
|
||||
}
|
||||
|
||||
// fetch list of remote VEVENTs and build hash table to index file name
|
||||
val calendar = davCalendar()
|
||||
currentDavResource = calendar
|
||||
calendar.calendarQuery("VEVENT", limitStart, null)
|
||||
|
||||
remoteResources = HashMap<String, DavResource>(davCollection.members.size)
|
||||
for (iCal in davCollection.members) {
|
||||
val fileName = iCal.fileName()
|
||||
Logger.log.fine("Found remote VEVENT: $fileName")
|
||||
remoteResources[fileName] = iCal
|
||||
return useRemoteCollection { remote ->
|
||||
Logger.log.info("Querying events since $limitStart")
|
||||
remote.calendarQuery("VEVENT", limitStart, null, callback)
|
||||
}
|
||||
|
||||
currentDavResource = null
|
||||
}
|
||||
|
||||
override fun downloadRemote() {
|
||||
Logger.log.info("Downloading ${toDownload.size} events ($MAX_MULTIGET at once)")
|
||||
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")
|
||||
|
||||
// download new/updated iCalendars from server
|
||||
for (bunch in ListUtils.partition(toDownload.toList(), MAX_MULTIGET)) {
|
||||
if (Thread.interrupted())
|
||||
return
|
||||
|
||||
Logger.log.info("Downloading ${bunch.joinToString(", ")}")
|
||||
|
||||
if (bunch.size == 1) {
|
||||
// only one contact, use GET
|
||||
val remote = bunch.first()
|
||||
currentDavResource = remote
|
||||
|
||||
val body = remote.get("text/calendar")
|
||||
|
||||
// CalDAV servers MUST return ETag on GET [https://tools.ietf.org/html/rfc4791#section-5.3.4]
|
||||
val eTag = remote.properties[GetETag.NAME] as GetETag?
|
||||
if (eTag == null || eTag.eTag.isNullOrEmpty())
|
||||
throw DavException("Received CalDAV GET response without ETag for ${remote.location}")
|
||||
|
||||
body.charStream()?.use { reader ->
|
||||
processVEvent(remote.fileName(), eTag.eTag!!, reader)
|
||||
}
|
||||
|
||||
} else {
|
||||
// multiple contacts, use multi-get
|
||||
val calendar = davCalendar()
|
||||
currentDavResource = calendar
|
||||
calendar.multiget(bunch.map { it.location })
|
||||
|
||||
// process multiget results
|
||||
for (remote in davCollection.members) {
|
||||
currentDavResource = remote
|
||||
|
||||
val eTag = (remote.properties[GetETag.NAME] as GetETag?)?.eTag
|
||||
?: throw DavException("Received multi-get response without ETag")
|
||||
|
||||
val calendarData = remote.properties[CalendarData.NAME] as CalendarData?
|
||||
val iCalendar = calendarData?.iCalendar
|
||||
?: throw DavException("Received multi-get response without event 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")
|
||||
|
||||
currentDavResource = null
|
||||
}
|
||||
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() {
|
||||
}
|
||||
|
||||
|
||||
// helpers
|
||||
|
||||
private fun davCalendar() = davCollection as DavCalendar
|
||||
|
||||
private fun processVEvent(fileName: String, eTag: String, reader: Reader) {
|
||||
val events: List<Event>
|
||||
try {
|
||||
@@ -186,24 +163,22 @@ class CalendarSyncManager(
|
||||
val newData = events.first()
|
||||
|
||||
// delete local event, if it exists
|
||||
val localEvent = localResources[fileName] as LocalEvent?
|
||||
currentLocalResource = localEvent
|
||||
if (localEvent != null) {
|
||||
Logger.log.info("Updating $fileName in local calendar")
|
||||
localEvent.eTag = eTag
|
||||
localEvent.update(newData)
|
||||
syncResult.stats.numUpdates++
|
||||
} else {
|
||||
Logger.log.info("Adding $fileName to local calendar")
|
||||
val newEvent = LocalEvent(localCalendar, newData, fileName, eTag)
|
||||
currentLocalResource = newEvent
|
||||
newEvent.add()
|
||||
syncResult.stats.numInserts++
|
||||
useLocal(localCollection.findByName(fileName)) { local ->
|
||||
if (local != null) {
|
||||
Logger.log.info("Updating $fileName in local calendar")
|
||||
local.eTag = eTag
|
||||
local.update(newData)
|
||||
syncResult.stats.numUpdates++
|
||||
} else {
|
||||
Logger.log.info("Adding $fileName to local calendar")
|
||||
useLocal(LocalEvent(localCollection, newData, fileName, eTag, LocalResource.FLAG_REMOTELY_PRESENT)) {
|
||||
it.add()
|
||||
}
|
||||
syncResult.stats.numInserts++
|
||||
}
|
||||
}
|
||||
} else
|
||||
Logger.log.severe("Received VCALENDAR with not exactly one VEVENT with UID, but without RECURRENCE-ID; ignoring $fileName")
|
||||
|
||||
currentLocalResource = null
|
||||
Logger.log.info("Received VCALENDAR with not exactly one VEVENT with UID and without RECURRENCE-ID; ignoring $fileName")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -20,16 +20,17 @@ import at.bitfire.davdroid.model.ServiceDB.Collections
|
||||
import at.bitfire.davdroid.resource.LocalCalendar
|
||||
import at.bitfire.davdroid.settings.ISettings
|
||||
import at.bitfire.ical4android.AndroidCalendar
|
||||
import okhttp3.HttpUrl
|
||||
import java.util.logging.Level
|
||||
|
||||
class CalendarsSyncAdapterService: SyncAdapterService() {
|
||||
|
||||
override fun syncAdapter() = SyncAdapter(this)
|
||||
override fun syncAdapter() = CalendarsSyncAdapter(this)
|
||||
|
||||
|
||||
protected class SyncAdapter(
|
||||
class CalendarsSyncAdapter(
|
||||
context: Context
|
||||
): SyncAdapterService.SyncAdapter(context) {
|
||||
): SyncAdapter(context) {
|
||||
|
||||
override fun sync(settings: ISettings, account: Account, extras: Bundle, authority: String, provider: ContentProviderClient, syncResult: SyncResult) {
|
||||
try {
|
||||
@@ -51,7 +52,7 @@ class CalendarsSyncAdapterService: SyncAdapterService() {
|
||||
|
||||
for (calendar in AndroidCalendar.find(account, provider, LocalCalendar.Factory, "${CalendarContract.Calendars.SYNC_EVENTS}!=0", null)) {
|
||||
Logger.log.info("Synchronizing calendar #${calendar.id}, URL: ${calendar.name}")
|
||||
CalendarSyncManager(context, account, accountSettings, extras, authority, syncResult, provider, calendar).use {
|
||||
CalendarSyncManager(context, settings, account, accountSettings, extras, authority, syncResult, calendar).use {
|
||||
it.performSync()
|
||||
}
|
||||
}
|
||||
@@ -75,8 +76,8 @@ class CalendarsSyncAdapterService: SyncAdapterService() {
|
||||
null
|
||||
}
|
||||
|
||||
fun remoteCalendars(service: Long?): MutableMap<String, CollectionInfo> {
|
||||
val collections = mutableMapOf<String, CollectionInfo>()
|
||||
fun remoteCalendars(service: Long?): MutableMap<HttpUrl, CollectionInfo> {
|
||||
val collections = mutableMapOf<HttpUrl, CollectionInfo>()
|
||||
service?.let {
|
||||
db.query(Collections._TABLE, null,
|
||||
"${Collections.SERVICE_ID}=? AND ${Collections.SUPPORTS_VEVENT}!=0 AND ${Collections.SYNC}",
|
||||
@@ -100,7 +101,8 @@ class CalendarsSyncAdapterService: SyncAdapterService() {
|
||||
val updateColors = settings.getManageCalendarColors()
|
||||
|
||||
for (calendar in AndroidCalendar.find(account, provider, LocalCalendar.Factory, null, null))
|
||||
calendar.name?.let { url ->
|
||||
calendar.name?.let {
|
||||
val url = HttpUrl.parse(it)!!
|
||||
val info = remote[url]
|
||||
if (info == null) {
|
||||
Logger.log.log(Level.INFO, "Deleting obsolete local calendar", url)
|
||||
|
||||
@@ -14,6 +14,7 @@ import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
import android.content.SyncResult
|
||||
import android.os.Bundle
|
||||
import android.provider.ContactsContract
|
||||
import at.bitfire.davdroid.AccountSettings
|
||||
import at.bitfire.davdroid.log.Logger
|
||||
import at.bitfire.davdroid.resource.LocalAddressBook
|
||||
@@ -22,25 +23,45 @@ import java.util.logging.Level
|
||||
|
||||
class ContactsSyncAdapterService: SyncAdapterService() {
|
||||
|
||||
companion object {
|
||||
const val PREVIOUS_GROUP_METHOD = "previous_group_method"
|
||||
}
|
||||
|
||||
override fun syncAdapter() = ContactsSyncAdapter(this)
|
||||
|
||||
|
||||
protected class ContactsSyncAdapter(
|
||||
class ContactsSyncAdapter(
|
||||
context: Context
|
||||
): SyncAdapter(context) {
|
||||
|
||||
override fun sync(settings: ISettings, account: Account, extras: Bundle, authority: String, provider: ContentProviderClient, syncResult: SyncResult) {
|
||||
try {
|
||||
val addressBook = LocalAddressBook(context, account, provider)
|
||||
val accountSettings = AccountSettings(context, settings, addressBook.mainAccount)
|
||||
|
||||
// handle group method change
|
||||
val groupMethod = accountSettings.getGroupMethod().name
|
||||
accountSettings.accountManager.getUserData(account, PREVIOUS_GROUP_METHOD)?.let { previousGroupMethod ->
|
||||
if (previousGroupMethod != groupMethod) {
|
||||
Logger.log.info("Group method changed, deleting all local contacts/groups")
|
||||
|
||||
// delete all local contacts and groups so that they will be downloaded again
|
||||
provider.delete(addressBook.syncAdapterURI(ContactsContract.RawContacts.CONTENT_URI), null, null)
|
||||
provider.delete(addressBook.syncAdapterURI(ContactsContract.Groups.CONTENT_URI), null, null)
|
||||
|
||||
// reset sync state
|
||||
addressBook.syncState = null
|
||||
}
|
||||
}
|
||||
accountSettings.accountManager.setUserData(account, PREVIOUS_GROUP_METHOD, groupMethod)
|
||||
|
||||
val accountSettings = AccountSettings(context, settings, addressBook.getMainAccount())
|
||||
if (!extras.containsKey(ContentResolver.SYNC_EXTRAS_MANUAL) && !checkSyncConditions(accountSettings))
|
||||
return
|
||||
|
||||
Logger.log.info("Synchronizing address book: ${addressBook.getURL()}")
|
||||
Logger.log.info("Taking settings from: ${addressBook.getMainAccount()}")
|
||||
Logger.log.info("Synchronizing address book: ${addressBook.url}")
|
||||
Logger.log.info("Taking settings from: ${addressBook.mainAccount}")
|
||||
|
||||
ContactsSyncManager(context, account, accountSettings, extras, authority, syncResult, provider, addressBook).use {
|
||||
ContactsSyncManager(context, settings, account, accountSettings, extras, authority, syncResult, provider, addressBook).use {
|
||||
it.performSync()
|
||||
}
|
||||
} catch(e: Exception) {
|
||||
|
||||
@@ -12,172 +12,181 @@ import android.accounts.Account
|
||||
import android.content.*
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.RemoteException
|
||||
import android.provider.ContactsContract
|
||||
import android.provider.ContactsContract.Groups
|
||||
import android.support.v4.app.NotificationCompat
|
||||
import android.support.v4.app.NotificationManagerCompat
|
||||
import at.bitfire.dav4android.DavAddressBook
|
||||
import at.bitfire.dav4android.DavResource
|
||||
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.*
|
||||
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
|
||||
import at.bitfire.davdroid.resource.LocalAddressBook
|
||||
import at.bitfire.davdroid.resource.LocalContact
|
||||
import at.bitfire.davdroid.resource.LocalGroup
|
||||
import at.bitfire.davdroid.resource.LocalResource
|
||||
import at.bitfire.davdroid.model.SyncState
|
||||
import at.bitfire.davdroid.resource.*
|
||||
import at.bitfire.davdroid.settings.ISettings
|
||||
import at.bitfire.davdroid.ui.NotificationUtils
|
||||
import at.bitfire.vcard4android.BatchOperation
|
||||
import at.bitfire.vcard4android.Contact
|
||||
import at.bitfire.vcard4android.ContactsStorageException
|
||||
import at.bitfire.vcard4android.GroupMethod
|
||||
import ezvcard.VCardVersion
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody
|
||||
import org.apache.commons.collections4.ListUtils
|
||||
import java.io.*
|
||||
import java.util.*
|
||||
import java.util.logging.Level
|
||||
|
||||
/**
|
||||
* <p>Synchronization manager for CardDAV collections; handles contacts and groups.</p>
|
||||
* Synchronization manager for CardDAV collections; handles contacts and groups.
|
||||
*
|
||||
* <p></p>Group handling differs according to the {@link #groupMethod}. There are two basic methods to
|
||||
* handle/manage groups:</p>
|
||||
* <ul>
|
||||
* <li>{@code CATEGORIES}: groups memberships are attached to each contact and represented as
|
||||
* "category". When a group is dirty or has been deleted, all its members have to be set to
|
||||
* dirty, too (because they have to be uploaded without the respective category). This
|
||||
* is done in {@link #prepareDirty()}. Empty groups can be deleted without further processing,
|
||||
* which is done in {@link #postProcess()} because groups may become empty after downloading
|
||||
* updated remoted contacts.</li>
|
||||
* <li>Groups as separate VCards: individual and group contacts (with a list of member UIDs) are
|
||||
* distinguished. When a local group is dirty, its members don't need to be set to dirty.
|
||||
* <ol>
|
||||
* <li>However, when a contact is dirty, it has
|
||||
* to be checked whether its group memberships have changed. In this case, the respective
|
||||
* groups have to be set to dirty. For instance, if contact A is in group G and H, and then
|
||||
* group membership of G is removed, the contact will be set to dirty because of the changed
|
||||
* {@link android.provider.ContactsContract.CommonDataKinds.GroupMembership}. DAVdroid will
|
||||
* then have to check whether the group memberships have actually changed, and if so,
|
||||
* all affected groups have to be set to dirty. To detect changes in group memberships,
|
||||
* DAVdroid always mirrors all {@link android.provider.ContactsContract.CommonDataKinds.GroupMembership}
|
||||
* data rows in respective {@link at.bitfire.vcard4android.CachedGroupMembership} rows.
|
||||
* If the cached group memberships are not the same as the current group member ships, the
|
||||
* difference set (in our example G, because its in the cached memberships, but not in the
|
||||
* actual ones) is marked as dirty. This is done in {@link #prepareDirty()}.</li>
|
||||
* <li>When downloading remote contacts, groups (+ member information) may be received
|
||||
* by the actual members. Thus, the member lists have to be cached until all VCards
|
||||
* are received. This is done by caching the member UIDs of each group in
|
||||
* {@link LocalGroup#COLUMN_PENDING_MEMBERS}. In {@link #postProcess()},
|
||||
* these "pending memberships" are assigned to the actual contacs and then cleaned up.</li>
|
||||
* </ol>
|
||||
* </ul>
|
||||
* Group handling differs according to the {@link #groupMethod}. There are two basic methods to
|
||||
* handle/manage groups:
|
||||
*
|
||||
* 1. CATEGORIES: groups memberships are attached to each contact and represented as
|
||||
* "category". When a group is dirty or has been deleted, all its members have to be set to
|
||||
* dirty, too (because they have to be uploaded without the respective category). This
|
||||
* is done in [uploadDirty]. Empty groups can be deleted without further processing,
|
||||
* which is done in [postProcess] because groups may become empty after downloading
|
||||
* updated remote contacts.
|
||||
*
|
||||
* 2. Groups as separate VCards: individual and group contacts (with a list of member UIDs) are
|
||||
* distinguished. When a local group is dirty, its members don't need to be set to dirty.
|
||||
*
|
||||
* However, when a contact is dirty, it has
|
||||
* to be checked whether its group memberships have changed. In this case, the respective
|
||||
* groups have to be set to dirty. For instance, if contact A is in group G and H, and then
|
||||
* group membership of G is removed, the contact will be set to dirty because of the changed
|
||||
* [android.provider.ContactsContract.CommonDataKinds.GroupMembership]. DAVdroid will
|
||||
* then have to check whether the group memberships have actually changed, and if so,
|
||||
* all affected groups have to be set to dirty. To detect changes in group memberships,
|
||||
* DAVdroid always mirrors all [android.provider.ContactsContract.CommonDataKinds.GroupMembership]
|
||||
* data rows in respective [at.bitfire.vcard4android.CachedGroupMembership] rows.
|
||||
* If the cached group memberships are not the same as the current group member ships, the
|
||||
* difference set (in our example G, because its in the cached memberships, but not in the
|
||||
* actual ones) is marked as dirty. This is done in [uploadDirty].
|
||||
*
|
||||
* When downloading remote contacts, groups (+ member information) may be received
|
||||
* by the actual members. Thus, the member lists have to be cached until all VCards
|
||||
* are received. This is done by caching the member UIDs of each group in
|
||||
* [LocalGroup.COLUMN_PENDING_MEMBERS]. In [postProcess],
|
||||
* these "pending memberships" are assigned to the actual contacts and then cleaned up.
|
||||
*/
|
||||
class ContactsSyncManager(
|
||||
context: Context,
|
||||
settings: ISettings,
|
||||
account: Account,
|
||||
settings: AccountSettings,
|
||||
accountSettings: AccountSettings,
|
||||
extras: Bundle,
|
||||
authority: String,
|
||||
syncResult: SyncResult,
|
||||
val provider: ContentProviderClient,
|
||||
private val localAddressBook: LocalAddressBook
|
||||
): SyncManager(context, account, settings, extras, authority, syncResult, "addressBook") {
|
||||
localAddressBook: LocalAddressBook
|
||||
): SyncManager<LocalAddress, LocalAddressBook, DavAddressBook>(context, settings, account, accountSettings, extras, authority, syncResult, localAddressBook) {
|
||||
|
||||
private val MAX_MULTIGET = 10
|
||||
|
||||
private var readOnly = false
|
||||
var numDiscarded = 0
|
||||
|
||||
private var hasVCard4 = false
|
||||
private lateinit var groupMethod: GroupMethod
|
||||
|
||||
|
||||
init {
|
||||
localCollection = localAddressBook
|
||||
companion object {
|
||||
infix fun <T> Set<T>.disjunct(other: Set<T>) = (this - other) union (other - this)
|
||||
}
|
||||
|
||||
override fun notificationId() = Constants.NOTIFICATION_CONTACTS_SYNC
|
||||
private val readOnly = localAddressBook.readOnly
|
||||
private var numDiscarded = 0
|
||||
|
||||
override fun getSyncErrorTitle() = context.getString(R.string.sync_error_contacts, account.name)!!
|
||||
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 (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 = localAddressBook.verifyDirty()
|
||||
val deleted = localAddressBook.getDeleted().size
|
||||
val reallyDirty = localCollection.verifyDirty()
|
||||
val deleted = localCollection.findDeleted().size
|
||||
if (extras.containsKey(ContentResolver.SYNC_EXTRAS_UPLOAD) && reallyDirty == 0 && deleted == 0) {
|
||||
Logger.log.info("This sync was called to up-sync dirty/deleted contacts, but no contacts have been changed")
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// set up Contacts Provider Settings
|
||||
val values = ContentValues(2)
|
||||
values.put(ContactsContract.Settings.SHOULD_SYNC, 1)
|
||||
values.put(ContactsContract.Settings.UNGROUPED_VISIBLE, 1)
|
||||
localAddressBook.updateSettings(values)
|
||||
|
||||
collectionURL = HttpUrl.parse(localAddressBook.getURL()) ?: return false
|
||||
collectionURL = HttpUrl.parse(localCollection.url) ?: return false
|
||||
davCollection = DavAddressBook(httpClient.okHttpClient, collectionURL)
|
||||
|
||||
readOnly = localAddressBook.getReadOnly()
|
||||
resourceDownloader = ResourceDownloader(davCollection.location)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun queryCapabilities() {
|
||||
// prepare remote address book
|
||||
davCollection.propfind(0, SupportedAddressData.NAME, GetCTag.NAME)
|
||||
(davCollection.properties[SupportedAddressData.NAME] as SupportedAddressData?)?.let {
|
||||
hasVCard4 = it.hasVCard4()
|
||||
}
|
||||
Logger.log.info("Server advertises VCard/4 support: $hasVCard4")
|
||||
|
||||
groupMethod = settings.getGroupMethod()
|
||||
override fun queryCapabilities(): SyncState? {
|
||||
Logger.log.info("Contact group method: $groupMethod")
|
||||
// in case of GROUP_VCARDs, treat groups as contacts in the local address book
|
||||
localCollection.includeGroups = groupMethod == GroupMethod.GROUP_VCARDS
|
||||
|
||||
localAddressBook.includeGroups = groupMethod == GroupMethod.GROUP_VCARDS
|
||||
return useRemoteCollection {
|
||||
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()
|
||||
}
|
||||
|
||||
response[SupportedReportSet::class.java]?.let {
|
||||
hasCollectionSync = it.reports.contains(SupportedReportSet.SYNC_COLLECTION)
|
||||
}
|
||||
|
||||
syncState = syncState(response)
|
||||
}
|
||||
}
|
||||
|
||||
Logger.log.info("Server supports vCard/4: $hasVCard4")
|
||||
Logger.log.info("Server supports Collection Sync: $hasCollectionSync")
|
||||
|
||||
syncState
|
||||
}
|
||||
}
|
||||
|
||||
override fun processLocallyDeleted() {
|
||||
override fun syncAlgorithm() = if (hasCollectionSync)
|
||||
SyncAlgorithm.COLLECTION_SYNC
|
||||
else
|
||||
SyncAlgorithm.PROPFIND_REPORT
|
||||
|
||||
override fun processLocallyDeleted(): Boolean {
|
||||
if (readOnly) {
|
||||
for (group in localAddressBook.getDeletedGroups()) {
|
||||
for (group in localCollection.findDeletedGroups()) {
|
||||
Logger.log.warning("Restoring locally deleted group (read-only address book!)")
|
||||
group.resetDeleted()
|
||||
useLocal(group) { it.resetDeleted() }
|
||||
numDiscarded++
|
||||
}
|
||||
|
||||
for (contact in localAddressBook.getDeletedContacts()) {
|
||||
for (contact in localCollection.findDeletedContacts()) {
|
||||
Logger.log.warning("Restoring locally deleted contact (read-only address book!)")
|
||||
contact.resetDeleted()
|
||||
useLocal(contact) { it.resetDeleted() }
|
||||
numDiscarded++
|
||||
}
|
||||
|
||||
if (numDiscarded > 0)
|
||||
notifyDiscardedChange()
|
||||
return false
|
||||
} else
|
||||
// mirror deletions to remote collection (DELETE)
|
||||
super.processLocallyDeleted()
|
||||
return super.processLocallyDeleted()
|
||||
}
|
||||
|
||||
override fun prepareDirty() {
|
||||
// generate UID/file name for newly created contacts
|
||||
super.prepareDirty()
|
||||
|
||||
override fun uploadDirty(): Boolean {
|
||||
if (readOnly) {
|
||||
for (group in localAddressBook.getDirtyGroups()) {
|
||||
for (group in localCollection.findDirtyGroups()) {
|
||||
Logger.log.warning("Resetting locally modified group to ETag=null (read-only address book!)")
|
||||
group.clearDirty(null)
|
||||
useLocal(group) { it.clearDirty(null) }
|
||||
numDiscarded++
|
||||
}
|
||||
|
||||
for (contact in localAddressBook.getDirtyContacts()) {
|
||||
for (contact in localCollection.findDirtyContacts()) {
|
||||
Logger.log.warning("Resetting locally modified contact to ETag=null (read-only address book!)")
|
||||
contact.clearDirty(null)
|
||||
useLocal(contact) { it.clearDirty(null) }
|
||||
numDiscarded++
|
||||
}
|
||||
|
||||
@@ -189,25 +198,27 @@ class ContactsSyncManager(
|
||||
/* groups memberships are represented as contact CATEGORIES */
|
||||
|
||||
// groups with DELETED=1: set all members to dirty, then remove group
|
||||
for (group in localAddressBook.getDeletedGroups()) {
|
||||
for (group in localCollection.findDeletedGroups()) {
|
||||
Logger.log.fine("Finally removing group $group")
|
||||
// useless because Android deletes group memberships as soon as a group is set to DELETED:
|
||||
// group.markMembersDirty()
|
||||
group.delete()
|
||||
useLocal(group) { it.delete() }
|
||||
}
|
||||
|
||||
// groups with DIRTY=1: mark all memberships as dirty, then clean DIRTY flag of group
|
||||
for (group in localAddressBook.getDirtyGroups()) {
|
||||
for (group in localCollection.findDirtyGroups()) {
|
||||
Logger.log.fine("Marking members of modified group $group as dirty")
|
||||
group.markMembersDirty()
|
||||
group.clearDirty(null)
|
||||
useLocal(group) {
|
||||
it.markMembersDirty()
|
||||
it.clearDirty(null)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
/* groups as separate VCards: there are group contacts and individual contacts */
|
||||
|
||||
// mark groups with changed members as dirty
|
||||
val batch = BatchOperation(localAddressBook.provider!!)
|
||||
for (contact in localAddressBook.getDirtyContacts())
|
||||
val batch = BatchOperation(localCollection.provider!!)
|
||||
for (contact in localCollection.findDirtyContacts())
|
||||
try {
|
||||
Logger.log.fine("Looking for changed group memberships of contact ${contact.fileName}")
|
||||
val cachedGroups = contact.getCachedGroupMemberships()
|
||||
@@ -215,7 +226,7 @@ class ContactsSyncManager(
|
||||
for (groupID in cachedGroups disjunct currentGroups) {
|
||||
Logger.log.fine("Marking group as dirty: $groupID")
|
||||
batch.enqueue(BatchOperation.Operation(
|
||||
ContentProviderOperation.newUpdate(localAddressBook.syncAdapterURI(ContentUris.withAppendedId(Groups.CONTENT_URI, groupID)))
|
||||
ContentProviderOperation.newUpdate(localCollection.syncAdapterURI(ContentUris.withAppendedId(Groups.CONTENT_URI, groupID)))
|
||||
.withValue(Groups.DIRTY, 1)
|
||||
.withYieldAllowed(true)
|
||||
))
|
||||
@@ -225,24 +236,26 @@ class ContactsSyncManager(
|
||||
batch.commit()
|
||||
}
|
||||
}
|
||||
|
||||
// generate UID/file name for newly created contacts
|
||||
return super.uploadDirty()
|
||||
}
|
||||
|
||||
private fun notifyDiscardedChange() {
|
||||
val notification = NotificationCompat.Builder(context)
|
||||
.setSmallIcon(R.drawable.ic_delete_light)
|
||||
.setLargeIcon(App.getLauncherBitmap(context))
|
||||
val notification = NotificationUtils.newBuilder(context, NotificationUtils.CHANNEL_SYNC_STATUS)
|
||||
.setSmallIcon(R.drawable.ic_delete_notification)
|
||||
.setContentTitle(context.getString(R.string.sync_contacts_read_only_address_book))
|
||||
.setContentText(context.resources.getQuantityString(R.plurals.sync_contacts_local_contact_changes_discarded, numDiscarded, numDiscarded))
|
||||
.setNumber(numDiscarded)
|
||||
.setSubText(account.name)
|
||||
.setCategory(NotificationCompat.CATEGORY_ERROR)
|
||||
.setPriority(NotificationCompat.PRIORITY_LOW)
|
||||
.setCategory(NotificationCompat.CATEGORY_STATUS)
|
||||
.setPriority(NotificationCompat.PRIORITY_MIN)
|
||||
.setLocalOnly(true)
|
||||
.setAutoCancel(true)
|
||||
.build()
|
||||
NotificationManagerCompat.from(context).notify("discarded_${account.name}", 0, notification)
|
||||
notificationManager.notify("discarded_${account.name}", 0, notification)
|
||||
}
|
||||
|
||||
override fun prepareUpload(resource: LocalResource): RequestBody {
|
||||
override fun prepareUpload(resource: LocalAddress): RequestBody = useLocal(resource) {
|
||||
val contact: Contact
|
||||
if (resource is LocalContact) {
|
||||
contact = resource.contact!!
|
||||
@@ -250,112 +263,71 @@ class ContactsSyncManager(
|
||||
if (groupMethod == GroupMethod.CATEGORIES) {
|
||||
// add groups as CATEGORIES
|
||||
for (groupID in resource.getGroupMemberships()) {
|
||||
try {
|
||||
provider.query(
|
||||
localAddressBook.syncAdapterURI(ContentUris.withAppendedId(Groups.CONTENT_URI, groupID)),
|
||||
arrayOf(Groups.TITLE), null, null, null
|
||||
)?.use { cursor ->
|
||||
if (cursor.moveToNext()) {
|
||||
val title = cursor.getString(0)
|
||||
if (!title.isNullOrEmpty())
|
||||
contact.categories.add(title)
|
||||
}
|
||||
provider.query(
|
||||
localCollection.syncAdapterURI(ContentUris.withAppendedId(Groups.CONTENT_URI, groupID)),
|
||||
arrayOf(Groups.TITLE), null, null, null
|
||||
)?.use { cursor ->
|
||||
if (cursor.moveToNext()) {
|
||||
val title = cursor.getString(0)
|
||||
if (!title.isNullOrEmpty())
|
||||
contact.categories.add(title)
|
||||
}
|
||||
} catch(e: RemoteException) {
|
||||
throw ContactsStorageException("Couldn't find group for adding CATEGORIES", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (resource is LocalGroup)
|
||||
contact = resource.contact!!
|
||||
else
|
||||
throw IllegalArgumentException("resource must be a LocalContact or a LocalGroup")
|
||||
throw IllegalArgumentException("resource must be LocalContact or LocalGroup")
|
||||
|
||||
Logger.log.log(Level.FINE, "Preparing upload of VCard ${resource.fileName}", contact)
|
||||
|
||||
val os = ByteArrayOutputStream()
|
||||
contact.write(if (hasVCard4) VCardVersion.V4_0 else VCardVersion.V3_0, groupMethod, os)
|
||||
|
||||
return RequestBody.create(
|
||||
RequestBody.create(
|
||||
if (hasVCard4) DavAddressBook.MIME_VCARD4 else DavAddressBook.MIME_VCARD3_UTF8,
|
||||
os.toByteArray()
|
||||
)
|
||||
}
|
||||
|
||||
override fun listRemote() {
|
||||
val addressBook = davAddressBook()
|
||||
currentDavResource = addressBook
|
||||
|
||||
// fetch list of remote VCards and build hash table to index file name
|
||||
addressBook.propfind(1, ResourceType.NAME, GetETag.NAME)
|
||||
|
||||
remoteResources = HashMap<String, DavResource>(davCollection.members.size)
|
||||
for (vCard in davCollection.members) {
|
||||
// ignore member collections
|
||||
val type = vCard.properties[ResourceType.NAME] as ResourceType?
|
||||
if (type != null && type.types.contains(ResourceType.COLLECTION))
|
||||
continue
|
||||
|
||||
val fileName = vCard.fileName()
|
||||
Logger.log.fine("Found remote VCard: $fileName")
|
||||
remoteResources[fileName] = vCard
|
||||
}
|
||||
|
||||
currentDavResource = null
|
||||
}
|
||||
|
||||
override fun downloadRemote() {
|
||||
Logger.log.info("Downloading ${toDownload.size} contacts ($MAX_MULTIGET at once)")
|
||||
|
||||
// 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 ListUtils.partition(toDownload.toList(), MAX_MULTIGET)) {
|
||||
if (Thread.interrupted())
|
||||
return
|
||||
|
||||
Logger.log.info("Downloading ${bunch.joinToString(", ")}")
|
||||
|
||||
if (bunch.size == 1) {
|
||||
// only one contact, use GET
|
||||
val remote = bunch.first()
|
||||
currentDavResource = remote
|
||||
|
||||
val body = remote.get("text/vcard;version=4.0, text/vcard;charset=utf-8;q=0.8, text/vcard;q=0.5")
|
||||
|
||||
// CardDAV servers MUST return ETag on GET [https://tools.ietf.org/html/rfc6352#section-6.3.2.3]
|
||||
val eTag = remote.properties[GetETag.NAME] as GetETag?
|
||||
if (eTag == null || eTag.eTag.isNullOrEmpty())
|
||||
throw DavException("Received CardDAV GET response without ETag for ${remote.location}")
|
||||
|
||||
body.charStream().use { reader ->
|
||||
processVCard(remote.fileName(), eTag.eTag!!, reader, downloader)
|
||||
}
|
||||
|
||||
} else {
|
||||
// multiple contacts, use multi-get
|
||||
val addressBook = davAddressBook()
|
||||
currentDavResource = addressBook
|
||||
addressBook.multiget(bunch.map { it.location }, hasVCard4)
|
||||
|
||||
// process multi-get results
|
||||
for (remote in davCollection.members) {
|
||||
currentDavResource = remote
|
||||
|
||||
val eTag = (remote.properties[GetETag.NAME] as GetETag?)?.eTag
|
||||
?: throw DavException("Received multi-get response without ETag")
|
||||
|
||||
val addressData = remote.properties[AddressData.NAME] as AddressData?
|
||||
val vCard = addressData?.vCard
|
||||
?: throw DavException("Received multi-get response without address data")
|
||||
|
||||
processVCard(remote.fileName(), eTag, StringReader(vCard), downloader)
|
||||
}
|
||||
override fun listAllRemote(callback: DavResponseCallback) =
|
||||
useRemoteCollection {
|
||||
it.propfind(1, ResourceType.NAME, GetETag.NAME, callback = callback)
|
||||
}
|
||||
|
||||
currentDavResource = null
|
||||
}
|
||||
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")
|
||||
|
||||
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")
|
||||
|
||||
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() {
|
||||
@@ -364,20 +336,18 @@ class ContactsSyncManager(
|
||||
|
||||
// remove empty groups
|
||||
Logger.log.info("Removing empty groups")
|
||||
localAddressBook.removeEmptyGroups()
|
||||
localCollection.removeEmptyGroups()
|
||||
|
||||
} else {
|
||||
/* VCard4 group handling: there are group contacts and individual contacts */
|
||||
Logger.log.info("Assigning memberships of downloaded contact groups")
|
||||
LocalGroup.applyPendingMemberships(localAddressBook)
|
||||
LocalGroup.applyPendingMemberships(localCollection)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// helpers
|
||||
|
||||
private fun davAddressBook() = davCollection as DavAddressBook
|
||||
|
||||
private fun processVCard(fileName: String, eTag: String, reader: Reader, downloader: Contact.Downloader) {
|
||||
Logger.log.info("Processing CardDAV resource $fileName")
|
||||
val contacts = Contact.fromReader(reader, downloader)
|
||||
@@ -390,78 +360,74 @@ class ContactsSyncManager(
|
||||
val newData = contacts.first()
|
||||
|
||||
if (groupMethod == GroupMethod.CATEGORIES && newData.group) {
|
||||
groupMethod = GroupMethod.GROUP_VCARDS
|
||||
Logger.log.warning("Received group VCard although group method is CATEGORIES. Deleting all groups; new group method: $groupMethod")
|
||||
localAddressBook.removeGroups()
|
||||
settings.setGroupMethod(groupMethod)
|
||||
Logger.log.warning("Received group VCard although group method is CATEGORIES. Saving as regular contact")
|
||||
newData.group = false
|
||||
}
|
||||
|
||||
// update local contact, if it exists
|
||||
var local = localResources[fileName]
|
||||
currentLocalResource = local
|
||||
if (local != null) {
|
||||
Logger.log.log(Level.INFO, "Updating $fileName in local address book", newData)
|
||||
useLocal(localCollection.findByName(fileName)) {
|
||||
var local = it
|
||||
if (local != null) {
|
||||
Logger.log.log(Level.INFO, "Updating $fileName in local address book", newData)
|
||||
|
||||
if (local is LocalGroup && newData.group) {
|
||||
// update group
|
||||
local.eTag = eTag
|
||||
local.updateFromServer(newData)
|
||||
syncResult.stats.numUpdates++
|
||||
if (local is LocalGroup && newData.group) {
|
||||
// update group
|
||||
local.eTag = eTag
|
||||
local.flags = LocalResource.FLAG_REMOTELY_PRESENT
|
||||
local.update(newData)
|
||||
syncResult.stats.numUpdates++
|
||||
|
||||
} else if (local is LocalContact && !newData.group) {
|
||||
// update contact
|
||||
local.eTag = eTag
|
||||
local.update(newData)
|
||||
syncResult.stats.numUpdates++
|
||||
} else if (local is LocalContact && !newData.group) {
|
||||
// update contact
|
||||
local.eTag = eTag
|
||||
local.flags = LocalResource.FLAG_REMOTELY_PRESENT
|
||||
local.update(newData)
|
||||
syncResult.stats.numUpdates++
|
||||
|
||||
} else {
|
||||
// group has become an individual contact or vice versa
|
||||
local.delete()
|
||||
local = null
|
||||
}
|
||||
}
|
||||
|
||||
if (local == null) {
|
||||
if (newData.group) {
|
||||
Logger.log.log(Level.INFO, "Creating local group", newData)
|
||||
val group = LocalGroup(localAddressBook, newData, fileName, eTag)
|
||||
currentLocalResource = group
|
||||
group.create()
|
||||
|
||||
local = group
|
||||
} else {
|
||||
Logger.log.log(Level.INFO, "Creating local contact", newData)
|
||||
val contact = LocalContact(localAddressBook, newData, fileName, eTag)
|
||||
currentLocalResource = contact
|
||||
contact.create()
|
||||
|
||||
local = contact
|
||||
}
|
||||
syncResult.stats.numInserts++
|
||||
}
|
||||
|
||||
if (groupMethod == GroupMethod.CATEGORIES && local is LocalContact) {
|
||||
// VCard3: update group memberships from CATEGORIES
|
||||
currentLocalResource = local
|
||||
|
||||
val batch = BatchOperation(provider)
|
||||
Logger.log.log(Level.FINE, "Removing contact group memberships")
|
||||
local.removeGroupMemberships(batch)
|
||||
|
||||
for (category in local.contact!!.categories) {
|
||||
val groupID = localAddressBook.findOrCreateGroup(category)
|
||||
Logger.log.log(Level.FINE, "Adding membership in group $category ($groupID)")
|
||||
local.addToGroup(batch, groupID)
|
||||
} else {
|
||||
// group has become an individual contact or vice versa
|
||||
local.delete()
|
||||
local = null
|
||||
}
|
||||
}
|
||||
|
||||
batch.commit()
|
||||
}
|
||||
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)) {
|
||||
it.add()
|
||||
local = it
|
||||
}
|
||||
} else {
|
||||
Logger.log.log(Level.INFO, "Creating local contact", newData)
|
||||
useLocal(LocalContact(localCollection, newData, fileName, eTag, LocalResource.FLAG_REMOTELY_PRESENT)) {
|
||||
it.add()
|
||||
local = it
|
||||
}
|
||||
}
|
||||
syncResult.stats.numInserts++
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && Build.VERSION.SDK_INT < Build.VERSION_CODES.O && local is LocalContact)
|
||||
if (groupMethod == GroupMethod.CATEGORIES)
|
||||
(local as? LocalContact)?.let { localContact ->
|
||||
// VCard3: update group memberships from CATEGORIES
|
||||
val batch = BatchOperation(provider)
|
||||
Logger.log.log(Level.FINE, "Removing contact group memberships")
|
||||
localContact.removeGroupMemberships(batch)
|
||||
|
||||
for (category in localContact.contact!!.categories) {
|
||||
val groupID = localCollection.findOrCreateGroup(category)
|
||||
Logger.log.log(Level.FINE, "Adding membership in group $category ($groupID)")
|
||||
localContact.addToGroup(batch, groupID)
|
||||
}
|
||||
|
||||
batch.commit()
|
||||
}
|
||||
|
||||
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
|
||||
local.updateHashCode(null)
|
||||
|
||||
currentLocalResource = null
|
||||
(local as? LocalContact)?.updateHashCode(null)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -485,12 +451,7 @@ class ContactsSyncManager(
|
||||
}
|
||||
|
||||
// authenticate only against a certain host, and only upon request
|
||||
val username = settings.username()
|
||||
val password = settings.password()
|
||||
val builder = if (username != null && password != null)
|
||||
HttpClient.Builder(context, baseUrl.host(), username, password)
|
||||
else
|
||||
HttpClient.Builder(context)
|
||||
val builder = HttpClient.Builder(context, baseUrl.host(), accountSettings.credentials())
|
||||
|
||||
// allow redirects
|
||||
builder.followRedirects(true)
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
* which accompanies this distribution, and is available at
|
||||
* http://www.gnu.org/licenses/gpl.html
|
||||
*/
|
||||
|
||||
package at.bitfire.davdroid.syncadapter
|
||||
|
||||
import android.accounts.AbstractAccountAuthenticator
|
||||
|
||||
@@ -8,30 +8,42 @@
|
||||
|
||||
package at.bitfire.davdroid.syncadapter
|
||||
|
||||
import android.Manifest
|
||||
import android.accounts.Account
|
||||
import android.app.PendingIntent
|
||||
import android.app.Service
|
||||
import android.content.*
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.wifi.WifiManager
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.support.v4.app.NotificationCompat
|
||||
import android.support.v4.app.NotificationManagerCompat
|
||||
import android.support.v4.content.ContextCompat
|
||||
import at.bitfire.davdroid.AccountSettings
|
||||
import at.bitfire.davdroid.App
|
||||
import at.bitfire.davdroid.Constants
|
||||
import at.bitfire.davdroid.R
|
||||
import at.bitfire.davdroid.log.Logger
|
||||
import at.bitfire.davdroid.settings.ISettings
|
||||
import at.bitfire.davdroid.settings.Settings
|
||||
import at.bitfire.davdroid.ui.PermissionsActivity
|
||||
import org.apache.commons.collections4.IteratorUtils
|
||||
import at.bitfire.davdroid.ui.AccountActivity
|
||||
import at.bitfire.davdroid.ui.AccountSettingsActivity
|
||||
import at.bitfire.davdroid.ui.NotificationUtils
|
||||
import java.lang.ref.WeakReference
|
||||
import java.util.*
|
||||
import java.util.logging.Level
|
||||
|
||||
abstract class SyncAdapterService: Service() {
|
||||
|
||||
abstract protected fun syncAdapter(): AbstractThreadedSyncAdapter
|
||||
companion object {
|
||||
/** Keep a list of running syncs to block multiple calls at the same time,
|
||||
* like run by some devices. Weak references are used for the case that a thread
|
||||
* is terminated and the `finally` block which cleans up [runningSyncs] is not
|
||||
* executed. */
|
||||
private val runningSyncs = mutableListOf<WeakReference<Pair<String, Account>>>()
|
||||
}
|
||||
|
||||
protected abstract fun syncAdapter(): AbstractThreadedSyncAdapter
|
||||
|
||||
override fun onBind(intent: Intent?) = syncAdapter().syncAdapterBinder!!
|
||||
|
||||
@@ -44,7 +56,7 @@ abstract class SyncAdapterService: Service() {
|
||||
private val syncPluginLoader = ServiceLoader.load(ISyncPlugin::class.java)
|
||||
}
|
||||
|
||||
private val syncPlugins = IteratorUtils.toList(syncPluginLoader.iterator())
|
||||
private val syncPlugins = syncPluginLoader.iterator().asSequence().toList()
|
||||
|
||||
init {
|
||||
syncPlugins.forEach { Logger.log.info("Registered sync plugin: ${it::class.java.name}") }
|
||||
@@ -56,43 +68,55 @@ abstract class SyncAdapterService: Service() {
|
||||
override fun onPerformSync(account: Account, extras: Bundle, authority: String, provider: ContentProviderClient, syncResult: SyncResult) {
|
||||
Logger.log.log(Level.INFO, "$authority sync of $account has been initiated", extras.keySet().joinToString(", "))
|
||||
|
||||
// required for dav4android (ServiceLoader)
|
||||
Thread.currentThread().contextClassLoader = context.classLoader
|
||||
|
||||
// load app settings
|
||||
val settings = Settings.getInstance(context)
|
||||
if (settings == null) {
|
||||
syncResult.databaseError = true
|
||||
Logger.log.severe("Couldn't connect to Settings service, aborting sync")
|
||||
return
|
||||
} else settings.use { settings ->
|
||||
val runSync = syncPlugins.all { it.beforeSync(context, settings, syncResult) }
|
||||
|
||||
if (runSync)
|
||||
sync(settings, account, extras, authority, provider, syncResult)
|
||||
|
||||
syncPlugins.forEach { it.afterSync(context, settings, syncResult) }
|
||||
// prevent multiple syncs of the same authority to be run for the same account
|
||||
val currentSync = Pair(authority, account)
|
||||
synchronized(runningSyncs) {
|
||||
if (runningSyncs.any { it.get() == currentSync }) {
|
||||
Logger.log.warning("There's already another $authority sync running for $account, aborting")
|
||||
return
|
||||
}
|
||||
runningSyncs += WeakReference(currentSync)
|
||||
}
|
||||
Logger.log.info("Sync for $authority complete")
|
||||
|
||||
try {
|
||||
// required for dav4android (ServiceLoader)
|
||||
Thread.currentThread().contextClassLoader = context.classLoader
|
||||
|
||||
// load app settings
|
||||
Settings.getInstance(context).use { settings ->
|
||||
if (settings == null) {
|
||||
syncResult.databaseError = true
|
||||
Logger.log.severe("Couldn't connect to Settings service, aborting sync")
|
||||
return
|
||||
}
|
||||
|
||||
val runSync = syncPlugins.all { it.beforeSync(context, settings, syncResult) }
|
||||
|
||||
if (runSync) {
|
||||
SyncManager.cancelNotifications(NotificationManagerCompat.from(context), authority, account)
|
||||
sync(settings, account, extras, authority, provider, syncResult)
|
||||
}
|
||||
|
||||
syncPlugins.forEach { it.afterSync(context, settings, syncResult) }
|
||||
}
|
||||
} finally {
|
||||
synchronized(runningSyncs) {
|
||||
runningSyncs.removeAll { it.get() == null || it.get() == currentSync }
|
||||
}
|
||||
}
|
||||
|
||||
Logger.log.info("Sync for $currentSync finished")
|
||||
}
|
||||
|
||||
override fun onSecurityException(account: Account, extras: Bundle, authority: String, syncResult: SyncResult) {
|
||||
Logger.log.log(Level.WARNING, "Security exception when opening content provider for $authority")
|
||||
syncResult.databaseError = true
|
||||
|
||||
val intent = Intent(context, PermissionsActivity::class.java)
|
||||
val intent = Intent(context, AccountActivity::class.java)
|
||||
intent.putExtra(AccountActivity.EXTRA_ACCOUNT, account)
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
|
||||
val notify = NotificationCompat.Builder(context)
|
||||
.setSmallIcon(R.drawable.ic_error_light)
|
||||
.setLargeIcon(App.getLauncherBitmap(context))
|
||||
.setContentTitle(context.getString(R.string.sync_error_permissions))
|
||||
.setContentText(context.getString(R.string.sync_error_permissions_text))
|
||||
.setContentIntent(PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT))
|
||||
.setCategory(NotificationCompat.CATEGORY_ERROR)
|
||||
.build()
|
||||
val nm = NotificationManagerCompat.from(context)
|
||||
nm.notify(Constants.NOTIFICATION_PERMISSIONS, notify)
|
||||
notifyPermissions(intent)
|
||||
}
|
||||
|
||||
protected fun checkSyncConditions(settings: AccountSettings): Boolean {
|
||||
@@ -105,6 +129,17 @@ abstract class SyncAdapterService: Service() {
|
||||
}
|
||||
|
||||
settings.getSyncWifiOnlySSIDs()?.let { onlySSIDs ->
|
||||
// getting the WiFi name requires location permission (and active location services) since Android 8.1
|
||||
// see https://issuetracker.google.com/issues/70633700
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1 &&
|
||||
ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
|
||||
val intent = Intent(context, AccountSettingsActivity::class.java)
|
||||
intent.putExtra(AccountActivity.EXTRA_ACCOUNT, settings.account)
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
|
||||
notifyPermissions(intent)
|
||||
}
|
||||
|
||||
val wifi = context.applicationContext.getSystemService(WIFI_SERVICE) as WifiManager
|
||||
val info = wifi.connectionInfo
|
||||
if (info == null || !onlySSIDs.contains(info.ssid.trim('"'))) {
|
||||
@@ -116,6 +151,18 @@ abstract class SyncAdapterService: Service() {
|
||||
return true
|
||||
}
|
||||
|
||||
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))
|
||||
.setContentText(context.getString(R.string.sync_error_permissions_text))
|
||||
.setContentIntent(PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT))
|
||||
.setCategory(NotificationCompat.CATEGORY_ERROR)
|
||||
.setAutoCancel(true)
|
||||
.build()
|
||||
NotificationManagerCompat.from(context).notify(NotificationUtils.NOTIFY_PERMISSIONS, notify)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -8,10 +8,17 @@
|
||||
package at.bitfire.davdroid.syncadapter
|
||||
|
||||
import android.accounts.Account
|
||||
import android.app.PendingIntent
|
||||
import android.content.*
|
||||
import android.content.pm.PackageManager
|
||||
import android.database.DatabaseUtils
|
||||
import android.graphics.drawable.BitmapDrawable
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.support.v4.app.NotificationCompat
|
||||
import android.support.v4.app.NotificationManagerCompat
|
||||
import at.bitfire.davdroid.AccountSettings
|
||||
import at.bitfire.davdroid.R
|
||||
import at.bitfire.davdroid.log.Logger
|
||||
import at.bitfire.davdroid.model.CollectionInfo
|
||||
import at.bitfire.davdroid.model.ServiceDB
|
||||
@@ -19,9 +26,11 @@ import at.bitfire.davdroid.model.ServiceDB.Collections
|
||||
import at.bitfire.davdroid.model.ServiceDB.Services
|
||||
import at.bitfire.davdroid.resource.LocalTaskList
|
||||
import at.bitfire.davdroid.settings.ISettings
|
||||
import at.bitfire.davdroid.ui.NotificationUtils
|
||||
import at.bitfire.ical4android.AndroidTaskList
|
||||
import at.bitfire.ical4android.TaskProvider
|
||||
import org.dmfs.provider.tasks.TaskContract
|
||||
import okhttp3.HttpUrl
|
||||
import org.dmfs.tasks.contract.TaskContract
|
||||
import java.util.logging.Level
|
||||
|
||||
/**
|
||||
@@ -29,34 +38,58 @@ import java.util.logging.Level
|
||||
*/
|
||||
class TasksSyncAdapterService: SyncAdapterService() {
|
||||
|
||||
override fun syncAdapter() = SyncAdapter(this)
|
||||
override fun syncAdapter() = TasksSyncAdapter(this)
|
||||
|
||||
|
||||
protected class SyncAdapter(
|
||||
class TasksSyncAdapter(
|
||||
context: Context
|
||||
): SyncAdapterService.SyncAdapter(context) {
|
||||
): SyncAdapter(context) {
|
||||
|
||||
override fun sync(settings: ISettings, account: Account, extras: Bundle, authority: String, provider: ContentProviderClient, syncResult: SyncResult) {
|
||||
try {
|
||||
val taskProvider = TaskProvider.fromProviderClient(provider)
|
||||
val settings = AccountSettings(context, settings, account)
|
||||
val taskProvider = TaskProvider.fromProviderClient(context, provider)
|
||||
val accountSettings = AccountSettings(context, settings, account)
|
||||
/* don't run sync if
|
||||
- sync conditions (e.g. "sync only in WiFi") are not met AND
|
||||
- this is is an automatic sync (i.e. manual syncs are run regardless of sync conditions)
|
||||
*/
|
||||
if (!extras.containsKey(ContentResolver.SYNC_EXTRAS_MANUAL) && !checkSyncConditions(settings))
|
||||
if (!extras.containsKey(ContentResolver.SYNC_EXTRAS_MANUAL) && !checkSyncConditions(accountSettings))
|
||||
return
|
||||
|
||||
updateLocalTaskLists(taskProvider, account, settings)
|
||||
updateLocalTaskLists(taskProvider, account, accountSettings)
|
||||
|
||||
for (taskList in AndroidTaskList.find(account, taskProvider, LocalTaskList.Factory, "${TaskContract.TaskLists.SYNC_ENABLED}!=0", null)) {
|
||||
Logger.log.info("Synchronizing task list #${taskList.id} [${taskList.syncId}]")
|
||||
TasksSyncManager(context, account, settings, extras, authority, syncResult, taskProvider, taskList).use {
|
||||
TasksSyncManager(context, settings, account, accountSettings, extras, authority, syncResult, taskList).use {
|
||||
it.performSync()
|
||||
}
|
||||
}
|
||||
} catch (e: TaskProvider.ProviderTooOldException) {
|
||||
val nm = NotificationManagerCompat.from(context)
|
||||
val message = context.getString(R.string.sync_error_opentasks_required_version, e.provider.minVersionName, e.installedVersionName)
|
||||
val notify = NotificationUtils.newBuilder(context)
|
||||
.setSmallIcon(R.drawable.ic_sync_error_notification)
|
||||
.setContentTitle(context.getString(R.string.sync_error_opentasks_too_old))
|
||||
.setContentText(message)
|
||||
.setStyle(NotificationCompat.BigTextStyle().bigText(message))
|
||||
.setCategory(NotificationCompat.CATEGORY_ERROR)
|
||||
|
||||
try {
|
||||
val icon = context.packageManager.getApplicationIcon(e.provider.packageName)
|
||||
if (icon is BitmapDrawable)
|
||||
notify.setLargeIcon(icon.bitmap)
|
||||
} catch(ignored: PackageManager.NameNotFoundException) {}
|
||||
|
||||
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=${e.provider.packageName}"))
|
||||
if (intent.resolveActivity(context.packageManager) != null)
|
||||
notify .setContentIntent(PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT))
|
||||
.setAutoCancel(true)
|
||||
|
||||
nm.notify(NotificationUtils.NOTIFY_OPENTASKS, notify.build())
|
||||
syncResult.databaseError = true
|
||||
} catch (e: Exception) {
|
||||
Logger.log.log(Level.SEVERE, "Couldn't sync task lists", e)
|
||||
syncResult.databaseError = true
|
||||
}
|
||||
|
||||
Logger.log.info("Task sync complete")
|
||||
@@ -76,8 +109,8 @@ class TasksSyncAdapterService: SyncAdapterService() {
|
||||
null
|
||||
}
|
||||
|
||||
fun remoteTaskLists(service: Long?): MutableMap<String, CollectionInfo> {
|
||||
val collections = mutableMapOf<String, CollectionInfo>()
|
||||
fun remoteTaskLists(service: Long?): MutableMap<HttpUrl, CollectionInfo> {
|
||||
val collections = mutableMapOf<HttpUrl, CollectionInfo>()
|
||||
service?.let {
|
||||
db.query(Collections._TABLE, null,
|
||||
"${Collections.SERVICE_ID}=? AND ${Collections.SUPPORTS_VTODO}!=0 AND ${Collections.SYNC}",
|
||||
@@ -101,7 +134,8 @@ class TasksSyncAdapterService: SyncAdapterService() {
|
||||
val updateColors = settings.getManageCalendarColors()
|
||||
|
||||
for (list in AndroidTaskList.find(account, provider, LocalTaskList.Factory, null, null))
|
||||
list.syncId?.let { url ->
|
||||
list.syncId?.let {
|
||||
val url = HttpUrl.parse(it)!!
|
||||
val info = remote[url]
|
||||
if (info == null) {
|
||||
Logger.log.fine("Deleting obsolete local task list $url")
|
||||
|
||||
@@ -14,150 +14,122 @@ import android.content.SyncResult
|
||||
import android.os.Bundle
|
||||
import at.bitfire.dav4android.DavCalendar
|
||||
import at.bitfire.dav4android.DavResource
|
||||
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.Constants
|
||||
import at.bitfire.davdroid.R
|
||||
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
|
||||
import at.bitfire.davdroid.settings.ISettings
|
||||
import at.bitfire.ical4android.InvalidCalendarException
|
||||
import at.bitfire.ical4android.Task
|
||||
import at.bitfire.ical4android.TaskProvider
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.RequestBody
|
||||
import org.apache.commons.collections4.ListUtils
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.Reader
|
||||
import java.io.StringReader
|
||||
import java.util.*
|
||||
import java.util.logging.Level
|
||||
|
||||
/**
|
||||
* Synchronization manager for CalDAV collections; handles tasks (VTODO)
|
||||
*/
|
||||
class TasksSyncManager(
|
||||
context: Context,
|
||||
settings: ISettings,
|
||||
account: Account,
|
||||
settings: AccountSettings,
|
||||
accountSettings: AccountSettings,
|
||||
extras: Bundle,
|
||||
authority: String,
|
||||
syncResult: SyncResult,
|
||||
val provider: TaskProvider,
|
||||
val localTaskList: LocalTaskList
|
||||
): SyncManager(context, account, settings, extras, authority, syncResult, "taskList/${localTaskList.id}") {
|
||||
|
||||
val MAX_MULTIGET = 30
|
||||
|
||||
|
||||
init {
|
||||
localCollection = localTaskList
|
||||
}
|
||||
|
||||
override fun notificationId() = Constants.NOTIFICATION_TASK_SYNC
|
||||
|
||||
override fun getSyncErrorTitle() = context.getString(R.string.sync_error_tasks, account.name)!!
|
||||
|
||||
localCollection: LocalTaskList
|
||||
): SyncManager<LocalTask, LocalTaskList, DavCalendar>(context, settings, account, accountSettings, extras, authority, syncResult, localCollection) {
|
||||
|
||||
override fun prepare(): Boolean {
|
||||
collectionURL = HttpUrl.parse(localTaskList.syncId ?: return false) ?: return false
|
||||
collectionURL = HttpUrl.parse(localCollection.syncId ?: return false) ?: return false
|
||||
davCollection = DavCalendar(httpClient.okHttpClient, collectionURL)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun queryCapabilities() {
|
||||
davCollection.propfind(0, GetCTag.NAME)
|
||||
}
|
||||
|
||||
override fun prepareUpload(resource: LocalResource): RequestBody {
|
||||
if (resource is LocalTask) {
|
||||
val task = requireNotNull(resource.task)
|
||||
Logger.log.log(Level.FINE, "Preparing upload of task ${resource.fileName}", task)
|
||||
|
||||
val os = ByteArrayOutputStream()
|
||||
task.write(os)
|
||||
|
||||
return RequestBody.create(
|
||||
DavCalendar.MIME_ICALENDAR,
|
||||
os.toByteArray()
|
||||
)
|
||||
} else
|
||||
throw IllegalArgumentException("resource must be a LocalTask")
|
||||
}
|
||||
|
||||
override fun listRemote() {
|
||||
// fetch list of remote VTODOs and build hash table to index file name
|
||||
val calendar = davCalendar()
|
||||
currentDavResource = calendar
|
||||
calendar.calendarQuery("VTODO", null, null)
|
||||
|
||||
remoteResources = HashMap<String, DavResource>(davCollection.members.size)
|
||||
for (vCard in davCollection.members) {
|
||||
val fileName = vCard.fileName()
|
||||
Logger.log.fine("Found remote VTODO: $fileName")
|
||||
remoteResources[fileName] = vCard
|
||||
}
|
||||
|
||||
currentDavResource = null
|
||||
}
|
||||
|
||||
override fun downloadRemote() {
|
||||
Logger.log.info("Downloading ${toDownload.size} tasks ($MAX_MULTIGET at once)")
|
||||
|
||||
// download new/updated iCalendars from server
|
||||
for (bunch in ListUtils.partition(toDownload.toList(), MAX_MULTIGET)) {
|
||||
if (Thread.interrupted())
|
||||
return
|
||||
|
||||
Logger.log.info("Downloading ${bunch.joinToString(", ")}")
|
||||
|
||||
if (bunch.size == 1) {
|
||||
// only one contact, use GET
|
||||
val remote = bunch.first()
|
||||
currentDavResource = remote
|
||||
|
||||
val body = remote.get("text/calendar")
|
||||
|
||||
// CalDAV servers MUST return ETag on GET [https://tools.ietf.org/html/rfc4791#section-5.3.4]
|
||||
val eTag = remote.properties[GetETag.NAME] as GetETag?
|
||||
if (eTag == null || eTag.eTag.isNullOrEmpty())
|
||||
throw DavException("Received CalDAV GET response without ETag for ${remote.location}")
|
||||
|
||||
body.charStream().use { reader ->
|
||||
processVTodo(remote.fileName(), eTag.eTag!!, reader)
|
||||
}
|
||||
|
||||
} else {
|
||||
// multiple contacts, use multi-get
|
||||
val calendar = davCalendar()
|
||||
currentDavResource = calendar
|
||||
calendar.multiget(bunch.map { it.location })
|
||||
|
||||
// process multiget results
|
||||
for (remote in davCollection.members) {
|
||||
currentDavResource = remote
|
||||
|
||||
val eTag = (remote.properties[GetETag.NAME] as GetETag?)?.eTag
|
||||
?: throw DavException("Received multi-get response without ETag")
|
||||
|
||||
val calendarData = remote.properties[CalendarData.NAME] as CalendarData?
|
||||
val iCalendar = calendarData?.iCalendar
|
||||
?: throw DavException("Received multi-get response without task data")
|
||||
|
||||
processVTodo(remote.fileName(), eTag, StringReader(iCalendar))
|
||||
override fun queryCapabilities() =
|
||||
useRemoteCollection {
|
||||
var syncState: SyncState? = null
|
||||
it.propfind(0, GetCTag.NAME, SyncToken.NAME) { response, relation ->
|
||||
if (relation == Response.HrefRelation.SELF)
|
||||
syncState = syncState(response)
|
||||
}
|
||||
syncState
|
||||
}
|
||||
|
||||
currentDavResource = null
|
||||
override fun syncAlgorithm() = SyncAlgorithm.PROPFIND_REPORT
|
||||
|
||||
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)
|
||||
|
||||
val os = ByteArrayOutputStream()
|
||||
task.write(os)
|
||||
|
||||
RequestBody.create(
|
||||
DavCalendar.MIME_ICALENDAR_UTF8,
|
||||
os.toByteArray()
|
||||
)
|
||||
}
|
||||
|
||||
override fun listAllRemote(callback: DavResponseCallback) {
|
||||
useRemoteCollection { remote ->
|
||||
Logger.log.info("Querying tasks")
|
||||
remote.calendarQuery("VTODO", null, null, callback)
|
||||
}
|
||||
}
|
||||
|
||||
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")
|
||||
|
||||
response.body()!!.use {
|
||||
processVTodo(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")
|
||||
|
||||
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 davCalendar() = davCollection as DavCalendar
|
||||
|
||||
private fun processVTodo(fileName: String, eTag: String, reader: Reader) {
|
||||
val tasks: List<Task>
|
||||
try {
|
||||
@@ -171,22 +143,22 @@ class TasksSyncManager(
|
||||
val newData = tasks.first()
|
||||
|
||||
// update local task, if it exists
|
||||
val localTask = localResources[fileName] as LocalTask?
|
||||
currentLocalResource = localTask
|
||||
if (localTask != null) {
|
||||
Logger.log.info("Updating $fileName in local tasklist")
|
||||
localTask.eTag = eTag
|
||||
localTask.update(newData)
|
||||
syncResult.stats.numUpdates++
|
||||
} else {
|
||||
Logger.log.info("Adding $fileName to local task list")
|
||||
val newTask = LocalTask(localTaskList, newData, fileName, eTag)
|
||||
currentLocalResource = newTask
|
||||
newTask.add()
|
||||
syncResult.stats.numInserts++
|
||||
useLocal(localCollection.findByName(fileName)) { local ->
|
||||
if (local != null) {
|
||||
Logger.log.info("Updating $fileName in local task list")
|
||||
local.eTag = eTag
|
||||
local.update(newData)
|
||||
syncResult.stats.numUpdates++
|
||||
} else {
|
||||
Logger.log.info("Adding $fileName to local task list")
|
||||
useLocal(LocalTask(localCollection, newData, fileName, eTag, LocalResource.FLAG_REMOTELY_PRESENT)) {
|
||||
it.add()
|
||||
}
|
||||
syncResult.stats.numInserts++
|
||||
}
|
||||
}
|
||||
} else
|
||||
Logger.log.severe("Received VCALENDAR with not exactly one VTODO; ignoring $fileName")
|
||||
Logger.log.info("Received VCALENDAR with not exactly one VTODO; ignoring $fileName")
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -8,86 +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", "2.x", "https://ical4j.github.io/",
|
||||
"Ben Fortuna", R.string.about_license_info_no_warranty, "bsd-3clause.html"
|
||||
), ComponentInfo(
|
||||
"OkHttp", null, "https://square.github.io/okhttp/",
|
||||
"Square, Inc.", R.string.about_license_info_no_warranty, "apache2.html"
|
||||
), ComponentInfo(
|
||||
"Project Lombok", null, "https://projectlombok.org/",
|
||||
"The Project Lombok Authors", R.string.about_license_info_no_warranty, "mit.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)
|
||||
}
|
||||
|
||||
|
||||
@@ -95,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 {
|
||||
val KEY_POSITION = "position"
|
||||
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(context, 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) {
|
||||
|
||||
var content: Spanned? = null
|
||||
|
||||
override fun onStartLoading() {
|
||||
if (content == null)
|
||||
forceLoad()
|
||||
else
|
||||
deliverResult(content)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -42,16 +42,16 @@ import at.bitfire.davdroid.model.ServiceDB.Collections
|
||||
import at.bitfire.davdroid.resource.LocalAddressBook
|
||||
import at.bitfire.davdroid.resource.LocalTaskList
|
||||
import at.bitfire.ical4android.TaskProvider
|
||||
import at.bitfire.vcard4android.ContactsStorageException
|
||||
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> {
|
||||
|
||||
companion object {
|
||||
@JvmField val EXTRA_ACCOUNT = "account"
|
||||
const val EXTRA_ACCOUNT = "account"
|
||||
|
||||
private fun requestSync(context: Context, account: Account) {
|
||||
val authorities = arrayOf(
|
||||
@@ -73,11 +73,15 @@ class AccountActivity: AppCompatActivity(), Toolbar.OnMenuItemClickListener, Pop
|
||||
lateinit var account: Account
|
||||
private var accountInfo: AccountInfo? = null
|
||||
|
||||
private val dbExecutor = Executors.newSingleThreadExecutor()
|
||||
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
account = intent.getParcelableExtra(EXTRA_ACCOUNT)
|
||||
// account may be a DAVdroid address book account -> use main account in this case
|
||||
account = LocalAddressBook.mainAccount(this,
|
||||
requireNotNull(intent.getParcelableExtra(EXTRA_ACCOUNT)))
|
||||
title = account.name
|
||||
|
||||
setContentView(R.layout.activity_account)
|
||||
@@ -106,6 +110,12 @@ class AccountActivity: AppCompatActivity(), Toolbar.OnMenuItemClickListener, Pop
|
||||
loaderManager.initLoader(0, null, this)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
|
||||
dbExecutor.shutdown()
|
||||
}
|
||||
|
||||
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
|
||||
if (grantResults.any { it == PackageManager.PERMISSION_GRANTED })
|
||||
// we've got additional permissions; try to load everything again
|
||||
@@ -141,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 ->
|
||||
@@ -192,39 +202,63 @@ class AccountActivity: AppCompatActivity(), Toolbar.OnMenuItemClickListener, Pop
|
||||
val info = adapter.getItem(position)
|
||||
val nowChecked = !info.selected
|
||||
|
||||
OpenHelper(this@AccountActivity).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()
|
||||
|
||||
if (nowChecked && info.type == CollectionInfo.Type.ADDRESS_BOOK && info.readOnly)
|
||||
Snackbar.make(parent, R.string.account_read_only_address_book_selected, Snackbar.LENGTH_LONG).show()
|
||||
}
|
||||
|
||||
private val onItemLongClickListener = AdapterView.OnItemLongClickListener { parent, view, position, _ ->
|
||||
val list = parent as ListView
|
||||
val adapter = list.adapter as ArrayAdapter<CollectionInfo>
|
||||
val info = adapter.getItem(position)
|
||||
|
||||
val popup = PopupMenu(this@AccountActivity, view)
|
||||
private val onActionOverflowListener = { anchor: View, info: CollectionInfo ->
|
||||
val popup = PopupMenu(this, anchor, Gravity.RIGHT)
|
||||
popup.inflate(R.menu.account_collection_operations)
|
||||
popup.setOnMenuItemClickListener({ item ->
|
||||
|
||||
with(popup.menu.findItem(R.id.force_read_only)) {
|
||||
if (info.readOnly)
|
||||
isVisible = false
|
||||
else
|
||||
isChecked = info.forceReadOnly
|
||||
}
|
||||
|
||||
popup.setOnMenuItemClickListener { item ->
|
||||
when (item.itemId) {
|
||||
R.id.force_read_only -> {
|
||||
val nowChecked = !item.isChecked
|
||||
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()))
|
||||
|
||||
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
|
||||
@@ -244,6 +278,8 @@ class AccountActivity: AppCompatActivity(), Toolbar.OnMenuItemClickListener, Pop
|
||||
}
|
||||
|
||||
val intent = Intent(Intent.ACTION_VIEW, uri)
|
||||
info.displayName?.let { intent.putExtra("title", it) }
|
||||
info.color?.let { intent.putExtra("color", it) }
|
||||
if (packageManager.resolveActivity(intent, 0) != null)
|
||||
startActivity(intent)
|
||||
else {
|
||||
@@ -251,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()
|
||||
}
|
||||
@@ -297,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
|
||||
|
||||
@@ -309,7 +349,6 @@ class AccountActivity: AppCompatActivity(), Toolbar.OnMenuItemClickListener, Pop
|
||||
adapter.addAll(carddav.collections)
|
||||
address_books.adapter = adapter
|
||||
address_books.onItemClickListener = onItemClickListener
|
||||
address_books.onItemLongClickListener = onItemLongClickListener
|
||||
|
||||
View.VISIBLE
|
||||
} ?: View.GONE
|
||||
@@ -326,7 +365,6 @@ class AccountActivity: AppCompatActivity(), Toolbar.OnMenuItemClickListener, Pop
|
||||
adapter.addAll(caldav.collections.filter { it.type == CollectionInfo.Type.CALENDAR })
|
||||
calendars.adapter = adapter
|
||||
calendars.onItemClickListener = onItemClickListener
|
||||
calendars.onItemLongClickListener = onItemLongClickListener
|
||||
|
||||
View.VISIBLE
|
||||
} ?: View.GONE
|
||||
@@ -344,6 +382,33 @@ class AccountActivity: AppCompatActivity(), Toolbar.OnMenuItemClickListener, Pop
|
||||
else
|
||||
View.GONE
|
||||
} ?: View.GONE
|
||||
|
||||
// ask for permissions
|
||||
val requiredPermissions = mutableSetOf<String>()
|
||||
info?.carddav?.let { carddav ->
|
||||
if (carddav.collections.any { it.type == CollectionInfo.Type.ADDRESS_BOOK }) {
|
||||
requiredPermissions += Manifest.permission.READ_CONTACTS
|
||||
requiredPermissions += Manifest.permission.WRITE_CONTACTS
|
||||
}
|
||||
}
|
||||
|
||||
info?.caldav?.let { caldav ->
|
||||
if (caldav.collections.any { it.type == CollectionInfo.Type.CALENDAR }) {
|
||||
requiredPermissions += Manifest.permission.READ_CALENDAR
|
||||
requiredPermissions += Manifest.permission.WRITE_CALENDAR
|
||||
|
||||
if (LocalTaskList.tasksProviderAvailable(this)) {
|
||||
requiredPermissions += TaskProvider.PERMISSION_READ_TASKS
|
||||
requiredPermissions += TaskProvider.PERMISSION_WRITE_TASKS
|
||||
}
|
||||
}
|
||||
if (caldav.collections.any { it.type == CollectionInfo.Type.WEBCAL })
|
||||
requiredPermissions += Manifest.permission.READ_CALENDAR
|
||||
}
|
||||
|
||||
val askPermissions = requiredPermissions.filter { ActivityCompat.checkSelfPermission(this, it) != PackageManager.PERMISSION_GRANTED }
|
||||
if (askPermissions.isNotEmpty())
|
||||
ActivityCompat.requestPermissions(this, askPermissions.toTypedArray(), 0)
|
||||
}
|
||||
|
||||
override fun onLoaderReset(loader: Loader<AccountInfo>) {
|
||||
@@ -353,40 +418,55 @@ class AccountActivity: AppCompatActivity(), Toolbar.OnMenuItemClickListener, Pop
|
||||
|
||||
|
||||
class AccountLoader(
|
||||
val activity: AccountActivity,
|
||||
context: Context,
|
||||
val account: Account
|
||||
): AsyncTaskLoader<AccountInfo>(activity), DavService.RefreshingStatusListener, ServiceConnection, SyncStatusObserver {
|
||||
): AsyncTaskLoader<AccountInfo>(context), DavService.RefreshingStatusListener, SyncStatusObserver {
|
||||
|
||||
private var syncStatusListener: Any? = null
|
||||
|
||||
private var davServiceConn: ServiceConnection? = null
|
||||
private var davService: DavService.InfoBinder? = null
|
||||
private lateinit var syncStatusListener: Any
|
||||
|
||||
override fun onStartLoading() {
|
||||
syncStatusListener = ContentResolver.addStatusChangeListener(ContentResolver.SYNC_OBSERVER_TYPE_ACTIVE, this)
|
||||
context.bindService(Intent(context, DavService::class.java), this, Context.BIND_AUTO_CREATE)
|
||||
// get notified when sync status changes
|
||||
if (syncStatusListener == null)
|
||||
syncStatusListener = ContentResolver.addStatusChangeListener(ContentResolver.SYNC_OBSERVER_TYPE_ACTIVE, this)
|
||||
|
||||
// bind to DavService to get notified when it's running
|
||||
if (davServiceConn == null) {
|
||||
davServiceConn = object: ServiceConnection {
|
||||
override fun onServiceConnected(name: ComponentName, service: IBinder) {
|
||||
// get notified when DavService is running
|
||||
davService = service as DavService.InfoBinder
|
||||
service.addRefreshingStatusListener(this@AccountLoader, false)
|
||||
|
||||
onContentChanged()
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected(name: ComponentName) {
|
||||
davService = null
|
||||
}
|
||||
}
|
||||
context.bindService(Intent(context, DavService::class.java), davServiceConn, Context.BIND_AUTO_CREATE)
|
||||
} else
|
||||
forceLoad()
|
||||
}
|
||||
|
||||
override fun onStopLoading() {
|
||||
davService?.removeRefreshingStatusListener(this)
|
||||
context.unbindService(this)
|
||||
override fun onReset() {
|
||||
ContentResolver.removeStatusChangeListener(syncStatusListener)
|
||||
}
|
||||
|
||||
override fun onServiceConnected(name: ComponentName, service: IBinder) {
|
||||
davService = service as DavService.InfoBinder
|
||||
service.addRefreshingStatusListener(this, false)
|
||||
|
||||
forceLoad()
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected(name: ComponentName) {
|
||||
davService = null
|
||||
davService?.removeRefreshingStatusListener(this)
|
||||
davServiceConn?.let {
|
||||
context.unbindService(it)
|
||||
davServiceConn = null
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDavRefreshStatusChanged(id: Long, refreshing: Boolean) =
|
||||
forceLoad()
|
||||
onContentChanged()
|
||||
|
||||
override fun onStatusChanged(which: Int) =
|
||||
forceLoad()
|
||||
onContentChanged()
|
||||
|
||||
override fun loadInBackground(): AccountInfo {
|
||||
val info = AccountInfo()
|
||||
@@ -413,9 +493,9 @@ class AccountActivity: AppCompatActivity(), Toolbar.OnMenuItemClickListener, Pop
|
||||
for (addrBookAccount in accountManager.getAccountsByType(context.getString(R.string.account_type_address_book))) {
|
||||
val addressBook = LocalAddressBook(context, addrBookAccount, null)
|
||||
try {
|
||||
if (account == addressBook.getMainAccount())
|
||||
if (account == addressBook.mainAccount)
|
||||
carddav.refreshing = carddav.refreshing || ContentResolver.isSyncActive(addrBookAccount, ContactsContract.AUTHORITY)
|
||||
} catch(e: ContactsStorageException) {
|
||||
} catch(e: Exception) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -436,8 +516,8 @@ class AccountActivity: AppCompatActivity(), Toolbar.OnMenuItemClickListener, Pop
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return info
|
||||
}
|
||||
|
||||
@@ -463,24 +543,20 @@ class AccountActivity: AppCompatActivity(), Toolbar.OnMenuItemClickListener, Pop
|
||||
// Webcal: check whether calendar is already subscribed by ICSdroid
|
||||
// (or any other app that stores the URL in Calendars.NAME)
|
||||
val webcalCollections = collections.filter { it.type == CollectionInfo.Type.WEBCAL }
|
||||
if (webcalCollections.isNotEmpty()) {
|
||||
if (ContextCompat.checkSelfPermission(context, Manifest.permission.READ_CALENDAR) == PackageManager.PERMISSION_GRANTED)
|
||||
context.contentResolver.acquireContentProviderClient(CalendarContract.AUTHORITY)?.let { provider ->
|
||||
try {
|
||||
for (info in webcalCollections) {
|
||||
provider.query(CalendarContract.Calendars.CONTENT_URI, null,
|
||||
"${CalendarContract.Calendars.NAME}=?", arrayOf(info.source), null)?.use { cursor ->
|
||||
if (cursor.moveToNext())
|
||||
info.selected = true
|
||||
}
|
||||
if (webcalCollections.isNotEmpty() && ContextCompat.checkSelfPermission(context, Manifest.permission.READ_CALENDAR) == PackageManager.PERMISSION_GRANTED)
|
||||
context.contentResolver.acquireContentProviderClient(CalendarContract.AUTHORITY)?.let { provider ->
|
||||
try {
|
||||
for (info in webcalCollections) {
|
||||
provider.query(CalendarContract.Calendars.CONTENT_URI, null,
|
||||
"${CalendarContract.Calendars.NAME}=?", arrayOf(info.source), null)?.use { cursor ->
|
||||
if (cursor.moveToNext())
|
||||
info.selected = true
|
||||
}
|
||||
} finally {
|
||||
provider.release()
|
||||
}
|
||||
} finally {
|
||||
provider.release()
|
||||
}
|
||||
else
|
||||
ActivityCompat.requestPermissions(activity, arrayOf(Manifest.permission.READ_CALENDAR), 0)
|
||||
}
|
||||
}
|
||||
|
||||
return collections
|
||||
}
|
||||
@@ -501,7 +577,7 @@ class AccountActivity: AppCompatActivity(), Toolbar.OnMenuItemClickListener, Pop
|
||||
checked.isChecked = info.selected
|
||||
|
||||
var tv: TextView = v.findViewById(R.id.title)
|
||||
tv.text = if (!info.displayName.isNullOrBlank()) info.displayName else info.url
|
||||
tv.text = if (!info.displayName.isNullOrBlank()) info.displayName else info.url.toString()
|
||||
|
||||
tv = v.findViewById(R.id.description)
|
||||
if (info.description.isNullOrBlank())
|
||||
@@ -511,8 +587,14 @@ class AccountActivity: AppCompatActivity(), Toolbar.OnMenuItemClickListener, Pop
|
||||
tv.text = info.description
|
||||
}
|
||||
|
||||
tv = v.findViewById(R.id.read_only)
|
||||
tv.visibility = if (info.readOnly) View.VISIBLE else View.GONE
|
||||
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 ->
|
||||
(context as? AccountActivity)?.let {
|
||||
it.onActionOverflowListener(view, info)
|
||||
}
|
||||
}
|
||||
|
||||
return v
|
||||
}
|
||||
@@ -536,10 +618,10 @@ class AccountActivity: AppCompatActivity(), Toolbar.OnMenuItemClickListener, Pop
|
||||
vColor.visibility = info.color?.let {
|
||||
vColor.setBackgroundColor(it)
|
||||
View.VISIBLE
|
||||
} ?: View.GONE
|
||||
} ?: View.INVISIBLE
|
||||
|
||||
var tv: TextView = v.findViewById(R.id.title)
|
||||
tv.text = if (!info.displayName.isNullOrBlank()) info.displayName else info.url
|
||||
tv.text = if (!info.displayName.isNullOrBlank()) info.displayName else info.url.toString()
|
||||
|
||||
tv = v.findViewById(R.id.description)
|
||||
if (info.description.isNullOrBlank())
|
||||
@@ -549,14 +631,24 @@ class AccountActivity: AppCompatActivity(), Toolbar.OnMenuItemClickListener, Pop
|
||||
tv.text = info.description
|
||||
}
|
||||
|
||||
tv = v.findViewById(R.id.read_only)
|
||||
tv.visibility = if (info.readOnly) View.VISIBLE else View.GONE
|
||||
v.findViewById<ImageView>(R.id.read_only).visibility =
|
||||
if (info.readOnly || info.forceReadOnly) View.VISIBLE else View.GONE
|
||||
|
||||
tv = v.findViewById(R.id.events)
|
||||
tv.visibility = if (info.supportsVEVENT) View.VISIBLE else View.GONE
|
||||
v.findViewById<ImageView>(R.id.events).visibility =
|
||||
if (info.supportsVEVENT) View.VISIBLE else View.GONE
|
||||
|
||||
tv = v.findViewById(R.id.tasks)
|
||||
tv.visibility = if (info.supportsVTODO) View.VISIBLE else View.GONE
|
||||
v.findViewById<ImageView>(R.id.tasks).visibility =
|
||||
if (info.supportsVTODO) View.VISIBLE else View.GONE
|
||||
|
||||
val overflow = v.findViewById<ImageView>(R.id.action_overflow)
|
||||
if (info.type == CollectionInfo.Type.WEBCAL)
|
||||
overflow.visibility = View.GONE
|
||||
else
|
||||
overflow.setOnClickListener { view ->
|
||||
(context as? AccountActivity)?.let {
|
||||
it.onActionOverflowListener(view, info)
|
||||
}
|
||||
}
|
||||
|
||||
return v
|
||||
}
|
||||
@@ -569,7 +661,7 @@ class AccountActivity: AppCompatActivity(), Toolbar.OnMenuItemClickListener, Pop
|
||||
|
||||
companion object {
|
||||
|
||||
val ARG_ACCOUNT = "account"
|
||||
const val ARG_ACCOUNT = "account"
|
||||
|
||||
fun newInstance(account: Account): RenameAccountFragment {
|
||||
val fragment = RenameAccountFragment()
|
||||
@@ -582,12 +674,12 @@ class AccountActivity: AppCompatActivity(), Toolbar.OnMenuItemClickListener, Pop
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val oldAccount: Account = arguments.getParcelable(ARG_ACCOUNT)
|
||||
val oldAccount: Account = arguments!!.getParcelable(ARG_ACCOUNT)
|
||||
|
||||
val editText = EditText(activity)
|
||||
editText.setText(oldAccount.name)
|
||||
|
||||
return AlertDialog.Builder(activity)
|
||||
return AlertDialog.Builder(activity!!)
|
||||
.setTitle(R.string.account_rename)
|
||||
.setMessage(R.string.account_rename_new_name)
|
||||
.setView(editText)
|
||||
@@ -608,48 +700,46 @@ 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.getMainAccount())
|
||||
addressBook.setMainAccount(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: ContactsStorageException) {
|
||||
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)
|
||||
|
||||
// update account_name of local tasks
|
||||
try {
|
||||
LocalTaskList.onRenameAccount(activity.contentResolver, oldAccount.name, newName)
|
||||
LocalTaskList.onRenameAccount(activity!!.contentResolver, oldAccount.name, newName)
|
||||
} catch(e: Exception) {
|
||||
Logger.log.log(Level.SEVERE, "Couldn't propagate new account name to tasks provider", e)
|
||||
}
|
||||
|
||||
// synchronize again
|
||||
requestSync(activity, Account(newName, oldAccount.type))
|
||||
requestSync(activity!!, Account(newName, oldAccount.type))
|
||||
}, null)
|
||||
activity.finish()
|
||||
activity!!.finish()
|
||||
})
|
||||
.setNegativeButton(android.R.string.cancel, { _, _ -> })
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||
.create()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,6 @@ import android.view.ViewGroup
|
||||
import android.widget.AbsListView
|
||||
import android.widget.AdapterView
|
||||
import android.widget.ArrayAdapter
|
||||
import at.bitfire.davdroid.AccountsChangedReceiver
|
||||
import at.bitfire.davdroid.R
|
||||
import kotlinx.android.synthetic.main.account_list_item.view.*
|
||||
|
||||
@@ -67,16 +66,28 @@ class AccountListFragment: ListFragment(), LoaderManager.LoaderCallbacks<Array<A
|
||||
|
||||
class AccountLoader(
|
||||
context: Context
|
||||
): AsyncTaskLoader<Array<Account>>(context), OnAccountsUpdateListener {
|
||||
): AsyncTaskLoader<Array<Account>>(context) {
|
||||
|
||||
override fun onStartLoading() =
|
||||
AccountsChangedReceiver.registerListener(this, true)
|
||||
private val accountManager = AccountManager.get(context)!!
|
||||
private var listener: OnAccountsUpdateListener? = null
|
||||
|
||||
override fun onStopLoading() =
|
||||
AccountsChangedReceiver.unregisterListener(this)
|
||||
override fun onStartLoading() {
|
||||
if (listener == null) {
|
||||
listener = OnAccountsUpdateListener { onContentChanged() }
|
||||
accountManager.addOnAccountsUpdatedListener(listener, null, false)
|
||||
}
|
||||
|
||||
override fun onAccountsUpdated(accounts: Array<Account>?) =
|
||||
forceLoad()
|
||||
forceLoad()
|
||||
}
|
||||
|
||||
override fun onReset() {
|
||||
listener?.let {
|
||||
try {
|
||||
accountManager.removeOnAccountsUpdatedListener(it)
|
||||
} catch(ignored: IllegalArgumentException) {}
|
||||
listener = null
|
||||
}
|
||||
}
|
||||
|
||||
override fun loadInBackground(): Array<Account> =
|
||||
AccountManager.get(context).getAccountsByType(context.getString(R.string.account_type))
|
||||
|
||||
@@ -8,25 +8,36 @@
|
||||
|
||||
package at.bitfire.davdroid.ui
|
||||
|
||||
import android.Manifest
|
||||
import android.accounts.Account
|
||||
import android.app.DialogFragment
|
||||
import android.app.LoaderManager
|
||||
import android.content.*
|
||||
import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.SyncStatusObserver
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.provider.CalendarContract
|
||||
import android.support.v14.preference.PreferenceFragment
|
||||
import android.security.KeyChain
|
||||
import android.support.v4.app.ActivityCompat
|
||||
import android.support.v4.app.DialogFragment
|
||||
import android.support.v4.app.LoaderManager
|
||||
import android.support.v4.app.NavUtils
|
||||
import android.support.v4.content.ContextCompat
|
||||
import android.support.v4.content.Loader
|
||||
import android.support.v7.app.AlertDialog
|
||||
import android.support.v7.app.AppCompatActivity
|
||||
import android.support.v7.preference.EditTextPreference
|
||||
import android.support.v7.preference.ListPreference
|
||||
import android.support.v7.preference.Preference
|
||||
import android.support.v7.preference.SwitchPreferenceCompat
|
||||
import android.support.v7.preference.*
|
||||
import android.view.MenuItem
|
||||
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
|
||||
@@ -34,7 +45,7 @@ import org.apache.commons.lang3.StringUtils
|
||||
class AccountSettingsActivity: AppCompatActivity() {
|
||||
|
||||
companion object {
|
||||
val EXTRA_ACCOUNT = "account"
|
||||
const val EXTRA_ACCOUNT = "account"
|
||||
}
|
||||
|
||||
private lateinit var account: Account
|
||||
@@ -49,7 +60,7 @@ class AccountSettingsActivity: AppCompatActivity() {
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
|
||||
if (savedInstanceState == null)
|
||||
fragmentManager.beginTransaction()
|
||||
supportFragmentManager.beginTransaction()
|
||||
.replace(android.R.id.content, DialogFragment.instantiate(this, AccountSettingsFragment::class.java.name, intent.extras))
|
||||
.commit()
|
||||
}
|
||||
@@ -64,13 +75,13 @@ class AccountSettingsActivity: AppCompatActivity() {
|
||||
false
|
||||
|
||||
|
||||
class AccountSettingsFragment: PreferenceFragment(), LoaderManager.LoaderCallbacks<Pair<ISettings, AccountSettings>?> {
|
||||
class AccountSettingsFragment: PreferenceFragmentCompat(), LoaderManager.LoaderCallbacks<Pair<ISettings, AccountSettings>> {
|
||||
lateinit var account: Account
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
account = arguments.getParcelable(EXTRA_ACCOUNT)
|
||||
account = arguments!!.getParcelable(EXTRA_ACCOUNT)
|
||||
loaderManager.initLoader(0, arguments, this)
|
||||
}
|
||||
|
||||
@@ -78,27 +89,54 @@ class AccountSettingsActivity: AppCompatActivity() {
|
||||
addPreferencesFromResource(R.xml.settings_account)
|
||||
}
|
||||
|
||||
override fun onCreateLoader(id: Int, args: Bundle) =
|
||||
AccountSettingsLoader(activity, args.getParcelable(EXTRA_ACCOUNT))
|
||||
override fun onCreateLoader(id: Int, args: Bundle?) =
|
||||
AccountSettingsLoader(requireActivity(), args!!.getParcelable(EXTRA_ACCOUNT))
|
||||
|
||||
override fun onLoadFinished(loader: Loader<Pair<ISettings, AccountSettings>?>, result: Pair<ISettings, AccountSettings>?) {
|
||||
override fun onLoadFinished(loader: Loader<Pair<ISettings, AccountSettings>>, result: Pair<ISettings, AccountSettings>?) {
|
||||
val (settings, accountSettings) = result ?: return
|
||||
|
||||
// preference group: authentication
|
||||
val prefUserName = findPreference("username") as EditTextPreference
|
||||
prefUserName.summary = accountSettings.username()
|
||||
prefUserName.text = accountSettings.username()
|
||||
prefUserName.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue ->
|
||||
accountSettings.username(newValue as String)
|
||||
loaderManager.restartLoader(0, arguments, this)
|
||||
false
|
||||
}
|
||||
|
||||
val prefPassword = findPreference("password") as EditTextPreference
|
||||
prefPassword.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue ->
|
||||
accountSettings.password(newValue as String)
|
||||
loaderManager.restartLoader(0, arguments, this)
|
||||
false
|
||||
val prefCertAlias = findPreference("certificate_alias") as Preference
|
||||
|
||||
val credentials = accountSettings.credentials()
|
||||
when (credentials.type) {
|
||||
Credentials.Type.UsernamePassword -> {
|
||||
prefUserName.isVisible = true
|
||||
prefUserName.summary = credentials.userName
|
||||
prefUserName.text = credentials.userName
|
||||
prefUserName.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue ->
|
||||
accountSettings.credentials(Credentials(newValue as String, credentials.password))
|
||||
loaderManager.restartLoader(0, arguments, this)
|
||||
false
|
||||
}
|
||||
|
||||
prefPassword.isVisible = true
|
||||
prefPassword.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue ->
|
||||
accountSettings.credentials(Credentials(credentials.userName, newValue as String))
|
||||
loaderManager.restartLoader(0, arguments, this)
|
||||
false
|
||||
}
|
||||
|
||||
prefCertAlias.isVisible = false
|
||||
}
|
||||
Credentials.Type.ClientCertificate -> {
|
||||
prefUserName.isVisible = false
|
||||
prefPassword.isVisible = false
|
||||
|
||||
prefCertAlias.isVisible = true
|
||||
prefCertAlias.summary = credentials.certificateAlias
|
||||
prefCertAlias.setOnPreferenceClickListener {
|
||||
KeyChain.choosePrivateKeyAlias(activity, { alias ->
|
||||
accountSettings.credentials(Credentials(certificateAlias = alias))
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
loaderManager.restartLoader(0, arguments, this)
|
||||
}
|
||||
}, null, null, null, -1, credentials.certificateAlias)
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// preference group: sync
|
||||
@@ -175,11 +213,18 @@ class AccountSettingsActivity: AppCompatActivity() {
|
||||
else
|
||||
prefWifiOnlySSIDs.setSummary(R.string.settings_sync_wifi_only_ssids_off)
|
||||
prefWifiOnlySSIDs.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue ->
|
||||
accountSettings.setSyncWifiOnlySSIDs((newValue as String).split(',').map { StringUtils.trimToNull(it) }.filterNotNull().distinct())
|
||||
accountSettings.setSyncWifiOnlySSIDs((newValue as String).split(',').mapNotNull { StringUtils.trimToNull(it) }.distinct())
|
||||
loaderManager.restartLoader(0, arguments, this)
|
||||
false
|
||||
}
|
||||
|
||||
// getting the WiFi name requires location permission (and active location services) since Android 8.1
|
||||
// see https://issuetracker.google.com/issues/70633700
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1 &&
|
||||
accountSettings.getSyncWifiOnly() && onlySSIDs != null &&
|
||||
ContextCompat.checkSelfPermission(requireActivity(), Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED)
|
||||
ActivityCompat.requestPermissions(requireActivity(), arrayOf(Manifest.permission.ACCESS_COARSE_LOCATION), 0)
|
||||
|
||||
// preference group: CardDAV
|
||||
(findPreference("contact_group_method") as ListPreference).let {
|
||||
if (syncIntervalContacts != null) {
|
||||
@@ -191,8 +236,22 @@ class AccountSettingsActivity: AppCompatActivity() {
|
||||
else {
|
||||
it.isEnabled = true
|
||||
it.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, groupMethod ->
|
||||
accountSettings.setGroupMethod(GroupMethod.valueOf(groupMethod as String))
|
||||
loaderManager.restartLoader(0, arguments, this)
|
||||
AlertDialog.Builder(requireActivity())
|
||||
.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) { _, _ ->
|
||||
// change group method
|
||||
accountSettings.setGroupMethod(GroupMethod.valueOf(groupMethod as String))
|
||||
loaderManager.restartLoader(0, arguments, this)
|
||||
|
||||
// reload all contacts
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -213,13 +272,29 @@ class AccountSettingsActivity: AppCompatActivity() {
|
||||
it.setSummary(R.string.settings_sync_time_range_past_none)
|
||||
}
|
||||
it.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue ->
|
||||
var days: Int
|
||||
try {
|
||||
days = (newValue as String).toInt()
|
||||
} catch(ignored: NumberFormatException) {
|
||||
days = -1
|
||||
val days = try {
|
||||
(newValue as String).toInt()
|
||||
} catch(e: NumberFormatException) {
|
||||
-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
|
||||
}
|
||||
@@ -251,15 +326,15 @@ class AccountSettingsActivity: AppCompatActivity() {
|
||||
accountSettings.setEventColors(true)
|
||||
loaderManager.restartLoader(0, arguments, this)
|
||||
} else
|
||||
AlertDialog.Builder(activity)
|
||||
AlertDialog.Builder(requireActivity())
|
||||
.setIcon(R.drawable.ic_error_dark)
|
||||
.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
|
||||
}
|
||||
@@ -268,7 +343,7 @@ class AccountSettingsActivity: AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onLoaderReset(loader: Loader<Pair<ISettings, AccountSettings>?>) {
|
||||
override fun onLoaderReset(loader: Loader<Pair<ISettings, AccountSettings>>) {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -279,17 +354,22 @@ class AccountSettingsActivity: AppCompatActivity() {
|
||||
val account: Account
|
||||
): SettingsLoader<Pair<ISettings, AccountSettings>?>(context), SyncStatusObserver {
|
||||
|
||||
var listenerHandle: Any? = null
|
||||
private var listenerHandle: Any? = null
|
||||
|
||||
override fun onStartLoading() {
|
||||
super.onStartLoading()
|
||||
listenerHandle = ContentResolver.addStatusChangeListener(ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS, this@AccountSettingsLoader)
|
||||
|
||||
if (listenerHandle == null)
|
||||
listenerHandle = ContentResolver.addStatusChangeListener(ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS, this)
|
||||
}
|
||||
|
||||
override fun onStopLoading() {
|
||||
super.onStopLoading()
|
||||
listenerHandle?.let { ContentResolver.removeStatusChangeListener(it) }
|
||||
listenerHandle = null
|
||||
override fun onReset() {
|
||||
super.onReset()
|
||||
|
||||
listenerHandle?.let {
|
||||
ContentResolver.removeStatusChangeListener(it)
|
||||
listenerHandle = null
|
||||
}
|
||||
}
|
||||
|
||||
override fun loadInBackground(): Pair<ISettings, AccountSettings>? {
|
||||
@@ -306,7 +386,7 @@ class AccountSettingsActivity: AppCompatActivity() {
|
||||
}
|
||||
|
||||
override fun onStatusChanged(which: Int) {
|
||||
forceLoad()
|
||||
onContentChanged()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||