mirror of
https://github.com/bitfireAT/davx5-ose.git
synced 2026-01-06 05:47:50 -05:00
Compare commits
224 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2d3bab3a13 | ||
|
|
0dcef80ca7 | ||
|
|
61c60a9958 | ||
|
|
f4d4370667 | ||
|
|
8d55796354 | ||
|
|
3f13d2642e | ||
|
|
5e6a12b6d5 | ||
|
|
5483f3fa0e | ||
|
|
c6c234f09e | ||
|
|
a72dd1437c | ||
|
|
f79b51596e | ||
|
|
772c50a05e | ||
|
|
fc3a304c8d | ||
|
|
a99f7a20e9 | ||
|
|
1473e02f2b | ||
|
|
340c8f0409 | ||
|
|
55762e8896 | ||
|
|
abd1308a07 | ||
|
|
641052a625 | ||
|
|
194f6941d8 | ||
|
|
a8b48bdf63 | ||
|
|
19b3418e59 | ||
|
|
11e7731064 | ||
|
|
0b51ad0eac | ||
|
|
751656ac22 | ||
|
|
21b60725ae | ||
|
|
4ba478d463 | ||
|
|
4878b77e7d | ||
|
|
44e18b6ce5 | ||
|
|
82ad561e13 | ||
|
|
8e37f80721 | ||
|
|
5ceb512e60 | ||
|
|
afa0bdaec9 | ||
|
|
3653f5cb2b | ||
|
|
f5fd92030e | ||
|
|
d5c58e576b | ||
|
|
00cfb6c5e4 | ||
|
|
a5924d3fc3 | ||
|
|
bb48e39faf | ||
|
|
b014ff1ab7 | ||
|
|
416212ced1 | ||
|
|
164c1ae869 | ||
|
|
cb530637b7 | ||
|
|
733bb644cd | ||
|
|
66138017d1 | ||
|
|
50aa42763b | ||
|
|
cee444e443 | ||
|
|
9f290bdda5 | ||
|
|
8da0a6f963 | ||
|
|
7804c4c319 | ||
|
|
cab6aee618 | ||
|
|
c91f36d10a | ||
|
|
494f20d3f7 | ||
|
|
850d05ab2a | ||
|
|
b7ee101385 | ||
|
|
a7e97dc626 | ||
|
|
bcbe1d44c3 | ||
|
|
ddeba1fa71 | ||
|
|
61f9125f85 | ||
|
|
7f06626b8b | ||
|
|
e321a86224 | ||
|
|
1f85e118d0 | ||
|
|
02bcb2579b | ||
|
|
abed9bf8bb | ||
|
|
0e1eb13fca | ||
|
|
8d6764a16b | ||
|
|
79037d7940 | ||
|
|
3ca636c2b2 | ||
|
|
350c85e161 | ||
|
|
eec0a64c03 | ||
|
|
8c9de140cf | ||
|
|
f05252882c | ||
|
|
77c0d3f6a8 | ||
|
|
ea5b9e3853 | ||
|
|
d98b109ec5 | ||
|
|
004887d642 | ||
|
|
c20c9b26db | ||
|
|
14cacb4c51 | ||
|
|
f2a87b88ba | ||
|
|
60d69c205a | ||
|
|
0849466450 | ||
|
|
f54bb0f010 | ||
|
|
73a2d6d20f | ||
|
|
9363f243fd | ||
|
|
19d05bad91 | ||
|
|
20709d4f61 | ||
|
|
848616c7e2 | ||
|
|
eb97773770 | ||
|
|
9ff9a6b935 | ||
|
|
9efa0199c5 | ||
|
|
2de8c116a9 | ||
|
|
75c03074df | ||
|
|
f8896b3e24 | ||
|
|
30bff9062d | ||
|
|
950ce6eaec | ||
|
|
f7154bd778 | ||
|
|
f96689da65 | ||
|
|
ce1262357e | ||
|
|
01e6e28384 | ||
|
|
988b6f7c7c | ||
|
|
494fe0e702 | ||
|
|
a74e159558 | ||
|
|
16c8bbc471 | ||
|
|
7c800db81d | ||
|
|
91cea341cd | ||
|
|
ba3c741f95 | ||
|
|
e0b0fe112d | ||
|
|
afc8b22843 | ||
|
|
89a936856c | ||
|
|
134e60784b | ||
|
|
e024bd56f9 | ||
|
|
dccd68962e | ||
|
|
b42c72dc96 | ||
|
|
51fb655e57 | ||
|
|
f4ca7b4a8b | ||
|
|
3f303c4718 | ||
|
|
6f86544e45 | ||
|
|
af1d9ac962 | ||
|
|
6e610382fa | ||
|
|
5723170e01 | ||
|
|
8808bca856 | ||
|
|
771293727c | ||
|
|
fe44128861 | ||
|
|
1e81964ce1 | ||
|
|
1aa18490c1 | ||
|
|
07e9e9b169 | ||
|
|
65a6583657 | ||
|
|
9791a4b730 | ||
|
|
a9336b90a9 | ||
|
|
e2edad18f4 | ||
|
|
b0ea4166f0 | ||
|
|
c333133bcb | ||
|
|
fb83488c78 | ||
|
|
266dc50f4f | ||
|
|
0cb51e06ad | ||
|
|
722d981c91 | ||
|
|
2215b47f2c | ||
|
|
a2d866b5bb | ||
|
|
27e59a6e6e | ||
|
|
0836859ea5 | ||
|
|
24722bfca9 | ||
|
|
8983ace0b1 | ||
|
|
0d3de671a2 | ||
|
|
f7527ddd8a | ||
|
|
c2414ab805 | ||
|
|
6046bb2229 | ||
|
|
e14c4da890 | ||
|
|
ca1e438e30 | ||
|
|
3bc37d9a66 | ||
|
|
6c4b8436f6 | ||
|
|
1be370daca | ||
|
|
3ca962c677 | ||
|
|
d463efc461 | ||
|
|
d6756e31ca | ||
|
|
bd5857c055 | ||
|
|
2faf5cae40 | ||
|
|
08f74f2eb0 | ||
|
|
ea084ef374 | ||
|
|
a267a6210e | ||
|
|
c4642c4aac | ||
|
|
ada9f328c8 | ||
|
|
aae701d9ee | ||
|
|
caec01ddba | ||
|
|
19e7967405 | ||
|
|
4478ae2c5a | ||
|
|
ca737f58e7 | ||
|
|
d755da2159 | ||
|
|
6617a7bcaa | ||
|
|
033c4d658b | ||
|
|
2b6c9e42c7 | ||
|
|
4a36edfe9d | ||
|
|
9f7f4e8411 | ||
|
|
07f665b245 | ||
|
|
068e05d41e | ||
|
|
fb9ce81a99 | ||
|
|
6237aacd96 | ||
|
|
f9a97da7f4 | ||
|
|
14959ce869 | ||
|
|
c8df1dc10a | ||
|
|
6712c24482 | ||
|
|
d7b3f89513 | ||
|
|
4c4857db6d | ||
|
|
f343691877 | ||
|
|
1426a047e1 | ||
|
|
3ac92a00b0 | ||
|
|
208936415d | ||
|
|
9a3b510193 | ||
|
|
3d43919995 | ||
|
|
e919fc8794 | ||
|
|
bc4bf8ebbf | ||
|
|
e26c61ba5a | ||
|
|
9074b74632 | ||
|
|
9a9ad8ec17 | ||
|
|
a6d11b1a42 | ||
|
|
eca941a78d | ||
|
|
29e0917a73 | ||
|
|
822d6f8c6f | ||
|
|
46b6d06b6a | ||
|
|
87ecd3c182 | ||
|
|
b02bdc6ecd | ||
|
|
f3af584494 | ||
|
|
0cf5a758ba | ||
|
|
840c7e741a | ||
|
|
da97c1f2b9 | ||
|
|
859ef6d29f | ||
|
|
351f5e8447 | ||
|
|
80b1bb8ee2 | ||
|
|
e558c45139 | ||
|
|
a2284de509 | ||
|
|
a61fe817ca | ||
|
|
15349b3d23 | ||
|
|
6eea640647 | ||
|
|
91864f76e4 | ||
|
|
3c303ff760 | ||
|
|
dec4f81faa | ||
|
|
3e691e6aef | ||
|
|
66ae1ddc8d | ||
|
|
d757f45e8b | ||
|
|
374acf1c70 | ||
|
|
b1ab14f311 | ||
|
|
d65a021536 | ||
|
|
8e61320225 | ||
|
|
ef698102f1 | ||
|
|
47e417158f |
20
.gitlab-ci.yml
Normal file
20
.gitlab-ci.yml
Normal file
@@ -0,0 +1,20 @@
|
||||
image: registry.gitlab.com/bitfireat/davdroid:latest
|
||||
|
||||
before_script:
|
||||
- git submodule update --init --recursive
|
||||
- export GRADLE_USER_HOME=`pwd`/.gradle; chmod +x gradlew
|
||||
- emulator64-x86 -avd test -no-audio -no-window & wait-for-emulator.sh
|
||||
- wget -q https://f-droid.org/repo/org.dmfs.tasks_481.apk && adb install org.dmfs.tasks_481.apk
|
||||
|
||||
cache:
|
||||
paths:
|
||||
- .gradle/
|
||||
|
||||
test:
|
||||
script:
|
||||
- ./gradlew check mergeAndroidReports
|
||||
artifacts:
|
||||
paths:
|
||||
- app/build/outputs/lint-results-debug.html
|
||||
- app/build/reports
|
||||
- build/reports
|
||||
12
README.md
12
README.md
@@ -1,4 +1,7 @@
|
||||
|
||||
[](https://gitlab.com/bitfireAT/davdroid/commits/master)
|
||||
|
||||
|
||||
DAVdroid
|
||||
========
|
||||
|
||||
@@ -7,10 +10,14 @@ comprehensive information about DAVdroid.
|
||||
|
||||
DAVdroid is licensed under the [GPLv3 License](LICENSE).
|
||||
|
||||
News and updates: [@davdroidapp](https://twitter.com/davdroidapp)
|
||||
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
|
||||
@@ -18,8 +25,6 @@ Parts of DAVdroid have been outsourced into these libraries:
|
||||
* [ical4android](https://gitlab.com/bitfireAT/ical4android) – iCalendar processing and Calendar Provider access
|
||||
* [vcard4android](https://gitlab.com/bitfireAT/vcard4android) – VCard processing and Contacts Provider access
|
||||
|
||||
[](https://flattr.com/submit/auto?user_id=bitfire&url=https://davdroid.bitfire.at&title=DAVdroid&category=software)
|
||||
|
||||
|
||||
USED THIRD-PARTY LIBRARIES
|
||||
==========================
|
||||
@@ -31,4 +36,3 @@ Those libraries are used by DAVdroid (alphabetically):
|
||||
* [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)
|
||||
* [SLF4J](http://www.slf4j.org/) – [MIT License](http://www.slf4j.org/license.html)
|
||||
|
||||
104
app/build.gradle
104
app/build.gradle
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2013 – 2016 Ricki Hirner (bitfire web engineering).
|
||||
* Copyright (c) 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
|
||||
@@ -7,54 +7,77 @@
|
||||
*/
|
||||
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlin-android-extensions'
|
||||
|
||||
android {
|
||||
compileSdkVersion 24
|
||||
buildToolsVersion "24.0.1"
|
||||
compileSdkVersion 26
|
||||
buildToolsVersion '26.0.0'
|
||||
|
||||
defaultConfig {
|
||||
applicationId "at.bitfire.davdroid"
|
||||
resValue "string", "packageID", applicationId
|
||||
|
||||
versionCode 114
|
||||
versionCode 163
|
||||
buildConfigField "long", "buildTime", System.currentTimeMillis() + "L"
|
||||
|
||||
minSdkVersion 14
|
||||
targetSdkVersion 24
|
||||
minSdkVersion 16
|
||||
targetSdkVersion 25
|
||||
|
||||
buildConfigField "boolean", "customCerts", "false"
|
||||
buildConfigField "at.bitfire.vcard4android.GroupMethod", "settingContactGroupMethod", "null"
|
||||
buildConfigField "Boolean", "settingVCardRFC6868", "null"
|
||||
}
|
||||
|
||||
productFlavors {
|
||||
standard {
|
||||
versionName "1.3"
|
||||
versionName "1.7"
|
||||
buildConfigField "boolean", "customCerts", "true"
|
||||
}
|
||||
|
||||
gplay {
|
||||
versionName "1.3-gplay"
|
||||
versionName "1.7-gplay"
|
||||
buildConfigField "boolean", "customCerts", "true"
|
||||
}
|
||||
icloud {
|
||||
applicationId "at.bitfire.cloudsync"
|
||||
resValue "string", "packageID", applicationId
|
||||
|
||||
versionName "1.3-cloud"
|
||||
|
||||
versionName "1.7-cloud"
|
||||
buildConfigField "at.bitfire.vcard4android.GroupMethod", "settingContactGroupMethod", "at.bitfire.vcard4android.GroupMethod.GROUP_VCARDS"
|
||||
buildConfigField "Boolean", "settingVCardRFC6868", "true"
|
||||
}
|
||||
soldupe {
|
||||
applicationId "com.soldupe.cloudsync"
|
||||
resValue "string", "packageID", applicationId
|
||||
minSdkVersion 21
|
||||
|
||||
versionName "1.7-soldupe"
|
||||
buildConfigField "boolean", "customCerts", "true"
|
||||
buildConfigField "at.bitfire.vcard4android.GroupMethod", "settingContactGroupMethod", "at.bitfire.vcard4android.GroupMethod.CATEGORIES"
|
||||
}
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
standard.java.srcDirs = [ "src/davdroid/java" ]
|
||||
standard.res.srcDirs = [ "src/davdroid/res" ]
|
||||
|
||||
gplay.java.srcDirs = [ "src/gplay/java", "src/davdroid/java" ]
|
||||
gplay.res.srcDirs = [ "src/gplay/res", "src/davdroid/res" ]
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
bitfire {
|
||||
storeFile file("${System.env.HOME}/Entwicklung/GooglePlay/bitfire.jks")
|
||||
storePassword '***REMOVED***'
|
||||
keyAlias 'bitfire'
|
||||
keyPassword '***REMOVED***'
|
||||
}
|
||||
soldupe {
|
||||
storeFile file("${System.env.HOME}/Entwicklung/GooglePlay/soldupe.jks")
|
||||
storePassword 'hei8eePh'
|
||||
keyAlias 'soldupe'
|
||||
keyPassword 'ocaip6oZ'
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
debug {
|
||||
minifyEnabled false
|
||||
@@ -62,51 +85,72 @@ android {
|
||||
release {
|
||||
minifyEnabled true
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
|
||||
|
||||
signingConfig signingConfigs.bitfire
|
||||
productFlavors.soldupe.signingConfig signingConfigs.soldupe
|
||||
}
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
disable 'GoogleAppIndexingWarning' // we don't need Google indexing, thanks
|
||||
disable 'GradleDependency'
|
||||
disable 'GradleDynamicVersion'
|
||||
disable 'IconColors'
|
||||
disable 'IconLauncherShape'
|
||||
disable 'IconMissingDensityFolder'
|
||||
disable 'ImpliedQuantity'
|
||||
disable 'MissingTranslation'
|
||||
disable 'MissingQuantity'
|
||||
disable 'ImpliedQuantity', 'MissingQuantity'
|
||||
disable 'MissingTranslation', 'ExtraTranslation' // translations from Transifex are not always up to date
|
||||
disable 'Recycle' // doesn't understand Lombok's @Cleanup
|
||||
disable 'RtlEnabled'
|
||||
disable 'RtlHardcoded'
|
||||
disable 'Typos'
|
||||
}
|
||||
packagingOptions {
|
||||
exclude 'LICENSE'
|
||||
exclude 'META-INF/LICENSE.txt'
|
||||
exclude 'META-INF/NOTICE.txt'
|
||||
exclude 'META-INF/DEPENDENCIES'
|
||||
exclude 'META-INF/LICENSE'
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compile project(':cert4android')
|
||||
compile project(':dav4android')
|
||||
compile project(':ical4android')
|
||||
compile project(':vcard4android')
|
||||
|
||||
compile 'com.android.support:appcompat-v7:24.+'
|
||||
compile 'com.android.support:cardview-v7:24.+'
|
||||
compile 'com.android.support:design:24.+'
|
||||
compile 'com.android.support:preference-v14:24.+'
|
||||
compile "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
|
||||
|
||||
//noinspection GradleDynamicVersion
|
||||
compile 'com.android.support:appcompat-v7:26.+'
|
||||
//noinspection GradleDynamicVersion
|
||||
compile 'com.android.support:cardview-v7:26.+'
|
||||
//noinspection GradleDynamicVersion
|
||||
compile 'com.android.support:design:26.+'
|
||||
//noinspection GradleDynamicVersion
|
||||
compile 'com.android.support:preference-v14:26.+'
|
||||
|
||||
compile 'com.github.yukuku:ambilwarna:2.0.1'
|
||||
compile project(':cert4android')
|
||||
|
||||
compile 'dnsjava:dnsjava:2.1.7'
|
||||
compile 'org.apache.commons:commons-lang3:3.4'
|
||||
compile 'com.squareup.okhttp3:logging-interceptor:3.8.1'
|
||||
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'
|
||||
provided 'org.projectlombok:lombok:1.16.10'
|
||||
|
||||
// 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.8.1'
|
||||
|
||||
testCompile 'junit:junit:4.12'
|
||||
testCompile 'com.squareup.okhttp3:mockwebserver:3.4.1'
|
||||
androidTestCompile 'com.squareup.okhttp3:mockwebserver:3.4.1'
|
||||
testCompile 'com.squareup.okhttp3:mockwebserver:3.8.1'
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
|
||||
# ProGuard usage for DAVdroid:
|
||||
# shrinking yes (main reason for using ProGuard)
|
||||
# optimization yes
|
||||
# obfuscation no (DAVdroid is open-source)
|
||||
# preverification no
|
||||
# shrinking yes (main reason for using ProGuard)
|
||||
# optimization yes
|
||||
# obfuscation no (DAVdroid is open-source)
|
||||
# preverification no
|
||||
|
||||
-dontobfuscate
|
||||
|
||||
@@ -13,29 +13,27 @@
|
||||
-dontpreverify
|
||||
|
||||
# ez-vcard
|
||||
-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
|
||||
-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
|
||||
-dontwarn sun.misc.Perf
|
||||
-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 groovy.** # Groovy-based ContentBuilder not used
|
||||
-dontwarn org.codehaus.groovy.**
|
||||
-dontwarn org.apache.commons.logging.** # Commons logging is not available
|
||||
-dontwarn net.fortuna.ical4j.model.** # ignore warnings from Groovy dependency
|
||||
-keep class net.fortuna.ical4j.model.** { *; } # keep all model classes (properties/factories, created at runtime)
|
||||
-dontwarn net.fortuna.ical4j.model.** # ignore warnings from Groovy dependency
|
||||
-dontwarn org.apache.log4j.** # ignore warnings from log4j dependency
|
||||
-keep class net.fortuna.ical4j.** { *; } # keep all model classes (properties/factories, created at runtime)
|
||||
|
||||
# okhttp
|
||||
-dontwarn java.nio.file.** # not available on Android
|
||||
-dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement
|
||||
|
||||
# MemorizingTrustManager
|
||||
-dontwarn de.duenndns.ssl.MemorizingTrustManager
|
||||
-dontwarn okio.**
|
||||
-dontwarn javax.annotation.Nullable
|
||||
-dontwarn javax.annotation.ParametersAreNonnullByDefault
|
||||
|
||||
# dnsjava
|
||||
-dontwarn sun.net.spi.nameservice.** # not available on Android
|
||||
-dontwarn sun.net.spi.nameservice.** # not available on Android
|
||||
|
||||
# DAVdroid + libs
|
||||
-keep class at.bitfire.** { *; } # all DAVdroid code is required
|
||||
-keep class at.bitfire.** { *; } # all DAVdroid code is required
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2013 – 2015 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,8 +8,9 @@
|
||||
|
||||
package at.bitfire.davdroid;
|
||||
|
||||
import android.os.Build;
|
||||
import android.test.InstrumentationTestCase;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.Socket;
|
||||
@@ -19,35 +20,38 @@ import javax.net.ssl.SSLSocket;
|
||||
import at.bitfire.cert4android.CustomCertManager;
|
||||
import okhttp3.mockwebserver.MockWebServer;
|
||||
|
||||
public class SSLSocketFactoryCompatTest extends InstrumentationTestCase {
|
||||
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 {
|
||||
|
||||
SSLSocketFactoryCompat factory;
|
||||
MockWebServer server = new MockWebServer();
|
||||
|
||||
@Override
|
||||
protected void setUp() throws Exception {
|
||||
factory = new SSLSocketFactoryCompat(new CustomCertManager(getInstrumentation().getTargetContext().getApplicationContext(), true));
|
||||
@Before
|
||||
public void startServer() throws Exception {
|
||||
factory = new SSLSocketFactoryCompat(new CustomCertManager(getTargetContext().getApplicationContext(), true));
|
||||
server.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void tearDown() throws Exception {
|
||||
@After
|
||||
public void stopServer() throws Exception {
|
||||
server.shutdown();
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testUpgradeTLS() throws IOException {
|
||||
Socket s = factory.createSocket(server.getHostName(), server.getPort());
|
||||
assertTrue(s instanceof SSLSocket);
|
||||
|
||||
SSLSocket ssl = (SSLSocket)s;
|
||||
assertFalse(org.apache.commons.lang3.ArrayUtils.contains(ssl.getEnabledProtocols(), "SSLv3"));
|
||||
assertTrue(org.apache.commons.lang3.ArrayUtils.contains(ssl.getEnabledProtocols(), "TLSv1"));
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 16) {
|
||||
assertTrue(org.apache.commons.lang3.ArrayUtils.contains(ssl.getEnabledProtocols(), "TLSv1.1"));
|
||||
assertTrue(org.apache.commons.lang3.ArrayUtils.contains(ssl.getEnabledProtocols(), "TLSv1.2"));
|
||||
}
|
||||
assertFalse(contains(ssl.getEnabledProtocols(), "SSLv3"));
|
||||
assertTrue(contains(ssl.getEnabledProtocols(), "TLSv1"));
|
||||
assertTrue(contains(ssl.getEnabledProtocols(), "TLSv1.1"));
|
||||
assertTrue(contains(ssl.getEnabledProtocols(), "TLSv1.2"));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -10,7 +10,7 @@ package at.bitfire.davdroid.model;
|
||||
|
||||
import android.content.ContentValues;
|
||||
|
||||
import junit.framework.TestCase;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
@@ -23,10 +23,16 @@ import at.bitfire.davdroid.model.ServiceDB.Collections;
|
||||
import okhttp3.mockwebserver.MockResponse;
|
||||
import okhttp3.mockwebserver.MockWebServer;
|
||||
|
||||
public class CollectionInfoTest extends TestCase {
|
||||
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 {
|
||||
|
||||
MockWebServer server = new MockWebServer();
|
||||
|
||||
@Test
|
||||
public void testFromDavResource() throws IOException, HttpException, DavException {
|
||||
// r/w address book
|
||||
server.enqueue(new MockResponse()
|
||||
@@ -42,13 +48,13 @@ public class CollectionInfoTest extends TestCase {
|
||||
"</response>" +
|
||||
"</multistatus>"));
|
||||
|
||||
DavResource dav = new DavResource(HttpClient.create(), server.url("/"));
|
||||
DavResource dav = new DavResource(HttpClient.create(null), server.url("/"));
|
||||
dav.propfind(0, ResourceType.NAME);
|
||||
CollectionInfo info = CollectionInfo.fromDavResource(dav);
|
||||
assertEquals(CollectionInfo.Type.ADDRESS_BOOK, info.type);
|
||||
assertFalse(info.readOnly);
|
||||
assertEquals("My Contacts", info.displayName);
|
||||
assertEquals("My Contacts Description", info.description);
|
||||
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()
|
||||
@@ -66,19 +72,20 @@ public class CollectionInfoTest extends TestCase {
|
||||
"</response>" +
|
||||
"</multistatus>"));
|
||||
|
||||
dav = new DavResource(HttpClient.create(), server.url("/"));
|
||||
dav = new DavResource(HttpClient.create(null), server.url("/"));
|
||||
dav.propfind(0, ResourceType.NAME);
|
||||
info = CollectionInfo.fromDavResource(dav);
|
||||
assertEquals(CollectionInfo.Type.CALENDAR, info.type);
|
||||
assertTrue(info.readOnly);
|
||||
assertNull(info.displayName);
|
||||
assertEquals("My Calendar", info.description);
|
||||
assertEquals(0xFFFF0000, (int)info.color);
|
||||
assertEquals("tzdata", info.timeZone);
|
||||
assertTrue(info.supportsVEVENT);
|
||||
assertTrue(info.supportsVTODO);
|
||||
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);
|
||||
@@ -93,18 +100,18 @@ public class CollectionInfoTest extends TestCase {
|
||||
values.put(Collections.SUPPORTS_VTODO, 1);
|
||||
values.put(Collections.SYNC, 1);
|
||||
|
||||
CollectionInfo info = CollectionInfo.fromDB(values);
|
||||
assertEquals(1, info.id);
|
||||
assertEquals(1, (long)info.serviceID);
|
||||
assertEquals("http://example.com", info.url);
|
||||
assertTrue(info.readOnly);
|
||||
assertEquals("display name", info.displayName);
|
||||
assertEquals("description", info.description);
|
||||
assertEquals(0xFFFF0000, (int)info.color);
|
||||
assertEquals("tzdata", info.timeZone);
|
||||
assertTrue(info.supportsVEVENT);
|
||||
assertTrue(info.supportsVTODO);
|
||||
assertTrue(info.selected);
|
||||
CollectionInfo info = new CollectionInfo(values);
|
||||
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());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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,7 +8,9 @@
|
||||
|
||||
package at.bitfire.davdroid.ui.setup;
|
||||
|
||||
import android.test.InstrumentationTestCase;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
@@ -18,18 +20,22 @@ 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.App;
|
||||
import at.bitfire.davdroid.HttpClient;
|
||||
import at.bitfire.davdroid.ui.setup.DavResourceFinder;
|
||||
import at.bitfire.davdroid.log.Logger;
|
||||
import at.bitfire.davdroid.ui.setup.DavResourceFinder.Configuration.ServiceInfo;
|
||||
import at.bitfire.davdroid.ui.setup.LoginCredentials;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.mockwebserver.Dispatcher;
|
||||
import okhttp3.mockwebserver.MockResponse;
|
||||
import okhttp3.mockwebserver.MockWebServer;
|
||||
import okhttp3.mockwebserver.RecordedRequest;
|
||||
|
||||
public class DavResourceFinderTest extends InstrumentationTestCase {
|
||||
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();
|
||||
|
||||
@@ -48,49 +54,50 @@ public class DavResourceFinderTest extends InstrumentationTestCase {
|
||||
SUBPATH_ADDRESSBOOK_HOMESET = "/addressbooks",
|
||||
SUBPATH_ADDRESSBOOK = "/addressbooks/private-contacts";
|
||||
|
||||
@Override
|
||||
protected void setUp() throws Exception {
|
||||
@Before
|
||||
public void initServerAndClient() throws Exception {
|
||||
server.setDispatcher(new TestDispatcher());
|
||||
server.start();
|
||||
|
||||
credentials = new LoginCredentials(URI.create("/"), "mock", "12345");
|
||||
finder = new DavResourceFinder(getInstrumentation().getContext(), credentials);
|
||||
finder = new DavResourceFinder(getTargetContext(), credentials);
|
||||
|
||||
client = HttpClient.create();
|
||||
client = HttpClient.addAuthentication(client, credentials.userName, credentials.password);
|
||||
client = HttpClient.create(null);
|
||||
client = HttpClient.addAuthentication(client, credentials.getUserName(), credentials.getPassword());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void tearDown() throws Exception {
|
||||
@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, server.url(PATH_CARDDAV + SUBPATH_PRINCIPAL));
|
||||
finder.rememberIfAddressBookOrHomeset(dav, info = new ServiceInfo());
|
||||
assertEquals(0, info.collections.size());
|
||||
assertEquals(0, info.homeSets.size());
|
||||
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.collections.size());
|
||||
assertEquals(1, info.homeSets.size());
|
||||
assertEquals(server.url(PATH_CARDDAV + SUBPATH_ADDRESSBOOK_HOMESET + "/").uri(), info.homeSets.iterator().next());
|
||||
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, server.url(PATH_CARDDAV + SUBPATH_ADDRESSBOOK));
|
||||
dav.propfind(0, ResourceType.NAME);
|
||||
finder.rememberIfAddressBookOrHomeset(dav, info = new ServiceInfo());
|
||||
assertEquals(1, info.collections.size());
|
||||
assertEquals(server.url(PATH_CARDDAV + SUBPATH_ADDRESSBOOK + "/").uri(), info.collections.keySet().iterator().next());
|
||||
assertEquals(0, info.homeSets.size());
|
||||
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));
|
||||
@@ -105,6 +112,7 @@ public class DavResourceFinderTest extends InstrumentationTestCase {
|
||||
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));
|
||||
@@ -170,7 +178,7 @@ public class DavResourceFinderTest extends InstrumentationTestCase {
|
||||
"</resourcetype>";
|
||||
break;
|
||||
}
|
||||
App.log.info("Sending props: " + props);
|
||||
Logger.log.info("Sending props: " + props);
|
||||
return new MockResponse()
|
||||
.setResponseCode(207)
|
||||
.setBody("<multistatus xmlns='DAV:' xmlns:CARD='urn:ietf:params:xml:ns:carddav'>" +
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
package at.bitfire.davdroid.ui;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.view.MenuItem;
|
||||
|
||||
import at.bitfire.davdroid.App;
|
||||
import at.bitfire.davdroid.BuildConfig;
|
||||
import at.bitfire.davdroid.Constants;
|
||||
import at.bitfire.davdroid.R;
|
||||
|
||||
public class DefaultAccountsDrawerHandler implements IAccountsDrawerHandler {
|
||||
|
||||
@Override
|
||||
public boolean onNavigationItemSelected(@NonNull Activity activity, MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case R.id.nav_about:
|
||||
activity.startActivity(new Intent(activity, AboutActivity.class));
|
||||
break;
|
||||
case R.id.nav_app_settings:
|
||||
activity.startActivity(new Intent(activity, AppSettingsActivity.class));
|
||||
break;
|
||||
case R.id.nav_twitter:
|
||||
activity.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("https://twitter.com/davdroidapp")));
|
||||
break;
|
||||
case R.id.nav_website:
|
||||
activity.startActivity(new Intent(Intent.ACTION_VIEW, Constants.webUri));
|
||||
break;
|
||||
case R.id.nav_faq:
|
||||
activity.startActivity(new Intent(Intent.ACTION_VIEW, Constants.webUri.buildUpon().appendEncodedPath("faq/").build()));
|
||||
break;
|
||||
case R.id.nav_forums:
|
||||
activity.startActivity(new Intent(Intent.ACTION_VIEW, Constants.webUri.buildUpon().appendEncodedPath("forums/").build()));
|
||||
break;
|
||||
case R.id.nav_donate:
|
||||
if (BuildConfig.FLAVOR != App.FLAVOR_GOOGLE_PLAY)
|
||||
activity.startActivity(new Intent(Intent.ACTION_VIEW, Constants.webUri.buildUpon().appendEncodedPath("donate/").build()));
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Copyright © Ricki Hirner (bitfire web engineering).
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the GNU Public License v3.0
|
||||
* which accompanies this distribution, and is available at
|
||||
* http://www.gnu.org/licenses/gpl.html
|
||||
*/
|
||||
|
||||
package at.bitfire.davdroid.ui
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.view.MenuItem
|
||||
import at.bitfire.davdroid.App
|
||||
import at.bitfire.davdroid.BuildConfig
|
||||
import at.bitfire.davdroid.R
|
||||
|
||||
class DefaultAccountsDrawerHandler: IAccountsDrawerHandler {
|
||||
|
||||
override fun onNavigationItemSelected(activity: Activity, item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
R.id.nav_about ->
|
||||
activity.startActivity(Intent(activity, AboutActivity::class.java))
|
||||
R.id.nav_app_settings ->
|
||||
activity.startActivity(Intent(activity, AppSettingsActivity::class.java))
|
||||
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))))
|
||||
R.id.nav_faq ->
|
||||
activity.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(activity.getString(R.string.navigation_drawer_faq_url))))
|
||||
R.id.nav_forums ->
|
||||
activity.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(activity.getString(R.string.homepage_url))
|
||||
.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))
|
||||
.buildUpon().appendEncodedPath("donate/").build()))
|
||||
else ->
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,203 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2013 – 2016 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.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.CompoundButton;
|
||||
import android.widget.EditText;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.RadioButton;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import java.net.IDN;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import at.bitfire.dav4android.Constants;
|
||||
import at.bitfire.davdroid.R;
|
||||
import at.bitfire.davdroid.ui.widget.EditPassword;
|
||||
|
||||
public class DefaultLoginCredentialsFragment extends Fragment implements CompoundButton.OnCheckedChangeListener {
|
||||
|
||||
RadioButton radioUseEmail;
|
||||
LinearLayout emailDetails;
|
||||
EditText editEmailAddress;
|
||||
EditPassword editEmailPassword;
|
||||
|
||||
RadioButton radioUseURL;
|
||||
LinearLayout urlDetails;
|
||||
EditText editBaseURL, editUserName;
|
||||
EditPassword editUrlPassword;
|
||||
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
View v = inflater.inflate(R.layout.login_credentials_fragment, container, false);
|
||||
|
||||
radioUseEmail = (RadioButton)v.findViewById(R.id.login_type_email);
|
||||
emailDetails = (LinearLayout)v.findViewById(R.id.login_type_email_details);
|
||||
editEmailAddress = (EditText)v.findViewById(R.id.email_address);
|
||||
editEmailPassword = (EditPassword)v.findViewById(R.id.email_password);
|
||||
|
||||
radioUseURL = (RadioButton)v.findViewById(R.id.login_type_url);
|
||||
urlDetails = (LinearLayout)v.findViewById(R.id.login_type_url_details);
|
||||
editBaseURL = (EditText)v.findViewById(R.id.base_url);
|
||||
editUserName = (EditText)v.findViewById(R.id.user_name);
|
||||
editUrlPassword = (EditPassword)v.findViewById(R.id.url_password);
|
||||
|
||||
radioUseEmail.setOnCheckedChangeListener(this);
|
||||
radioUseURL.setOnCheckedChangeListener(this);
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
// first call
|
||||
|
||||
Activity activity = getActivity();
|
||||
Intent intent = (activity != null) ? activity.getIntent() : null;
|
||||
if (intent != null) {
|
||||
// we've got initial login data
|
||||
String url = intent.getStringExtra(LoginActivity.EXTRA_URL),
|
||||
username = intent.getStringExtra(LoginActivity.EXTRA_USERNAME),
|
||||
password = intent.getStringExtra(LoginActivity.EXTRA_PASSWORD);
|
||||
|
||||
if (url != null) {
|
||||
radioUseURL.setChecked(true);
|
||||
editBaseURL.setText(url);
|
||||
editUserName.setText(username);
|
||||
editUrlPassword.setText(password);
|
||||
} else {
|
||||
radioUseEmail.setChecked(true);
|
||||
editEmailAddress.setText(username);
|
||||
editEmailPassword.setText(password);
|
||||
}
|
||||
|
||||
} else
|
||||
radioUseEmail.setChecked(true);
|
||||
}
|
||||
|
||||
final Button login = (Button)v.findViewById(R.id.login);
|
||||
login.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
LoginCredentials credentials = validateLoginData();
|
||||
if (credentials != null)
|
||||
DetectConfigurationFragment.newInstance(credentials).show(getFragmentManager(), null);
|
||||
}
|
||||
});
|
||||
|
||||
return v;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
|
||||
if (isChecked) {
|
||||
boolean loginByEmail = buttonView == radioUseEmail;
|
||||
emailDetails.setVisibility(loginByEmail ? View.VISIBLE : View.GONE);
|
||||
urlDetails.setVisibility(loginByEmail ? View.GONE : View.VISIBLE);
|
||||
(loginByEmail ? editEmailAddress : editBaseURL).requestFocus();
|
||||
}
|
||||
}
|
||||
|
||||
protected LoginCredentials validateLoginData() {
|
||||
if (radioUseEmail.isChecked()) {
|
||||
URI uri = null;
|
||||
boolean valid = true;
|
||||
|
||||
String email = editEmailAddress.getText().toString();
|
||||
if (!email.matches(".+@.+")) {
|
||||
editEmailAddress.setError(getString(R.string.login_email_address_error));
|
||||
valid = false;
|
||||
} else
|
||||
try {
|
||||
uri = new URI("mailto", email, null);
|
||||
} catch (URISyntaxException e) {
|
||||
editEmailAddress.setError(e.getLocalizedMessage());
|
||||
valid = false;
|
||||
}
|
||||
|
||||
String password = editEmailPassword.getText().toString();
|
||||
if (password.isEmpty()) {
|
||||
editEmailPassword.setError(getString(R.string.login_password_required));
|
||||
valid = false;
|
||||
}
|
||||
|
||||
return valid ? new LoginCredentials(uri, email, password) : null;
|
||||
|
||||
} else if (radioUseURL.isChecked()) {
|
||||
URI uri = null;
|
||||
boolean valid = true;
|
||||
|
||||
Uri baseUrl = Uri.parse(editBaseURL.getText().toString());
|
||||
String scheme = baseUrl.getScheme();
|
||||
if ("https".equalsIgnoreCase(scheme) || "http".equalsIgnoreCase(scheme)) {
|
||||
String host = baseUrl.getHost();
|
||||
if (StringUtils.isEmpty(host)) {
|
||||
editBaseURL.setError(getString(R.string.login_url_host_name_required));
|
||||
valid = false;
|
||||
} else
|
||||
try {
|
||||
host = IDN.toASCII(host);
|
||||
} catch(IllegalArgumentException e) {
|
||||
Constants.log.log(Level.WARNING, "Host name not conforming to RFC 3490", e);
|
||||
}
|
||||
|
||||
String path = baseUrl.getEncodedPath();
|
||||
int port = baseUrl.getPort();
|
||||
try {
|
||||
uri = new URI(baseUrl.getScheme(), null, host, port, path, null, null);
|
||||
} catch (URISyntaxException e) {
|
||||
editBaseURL.setError(e.getLocalizedMessage());
|
||||
valid = false;
|
||||
}
|
||||
} else {
|
||||
editBaseURL.setError(getString(R.string.login_url_must_be_http_or_https));
|
||||
valid = false;
|
||||
}
|
||||
|
||||
String userName = editUserName.getText().toString();
|
||||
if (userName.isEmpty()) {
|
||||
editUserName.setError(getString(R.string.login_user_name_required));
|
||||
valid = false;
|
||||
}
|
||||
|
||||
String password = editUrlPassword.getText().toString();
|
||||
if (password.isEmpty()) {
|
||||
editUrlPassword.setError(getString(R.string.login_password_required));
|
||||
valid = false;
|
||||
}
|
||||
|
||||
return valid ? new LoginCredentials(uri, userName, password) : null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
public static class Factory implements ILoginCredentialsFragment {
|
||||
|
||||
@Override
|
||||
public Fragment getFragment() {
|
||||
return new DefaultLoginCredentialsFragment();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
/*
|
||||
* 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.app.Fragment
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.CompoundButton
|
||||
import at.bitfire.dav4android.Constants
|
||||
import at.bitfire.davdroid.R
|
||||
import kotlinx.android.synthetic.standard.login_credentials_fragment.view.*
|
||||
import java.net.IDN
|
||||
import java.net.URI
|
||||
import java.net.URISyntaxException
|
||||
import java.util.logging.Level
|
||||
|
||||
class DefaultLoginCredentialsFragment: Fragment(), CompoundButton.OnCheckedChangeListener {
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
val v = inflater.inflate(R.layout.login_credentials_fragment, container, false)
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
// first call
|
||||
activity?.intent?.let {
|
||||
// we've got initial login data
|
||||
val url = it.getStringExtra(LoginActivity.EXTRA_URL)
|
||||
val username = it.getStringExtra(LoginActivity.EXTRA_USERNAME)
|
||||
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)
|
||||
} else {
|
||||
v.login_type_email.isChecked = true
|
||||
v.email_address.setText(username)
|
||||
v.email_password.setText(password)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
v.login.setOnClickListener({ _ ->
|
||||
validateLoginData()?.let { credentials ->
|
||||
DetectConfigurationFragment.newInstance(credentials).show(fragmentManager, null)
|
||||
}
|
||||
})
|
||||
|
||||
// initialize to Login by email
|
||||
onCheckedChanged(v)
|
||||
|
||||
v.login_type_email.setOnCheckedChangeListener(this)
|
||||
v.login_type_url.setOnCheckedChangeListener(this)
|
||||
|
||||
return v
|
||||
}
|
||||
|
||||
override fun onCheckedChanged(buttonView: CompoundButton, isChecked: Boolean) {
|
||||
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()
|
||||
}
|
||||
|
||||
private fun validateLoginData(): LoginCredentials? {
|
||||
if (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)
|
||||
valid = false
|
||||
} 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) {
|
||||
view.base_url.error = e.localizedMessage
|
||||
valid = false
|
||||
}
|
||||
} else {
|
||||
view.base_url.error = getString(R.string.login_url_must_be_http_or_https)
|
||||
valid = false
|
||||
}
|
||||
|
||||
val userName = view.user_name.text.toString()
|
||||
if (userName.isBlank()) {
|
||||
view.user_name.error = getString(R.string.login_user_name_required)
|
||||
valid = false
|
||||
}
|
||||
|
||||
val password = view.url_password.getText().toString()
|
||||
if (password.isEmpty()) {
|
||||
view.url_password.setError(getString(R.string.login_password_required))
|
||||
valid = false
|
||||
}
|
||||
|
||||
return if (valid && uri != null)
|
||||
LoginCredentials(uri, userName, password)
|
||||
else
|
||||
null
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
class Factory: ILoginCredentialsFragment {
|
||||
|
||||
override fun getFragment() = DefaultLoginCredentialsFragment()
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
<vector android:alpha="0.54" android:height="24dp"
|
||||
android:viewportHeight="24.0" android:viewportWidth="24.0"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FF000000" android:pathData="M19.43,12.98c0.04,-0.32 0.07,-0.64 0.07,-0.98s-0.03,-0.66 -0.07,-0.98l2.11,-1.65c0.19,-0.15 0.24,-0.42 0.12,-0.64l-2,-3.46c-0.12,-0.22 -0.39,-0.3 -0.61,-0.22l-2.49,1c-0.52,-0.4 -1.08,-0.73 -1.69,-0.98l-0.38,-2.65C14.46,2.18 14.25,2 14,2h-4c-0.25,0 -0.46,0.18 -0.49,0.42l-0.38,2.65c-0.61,0.25 -1.17,0.59 -1.69,0.98l-2.49,-1c-0.23,-0.09 -0.49,0 -0.61,0.22l-2,3.46c-0.13,0.22 -0.07,0.49 0.12,0.64l2.11,1.65c-0.04,0.32 -0.07,0.65 -0.07,0.98s0.03,0.66 0.07,0.98l-2.11,1.65c-0.19,0.15 -0.24,0.42 -0.12,0.64l2,3.46c0.12,0.22 0.39,0.3 0.61,0.22l2.49,-1c0.52,0.4 1.08,0.73 1.69,0.98l0.38,2.65c0.03,0.24 0.24,0.42 0.49,0.42h4c0.25,0 0.46,-0.18 0.49,-0.42l0.38,-2.65c0.61,-0.25 1.17,-0.59 1.69,-0.98l2.49,1c0.23,0.09 0.49,0 0.61,-0.22l2,-3.46c0.12,-0.22 0.07,-0.49 -0.12,-0.64l-2.11,-1.65zM12,15.5c-1.93,0 -3.5,-1.57 -3.5,-3.5s1.57,-3.5 3.5,-3.5 3.5,1.57 3.5,3.5 -1.57,3.5 -3.5,3.5z"/>
|
||||
</vector>
|
||||
@@ -1,14 +0,0 @@
|
||||
<!--
|
||||
~ Copyright © 2013 – 2016 Ricki Hirner (bitfire web engineering).
|
||||
~ All rights reserved. This program and the accompanying materials
|
||||
~ are made available under the terms of the GNU Public License v3.0
|
||||
~ which accompanies this distribution, and is available at
|
||||
~ http://www.gnu.org/licenses/gpl.html
|
||||
-->
|
||||
|
||||
<vector android:height="24dp"
|
||||
android:viewportHeight="24.0" android:viewportWidth="24.0"
|
||||
android:alpha="0.54"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FF000000" android:pathData="M18,16.08c-0.76,0 -1.44,0.3 -1.96,0.77L8.91,12.7c0.05,-0.23 0.09,-0.46 0.09,-0.7s-0.04,-0.47 -0.09,-0.7l7.05,-4.11c0.54,0.5 1.25,0.81 2.04,0.81 1.66,0 3,-1.34 3,-3s-1.34,-3 -3,-3 -3,1.34 -3,3c0,0.24 0.04,0.47 0.09,0.7L8.04,9.81C7.5,9.31 6.79,9 6,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3c0.79,0 1.5,-0.31 2.04,-0.81l7.12,4.16c-0.05,0.21 -0.08,0.43 -0.08,0.65 0,1.61 1.31,2.92 2.92,2.92 1.61,0 2.92,-1.31 2.92,-2.92s-1.31,-2.92 -2.92,-2.92z"/>
|
||||
</vector>
|
||||
@@ -1,5 +0,0 @@
|
||||
<vector android:alpha="0.54" android:height="24dp"
|
||||
android:viewportHeight="24.0" android:viewportWidth="24.0"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FF000000" android:pathData="M12,4V1L8,5l4,4V6c3.31,0 6,2.69 6,6 0,1.01 -0.25,1.97 -0.7,2.8l1.46,1.46C19.54,15.03 20,13.57 20,12c0,-4.42 -3.58,-8 -8,-8zm0,14c-3.31,0 -6,-2.69 -6,-6 0,-1.01 0.25,-1.97 0.7,-2.8L5.24,7.74C4.46,8.97 4,10.43 4,12c0,4.42 3.58,8 8,8v3l4,-4 -4,-4v3z"/>
|
||||
</vector>
|
||||
@@ -11,8 +11,6 @@
|
||||
<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_development_version">DAVdroid preview vydání</string>
|
||||
<string name="startup_development_version_message">Toto je vývojová verze aplikace DAVdroid. Mějte na paměti, že vše nemusí správně fungovat. Budeme rádi za konstruktivní zpětnou vazbu, která pomůže vylepšit DAVdroid.</string>
|
||||
<string name="startup_development_version_give_feedback">Dát zpětnou vazbu</string>
|
||||
<string name="startup_donate">Open Source informace</string>
|
||||
<string name="startup_donate_message">Jsme velice rádi že používáte DAVdroid, software s otevřeným zdrojovým kódem (GPLv3). Vývoj této aplikace je náročný a trval již několik tisíc hodin, velice nás potěší přispějete-li na jeho vývoj.</string>
|
||||
@@ -44,7 +42,6 @@
|
||||
<string name="navigation_drawer_external_links">Externí odkazy</string>
|
||||
<string name="navigation_drawer_website">Webová stránka</string>
|
||||
<string name="navigation_drawer_faq">FAQ</string>
|
||||
<string name="navigation_drawer_forums">Komunita</string>
|
||||
<string name="navigation_drawer_donate">Obdarovat</string>
|
||||
<string name="account_list_empty">Vítejte v aplikaci DAVdroid!\n\nNyní můžete přidat CalDAV/CardDAV účet.</string>
|
||||
<!--DavService-->
|
||||
@@ -56,7 +53,19 @@
|
||||
<string name="app_settings_reset_hints">Resetovat nápovědu</string>
|
||||
<string name="app_settings_reset_hints_summary">Znovu povolí vypnuté texty nápovědy</string>
|
||||
<string name="app_settings_reset_hints_success">Budou zobrazovány všechny texty nápovědy</string>
|
||||
<string name="app_settings_connection">Připojení</string>
|
||||
<string name="app_settings_override_proxy">Přepsat proxy nastavení</string>
|
||||
<string name="app_settings_override_proxy_on">Použít vlastní proxy nastavení</string>
|
||||
<string name="app_settings_override_proxy_off">Použít výchozí systémová proxy nastavení</string>
|
||||
<string name="app_settings_override_proxy_host">HTTP proxy hostname</string>
|
||||
<string name="app_settings_override_proxy_port">HTTP proxy port</string>
|
||||
<string name="app_settings_security">Zabezpečení</string>
|
||||
<string name="app_settings_distrust_system_certs">Nedůvěřovat systémovým certifikátům</string>
|
||||
<string name="app_settings_distrust_system_certs_on">Systémovým a uživatelem přidaným CA nebude důvěřováno</string>
|
||||
<string name="app_settings_distrust_system_certs_off">Systémovým a uživatelem přidaným CA bude důvěřováno (doporučeno)</string>
|
||||
<string name="app_settings_reset_certificates">Resetovat (ne)důvěryhodné certifikáty</string>
|
||||
<string name="app_settings_reset_certificates_summary">Resetovat důvěryhodnost všech vlastních certifikátů</string>
|
||||
<string name="app_settings_reset_certificates_success">Všechny vlastní certifikáty byly resetovány</string>
|
||||
<string name="app_settings_debug">Ladění</string>
|
||||
<string name="app_settings_log_to_external_storage">Logovat do externího souboru</string>
|
||||
<string name="app_settings_log_to_external_storage_on">Logování do externího úložiště (pokud dostupné)</string>
|
||||
@@ -67,6 +76,9 @@
|
||||
<string name="account_synchronize_now">Synchronizovat nyní</string>
|
||||
<string name="account_synchronizing_now">Probíhá synchronizace</string>
|
||||
<string name="account_settings">Nastavení účtu</string>
|
||||
<string name="account_rename">Přejmenovat účet</string>
|
||||
<string name="account_rename_new_name">Neuložená místní data mohou být vynechána. Po přejmenování je vyžadována nová synchronizace. Nové jméno účtu:</string>
|
||||
<string name="account_rename_rename">Přejmenovat</string>
|
||||
<string name="account_delete">Smazat účet</string>
|
||||
<string name="account_delete_confirmation_title">Opravdu smazat účet?</string>
|
||||
<string name="account_delete_confirmation_text">Všechny místní kopie adresáře, kalendářů a úkolů budou smazány.</string>
|
||||
@@ -122,7 +134,6 @@
|
||||
<string name="settings_sync_interval_contacts">Interval synchronizace kontaktů</string>
|
||||
<string name="settings_sync_summary_manually">Pouze manuálně</string>
|
||||
<string name="settings_sync_summary_periodically" tools:ignore="PluralsCandidate">Každých %d minut a ihned při lokálních změnách</string>
|
||||
<string name="settings_sync_summary_not_available">Nedostupný</string>
|
||||
<string name="settings_sync_interval_calendars">Interval synchronizace kalendáře</string>
|
||||
<string name="settings_sync_interval_tasks">Interval synchronizace úkolů</string>
|
||||
<string-array name="settings_sync_interval_seconds">
|
||||
@@ -148,10 +159,6 @@
|
||||
<string name="settings_sync_wifi_only">Synchronizovat pouze přes WiFi</string>
|
||||
<string name="settings_sync_wifi_only_on">Synchronizace omezena na WiFi připojení</string>
|
||||
<string name="settings_sync_wifi_only_off">Druh připojení není brán v potaz</string>
|
||||
<string name="settings_sync_wifi_only_ssid">Omezení WiFi SSID</string>
|
||||
<string name="settings_sync_wifi_only_ssid_on">Synchronizace pouze přes %s</string>
|
||||
<string name="settings_sync_wifi_only_ssid_off">Použít všechna WiFi připojení</string>
|
||||
<string name="settings_sync_wifi_only_ssid_message">Zadejte jméno WiFi sítě (SSID) pro omezení synchronizace na tutu síť, nebo ponechte prázdné pro použití všech WiFi připojení.</string>
|
||||
<string name="settings_carddav">CardDAV</string>
|
||||
<string name="settings_contact_group_method">Metoda seskupování kontaktů</string>
|
||||
<string-array name="settings_contact_group_method_values">
|
||||
@@ -162,9 +169,6 @@
|
||||
<item>Skupiny jsou oddělené soubory VCard</item>
|
||||
<item>Skupiny jsou kategorie na kontakt</item>
|
||||
</string-array>
|
||||
<string name="settings_rfc6868_for_vcards">Použít RFC6868 pro VCard</string>
|
||||
<string name="settings_rfc6868_for_vcards_on">Uvozovky lze použít v hodnotách parametrů</string>
|
||||
<string name="settings_rfc6868_for_vcards_off">Uvozovky nelze použít v hodnotách parametrů</string>
|
||||
<string name="settings_caldav">CalDAV</string>
|
||||
<string name="settings_sync_time_range_past">Časový limit pro staré události</string>
|
||||
<string name="settings_sync_time_range_past_none">Synchronizovat všechny události</string>
|
||||
@@ -177,9 +181,6 @@
|
||||
<string name="settings_manage_calendar_colors">Spravovat barvy kalendářů</string>
|
||||
<string name="settings_manage_calendar_colors_on">Barvy kalendářů spravuje DAVdroid</string>
|
||||
<string name="settings_manage_calendar_colors_off">Barvy kalendářů nespravuje DAVdroid</string>
|
||||
<string name="settings_version_update">Aktualizace verze DAVdroid</string>
|
||||
<string name="settings_version_update_settings_updated">Vnitřní nastavení byla aktualizována.</string>
|
||||
<string name="settings_version_update_install_hint">Při problémech odinstalujte a znovu nainstalujte DAVdroid.</string>
|
||||
<!--collection management-->
|
||||
<string name="create_addressbook">Vytvořit adresář</string>
|
||||
<string name="create_addressbook_display_name_hint">Můj adresář</string>
|
||||
@@ -232,4 +233,6 @@
|
||||
</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>
|
||||
</resources>
|
||||
|
||||
@@ -2,14 +2,17 @@
|
||||
<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="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="please_wait">Vent venligst ...</string>
|
||||
<string name="send">Send</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_dont_show_again">Vis ikke igen</string>
|
||||
<string name="startup_development_version">DAVdroid Preview</string>
|
||||
<string name="startup_development_version_message">Dette er udviklingsversionen af DAVdroid. Bemærk, at nogle ting måske ikke fungerer, som man forventer. Giv os gerne konstruktiv kritik for at forbedre DAVdroid.</string>
|
||||
<string name="startup_development_version_give_feedback">Giv feedback</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>
|
||||
@@ -41,9 +44,10 @@
|
||||
<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_forums">Community</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="accounts_global_sync_disabled">Automatisk synkronisering på tværs af systemet er deaktiveret</string>
|
||||
<string name="accounts_global_sync_enable">Aktiver</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>
|
||||
@@ -53,7 +57,19 @@
|
||||
<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_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>
|
||||
<string name="app_settings_override_proxy_on">Brug brugerdefinerede proxyindstillinger</string>
|
||||
<string name="app_settings_override_proxy_off">Brug systemstandard proxyindstillinger</string>
|
||||
<string name="app_settings_override_proxy_host">HTTP proxy værtsnavn</string>
|
||||
<string name="app_settings_override_proxy_port">HTTP proxy port</string>
|
||||
<string name="app_settings_security">Sikkerhed</string>
|
||||
<string name="app_settings_distrust_system_certs">Stol ikke på systemcertifikater</string>
|
||||
<string name="app_settings_distrust_system_certs_on">System og brugertilføjede CA\'er vil ikke blive betroet</string>
|
||||
<string name="app_settings_distrust_system_certs_off">System og brugertilføjede CA\'er vil blive betroet (anbefalet)</string>
|
||||
<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_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>
|
||||
@@ -64,6 +80,9 @@
|
||||
<string name="account_synchronize_now">Synkroniser nu</string>
|
||||
<string name="account_synchronizing_now">Synkroniserer nu</string>
|
||||
<string name="account_settings">Opsætning af konti</string>
|
||||
<string name="account_rename">Omdøb 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_delete_confirmation_text">Alle lokale kopier af addessebøger, kalendere og opgavelister vil blive slettet.</string>
|
||||
@@ -119,7 +138,6 @@
|
||||
<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_summary_not_available">Ikke tilgængeligt</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_seconds">
|
||||
@@ -145,10 +163,6 @@
|
||||
<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_ssid">Begræsning til WiFi-SSID</string>
|
||||
<string name="settings_sync_wifi_only_ssid_on">Vil kun blive synkroniseret over %s</string>
|
||||
<string name="settings_sync_wifi_only_ssid_off">Alle WiFi-forbindelse vil kunne bruges</string>
|
||||
<string name="settings_sync_wifi_only_ssid_message">Indtast navnet på et WiFi-netværk (SSID) for at begrænse synkronisering til dette netværk, eller efterlad feltet blank for at acceptere alle WiFi-forbindelser.</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">
|
||||
@@ -159,9 +173,6 @@
|
||||
<item>Grupper er særskilte VCards</item>
|
||||
<item>Grupper er kategorier pr. kontakt</item>
|
||||
</string-array>
|
||||
<string name="settings_rfc6868_for_vcards">Anvend RFC6868 for VCards</string>
|
||||
<string name="settings_rfc6868_for_vcards_on">Dobbelte citationstegn kan bruges til at sætte parametre</string>
|
||||
<string name="settings_rfc6868_for_vcards_off">Dobbelte citationstegn kan ikke bruges til at sætte parametre</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>
|
||||
@@ -173,9 +184,6 @@
|
||||
<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_version_update">Versionsopdatering af DAVdroid</string>
|
||||
<string name="settings_version_update_settings_updated">Interne indstillinger er blevet opdateret.</string>
|
||||
<string name="settings_version_update_install_hint">Problemer? Afinstaller DAVdroid og geninstaller.</string>
|
||||
<!--collection management-->
|
||||
<string name="create_addressbook">Opret adressebog</string>
|
||||
<string name="create_addressbook_display_name_hint">Min adressebog</string>
|
||||
@@ -228,4 +236,6 @@
|
||||
</string-array>
|
||||
<string name="sync_error_unauthorized">Fejl i brugernavn/adgangskode</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>
|
||||
</resources>
|
||||
|
||||
@@ -7,9 +7,10 @@
|
||||
<string name="please_wait">Por favor, espere...</string>
|
||||
<string name="send">Enviar</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_development_version">Versión candidata final de DAVdroid</string>
|
||||
<string name="startup_development_version_message">Esta es una versión de desarrollo de DAVdroid. Tenga presente que puede que no todo funcione como espera. Por favor, denos una retroalimentación constructiva para mejorar DAVdroid.</string>
|
||||
<string name="startup_development_version_give_feedback">Dar retroalimentación</string>
|
||||
<string name="startup_donate">Información de código abierto</string>
|
||||
<string name="startup_donate_message">Nos alegra que uses DAVdroid, que es software de código abierto (GPLv3). Debido al duro trabajo que supone el desarrollo de DAVdroid y los cientos de horas de trabajo, por favor, considera hacer una donación.</string>
|
||||
@@ -41,9 +42,8 @@
|
||||
<string name="navigation_drawer_external_links">Enlaces externos</string>
|
||||
<string name="navigation_drawer_website">Sitio web</string>
|
||||
<string name="navigation_drawer_faq">Preguntas frequentes</string>
|
||||
<string name="navigation_drawer_forums">Comunidad</string>
|
||||
<string name="navigation_drawer_donate">Donar</string>
|
||||
<string name="account_list_empty">Bienvenido a DAVdroid!\n\nAhora puede añadir una cuenta CalDAV/CardDAV.</string>
|
||||
<string name="account_list_empty">Bienvenido a DAVdroid!\n\nAhora puedes añadir una cuenta CalDAV/CardDAV.</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>
|
||||
@@ -51,9 +51,21 @@
|
||||
<string name="app_settings">Ajustes</string>
|
||||
<string name="app_settings_user_interface">Interfaz de usuario</string>
|
||||
<string name="app_settings_reset_hints">Restablecer advertencias</string>
|
||||
<string name="app_settings_reset_hints_summary">Habilita las advertencias que han sido cesadas con anterioridad</string>
|
||||
<string name="app_settings_reset_hints_success">Todas las advertencias serán mostradas de nuevo</string>
|
||||
<string name="app_settings_reset_hints_summary">Habilita las advertencias que han sido rechazadas con anterioridad</string>
|
||||
<string name="app_settings_reset_hints_success">Todas las advertencias se mostrarán nuevamente</string>
|
||||
<string name="app_settings_connection">Conexión</string>
|
||||
<string name="app_settings_override_proxy">Anular ajustes del proxy</string>
|
||||
<string name="app_settings_override_proxy_on">Usar ajustes personalizados del proxy</string>
|
||||
<string name="app_settings_override_proxy_off">Usar ajustes del proxy predefinidos por el sistema</string>
|
||||
<string name="app_settings_override_proxy_host">Nombre del host del proxy HTTP</string>
|
||||
<string name="app_settings_override_proxy_port">Puerto del proxy HTTP</string>
|
||||
<string name="app_settings_security">Seguridad</string>
|
||||
<string name="app_settings_distrust_system_certs">Invalidar los certificados del sistema</string>
|
||||
<string name="app_settings_distrust_system_certs_on">Los CA del sistema y los añadidos por el usuario no serán válidos</string>
|
||||
<string name="app_settings_distrust_system_certs_off">Los CA del sistema y los añadidos por el usuario serán usados y de confianza (recomendado)</string>
|
||||
<string name="app_settings_reset_certificates">Reiniciar certificados (in)validados</string>
|
||||
<string name="app_settings_reset_certificates_summary">Reinicia la validez de todos los certificados particulares</string>
|
||||
<string name="app_settings_reset_certificates_success">Todos los certificados particulares han sido limpiados</string>
|
||||
<string name="app_settings_debug">Depuración</string>
|
||||
<string name="app_settings_log_to_external_storage">Registrar en fichero externo</string>
|
||||
<string name="app_settings_log_to_external_storage_on">Registro en almacenamiento externo (si está disponible)</string>
|
||||
@@ -64,9 +76,12 @@
|
||||
<string name="account_synchronize_now">Sincronizar ahora</string>
|
||||
<string name="account_synchronizing_now">Sincronizando...</string>
|
||||
<string name="account_settings">Ajustes de cuenta</string>
|
||||
<string name="account_rename">Renombrar cuenta</string>
|
||||
<string name="account_rename_new_name">Información local no guardada puede ser desechada. Se requiere resincronizar después de renombrar. Nuevo nombre de cuenta:</string>
|
||||
<string name="account_rename_rename">Renombrar</string>
|
||||
<string name="account_delete">Eliminar cuenta</string>
|
||||
<string name="account_delete_confirmation_title">¿Seguro que desea eliminar la cuenta?</string>
|
||||
<string name="account_delete_confirmation_text">Todas las copias locales de sus contactos, calendarios y tareas serán eliminadas.</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_refresh_address_book_list">Refrescar contactos</string>
|
||||
<string name="account_create_new_address_book">Crear nueva lista de contactos</string>
|
||||
<string name="account_refresh_calendar_list">Refrescar calendario</string>
|
||||
@@ -74,13 +89,13 @@
|
||||
<!--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 sus calendarios locales, DAVdroid necesita acceder a los mismos.</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 sus contactos locales, DAVdroid necesita acceder a los mismos.</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 sus listas de tareas locales, DAVdroid necesita acceder a 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>
|
||||
<!--AddAccountActivity-->
|
||||
<string name="login_title">Añadir cuenta</string>
|
||||
@@ -99,12 +114,13 @@
|
||||
<string name="login_back">Volver</string>
|
||||
<string name="login_create_account">Crear cuenta</string>
|
||||
<string name="login_account_name">Nombre de cuenta</string>
|
||||
<string name="login_account_name_info">Use su dirección de correo como nombre de su cuenta puesto que Android usará el nombre de la cuenta como campo de \"organizador\" en los eventos que cree. No puede tener dos cuentas con el mismo nombre.</string>
|
||||
<string name="login_account_name_info">Usa tu dirección de correo como nombre de cuenta puesto que Android usará el nombre de la cuenta como campo de \"organizador\" en los eventos que cree. No puedes tener dos cuentas con el mismo nombre.</string>
|
||||
<string name="login_account_contact_group_method">Método de contacto de grupo:</string>
|
||||
<string name="login_account_name_required">Nombre de cuenta requerido</string>
|
||||
<string name="login_account_not_created">La cuenta no pudo ser creada</string>
|
||||
<string name="login_configuration_detection">Detectar configuración</string>
|
||||
<string name="login_querying_server">Por favor espere, consultando al servidor...</string>
|
||||
<string name="login_no_caldav_carddav">No se pudo encontrar servicio CalDAV o CardDAV.</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>
|
||||
@@ -118,7 +134,6 @@
|
||||
<string name="settings_sync_interval_contacts">Intervalo de sincronización de contactos</string>
|
||||
<string name="settings_sync_summary_manually">Solo manualmente</string>
|
||||
<string name="settings_sync_summary_periodically" tools:ignore="PluralsCandidate">Cada %d minutos + inmediatamente con cambios locales</string>
|
||||
<string name="settings_sync_summary_not_available">No disponible</string>
|
||||
<string name="settings_sync_interval_calendars">Intervalo de sincronización de calendarios</string>
|
||||
<string name="settings_sync_interval_tasks">Intervalo de sincronizacion de Tasks</string>
|
||||
<string-array name="settings_sync_interval_seconds">
|
||||
@@ -143,11 +158,17 @@
|
||||
</string-array>
|
||||
<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 tomada en consideración</string>
|
||||
<string name="settings_sync_wifi_only_ssid">Restricción WiFi SSID</string>
|
||||
<string name="settings_sync_wifi_only_ssid_on">Sólo se sincronizará sobre %s</string>
|
||||
<string name="settings_sync_wifi_only_ssid_off">Todas las conexiones WiFi pueden ser usadas</string>
|
||||
<string name="settings_sync_wifi_only_ssid_message">Introduzca el nombre de una red WiFi (SSID) para restringir la sincronización a esta red, o deje el campo en blanco para usar todas las conexiones WiFi.</string>
|
||||
<string name="settings_sync_wifi_only_off">Tipo de conexión no tenido en cuenta</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">
|
||||
<item>GROUP_VCARDS</item>
|
||||
<item>CATEGORIES</item>
|
||||
</string-array>
|
||||
<string-array name="settings_contact_group_method_entries">
|
||||
<item>Los groups tienen VCards separadas</item>
|
||||
<item>Los groups tienen una categoría por contacto</item>
|
||||
</string-array>
|
||||
<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>
|
||||
@@ -155,13 +176,10 @@
|
||||
<item quantity="one">Los eventos anteriores a un día serán ignorados</item>
|
||||
<item quantity="other">Los eventos anteriores a %d días serán ignorados</item>
|
||||
</plurals>
|
||||
<string name="settings_sync_time_range_past_message">Los eventos anteriores a este número de días serán ignorados (puede ser 0). Deje en blanco el campo para sincronizar todos los eventos.</string>
|
||||
<string name="settings_sync_time_range_past_message">Los eventos anteriores a este número de días serán ignorados (puede ser 0). Deja en blanco el campo para sincronizar todos los eventos.</string>
|
||||
<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_version_update">Actualización de la versión de Android</string>
|
||||
<string name="settings_version_update_settings_updated">Los ajustes internos han sido actualizados.</string>
|
||||
<string name="settings_version_update_install_hint">¿Problemas? Desinstala y vuelve a instalar DAVdroid.</string>
|
||||
<!--collection management-->
|
||||
<string name="create_addressbook">Crear nueva lista de contactos</string>
|
||||
<string name="create_addressbook_display_name_hint">Agendas</string>
|
||||
@@ -180,7 +198,7 @@
|
||||
<string name="create_collection_home_set">Establecer localización:</string>
|
||||
<string name="create_collection_create">Crear</string>
|
||||
<string name="delete_collection">Eliminar colección</string>
|
||||
<string name="delete_collection_confirm_title">¿Está seguro/a?</string>
|
||||
<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>
|
||||
<!--ExceptionInfoFragment-->
|
||||
@@ -198,6 +216,22 @@
|
||||
<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>
|
||||
<!--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>
|
||||
</resources>
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<!--common strings-->
|
||||
<string name="app_name">DAVdroid</string>
|
||||
<string name="account_title_address_book">Carnet d\'adresses DAVdroid</string>
|
||||
<string name="address_books_authority_title">Carnets d\'adresses</string>
|
||||
<string name="help">Aide</string>
|
||||
<string name="manage_accounts">Gestion des comptes</string>
|
||||
<string name="please_wait">SVP attendez ...</string>
|
||||
@@ -9,10 +11,8 @@
|
||||
<!--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ésactivez pour DAVdroid</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_development_version">Pré-version de DAVdroid</string>
|
||||
<string name="startup_development_version_message">Il s\'agit d\'une version de développement de DAVdroid. Il se peut que les choses ne fonctionnent pas comme prévu. S’il vous plaît faites-nous un retour pour améliorer DAVdroid.</string>
|
||||
<string name="startup_development_version_give_feedback">Faire un commentaire</string>
|
||||
<string name="startup_donate">Open-Source Information</string>
|
||||
<string name="startup_donate_message">Nous sommes heureux que vous utilisez DAVdroid, qui est un logiciel open-source (GPLv3). Parce que développer DAVdroid est un travail difficile et nous a pris de nombreuses heures, s\'il vous plaît envisager de faire un don.</string>
|
||||
@@ -20,7 +20,7 @@
|
||||
<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\'information</string>
|
||||
<string name="startup_google_play_accounts_removed_more_info">Plus d\'informations</string>
|
||||
<string name="startup_opentasks_not_installed">OpenTasks n\'est pas installé</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>
|
||||
@@ -37,15 +37,17 @@
|
||||
<!--AccountsActivity-->
|
||||
<string name="navigation_drawer_open">Ouvrir le tiroir de navigation</string>
|
||||
<string name="navigation_drawer_close">Fermer le tiroir de navigation</string>
|
||||
<string name="navigation_drawer_subtitle">Adaptateur de synchronisation CalDAV/CardDAV</string>
|
||||
<string name="navigation_drawer_about">A propos / Licence</string>
|
||||
<string name="navigation_drawer_settings">Paramètres</string>
|
||||
<string name="navigation_drawer_news_updates">Actualité & mise à jour</string>
|
||||
<string name="navigation_drawer_external_links">Liens externes</string>
|
||||
<string name="navigation_drawer_website">Site Web</string>
|
||||
<string name="navigation_drawer_faq">FAQ</string>
|
||||
<string name="navigation_drawer_forums">Communauté</string>
|
||||
<string name="navigation_drawer_donate">Faire un don</string>
|
||||
<string name="account_list_empty">Bienvenue sur DAVdroid!\n\nVous pouvez maintenant ajouter un compte CalDAV/CardDAV.</string>
|
||||
<string name="accounts_global_sync_disabled">La synchronisation automatique globale est désactivée</string>
|
||||
<string name="accounts_global_sync_enable">Activer</string>
|
||||
<!--DavService-->
|
||||
<string name="dav_service_refresh_failed">La détection du service a échoué</string>
|
||||
<string name="dav_service_refresh_couldnt_refresh">Impossible d\'actualiser la liste de collection</string>
|
||||
@@ -53,9 +55,21 @@
|
||||
<string name="app_settings">Paramètres</string>
|
||||
<string name="app_settings_user_interface">Interface utilisateur</string>
|
||||
<string name="app_settings_reset_hints">Réinitialiser les astuces</string>
|
||||
<string name="app_settings_reset_hints_summary">Réactiver les astuces qui ont été vu précédemment</string>
|
||||
<string name="app_settings_reset_hints_summary">Réactiver les astuces qui ont été vues précédemment</string>
|
||||
<string name="app_settings_reset_hints_success">Toutes les astuces seront affichés à nouveau</string>
|
||||
<string name="app_settings_connection">Connexion</string>
|
||||
<string name="app_settings_override_proxy">Ignorer les paramètres proxy</string>
|
||||
<string name="app_settings_override_proxy_on">Utiliser des paramètres proxy personnalisés</string>
|
||||
<string name="app_settings_override_proxy_off">Utiliser les paramètres proxy du système</string>
|
||||
<string name="app_settings_override_proxy_host">Nom de l\'hôte du proxy HTTP</string>
|
||||
<string name="app_settings_override_proxy_port">Port du proxy HTTP</string>
|
||||
<string name="app_settings_security">Sécurité</string>
|
||||
<string name="app_settings_distrust_system_certs">Révoquer les certificats du système</string>
|
||||
<string name="app_settings_distrust_system_certs_on">Les certificats du système et ceux ajoutés par l\'utilisateur ne seront pas dignes de confiance</string>
|
||||
<string name="app_settings_distrust_system_certs_off">Les certificats du système et ceux ajoutés par l\'utilisateur seront dignes de confiance (recommandé)</string>
|
||||
<string name="app_settings_reset_certificates">Réinitialiser les certificats de (non)confiance</string>
|
||||
<string name="app_settings_reset_certificates_summary">Réinitialiser la confiance de tous les certificats personnalisés</string>
|
||||
<string name="app_settings_reset_certificates_success">Tous les certificats personnalisés ont été effacés</string>
|
||||
<string name="app_settings_debug">Débogage</string>
|
||||
<string name="app_settings_log_to_external_storage">Journaliser dans un fichier externe</string>
|
||||
<string name="app_settings_log_to_external_storage_on">Journaliser sur le stockage externe (si disponible)</string>
|
||||
@@ -66,11 +80,14 @@
|
||||
<string name="account_synchronize_now">Synchroniser maintenant</string>
|
||||
<string name="account_synchronizing_now">Synchronisation en cours</string>
|
||||
<string name="account_settings">Paramètres du compte</string>
|
||||
<string name="account_rename">Renommer le compte</string>
|
||||
<string name="account_rename_new_name">Les données locales non enregistrées pourraient être perdues. Une re-synchronisation est nécessaire après avoir renommé le compte. Nouveau nom du compte : </string>
|
||||
<string name="account_rename_rename">Renommer</string>
|
||||
<string name="account_delete">Supprimer le compte</string>
|
||||
<string name="account_delete_confirmation_title">Voulez-vous vraiment supprimer le compte?</string>
|
||||
<string name="account_delete_confirmation_text">Toutes les copies locales des carnets d\'adresses, des calendriers et des listes de tâches seront supprimées.</string>
|
||||
<string name="account_refresh_address_book_list">Actualiser le carnet d\'adresse</string>
|
||||
<string name="account_create_new_address_book">Créer un nouveau carnet d\'adresse</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>
|
||||
<!--PermissionsActivity-->
|
||||
@@ -85,10 +102,11 @@
|
||||
<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>
|
||||
<string name="login_email_address_error">Une adresse e-mail valide est requis</string>
|
||||
<string name="login_email_address_error">Une adresse e-mail valide est requise</string>
|
||||
<string name="login_password">Mot de passe</string>
|
||||
<string name="login_password_required">Mot de passe requis</string>
|
||||
<string name="login_type_url">Connexion avec une URL et un nom d\'utilisateur</string>
|
||||
@@ -102,6 +120,7 @@
|
||||
<string name="login_create_account">Créer un compte</string>
|
||||
<string name="login_account_name">Nom du compte</string>
|
||||
<string name="login_account_name_info">Utilisez votre adresse e-mail comme nom de compte car Android utilisera ce nom en tant que champ ORGANISATEUR pour les événements que vous créerez. Vous ne pouvez pas avoir deux comptes avec le même nom.</string>
|
||||
<string name="login_account_contact_group_method">Méthode pour les contacts de type groupe :</string>
|
||||
<string name="login_account_name_required">Nom du compte requis</string>
|
||||
<string name="login_account_not_created">Le compte n\'a pas pu être créé</string>
|
||||
<string name="login_configuration_detection">Détection de la configuration</string>
|
||||
@@ -117,12 +136,11 @@
|
||||
<string name="settings_password_summary">Mettre à jour le mot de passe </string>
|
||||
<string name="settings_enter_password">Saisissez votre mot de passe :</string>
|
||||
<string name="settings_sync">Synchronisation</string>
|
||||
<string name="settings_sync_interval_contacts">Interval de synchronisation des carnets d\'adresses</string>
|
||||
<string name="settings_sync_interval_contacts">Intervalle de synchronisation des carnets d\'adresses</string>
|
||||
<string name="settings_sync_summary_manually">Manuellement</string>
|
||||
<string name="settings_sync_summary_periodically" tools:ignore="PluralsCandidate">Toutes les %d minutes et immédiatement après un changement local</string>
|
||||
<string name="settings_sync_summary_not_available">Indisponible</string>
|
||||
<string name="settings_sync_interval_calendars">Interval de synchronisation des agendas</string>
|
||||
<string name="settings_sync_interval_tasks">Interval de synchronisation des tâches</string>
|
||||
<string name="settings_sync_interval_calendars">Intervalle de synchronisation des agendas</string>
|
||||
<string name="settings_sync_interval_tasks">Intervalle de synchronisation des tâches</string>
|
||||
<string-array name="settings_sync_interval_seconds">
|
||||
<item>-1</item>
|
||||
<item>300</item>
|
||||
@@ -146,18 +164,16 @@
|
||||
<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_ssid">Restriction WiFi SSID</string>
|
||||
<string name="settings_sync_wifi_only_ssid_on">Sera seulement synchroniser avec %s</string>
|
||||
<string name="settings_sync_wifi_only_ssid_off">Toutes les connexions WiFi peuvent être utilisées</string>
|
||||
<string name="settings_sync_wifi_only_ssid_message">Entrez le nom d\'un réseau WiFi (SSID) pour restreindre la synchronisation à ce réseau, ou laisser vide pour autoriser toutes les connexions WiFi.</string>
|
||||
<string name="settings_carddav">CardDAV</string>
|
||||
<string name="settings_contact_group_method">Méthode pour les contacts de type groupe</string>
|
||||
<string-array name="settings_contact_group_method_values">
|
||||
<item>GROUP_VCARDS</item>
|
||||
<item>CATEGORIES</item>
|
||||
</string-array>
|
||||
<string name="settings_rfc6868_for_vcards">Utiliser RFC6868 pour VCards</string>
|
||||
<string name="settings_rfc6868_for_vcards_on">Les guillemets doubles peuvent être utilisés dans les valeurs de paramètre</string>
|
||||
<string name="settings_rfc6868_for_vcards_off">Les guillemets doubles ne peuvent pas être utilisés dans les valeurs de paramètre</string>
|
||||
<string-array name="settings_contact_group_method_entries">
|
||||
<item>Les groupes sont des VCards indépendantes</item>
|
||||
<item>Les groupes sont des catégories pour chacun des contacts</item>
|
||||
</string-array>
|
||||
<string name="settings_caldav">CalDAV</string>
|
||||
<string name="settings_sync_time_range_past">Limite des événements passés</string>
|
||||
<string name="settings_sync_time_range_past_none">Tous les événements seront synchronisés</string>
|
||||
@@ -169,9 +185,6 @@
|
||||
<string name="settings_manage_calendar_colors">Choisir couleur du calendrier</string>
|
||||
<string name="settings_manage_calendar_colors_on">Les couleurs de calendrier sont gérées par DAVdroid</string>
|
||||
<string name="settings_manage_calendar_colors_off">Les couleurs de calendrier ne sont pas gérées par DAVdroid</string>
|
||||
<string name="settings_version_update">Mise à jour de la version de DAVdroid</string>
|
||||
<string name="settings_version_update_settings_updated">Les paramètres internes ont été mis à jour.</string>
|
||||
<string name="settings_version_update_install_hint">Un problème? Désinstaller DAVdroid, puis réinstaller.</string>
|
||||
<!--collection management-->
|
||||
<string name="create_addressbook">Créer un carnet d\'adresses</string>
|
||||
<string name="create_addressbook_display_name_hint">Mon carnet d\'adresses</string>
|
||||
@@ -224,4 +237,6 @@
|
||||
</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>
|
||||
</resources>
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<!--common strings-->
|
||||
<string name="app_name">DAVdroid</string>
|
||||
<string name="account_title_address_book">DAVdroid címjegyzék</string>
|
||||
<string name="address_books_authority_title">Címjegyzékek</string>
|
||||
<string name="help">Súgó</string>
|
||||
<string name="manage_accounts">Fiókok kezelése</string>
|
||||
<string name="please_wait">Kérjük, várjon ...</string>
|
||||
@@ -11,8 +13,6 @@
|
||||
<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_development_version">DAVdroid előzetes kiadás</string>
|
||||
<string name="startup_development_version_message">Ez a DAVdroid egy fejlesztői verziója. Elképzelhető, hogy nem minden működik úgy, ahogyan kellene. Ha így lenne, kérjük küldjön a tapasztaltakról visszajelzést.</string>
|
||||
<string name="startup_development_version_give_feedback">Visszajelzés küldése</string>
|
||||
<string name="startup_donate">A forrás nyíltságával kapcsolatos információk</string>
|
||||
<string name="startup_donate_message">Örülünk, hogy használja a DAVdroidot. A DAVdroid nyílt forráskódú (GPLv3) szoftver, de a fejlesztése kemény munkát jelent, már eddig több ezer munkaórát emésztett fel, ezért kérjük, fontolja meg, hogy támogassa munkánkat.</string>
|
||||
@@ -44,9 +44,10 @@
|
||||
<string name="navigation_drawer_external_links">Weblapok</string>
|
||||
<string name="navigation_drawer_website">Honlap</string>
|
||||
<string name="navigation_drawer_faq">GYIK</string>
|
||||
<string name="navigation_drawer_forums">Közösség</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>
|
||||
<string name="accounts_global_sync_disabled">A rendszerszintű automatikus szinkronizálás ki van kapcsolva</string>
|
||||
<string name="accounts_global_sync_enable">Bekapcsolás</string>
|
||||
<!--DavService-->
|
||||
<string name="dav_service_refresh_failed">Szolgáltatások felderítése nem sikerült</string>
|
||||
<string name="dav_service_refresh_couldnt_refresh">Gyűjteménylista frissítése nem sikerült</string>
|
||||
@@ -56,7 +57,19 @@
|
||||
<string name="app_settings_reset_hints">Tippek visszaállítása</string>
|
||||
<string name="app_settings_reset_hints_summary">Újra jelenjen meg az összes tipp</string>
|
||||
<string name="app_settings_reset_hints_success">Az összes tipp újra meg fog jelenni</string>
|
||||
<string name="app_settings_connection">Kapcsolat</string>
|
||||
<string name="app_settings_override_proxy">Proxybeállítások felülírása</string>
|
||||
<string name="app_settings_override_proxy_on">Egyedi proxybeállítások</string>
|
||||
<string name="app_settings_override_proxy_off">Az alapértelmezett proxybeállítás használata</string>
|
||||
<string name="app_settings_override_proxy_host">HTTP proxyállomás neve</string>
|
||||
<string name="app_settings_override_proxy_port">HTTP proxy port</string>
|
||||
<string name="app_settings_security">Biztonság</string>
|
||||
<string name="app_settings_distrust_system_certs">A rendszertanúsítványok elfogadása</string>
|
||||
<string name="app_settings_distrust_system_certs_on">A rendszer által kezelt, előre vagy felhasználó által telepített tanúsítványok figyelmen kívül lesznek hagyva</string>
|
||||
<string name="app_settings_distrust_system_certs_off">A rendszer által kezelt, előre vagy felhasználó által telepített tanúsítványok megbízhatóak (javasolt)</string>
|
||||
<string name="app_settings_reset_certificates">A tanúsítványok megbízhatóságának törlésére</string>
|
||||
<string name="app_settings_reset_certificates_summary">A tanúsítványok megbízhatóságával kapcsolatos beállítások törlésére</string>
|
||||
<string name="app_settings_reset_certificates_success">A tanúsítványok megbízhatóságával kapcsolatos beállítások törölve</string>
|
||||
<string name="app_settings_debug">Hibakeresés</string>
|
||||
<string name="app_settings_log_to_external_storage">Naplózás fájlba</string>
|
||||
<string name="app_settings_log_to_external_storage_on">Naplózás külső tárhelyre (ha elérhető)</string>
|
||||
@@ -67,6 +80,9 @@
|
||||
<string name="account_synchronize_now">Szinkronizálás most</string>
|
||||
<string name="account_synchronizing_now">Szinkronizálás</string>
|
||||
<string name="account_settings">Fiókbeállítások</string>
|
||||
<string name="account_rename">Fiók átnevezése</string>
|
||||
<string name="account_rename_new_name">Az elmentetlen helyben tárolt adatok elvesznek. Az átnevezés után szinkronizálásra lesz szükség. Új fióknév:</string>
|
||||
<string name="account_rename_rename">Átnevez</string>
|
||||
<string name="account_delete">Fiók törlése</string>
|
||||
<string name="account_delete_confirmation_title">Valóban törölni akarja a fiókot?</string>
|
||||
<string name="account_delete_confirmation_text">Az összes címjegyzék, naptár és feladatlista helyi példányai törölve lesznek.</string>
|
||||
@@ -122,7 +138,6 @@
|
||||
<string name="settings_sync_interval_contacts">Névjegyszinkronizálás sűrűsége</string>
|
||||
<string name="settings_sync_summary_manually">Manuális</string>
|
||||
<string name="settings_sync_summary_periodically" tools:ignore="PluralsCandidate">Minden %d percben + az eszközön történt módosítás után</string>
|
||||
<string name="settings_sync_summary_not_available">Nem elérhető</string>
|
||||
<string name="settings_sync_interval_calendars">Naptárszinkronizálás sűrűsége</string>
|
||||
<string name="settings_sync_interval_tasks">Feladatlisták szinkronizálásának sűrűsége</string>
|
||||
<string-array name="settings_sync_interval_seconds">
|
||||
@@ -148,10 +163,6 @@
|
||||
<string name="settings_sync_wifi_only">Szinkronizálás csak WIFI-n</string>
|
||||
<string name="settings_sync_wifi_only_on">Csak WIFI kapcsolat keresztül történjen szinkronizálás</string>
|
||||
<string name="settings_sync_wifi_only_off">Szinkronizálás a kapcsolat típusától függetlenül</string>
|
||||
<string name="settings_sync_wifi_only_ssid">WIFI SSID szűkítés</string>
|
||||
<string name="settings_sync_wifi_only_ssid_on">Szinkronizálás csak a(z) %s hálózatra kapcsolódva</string>
|
||||
<string name="settings_sync_wifi_only_ssid_off">Szinkronizálás bármely WIFI hálózaton</string>
|
||||
<string name="settings_sync_wifi_only_ssid_message">Adja meg a WIFI hálózat nevét (SSID) a szinkronizálás egy hálózatra való korlátozához, vagy hagyja üresen, ha nem akar ilyen szűkítést.</string>
|
||||
<string name="settings_carddav">CardDAV</string>
|
||||
<string name="settings_contact_group_method">A csoportok kezelésének módja</string>
|
||||
<string-array name="settings_contact_group_method_values">
|
||||
@@ -162,9 +173,6 @@
|
||||
<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_rfc6868_for_vcards">RFC6868 szabványnak megfelelő VCards használata</string>
|
||||
<string name="settings_rfc6868_for_vcards_on">A paraméterértékek tartalmazhatnak idézőjelet</string>
|
||||
<string name="settings_rfc6868_for_vcards_off">A paraméterértékek nem tartalmazhatnak idézőjelet</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>
|
||||
@@ -176,9 +184,6 @@
|
||||
<string name="settings_manage_calendar_colors">Naptárszínek kezelése</string>
|
||||
<string name="settings_manage_calendar_colors_on">A naptárszíneket a DAVdroid kezeli</string>
|
||||
<string name="settings_manage_calendar_colors_off">A naptárszíneket nem a DAVdroid kezeli</string>
|
||||
<string name="settings_version_update">DAVdroid frissítése</string>
|
||||
<string name="settings_version_update_settings_updated">A belső beállítások frissítve lettek.</string>
|
||||
<string name="settings_version_update_install_hint">Probléma? Próbálja meg törölni majd újratelepíteni a DAVdroid-ot.</string>
|
||||
<!--collection management-->
|
||||
<string name="create_addressbook">Címjegyzék létrehozása</string>
|
||||
<string name="create_addressbook_display_name_hint">Új címjegyzék</string>
|
||||
@@ -231,4 +236,6 @@
|
||||
</string-array>
|
||||
<string name="sync_error_unauthorized">A felhasználónév vagy jelszó hibás</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>
|
||||
</resources>
|
||||
|
||||
@@ -2,84 +2,240 @@
|
||||
<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="help">Aiuto</string>
|
||||
<string name="manage_accounts">Gestione account</string>
|
||||
<string name="please_wait">Attendere prego …</string>
|
||||
<string name="send">Invia</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_development_version_give_feedback">Invia un rapporto</string>
|
||||
<string name="startup_donate">Informazioni sull\'Open-Source</string>
|
||||
<string name="startup_donate_message">Siamo soddisfatti del tuo uso di DAVdroid, programma Open-Source (GPLv3). Poiché lo sviluppo è un\'iniziativa complessa che comporta molte ore di lavoro ti invitiamo a fare una donazione.</string>
|
||||
<string name="startup_donate_now">Mostra pagina donazioni</string>
|
||||
<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_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_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>
|
||||
<string name="logging_to_external_storage">Log su dispositivo esterno: %s</string>
|
||||
<string name="logging_to_external_storage_warning">Cancellare prima possibile i file di log!</string>
|
||||
<string name="logging_couldnt_create_file">Non riesco a creare il file di log esterno: %s</string>
|
||||
<string name="logging_no_external_storage">Dispositivo esterno non disponibile</string>
|
||||
<!--AccountsActivity-->
|
||||
<string name="navigation_drawer_open">Apri barra di navigazione</string>
|
||||
<string name="navigation_drawer_close">Chiudi barra di navigazione</string>
|
||||
<string name="navigation_drawer_subtitle">CalDAV/CardDAV adattatore di sincronizzazione</string>
|
||||
<string name="navigation_drawer_about">Informazioni / Licenza</string>
|
||||
<string name="navigation_drawer_settings">Impostazioni</string>
|
||||
<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_faq">Domande Frequenti</string>
|
||||
<string name="navigation_drawer_forums">Comunità</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>
|
||||
<string name="accounts_global_sync_enable">Attiva</string>
|
||||
<!--DavService-->
|
||||
<string name="dav_service_refresh_failed">Fallita l\'individuazione dei servizi</string>
|
||||
<string name="dav_service_refresh_couldnt_refresh">Impossibile aggiornare la lista delle raccolte</string>
|
||||
<!--AppSettingsActivity-->
|
||||
<string name="app_settings">Impostazioni</string>
|
||||
<string name="app_settings_user_interface">Interfaccia utente</string>
|
||||
<string name="app_settings_reset_hints">Reimposta i suggerimenti</string>
|
||||
<string name="app_settings_reset_hints_summary">Riabilita i suggerimenti precedentemente disabilitati</string>
|
||||
<string name="app_settings_reset_hints_success">I suggerimenti verranno mostrati</string>
|
||||
<string name="app_settings_connection">Connessione</string>
|
||||
<string name="app_settings_override_proxy">Non rispettare la impostazioni del proxy</string>
|
||||
<string name="app_settings_override_proxy_on">Impostazioni personalizzate del proxy</string>
|
||||
<string name="app_settings_override_proxy_off">Usa le impostazioni di sistema del proxy</string>
|
||||
<string name="app_settings_override_proxy_host">Nome host del proxy HTTP</string>
|
||||
<string name="app_settings_override_proxy_port">Porta del proxy HTTP</string>
|
||||
<string name="app_settings_security">Sicurezza</string>
|
||||
<string name="app_settings_distrust_system_certs">Non ti fidare dei certificati di sistema</string>
|
||||
<string name="app_settings_distrust_system_certs_on">Le CA di sistema e quelle aggiunte dall\'utente non sono affidabili</string>
|
||||
<string name="app_settings_distrust_system_certs_off">Le CA di sistema e quelle aggiunte dall\'utente sono affidabili (raccomandato)</string>
|
||||
<string name="app_settings_reset_certificates">Reimposta la fiducia in tutti i certificati</string>
|
||||
<string name="app_settings_reset_certificates_summary">Reimposta la fiducia nei certificati aggiunti</string>
|
||||
<string name="app_settings_reset_certificates_success">Sono stati cancellati tutti i certificati aggiunti</string>
|
||||
<string name="app_settings_debug">Debug</string>
|
||||
<string name="app_settings_log_to_external_storage">Log su file esterno</string>
|
||||
<string name="app_settings_log_to_external_storage_on">Log su dispositivo esterno (se disponibile)</string>
|
||||
<string name="app_settings_log_to_external_storage_off">Log su file esterno disabilitato</string>
|
||||
<string name="app_settings_show_debug_info">Mostra informazioni di debug</string>
|
||||
<string name="app_settings_show_debug_info_details">Mostra e condividi i dettagli del programma e della configurazione</string>
|
||||
<!--AccountActivity-->
|
||||
<string name="account_synchronize_now">Sincronizza adesso</string>
|
||||
<string name="account_synchronizing_now">Sincronizzazione in corso</string>
|
||||
<string name="account_settings">Impostazioni account</string>
|
||||
<string name="account_rename">Rinomina account</string>
|
||||
<string name="account_rename_new_name">I dati locali non salvati potrebbero essere rimossi. È necessario effettuare la risincronizzazione dopo la rinomina. Nuovo nome dell\'account:</string>
|
||||
<string name="account_rename_rename">Rinomina</string>
|
||||
<string name="account_delete">Elimina account</string>
|
||||
<string name="account_delete_confirmation_title">Cancellare l\'account?</string>
|
||||
<string name="account_delete_confirmation_text">Tutte le copie locali delle rubriche, dei calendari e degli elenchi attività verranno eliminate.</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>
|
||||
<!--PermissionsActivity-->
|
||||
<string name="permissions_title">Permessi DAVdroid</string>
|
||||
<string name="permissions_calendar">Permessi Calendario</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_title">Aggiungi account</string>
|
||||
<string name="login_type_email">Accedi con indirizzo email</string>
|
||||
<string name="login_email_address">Indirizzo email</string>
|
||||
<string name="login_email_address_error">È necessario un indirizzo email valido</string>
|
||||
<string name="login_password">Password</string>
|
||||
<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_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_login">Login</string>
|
||||
<string name="login_back">Indietro</string>
|
||||
<string name="login_create_account">Crea account</string>
|
||||
<string name="login_account_name">Nome account</string>
|
||||
<string name="login_account_name_info">Inserisci il tuo indirizzo email come nome dell\'account in quanto Android userà il nome dell\'account nel campo ORGANIZER degli eventi creati. Non è possibile avere due account con nome uguale.</string>
|
||||
<string name="login_account_contact_group_method">Metodo del contact group:</string>
|
||||
<string name="login_account_name_required">Richiesto il nome dell\'account</string>
|
||||
<string name="login_account_not_created">L\'account non può essere creato</string>
|
||||
<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>
|
||||
<string name="settings_username">Nome utente</string>
|
||||
<string name="settings_enter_username">Inserisci nome utente:</string>
|
||||
<string name="settings_password">Password</string>
|
||||
<string name="settings_password_summary">Aggiorna la password come sul tuo server.</string>
|
||||
<string name="settings_enter_password">Inserisci la tua password:</string>
|
||||
<string name="settings_sync">Sincronizzazione</string>
|
||||
<string name="settings_sync_interval_contacts">Intervallo sincr. Contatti</string>
|
||||
<string name="settings_sync_summary_manually">Solo manualmente</string>
|
||||
<string name="settings_sync_interval_calendars">Intervallo sincr. Calendari</string>
|
||||
<string name="settings_sync_interval_tasks">Intervallo sincr. Attività</string>
|
||||
<string name="settings_sync_summary_periodically" tools:ignore="PluralsCandidate">Ogni %d minuti e a seguito di ogni cambiamento locale</string>
|
||||
<string name="settings_sync_interval_calendars">Intervallo sincr. calendari</string>
|
||||
<string name="settings_sync_interval_tasks">Intervallo sincr. attività</string>
|
||||
<string-array name="settings_sync_interval_seconds">
|
||||
<item>-1</item>
|
||||
<item>300</item>
|
||||
<item>600</item>
|
||||
<item>900</item>
|
||||
<item>3600</item>
|
||||
<item>7200</item>
|
||||
<item>14400</item>
|
||||
<item>86400</item>
|
||||
</string-array>
|
||||
<string-array name="settings_sync_interval_names">
|
||||
<item>Solo manualmente</item>
|
||||
<item>Ogni 5 minuti</item>
|
||||
<item>Ogni 10 minuti</item>
|
||||
<item>Ogni 15 minuti</item>
|
||||
<item>Ogni ora</item>
|
||||
<item>Ogni 2 ore</item>
|
||||
<item>Ogni 4 ore</item>
|
||||
<item>Una volta al giorno</item>
|
||||
</string-array>
|
||||
<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_carddav">CardDAV</string>
|
||||
<string name="settings_contact_group_method">Organizzazione dei gruppi di contatto</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>I gruppi sono VCards separate</item>
|
||||
<item>I gruppi sono categorie per ogni contatto</item>
|
||||
</string-array>
|
||||
<string name="settings_caldav">CalDAV</string>
|
||||
<string name="settings_sync_time_range_past">Limite di tempo per gli eventi trascorsi</string>
|
||||
<string name="settings_sync_time_range_past_none">Verranno sincronizzati tutti gli eventi</string>
|
||||
<string name="settings_version_update_install_hint">Problemi? Disinstalla e poi re-installa DAVdroid.</string>
|
||||
<plurals name="settings_sync_time_range_past_days">
|
||||
<item quantity="one">Eventi più vecchi di un giorno saranno ignorati</item>
|
||||
<item quantity="other">Eventi più vecchi di %d giorni saranno ignorati</item>
|
||||
</plurals>
|
||||
<string name="settings_sync_time_range_past_message">Eventi più vecchi di questo numero di giorni verranno ignorati(può anche essere 0). Lasciare in bianco per sincronizzare tutti gli eventi.</string>
|
||||
<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>
|
||||
<!--collection management-->
|
||||
<string name="create_addressbook">Crea indirizzario</string>
|
||||
<string name="create_addressbook_display_name_hint">Il mio indirizzario</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>
|
||||
<string name="create_calendar_type">Tipo di raccolta:</string>
|
||||
<string name="create_calendar_type_only_events">Calendario (solo eventi)</string>
|
||||
<string name="create_calendar_type_only_tasks">Elenco attività (solo attività)</string>
|
||||
<string name="create_calendar_type_events_and_tasks">Combinato (eventi e attività)</string>
|
||||
<string name="create_collection_color">Imposta colore della raccolta</string>
|
||||
<string name="create_collection_creating">Crea una raccolta</string>
|
||||
<string name="create_collection_display_name">Mostra il nome (titolo) di questa raccolta:</string>
|
||||
<string name="create_collection_display_name_required">Il titolo è richiesto</string>
|
||||
<string name="create_collection_description">Descrizione (opzionale):</string>
|
||||
<string name="create_collection_home_set">Imposta la home:</string>
|
||||
<string name="create_collection_create">Crea</string>
|
||||
<string name="delete_collection">Elimina raccolta</string>
|
||||
<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>
|
||||
<!--ExceptionInfoFragment-->
|
||||
<string name="exception">Si è verificato un errore.</string>
|
||||
<string name="exception_httpexception">Si è verificato un errore HTTP.</string>
|
||||
<string name="exception_ioexception">Si è verificato un errore di I/O.</string>
|
||||
<string name="exception_show_details">Mostra dettagli</string>
|
||||
<!--sync errors and DebugInfoActivity-->
|
||||
<string name="sync_error_permissions">Permessi DAVdroid</string>
|
||||
<string name="debug_info_title">Informazioni di debug</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>
|
||||
<!--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>
|
||||
</resources>
|
||||
|
||||
@@ -2,18 +2,22 @@
|
||||
<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="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>
|
||||
<!--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_development_version">DAVdroid プレビュー リリース</string>
|
||||
<string name="startup_development_version_message">これは DAVdroid の開発版です。期待した通りに動作しない可能性があることにご注意ください。私たちが DAVdroid を改善するために、建設的なフィードバックをお願いします。</string>
|
||||
<string name="startup_development_version">プレビュー リリース</string>
|
||||
<string name="startup_development_version_message">これは %s の開発版です。期待した通りに動作しない可能性があります。フィードバックを歓迎します。</string>
|
||||
<string name="startup_development_version_give_feedback">フィードバックする</string>
|
||||
<string name="startup_development_version_feedback_url">https://davdroid.bitfire.at/forums/?pk_campaign=davdroid-app</string>
|
||||
<string name="startup_donate">オープンソース情報</string>
|
||||
<string name="startup_donate_message">あなたがオープンソース ソフトウェア (GPLv3) の DAVdroid を使用していただくことに、私たちは満足しています。 DAVdroid の開発はハードワークで、何千もの作業時間がかかりました。寄付をご検討ください。</string>
|
||||
<string name="startup_donate_now">寄付ページを表示</string>
|
||||
@@ -44,9 +48,12 @@
|
||||
<string name="navigation_drawer_external_links">外部リンク</string>
|
||||
<string name="navigation_drawer_website">Web サイト</string>
|
||||
<string name="navigation_drawer_faq">FAQ</string>
|
||||
<string name="navigation_drawer_forums">コミュニティ</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>
|
||||
<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>
|
||||
@@ -56,7 +63,19 @@
|
||||
<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_override_proxy">プロキシ設定を上書き</string>
|
||||
<string name="app_settings_override_proxy_on">カスタムのプロキシ設定を使用する</string>
|
||||
<string name="app_settings_override_proxy_off">システムデフォルトのプロキシ設定を使用する</string>
|
||||
<string name="app_settings_override_proxy_host">HTTP プロキシのホスト名</string>
|
||||
<string name="app_settings_override_proxy_port">HTTP プロキシのポート</string>
|
||||
<string name="app_settings_security">セキュリティ</string>
|
||||
<string name="app_settings_distrust_system_certs">システム証明書の信頼を解除</string>
|
||||
<string name="app_settings_distrust_system_certs_on">システムとユーザーが追加したCAを信頼しない</string>
|
||||
<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_debug">デバッグ</string>
|
||||
<string name="app_settings_log_to_external_storage">外部ファイルにログ出力</string>
|
||||
<string name="app_settings_log_to_external_storage_on">(可能な場合) 外部ストレージにログ出力します</string>
|
||||
@@ -67,6 +86,9 @@
|
||||
<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_rename">名前を変更</string>
|
||||
<string name="account_delete">アカウントを削除</string>
|
||||
<string name="account_delete_confirmation_title">アカウントを削除してもよろしいですか?</string>
|
||||
<string name="account_delete_confirmation_text">アドレス帳、カレンダー、タスクリストのローカルコピーがすべて削除されます。</string>
|
||||
@@ -86,6 +108,7 @@
|
||||
<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>
|
||||
@@ -122,19 +145,8 @@
|
||||
<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_summary_not_available">利用不可</string>
|
||||
<string name="settings_sync_interval_calendars">カレンダー同期間隔</string>
|
||||
<string name="settings_sync_interval_tasks">タスク同期間隔</string>
|
||||
<string-array name="settings_sync_interval_seconds">
|
||||
<item>-1</item>
|
||||
<item>300</item>
|
||||
<item>600</item>
|
||||
<item>900</item>
|
||||
<item>3600</item>
|
||||
<item>7200</item>
|
||||
<item>14400</item>
|
||||
<item>86400</item>
|
||||
</string-array>
|
||||
<string-array name="settings_sync_interval_names">
|
||||
<item>手動のみ</item>
|
||||
<item>5 分ごと</item>
|
||||
@@ -148,10 +160,10 @@
|
||||
<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_ssid">WiFi SSID 制限</string>
|
||||
<string name="settings_sync_wifi_only_ssid_on">%s でのみ同期します</string>
|
||||
<string name="settings_sync_wifi_only_ssid_off">すべての WiFi 接続を使用することができます</string>
|
||||
<string name="settings_sync_wifi_only_ssid_message">このネットワークで同期を制限する WiFi ネットワーク (SSID) の名前を入力してください。すべての WiFi 接続は空白のままにします。</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">利用可能な WiFi ネットワークのカンマ区切りの名前 (SSID) (空白にするとすべて)</string>
|
||||
<string name="settings_carddav">CardDAV</string>
|
||||
<string name="settings_contact_group_method">連絡先グループ方法</string>
|
||||
<string-array name="settings_contact_group_method_values">
|
||||
@@ -162,9 +174,6 @@
|
||||
<item>グループは個別の VCards</item>
|
||||
<item>グループは連絡先カテゴリーごと</item>
|
||||
</string-array>
|
||||
<string name="settings_rfc6868_for_vcards">RFC6868 の VCards を使用する</string>
|
||||
<string name="settings_rfc6868_for_vcards_on">二重引用符を、パラメーター値に使用することができます</string>
|
||||
<string name="settings_rfc6868_for_vcards_off">二重引用符を、パラメーター値に使用することができません</string>
|
||||
<string name="settings_caldav">CalDAV</string>
|
||||
<string name="settings_sync_time_range_past">過去イベントの時間限度</string>
|
||||
<string name="settings_sync_time_range_past_none">すべてのイベントが同期されます</string>
|
||||
@@ -175,9 +184,6 @@
|
||||
<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_version_update">DAVdroid バージョンの更新</string>
|
||||
<string name="settings_version_update_settings_updated">内部設定が更新されました。</string>
|
||||
<string name="settings_version_update_install_hint">問題がありますか? DAVdroid をアンインストールして、再度インストールしてください。</string>
|
||||
<!--collection management-->
|
||||
<string name="create_addressbook">アドレス帳を作成</string>
|
||||
<string name="create_addressbook_display_name_hint">マイ アドレス帳</string>
|
||||
@@ -230,4 +236,6 @@
|
||||
</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>
|
||||
</resources>
|
||||
|
||||
@@ -2,15 +2,22 @@
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<!--common strings-->
|
||||
<string name="app_name">DAVdroid</string>
|
||||
<string name="account_title_address_book">DAVdroid Adresboek</string>
|
||||
<string name="address_books_authority_title">Adresboeken</string>
|
||||
<string name="help">Help</string>
|
||||
<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_development_version">DAVdroid voorlopige versie</string>
|
||||
<string name="startup_development_version_message">Dit is een ontwikkelversie van DAVdroid. Het kan zijn dat dingen niet werken zoals verwacht. Geef ons constructieve feedback om DAVdroid te verbeteren.</string>
|
||||
<string name="startup_development_version">Voorvertoning van uitgave</string>
|
||||
<string name="startup_development_version_message">Dit is ontwikkelversie %s. Onderdelen werken niet zoals verwacht. Je terugkoppeling is welkom.</string>
|
||||
<string name="startup_development_version_give_feedback">Feedback geven</string>
|
||||
<string name="startup_development_version_feedback_url">https://davdroid.bitfire.at/forums/?pk_campaign=davdroid-app</string>
|
||||
<string name="startup_donate">Open-Source informatie</string>
|
||||
<string name="startup_donate_message">We zijn blij dat je DAVdroid gebruikt, wat open-source software (GPLv3) is. Omdat de ontwikkeling van DAVdroid hard werk is en duizenden uren in beslag neemt. overweeg alstublieft een donatie.</string>
|
||||
<string name="startup_donate_now">Toon donatie pagina</string>
|
||||
@@ -41,9 +48,11 @@
|
||||
<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_forums">Community</string>
|
||||
<string name="navigation_drawer_faq_url">https://davdroid.bitfire.at/faq/?pk_campaign=davdroid-app</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>
|
||||
<string name="accounts_global_sync_enable">Inschakelen</string>
|
||||
<!--DavService-->
|
||||
<string name="dav_service_refresh_failed">Service herkenning is mislukt</string>
|
||||
<string name="dav_service_refresh_couldnt_refresh">Kon de collectie lijst niet vernieuwen</string>
|
||||
@@ -53,7 +62,19 @@
|
||||
<string name="app_settings_reset_hints">Hints resetten </string>
|
||||
<string name="app_settings_reset_hints_summary">Hints die al gezien zijn opnieuw weergeven</string>
|
||||
<string name="app_settings_reset_hints_success">Alle hints worden opnieuw weergegeven</string>
|
||||
<string name="app_settings_connection">Verbinding</string>
|
||||
<string name="app_settings_override_proxy">Proxy instellingen overschrijven</string>
|
||||
<string name="app_settings_override_proxy_on">Eigen proxy instellingen gebruiken</string>
|
||||
<string name="app_settings_override_proxy_off">Systeem proxy instellingen gebruiken</string>
|
||||
<string name="app_settings_override_proxy_host">HTTP proxy beheerder naam</string>
|
||||
<string name="app_settings_override_proxy_port">HTTP proxy poort</string>
|
||||
<string name="app_settings_security">Beveiliging</string>
|
||||
<string name="app_settings_distrust_system_certs">Systeem certificaten niet vertrouwen</string>
|
||||
<string name="app_settings_distrust_system_certs_on">Systeem en CAs van toegevoegde gebruiker wordt niet vertrouwd</string>
|
||||
<string name="app_settings_distrust_system_certs_off">Systeem en CAs van toegevoegde gebruiker wordt vertrouwd (aanbevolen)</string>
|
||||
<string name="app_settings_reset_certificates">Resetten (niet) vertrouwde certificaten</string>
|
||||
<string name="app_settings_reset_certificates_summary">Resetten alle bewerkte certificaten</string>
|
||||
<string name="app_settings_reset_certificates_success">Alle bewerkte certificaten zijn vrijgemaakt</string>
|
||||
<string name="app_settings_debug">Debuggen</string>
|
||||
<string name="app_settings_log_to_external_storage">Log naar extern bestand</string>
|
||||
<string name="app_settings_log_to_external_storage_on">Loggen naar externe opslag (wanneer beschikbaar)</string>
|
||||
@@ -64,6 +85,9 @@
|
||||
<string name="account_synchronize_now">Synchroniseer nu</string>
|
||||
<string name="account_synchronizing_now">Aan het synchronizeren...</string>
|
||||
<string name="account_settings">Account instellingen</string>
|
||||
<string name="account_rename">Account hernoemen</string>
|
||||
<string name="account_rename_new_name">Niet opgeslagen lokale informatie mag verloren gaan. Synchronisatie is noodzakelijk na hernoemen. Nieuw account naam:</string>
|
||||
<string name="account_rename_rename">Hernoemen</string>
|
||||
<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>
|
||||
@@ -83,6 +107,7 @@
|
||||
<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>
|
||||
<!--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>
|
||||
@@ -119,7 +144,6 @@
|
||||
<string name="settings_sync_interval_contacts">Contacten verversen</string>
|
||||
<string name="settings_sync_summary_manually">Alleen handmatig</string>
|
||||
<string name="settings_sync_summary_periodically" tools:ignore="PluralsCandidate">Elke %d minuten + meteen na wijziging</string>
|
||||
<string name="settings_sync_summary_not_available">Niet beschikbaar</string>
|
||||
<string name="settings_sync_interval_calendars">Agenda\'s verversen</string>
|
||||
<string name="settings_sync_interval_tasks">Taak sync. tussentijd</string>
|
||||
<string-array name="settings_sync_interval_seconds">
|
||||
@@ -145,10 +169,6 @@
|
||||
<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_ssid">WiFi SSID beperking</string>
|
||||
<string name="settings_sync_wifi_only_ssid_on">Zal alleen synchroniseren over %s</string>
|
||||
<string name="settings_sync_wifi_only_ssid_off">Alle WiFI verbindingen mogen worden gebruikt</string>
|
||||
<string name="settings_sync_wifi_only_ssid_message">Type de naam van het WiFi netwerk (SSID) om synchronisatie tot dit netwerk te beperken. Leeg laten voor sync over alle netwerken.</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">
|
||||
@@ -159,9 +179,6 @@
|
||||
<item>Groepen zijn apparte VCards</item>
|
||||
<item>Groepen zijn per-contact categories</item>
|
||||
</string-array>
|
||||
<string name="settings_rfc6868_for_vcards">Gebruik RFC6868 voor VCards</string>
|
||||
<string name="settings_rfc6868_for_vcards_on">Aanhalingstekens kunnen worden gebruikt voor parameter waardes</string>
|
||||
<string name="settings_rfc6868_for_vcards_off">Aanhalingstekens kunnen niet worden gebruikt voor parameter waardes</string>
|
||||
<string name="settings_caldav">CalDAV</string>
|
||||
<string name="settings_sync_time_range_past">Tijdslimiet verleden afspraken</string>
|
||||
<string name="settings_sync_time_range_past_none">Alle afspraken worden gesynchronizeerd</string>
|
||||
@@ -173,9 +190,6 @@
|
||||
<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_version_update">DAVdroid versie update</string>
|
||||
<string name="settings_version_update_settings_updated">Interne instellingen zijn bijgewerkt.</string>
|
||||
<string name="settings_version_update_install_hint">Problemen? Deïnstalleer DAVdroid, daarna herinstalleren.</string>
|
||||
<!--collection management-->
|
||||
<string name="create_addressbook">Maak adresboek</string>
|
||||
<string name="create_addressbook_display_name_hint">Mijn adresboek</string>
|
||||
@@ -228,4 +242,6 @@
|
||||
</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>
|
||||
</resources>
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<!--common strings-->
|
||||
<string name="app_name">DAVdroid</string>
|
||||
<string name="account_title_address_book">Książka adresowa DAVdroid</string>
|
||||
<string name="address_books_authority_title">Książka adresowa</string>
|
||||
<string name="help">Pomoc</string>
|
||||
<string name="manage_accounts">Zadządzaj kontami</string>
|
||||
<string name="please_wait">Proszę czekać</string>
|
||||
@@ -11,8 +13,8 @@
|
||||
<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_development_version">DAVdroid Preview Release</string>
|
||||
<string name="startup_development_version_message">Jest to wersja rozwojowa DAVdroid. Należy pamiętać, że rzeczy mogą nie działać zgodnie z oczekiwaniami. Prosimy o konstruktywny informacje zwrotne, aby ulepszyć DAVdroid.</string>
|
||||
<string name="startup_development_version">Poprzednie wydanie</string>
|
||||
<string name="startup_development_version_message">To jest wersja rozwojowa 1%s. Nie wszystko może działać zgodnie z oczekiwaniami. Twoja opinia jest mile widziana.</string>
|
||||
<string name="startup_development_version_give_feedback">Przekaż opinię</string>
|
||||
<string name="startup_donate">Informacje Open-Source</string>
|
||||
<string name="startup_donate_message">Jesteśmy szczęśliwi, że używasz DAVdroid, który jest oprogramowaniem open-source (GPLv3). Ponieważ rozwijanie DAVdroid jest ciężką pracą i zajęło nam tysiące godzin pracy, prosimy o rozważenie darowizny.</string>
|
||||
@@ -44,9 +46,10 @@
|
||||
<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_forums">Społeczność</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>
|
||||
<string name="accounts_global_sync_disabled">Automatyczna synchronizacja dla całego systemu jest wyłączona</string>
|
||||
<string name="accounts_global_sync_enable">Włącz</string>
|
||||
<!--DavService-->
|
||||
<string name="dav_service_refresh_failed">Wykrycie serwisu nie powiodło się</string>
|
||||
<string name="dav_service_refresh_couldnt_refresh">Nie można odświeżyć listy kolekcji</string>
|
||||
@@ -56,7 +59,19 @@
|
||||
<string name="app_settings_reset_hints">Zresetuj podpowiedzi</string>
|
||||
<string name="app_settings_reset_hints_summary">Ponownie włącz wskazówki, które zostały usunięte wcześniej</string>
|
||||
<string name="app_settings_reset_hints_success">Wszystkie wskazówki pojawią się ponownie</string>
|
||||
<string name="app_settings_connection">Łączność</string>
|
||||
<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_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>
|
||||
<string name="app_settings_distrust_system_certs_off">CA systemowe i użytkownika zostaną dodane (zalecane)</string>
|
||||
<string name="app_settings_reset_certificates">Zresetuj (nie)zaufane certyfikaty</string>
|
||||
<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_on">Logowanie do zewnętrznej pamięci (jeśli jest dostępna)</string>
|
||||
@@ -67,6 +82,9 @@
|
||||
<string name="account_synchronize_now">Synchronizuj teraz</string>
|
||||
<string name="account_synchronizing_now">Synchronizcja w toku</string>
|
||||
<string name="account_settings">Ustawienia konta</string>
|
||||
<string name="account_rename">Zmień nazwę konta</string>
|
||||
<string name="account_rename_new_name">Niezapisane dane lokalne mogą zostać usunięte. Ponowna synchronizacja jest wymagana po zmianie nazwy. Nowa nazwa konta:</string>
|
||||
<string name="account_rename_rename">Zmień nazwę</string>
|
||||
<string name="account_delete">Usuń konto</string>
|
||||
<string name="account_delete_confirmation_title">Naprawdę chcesz usunąć konto?</string>
|
||||
<string name="account_delete_confirmation_text">Wszystkie lokalne kopie książek adresowych, kalendarzy i list zadań zostaną usunięte.</string>
|
||||
@@ -122,7 +140,6 @@
|
||||
<string name="settings_sync_interval_contacts">Okres synchronizacji kontktó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_summary_not_available">Niedostępne</string>
|
||||
<string name="settings_sync_interval_calendars">Okres synchronizacji kalendarzy</string>
|
||||
<string name="settings_sync_interval_tasks">Okres synchronizacji list zadań</string>
|
||||
<string-array name="settings_sync_interval_seconds">
|
||||
@@ -148,34 +165,25 @@
|
||||
<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_off">Rodzaj połączenia nie jest brany pod uwagę</string>
|
||||
<string name="settings_sync_wifi_only_ssid">Organicznie WiFi SSID</string>
|
||||
<string name="settings_sync_wifi_only_ssid_on">Będzie synchronizować tylko przez %s.</string>
|
||||
<string name="settings_sync_wifi_only_ssid_off">Wszystkie połączenia WiFi mogą zostać wykorzystane</string>
|
||||
<string name="settings_sync_wifi_only_ssid_message">Wprowadź nazwę sieci WiFi (SSID), aby ograniczyć synchronizację do tej sieci lub pozostaw puste dla wszystkich połączeń WiFi.</string>
|
||||
<string name="settings_carddav">CardDAV</string>
|
||||
<string name="settings_contact_group_method">Metoda grupowania kontaktów</string>
|
||||
<string-array name="settings_contact_group_method_values">
|
||||
<item>GROUP_VCARDS</item>
|
||||
<item>CATEGORIES</item>
|
||||
</string-array>
|
||||
<string name="settings_rfc6868_for_vcards">Użyj RFC6868 dla VCards</string>
|
||||
<string name="settings_rfc6868_for_vcards_on">Podwójne apostrofy można stosować w wartościach parametrów</string>
|
||||
<string name="settings_rfc6868_for_vcards_off">Podwójne apostrofy nie można stosować w wartościach parametrów</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>
|
||||
<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>
|
||||
<item quantity="many">Wydarzenia starsze niż %d dni zostaną zignorowane.</item>
|
||||
<item quantity="other">Wydarzenia starsze niż %d dni zostaną zignorowane.</item>
|
||||
</plurals>
|
||||
<string name="settings_sync_time_range_past_message">Wydarzenia, które są starsze niż podana liczba dni zostaną zignorowane (może być 0). Zostaw puste, aby synchronizować wszystkie wydarzenia.</string>
|
||||
<string name="settings_manage_calendar_colors">Zarządzaj kolorami kalendarza</string>
|
||||
<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_version_update">Aktualizacji wersji DAVdroid</string>
|
||||
<string name="settings_version_update_settings_updated">Ustawienia wewnętrzne zostały zaktualizowane.</string>
|
||||
<string name="settings_version_update_install_hint">Problemy? Odinstaluj DAVdroid i zainstaluj ponownie.</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>
|
||||
@@ -228,4 +236,6 @@
|
||||
</string-array>
|
||||
<string name="sync_error_unauthorized">Błędna nazwa użytkownika lub hasło</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>
|
||||
</resources>
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<!--common strings-->
|
||||
<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="help">Ajuda</string>
|
||||
<string name="manage_accounts">Gerenciar contas</string>
|
||||
<string name="please_wait">Por favor, aguarde...</string>
|
||||
@@ -11,8 +13,8 @@
|
||||
<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_battery_optimization_disable">Desligar para o DAVdroid</string>
|
||||
<string name="startup_dont_show_again">Não mostrar novamente</string>
|
||||
<string name="startup_development_version">Versão prévia do DAVdroid</string>
|
||||
<string name="startup_development_version_message">Esta é uma versão de desenvolvimento do DAVdroid. Lembre-se de que as coisas podem não funcionar como esperado. Por favor, envie sugestões e comentários construtivos para melhorar o DAVdroid.</string>
|
||||
<string name="startup_development_version">Preview Release</string>
|
||||
<string name="startup_development_version_message">Esta é uma versão de desenvolvimento do %s. Alguma coisa pode não funcionar como esperado. Suas sugestões e comentários são bem-vindos.</string>
|
||||
<string name="startup_development_version_give_feedback">Enviar sugestões</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>
|
||||
@@ -44,9 +46,10 @@
|
||||
<string name="navigation_drawer_external_links">Links externos</string>
|
||||
<string name="navigation_drawer_website">Site na Web</string>
|
||||
<string name="navigation_drawer_faq">Perguntas fequentes</string>
|
||||
<string name="navigation_drawer_forums">Comunidade</string>
|
||||
<string name="navigation_drawer_donate">Doações</string>
|
||||
<string name="account_list_empty">Bem-vindo ao DAVdroid!\n\nVocê pode adicionar uma conta CalDAV/CardDAV agora.</string>
|
||||
<string name="accounts_global_sync_disabled">A sincronização automática pelo sistema está desativada</string>
|
||||
<string name="accounts_global_sync_enable">Ativar</string>
|
||||
<!--DavService-->
|
||||
<string name="dav_service_refresh_failed">Falha na detecção do serviço</string>
|
||||
<string name="dav_service_refresh_couldnt_refresh">Não foi possível atualizar a lista da coleção</string>
|
||||
@@ -56,9 +59,16 @@
|
||||
<string name="app_settings_reset_hints">Restaurar sugestões</string>
|
||||
<string name="app_settings_reset_hints_summary">Restaura as sugestões que foram descartadas anteriormente</string>
|
||||
<string name="app_settings_reset_hints_success">Todas as sugestões serão exibidas novamente</string>
|
||||
<string name="app_settings_connection">Conexão</string>
|
||||
<string name="app_settings_override_proxy">Substituir as configurações de proxy</string>
|
||||
<string name="app_settings_override_proxy_on">Usar configurações de proxy personalizadas</string>
|
||||
<string name="app_settings_override_proxy_off">Usar configurações de proxy padrão do sistema</string>
|
||||
<string name="app_settings_override_proxy_host">Nome do servidor proxy HTTP</string>
|
||||
<string name="app_settings_override_proxy_port">Porta do proxy HTTP</string>
|
||||
<string name="app_settings_security">Segurança</string>
|
||||
<string name="app_settings_distrust_system_certs">Desconfiar dos certificados de sistema</string>
|
||||
<string name="app_settings_distrust_system_certs_on">ACs adicionadas pelo usuário e pelo sistema não serão confiáveis</string>
|
||||
<string name="app_settings_distrust_system_certs_off">ACs adicionadas pelo usuário e pelo sistema serão confiáveis (recomendado)</string>
|
||||
<string name="app_settings_reset_certificates">Restaurar certificados não-confiáveis</string>
|
||||
<string name="app_settings_reset_certificates_summary">Restaura a confiança de todos os certificados personalizados</string>
|
||||
<string name="app_settings_reset_certificates_success">Todos os certificados personalizados foram restaurados</string>
|
||||
@@ -67,11 +77,14 @@
|
||||
<string name="app_settings_log_to_external_storage_on">Registrando no armazenamento externo (se disponível)</string>
|
||||
<string name="app_settings_log_to_external_storage_off">O registro em arquivo externo está desativado</string>
|
||||
<string name="app_settings_show_debug_info">Mostrar informações de depuração</string>
|
||||
<string name="app_settings_show_debug_info_details">Exibir/compartilhar o software e os detalhes da configuração</string>
|
||||
<string name="app_settings_show_debug_info_details">Exibe/compartilha o software e os detalhes da configuração</string>
|
||||
<!--AccountActivity-->
|
||||
<string name="account_synchronize_now">Sincronizar agora</string>
|
||||
<string name="account_synchronizing_now">Sincronizando</string>
|
||||
<string name="account_settings">Configurações da conta</string>
|
||||
<string name="account_rename">Renomear conta</string>
|
||||
<string name="account_rename_new_name">Dados locais que não foram salvos podem ser descartados. É necessário efetuar uma nova sincronização após renomear. Novo nome da conta:</string>
|
||||
<string name="account_rename_rename">Renomear</string>
|
||||
<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>
|
||||
@@ -127,7 +140,6 @@
|
||||
<string name="settings_sync_interval_contacts">Intervalo sinc. de contatos</string>
|
||||
<string name="settings_sync_summary_manually">Apenas manualmente</string>
|
||||
<string name="settings_sync_summary_periodically" tools:ignore="PluralsCandidate">A cada %d minutos + imediatamente nas alterações locais</string>
|
||||
<string name="settings_sync_summary_not_available">Indisponível</string>
|
||||
<string name="settings_sync_interval_calendars">Intervalo sinc. de calendários</string>
|
||||
<string name="settings_sync_interval_tasks">Intervalo sinc. de tarefas</string>
|
||||
<string-array name="settings_sync_interval_seconds">
|
||||
@@ -153,10 +165,6 @@
|
||||
<string name="settings_sync_wifi_only">Sincronizar apenas por Wi-Fi</string>
|
||||
<string name="settings_sync_wifi_only_on">Sincronização restrita a conexões Wi-Fi</string>
|
||||
<string name="settings_sync_wifi_only_off">O tipo de conexão não é considerado</string>
|
||||
<string name="settings_sync_wifi_only_ssid">Restrição de SSID Wi-Fi</string>
|
||||
<string name="settings_sync_wifi_only_ssid_on">Sincronizará apenas com %s</string>
|
||||
<string name="settings_sync_wifi_only_ssid_off">Qualquer conexão Wi-Fi pode ser utilizada</string>
|
||||
<string name="settings_sync_wifi_only_ssid_message">Informe o nome da rede Wi-Fi (SSID) que será usada para sincronização ou deixe em branco para usar qualquer conexão Wi-Fi.</string>
|
||||
<string name="settings_carddav">CardDAV</string>
|
||||
<string name="settings_contact_group_method">Método do grupo Contato</string>
|
||||
<string-array name="settings_contact_group_method_values">
|
||||
@@ -167,9 +175,6 @@
|
||||
<item>Grupos são VCards separados</item>
|
||||
<item>Grupos são categorias por contato</item>
|
||||
</string-array>
|
||||
<string name="settings_rfc6868_for_vcards">Usar RFC6868 para VCards</string>
|
||||
<string name="settings_rfc6868_for_vcards_on">Aspas podem ser utilizadas nos valores dos parâmetros</string>
|
||||
<string name="settings_rfc6868_for_vcards_off">Aspas não podem ser utilizadas nos valores dos parâmetros</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>
|
||||
@@ -181,9 +186,6 @@
|
||||
<string name="settings_manage_calendar_colors">Gerenciar cores dos calendários</string>
|
||||
<string name="settings_manage_calendar_colors_on">Cores dos calendários definidas pelo DAVdroid</string>
|
||||
<string name="settings_manage_calendar_colors_off">Cores dos calendários não definidas pelo DAVdroid</string>
|
||||
<string name="settings_version_update">Atualização da versão do DAVdroid</string>
|
||||
<string name="settings_version_update_settings_updated">As configurações internas foram atualizadas.</string>
|
||||
<string name="settings_version_update_install_hint">Problemas? Desinstale o DAVdroid e instale novamente.</string>
|
||||
<!--collection management-->
|
||||
<string name="create_addressbook">Criar livro de endereços</string>
|
||||
<string name="create_addressbook_display_name_hint">Meu livro de endereços</string>
|
||||
|
||||
@@ -25,7 +25,6 @@
|
||||
<string name="settings_sync_interval_contacts">Intervalo de sincronização dos contatos</string>
|
||||
<string name="settings_sync_summary_manually">Manualmente apenas</string>
|
||||
<string name="settings_sync_summary_periodically" tools:ignore="PluralsCandidate">A cada %d minutos + imediatamente em alterações locais</string>
|
||||
<string name="settings_sync_summary_not_available">Não avaliado</string>
|
||||
<string name="settings_sync_interval_calendars">Intervalo de sincronização do calendário</string>
|
||||
<string-array name="settings_sync_interval_seconds">
|
||||
<item>-1</item>
|
||||
|
||||
@@ -3,20 +3,128 @@
|
||||
<!--common strings-->
|
||||
<string name="app_name">DAVdroid</string>
|
||||
<string name="help">Помощь</string>
|
||||
<string name="manage_accounts">Управление аккаунтами</string>
|
||||
<string name="please_wait">Пожалуйста подождите...</string>
|
||||
<string name="send">Отправить</string>
|
||||
<!--startup dialogs-->
|
||||
<string name="startup_battery_optimization">Оптимизация батареи</string>
|
||||
<string name="startup_battery_optimization_message">Андроид может отключить/уменьшить синхронизацию DAVdroid через несколько дней. Чтобы этого не произошло, выключите оптимизацию батареи.</string>
|
||||
<string name="startup_battery_optimization_disable">Отключить для DAVdroid</string>
|
||||
<string name="startup_dont_show_again">Больше не показывать</string>
|
||||
<string name="startup_development_version_give_feedback">Оставить отзыв</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_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_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_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>
|
||||
<string name="logging_to_external_storage">Сохранение логов во внешнем хранилище: %s</string>
|
||||
<string name="logging_to_external_storage_warning">Удалять логи насколько возможно быстро!</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_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_faq">ЧАВО</string>
|
||||
<string name="navigation_drawer_donate">Пожертвовать</string>
|
||||
<string name="account_list_empty">Вас приветствует DAVdroid\n\nМожете добавить CalDAV/CardDAV аккаунт сейчас.</string>
|
||||
<!--DavService-->
|
||||
<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_override_proxy">Переопределить настройки прокси-сервера</string>
|
||||
<string name="app_settings_override_proxy_on">Использовать пользовательские настройки прокси-сервера</string>
|
||||
<string name="app_settings_override_proxy_off">Использовать системные настройки прокси-сервера</string>
|
||||
<string name="app_settings_override_proxy_host">Имя хоста HTTP прокси-сервера</string>
|
||||
<string name="app_settings_override_proxy_port">Порт HTTP прокси-сервера</string>
|
||||
<string name="app_settings_security">Безопасность</string>
|
||||
<string name="app_settings_distrust_system_certs">Не доверять системным сертификатам</string>
|
||||
<string name="app_settings_distrust_system_certs_on">Не доверять системным и добавленным пользователем CA</string>
|
||||
<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_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_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_rename">Переименовать</string>
|
||||
<string name="account_delete">Удалить аккаунт</string>
|
||||
<string name="account_delete_confirmation_title">Вы действительно хотите удалить аккаунт?</string>
|
||||
<string name="account_delete_confirmation_text">Все локальные копии контактов, календарей и задач будут удалены.</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 событий с Вашими локальными календарями 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_type_email">Вход по адресу email</string>
|
||||
<string name="login_type_url">Вход через URL и имя пользователя</string>
|
||||
<string name="login_title">Добавить аккаунт</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_url_must_be_http_or_https">URL должен начинаться с http(s)://</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_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_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>
|
||||
<!--AccountSettingsActivity-->
|
||||
<string name="settings_authentication">Идентификация</string>
|
||||
<string name="settings_title">Настройки: %s</string>
|
||||
<string name="settings_authentication">Аутентификация</string>
|
||||
<string name="settings_username">Имя</string>
|
||||
<string name="settings_enter_username">Введите имя пользователя:</string>
|
||||
<string name="settings_password">Пароль</string>
|
||||
@@ -26,9 +134,8 @@
|
||||
<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_summary_not_available">Недоступно</string>
|
||||
<string name="settings_sync_interval_calendars">Период синхронизации календарей</string>
|
||||
<string name="settings_sync_interval_tasks">Период синхронизации задач</string>
|
||||
<string name="settings_sync_interval_calendars">Интервал синхронизации календарей</string>
|
||||
<string name="settings_sync_interval_tasks">Интервал синхронизации задач</string>
|
||||
<string-array name="settings_sync_interval_seconds">
|
||||
<item>-1</item>
|
||||
<item>300</item>
|
||||
@@ -49,16 +156,80 @@
|
||||
<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_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>
|
||||
<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>
|
||||
<!--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_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_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_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_deleting_collection">Удаление коллекции</string>
|
||||
<!--ExceptionInfoFragment-->
|
||||
<string name="exception">Произошла ошибка.</string>
|
||||
<string name="exception_httpexception">Произошла ошибка HTTP</string>
|
||||
<string name="exception_ioexception">Произошла ошибка ввода/вывода.</string>
|
||||
<string name="exception_show_details">Подробнее</string>
|
||||
<!--sync errors and DebugInfoActivity-->
|
||||
<string name="debug_info_title">Отладочная информация</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_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 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>
|
||||
</resources>
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<!--common strings-->
|
||||
<string name="app_name">ДАВдроид</string>
|
||||
<string name="account_title_address_book">ДАВдроид адресар</string>
|
||||
<string name="address_books_authority_title">Адресари</string>
|
||||
<string name="help">Помоћ</string>
|
||||
<string name="manage_accounts">Управљај налозима</string>
|
||||
<string name="please_wait">Сачекајте…</string>
|
||||
@@ -11,8 +13,6 @@
|
||||
<string name="startup_battery_optimization_message">Андроид може да искључи/умањи синхронизацију ДАВдроида након неколико дана. Да бисте спречили ово, искључите оптимизацију батерије.</string>
|
||||
<string name="startup_battery_optimization_disable">Искључи за ДАВдроид</string>
|
||||
<string name="startup_dont_show_again">Не приказуј поново</string>
|
||||
<string name="startup_development_version">ДАВдроид прелиминарно издање</string>
|
||||
<string name="startup_development_version_message">Ово је развојно издање ДАВдроида. Имајте на уму да можда неће радити очекивано. Замољавамо вас за конструктивне повратне информације како бисмо га побољшали.</string>
|
||||
<string name="startup_development_version_give_feedback">Повратне информације</string>
|
||||
<string name="startup_donate">Подаци о отвореном кôду</string>
|
||||
<string name="startup_donate_message">Драго нам је да користите ДАВдроид, софтвер отвореног кôда (ГПЛв3). Развој ДАВдроида није баш лак посао и захтева хиљаде радних сати, стога вас молимо да размотрите донацију.</string>
|
||||
@@ -44,9 +44,10 @@
|
||||
<string name="navigation_drawer_external_links">Вањске везе</string>
|
||||
<string name="navigation_drawer_website">Веб-сајт</string>
|
||||
<string name="navigation_drawer_faq">ЧПП</string>
|
||||
<string name="navigation_drawer_forums">Заједница</string>
|
||||
<string name="navigation_drawer_donate">Донирај</string>
|
||||
<string name="account_list_empty">Добро дошли у ДАВдроид!\n\nМожете сада да додате КалДАВ/КардДАВ налог.</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>
|
||||
@@ -56,7 +57,19 @@
|
||||
<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_override_proxy">Надјачај поставке проксија</string>
|
||||
<string name="app_settings_override_proxy_on">Користи прилагођене поставке проксија</string>
|
||||
<string name="app_settings_override_proxy_off">Користи системски подразумеване поставке проксија</string>
|
||||
<string name="app_settings_override_proxy_host">ХТТП прокси домаћин</string>
|
||||
<string name="app_settings_override_proxy_port">ХТТП прокси порт</string>
|
||||
<string name="app_settings_security">Безбедност</string>
|
||||
<string name="app_settings_distrust_system_certs">Посумњај у системске сертификате</string>
|
||||
<string name="app_settings_distrust_system_certs_on">Системски и кориснички додати сертификати неће бити поуздани</string>
|
||||
<string name="app_settings_distrust_system_certs_off">Системски и кориснички додати сертификати ће бити поуздани (препоручљиво)</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_debug">Тражење грешака</string>
|
||||
<string name="app_settings_log_to_external_storage">Уписуј у спољашњи фајл</string>
|
||||
<string name="app_settings_log_to_external_storage_on">Уписивање евиденције у спољашње складиште (ако је доступно)</string>
|
||||
@@ -67,6 +80,9 @@
|
||||
<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_rename">Преименуј</string>
|
||||
<string name="account_delete">Обриши налог</string>
|
||||
<string name="account_delete_confirmation_title">Заиста обрисати налог?</string>
|
||||
<string name="account_delete_confirmation_text">Све локалне копије адресара, календара и листи задатака ће бити обрисане.</string>
|
||||
@@ -122,7 +138,6 @@
|
||||
<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_summary_not_available">Није доступно</string>
|
||||
<string name="settings_sync_interval_calendars">Интервал синх. календара</string>
|
||||
<string name="settings_sync_interval_tasks">Интервал синх. задатака</string>
|
||||
<string-array name="settings_sync_interval_seconds">
|
||||
@@ -148,10 +163,6 @@
|
||||
<string name="settings_sync_wifi_only">Само преко бежичног</string>
|
||||
<string name="settings_sync_wifi_only_on">Синхронизовање само преко бежичних мрежа</string>
|
||||
<string name="settings_sync_wifi_only_off">Тип везе није узет у обзир</string>
|
||||
<string name="settings_sync_wifi_only_ssid">Ограничења ССИД-а бежичних</string>
|
||||
<string name="settings_sync_wifi_only_ssid_on">Синхронизовање само преко %s</string>
|
||||
<string name="settings_sync_wifi_only_ssid_off">Коришћење свих бежичних мрежа</string>
|
||||
<string name="settings_sync_wifi_only_ssid_message">Унесите назив бежичне мреже (њен ССИД) да бисте ограничили синхронизацију само на ту мрежу, или оставите празно за синхронизовање преко било које бежичне мреже.</string>
|
||||
<string name="settings_carddav">КардДАВ</string>
|
||||
<string name="settings_contact_group_method">Режим група контаката</string>
|
||||
<string-array name="settings_contact_group_method_values">
|
||||
@@ -162,9 +173,6 @@
|
||||
<item>Групе су одвојене В-карте</item>
|
||||
<item>Групе су категорије по контакту</item>
|
||||
</string-array>
|
||||
<string name="settings_rfc6868_for_vcards">Користи РФЦ6868 за В-карте</string>
|
||||
<string name="settings_rfc6868_for_vcards_on">Двоструки наводници могу да се користе у вредностима параметара</string>
|
||||
<string name="settings_rfc6868_for_vcards_off">Двоструки наводници не могу да се користе у вредностима параметара</string>
|
||||
<string name="settings_caldav">КалДАВ</string>
|
||||
<string name="settings_sync_time_range_past">Ограничење догађаја у прошлости</string>
|
||||
<string name="settings_sync_time_range_past_none">Сви догађаји се синхронизују</string>
|
||||
@@ -177,9 +185,6 @@
|
||||
<string name="settings_manage_calendar_colors">Управљај бојама календара</string>
|
||||
<string name="settings_manage_calendar_colors_on">Бојама календара управља ДАВдроид</string>
|
||||
<string name="settings_manage_calendar_colors_off">Боје календара није поставио ДАВдроид</string>
|
||||
<string name="settings_version_update">Надоградња ДАВдроид издања</string>
|
||||
<string name="settings_version_update_settings_updated">Унутрашње поставке су ажуриране.</string>
|
||||
<string name="settings_version_update_install_hint">Проблеми? Уклоните ДАВдроид па га поново инсталирајте.</string>
|
||||
<!--collection management-->
|
||||
<string name="create_addressbook">Направи адресар</string>
|
||||
<string name="create_addressbook_display_name_hint">Мој адресар</string>
|
||||
@@ -232,4 +237,6 @@
|
||||
</string-array>
|
||||
<string name="sync_error_unauthorized">Корисничко име или лозинка погрешни</string>
|
||||
<!--cert4android-->
|
||||
<string name="certificate_notification_connection_security">ДАВдроид: Безбедност везе</string>
|
||||
<string name="trust_certificate_unknown_certificate_found">ДАВдроид је наишао на непознат сертификат. Желите ли да се поуздате у њега?</string>
|
||||
</resources>
|
||||
|
||||
@@ -8,8 +8,6 @@
|
||||
<string name="send">Gönder</string>
|
||||
<!--startup dialogs-->
|
||||
<string name="startup_dont_show_again">Bir daha gösterme</string>
|
||||
<string name="startup_development_version">DAVDroid Önizlenim Dağıtımı</string>
|
||||
<string name="startup_development_version_message">Bu DAVdroid\'in bir geliştirme sürümüdür. Bazı şeyler beklendiği gibi çalışmayabilir. DAVdroid\'i iyileştirmek için bize lütfen yapıcı eleştirilerini ilet.</string>
|
||||
<string name="startup_development_version_give_feedback">Geribildirim ver</string>
|
||||
<string name="startup_donate">Açık-Kaynak Bilgisi</string>
|
||||
<string name="startup_donate_message">Açık kaynaklı yazılım (GPLv3) olan DAVdroid\'i kullandığına çok mutluyuz. DAVdroid\'i geliştirmek zor bir iş ve üzerinde binlerce saat çalıştığımızdan, lütfen bir bağışta bulunmayı düşün.</string>
|
||||
@@ -41,7 +39,6 @@
|
||||
<string name="navigation_drawer_external_links">Harici bağlantılar</string>
|
||||
<string name="navigation_drawer_website">Web sitesi</string>
|
||||
<string name="navigation_drawer_faq">SSS</string>
|
||||
<string name="navigation_drawer_forums">Camia</string>
|
||||
<string name="navigation_drawer_donate">Bağış yap</string>
|
||||
<string name="account_list_empty">DAVdroid\'e hoşgeldin!\n\nŞimdi bir CalDAV/CardDAV hesabı ekleyebilirsin.</string>
|
||||
<!--DavService-->
|
||||
@@ -119,7 +116,6 @@
|
||||
<string name="settings_sync_interval_contacts">Kişiler senk. aralığı</string>
|
||||
<string name="settings_sync_summary_manually">Sadece elle</string>
|
||||
<string name="settings_sync_summary_periodically" tools:ignore="PluralsCandidate">Her %d dakika + yerel değişikliklerde hemen</string>
|
||||
<string name="settings_sync_summary_not_available">Mevcut değil</string>
|
||||
<string name="settings_sync_interval_calendars">Takvimler senk. aralığı</string>
|
||||
<string name="settings_sync_interval_tasks">İşler senk. aralığı</string>
|
||||
<string-array name="settings_sync_interval_seconds">
|
||||
@@ -145,10 +141,6 @@
|
||||
<string name="settings_sync_wifi_only">Sadece WiFi üzerinden senkronize et</string>
|
||||
<string name="settings_sync_wifi_only_on">Senkronizasyon WiFi bağlantıları ile kısıtlıdır</string>
|
||||
<string name="settings_sync_wifi_only_off">Bağlantı tipi göz önünde bulundurulmaz</string>
|
||||
<string name="settings_sync_wifi_only_ssid">WiFi SSID kısıtlaması</string>
|
||||
<string name="settings_sync_wifi_only_ssid_on">Sadece %s üzerinden senkronize olur</string>
|
||||
<string name="settings_sync_wifi_only_ssid_off">Tüm WiFi bağlantıları kullanılabilir</string>
|
||||
<string name="settings_sync_wifi_only_ssid_message">Senkronizasyonu sadece bir WiFi ağına kısıtlamak için bu ağın adını (SSID) gir, veya tüm WiFi bağlantıları için boş bırak.</string>
|
||||
<string name="settings_caldav">CalDAV</string>
|
||||
<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>
|
||||
@@ -159,9 +151,6 @@
|
||||
<string name="settings_manage_calendar_colors">Takvim renklerini yönet</string>
|
||||
<string name="settings_manage_calendar_colors_on">Takvim renkleri DAVdroid tarafından yönetilmekte</string>
|
||||
<string name="settings_manage_calendar_colors_off">Takvim renkleri DAVdroid tarafından ayarlanmadı</string>
|
||||
<string name="settings_version_update">DAVdroid sürüm güncellemesi</string>
|
||||
<string name="settings_version_update_settings_updated">Dahili ayarlar güncellendi.</string>
|
||||
<string name="settings_version_update_install_hint">Sorunlar mı var? DAVdroid\'i kaldırıp, yeniden kurun.</string>
|
||||
<!--collection management-->
|
||||
<string name="create_addressbook">Rehber yarat</string>
|
||||
<string name="create_addressbook_display_name_hint">Benim Rehberim</string>
|
||||
|
||||
@@ -2,19 +2,134 @@
|
||||
<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="help">Допомога</string>
|
||||
<string name="manage_accounts">Керування обліковими записами</string>
|
||||
<string name="please_wait">Будь ласка, зачекайте...</string>
|
||||
<string name="send">Відправити</string>
|
||||
<!--startup dialogs-->
|
||||
<string name="startup_battery_optimization">Оптимізація енергоспоживання</string>
|
||||
<string name="startup_battery_optimization_message">Android може вимкнути, чи призупинити синхронізацію DAVdroid через деякий час. Аби запобігти цьому, вимкніть оптимізацію енергоспоживання для додатку.</string>
|
||||
<string name="startup_battery_optimization_disable">Вимкнути для DAVdroid</string>
|
||||
<string name="startup_dont_show_again">Не показувати знову</string>
|
||||
<string name="startup_development_version">Попередній випуск</string>
|
||||
<string name="startup_development_version_message">Це версія 1%s для розробників. Майте на увазі, дещо може працювати не так, як заплановано. Будь ласка, залиште конструктивний відгук.</string>
|
||||
<string name="startup_development_version_give_feedback">Залишити відгук</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_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_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_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>
|
||||
<string name="logging_to_external_storage">Звітування до зовнішнього сховища: %s</string>
|
||||
<string name="logging_to_external_storage_warning">Вилучіть звіт якомога швидше!</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_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_faq">Питання/Відповіді</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>
|
||||
<string name="accounts_global_sync_enable">Увімкнути</string>
|
||||
<!--DavService-->
|
||||
<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_override_proxy">Перевизначити налаштування проксі</string>
|
||||
<string name="app_settings_override_proxy_on">Власні налаштування проксі</string>
|
||||
<string name="app_settings_override_proxy_off">Типові системні налаштування проксі</string>
|
||||
<string name="app_settings_override_proxy_host">Ім\'я хосту HTTP проксі</string>
|
||||
<string name="app_settings_override_proxy_port">Порт HTTP проксі</string>
|
||||
<string name="app_settings_security">Безпека</string>
|
||||
<string name="app_settings_distrust_system_certs">Не довіряти системним сертифікатам</string>
|
||||
<string name="app_settings_distrust_system_certs_on">Не довіряти системним та доданим користувачем сертифікатам</string>
|
||||
<string name="app_settings_distrust_system_certs_off">Довіряти системним та доданим користувачем сертифікатам (рекомендується)</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_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_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_rename">Перейменувати</string>
|
||||
<string name="account_delete">Видалити запис</string>
|
||||
<string name="account_delete_confirmation_title">Дійсно видалити обліковий запис?</string>
|
||||
<string name="account_delete_confirmation_text">Всі локальні копії адресних книг, календарів та завдань будуть вилучені.</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>
|
||||
<!--AddAccountActivity-->
|
||||
<string name="login_title">Додати запис</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_url_must_be_http_or_https">URL адреса повинна починатися з http(s)://</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_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_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>
|
||||
<!--AccountSettingsActivity-->
|
||||
<string name="settings_title">Налаштування: %s</string>
|
||||
<string name="settings_authentication">Автентифікація</string>
|
||||
<string name="settings_username">Ім\'я користувача</string>
|
||||
<string name="settings_enter_username">Введіть ім\'я користувача:</string>
|
||||
@@ -25,7 +140,6 @@
|
||||
<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_summary_not_available">Не доступно</string>
|
||||
<string name="settings_sync_interval_calendars">Інтервал синхронізації календарів</string>
|
||||
<string name="settings_sync_interval_tasks">Інтервал синхронізації завдань</string>
|
||||
<string-array name="settings_sync_interval_seconds">
|
||||
@@ -48,8 +162,79 @@
|
||||
<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_carddav">CardDAV</string>
|
||||
<string name="settings_contact_group_method">Метод групування контактів</string>
|
||||
<string-array name="settings_contact_group_method_entries">
|
||||
<item>Групи як окремі VCard</item>
|
||||
<item>Групи як категорії в середині контактів</item>
|
||||
</string-array>
|
||||
<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="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>
|
||||
<!--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_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_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_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_deleting_collection">Видалення колекції</string>
|
||||
<!--ExceptionInfoFragment-->
|
||||
<string name="exception">Трапилась помилка.</string>
|
||||
<string name="exception_httpexception">Трапилась помилка HTTP.</string>
|
||||
<string name="exception_ioexception">Трапилась помилка I/O.</string>
|
||||
<string name="exception_show_details">Показати подробиці</string>
|
||||
<!--sync errors and DebugInfoActivity-->
|
||||
<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>
|
||||
</resources>
|
||||
|
||||
@@ -2,24 +2,25 @@
|
||||
<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="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>
|
||||
<!--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_battery_optimization_message">系统可能会在几天后减少或停用 DAVdroid 同步。为了避免这一情况,请禁用对 DAVdroid 的电池优化。</string>
|
||||
<string name="startup_battery_optimization_disable">禁用电池优化</string>
|
||||
<string name="startup_dont_show_again">不再显示</string>
|
||||
<string name="startup_development_version">DAVdroid 预览版</string>
|
||||
<string name="startup_development_version_message">这是 DAVdroid 的开发版本,部分功能可能无法正常工作。请您提出建设性反馈,帮助我们完善 DAVdroid。</string>
|
||||
<string name="startup_development_version_give_feedback">反馈</string>
|
||||
<string name="startup_donate">开源信息</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 商店 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_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>
|
||||
@@ -41,12 +42,14 @@
|
||||
<string name="navigation_drawer_about">关于 / 许可</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_external_links">外部链接</string>
|
||||
<string name="navigation_drawer_website">应用网站</string>
|
||||
<string name="navigation_drawer_faq">常见问题</string>
|
||||
<string name="navigation_drawer_forums">社区</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_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>
|
||||
@@ -56,7 +59,19 @@
|
||||
<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_override_proxy">覆盖代理设置</string>
|
||||
<string name="app_settings_override_proxy_on">使用自定义代理设置</string>
|
||||
<string name="app_settings_override_proxy_off">使用系统默认代理设置</string>
|
||||
<string name="app_settings_override_proxy_host">HTTP 代理主机名</string>
|
||||
<string name="app_settings_override_proxy_port">HTTP 代理端口</string>
|
||||
<string name="app_settings_security">安全</string>
|
||||
<string name="app_settings_distrust_system_certs">不信任系统证书</string>
|
||||
<string name="app_settings_distrust_system_certs_on">系统和用户增加的发布者不会被信任</string>
|
||||
<string name="app_settings_distrust_system_certs_off">系统和用户增加的发布者会被信任(推荐)</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_debug">调试</string>
|
||||
<string name="app_settings_log_to_external_storage">外部文件日志</string>
|
||||
<string name="app_settings_log_to_external_storage_on">记录日志到外部存储(如果可用)</string>
|
||||
@@ -67,6 +82,9 @@
|
||||
<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_rename">重命名</string>
|
||||
<string name="account_delete">删除账户</string>
|
||||
<string name="account_delete_confirmation_title">真的要删除账户吗?</string>
|
||||
<string name="account_delete_confirmation_text">所有通讯录、日历和任务列表的本机存储将被删除。</string>
|
||||
@@ -86,6 +104,7 @@
|
||||
<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>
|
||||
@@ -122,7 +141,6 @@
|
||||
<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_summary_not_available">不可用</string>
|
||||
<string name="settings_sync_interval_calendars">日历自动同步间隔</string>
|
||||
<string name="settings_sync_interval_tasks">任务自动同步间隔</string>
|
||||
<string-array name="settings_sync_interval_seconds">
|
||||
@@ -148,10 +166,6 @@
|
||||
<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_ssid">WiFi SSID 限制</string>
|
||||
<string name="settings_sync_wifi_only_ssid_on">同步只在 %s 网络下进行</string>
|
||||
<string name="settings_sync_wifi_only_ssid_off">任何 WiFi 网络下均会同步</string>
|
||||
<string name="settings_sync_wifi_only_ssid_message">输入 WiFi 网络的名称 (SSID) ,即可限制同步只在此网络下进行。留空则不限制。</string>
|
||||
<string name="settings_carddav">CardDAV</string>
|
||||
<string name="settings_contact_group_method">联系人分组方式</string>
|
||||
<string-array name="settings_contact_group_method_values">
|
||||
@@ -162,9 +176,6 @@
|
||||
<item>分为不同的 VCards</item>
|
||||
<item>每个联系人的分类</item>
|
||||
</string-array>
|
||||
<string name="settings_rfc6868_for_vcards">为 VCards 启用 RFC6868</string>
|
||||
<string name="settings_rfc6868_for_vcards_on">属性值中可使用双引号</string>
|
||||
<string name="settings_rfc6868_for_vcards_off">属性值中不可使用双引号</string>
|
||||
<string name="settings_caldav">CalDAV</string>
|
||||
<string name="settings_sync_time_range_past">旧日程时间限制</string>
|
||||
<string name="settings_sync_time_range_past_none">同步所有日程</string>
|
||||
@@ -175,9 +186,6 @@
|
||||
<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_version_update">DAVdroid 版本升级</string>
|
||||
<string name="settings_version_update_settings_updated">应用设置已更新。</string>
|
||||
<string name="settings_version_update_install_hint">出现问题了?请卸载 DAVdroid,再重新安装。</string>
|
||||
<!--collection management-->
|
||||
<string name="create_addressbook">创建通讯录</string>
|
||||
<string name="create_addressbook_display_name_hint">我的通讯录</string>
|
||||
@@ -230,4 +238,6 @@
|
||||
</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>
|
||||
</resources>
|
||||
|
||||
7
app/src/gplay/AndroidManifest.xml
Normal file
7
app/src/gplay/AndroidManifest.xml
Normal file
@@ -0,0 +1,7 @@
|
||||
<?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>
|
||||
@@ -16,6 +16,7 @@
|
||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.READ_SYNC_SETTINGS"/>
|
||||
<uses-permission android:name="android.permission.READ_SYNC_STATS"/>
|
||||
<uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS"/>
|
||||
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/>
|
||||
|
||||
@@ -52,11 +53,11 @@
|
||||
tools:ignore="UnusedAttribute">
|
||||
|
||||
<receiver
|
||||
android:name=".App$ReinitLoggingReceiver"
|
||||
android:name=".model.Settings$ReinitSettingsReceiver"
|
||||
android:exported="false"
|
||||
android:process=":sync">
|
||||
<intent-filter>
|
||||
<action android:name="at.bitfire.davdroid.REINIT_LOGGER"/>
|
||||
<action android:name="at.bitfire.davdroid.REINIT_SETTINGS"/>
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
<receiver
|
||||
@@ -68,6 +69,7 @@
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<!-- account type "DAVdroid" -->
|
||||
<service
|
||||
android:name=".syncadapter.AccountAuthenticatorService"
|
||||
android:exported="false">
|
||||
@@ -79,6 +81,65 @@
|
||||
android:name="android.accounts.AccountAuthenticator"
|
||||
android:resource="@xml/account_authenticator"/>
|
||||
</service>
|
||||
<service
|
||||
android:name=".syncadapter.CalendarsSyncAdapterService"
|
||||
android:exported="true"
|
||||
android:process=":sync"
|
||||
tools:ignore="ExportedService">
|
||||
<intent-filter>
|
||||
<action android:name="android.content.SyncAdapter"/>
|
||||
</intent-filter>
|
||||
|
||||
<meta-data
|
||||
android:name="android.content.SyncAdapter"
|
||||
android:resource="@xml/sync_calendars"/>
|
||||
</service>
|
||||
|
||||
<service
|
||||
android:name=".syncadapter.TasksSyncAdapterService"
|
||||
android:exported="true"
|
||||
android:process=":sync"
|
||||
tools:ignore="ExportedService">
|
||||
<intent-filter>
|
||||
<action android:name="android.content.SyncAdapter"/>
|
||||
</intent-filter>
|
||||
|
||||
<meta-data
|
||||
android:name="android.content.SyncAdapter"
|
||||
android:resource="@xml/sync_tasks"/>
|
||||
</service>
|
||||
|
||||
<!-- account type "DAVdroid Address book" -->
|
||||
<service
|
||||
android:name=".syncadapter.NullAuthenticatorService"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="android.accounts.AccountAuthenticator"/>
|
||||
</intent-filter>
|
||||
|
||||
<meta-data
|
||||
android:name="android.accounts.AccountAuthenticator"
|
||||
android:resource="@xml/account_authenticator_address_book"/>
|
||||
</service>
|
||||
<provider
|
||||
android:authorities="@string/address_books_authority"
|
||||
android:exported="false"
|
||||
android:label="@string/address_books_authority_title"
|
||||
android:name=".syncadapter.AddressBookProvider"
|
||||
android:multiprocess="false"/>
|
||||
<service
|
||||
android:name=".syncadapter.AddressBooksSyncAdapterService"
|
||||
android:exported="true"
|
||||
android:process=":sync"
|
||||
tools:ignore="ExportedService">
|
||||
<intent-filter>
|
||||
<action android:name="android.content.SyncAdapter"/>
|
||||
</intent-filter>
|
||||
|
||||
<meta-data
|
||||
android:name="android.content.SyncAdapter"
|
||||
android:resource="@xml/sync_address_books"/>
|
||||
</service>
|
||||
<service
|
||||
android:name=".syncadapter.ContactsSyncAdapterService"
|
||||
android:exported="true"
|
||||
@@ -95,46 +156,26 @@
|
||||
android:name="android.provider.CONTACTS_STRUCTURE"
|
||||
android:resource="@xml/contacts"/>
|
||||
</service>
|
||||
<service
|
||||
android:name=".syncadapter.CalendarsSyncAdapterService"
|
||||
android:exported="true"
|
||||
android:process=":sync"
|
||||
tools:ignore="ExportedService">
|
||||
<intent-filter>
|
||||
<action android:name="android.content.SyncAdapter"/>
|
||||
</intent-filter>
|
||||
|
||||
<meta-data
|
||||
android:name="android.content.SyncAdapter"
|
||||
android:resource="@xml/sync_calendars"/>
|
||||
</service>
|
||||
<service
|
||||
android:name=".syncadapter.TasksSyncAdapterService"
|
||||
android:exported="true"
|
||||
android:process=":sync"
|
||||
tools:ignore="ExportedService">
|
||||
<intent-filter>
|
||||
<action android:name="android.content.SyncAdapter"/>
|
||||
</intent-filter>
|
||||
|
||||
<meta-data
|
||||
android:name="android.content.SyncAdapter"
|
||||
android:resource="@xml/sync_tasks"/>
|
||||
</service>
|
||||
|
||||
<service
|
||||
android:name=".DavService"
|
||||
android:enabled="true">
|
||||
</service>
|
||||
<receiver
|
||||
android:name=".AccountsChangedReceiver"
|
||||
android:enabled="true"
|
||||
android:exported="true">
|
||||
|
||||
<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"
|
||||
@@ -184,6 +225,16 @@
|
||||
android:exported="true"
|
||||
android:label="@string/debug_info_title">
|
||||
</activity>
|
||||
<provider
|
||||
android:name="android.support.v4.content.FileProvider"
|
||||
android:authorities="@string/authority_log_provider"
|
||||
android:grantUriPermissions="true"
|
||||
android:exported="false">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/log_paths" />
|
||||
</provider>
|
||||
|
||||
|
||||
</application>
|
||||
|
||||
|
||||
@@ -1,461 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2013 – 2015 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.Account;
|
||||
import android.accounts.AccountManager;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.annotation.TargetApi;
|
||||
import android.app.Notification;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.ContentProviderClient;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.PeriodicSync;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.provider.CalendarContract;
|
||||
import android.provider.ContactsContract;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.app.NotificationManagerCompat;
|
||||
import android.support.v7.app.NotificationCompat;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import at.bitfire.davdroid.model.ServiceDB;
|
||||
import at.bitfire.davdroid.model.ServiceDB.Collections;
|
||||
import at.bitfire.davdroid.model.ServiceDB.HomeSets;
|
||||
import at.bitfire.davdroid.model.ServiceDB.Services;
|
||||
import at.bitfire.davdroid.resource.LocalAddressBook;
|
||||
import at.bitfire.davdroid.resource.LocalCalendar;
|
||||
import at.bitfire.davdroid.resource.LocalTaskList;
|
||||
import at.bitfire.ical4android.CalendarStorageException;
|
||||
import at.bitfire.ical4android.TaskProvider;
|
||||
import at.bitfire.vcard4android.ContactsStorageException;
|
||||
import at.bitfire.vcard4android.GroupMethod;
|
||||
import lombok.Cleanup;
|
||||
import okhttp3.HttpUrl;
|
||||
|
||||
public class AccountSettings {
|
||||
private final static int CURRENT_VERSION = 4;
|
||||
private final static String
|
||||
KEY_SETTINGS_VERSION = "version",
|
||||
|
||||
KEY_USERNAME = "user_name",
|
||||
|
||||
KEY_WIFI_ONLY = "wifi_only", // sync on WiFi only (default: false)
|
||||
KEY_WIFI_ONLY_SSID = "wifi_only_ssid"; // restrict sync to specific WiFi SSID
|
||||
|
||||
/** Whether to use RFC 6868 for VCards
|
||||
* value = null (not existing) use RFC6868-style encoding (default value)
|
||||
* "0" don't use RFC 6868-style encoding
|
||||
*/
|
||||
private final static String KEY_VCARD_RFC6868 = "vcard_rfc6868";
|
||||
|
||||
/** 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
|
||||
*/
|
||||
private final static String KEY_TIME_RANGE_PAST_DAYS = "time_range_past_days";
|
||||
private final static int 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 */
|
||||
private final static String KEY_MANAGE_CALENDAR_COLORS = "manage_calendar_colors";
|
||||
|
||||
/** Contact group method:
|
||||
value = null (not existing) groups as separate VCards (default)
|
||||
"CATEGORIES" groups are per-contact CATEGORIES
|
||||
*/
|
||||
private final static String KEY_CONTACT_GROUP_METHOD = "contact_group_method";
|
||||
|
||||
public final static long SYNC_INTERVAL_MANUALLY = -1;
|
||||
|
||||
final Context context;
|
||||
final AccountManager accountManager;
|
||||
final Account account;
|
||||
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
||||
public AccountSettings(@NonNull Context context, @NonNull Account account) throws InvalidAccountException {
|
||||
this.context = context;
|
||||
this.account = account;
|
||||
|
||||
accountManager = AccountManager.get(context);
|
||||
|
||||
synchronized(AccountSettings.class) {
|
||||
String versionStr = accountManager.getUserData(account, KEY_SETTINGS_VERSION);
|
||||
if (versionStr == null)
|
||||
throw new InvalidAccountException(account);
|
||||
|
||||
int version = 0;
|
||||
try {
|
||||
version = Integer.parseInt(versionStr);
|
||||
} catch (NumberFormatException ignored) {
|
||||
}
|
||||
App.log.info("Account " + account.name + " has version " + version + ", current version: " + CURRENT_VERSION);
|
||||
|
||||
if (version < CURRENT_VERSION) {
|
||||
Notification notify = new NotificationCompat.Builder(context)
|
||||
.setSmallIcon(R.drawable.ic_error_light)
|
||||
.setLargeIcon(App.getLauncherBitmap(context))
|
||||
.setContentTitle(context.getString(R.string.settings_version_update))
|
||||
.setContentText(context.getString(R.string.settings_version_update_settings_updated))
|
||||
.setSubText(context.getString(R.string.settings_version_update_install_hint))
|
||||
.setStyle(new NotificationCompat.BigTextStyle()
|
||||
.bigText(context.getString(R.string.settings_version_update_settings_updated)))
|
||||
.setCategory(NotificationCompat.CATEGORY_SYSTEM)
|
||||
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||
.setContentIntent(PendingIntent.getActivity(context, 0,
|
||||
new Intent(Intent.ACTION_VIEW, Constants.webUri.buildUpon().appendEncodedPath("faq/entry/davdroid-not-working-after-update/").build()),
|
||||
PendingIntent.FLAG_CANCEL_CURRENT))
|
||||
.setLocalOnly(true)
|
||||
.build();
|
||||
NotificationManagerCompat nm = NotificationManagerCompat.from(context);
|
||||
nm.notify(Constants.NOTIFICATION_ACCOUNT_SETTINGS_UPDATED, notify);
|
||||
|
||||
update(version);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static Bundle initialUserData(String userName) {
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putString(KEY_SETTINGS_VERSION, String.valueOf(CURRENT_VERSION));
|
||||
bundle.putString(KEY_USERNAME, userName);
|
||||
return bundle;
|
||||
}
|
||||
|
||||
|
||||
// authentication settings
|
||||
|
||||
public String username() { return accountManager.getUserData(account, KEY_USERNAME); }
|
||||
public void username(@NonNull String userName) { accountManager.setUserData(account, KEY_USERNAME, userName); }
|
||||
|
||||
public String password() { return accountManager.getPassword(account); }
|
||||
public void password(@NonNull String password) { accountManager.setPassword(account, password); }
|
||||
|
||||
|
||||
// sync. settings
|
||||
|
||||
public Long getSyncInterval(@NonNull String authority) {
|
||||
if (ContentResolver.getIsSyncable(account, authority) <= 0)
|
||||
return null;
|
||||
|
||||
if (ContentResolver.getSyncAutomatically(account, authority)) {
|
||||
List<PeriodicSync> syncs = ContentResolver.getPeriodicSyncs(account, authority);
|
||||
if (syncs.isEmpty())
|
||||
return SYNC_INTERVAL_MANUALLY;
|
||||
else
|
||||
return syncs.get(0).period;
|
||||
} else
|
||||
return SYNC_INTERVAL_MANUALLY;
|
||||
}
|
||||
|
||||
public void setSyncInterval(@NonNull String authority, long seconds) {
|
||||
if (seconds == SYNC_INTERVAL_MANUALLY) {
|
||||
ContentResolver.setSyncAutomatically(account, authority, false);
|
||||
} else {
|
||||
ContentResolver.setSyncAutomatically(account, authority, true);
|
||||
ContentResolver.addPeriodicSync(account, authority, new Bundle(), seconds);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean getSyncWifiOnly() {
|
||||
return accountManager.getUserData(account, KEY_WIFI_ONLY) != null;
|
||||
}
|
||||
|
||||
public void setSyncWiFiOnly(boolean wiFiOnly) {
|
||||
accountManager.setUserData(account, KEY_WIFI_ONLY, wiFiOnly ? "1" : null);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getSyncWifiOnlySSID() {
|
||||
return accountManager.getUserData(account, KEY_WIFI_ONLY_SSID);
|
||||
}
|
||||
|
||||
public void setSyncWifiOnlySSID(String ssid) {
|
||||
accountManager.setUserData(account, KEY_WIFI_ONLY_SSID, ssid);
|
||||
}
|
||||
|
||||
|
||||
// CardDAV settings
|
||||
|
||||
public boolean getVCardRFC6868() {
|
||||
if (BuildConfig.settingVCardRFC6868 != null)
|
||||
return BuildConfig.settingVCardRFC6868;
|
||||
|
||||
return accountManager.getUserData(account, KEY_VCARD_RFC6868) == null;
|
||||
}
|
||||
|
||||
public void setVCardRFC6868(boolean use) {
|
||||
if (BuildConfig.settingVCardRFC6868 == null)
|
||||
accountManager.setUserData(account, KEY_VCARD_RFC6868, use ? null : "0");
|
||||
else if (BuildConfig.settingVCardRFC6868 != use)
|
||||
throw new UnsupportedOperationException("Setting is read-only");
|
||||
}
|
||||
|
||||
|
||||
// CalDAV settings
|
||||
|
||||
@Nullable
|
||||
public Integer getTimeRangePastDays() {
|
||||
String strDays = accountManager.getUserData(account, KEY_TIME_RANGE_PAST_DAYS);
|
||||
if (strDays != null) {
|
||||
int days = Integer.valueOf(strDays);
|
||||
return days < 0 ? null : days;
|
||||
} else
|
||||
return DEFAULT_TIME_RANGE_PAST_DAYS;
|
||||
}
|
||||
|
||||
public void setTimeRangePastDays(@Nullable Integer days) {
|
||||
accountManager.setUserData(account, KEY_TIME_RANGE_PAST_DAYS, String.valueOf(days == null ? -1 : days));
|
||||
}
|
||||
|
||||
public boolean getManageCalendarColors() {
|
||||
return accountManager.getUserData(account, KEY_MANAGE_CALENDAR_COLORS) == null;
|
||||
}
|
||||
|
||||
public void setManageCalendarColors(boolean manage) {
|
||||
accountManager.setUserData(account, KEY_MANAGE_CALENDAR_COLORS, manage ? null : "0");
|
||||
}
|
||||
|
||||
|
||||
// CardDAV settings
|
||||
|
||||
@NonNull
|
||||
public GroupMethod getGroupMethod() {
|
||||
if (BuildConfig.settingContactGroupMethod != null)
|
||||
return BuildConfig.settingContactGroupMethod;
|
||||
|
||||
final String name = accountManager.getUserData(account, KEY_CONTACT_GROUP_METHOD);
|
||||
return name != null ?
|
||||
GroupMethod.valueOf(name) :
|
||||
GroupMethod.GROUP_VCARDS;
|
||||
}
|
||||
|
||||
public void setGroupMethod(@NonNull GroupMethod method) {
|
||||
if (BuildConfig.settingContactGroupMethod == null) {
|
||||
final String name = method == GroupMethod.GROUP_VCARDS ? null : method.name();
|
||||
accountManager.setUserData(account, KEY_CONTACT_GROUP_METHOD, name);
|
||||
} else if (BuildConfig.settingContactGroupMethod != method)
|
||||
throw new UnsupportedOperationException("Setting is read-only");
|
||||
}
|
||||
|
||||
|
||||
// update from previous account settings
|
||||
|
||||
private void update(int fromVersion) {
|
||||
for (int toVersion = fromVersion + 1; toVersion <= CURRENT_VERSION; toVersion++) {
|
||||
App.log.info("Updating account " + account.name + " from version " + fromVersion + " to " + toVersion);
|
||||
try {
|
||||
Method updateProc = getClass().getDeclaredMethod("update_" + fromVersion + "_" + toVersion);
|
||||
updateProc.invoke(this);
|
||||
accountManager.setUserData(account, KEY_SETTINGS_VERSION, String.valueOf(toVersion));
|
||||
} catch (Exception e) {
|
||||
App.log.log(Level.SEVERE, "Couldn't update account settings", e);
|
||||
}
|
||||
fromVersion = toVersion;
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "Recycle", "unused" })
|
||||
private void update_1_2() throws ContactsStorageException {
|
||||
/* - KEY_ADDRESSBOOK_URL ("addressbook_url"),
|
||||
- KEY_ADDRESSBOOK_CTAG ("addressbook_ctag"),
|
||||
- KEY_ADDRESSBOOK_VCARD_VERSION ("addressbook_vcard_version") are not used anymore (now stored in ContactsContract.SyncState)
|
||||
- KEY_LAST_ANDROID_VERSION ("last_android_version") has been added
|
||||
*/
|
||||
|
||||
// move previous address book info to ContactsContract.SyncState
|
||||
@Cleanup("release") ContentProviderClient provider = context.getContentResolver().acquireContentProviderClient(ContactsContract.AUTHORITY);
|
||||
if (provider == null)
|
||||
throw new ContactsStorageException("Couldn't access Contacts provider");
|
||||
|
||||
LocalAddressBook addr = new LocalAddressBook(account, provider);
|
||||
|
||||
// until now, ContactsContract.Settings.UNGROUPED_VISIBLE was not set explicitly
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(ContactsContract.Settings.UNGROUPED_VISIBLE, 1);
|
||||
addr.updateSettings(values);
|
||||
|
||||
String url = accountManager.getUserData(account, "addressbook_url");
|
||||
if (!TextUtils.isEmpty(url))
|
||||
addr.setURL(url);
|
||||
accountManager.setUserData(account, "addressbook_url", null);
|
||||
|
||||
String cTag = accountManager.getUserData(account, "addressbook_ctag");
|
||||
if (!TextUtils.isEmpty(cTag))
|
||||
addr.setCTag(cTag);
|
||||
accountManager.setUserData(account, "addressbook_ctag", null);
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "Recycle", "unused" })
|
||||
private void update_2_3() {
|
||||
// Don't show a warning for Android updates anymore
|
||||
accountManager.setUserData(account, "last_android_version", null);
|
||||
|
||||
Long serviceCardDAV = null, serviceCalDAV = null;
|
||||
|
||||
ServiceDB.OpenHelper dbHelper = new ServiceDB.OpenHelper(context);
|
||||
try {
|
||||
SQLiteDatabase db = dbHelper.getWritableDatabase();
|
||||
// we have to create the WebDAV Service database only from the old address book, calendar and task list URLs
|
||||
|
||||
// CardDAV: migrate address books
|
||||
ContentProviderClient client = context.getContentResolver().acquireContentProviderClient(ContactsContract.AUTHORITY);
|
||||
if (client != null)
|
||||
try {
|
||||
LocalAddressBook addrBook = new LocalAddressBook(account, client);
|
||||
String url = addrBook.getURL();
|
||||
if (url != null) {
|
||||
App.log.fine("Migrating address book " + url);
|
||||
|
||||
// insert CardDAV service
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(Services.ACCOUNT_NAME, account.name);
|
||||
values.put(Services.SERVICE, Services.SERVICE_CARDDAV);
|
||||
serviceCardDAV = db.insert(Services._TABLE, null, values);
|
||||
|
||||
// insert address book
|
||||
values.clear();
|
||||
values.put(Collections.SERVICE_ID, serviceCardDAV);
|
||||
values.put(Collections.URL, url);
|
||||
values.put(Collections.SYNC, 1);
|
||||
db.insert(Collections._TABLE, null, values);
|
||||
|
||||
// insert home set
|
||||
HttpUrl homeSet = HttpUrl.parse(url).resolve("../");
|
||||
values.clear();
|
||||
values.put(HomeSets.SERVICE_ID, serviceCardDAV);
|
||||
values.put(HomeSets.URL, homeSet.toString());
|
||||
db.insert(HomeSets._TABLE, null, values);
|
||||
}
|
||||
|
||||
} catch (ContactsStorageException e) {
|
||||
App.log.log(Level.SEVERE, "Couldn't migrate address book", e);
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
|
||||
// CalDAV: migrate calendars + task lists
|
||||
Set<String> collections = new HashSet<>();
|
||||
Set<HttpUrl> homeSets = new HashSet<>();
|
||||
|
||||
client = context.getContentResolver().acquireContentProviderClient(CalendarContract.AUTHORITY);
|
||||
if (client != null)
|
||||
try {
|
||||
LocalCalendar calendars[] = (LocalCalendar[])LocalCalendar.find(account, client, LocalCalendar.Factory.INSTANCE, null, null);
|
||||
for (LocalCalendar calendar : calendars) {
|
||||
String url = calendar.getName();
|
||||
App.log.fine("Migrating calendar " + url);
|
||||
collections.add(url);
|
||||
homeSets.add(HttpUrl.parse(url).resolve("../"));
|
||||
}
|
||||
} catch (CalendarStorageException e) {
|
||||
App.log.log(Level.SEVERE, "Couldn't migrate calendars", e);
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
|
||||
TaskProvider provider = LocalTaskList.acquireTaskProvider(context.getContentResolver());
|
||||
if (provider != null)
|
||||
try {
|
||||
LocalTaskList[] taskLists = (LocalTaskList[])LocalTaskList.find(account, provider, LocalTaskList.Factory.INSTANCE, null, null);
|
||||
for (LocalTaskList taskList : taskLists) {
|
||||
String url = taskList.getSyncId();
|
||||
App.log.fine("Migrating task list " + url);
|
||||
collections.add(url);
|
||||
homeSets.add(HttpUrl.parse(url).resolve("../"));
|
||||
}
|
||||
} catch (CalendarStorageException e) {
|
||||
App.log.log(Level.SEVERE, "Couldn't migrate task lists", e);
|
||||
} finally {
|
||||
provider.close();
|
||||
}
|
||||
|
||||
if (!collections.isEmpty()) {
|
||||
// insert CalDAV service
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(Services.ACCOUNT_NAME, account.name);
|
||||
values.put(Services.SERVICE, Services.SERVICE_CALDAV);
|
||||
serviceCalDAV = db.insert(Services._TABLE, null, values);
|
||||
|
||||
// insert collections
|
||||
for (String url : collections) {
|
||||
values.clear();
|
||||
values.put(Collections.SERVICE_ID, serviceCalDAV);
|
||||
values.put(Collections.URL, url);
|
||||
values.put(Collections.SYNC, 1);
|
||||
db.insert(Collections._TABLE, null, values);
|
||||
}
|
||||
|
||||
// insert home sets
|
||||
for (HttpUrl homeSet : homeSets) {
|
||||
values.clear();
|
||||
values.put(HomeSets.SERVICE_ID, serviceCalDAV);
|
||||
values.put(HomeSets.URL, homeSet.toString());
|
||||
db.insert(HomeSets._TABLE, null, values);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
dbHelper.close();
|
||||
}
|
||||
|
||||
// initiate service detection (refresh) to get display names, colors etc.
|
||||
Intent refresh = new Intent(context, DavService.class);
|
||||
refresh.setAction(DavService.ACTION_REFRESH_COLLECTIONS);
|
||||
if (serviceCardDAV != null) {
|
||||
refresh.putExtra(DavService.EXTRA_DAV_SERVICE_ID, serviceCardDAV);
|
||||
context.startService(refresh);
|
||||
}
|
||||
if (serviceCalDAV != null) {
|
||||
refresh.putExtra(DavService.EXTRA_DAV_SERVICE_ID, serviceCalDAV);
|
||||
context.startService(refresh);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "Recycle", "unused" })
|
||||
private void update_3_4() {
|
||||
setGroupMethod(GroupMethod.CATEGORIES);
|
||||
}
|
||||
|
||||
|
||||
public static class AppUpdatedReceiver extends BroadcastReceiver {
|
||||
|
||||
@Override
|
||||
@SuppressLint("UnsafeProtectedBroadcastReceiver,MissingPermission")
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
App.log.info("DAVdroid was updated, checking for AccountSettings version");
|
||||
|
||||
// peek into AccountSettings to initiate a possible migration
|
||||
AccountManager accountManager = AccountManager.get(context);
|
||||
for (Account account : accountManager.getAccountsByType(context.getString(R.string.account_type)))
|
||||
try {
|
||||
App.log.info("Checking account " + account.name);
|
||||
new AccountSettings(context, account);
|
||||
} catch (InvalidAccountException e) {
|
||||
App.log.log(Level.SEVERE, "Couldn't check for updated account settings", e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
484
app/src/main/java/at/bitfire/davdroid/AccountSettings.kt
Normal file
484
app/src/main/java/at/bitfire/davdroid/AccountSettings.kt
Normal file
@@ -0,0 +1,484 @@
|
||||
/*
|
||||
* 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.Account
|
||||
import android.accounts.AccountManager
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.*
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Parcel
|
||||
import android.os.RemoteException
|
||||
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.ServiceDB
|
||||
import at.bitfire.davdroid.model.ServiceDB.*
|
||||
import at.bitfire.davdroid.model.ServiceDB.Collections
|
||||
import at.bitfire.davdroid.resource.LocalAddressBook
|
||||
import at.bitfire.davdroid.resource.LocalCalendar
|
||||
import at.bitfire.davdroid.resource.LocalTaskList
|
||||
import at.bitfire.ical4android.AndroidCalendar
|
||||
import at.bitfire.ical4android.AndroidTaskList
|
||||
import at.bitfire.ical4android.CalendarStorageException
|
||||
import at.bitfire.vcard4android.ContactsStorageException
|
||||
import at.bitfire.vcard4android.GroupMethod
|
||||
import okhttp3.HttpUrl
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import java.util.*
|
||||
import java.util.logging.Level
|
||||
|
||||
class AccountSettings @Throws(InvalidAccountException::class) constructor(
|
||||
val context: Context,
|
||||
val account: Account
|
||||
) {
|
||||
|
||||
companion object {
|
||||
|
||||
val CURRENT_VERSION = 7
|
||||
val KEY_SETTINGS_VERSION = "version"
|
||||
|
||||
val KEY_USERNAME = "user_name"
|
||||
|
||||
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
|
||||
|
||||
/** 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
|
||||
|
||||
/* 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"
|
||||
|
||||
/** 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"
|
||||
|
||||
@JvmField
|
||||
val SYNC_INTERVAL_MANUALLY = -1L
|
||||
|
||||
fun initialUserData(userName: String): Bundle {
|
||||
val bundle = Bundle(2)
|
||||
bundle.putString(KEY_SETTINGS_VERSION, CURRENT_VERSION.toString())
|
||||
bundle.putString(KEY_USERNAME, userName)
|
||||
return bundle
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
val accountManager: AccountManager = AccountManager.get(context)
|
||||
|
||||
init {
|
||||
synchronized(AccountSettings::class.java) {
|
||||
val versionStr = accountManager.getUserData(account, KEY_SETTINGS_VERSION) ?: throw InvalidAccountException(account)
|
||||
var version = 0
|
||||
try {
|
||||
version = Integer.parseInt(versionStr)
|
||||
} catch (e: NumberFormatException) {
|
||||
}
|
||||
Logger.log.fine("Account ${account.name} has version $version, current version: $CURRENT_VERSION")
|
||||
|
||||
if (version < CURRENT_VERSION)
|
||||
update(version)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// authentication settings
|
||||
|
||||
fun username(): String? = accountManager.getUserData(account, KEY_USERNAME)
|
||||
fun username(userName: String) = accountManager.setUserData(account, KEY_USERNAME, userName)
|
||||
|
||||
fun password(): String? = accountManager.getPassword(account)
|
||||
fun password(password: String) = accountManager.setPassword(account, password)
|
||||
|
||||
|
||||
// sync. settings
|
||||
|
||||
fun getSyncInterval(authority: String): Long? {
|
||||
if (ContentResolver.getIsSyncable(account, authority) <= 0)
|
||||
return null
|
||||
|
||||
return if (ContentResolver.getSyncAutomatically(account, authority))
|
||||
ContentResolver.getPeriodicSyncs(account, authority).firstOrNull()?.period ?: SYNC_INTERVAL_MANUALLY
|
||||
else
|
||||
SYNC_INTERVAL_MANUALLY
|
||||
}
|
||||
|
||||
fun setSyncInterval(authority: String, seconds: Long) {
|
||||
if (seconds == SYNC_INTERVAL_MANUALLY) {
|
||||
ContentResolver.setSyncAutomatically(account, authority, false)
|
||||
} else {
|
||||
ContentResolver.setSyncAutomatically(account, authority, true)
|
||||
ContentResolver.addPeriodicSync(account, authority, Bundle(), seconds)
|
||||
}
|
||||
}
|
||||
|
||||
fun getSyncWifiOnly() = accountManager.getUserData(account, KEY_WIFI_ONLY) != null
|
||||
fun setSyncWiFiOnly(wiFiOnly: Boolean) =
|
||||
accountManager.setUserData(account, KEY_WIFI_ONLY, if (wiFiOnly) "1" else null)
|
||||
|
||||
fun getSyncWifiOnlySSIDs(): List<String>? =
|
||||
accountManager.getUserData(account, KEY_WIFI_ONLY_SSIDS)?.split(',')
|
||||
fun setSyncWifiOnlySSIDs(ssids: List<String>?) =
|
||||
accountManager.setUserData(account, KEY_WIFI_ONLY_SSIDS, StringUtils.trimToNull(ssids?.joinToString(",")))
|
||||
|
||||
|
||||
// CalDAV settings
|
||||
|
||||
fun getTimeRangePastDays(): Int? {
|
||||
val strDays = accountManager.getUserData(account, KEY_TIME_RANGE_PAST_DAYS)
|
||||
if (strDays != null) {
|
||||
val days = Integer.valueOf(strDays)
|
||||
return if (days < 0) null else days
|
||||
} else
|
||||
return DEFAULT_TIME_RANGE_PAST_DAYS
|
||||
}
|
||||
|
||||
fun setTimeRangePastDays(days: Int?) =
|
||||
accountManager.setUserData(account, KEY_TIME_RANGE_PAST_DAYS, (days ?: -1).toString())
|
||||
|
||||
fun getManageCalendarColors() = accountManager.getUserData(account, KEY_MANAGE_CALENDAR_COLORS) == null
|
||||
fun setManageCalendarColors(manage: Boolean) =
|
||||
accountManager.setUserData(account, KEY_MANAGE_CALENDAR_COLORS, if (manage) null else "0")
|
||||
|
||||
|
||||
// CardDAV settings
|
||||
|
||||
fun getGroupMethod(): GroupMethod {
|
||||
BuildConfig.settingContactGroupMethod?.let { return it }
|
||||
|
||||
val name = accountManager.getUserData(account, KEY_CONTACT_GROUP_METHOD)
|
||||
return if (name != null)
|
||||
GroupMethod.valueOf(name)
|
||||
else
|
||||
GroupMethod.GROUP_VCARDS
|
||||
}
|
||||
|
||||
fun setGroupMethod(method: GroupMethod) {
|
||||
if (BuildConfig.settingContactGroupMethod == null) {
|
||||
val name = if (method == GroupMethod.GROUP_VCARDS) null else method.name
|
||||
accountManager.setUserData(account, KEY_CONTACT_GROUP_METHOD, name)
|
||||
} else if (BuildConfig.settingContactGroupMethod != method)
|
||||
throw UnsupportedOperationException("Group method setting is read-only")
|
||||
}
|
||||
|
||||
|
||||
// update from previous account settings
|
||||
|
||||
private fun update(baseVersion: Int) {
|
||||
for (toVersion in baseVersion+1 .. CURRENT_VERSION) {
|
||||
val fromVersion = toVersion-1
|
||||
Logger.log.info("Updating account ${account.name} from version $fromVersion to $toVersion")
|
||||
try {
|
||||
val updateProc = this::class.java.getDeclaredMethod("update_${fromVersion}_$toVersion")
|
||||
updateProc.invoke(this)
|
||||
accountManager.setUserData(account, KEY_SETTINGS_VERSION, toVersion.toString())
|
||||
} catch (e: Exception) {
|
||||
Logger.log.log(Level.SEVERE, "Couldn't update account settings", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
private fun update_6_7() {
|
||||
// add calendar colors
|
||||
context.contentResolver.acquireContentProviderClient(CalendarContract.AUTHORITY)?.let { provider ->
|
||||
try {
|
||||
AndroidCalendar.insertColors(provider, account)
|
||||
} finally {
|
||||
if (Build.VERSION.SDK_INT >= 24)
|
||||
provider.close()
|
||||
else
|
||||
provider.release()
|
||||
}
|
||||
}
|
||||
|
||||
// update allowed WiFi settings key
|
||||
val onlySSID = accountManager.getUserData(account, "wifi_only_ssid")
|
||||
accountManager.setUserData(account, KEY_WIFI_ONLY_SSIDS, onlySSID)
|
||||
accountManager.setUserData(account, "wifi_only_ssid", null)
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
private fun update_5_6() {
|
||||
context.contentResolver.acquireContentProviderClient(ContactsContract.AUTHORITY)?.let { provider ->
|
||||
val parcel = Parcel.obtain()
|
||||
try {
|
||||
// don't run syncs during the migration
|
||||
ContentResolver.setIsSyncable(account, ContactsContract.AUTHORITY, 0)
|
||||
ContentResolver.setIsSyncable(account, App.addressBooksAuthority, 0)
|
||||
ContentResolver.cancelSync(account, null)
|
||||
|
||||
// get previous address book settings (including URL)
|
||||
val raw = ContactsContract.SyncState.get(provider, account)
|
||||
if (raw == null)
|
||||
Logger.log.info("No contacts sync state, ignoring account")
|
||||
else {
|
||||
parcel.unmarshall(raw, 0, raw.size)
|
||||
parcel.setDataPosition(0)
|
||||
val params = parcel.readBundle()
|
||||
val url = params.getString("url")
|
||||
if (url == null)
|
||||
Logger.log.info("No address book URL, ignoring account")
|
||||
else {
|
||||
// create new address book
|
||||
val info = CollectionInfo(url)
|
||||
info.type = CollectionInfo.Type.ADDRESS_BOOK
|
||||
info.displayName = account.name
|
||||
Logger.log.log(Level.INFO, "Creating new address book account", url)
|
||||
val addressBookAccount = Account(LocalAddressBook.accountName(account, info), App.addressBookAccountType)
|
||||
if (!accountManager.addAccountExplicitly(addressBookAccount, null, LocalAddressBook.initialUserData(account, info.url)))
|
||||
throw ContactsStorageException("Couldn't create address book account")
|
||||
|
||||
// move contacts to new address book
|
||||
Logger.log.info("Moving contacts from $account to $addressBookAccount")
|
||||
val newAccount = ContentValues(2)
|
||||
newAccount.put(ContactsContract.RawContacts.ACCOUNT_NAME, addressBookAccount.name)
|
||||
newAccount.put(ContactsContract.RawContacts.ACCOUNT_TYPE, addressBookAccount.type)
|
||||
val affected = provider.update(ContactsContract.RawContacts.CONTENT_URI.buildUpon()
|
||||
.appendQueryParameter(ContactsContract.RawContacts.ACCOUNT_NAME, account.name)
|
||||
.appendQueryParameter(ContactsContract.RawContacts.ACCOUNT_TYPE, account.type)
|
||||
.appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true").build(),
|
||||
newAccount,
|
||||
"${ContactsContract.RawContacts.ACCOUNT_NAME}=? AND ${ContactsContract.RawContacts.ACCOUNT_TYPE}=?",
|
||||
arrayOf(account.name, account.type))
|
||||
Logger.log.info("$affected contacts moved to new address book")
|
||||
}
|
||||
|
||||
ContactsContract.SyncState.set(provider, account, null)
|
||||
}
|
||||
} catch(e: RemoteException) {
|
||||
throw ContactsStorageException("Couldn't migrate contacts to new address book", e)
|
||||
} finally {
|
||||
parcel.recycle()
|
||||
if (Build.VERSION.SDK_INT >= 24)
|
||||
provider.close()
|
||||
else
|
||||
provider.release()
|
||||
}
|
||||
}
|
||||
|
||||
// update version number so that further syncs don't repeat the migration
|
||||
accountManager.setUserData(account, KEY_SETTINGS_VERSION, "6")
|
||||
|
||||
// request sync of new address book account
|
||||
ContentResolver.setIsSyncable(account, App.addressBooksAuthority, 1)
|
||||
setSyncInterval(App.addressBooksAuthority, Constants.DEFAULT_SYNC_INTERVAL)
|
||||
}
|
||||
|
||||
/* Android 7.1.1 OpenTasks fix */
|
||||
@Suppress("unused")
|
||||
private fun update_4_5() {
|
||||
// call PackageChangedReceiver which then enables/disables OpenTasks sync when it's (not) available
|
||||
PackageChangedReceiver.updateTaskSync(context)
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
private fun update_3_4() {
|
||||
setGroupMethod(GroupMethod.CATEGORIES)
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
private fun update_2_3() {
|
||||
// Don't show a warning for Android updates anymore
|
||||
accountManager.setUserData(account, "last_android_version", null)
|
||||
|
||||
var serviceCardDAV: Long? = null
|
||||
var serviceCalDAV: Long? = null
|
||||
|
||||
ServiceDB.OpenHelper(context).use { dbHelper ->
|
||||
val db = dbHelper.writableDatabase
|
||||
// we have to create the WebDAV Service database only from the old address book, calendar and task list URLs
|
||||
|
||||
// CardDAV: migrate address books
|
||||
context.contentResolver.acquireContentProviderClient(ContactsContract.AUTHORITY)?.let { client ->
|
||||
try {
|
||||
val addrBook = LocalAddressBook(context, account, client)
|
||||
val url = addrBook.getURL()
|
||||
Logger.log.fine("Migrating address book $url")
|
||||
|
||||
// insert CardDAV service
|
||||
val values = ContentValues(3)
|
||||
values.put(Services.ACCOUNT_NAME, account.name)
|
||||
values.put(Services.SERVICE, Services.SERVICE_CARDDAV)
|
||||
serviceCardDAV = db.insert(Services._TABLE, null, values)
|
||||
|
||||
// insert address book
|
||||
values.clear()
|
||||
values.put(Collections.SERVICE_ID, serviceCardDAV)
|
||||
values.put(Collections.URL, url)
|
||||
values.put(Collections.SYNC, 1)
|
||||
db.insert(Collections._TABLE, null, values)
|
||||
|
||||
// insert home set
|
||||
HttpUrl.parse(url)?.let {
|
||||
val homeSet = it.resolve("../")
|
||||
values.clear()
|
||||
values.put(HomeSets.SERVICE_ID, serviceCardDAV)
|
||||
values.put(HomeSets.URL, homeSet.toString())
|
||||
db.insert(HomeSets._TABLE, null, values)
|
||||
}
|
||||
} catch (e: ContactsStorageException) {
|
||||
Logger.log.log(Level.SEVERE, "Couldn't migrate address book", e)
|
||||
} finally {
|
||||
if (Build.VERSION.SDK_INT >= 24)
|
||||
client.close()
|
||||
else
|
||||
@Suppress("deprecation")
|
||||
client.release()
|
||||
}
|
||||
}
|
||||
|
||||
// CalDAV: migrate calendars + task lists
|
||||
val collections = HashSet<String>()
|
||||
val homeSets = HashSet<HttpUrl>()
|
||||
|
||||
context.contentResolver.acquireContentProviderClient(CalendarContract.AUTHORITY)?.let { client ->
|
||||
try {
|
||||
val calendars = AndroidCalendar.find(account, client, LocalCalendar.Factory, null, null)
|
||||
for (calendar in calendars)
|
||||
calendar.name?.let { url ->
|
||||
Logger.log.fine("Migrating calendar $url")
|
||||
collections.add(url)
|
||||
HttpUrl.parse(url)?.resolve("../")?.let { homeSets.add(it) }
|
||||
}
|
||||
} catch (e: CalendarStorageException) {
|
||||
Logger.log.log(Level.SEVERE, "Couldn't migrate calendars", e)
|
||||
} finally {
|
||||
if (Build.VERSION.SDK_INT >= 24)
|
||||
client.close()
|
||||
else
|
||||
@Suppress("deprecation")
|
||||
client.release()
|
||||
}
|
||||
}
|
||||
|
||||
AndroidTaskList.acquireTaskProvider(context.contentResolver)?.use { provider ->
|
||||
try {
|
||||
val taskLists = AndroidTaskList.find(account, provider, LocalTaskList.Factory, null, null)
|
||||
for (taskList in taskLists)
|
||||
taskList.syncId?.let { url ->
|
||||
Logger.log.fine("Migrating task list $url")
|
||||
collections.add(url)
|
||||
HttpUrl.parse(url)?.resolve("../")?.let { homeSets.add(it) }
|
||||
}
|
||||
} catch (e: CalendarStorageException) {
|
||||
Logger.log.log(Level.SEVERE, "Couldn't migrate task lists", e)
|
||||
}
|
||||
}
|
||||
|
||||
if (!collections.isEmpty()) {
|
||||
// insert CalDAV service
|
||||
val values = ContentValues(3)
|
||||
values.put(Services.ACCOUNT_NAME, account.name)
|
||||
values.put(Services.SERVICE, Services.SERVICE_CALDAV)
|
||||
serviceCalDAV = db.insert(Services._TABLE, null, values)
|
||||
|
||||
// insert collections
|
||||
for (url in collections) {
|
||||
values.clear()
|
||||
values.put(Collections.SERVICE_ID, serviceCalDAV)
|
||||
values.put(Collections.URL, url)
|
||||
values.put(Collections.SYNC, 1)
|
||||
db.insert(Collections._TABLE, null, values)
|
||||
}
|
||||
|
||||
// insert home sets
|
||||
for (homeSet in homeSets) {
|
||||
values.clear()
|
||||
values.put(HomeSets.SERVICE_ID, serviceCalDAV)
|
||||
values.put(HomeSets.URL, homeSet.toString())
|
||||
db.insert(HomeSets._TABLE, null, values)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// initiate service detection (refresh) to get display names, colors etc.
|
||||
val refresh = Intent(context, DavService::class.java)
|
||||
refresh.action = DavService.ACTION_REFRESH_COLLECTIONS
|
||||
serviceCardDAV?.let {
|
||||
refresh.putExtra(DavService.EXTRA_DAV_SERVICE_ID, it)
|
||||
context.startService(refresh)
|
||||
}
|
||||
serviceCalDAV?.let {
|
||||
refresh.putExtra(DavService.EXTRA_DAV_SERVICE_ID, it)
|
||||
context.startService(refresh)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
private fun update_1_2() {
|
||||
/* - KEY_ADDRESSBOOK_URL ("addressbook_url"),
|
||||
- KEY_ADDRESSBOOK_CTAG ("addressbook_ctag"),
|
||||
- KEY_ADDRESSBOOK_VCARD_VERSION ("addressbook_vcard_version") are not used anymore (now stored in ContactsContract.SyncState)
|
||||
- KEY_LAST_ANDROID_VERSION ("last_android_version") has been added
|
||||
*/
|
||||
|
||||
// move previous address book info to ContactsContract.SyncState
|
||||
val provider = context.contentResolver.acquireContentProviderClient(ContactsContract.AUTHORITY) ?:
|
||||
throw ContactsStorageException("Couldn't access Contacts provider")
|
||||
|
||||
try {
|
||||
val addr = LocalAddressBook(context, account, provider)
|
||||
|
||||
// until now, ContactsContract.Settings.UNGROUPED_VISIBLE was not set explicitly
|
||||
val values = ContentValues()
|
||||
values.put(ContactsContract.Settings.UNGROUPED_VISIBLE, 1)
|
||||
addr.updateSettings(values)
|
||||
|
||||
val url = accountManager.getUserData(account, "addressbook_url")
|
||||
if (!url.isNullOrEmpty())
|
||||
addr.setURL(url)
|
||||
accountManager.setUserData(account, "addressbook_url", null)
|
||||
|
||||
val cTag = accountManager.getUserData (account, "addressbook_ctag")
|
||||
if (!cTag.isNullOrEmpty())
|
||||
addr.setCTag(cTag)
|
||||
accountManager.setUserData(account, "addressbook_ctag", null)
|
||||
} finally {
|
||||
if (Build.VERSION.SDK_INT >= 24)
|
||||
provider.close()
|
||||
else
|
||||
@Suppress("deprecation")
|
||||
provider.release()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class AppUpdatedReceiver: BroadcastReceiver() {
|
||||
|
||||
@SuppressLint("UnsafeProtectedBroadcastReceiver,MissingPermission")
|
||||
override fun onReceive(context: Context, intent: Intent?) {
|
||||
Logger.log.info("DAVdroid was updated, checking for AccountSettings version")
|
||||
|
||||
// peek into AccountSettings to initiate a possible migration
|
||||
val accountManager = AccountManager.get(context)
|
||||
for (account in accountManager.getAccountsByType(context.getString(R.string.account_type)))
|
||||
try {
|
||||
Logger.log.info("Checking account ${account.name}")
|
||||
AccountSettings(context, account)
|
||||
} catch(e: InvalidAccountException) {
|
||||
Logger.log.log(Level.SEVERE, "Couldn't check for updated account settings", e)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2013 – 2016 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.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
public class AccountsChangedReceiver extends BroadcastReceiver {
|
||||
|
||||
protected static final List<OnAccountsUpdateListener> listeners = new LinkedList<>();
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
if (AccountManager.LOGIN_ACCOUNTS_CHANGED_ACTION.equals(intent.getAction())) {
|
||||
Intent serviceIntent = new Intent(context, DavService.class);
|
||||
serviceIntent.setAction(DavService.ACTION_ACCOUNTS_UPDATED);
|
||||
context.startService(serviceIntent);
|
||||
|
||||
for (OnAccountsUpdateListener listener : listeners)
|
||||
listener.onAccountsUpdated(null);
|
||||
}
|
||||
}
|
||||
|
||||
public static void registerListener(OnAccountsUpdateListener listener, boolean callImmediately) {
|
||||
listeners.add(listener);
|
||||
if (callImmediately)
|
||||
listener.onAccountsUpdated(null);
|
||||
}
|
||||
|
||||
public static void unregisterListener(OnAccountsUpdateListener listener) {
|
||||
listeners.remove(listener);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* 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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,177 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2013 – 2016 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.annotation.TargetApi;
|
||||
import android.app.Application;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.drawable.BitmapDrawable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Build;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.app.NotificationManagerCompat;
|
||||
import android.support.v7.app.NotificationCompat;
|
||||
import android.util.Log;
|
||||
|
||||
import org.apache.commons.lang3.time.DateFormatUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.logging.FileHandler;
|
||||
import java.util.logging.Handler;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import javax.net.ssl.HostnameVerifier;
|
||||
|
||||
import at.bitfire.cert4android.CustomCertManager;
|
||||
import at.bitfire.davdroid.log.LogcatHandler;
|
||||
import at.bitfire.davdroid.log.PlainTextFormatter;
|
||||
import at.bitfire.davdroid.model.ServiceDB;
|
||||
import at.bitfire.davdroid.model.Settings;
|
||||
import lombok.Cleanup;
|
||||
import lombok.Getter;
|
||||
import okhttp3.internal.tls.OkHostnameVerifier;
|
||||
|
||||
public class App extends Application {
|
||||
public static final String
|
||||
FLAVOR_GOOGLE_PLAY = "gplay",
|
||||
FLAVOR_ICLOUD = "icloud",
|
||||
FLAVOR_STANDARD = "standard";
|
||||
|
||||
public static final String
|
||||
DISTRUST_SYSTEM_CERTIFICATES = "distrustSystemCerts",
|
||||
LOG_TO_EXTERNAL_STORAGE = "logToExternalStorage";
|
||||
|
||||
@Getter
|
||||
private static CustomCertManager certManager;
|
||||
|
||||
@Getter
|
||||
private static SSLSocketFactoryCompat sslSocketFactoryCompat;
|
||||
|
||||
@Getter
|
||||
private static HostnameVerifier hostnameVerifier;
|
||||
|
||||
public final static Logger log = Logger.getLogger("davdroid");
|
||||
static {
|
||||
at.bitfire.dav4android.Constants.log = Logger.getLogger("davdroid.dav4android");
|
||||
at.bitfire.cert4android.Constants.log = Logger.getLogger("davdroid.cert4android");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
reinitCertManager();
|
||||
reinitLogger();
|
||||
}
|
||||
|
||||
public void reinitCertManager() {
|
||||
if (BuildConfig.customCerts) {
|
||||
if (certManager != null)
|
||||
certManager.close();
|
||||
|
||||
@Cleanup ServiceDB.OpenHelper dbHelper = new ServiceDB.OpenHelper(this);
|
||||
Settings settings = new Settings(dbHelper.getReadableDatabase());
|
||||
|
||||
certManager = new CustomCertManager(this, !settings.getBoolean(DISTRUST_SYSTEM_CERTIFICATES, false));
|
||||
sslSocketFactoryCompat = new SSLSocketFactoryCompat(certManager);
|
||||
hostnameVerifier = certManager.hostnameVerifier(OkHostnameVerifier.INSTANCE);
|
||||
}
|
||||
}
|
||||
|
||||
public void reinitLogger() {
|
||||
// don't use Android default logging, we have our own handlers
|
||||
log.setUseParentHandlers(false);
|
||||
|
||||
@Cleanup ServiceDB.OpenHelper dbHelper = new ServiceDB.OpenHelper(this);
|
||||
Settings settings = new Settings(dbHelper.getReadableDatabase());
|
||||
|
||||
boolean logToFile = settings.getBoolean(LOG_TO_EXTERNAL_STORAGE, false),
|
||||
logVerbose = logToFile || Log.isLoggable(log.getName(), Log.DEBUG);
|
||||
|
||||
// set logging level according to preferences
|
||||
log.setLevel(logVerbose ? Level.ALL : Level.INFO);
|
||||
|
||||
// remove all handlers
|
||||
for (Handler handler : log.getHandlers())
|
||||
log.removeHandler(handler);
|
||||
|
||||
// add logcat handler
|
||||
log.addHandler(LogcatHandler.INSTANCE);
|
||||
|
||||
NotificationManagerCompat nm = NotificationManagerCompat.from(this);
|
||||
// log to external file according to preferences
|
||||
if (logToFile) {
|
||||
NotificationCompat.Builder builder = new NotificationCompat.Builder(this);
|
||||
builder .setSmallIcon(R.drawable.ic_sd_storage_light)
|
||||
.setLargeIcon(getLauncherBitmap(this))
|
||||
.setContentTitle(getString(R.string.logging_davdroid_file_logging))
|
||||
.setLocalOnly(true);
|
||||
|
||||
File dir = getExternalFilesDir(null);
|
||||
if (dir != null)
|
||||
try {
|
||||
String fileName = new File(dir, "davdroid-" + android.os.Process.myPid() + "-" +
|
||||
DateFormatUtils.format(System.currentTimeMillis(), "yyyyMMdd-HHmmss") + ".txt").toString();
|
||||
log.info("Logging to " + fileName);
|
||||
|
||||
FileHandler fileHandler = new FileHandler(fileName);
|
||||
fileHandler.setFormatter(PlainTextFormatter.DEFAULT);
|
||||
log.addHandler(fileHandler);
|
||||
builder .setContentText(dir.getPath())
|
||||
.setSubText(getString(R.string.logging_to_external_storage_warning))
|
||||
.setCategory(NotificationCompat.CATEGORY_STATUS)
|
||||
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||
.setStyle(new NotificationCompat.BigTextStyle()
|
||||
.bigText(getString(R.string.logging_to_external_storage, dir.getPath())))
|
||||
.setOngoing(true);
|
||||
|
||||
} catch (IOException e) {
|
||||
log.log(Level.SEVERE, "Couldn't create external log file", e);
|
||||
|
||||
builder .setContentText(getString(R.string.logging_couldnt_create_file, e.getLocalizedMessage()))
|
||||
.setCategory(NotificationCompat.CATEGORY_ERROR);
|
||||
}
|
||||
else
|
||||
builder.setContentText(getString(R.string.logging_no_external_storage));
|
||||
|
||||
nm.notify(Constants.NOTIFICATION_EXTERNAL_FILE_LOGGING, builder.build());
|
||||
} else
|
||||
nm.cancel(Constants.NOTIFICATION_EXTERNAL_FILE_LOGGING);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
||||
public static Bitmap getLauncherBitmap(@NonNull Context context) {
|
||||
Bitmap bitmapLogo = null;
|
||||
Drawable drawableLogo = android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP ?
|
||||
context.getDrawable(R.mipmap.ic_launcher) :
|
||||
context.getResources().getDrawable(R.mipmap.ic_launcher);
|
||||
if (drawableLogo instanceof BitmapDrawable)
|
||||
bitmapLogo = ((BitmapDrawable)drawableLogo).getBitmap();
|
||||
return bitmapLogo;
|
||||
}
|
||||
|
||||
|
||||
public static class ReinitLoggingReceiver extends BroadcastReceiver {
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
log.info("Received broadcast: re-initializing logger");
|
||||
|
||||
App app = (App)context.getApplicationContext();
|
||||
app.reinitLogger();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
65
app/src/main/java/at/bitfire/davdroid/App.kt
Normal file
65
app/src/main/java/at/bitfire/davdroid/App.kt
Normal file
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* 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.app.Application
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.drawable.BitmapDrawable
|
||||
import at.bitfire.davdroid.log.Logger
|
||||
|
||||
class App: Application() {
|
||||
|
||||
companion object {
|
||||
|
||||
@JvmField val FLAVOR_GOOGLE_PLAY = "gplay"
|
||||
@JvmField val FLAVOR_ICLOUD = "icloud"
|
||||
@JvmField val FLAVOR_SOLDUPE = "soldupe"
|
||||
@JvmField val FLAVOR_STANDARD = "standard"
|
||||
|
||||
@JvmField val DISTRUST_SYSTEM_CERTIFICATES = "distrustSystemCerts"
|
||||
@JvmField val LOG_TO_EXTERNAL_STORAGE = "logToExternalStorage"
|
||||
@JvmField val OVERRIDE_PROXY = "overrideProxy"
|
||||
@JvmField val OVERRIDE_PROXY_HOST = "overrideProxyHost"
|
||||
@JvmField val OVERRIDE_PROXY_PORT = "overrideProxyPort"
|
||||
|
||||
@JvmField val OVERRIDE_PROXY_HOST_DEFAULT = "localhost"
|
||||
@JvmField val OVERRIDE_PROXY_PORT_DEFAULT = 8118
|
||||
|
||||
lateinit var addressBookAccountType: String
|
||||
lateinit var addressBooksAuthority: String
|
||||
|
||||
@JvmStatic
|
||||
fun getLauncherBitmap(context: Context): Bitmap? {
|
||||
val drawableLogo = if (android.os.Build.VERSION.SDK_INT >= 21)
|
||||
context.getDrawable(R.mipmap.ic_launcher)
|
||||
else
|
||||
@Suppress("deprecation")
|
||||
context.resources.getDrawable(R.mipmap.ic_launcher)
|
||||
return if (drawableLogo is BitmapDrawable)
|
||||
drawableLogo.bitmap
|
||||
else
|
||||
null
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
CustomCertificates.reinitCertManager(this)
|
||||
Logger.reinitLogger(this)
|
||||
|
||||
addressBookAccountType = getString(R.string.account_type_address_book)
|
||||
addressBooksAuthority = getString(R.string.address_books_authority)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2013 – 2015 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 java.lang.reflect.Array;
|
||||
|
||||
public class ArrayUtils {
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public static <T> T[][] partition(T[] bigArray, int max) {
|
||||
int nItems = bigArray.length;
|
||||
int nPartArrays = (nItems + max-1)/max;
|
||||
|
||||
T[][] partArrays = (T[][])Array.newInstance(bigArray.getClass().getComponentType(), nPartArrays, 0);
|
||||
|
||||
// nItems is now the number of remaining items
|
||||
for (int i = 0; nItems > 0; i++) {
|
||||
int n = (nItems < max) ? nItems : max;
|
||||
partArrays[i] = (T[])Array.newInstance(bigArray.getClass().getComponentType(), n);
|
||||
System.arraycopy(bigArray, i*max, partArrays[i], 0, n);
|
||||
|
||||
nItems -= n;
|
||||
}
|
||||
|
||||
return partArrays;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2013 – 2015 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.net.Uri;
|
||||
|
||||
public class Constants {
|
||||
|
||||
// notification IDs
|
||||
public final static int
|
||||
NOTIFICATION_ACCOUNT_SETTINGS_UPDATED = 0,
|
||||
NOTIFICATION_EXTERNAL_FILE_LOGGING = 1,
|
||||
NOTIFICATION_REFRESH_COLLECTIONS = 2,
|
||||
NOTIFICATION_CONTACTS_SYNC = 10,
|
||||
NOTIFICATION_CALENDAR_SYNC = 11,
|
||||
NOTIFICATION_TASK_SYNC = 12,
|
||||
NOTIFICATION_PERMISSIONS = 20,
|
||||
NOTIFICATION_SUBSCRIPTION = 21;
|
||||
|
||||
public static final Uri webUri = BuildConfig.FLAVOR == App.FLAVOR_ICLOUD ?
|
||||
Uri.parse("https://multisync.cloud/?pk_campaign=multisync-app") :
|
||||
Uri.parse("https://davdroid.bitfire.at/?pk_campaign=davdroid-app");
|
||||
|
||||
}
|
||||
24
app/src/main/java/at/bitfire/davdroid/Constants.kt
Normal file
24
app/src/main/java/at/bitfire/davdroid/Constants.kt
Normal file
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
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
|
||||
|
||||
@JvmField
|
||||
val DEFAULT_SYNC_INTERVAL = 4 * 3600L // 4 hours
|
||||
|
||||
}
|
||||
41
app/src/main/java/at/bitfire/davdroid/CustomCertificates.kt
Normal file
41
app/src/main/java/at/bitfire/davdroid/CustomCertificates.kt
Normal file
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* 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.content.Context
|
||||
import android.os.Build
|
||||
import at.bitfire.cert4android.CustomCertManager
|
||||
import at.bitfire.davdroid.model.ServiceDB
|
||||
import at.bitfire.davdroid.model.Settings
|
||||
import okhttp3.internal.tls.OkHostnameVerifier
|
||||
import javax.net.ssl.HostnameVerifier
|
||||
import javax.net.ssl.SSLSocketFactory
|
||||
|
||||
object CustomCertificates {
|
||||
|
||||
@JvmField var certManager: CustomCertManager? = null
|
||||
var sslSocketFactoryCompat: SSLSocketFactory? = null
|
||||
var hostnameVerifier: HostnameVerifier? = null
|
||||
|
||||
fun reinitCertManager(context: Context) {
|
||||
if (BuildConfig.customCerts) {
|
||||
certManager?.close()
|
||||
|
||||
ServiceDB.OpenHelper(context).use { dbHelper ->
|
||||
val settings = Settings(dbHelper.readableDatabase)
|
||||
|
||||
val mgr = CustomCertManager(context.applicationContext, !settings.getBoolean(App.DISTRUST_SYSTEM_CERTIFICATES, false))
|
||||
certManager = mgr
|
||||
hostnameVerifier = mgr.hostnameVerifier(OkHostnameVerifier.INSTANCE)
|
||||
sslSocketFactoryCompat = SSLSocketFactoryCompat(mgr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,428 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2013 – 2016 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.Account;
|
||||
import android.accounts.AccountManager;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Notification;
|
||||
import android.app.PendingIntent;
|
||||
import android.app.Service;
|
||||
import android.content.ContentValues;
|
||||
import android.content.Intent;
|
||||
import android.database.Cursor;
|
||||
import android.database.DatabaseUtils;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.os.Binder;
|
||||
import android.os.IBinder;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.app.NotificationManagerCompat;
|
||||
import android.support.v7.app.NotificationCompat;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import org.apache.commons.collections4.iterators.IteratorChain;
|
||||
import org.apache.commons.collections4.iterators.SingletonIterator;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import at.bitfire.dav4android.DavResource;
|
||||
import at.bitfire.dav4android.UrlUtils;
|
||||
import at.bitfire.dav4android.exception.DavException;
|
||||
import at.bitfire.dav4android.exception.HttpException;
|
||||
import at.bitfire.dav4android.property.AddressbookHomeSet;
|
||||
import at.bitfire.dav4android.property.CalendarHomeSet;
|
||||
import at.bitfire.dav4android.property.CalendarProxyReadFor;
|
||||
import at.bitfire.dav4android.property.CalendarProxyWriteFor;
|
||||
import at.bitfire.dav4android.property.GroupMembership;
|
||||
import at.bitfire.davdroid.model.CollectionInfo;
|
||||
import at.bitfire.davdroid.model.ServiceDB.Collections;
|
||||
import at.bitfire.davdroid.model.ServiceDB.HomeSets;
|
||||
import at.bitfire.davdroid.model.ServiceDB.OpenHelper;
|
||||
import at.bitfire.davdroid.model.ServiceDB.Services;
|
||||
import at.bitfire.davdroid.ui.DebugInfoActivity;
|
||||
import lombok.Cleanup;
|
||||
import okhttp3.HttpUrl;
|
||||
import okhttp3.OkHttpClient;
|
||||
|
||||
public class DavService extends Service {
|
||||
|
||||
public static final String
|
||||
ACTION_ACCOUNTS_UPDATED = "accountsUpdated",
|
||||
ACTION_REFRESH_COLLECTIONS = "refreshCollections",
|
||||
EXTRA_DAV_SERVICE_ID = "davServiceID";
|
||||
|
||||
private final IBinder binder = new InfoBinder();
|
||||
|
||||
private final Set<Long> runningRefresh = new HashSet<>();
|
||||
private final List<WeakReference<RefreshingStatusListener>> refreshingStatusListeners = new LinkedList<>();
|
||||
|
||||
|
||||
@Override
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
if (intent != null) {
|
||||
String action = intent.getAction();
|
||||
long id = intent.getLongExtra(EXTRA_DAV_SERVICE_ID, -1);
|
||||
|
||||
switch (action) {
|
||||
case ACTION_ACCOUNTS_UPDATED:
|
||||
cleanupAccounts();
|
||||
break;
|
||||
case ACTION_REFRESH_COLLECTIONS:
|
||||
if (runningRefresh.add(id)) {
|
||||
new Thread(new RefreshCollections(id)).start();
|
||||
for (WeakReference<RefreshingStatusListener> ref : refreshingStatusListeners) {
|
||||
RefreshingStatusListener listener = ref.get();
|
||||
if (listener != null)
|
||||
listener.onDavRefreshStatusChanged(id, true);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
|
||||
|
||||
/* BOUND SERVICE PART
|
||||
for communicating with the activities
|
||||
*/
|
||||
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
return binder;
|
||||
}
|
||||
|
||||
public interface RefreshingStatusListener {
|
||||
void onDavRefreshStatusChanged(long id, boolean refreshing);
|
||||
}
|
||||
|
||||
public class InfoBinder extends Binder {
|
||||
public boolean isRefreshing(long id) {
|
||||
return runningRefresh.contains(id);
|
||||
}
|
||||
|
||||
public void addRefreshingStatusListener(@NonNull RefreshingStatusListener listener, boolean callImmediate) {
|
||||
refreshingStatusListeners.add(new WeakReference<>(listener));
|
||||
if (callImmediate)
|
||||
for (long id : runningRefresh)
|
||||
listener.onDavRefreshStatusChanged(id, true);
|
||||
}
|
||||
|
||||
public void removeRefreshingStatusListener(@NonNull RefreshingStatusListener listener) {
|
||||
for (Iterator<WeakReference<RefreshingStatusListener>> iterator = refreshingStatusListeners.iterator(); iterator.hasNext(); ) {
|
||||
RefreshingStatusListener item = iterator.next().get();
|
||||
if (listener.equals(item))
|
||||
iterator.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* ACTION RUNNABLES
|
||||
which actually do the work
|
||||
*/
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
void cleanupAccounts() {
|
||||
App.log.info("Cleaning up orphaned accounts");
|
||||
|
||||
final OpenHelper dbHelper = new OpenHelper(this);
|
||||
try {
|
||||
SQLiteDatabase db = dbHelper.getWritableDatabase();
|
||||
|
||||
List<String> sqlAccountNames = new LinkedList<>();
|
||||
AccountManager am = AccountManager.get(this);
|
||||
for (Account account : am.getAccountsByType(getString(R.string.account_type)))
|
||||
sqlAccountNames.add(DatabaseUtils.sqlEscapeString(account.name));
|
||||
|
||||
if (sqlAccountNames.isEmpty())
|
||||
db.delete(Services._TABLE, null, null);
|
||||
else
|
||||
db.delete(Services._TABLE, Services.ACCOUNT_NAME + " NOT IN (" + TextUtils.join(",", sqlAccountNames) + ")", null);
|
||||
} finally {
|
||||
dbHelper.close();
|
||||
}
|
||||
}
|
||||
|
||||
private class RefreshCollections implements Runnable {
|
||||
final long service;
|
||||
final OpenHelper dbHelper;
|
||||
SQLiteDatabase db;
|
||||
|
||||
RefreshCollections(long davServiceId) {
|
||||
this.service = davServiceId;
|
||||
dbHelper = new OpenHelper(DavService.this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
Account account = null;
|
||||
|
||||
try {
|
||||
db = dbHelper.getWritableDatabase();
|
||||
|
||||
String serviceType = serviceType();
|
||||
App.log.info("Refreshing " + serviceType + " collections of service #" + service);
|
||||
|
||||
// get account
|
||||
account = account();
|
||||
|
||||
// create authenticating OkHttpClient (credentials taken from account settings)
|
||||
OkHttpClient httpClient = HttpClient.create(DavService.this, account);
|
||||
|
||||
// refresh home sets: principal
|
||||
Set<HttpUrl> homeSets = readHomeSets();
|
||||
HttpUrl principal = readPrincipal();
|
||||
if (principal != null) {
|
||||
App.log.fine("Querying principal for home sets");
|
||||
DavResource dav = new DavResource(httpClient, principal);
|
||||
queryHomeSets(serviceType, dav, homeSets);
|
||||
|
||||
// refresh home sets: calendar-proxy-read/write-for
|
||||
CalendarProxyReadFor proxyRead = (CalendarProxyReadFor)dav.properties.get(CalendarProxyReadFor.NAME);
|
||||
if (proxyRead != null)
|
||||
for (String href : proxyRead.principals) {
|
||||
App.log.fine("Principal is a read-only proxy for " + href + ", checking for home sets");
|
||||
queryHomeSets(serviceType, new DavResource(httpClient, dav.location.resolve(href)), homeSets);
|
||||
}
|
||||
CalendarProxyWriteFor proxyWrite = (CalendarProxyWriteFor)dav.properties.get(CalendarProxyWriteFor.NAME);
|
||||
if (proxyWrite != null)
|
||||
for (String href : proxyWrite.principals) {
|
||||
App.log.fine("Principal is a read-write proxy for " + href + ", checking for home sets");
|
||||
queryHomeSets(serviceType, new DavResource(httpClient, dav.location.resolve(href)), homeSets);
|
||||
}
|
||||
|
||||
// refresh home sets: direct group memberships
|
||||
GroupMembership groupMembership = (GroupMembership)dav.properties.get(GroupMembership.NAME);
|
||||
if (groupMembership != null)
|
||||
for (String href : groupMembership.hrefs) {
|
||||
App.log.fine("Principal is member of group " + href + ", checking for home sets");
|
||||
DavResource group = new DavResource(httpClient, dav.location.resolve(href));
|
||||
try {
|
||||
queryHomeSets(serviceType, group, homeSets);
|
||||
} catch(HttpException|DavException e) {
|
||||
App.log.log(Level.WARNING, "Couldn't query member group ", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// now refresh collections (taken from home sets)
|
||||
Map<HttpUrl, CollectionInfo> collections = readCollections();
|
||||
|
||||
// (remember selections before)
|
||||
Set<HttpUrl> selectedCollections = new HashSet<>();
|
||||
for (CollectionInfo info : collections.values())
|
||||
if (info.selected)
|
||||
selectedCollections.add(HttpUrl.parse(info.url));
|
||||
|
||||
for (Iterator<HttpUrl> itHomeSets = homeSets.iterator(); itHomeSets.hasNext(); ) {
|
||||
HttpUrl homeSet = itHomeSets.next();
|
||||
App.log.fine("Listing home set " + homeSet);
|
||||
|
||||
DavResource dav = new DavResource(httpClient, homeSet);
|
||||
try {
|
||||
dav.propfind(1, CollectionInfo.DAV_PROPERTIES);
|
||||
IteratorChain<DavResource> itCollections = new IteratorChain<>(dav.members.iterator(), new SingletonIterator(dav));
|
||||
while (itCollections.hasNext()) {
|
||||
DavResource member = itCollections.next();
|
||||
CollectionInfo info = CollectionInfo.fromDavResource(member);
|
||||
info.confirmed = true;
|
||||
App.log.log(Level.FINE, "Found collection", info);
|
||||
|
||||
if ((serviceType.equals(Services.SERVICE_CARDDAV) && info.type == CollectionInfo.Type.ADDRESS_BOOK) ||
|
||||
(serviceType.equals(Services.SERVICE_CALDAV) && info.type == CollectionInfo.Type.CALENDAR))
|
||||
collections.put(member.location, info);
|
||||
}
|
||||
} catch(HttpException e) {
|
||||
if (e.status == 403 || e.status == 404 || e.status == 410)
|
||||
// delete home set only if it was not accessible (40x)
|
||||
itHomeSets.remove();
|
||||
}
|
||||
}
|
||||
|
||||
// check/refresh unconfirmed collections
|
||||
for (Iterator<Map.Entry<HttpUrl, CollectionInfo>> iterator = collections.entrySet().iterator(); iterator.hasNext(); ) {
|
||||
Map.Entry<HttpUrl, CollectionInfo> entry = iterator.next();
|
||||
HttpUrl url = entry.getKey();
|
||||
CollectionInfo info = entry.getValue();
|
||||
|
||||
if (!info.confirmed)
|
||||
try {
|
||||
DavResource dav = new DavResource(httpClient, url);
|
||||
dav.propfind(0, CollectionInfo.DAV_PROPERTIES);
|
||||
info = CollectionInfo.fromDavResource(dav);
|
||||
info.confirmed = true;
|
||||
|
||||
// remove unusable collections
|
||||
if ((serviceType.equals(Services.SERVICE_CARDDAV) && info.type != CollectionInfo.Type.ADDRESS_BOOK) ||
|
||||
(serviceType.equals(Services.SERVICE_CALDAV) && info.type != CollectionInfo.Type.CALENDAR))
|
||||
iterator.remove();
|
||||
} catch(HttpException e) {
|
||||
if (e.status == 403 || e.status == 404 || e.status == 410)
|
||||
// delete collection only if it was not accessible (40x)
|
||||
iterator.remove();
|
||||
else
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
// restore selections
|
||||
for (HttpUrl url : selectedCollections) {
|
||||
CollectionInfo info = collections.get(url);
|
||||
if (info != null)
|
||||
info.selected = true;
|
||||
}
|
||||
|
||||
try {
|
||||
db.beginTransactionNonExclusive();
|
||||
saveHomeSets(homeSets);
|
||||
saveCollections(collections.values());
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
|
||||
} catch(InvalidAccountException e) {
|
||||
App.log.log(Level.SEVERE, "Invalid account", e);
|
||||
} catch(IOException|HttpException|DavException e) {
|
||||
App.log.log(Level.SEVERE, "Couldn't refresh collection list", e);
|
||||
|
||||
Intent debugIntent = new Intent(DavService.this, DebugInfoActivity.class);
|
||||
debugIntent.putExtra(DebugInfoActivity.KEY_THROWABLE, e);
|
||||
if (account != null)
|
||||
debugIntent.putExtra(DebugInfoActivity.KEY_ACCOUNT, account);
|
||||
|
||||
NotificationManagerCompat nm = NotificationManagerCompat.from(DavService.this);
|
||||
Notification notify = new NotificationCompat.Builder(DavService.this)
|
||||
.setSmallIcon(R.drawable.ic_error_light)
|
||||
.setLargeIcon(App.getLauncherBitmap(DavService.this))
|
||||
.setContentTitle(getString(R.string.dav_service_refresh_failed))
|
||||
.setContentText(getString(R.string.dav_service_refresh_couldnt_refresh))
|
||||
.setContentIntent(PendingIntent.getActivity(DavService.this, 0, debugIntent, PendingIntent.FLAG_UPDATE_CURRENT))
|
||||
.build();
|
||||
nm.notify(Constants.NOTIFICATION_REFRESH_COLLECTIONS, notify);
|
||||
} finally {
|
||||
dbHelper.close();
|
||||
|
||||
runningRefresh.remove(service);
|
||||
for (WeakReference<RefreshingStatusListener> ref : refreshingStatusListeners) {
|
||||
RefreshingStatusListener listener = ref.get();
|
||||
if (listener != null)
|
||||
listener.onDavRefreshStatusChanged(service, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given URL defines home sets and adds them to the home set list.
|
||||
* @param serviceType CalDAV/CardDAV (calendar home set / addressbook home set)
|
||||
* @param dav DavResource to check
|
||||
* @param homeSets set where found home set URLs will be put into
|
||||
*/
|
||||
private void queryHomeSets(String serviceType, DavResource dav, Set<HttpUrl> homeSets) throws IOException, HttpException, DavException {
|
||||
if (Services.SERVICE_CARDDAV.equals(serviceType)) {
|
||||
dav.propfind(0, AddressbookHomeSet.NAME, GroupMembership.NAME);
|
||||
AddressbookHomeSet addressbookHomeSet = (AddressbookHomeSet)dav.properties.get(AddressbookHomeSet.NAME);
|
||||
if (addressbookHomeSet != null)
|
||||
for (String href : addressbookHomeSet.hrefs)
|
||||
homeSets.add(UrlUtils.withTrailingSlash(dav.location.resolve(href)));
|
||||
} else if (Services.SERVICE_CALDAV.equals(serviceType)) {
|
||||
dav.propfind(0, CalendarHomeSet.NAME, CalendarProxyReadFor.NAME, CalendarProxyWriteFor.NAME, GroupMembership.NAME);
|
||||
CalendarHomeSet calendarHomeSet = (CalendarHomeSet)dav.properties.get(CalendarHomeSet.NAME);
|
||||
if (calendarHomeSet != null)
|
||||
for (String href : calendarHomeSet.hrefs)
|
||||
homeSets.add(UrlUtils.withTrailingSlash(dav.location.resolve(href)));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@NonNull
|
||||
private Account account() {
|
||||
@Cleanup Cursor cursor = db.query(Services._TABLE, new String[] { Services.ACCOUNT_NAME }, Services.ID + "=?", new String[] { String.valueOf(service) }, null, null, null);
|
||||
if (cursor.moveToNext()) {
|
||||
return new Account(cursor.getString(0), getString(R.string.account_type));
|
||||
} else
|
||||
throw new IllegalArgumentException("Service not found");
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private String serviceType() {
|
||||
@Cleanup Cursor cursor = db.query(Services._TABLE, new String[] { Services.SERVICE }, Services.ID + "=?", new String[] { String.valueOf(service) }, null, null, null);
|
||||
if (cursor.moveToNext())
|
||||
return cursor.getString(0);
|
||||
else
|
||||
throw new IllegalArgumentException("Service not found");
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private HttpUrl readPrincipal() {
|
||||
@Cleanup Cursor cursor = db.query(Services._TABLE, new String[] { Services.PRINCIPAL }, Services.ID + "=?", new String[] { String.valueOf(service) }, null, null, null);
|
||||
if (cursor.moveToNext()) {
|
||||
String principal = cursor.getString(0);
|
||||
if (principal != null)
|
||||
return HttpUrl.parse(cursor.getString(0));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private Set<HttpUrl> readHomeSets() {
|
||||
Set<HttpUrl> homeSets = new LinkedHashSet<>();
|
||||
@Cleanup Cursor cursor = db.query(HomeSets._TABLE, new String[] { HomeSets.URL }, HomeSets.SERVICE_ID + "=?", new String[] { String.valueOf(service) }, null, null, null);
|
||||
while (cursor.moveToNext())
|
||||
homeSets.add(HttpUrl.parse(cursor.getString(0)));
|
||||
return homeSets;
|
||||
}
|
||||
|
||||
private void saveHomeSets(Set<HttpUrl> homeSets) {
|
||||
db.delete(HomeSets._TABLE, HomeSets.SERVICE_ID + "=?", new String[] { String.valueOf(service) });
|
||||
for (HttpUrl homeSet : homeSets) {
|
||||
ContentValues values = new ContentValues(1);
|
||||
values.put(HomeSets.SERVICE_ID, service);
|
||||
values.put(HomeSets.URL, homeSet.toString());
|
||||
db.insertOrThrow(HomeSets._TABLE, null, values);
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private Map<HttpUrl, CollectionInfo> readCollections() {
|
||||
Map<HttpUrl, CollectionInfo> collections = new LinkedHashMap<>();
|
||||
@Cleanup Cursor cursor = db.query(Collections._TABLE, null, Collections.SERVICE_ID + "=?", new String[]{String.valueOf(service)}, null, null, null);
|
||||
while (cursor.moveToNext()) {
|
||||
ContentValues values = new ContentValues();
|
||||
DatabaseUtils.cursorRowToContentValues(cursor, values);
|
||||
collections.put(HttpUrl.parse(values.getAsString(Collections.URL)), CollectionInfo.fromDB(values));
|
||||
}
|
||||
return collections;
|
||||
}
|
||||
|
||||
private void saveCollections(Iterable<CollectionInfo> collections) {
|
||||
db.delete(Collections._TABLE, HomeSets.SERVICE_ID + "=?", new String[] { String.valueOf(service) });
|
||||
for (CollectionInfo collection : collections) {
|
||||
ContentValues values = collection.toDB();
|
||||
App.log.log(Level.FINE, "Saving collection", values);
|
||||
values.put(Collections.SERVICE_ID, service);
|
||||
db.insertWithOnConflict(Collections._TABLE, null, values, SQLiteDatabase.CONFLICT_REPLACE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
377
app/src/main/java/at/bitfire/davdroid/DavService.kt
Normal file
377
app/src/main/java/at/bitfire/davdroid/DavService.kt
Normal file
@@ -0,0 +1,377 @@
|
||||
/*
|
||||
* 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.Account
|
||||
import android.accounts.AccountManager
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.PendingIntent
|
||||
import android.app.Service
|
||||
import android.content.ContentValues
|
||||
import android.content.Intent
|
||||
import android.database.DatabaseUtils
|
||||
import android.database.sqlite.SQLiteDatabase
|
||||
import android.os.Binder
|
||||
import android.support.v4.app.NotificationManagerCompat
|
||||
import android.support.v7.app.NotificationCompat
|
||||
import at.bitfire.dav4android.DavResource
|
||||
import at.bitfire.dav4android.UrlUtils
|
||||
import at.bitfire.dav4android.exception.DavException
|
||||
import at.bitfire.dav4android.exception.HttpException
|
||||
import at.bitfire.dav4android.property.*
|
||||
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.ui.DebugInfoActivity
|
||||
import at.bitfire.vcard4android.ContactsStorageException
|
||||
import okhttp3.HttpUrl
|
||||
import org.apache.commons.collections4.iterators.IteratorChain
|
||||
import org.apache.commons.collections4.iterators.SingletonIterator
|
||||
import java.io.IOException
|
||||
import java.lang.ref.WeakReference
|
||||
import java.util.*
|
||||
import java.util.logging.Level
|
||||
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"
|
||||
}
|
||||
|
||||
private val runningRefresh = HashSet<Long>()
|
||||
private val refreshingStatusListeners = LinkedList<WeakReference<RefreshingStatusListener>>()
|
||||
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
intent?.let {
|
||||
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) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
|
||||
|
||||
/* BOUND SERVICE PART
|
||||
for communicating with the activities
|
||||
*/
|
||||
|
||||
interface RefreshingStatusListener {
|
||||
fun onDavRefreshStatusChanged(id: Long, refreshing: Boolean)
|
||||
}
|
||||
|
||||
private val binder = InfoBinder()
|
||||
|
||||
inner class InfoBinder: Binder() {
|
||||
fun isRefreshing(id: Long) = runningRefresh.contains(id)
|
||||
|
||||
fun addRefreshingStatusListener(listener: RefreshingStatusListener, callImmediate: Boolean) {
|
||||
refreshingStatusListeners += WeakReference<RefreshingStatusListener>(listener)
|
||||
if (callImmediate)
|
||||
runningRefresh.forEach { id -> listener.onDavRefreshStatusChanged(id, true) }
|
||||
}
|
||||
|
||||
fun removeRefreshingStatusListener(listener: RefreshingStatusListener) {
|
||||
val iter = refreshingStatusListeners.iterator()
|
||||
while (iter.hasNext()) {
|
||||
val item = iter.next().get()
|
||||
if (listener == item)
|
||||
iter.remove()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent?) = binder
|
||||
|
||||
|
||||
|
||||
/* ACTION RUNNABLES
|
||||
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(App.addressBookAccountType)
|
||||
.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)
|
||||
}
|
||||
}
|
||||
|
||||
fun refreshCollections(service: Long) {
|
||||
OpenHelper(this@DavService).use { dbHelper ->
|
||||
val db = dbHelper.writableDatabase
|
||||
|
||||
val serviceType by lazy {
|
||||
db.query(Services._TABLE, arrayOf(Services.SERVICE), "${Services.ID}=?", arrayOf(service.toString()), null, null, null)?.use { cursor ->
|
||||
if (cursor.moveToNext())
|
||||
return@lazy cursor.getString(0)
|
||||
} ?: throw IllegalArgumentException("Service not found")
|
||||
}
|
||||
|
||||
val account by lazy {
|
||||
db.query(Services._TABLE, arrayOf(Services.ACCOUNT_NAME), "${Services.ID}=?", arrayOf(service.toString()), null, null, null)?.use { cursor ->
|
||||
if (cursor.moveToNext())
|
||||
return@lazy Account(cursor.getString(0), getString(R.string.account_type))
|
||||
}
|
||||
throw IllegalArgumentException("Account not found")
|
||||
}
|
||||
|
||||
val homeSets by lazy {
|
||||
val homeSets = mutableSetOf<HttpUrl>()
|
||||
db.query(HomeSets._TABLE, arrayOf(HomeSets.URL), "${HomeSets.SERVICE_ID}=?", arrayOf(service.toString()), null, null, null)?.use { cursor ->
|
||||
while (cursor.moveToNext())
|
||||
HttpUrl.parse(cursor.getString(0))?.let { homeSets += it }
|
||||
}
|
||||
homeSets
|
||||
}
|
||||
|
||||
val collections by lazy {
|
||||
val collections = mutableMapOf<HttpUrl, CollectionInfo>()
|
||||
db.query(Collections._TABLE, null, "${Collections.SERVICE_ID}=?", arrayOf(service.toString()), null, null, null)?.use { cursor ->
|
||||
while (cursor.moveToNext()) {
|
||||
val values = ContentValues()
|
||||
DatabaseUtils.cursorRowToContentValues(cursor, values)
|
||||
values.getAsString(Collections.URL)?.let { url ->
|
||||
HttpUrl.parse(url)?.let { collections.put(it, CollectionInfo(values)) }
|
||||
}
|
||||
}
|
||||
}
|
||||
collections
|
||||
}
|
||||
|
||||
fun readPrincipal(): HttpUrl? {
|
||||
db.query(Services._TABLE, arrayOf(Services.PRINCIPAL), "${Services.ID}=?", arrayOf(service.toString()), null, null, null)?.use { cursor ->
|
||||
if (cursor.moveToNext())
|
||||
cursor.getString(0)?.let { return HttpUrl.parse(it) }
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) }
|
||||
}
|
||||
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)) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun saveHomeSets() {
|
||||
db.delete(HomeSets._TABLE, "${HomeSets.SERVICE_ID}=?", arrayOf(service.toString()))
|
||||
for (homeSet in homeSets) {
|
||||
val values = ContentValues(2)
|
||||
values.put(HomeSets.SERVICE_ID, service)
|
||||
values.put(HomeSets.URL, homeSet.toString())
|
||||
db.insertOrThrow(HomeSets._TABLE, null, values)
|
||||
}
|
||||
}
|
||||
|
||||
fun saveCollections() {
|
||||
db.delete(Collections._TABLE, "${HomeSets.SERVICE_ID}=?", arrayOf(service.toString()))
|
||||
for ((_,collection) in collections) {
|
||||
val values = collection.toDB()
|
||||
Logger.log.log(Level.FINE, "Saving collection", values)
|
||||
values.put(Collections.SERVICE_ID, service)
|
||||
db.insertWithOnConflict(Collections._TABLE, null, values, SQLiteDatabase.CONFLICT_REPLACE)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
Logger.log.info("Refreshing $serviceType collections of service #$service")
|
||||
|
||||
// create authenticating OkHttpClient (credentials taken from account settings)
|
||||
val httpClient = HttpClient.create(this@DavService, account)
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// remember selected collections
|
||||
val selectedCollections = HashSet<HttpUrl>()
|
||||
collections.values
|
||||
.filter { it.selected }
|
||||
.forEach { (url,_) -> HttpUrl.parse(url)?.let { selectedCollections.add(it) } }
|
||||
|
||||
// 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 && info.type == CollectionInfo.Type.CALENDAR))
|
||||
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
|
||||
|
||||
// remove unusable collections
|
||||
if ((serviceType == Services.SERVICE_CARDDAV && info.type != CollectionInfo.Type.ADDRESS_BOOK) ||
|
||||
(serviceType == Services.SERVICE_CALDAV && info.type != CollectionInfo.Type.CALENDAR))
|
||||
itCollections.remove()
|
||||
} 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
|
||||
}
|
||||
}
|
||||
|
||||
// restore selections
|
||||
for (url in selectedCollections)
|
||||
collections[url]?.let { it.selected = true }
|
||||
|
||||
db.beginTransactionNonExclusive()
|
||||
try {
|
||||
saveHomeSets()
|
||||
saveCollections()
|
||||
db.setTransactionSuccessful()
|
||||
} finally {
|
||||
db.endTransaction()
|
||||
}
|
||||
|
||||
} catch(e: InvalidAccountException) {
|
||||
Logger.log.log(Level.SEVERE, "Invalid account", e)
|
||||
} catch(e: Exception) {
|
||||
Logger.log.log(Level.SEVERE, "Couldn't refresh collection list", e)
|
||||
|
||||
val debugIntent = Intent(this@DavService, 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))
|
||||
.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))
|
||||
.build()
|
||||
nm.notify(Constants.NOTIFICATION_REFRESH_COLLECTIONS, notify)
|
||||
} finally {
|
||||
runningRefresh.remove(service)
|
||||
refreshingStatusListeners.forEach { it.get()?.onDavRefreshStatusChanged(service, false) }
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2013 – 2015 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.annotation.NonNull;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
import okhttp3.HttpUrl;
|
||||
|
||||
public class DavUtils {
|
||||
|
||||
public static String ARGBtoCalDAVColor(int colorWithAlpha) {
|
||||
byte alpha = (byte)(colorWithAlpha >> 24);
|
||||
int color = colorWithAlpha & 0xFFFFFF;
|
||||
return String.format("#%06X%02X", color, alpha);
|
||||
}
|
||||
|
||||
public static String lastSegmentOfUrl(@NonNull String url) {
|
||||
// the list returned by HttpUrl.pathSegments() is unmodifiable, so we have to create a copy
|
||||
List<String> segments = new LinkedList<>(HttpUrl.parse(url).pathSegments());
|
||||
Collections.reverse(segments);
|
||||
|
||||
for (String segment : segments)
|
||||
if (!StringUtils.isEmpty(segment))
|
||||
return segment;
|
||||
|
||||
return "/";
|
||||
}
|
||||
|
||||
}
|
||||
38
app/src/main/java/at/bitfire/davdroid/DavUtils.kt
Normal file
38
app/src/main/java/at/bitfire/davdroid/DavUtils.kt
Normal file
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* 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 okhttp3.HttpUrl
|
||||
import java.util.*
|
||||
|
||||
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")
|
||||
|
||||
// the list returned by HttpUrl.pathSegments() is unmodifiable, so we have to create a copy
|
||||
val segments = LinkedList<String>(httpUrl.pathSegments())
|
||||
Collections.reverse(segments)
|
||||
|
||||
for (segment in segments)
|
||||
if (segment.isNotEmpty())
|
||||
return segment
|
||||
|
||||
return "/"
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,138 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2013 – 2015 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.Account;
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.Locale;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import at.bitfire.dav4android.BasicDigestAuthHandler;
|
||||
import okhttp3.Interceptor;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.Response;
|
||||
import okhttp3.logging.HttpLoggingInterceptor;
|
||||
|
||||
public class HttpClient {
|
||||
private static final OkHttpClient client = new OkHttpClient();
|
||||
private static final UserAgentInterceptor userAgentInterceptor = new UserAgentInterceptor();
|
||||
|
||||
private static final String userAgent;
|
||||
static {
|
||||
String date = new SimpleDateFormat("yyyy/MM/dd", Locale.US).format(new Date(BuildConfig.buildTime));
|
||||
userAgent = "DAVdroid/" + BuildConfig.VERSION_NAME + " (" + date + "; dav4android; okhttp3) Android/" + Build.VERSION.RELEASE;
|
||||
}
|
||||
|
||||
private HttpClient() {
|
||||
}
|
||||
|
||||
public static OkHttpClient create(@NonNull Context context, @NonNull Account account, @NonNull final Logger logger) throws InvalidAccountException {
|
||||
OkHttpClient.Builder builder = defaultBuilder(logger);
|
||||
|
||||
// use account settings for authentication
|
||||
AccountSettings settings = new AccountSettings(context, account);
|
||||
builder = addAuthentication(builder, null, settings.username(), settings.password());
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
public static OkHttpClient create(@NonNull Logger logger) {
|
||||
return defaultBuilder(logger).build();
|
||||
}
|
||||
|
||||
public static OkHttpClient create(@NonNull Context context, @NonNull Account account) throws InvalidAccountException {
|
||||
return create(context, account, App.log);
|
||||
}
|
||||
|
||||
public static OkHttpClient create() {
|
||||
return create(App.log);
|
||||
}
|
||||
|
||||
|
||||
private static OkHttpClient.Builder defaultBuilder(@NonNull final Logger logger) {
|
||||
OkHttpClient.Builder builder = client.newBuilder();
|
||||
|
||||
// use MemorizingTrustManager to manage self-signed certificates
|
||||
if (App.getSslSocketFactoryCompat() != null && App.getCertManager() != null)
|
||||
builder.sslSocketFactory(App.getSslSocketFactoryCompat(), App.getCertManager());
|
||||
if (App.getHostnameVerifier() != null)
|
||||
builder.hostnameVerifier(App.getHostnameVerifier());
|
||||
|
||||
// set timeouts
|
||||
builder.connectTimeout(30, TimeUnit.SECONDS);
|
||||
builder.writeTimeout(30, TimeUnit.SECONDS);
|
||||
builder.readTimeout(120, TimeUnit.SECONDS);
|
||||
|
||||
// don't allow redirects, because it would break PROPFIND handling
|
||||
builder.followRedirects(false);
|
||||
|
||||
// add User-Agent to every request
|
||||
builder.addNetworkInterceptor(userAgentInterceptor);
|
||||
|
||||
// add cookie store for non-persistent cookies (some services like Horde use cookies for session tracking)
|
||||
builder.cookieJar(new MemoryCookieStore());
|
||||
|
||||
// add network logging, if requested
|
||||
if (logger.isLoggable(Level.FINEST)) {
|
||||
HttpLoggingInterceptor loggingInterceptor = new HttpLoggingInterceptor(new HttpLoggingInterceptor.Logger() {
|
||||
@Override
|
||||
public void log(String message) {
|
||||
logger.finest(message);
|
||||
}
|
||||
});
|
||||
loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
|
||||
builder.addInterceptor(loggingInterceptor);
|
||||
}
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
private static OkHttpClient.Builder addAuthentication(@NonNull OkHttpClient.Builder builder, @Nullable String host, @NonNull String username, @NonNull String password) {
|
||||
BasicDigestAuthHandler authHandler = new BasicDigestAuthHandler(host, username, password);
|
||||
return builder
|
||||
.addNetworkInterceptor(authHandler)
|
||||
.authenticator(authHandler);
|
||||
}
|
||||
|
||||
public static OkHttpClient addAuthentication(@NonNull OkHttpClient client, @NonNull String username, @NonNull String password) {
|
||||
OkHttpClient.Builder builder = client.newBuilder();
|
||||
addAuthentication(builder, null, username, password);
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
public static OkHttpClient addAuthentication(@NonNull OkHttpClient client, @NonNull String host, @NonNull String username, @NonNull String password) {
|
||||
OkHttpClient.Builder builder = client.newBuilder();
|
||||
addAuthentication(builder, host, username, password);
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
|
||||
static class UserAgentInterceptor implements Interceptor {
|
||||
@Override
|
||||
public Response intercept(Chain chain) throws IOException {
|
||||
Locale locale = Locale.getDefault();
|
||||
Request request = chain.request().newBuilder()
|
||||
.header("User-Agent", userAgent)
|
||||
.header("Accept-Language", locale.getLanguage() + "-" + locale.getCountry() + ", " + locale.getLanguage() + ";q=0.7, *;q=0.5")
|
||||
.build();
|
||||
return chain.proceed(request);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
154
app/src/main/java/at/bitfire/davdroid/HttpClient.kt
Normal file
154
app/src/main/java/at/bitfire/davdroid/HttpClient.kt
Normal file
@@ -0,0 +1,154 @@
|
||||
/*
|
||||
* 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.Account
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import at.bitfire.dav4android.BasicDigestAuthHandler
|
||||
import at.bitfire.dav4android.UrlUtils
|
||||
import at.bitfire.davdroid.log.Logger
|
||||
import at.bitfire.davdroid.model.ServiceDB
|
||||
import at.bitfire.davdroid.model.Settings
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Response
|
||||
import okhttp3.logging.HttpLoggingInterceptor
|
||||
import java.net.InetSocketAddress
|
||||
import java.net.Proxy
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.logging.Level
|
||||
|
||||
class HttpClient private constructor() {
|
||||
|
||||
companion object {
|
||||
|
||||
private val client = OkHttpClient()
|
||||
private val userAgentInterceptor = UserAgentInterceptor()
|
||||
|
||||
private val productName = when(BuildConfig.FLAVOR) {
|
||||
App.FLAVOR_ICLOUD -> "MultiSync for Cloud"
|
||||
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}"
|
||||
|
||||
|
||||
@JvmStatic
|
||||
@JvmOverloads
|
||||
fun create(context: Context?, settings: AccountSettings? = null, logger: java.util.logging.Logger = Logger.log): OkHttpClient {
|
||||
var builder = defaultBuilder(context, logger)
|
||||
|
||||
// use account settings for authentication
|
||||
settings?.let {
|
||||
val userName = it.username()
|
||||
val password = it.password()
|
||||
if (userName != null && password != null)
|
||||
builder = addAuthentication(builder, null, userName, password)
|
||||
}
|
||||
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
@Throws(InvalidAccountException::class)
|
||||
fun create(context: Context, account: Account) =
|
||||
create(context, AccountSettings(context, account))
|
||||
|
||||
|
||||
private fun defaultBuilder(context: Context?, logger: java.util.logging.Logger): OkHttpClient.Builder {
|
||||
val builder = client.newBuilder()
|
||||
|
||||
// use MemorizingTrustManager to manage self-signed certificates
|
||||
if (CustomCertificates.sslSocketFactoryCompat != null && CustomCertificates.certManager != null)
|
||||
builder.sslSocketFactory(CustomCertificates.sslSocketFactoryCompat, CustomCertificates.certManager)
|
||||
CustomCertificates.hostnameVerifier?.let { builder.hostnameVerifier(it) }
|
||||
|
||||
// set timeouts
|
||||
builder.connectTimeout(30, TimeUnit.SECONDS)
|
||||
builder.writeTimeout(30, TimeUnit.SECONDS)
|
||||
builder.readTimeout(120, TimeUnit.SECONDS)
|
||||
|
||||
// don't allow redirects, because it would break PROPFIND handling
|
||||
builder.followRedirects(false)
|
||||
|
||||
// custom proxy support
|
||||
context?.let {
|
||||
ServiceDB.OpenHelper(it).use { dbHelper ->
|
||||
try {
|
||||
val settings = Settings(dbHelper.readableDatabase)
|
||||
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)
|
||||
builder.proxy(proxy)
|
||||
Logger.log.log(Level.INFO, "Using proxy", proxy)
|
||||
}
|
||||
} catch(e: Exception) {
|
||||
Logger.log.log(Level.SEVERE, "Can't set proxy, ignoring", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// add User-Agent to every request
|
||||
builder.addNetworkInterceptor(userAgentInterceptor)
|
||||
|
||||
// add cookie store for non-persistent cookies (some services like Horde use cookies for session tracking)
|
||||
builder.cookieJar(MemoryCookieStore())
|
||||
|
||||
// add network logging, if requested
|
||||
if (logger.isLoggable(Level.FINEST)) {
|
||||
val loggingInterceptor = HttpLoggingInterceptor(HttpLoggingInterceptor.Logger {
|
||||
message -> logger.finest(message)
|
||||
})
|
||||
loggingInterceptor.level = HttpLoggingInterceptor.Level.BODY
|
||||
builder.addInterceptor(loggingInterceptor)
|
||||
}
|
||||
|
||||
return builder
|
||||
}
|
||||
|
||||
fun addAuthentication(builder: OkHttpClient.Builder, host: String?, username: String, password: String): OkHttpClient.Builder {
|
||||
val authHandler = BasicDigestAuthHandler(UrlUtils.hostToDomain(host), username, password);
|
||||
return builder
|
||||
.addNetworkInterceptor(authHandler)
|
||||
.authenticator(authHandler)
|
||||
}
|
||||
|
||||
@JvmOverloads
|
||||
@JvmStatic
|
||||
fun addAuthentication(client: OkHttpClient, host: String? = null, username: String, password: String): OkHttpClient {
|
||||
val builder = client.newBuilder()
|
||||
addAuthentication(builder, host, username, password)
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
private class UserAgentInterceptor: Interceptor {
|
||||
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
val locale = Locale.getDefault()
|
||||
val request = chain.request().newBuilder()
|
||||
.header("User-Agent", userAgent)
|
||||
.header("Accept-Language", "${locale.language}-${locale.country}, ${locale.language};q=0.7, *;q=0.5")
|
||||
.build()
|
||||
return chain.proceed(request)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2013 – 2016 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.Account;
|
||||
|
||||
public class InvalidAccountException extends Exception {
|
||||
|
||||
public InvalidAccountException(Account account) {
|
||||
super("Invalid account: " + account);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
/*
|
||||
* Copyright © Ricki Hirner (bitfire web engineering).
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the GNU Public License v3.0
|
||||
* which accompanies this distribution, and is available at
|
||||
* http://www.gnu.org/licenses/gpl.html
|
||||
*/
|
||||
|
||||
package at.bitfire.davdroid
|
||||
|
||||
import android.accounts.Account
|
||||
|
||||
class InvalidAccountException(account: Account): Exception("Invalid account: $account")
|
||||
@@ -1,69 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2013 – 2016 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.apache.commons.collections4.MapIterator;
|
||||
import org.apache.commons.collections4.keyvalue.MultiKey;
|
||||
import org.apache.commons.collections4.map.HashedMap;
|
||||
import org.apache.commons.collections4.map.MultiKeyMap;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
import okhttp3.Cookie;
|
||||
import okhttp3.CookieJar;
|
||||
import okhttp3.HttpUrl;
|
||||
|
||||
/**
|
||||
* Primitive cookie store that stores cookies in a (volatile) hash map.
|
||||
* Will be sufficient for session cookies.
|
||||
*/
|
||||
public class MemoryCookieStore implements CookieJar {
|
||||
|
||||
/**
|
||||
* Stored cookies. The multi-key consists of three parts: name, domain, and path.
|
||||
* This ensures that cookies can be overwritten. [RFC 6265 5.3 Storage Model]
|
||||
* Not thread-safe!
|
||||
*/
|
||||
protected final MultiKeyMap<String, Cookie> storage = MultiKeyMap.multiKeyMap(new HashedMap<MultiKey<? extends String>, Cookie>());
|
||||
|
||||
@Override
|
||||
public void saveFromResponse(HttpUrl url, List<Cookie> cookies) {
|
||||
synchronized(storage) {
|
||||
for (Cookie cookie : cookies)
|
||||
storage.put(cookie.name(), cookie.domain(), cookie.path(), cookie);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Cookie> loadForRequest(HttpUrl url) {
|
||||
List<Cookie> cookies = new LinkedList<>();
|
||||
|
||||
synchronized(storage) {
|
||||
MapIterator<MultiKey<? extends String>, Cookie> iter = storage.mapIterator();
|
||||
while (iter.hasNext()) {
|
||||
iter.next();
|
||||
Cookie cookie = iter.getValue();
|
||||
|
||||
// remove expired cookies
|
||||
if (cookie.expiresAt() <= System.currentTimeMillis()) {
|
||||
iter.remove();
|
||||
continue;
|
||||
}
|
||||
|
||||
// add applicable cookies
|
||||
if (cookie.matches(url))
|
||||
cookies.add(cookie);
|
||||
}
|
||||
}
|
||||
|
||||
return cookies;
|
||||
}
|
||||
|
||||
}
|
||||
63
app/src/main/java/at/bitfire/davdroid/MemoryCookieStore.kt
Normal file
63
app/src/main/java/at/bitfire/davdroid/MemoryCookieStore.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
|
||||
|
||||
import okhttp3.Cookie
|
||||
import okhttp3.CookieJar
|
||||
import okhttp3.HttpUrl
|
||||
import org.apache.commons.collections4.keyvalue.MultiKey
|
||||
import org.apache.commons.collections4.map.HashedMap
|
||||
import org.apache.commons.collections4.map.MultiKeyMap
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Primitive cookie store that stores cookies in a (volatile) hash map.
|
||||
* Will be sufficient for session cookies.
|
||||
*/
|
||||
class MemoryCookieStore: CookieJar {
|
||||
|
||||
/**
|
||||
* Stored cookies. The multi-key consists of three parts: name, domain, and path.
|
||||
* 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>())!!
|
||||
|
||||
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) {
|
||||
synchronized(storage) {
|
||||
for (cookie in cookies)
|
||||
storage.put(cookie.name(), cookie.domain(), cookie.path(), cookie)
|
||||
}
|
||||
}
|
||||
|
||||
override fun loadForRequest(url: HttpUrl): List<Cookie> {
|
||||
val cookies = LinkedList<Cookie>()
|
||||
|
||||
synchronized(storage) {
|
||||
val iter = storage.mapIterator()
|
||||
while (iter.hasNext()) {
|
||||
iter.next()
|
||||
val cookie = iter.value
|
||||
|
||||
// remove expired cookies
|
||||
if (cookie.expiresAt() <= System.currentTimeMillis()) {
|
||||
iter.remove()
|
||||
continue
|
||||
}
|
||||
|
||||
// add applicable cookies
|
||||
if (cookie.matches(url))
|
||||
cookies += cookie
|
||||
}
|
||||
}
|
||||
|
||||
return cookies
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* 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.Account
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import at.bitfire.davdroid.log.Logger
|
||||
import at.bitfire.davdroid.model.ServiceDB
|
||||
import at.bitfire.davdroid.model.ServiceDB.Services
|
||||
import at.bitfire.davdroid.resource.LocalTaskList
|
||||
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")
|
||||
|
||||
// check all accounts and (de)activate OpenTasks if a CalDAV service is defined
|
||||
ServiceDB.OpenHelper(context).use { dbHelper ->
|
||||
val db = dbHelper.readableDatabase
|
||||
|
||||
db.query(Services._TABLE, arrayOf(Services.ACCOUNT_NAME),
|
||||
"${Services.SERVICE}=?", arrayOf(Services.SERVICE_CALDAV), null, null, null)?.use { cursor ->
|
||||
while (cursor.moveToNext()) {
|
||||
val account = Account(cursor.getString(0), context.getString(R.string.account_type))
|
||||
|
||||
if (tasksInstalled) {
|
||||
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())
|
||||
}
|
||||
} else
|
||||
ContentResolver.setIsSyncable(account, TaskProvider.ProviderName.OpenTasks.authority, 0)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,170 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2013 – 2015 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.os.Build;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.InetAddress;
|
||||
import java.net.Socket;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.SSLSocket;
|
||||
import javax.net.ssl.SSLSocketFactory;
|
||||
import javax.net.ssl.X509TrustManager;
|
||||
|
||||
import lombok.Cleanup;
|
||||
|
||||
public class SSLSocketFactoryCompat extends SSLSocketFactory {
|
||||
|
||||
private SSLSocketFactory delegate;
|
||||
|
||||
// Android 5.0+ (API level21) provides reasonable default settings
|
||||
// but it still allows SSLv3
|
||||
// https://developer.android.com/reference/javax/net/ssl/SSLSocket.html
|
||||
static String protocols[] = null, cipherSuites[] = null;
|
||||
static {
|
||||
try {
|
||||
@Cleanup SSLSocket socket = (SSLSocket)SSLSocketFactory.getDefault().createSocket();
|
||||
if (socket != null) {
|
||||
/* 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
|
||||
List<String> protocols = new LinkedList<>();
|
||||
for (String protocol : socket.getSupportedProtocols())
|
||||
if (!protocol.toUpperCase(Locale.US).contains("SSL"))
|
||||
protocols.add(protocol);
|
||||
App.log.info("Setting allowed TLS protocols: " + TextUtils.join(", ", protocols));
|
||||
SSLSocketFactoryCompat.protocols = protocols.toArray(new String[protocols.size()]);
|
||||
|
||||
/* set up reasonable cipher suites */
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
||||
// choose known secure cipher suites
|
||||
List<String> allowedCiphers = Arrays.asList(
|
||||
// TLS 1.2
|
||||
"TLS_RSA_WITH_AES_256_GCM_SHA384",
|
||||
"TLS_RSA_WITH_AES_128_GCM_SHA256",
|
||||
"TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256",
|
||||
"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
|
||||
"TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
|
||||
"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256",
|
||||
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
|
||||
// maximum interoperability
|
||||
"TLS_RSA_WITH_3DES_EDE_CBC_SHA",
|
||||
"TLS_RSA_WITH_AES_128_CBC_SHA",
|
||||
// additionally
|
||||
"TLS_RSA_WITH_AES_256_CBC_SHA",
|
||||
"TLS_ECDHE_ECDSA_WITH_3DES_EDE_CBC_SHA",
|
||||
"TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA",
|
||||
"TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA",
|
||||
"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA");
|
||||
List<String> availableCiphers = Arrays.asList(socket.getSupportedCipherSuites());
|
||||
App.log.info("Available cipher suites: " + TextUtils.join(", ", availableCiphers));
|
||||
App.log.info("Cipher suites enabled by default: " + TextUtils.join(", ", socket.getEnabledCipherSuites()));
|
||||
|
||||
// take all allowed ciphers that are available and put them into preferredCiphers
|
||||
HashSet<String> preferredCiphers = new HashSet<>(allowedCiphers);
|
||||
preferredCiphers.retainAll(availableCiphers);
|
||||
|
||||
/* For maximum security, preferredCiphers should *replace* enabled ciphers (thus disabling
|
||||
* ciphers which are enabled by default, but have become unsecure), but I guess for
|
||||
* the security level of DAVdroid and maximum compatibility, disabling of insecure
|
||||
* ciphers should be a server-side task */
|
||||
|
||||
// add preferred ciphers to enabled ciphers
|
||||
HashSet<String> enabledCiphers = preferredCiphers;
|
||||
enabledCiphers.addAll(new HashSet<>(Arrays.asList(socket.getEnabledCipherSuites())));
|
||||
|
||||
App.log.info("Enabling (only) those TLS ciphers: " + TextUtils.join(", ", enabledCiphers));
|
||||
SSLSocketFactoryCompat.cipherSuites = enabledCiphers.toArray(new String[enabledCiphers.size()]);
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
App.log.severe("Couldn't determine default TLS settings");
|
||||
}
|
||||
}
|
||||
|
||||
public SSLSocketFactoryCompat(@NonNull X509TrustManager trustManager) {
|
||||
try {
|
||||
SSLContext sslContext = SSLContext.getInstance("TLS");
|
||||
sslContext.init(null, new X509TrustManager[] { trustManager }, null);
|
||||
delegate = sslContext.getSocketFactory();
|
||||
} catch (GeneralSecurityException e) {
|
||||
throw new AssertionError(); // The system has no TLS. Just give up.
|
||||
}
|
||||
}
|
||||
|
||||
private void upgradeTLS(SSLSocket ssl) {
|
||||
if (protocols != null)
|
||||
ssl.setEnabledProtocols(protocols);
|
||||
|
||||
if (cipherSuites != null)
|
||||
ssl.setEnabledCipherSuites(cipherSuites);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String[] getDefaultCipherSuites() {
|
||||
return cipherSuites;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getSupportedCipherSuites() {
|
||||
return cipherSuites;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Socket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException {
|
||||
Socket ssl = delegate.createSocket(s, host, port, autoClose);
|
||||
if (ssl instanceof SSLSocket)
|
||||
upgradeTLS((SSLSocket)ssl);
|
||||
return ssl;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Socket createSocket(String host, int port) throws IOException {
|
||||
Socket ssl = delegate.createSocket(host, port);
|
||||
if (ssl instanceof SSLSocket)
|
||||
upgradeTLS((SSLSocket)ssl);
|
||||
return ssl;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException {
|
||||
Socket ssl = delegate.createSocket(host, port, localHost, localPort);
|
||||
if (ssl instanceof SSLSocket)
|
||||
upgradeTLS((SSLSocket)ssl);
|
||||
return ssl;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Socket createSocket(InetAddress host, int port) throws IOException {
|
||||
Socket ssl = delegate.createSocket(host, port);
|
||||
if (ssl instanceof SSLSocket)
|
||||
upgradeTLS((SSLSocket)ssl);
|
||||
return ssl;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException {
|
||||
Socket ssl = delegate.createSocket(address, port, localAddress, localPort);
|
||||
if (ssl instanceof SSLSocket)
|
||||
upgradeTLS((SSLSocket)ssl);
|
||||
return ssl;
|
||||
}
|
||||
|
||||
}
|
||||
160
app/src/main/java/at/bitfire/davdroid/SSLSocketFactoryCompat.kt
Normal file
160
app/src/main/java/at/bitfire/davdroid/SSLSocketFactoryCompat.kt
Normal file
@@ -0,0 +1,160 @@
|
||||
/*
|
||||
* 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.os.Build
|
||||
import at.bitfire.davdroid.log.Logger
|
||||
import java.io.IOException
|
||||
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
|
||||
|
||||
class SSLSocketFactoryCompat(trustManager: X509TrustManager): SSLSocketFactory() {
|
||||
|
||||
private var delegate: SSLSocketFactory
|
||||
|
||||
companion object {
|
||||
// Android 5.0+ (API level 21) provides reasonable default settings
|
||||
// but it still allows SSLv3
|
||||
// https://developer.android.com/reference/javax/net/ssl/SSLSocket.html
|
||||
var protocols: Array<String>? = null
|
||||
var cipherSuites: Array<String>? = null
|
||||
init {
|
||||
if (Build.VERSION.SDK_INT >= 23) {
|
||||
// Since Android 6.0 (API level 23),
|
||||
// - TLSv1.1 and TLSv1.2 is enabled by default
|
||||
// - SSLv3 is disabled by default
|
||||
// - all modern ciphers are activated by default
|
||||
protocols = null
|
||||
cipherSuites = null
|
||||
Logger.log.fine("Using device default TLS protocols/ciphers")
|
||||
} else {
|
||||
val socket = SSLSocketFactory.getDefault().createSocket() as SSLSocket?
|
||||
try {
|
||||
socket?.let {
|
||||
/* 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
|
||||
val _protocols = LinkedList<String>()
|
||||
for (protocol in socket.supportedProtocols.filterNot { it.contains("SSL", true) })
|
||||
_protocols += protocol
|
||||
Logger.log.info("Enabling (only) these TLS protocols: ${_protocols.joinToString(", ")}")
|
||||
protocols = _protocols.toTypedArray()
|
||||
|
||||
/* set up reasonable cipher suites */
|
||||
val knownCiphers = arrayOf<String>(
|
||||
// TLS 1.2
|
||||
"TLS_RSA_WITH_AES_256_GCM_SHA384",
|
||||
"TLS_RSA_WITH_AES_128_GCM_SHA256",
|
||||
"TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256",
|
||||
"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
|
||||
"TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
|
||||
"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256",
|
||||
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
|
||||
// maximum interoperability
|
||||
"TLS_RSA_WITH_3DES_EDE_CBC_SHA",
|
||||
"SSL_RSA_WITH_3DES_EDE_CBC_SHA",
|
||||
"TLS_RSA_WITH_AES_128_CBC_SHA",
|
||||
// additionally
|
||||
"TLS_RSA_WITH_AES_256_CBC_SHA",
|
||||
"TLS_ECDHE_ECDSA_WITH_3DES_EDE_CBC_SHA",
|
||||
"TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA",
|
||||
"TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA",
|
||||
"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA"
|
||||
)
|
||||
val availableCiphers = socket.supportedCipherSuites
|
||||
Logger.log.info("Available cipher suites: ${availableCiphers.joinToString(", ")}")
|
||||
|
||||
/* For maximum security, preferredCiphers should *replace* enabled ciphers (thus
|
||||
* disabling ciphers which are enabled by default, but have become unsecure), but for
|
||||
* the security level of DAVdroid and maximum compatibility, disabling of insecure
|
||||
* ciphers should be a server-side task */
|
||||
|
||||
// for the final set of enabled ciphers, take the ciphers enabled by default, ...
|
||||
val _cipherSuites = LinkedList<String>()
|
||||
_cipherSuites.addAll(socket.enabledCipherSuites)
|
||||
Logger.log.fine("Cipher suites enabled by default: ${_cipherSuites.joinToString(", ")}")
|
||||
// ... add explicitly allowed ciphers ...
|
||||
_cipherSuites.addAll(knownCiphers)
|
||||
// ... and keep only those which are actually available
|
||||
_cipherSuites.retainAll(availableCiphers)
|
||||
|
||||
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")
|
||||
} finally {
|
||||
socket?.close() // doesn't implement Closeable on all supported Android versions
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
init {
|
||||
try {
|
||||
val sslContext = SSLContext.getInstance("TLS")
|
||||
sslContext.init(null, arrayOf(trustManager), null)
|
||||
delegate = sslContext.socketFactory
|
||||
} catch (e: GeneralSecurityException) {
|
||||
throw IllegalStateException() // system has no TLS
|
||||
}
|
||||
}
|
||||
|
||||
override fun getDefaultCipherSuites(): Array<String>? = cipherSuites ?: delegate.defaultCipherSuites
|
||||
override fun getSupportedCipherSuites(): Array<String>? = cipherSuites ?: delegate.supportedCipherSuites
|
||||
|
||||
override fun createSocket(s: Socket, host: String, port: Int, autoClose: Boolean): Socket {
|
||||
val ssl = delegate.createSocket(s, host, port, autoClose)
|
||||
if (ssl is SSLSocket)
|
||||
upgradeTLS(ssl)
|
||||
return ssl
|
||||
}
|
||||
|
||||
override fun createSocket(host: String, port: Int): Socket {
|
||||
val ssl = delegate.createSocket(host, port)
|
||||
if (ssl is SSLSocket)
|
||||
upgradeTLS(ssl)
|
||||
return ssl
|
||||
}
|
||||
|
||||
override fun createSocket(host: String, port: Int, localHost: InetAddress, localPort: Int): Socket {
|
||||
val ssl = delegate.createSocket(host, port, localHost, localPort)
|
||||
if (ssl is SSLSocket)
|
||||
upgradeTLS(ssl)
|
||||
return ssl
|
||||
}
|
||||
|
||||
override fun createSocket(host: InetAddress, port: Int): Socket {
|
||||
val ssl = delegate.createSocket(host, port)
|
||||
if (ssl is SSLSocket)
|
||||
upgradeTLS(ssl)
|
||||
return ssl
|
||||
}
|
||||
|
||||
override fun createSocket(address: InetAddress, port: Int, localAddress: InetAddress, localPort: Int): Socket {
|
||||
val ssl = delegate.createSocket(address, port, localAddress, localPort)
|
||||
if (ssl is SSLSocket)
|
||||
upgradeTLS(ssl)
|
||||
return ssl
|
||||
}
|
||||
|
||||
|
||||
private fun upgradeTLS(ssl: SSLSocket) {
|
||||
protocols?.let { ssl.enabledProtocols = it }
|
||||
cipherSuites?.let { ssl.enabledCipherSuites = it }
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2013 – 2016 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.log;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import org.apache.commons.lang3.math.NumberUtils;
|
||||
|
||||
import java.util.logging.Handler;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.LogRecord;
|
||||
|
||||
public class LogcatHandler extends Handler {
|
||||
private static final int MAX_LINE_LENGTH = 3000;
|
||||
public static final LogcatHandler INSTANCE = new LogcatHandler();
|
||||
|
||||
private LogcatHandler() {
|
||||
super();
|
||||
setFormatter(PlainTextFormatter.LOGCAT);
|
||||
setLevel(Level.ALL);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void publish(LogRecord r) {
|
||||
String text = getFormatter().format(r);
|
||||
int level = r.getLevel().intValue();
|
||||
|
||||
int end = text.length();
|
||||
for (int pos = 0; pos < end; pos += MAX_LINE_LENGTH) {
|
||||
String line = text.substring(pos, NumberUtils.min(pos + MAX_LINE_LENGTH, end));
|
||||
|
||||
if (level >= Level.SEVERE.intValue())
|
||||
Log.e(r.getLoggerName(), line);
|
||||
else if (level >= Level.WARNING.intValue())
|
||||
Log.w(r.getLoggerName(), line);
|
||||
else if (level >= Level.CONFIG.intValue())
|
||||
Log.i(r.getLoggerName(), line);
|
||||
else if (level >= Level.FINER.intValue())
|
||||
Log.d(r.getLoggerName(), line);
|
||||
else
|
||||
Log.v(r.getLoggerName(), line);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void flush() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
}
|
||||
|
||||
}
|
||||
50
app/src/main/java/at/bitfire/davdroid/log/LogcatHandler.kt
Normal file
50
app/src/main/java/at/bitfire/davdroid/log/LogcatHandler.kt
Normal file
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* 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.log
|
||||
|
||||
import android.util.Log
|
||||
|
||||
import org.apache.commons.lang3.math.NumberUtils
|
||||
|
||||
import java.util.logging.Handler
|
||||
import java.util.logging.Level
|
||||
import java.util.logging.LogRecord
|
||||
|
||||
object LogcatHandler: Handler() {
|
||||
|
||||
private val MAX_LINE_LENGTH = 3000
|
||||
|
||||
init {
|
||||
formatter = PlainTextFormatter.LOGCAT
|
||||
level = Level.ALL
|
||||
}
|
||||
|
||||
override fun publish(r: LogRecord) {
|
||||
val text = formatter.format(r)
|
||||
val level = r.level.intValue()
|
||||
|
||||
val end = text.length
|
||||
var pos = 0
|
||||
while (pos < end) {
|
||||
val line = text.substring(pos, NumberUtils.min(pos + MAX_LINE_LENGTH, end))
|
||||
when {
|
||||
level >= Level.SEVERE.intValue() -> Log.e(r.loggerName, line)
|
||||
level >= Level.WARNING.intValue() -> Log.w(r.loggerName, line)
|
||||
level >= Level.CONFIG.intValue() -> Log.i(r.loggerName, line)
|
||||
level >= Level.FINER.intValue() -> Log.d(r.loggerName, line)
|
||||
else -> Log.v(r.loggerName, line)
|
||||
}
|
||||
pos += MAX_LINE_LENGTH
|
||||
}
|
||||
}
|
||||
|
||||
override fun flush() {}
|
||||
override fun close() {}
|
||||
|
||||
}
|
||||
104
app/src/main/java/at/bitfire/davdroid/log/Logger.kt
Normal file
104
app/src/main/java/at/bitfire/davdroid/log/Logger.kt
Normal file
@@ -0,0 +1,104 @@
|
||||
/*
|
||||
* 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.log
|
||||
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Process
|
||||
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.model.ServiceDB
|
||||
import at.bitfire.davdroid.model.Settings
|
||||
import at.bitfire.davdroid.ui.AppSettingsActivity
|
||||
import org.apache.commons.lang3.time.DateFormatUtils
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.util.logging.FileHandler
|
||||
import java.util.logging.Level
|
||||
import java.util.logging.Logger
|
||||
|
||||
object Logger {
|
||||
|
||||
@JvmField
|
||||
val log = Logger.getLogger("davdroid")!!
|
||||
init {
|
||||
at.bitfire.dav4android.Constants.log = Logger.getLogger("davdroid.dav4android")
|
||||
at.bitfire.cert4android.Constants.log = Logger.getLogger("davdroid.cert4android")
|
||||
}
|
||||
|
||||
fun reinitLogger(context: Context) {
|
||||
ServiceDB.OpenHelper(context).use { dbHelper ->
|
||||
val settings = Settings(dbHelper.getReadableDatabase())
|
||||
|
||||
val logToFile = settings.getBoolean(App.LOG_TO_EXTERNAL_STORAGE, false)
|
||||
val logVerbose = logToFile || Log.isLoggable(log.name, Log.DEBUG)
|
||||
|
||||
log.info("Verbose logging: $logVerbose")
|
||||
|
||||
// set logging level according to preferences
|
||||
val rootLogger = Logger.getLogger("")
|
||||
rootLogger.level = if (logVerbose) Level.ALL else Level.INFO
|
||||
|
||||
// remove all handlers and add our own logcat handler
|
||||
rootLogger.useParentHandlers = false
|
||||
rootLogger.handlers.forEach { rootLogger.removeHandler(it) }
|
||||
rootLogger.addHandler(LogcatHandler)
|
||||
|
||||
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))
|
||||
.setContentTitle(context.getString(R.string.logging_davdroid_file_logging))
|
||||
.setLocalOnly(true)
|
||||
|
||||
val dir = context.getExternalFilesDir(null)
|
||||
if (dir != null)
|
||||
try {
|
||||
val fileName = File(dir, "davdroid-${Process.myPid()}-${DateFormatUtils.format(System.currentTimeMillis(), "yyyyMMdd-HHmmss")}.txt").toString()
|
||||
log.info("Logging to $fileName")
|
||||
|
||||
val fileHandler = FileHandler(fileName)
|
||||
fileHandler.formatter = PlainTextFormatter.DEFAULT
|
||||
log.addHandler(fileHandler)
|
||||
|
||||
val prefIntent = Intent(context, AppSettingsActivity::class.java)
|
||||
prefIntent.putExtra(AppSettingsActivity.EXTRA_SCROLL_TO, "log_to_external_storage")
|
||||
|
||||
builder .setContentText(dir.path)
|
||||
.setSubText(context.getString(R.string.logging_to_external_storage_warning))
|
||||
.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)))
|
||||
.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))
|
||||
.setCategory(NotificationCompat.CATEGORY_ERROR)
|
||||
}
|
||||
else
|
||||
builder.setContentText(context.getString(R.string.logging_no_external_storage))
|
||||
|
||||
nm.notify(Constants.NOTIFICATION_EXTERNAL_FILE_LOGGING, builder.build())
|
||||
} else
|
||||
nm.cancel(Constants.NOTIFICATION_EXTERNAL_FILE_LOGGING)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2013 – 2016 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.log;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.lang3.exception.ExceptionUtils;
|
||||
import org.apache.commons.lang3.time.DateFormatUtils;
|
||||
|
||||
import java.util.logging.Formatter;
|
||||
import java.util.logging.LogRecord;
|
||||
|
||||
public class PlainTextFormatter extends Formatter {
|
||||
public final static PlainTextFormatter
|
||||
LOGCAT = new PlainTextFormatter(true),
|
||||
DEFAULT = new PlainTextFormatter(false);
|
||||
|
||||
private final boolean logcat;
|
||||
|
||||
private PlainTextFormatter(boolean onLogcat) {
|
||||
this.logcat = onLogcat;
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("ThrowableResultOfMethodCallIgnored")
|
||||
public String format(LogRecord r) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
|
||||
if (!logcat)
|
||||
builder .append(DateFormatUtils.format(r.getMillis(), "yyyy-MM-dd HH:mm:ss"))
|
||||
.append(" ").append(r.getThreadID()).append(" ");
|
||||
|
||||
builder.append(String.format("[%s] %s", shortClassName(r.getSourceClassName()), r.getMessage()));
|
||||
|
||||
if (r.getThrown() != null)
|
||||
builder .append("\nEXCEPTION ")
|
||||
.append(ExceptionUtils.getStackTrace(r.getThrown()));
|
||||
|
||||
if (r.getParameters() != null) {
|
||||
int idx = 1;
|
||||
for (Object param : r.getParameters())
|
||||
builder.append("\n\tPARAMETER #").append(idx++).append(" = ").append(param);
|
||||
}
|
||||
|
||||
if (!logcat)
|
||||
builder.append("\n");
|
||||
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
private String shortClassName(String className) {
|
||||
String s = StringUtils.replace(className, "at.bitfire.davdroid.", "");
|
||||
return StringUtils.replace(s, "at.bitfire.", "");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* Copyright © Ricki Hirner (bitfire web engineering).
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the GNU Public License v3.0
|
||||
* which accompanies this distribution, and is available at
|
||||
* http://www.gnu.org/licenses/gpl.html
|
||||
*/
|
||||
|
||||
package at.bitfire.davdroid.log
|
||||
|
||||
import org.apache.commons.lang3.exception.ExceptionUtils
|
||||
import org.apache.commons.lang3.time.DateFormatUtils
|
||||
import java.util.logging.Formatter
|
||||
import java.util.logging.LogRecord
|
||||
|
||||
class PlainTextFormatter private constructor(
|
||||
val logcat: Boolean
|
||||
): Formatter() {
|
||||
|
||||
companion object {
|
||||
@JvmField val LOGCAT = PlainTextFormatter(true)
|
||||
@JvmField val DEFAULT = PlainTextFormatter(false)
|
||||
}
|
||||
|
||||
override fun format(r: LogRecord): String {
|
||||
val builder = StringBuilder()
|
||||
|
||||
if (!logcat)
|
||||
builder .append(DateFormatUtils.format(r.millis, "yyyy-MM-dd HH:mm:ss"))
|
||||
.append(" ").append(r.threadID).append(" ")
|
||||
|
||||
if (r.sourceClassName.replaceFirst("\\$.*", "") != r.loggerName) {
|
||||
val className = shortClassName(r.sourceClassName)
|
||||
if (className != "ical4android.AndroidAppender")
|
||||
builder.append("[").append(className).append("] ")
|
||||
}
|
||||
|
||||
builder.append(r.message)
|
||||
|
||||
r.thrown?.let {
|
||||
builder .append("\nEXCEPTION ")
|
||||
.append(ExceptionUtils.getStackTrace(it))
|
||||
}
|
||||
|
||||
r.parameters?.let {
|
||||
for ((idx, param) in it.withIndex())
|
||||
builder.append("\n\tPARAMETER #").append(idx).append(" = ").append(param)
|
||||
}
|
||||
|
||||
if (!logcat)
|
||||
builder.append("\n")
|
||||
|
||||
return builder.toString()
|
||||
}
|
||||
|
||||
private fun shortClassName(className: String) = className
|
||||
.replace("at.bitfire.davdroid.", "")
|
||||
.replace("at.bitfire.", "")
|
||||
.replace(Regex("\\$.*$"), "")
|
||||
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2013 – 2016 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.log;
|
||||
|
||||
import java.util.logging.Handler;
|
||||
import java.util.logging.LogRecord;
|
||||
|
||||
public class StringHandler extends Handler {
|
||||
|
||||
StringBuilder builder = new StringBuilder();
|
||||
|
||||
public StringHandler() {
|
||||
super();
|
||||
setFormatter(PlainTextFormatter.DEFAULT);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void publish(LogRecord record) {
|
||||
builder.append(getFormatter().format(record));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void flush() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return builder.toString();
|
||||
}
|
||||
}
|
||||
31
app/src/main/java/at/bitfire/davdroid/log/StringHandler.kt
Normal file
31
app/src/main/java/at/bitfire/davdroid/log/StringHandler.kt
Normal file
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* 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.log
|
||||
|
||||
import java.util.logging.Handler;
|
||||
import java.util.logging.LogRecord;
|
||||
|
||||
class StringHandler: Handler() {
|
||||
|
||||
val builder = StringBuilder()
|
||||
|
||||
init {
|
||||
formatter = PlainTextFormatter.DEFAULT
|
||||
}
|
||||
|
||||
override fun publish(record: LogRecord) {
|
||||
builder.append(formatter.format(record))
|
||||
}
|
||||
|
||||
override fun flush() {}
|
||||
override fun close() {}
|
||||
|
||||
override fun toString() = builder.toString()
|
||||
|
||||
}
|
||||
@@ -1,163 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2013 – 2016 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.apache.commons.lang3.StringUtils;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
import at.bitfire.dav4android.DavResource;
|
||||
import at.bitfire.dav4android.Property;
|
||||
import at.bitfire.dav4android.property.AddressbookDescription;
|
||||
import at.bitfire.dav4android.property.CalendarColor;
|
||||
import at.bitfire.dav4android.property.CalendarDescription;
|
||||
import at.bitfire.dav4android.property.CalendarTimezone;
|
||||
import at.bitfire.dav4android.property.CurrentUserPrivilegeSet;
|
||||
import at.bitfire.dav4android.property.DisplayName;
|
||||
import at.bitfire.dav4android.property.ResourceType;
|
||||
import at.bitfire.dav4android.property.SupportedAddressData;
|
||||
import at.bitfire.dav4android.property.SupportedCalendarComponentSet;
|
||||
import at.bitfire.davdroid.model.ServiceDB.Collections;
|
||||
import lombok.ToString;
|
||||
|
||||
@ToString
|
||||
public class CollectionInfo implements Serializable {
|
||||
public long id;
|
||||
public Long serviceID;
|
||||
|
||||
public enum Type {
|
||||
ADDRESS_BOOK,
|
||||
CALENDAR
|
||||
}
|
||||
public Type type;
|
||||
|
||||
public String url;
|
||||
|
||||
public boolean readOnly;
|
||||
public String displayName, description;
|
||||
public Integer color;
|
||||
|
||||
public String timeZone;
|
||||
public Boolean supportsVEVENT;
|
||||
public Boolean supportsVTODO;
|
||||
|
||||
public boolean selected;
|
||||
|
||||
// non-persistent properties
|
||||
public boolean confirmed;
|
||||
|
||||
|
||||
public static final Property.Name[] DAV_PROPERTIES = {
|
||||
ResourceType.NAME,
|
||||
CurrentUserPrivilegeSet.NAME,
|
||||
DisplayName.NAME,
|
||||
AddressbookDescription.NAME, SupportedAddressData.NAME,
|
||||
CalendarDescription.NAME, CalendarColor.NAME, SupportedCalendarComponentSet.NAME
|
||||
};
|
||||
|
||||
public static CollectionInfo fromDavResource(DavResource dav) {
|
||||
CollectionInfo info = new CollectionInfo();
|
||||
info.url = dav.location.toString();
|
||||
|
||||
ResourceType type = (ResourceType)dav.properties.get(ResourceType.NAME);
|
||||
if (type != null) {
|
||||
if (type.types.contains(ResourceType.ADDRESSBOOK))
|
||||
info.type = Type.ADDRESS_BOOK;
|
||||
else if (type.types.contains(ResourceType.CALENDAR))
|
||||
info.type = Type.CALENDAR;
|
||||
}
|
||||
|
||||
info.readOnly = false;
|
||||
CurrentUserPrivilegeSet privilegeSet = (CurrentUserPrivilegeSet)dav.properties.get(CurrentUserPrivilegeSet.NAME);
|
||||
if (privilegeSet != null)
|
||||
info.readOnly = !privilegeSet.mayWriteContent;
|
||||
|
||||
DisplayName displayName = (DisplayName)dav.properties.get(DisplayName.NAME);
|
||||
if (displayName != null && !StringUtils.isEmpty(displayName.displayName))
|
||||
info.displayName = displayName.displayName;
|
||||
|
||||
if (info.type == Type.ADDRESS_BOOK) {
|
||||
AddressbookDescription addressbookDescription = (AddressbookDescription)dav.properties.get(AddressbookDescription.NAME);
|
||||
if (addressbookDescription != null)
|
||||
info.description = addressbookDescription.description;
|
||||
|
||||
} else if (info.type == Type.CALENDAR) {
|
||||
CalendarDescription calendarDescription = (CalendarDescription)dav.properties.get(CalendarDescription.NAME);
|
||||
if (calendarDescription != null)
|
||||
info.description = calendarDescription.description;
|
||||
|
||||
CalendarColor calendarColor = (CalendarColor)dav.properties.get(CalendarColor.NAME);
|
||||
if (calendarColor != null)
|
||||
info.color = calendarColor.color;
|
||||
|
||||
CalendarTimezone timeZone = (CalendarTimezone)dav.properties.get(CalendarTimezone.NAME);
|
||||
if (timeZone != null)
|
||||
info.timeZone = timeZone.vTimeZone;
|
||||
|
||||
info.supportsVEVENT = info.supportsVTODO = true;
|
||||
SupportedCalendarComponentSet supportedCalendarComponentSet = (SupportedCalendarComponentSet)dav.properties.get(SupportedCalendarComponentSet.NAME);
|
||||
if (supportedCalendarComponentSet != null) {
|
||||
info.supportsVEVENT = supportedCalendarComponentSet.supportsEvents;
|
||||
info.supportsVTODO = supportedCalendarComponentSet.supportsTasks;
|
||||
}
|
||||
}
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
|
||||
public static CollectionInfo fromDB(ContentValues values) {
|
||||
CollectionInfo info = new CollectionInfo();
|
||||
info.id = values.getAsLong(Collections.ID);
|
||||
info.serviceID = values.getAsLong(Collections.SERVICE_ID);
|
||||
|
||||
info.url = values.getAsString(Collections.URL);
|
||||
info.readOnly = values.getAsInteger(Collections.READ_ONLY) != 0;
|
||||
info.displayName = values.getAsString(Collections.DISPLAY_NAME);
|
||||
info.description = values.getAsString(Collections.DESCRIPTION);
|
||||
|
||||
info.color = values.getAsInteger(Collections.COLOR);
|
||||
|
||||
info.timeZone = values.getAsString(Collections.TIME_ZONE);
|
||||
info.supportsVEVENT = getAsBooleanOrNull(values, Collections.SUPPORTS_VEVENT);
|
||||
info.supportsVTODO = getAsBooleanOrNull(values, Collections.SUPPORTS_VTODO);
|
||||
|
||||
info.selected = values.getAsInteger(Collections.SYNC) != 0;
|
||||
return info;
|
||||
}
|
||||
|
||||
public ContentValues toDB() {
|
||||
ContentValues values = new ContentValues();
|
||||
// Collections.SERVICE_ID is never changed
|
||||
|
||||
values.put(Collections.URL, url);
|
||||
values.put(Collections.READ_ONLY, readOnly ? 1 : 0);
|
||||
values.put(Collections.DISPLAY_NAME, displayName);
|
||||
values.put(Collections.DESCRIPTION, description);
|
||||
values.put(Collections.COLOR, color);
|
||||
|
||||
values.put(Collections.TIME_ZONE, timeZone);
|
||||
if (supportsVEVENT != null)
|
||||
values.put(Collections.SUPPORTS_VEVENT, supportsVEVENT ? 1 : 0);
|
||||
if (supportsVTODO != null)
|
||||
values.put(Collections.SUPPORTS_VTODO, supportsVTODO ? 1 : 0);
|
||||
|
||||
values.put(Collections.SYNC, selected ? 1 : 0);
|
||||
return values;
|
||||
}
|
||||
|
||||
|
||||
private static Boolean getAsBooleanOrNull(ContentValues values, String field) {
|
||||
Integer i = values.getAsInteger(field);
|
||||
return (i == null) ? null : (i != 0);
|
||||
}
|
||||
|
||||
}
|
||||
140
app/src/main/java/at/bitfire/davdroid/model/CollectionInfo.kt
Normal file
140
app/src/main/java/at/bitfire/davdroid/model/CollectionInfo.kt
Normal file
@@ -0,0 +1,140 @@
|
||||
/*
|
||||
* 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 at.bitfire.dav4android.DavResource
|
||||
import at.bitfire.dav4android.property.*
|
||||
import at.bitfire.davdroid.model.ServiceDB.Collections
|
||||
import java.io.Serializable
|
||||
|
||||
data class CollectionInfo @JvmOverloads constructor(
|
||||
val url: String,
|
||||
|
||||
var id: Long? = null,
|
||||
var serviceID: Long? = null,
|
||||
|
||||
var type: Type? = null,
|
||||
|
||||
var readOnly: Boolean = false,
|
||||
var displayName: String? = null,
|
||||
var description: String? = null,
|
||||
var color: Int? = null,
|
||||
|
||||
var timeZone: String? = null,
|
||||
var supportsVEVENT: Boolean = false,
|
||||
var supportsVTODO: Boolean = false,
|
||||
|
||||
var selected: Boolean = false,
|
||||
|
||||
// non-persistent properties
|
||||
var confirmed: Boolean = false
|
||||
): Serializable {
|
||||
|
||||
enum class Type {
|
||||
ADDRESS_BOOK,
|
||||
CALENDAR
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@JvmField
|
||||
val DAV_PROPERTIES = arrayOf(
|
||||
ResourceType.NAME,
|
||||
CurrentUserPrivilegeSet.NAME,
|
||||
DisplayName.NAME,
|
||||
AddressbookDescription.NAME, SupportedAddressData.NAME,
|
||||
CalendarDescription.NAME, CalendarColor.NAME, SupportedCalendarComponentSet.NAME
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
|
||||
constructor(dav: DavResource): this(dav.location.toString()) {
|
||||
(dav.properties[ResourceType.NAME] as ResourceType?)?.let { type ->
|
||||
when {
|
||||
type.types.contains(ResourceType.ADDRESSBOOK) -> this.type = Type.ADDRESS_BOOK
|
||||
type.types.contains(ResourceType.CALENDAR) -> this.type = Type.CALENDAR
|
||||
}
|
||||
}
|
||||
|
||||
(dav.properties[CurrentUserPrivilegeSet.NAME] as CurrentUserPrivilegeSet?)?.let { privilegeSet ->
|
||||
readOnly = !privilegeSet.mayWriteContent
|
||||
}
|
||||
|
||||
(dav.properties[DisplayName.NAME] as DisplayName?)?.let {
|
||||
if (!it.displayName.isNullOrEmpty())
|
||||
displayName = it.displayName
|
||||
}
|
||||
|
||||
when (type) {
|
||||
Type.ADDRESS_BOOK -> {
|
||||
(dav.properties[AddressbookDescription.NAME] as AddressbookDescription?)?.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 }
|
||||
|
||||
supportsVEVENT = true
|
||||
supportsVTODO = true
|
||||
(dav.properties[SupportedCalendarComponentSet.NAME] as SupportedCalendarComponentSet?)?.let {
|
||||
supportsVEVENT = it.supportsEvents
|
||||
supportsVTODO = it.supportsTasks
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
constructor(values: ContentValues): this(values.getAsString(Collections.URL)) {
|
||||
id = values.getAsLong(Collections.ID)
|
||||
serviceID = values.getAsLong(Collections.SERVICE_ID)
|
||||
|
||||
readOnly = values.getAsInteger(Collections.READ_ONLY) != 0
|
||||
displayName = values.getAsString(Collections.DISPLAY_NAME)
|
||||
description = values.getAsString(Collections.DESCRIPTION)
|
||||
|
||||
color = values.getAsInteger(Collections.COLOR)
|
||||
|
||||
timeZone = values.getAsString(Collections.TIME_ZONE)
|
||||
supportsVEVENT = getAsBooleanOrNull(values, Collections.SUPPORTS_VEVENT) ?: false
|
||||
supportsVTODO = getAsBooleanOrNull(values, Collections.SUPPORTS_VTODO) ?: false
|
||||
|
||||
selected = values.getAsInteger(Collections.SYNC) != 0
|
||||
}
|
||||
|
||||
fun toDB(): ContentValues {
|
||||
val values = ContentValues()
|
||||
// Collections.SERVICE_ID is never changed
|
||||
|
||||
values.put(Collections.URL, url)
|
||||
values.put(Collections.READ_ONLY, if (readOnly) 1 else 0)
|
||||
values.put(Collections.DISPLAY_NAME, displayName)
|
||||
values.put(Collections.DESCRIPTION, description)
|
||||
values.put(Collections.COLOR, color)
|
||||
|
||||
values.put(Collections.TIME_ZONE, timeZone)
|
||||
values.put(Collections.SUPPORTS_VEVENT, if (supportsVEVENT) 1 else 0)
|
||||
values.put(Collections.SUPPORTS_VTODO, if (supportsVTODO) 1 else 0)
|
||||
|
||||
values.put(Collections.SYNC, if (selected) 1 else 0)
|
||||
return values
|
||||
}
|
||||
|
||||
|
||||
private fun getAsBooleanOrNull(values: ContentValues, field: String): Boolean? {
|
||||
val i = values.getAsInteger(field)
|
||||
return if (i == null)
|
||||
null
|
||||
else
|
||||
(i != 0)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,186 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2013 – 2016 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.Context;
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteException;
|
||||
import android.database.sqlite.SQLiteOpenHelper;
|
||||
import android.os.Build;
|
||||
|
||||
import at.bitfire.davdroid.App;
|
||||
import lombok.Cleanup;
|
||||
|
||||
public class ServiceDB {
|
||||
|
||||
public static class Settings {
|
||||
public static final String
|
||||
_TABLE = "settings",
|
||||
NAME = "setting",
|
||||
VALUE = "value";
|
||||
}
|
||||
|
||||
public static class Services {
|
||||
public static final String
|
||||
_TABLE = "services",
|
||||
ID = "_id",
|
||||
ACCOUNT_NAME = "accountName",
|
||||
SERVICE = "service",
|
||||
PRINCIPAL = "principal";
|
||||
|
||||
// allowed values for SERVICE column
|
||||
public static final String
|
||||
SERVICE_CALDAV = "caldav",
|
||||
SERVICE_CARDDAV = "carddav";
|
||||
}
|
||||
|
||||
public static class HomeSets {
|
||||
public static final String
|
||||
_TABLE = "homesets",
|
||||
ID = "_id",
|
||||
SERVICE_ID = "serviceID",
|
||||
URL = "url";
|
||||
}
|
||||
|
||||
public static class Collections {
|
||||
public static final String
|
||||
_TABLE = "collections",
|
||||
ID = "_id",
|
||||
SERVICE_ID = "serviceID",
|
||||
URL = "url",
|
||||
READ_ONLY = "readOnly",
|
||||
DISPLAY_NAME = "displayName",
|
||||
DESCRIPTION = "description",
|
||||
COLOR = "color",
|
||||
TIME_ZONE = "timezone",
|
||||
SUPPORTS_VEVENT = "supportsVEVENT",
|
||||
SUPPORTS_VTODO = "supportsVTODO",
|
||||
SYNC = "sync";
|
||||
}
|
||||
|
||||
|
||||
public static class OpenHelper extends SQLiteOpenHelper {
|
||||
private static final String DATABASE_NAME = "services.db";
|
||||
private static final int DATABASE_VERSION = 1;
|
||||
|
||||
public OpenHelper(Context context) {
|
||||
super(context, DATABASE_NAME, null, DATABASE_VERSION);
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN)
|
||||
setWriteAheadLoggingEnabled(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onOpen(SQLiteDatabase db) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN)
|
||||
db.setForeignKeyConstraintsEnabled(true);
|
||||
else {
|
||||
if (!db.enableWriteAheadLogging())
|
||||
App.log.warning("Couldn't enable write-ahead logging");
|
||||
|
||||
db.execSQL("PRAGMA foreign_keys=ON;");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(SQLiteDatabase db) {
|
||||
App.log.info("Creating database " + db.getPath());
|
||||
|
||||
db.execSQL("CREATE TABLE " + Settings._TABLE + "(" +
|
||||
Settings.NAME + " TEXT NOT NULL," +
|
||||
Settings.VALUE + " TEXT NOT NULL" +
|
||||
")");
|
||||
db.execSQL("CREATE UNIQUE INDEX settings_name ON " + Settings._TABLE + " (" + Settings.NAME + ")");
|
||||
|
||||
db.execSQL("CREATE TABLE " + Services._TABLE + "(" +
|
||||
Services.ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
|
||||
Services.ACCOUNT_NAME + " TEXT NOT NULL," +
|
||||
Services.SERVICE + " TEXT NOT NULL," +
|
||||
Services.PRINCIPAL + " TEXT NULL" +
|
||||
")");
|
||||
db.execSQL("CREATE UNIQUE INDEX services_account ON " + Services._TABLE + " (" + Services.ACCOUNT_NAME + "," + Services.SERVICE + ")");
|
||||
|
||||
db.execSQL("CREATE TABLE " + HomeSets._TABLE + "(" +
|
||||
HomeSets.ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
|
||||
HomeSets.SERVICE_ID + " INTEGER NOT NULL REFERENCES " + Services._TABLE +" ON DELETE CASCADE," +
|
||||
HomeSets.URL + " TEXT NOT NULL" +
|
||||
")");
|
||||
db.execSQL("CREATE UNIQUE INDEX homesets_service_url ON " + HomeSets._TABLE + "(" + HomeSets.SERVICE_ID + "," + HomeSets.URL + ")");
|
||||
|
||||
db.execSQL("CREATE TABLE " + Collections._TABLE + "(" +
|
||||
Collections.ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
|
||||
Collections.SERVICE_ID + " INTEGER NOT NULL REFERENCES " + Services._TABLE +" ON DELETE CASCADE," +
|
||||
Collections.URL + " TEXT NOT NULL," +
|
||||
Collections.READ_ONLY + " INTEGER DEFAULT 0 NOT NULL," +
|
||||
Collections.DISPLAY_NAME + " TEXT NULL," +
|
||||
Collections.DESCRIPTION + " TEXT NULL," +
|
||||
Collections.COLOR + " INTEGER NULL," +
|
||||
Collections.TIME_ZONE + " TEXT NULL," +
|
||||
Collections.SUPPORTS_VEVENT + " INTEGER NULL," +
|
||||
Collections.SUPPORTS_VTODO + " INTEGER NULL," +
|
||||
Collections.SYNC + " INTEGER DEFAULT 0 NOT NULL" +
|
||||
")");
|
||||
db.execSQL("CREATE UNIQUE INDEX collections_service_url ON " + Collections._TABLE + "(" + Collections.SERVICE_ID + "," + Collections.URL + ")");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
|
||||
}
|
||||
|
||||
|
||||
public void dump(StringBuilder sb) {
|
||||
SQLiteDatabase db = getReadableDatabase();
|
||||
db.beginTransactionNonExclusive();
|
||||
|
||||
// iterate through all tables
|
||||
@Cleanup Cursor cursorTables = db.query("sqlite_master", new String[] { "name" }, "type='table'", null, null, null, null);
|
||||
while (cursorTables.moveToNext()) {
|
||||
String table = cursorTables.getString(0);
|
||||
sb.append(table).append("\n");
|
||||
@Cleanup Cursor cursor = db.query(table, null, null, null, null, null, null);
|
||||
|
||||
// print columns
|
||||
int cols = cursor.getColumnCount();
|
||||
sb.append("\t| ");
|
||||
for (int i = 0; i < cols; i++) {
|
||||
sb.append(" ");
|
||||
sb.append(cursor.getColumnName(i));
|
||||
sb.append(" |");
|
||||
}
|
||||
sb.append("\n");
|
||||
|
||||
// print rows
|
||||
while (cursor.moveToNext()) {
|
||||
sb.append("\t| ");
|
||||
for (int i = 0; i < cols; i++) {
|
||||
sb.append(" ");
|
||||
try {
|
||||
String value = cursor.getString(i);
|
||||
if (value != null)
|
||||
sb.append(value
|
||||
.replace("\r", "<CR>")
|
||||
.replace("\n", "<LF>"));
|
||||
else
|
||||
sb.append("<null>");
|
||||
|
||||
} catch (SQLiteException e) {
|
||||
sb.append("<unprintable>");
|
||||
}
|
||||
sb.append(" |");
|
||||
}
|
||||
sb.append("\n");
|
||||
}
|
||||
sb.append("----------\n");
|
||||
}
|
||||
db.endTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
176
app/src/main/java/at/bitfire/davdroid/model/ServiceDB.kt
Normal file
176
app/src/main/java/at/bitfire/davdroid/model/ServiceDB.kt
Normal file
@@ -0,0 +1,176 @@
|
||||
/*
|
||||
* 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.content.Context
|
||||
import android.database.sqlite.SQLiteDatabase
|
||||
import android.database.sqlite.SQLiteException
|
||||
import android.database.sqlite.SQLiteOpenHelper
|
||||
import at.bitfire.davdroid.log.Logger
|
||||
import java.io.Closeable
|
||||
|
||||
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"
|
||||
|
||||
// allowed values for SERVICE column
|
||||
@JvmField val SERVICE_CALDAV = "caldav"
|
||||
@JvmField val SERVICE_CARDDAV = "carddav"
|
||||
}
|
||||
|
||||
object HomeSets {
|
||||
@JvmField val _TABLE = "homesets"
|
||||
@JvmField val ID = "_id"
|
||||
@JvmField val SERVICE_ID = "serviceID"
|
||||
@JvmField val URL = "url"
|
||||
}
|
||||
|
||||
object Collections {
|
||||
@JvmField val _TABLE = "collections"
|
||||
@JvmField val ID = "_id"
|
||||
@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 SYNC = "sync"
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@JvmStatic
|
||||
fun onRenameAccount(db: SQLiteDatabase, oldName: String, newName: String) {
|
||||
val values = ContentValues(1)
|
||||
values.put(Services.ACCOUNT_NAME, newName)
|
||||
db.updateWithOnConflict(Services._TABLE, values, Services.ACCOUNT_NAME + "=?", arrayOf(oldName), SQLiteDatabase.CONFLICT_REPLACE)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
class OpenHelper(
|
||||
context: Context
|
||||
): SQLiteOpenHelper(context, DATABASE_NAME, null, DATABASE_VERSION), Closeable {
|
||||
|
||||
companion object {
|
||||
val DATABASE_NAME = "services.db"
|
||||
val DATABASE_VERSION = 1
|
||||
}
|
||||
|
||||
override fun onConfigure(db: SQLiteDatabase) {
|
||||
setWriteAheadLoggingEnabled(true)
|
||||
db.setForeignKeyConstraintsEnabled(true)
|
||||
}
|
||||
|
||||
override fun onCreate(db: SQLiteDatabase) {
|
||||
Logger.log.info("Creating database " + db.path)
|
||||
|
||||
db.execSQL("CREATE TABLE ${Settings._TABLE}(" +
|
||||
"${Settings.NAME} TEXT NOT NULL," +
|
||||
"${Settings.VALUE} TEXT NOT NULL)")
|
||||
db.execSQL("CREATE UNIQUE INDEX settings_name ON ${Settings._TABLE} (${Settings.NAME})")
|
||||
|
||||
db.execSQL("CREATE TABLE ${Services._TABLE}(" +
|
||||
"${Services.ID} INTEGER PRIMARY KEY AUTOINCREMENT," +
|
||||
"${Services.ACCOUNT_NAME} TEXT NOT NULL," +
|
||||
"${Services.SERVICE} TEXT NOT NULL," +
|
||||
"${Services.PRINCIPAL} TEXT NULL)")
|
||||
db.execSQL("CREATE UNIQUE INDEX services_account ON ${Services._TABLE} (${Services.ACCOUNT_NAME},${Services.SERVICE})")
|
||||
|
||||
db.execSQL("CREATE TABLE ${HomeSets._TABLE}(" +
|
||||
"${HomeSets.ID} INTEGER PRIMARY KEY AUTOINCREMENT," +
|
||||
"${HomeSets.SERVICE_ID} INTEGER NOT NULL REFERENCES ${Services._TABLE} ON DELETE CASCADE," +
|
||||
"${HomeSets.URL} TEXT NOT NULL)")
|
||||
db.execSQL("CREATE UNIQUE INDEX homesets_service_url ON ${HomeSets._TABLE}(${HomeSets.SERVICE_ID},${HomeSets.URL})")
|
||||
|
||||
db.execSQL("CREATE TABLE ${Collections._TABLE}(" +
|
||||
"${Collections.ID} INTEGER PRIMARY KEY AUTOINCREMENT," +
|
||||
"${Collections.SERVICE_ID} INTEGER NOT NULL REFERENCES ${Services._TABLE} ON DELETE CASCADE," +
|
||||
"${Collections.URL} TEXT NOT NULL," +
|
||||
"${Collections.READ_ONLY} INTEGER DEFAULT 0 NOT NULL," +
|
||||
"${Collections.DISPLAY_NAME} TEXT NULL," +
|
||||
"${Collections.DESCRIPTION} TEXT NULL," +
|
||||
"${Collections.COLOR} INTEGER NULL," +
|
||||
"${Collections.TIME_ZONE} TEXT NULL," +
|
||||
"${Collections.SUPPORTS_VEVENT} INTEGER NULL," +
|
||||
"${Collections.SUPPORTS_VTODO} INTEGER NULL," +
|
||||
"${Collections.SYNC} INTEGER DEFAULT 0 NOT NULL)")
|
||||
db.execSQL("CREATE UNIQUE INDEX collections_service_url ON ${Collections._TABLE}(${Collections.SERVICE_ID},${Collections.URL})")
|
||||
}
|
||||
|
||||
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
|
||||
// no different versions yet
|
||||
}
|
||||
|
||||
|
||||
fun dump(sb: StringBuilder) {
|
||||
val db = readableDatabase
|
||||
db.beginTransactionNonExclusive()
|
||||
|
||||
// iterate through all tables
|
||||
db.query("sqlite_master", arrayOf("name"), "type='table'", null, null, null, null).use { cursorTables ->
|
||||
while (cursorTables.moveToNext()) {
|
||||
val table = cursorTables.getString(0)
|
||||
sb.append(table).append("\n")
|
||||
db.query(table, null, null, null, null, null, null).use { cursor ->
|
||||
// print columns
|
||||
val cols = cursor.columnCount
|
||||
sb.append("\t| ")
|
||||
for (i in 0 .. cols-1)
|
||||
sb .append(" ")
|
||||
.append(cursor.getColumnName(i))
|
||||
.append(" |")
|
||||
sb.append("\n")
|
||||
|
||||
// print rows
|
||||
while (cursor.moveToNext()) {
|
||||
sb.append("\t| ")
|
||||
for (i in 0 .. cols-1) {
|
||||
sb.append(" ")
|
||||
try {
|
||||
val value = cursor.getString(i)
|
||||
if (value != null)
|
||||
sb.append(value
|
||||
.replace("\r", "<CR>")
|
||||
.replace("\n", "<LF>"))
|
||||
else
|
||||
sb.append("<null>")
|
||||
|
||||
} catch (e: SQLiteException) {
|
||||
sb.append("<unprintable>")
|
||||
}
|
||||
sb.append(" |")
|
||||
}
|
||||
sb.append("\n")
|
||||
}
|
||||
sb.append("----------\n")
|
||||
}
|
||||
}
|
||||
db.endTransaction()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2013 – 2016 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.database.Cursor;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
|
||||
import lombok.Cleanup;
|
||||
|
||||
public class Settings {
|
||||
|
||||
final SQLiteDatabase db;
|
||||
|
||||
public Settings(SQLiteDatabase db) {
|
||||
this.db = db;
|
||||
}
|
||||
|
||||
public boolean getBoolean(String name, boolean defaultValue) {
|
||||
@Cleanup Cursor cursor = db.query(ServiceDB.Settings._TABLE, new String[] { ServiceDB.Settings.VALUE },
|
||||
ServiceDB.Settings.NAME + "=?", new String[] { name }, null, null, null);
|
||||
if (cursor.moveToNext())
|
||||
return cursor.getInt(0) != 0;
|
||||
else
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
public void putBoolean(String name, boolean value) {
|
||||
ContentValues values = new ContentValues(2);
|
||||
values.put(ServiceDB.Settings.NAME, name);
|
||||
values.put(ServiceDB.Settings.VALUE, value ? 1 : 0);
|
||||
db.insertWithOnConflict(ServiceDB.Settings._TABLE, null, values, SQLiteDatabase.CONFLICT_REPLACE);
|
||||
}
|
||||
|
||||
public void remove(String name) {
|
||||
db.delete(ServiceDB.Settings._TABLE, ServiceDB.Settings.NAME + "=?", new String[] { name });
|
||||
}
|
||||
|
||||
}
|
||||
94
app/src/main/java/at/bitfire/davdroid/model/Settings.kt
Normal file
94
app/src/main/java/at/bitfire/davdroid/model/Settings.kt
Normal file
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
* 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.BroadcastReceiver
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.database.sqlite.SQLiteDatabase
|
||||
import at.bitfire.davdroid.CustomCertificates
|
||||
import at.bitfire.davdroid.log.Logger
|
||||
|
||||
class Settings(
|
||||
val db: SQLiteDatabase
|
||||
) {
|
||||
|
||||
fun getBoolean(name: String, defaultValue: Boolean): Boolean {
|
||||
db.query(ServiceDB.Settings._TABLE, arrayOf(ServiceDB.Settings.VALUE),
|
||||
"${ServiceDB.Settings.NAME}=?", arrayOf(name), null, null, null)?.use { cursor ->
|
||||
if (cursor.moveToNext() && !cursor.isNull(0))
|
||||
return cursor.getInt(0) != 0
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
fun putBoolean(name: String, value: Boolean) {
|
||||
val values = ContentValues(2)
|
||||
values.put(ServiceDB.Settings.NAME, name)
|
||||
values.put(ServiceDB.Settings.VALUE, if (value) 1 else 0)
|
||||
db.insertWithOnConflict(ServiceDB.Settings._TABLE, null, values, SQLiteDatabase.CONFLICT_REPLACE)
|
||||
}
|
||||
|
||||
|
||||
fun getInt(name: String, defaultValue: Int): Int {
|
||||
db.query(ServiceDB.Settings._TABLE, arrayOf(ServiceDB.Settings.VALUE),
|
||||
"${ServiceDB.Settings.NAME}=?", arrayOf(name), null, null, null)?.use { cursor ->
|
||||
if (cursor.moveToNext() && !cursor.isNull(0))
|
||||
return if (cursor.isNull(0)) defaultValue else cursor.getInt(0)
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
fun putInt(name: String, value: Int?) {
|
||||
val values = ContentValues(2)
|
||||
values.put(ServiceDB.Settings.NAME, name)
|
||||
values.put(ServiceDB.Settings.VALUE, value)
|
||||
db.insertWithOnConflict(ServiceDB.Settings._TABLE, null, values, SQLiteDatabase.CONFLICT_REPLACE)
|
||||
}
|
||||
|
||||
|
||||
fun getString(name: String, defaultValue: String?): String? {
|
||||
db.query(ServiceDB.Settings._TABLE, arrayOf(ServiceDB.Settings.VALUE),
|
||||
"${ServiceDB.Settings.NAME}=?", arrayOf(name), null, null, null)?.use { cursor ->
|
||||
if (cursor.moveToNext())
|
||||
return cursor.getString(0)
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
fun putString(name: String, value: String?) {
|
||||
val values = ContentValues(2)
|
||||
values.put(ServiceDB.Settings.NAME, name)
|
||||
values.put(ServiceDB.Settings.VALUE, value)
|
||||
db.insertWithOnConflict(ServiceDB.Settings._TABLE, null, values, SQLiteDatabase.CONFLICT_REPLACE)
|
||||
}
|
||||
|
||||
|
||||
fun remove(name: String) {
|
||||
db.delete(ServiceDB.Settings._TABLE, "${ServiceDB.Settings.NAME}=?", arrayOf(name))
|
||||
}
|
||||
|
||||
|
||||
class ReinitSettingsReceiver: BroadcastReceiver() {
|
||||
|
||||
companion object {
|
||||
@JvmField val ACTION_REINIT_SETTINGS = "at.bitfire.davdroid.REINIT_SETTINGS"
|
||||
}
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
Logger.log.info("Received broadcast: re-initializing settings (logger/cert manager)")
|
||||
|
||||
CustomCertificates.reinitCertManager(context)
|
||||
Logger.reinitLogger(context)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2013 – 2016 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.provider.ContactsContract.RawContacts;
|
||||
|
||||
public class UnknownProperties {
|
||||
|
||||
public static final String CONTENT_ITEM_TYPE = "x.davdroid/unknown-properties";
|
||||
|
||||
public static final String
|
||||
MIMETYPE = RawContacts.Data.MIMETYPE,
|
||||
RAW_CONTACT_ID = RawContacts.Data.RAW_CONTACT_ID,
|
||||
UNKNOWN_PROPERTIES = RawContacts.Data.DATA1;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* 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.provider.ContactsContract.RawContacts;
|
||||
|
||||
object UnknownProperties {
|
||||
|
||||
@JvmField
|
||||
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
|
||||
|
||||
}
|
||||
@@ -1,229 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2013 – 2015 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 android.accounts.Account;
|
||||
import android.content.ContentProviderClient;
|
||||
import android.content.ContentUris;
|
||||
import android.content.ContentValues;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.Parcel;
|
||||
import android.os.RemoteException;
|
||||
import android.provider.ContactsContract.Groups;
|
||||
import android.provider.ContactsContract.RawContacts;
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
import at.bitfire.vcard4android.AndroidAddressBook;
|
||||
import at.bitfire.vcard4android.AndroidContact;
|
||||
import at.bitfire.vcard4android.AndroidGroup;
|
||||
import at.bitfire.vcard4android.ContactsStorageException;
|
||||
import lombok.Cleanup;
|
||||
|
||||
|
||||
public class LocalAddressBook extends AndroidAddressBook implements LocalCollection {
|
||||
|
||||
protected static final String
|
||||
SYNC_STATE_CTAG = "ctag",
|
||||
SYNC_STATE_URL = "url";
|
||||
|
||||
private final Bundle syncState = new Bundle();
|
||||
|
||||
/**
|
||||
* Whether contact groups (LocalGroup resources) are included in query results for
|
||||
* {@link #getAll()}, {@link #getDeleted()}, {@link #getDirty()} and
|
||||
* {@link #getWithoutFileName()}.
|
||||
*/
|
||||
public boolean includeGroups = true;
|
||||
|
||||
|
||||
public LocalAddressBook(Account account, ContentProviderClient provider) {
|
||||
super(account, provider, LocalGroup.Factory.INSTANCE, LocalContact.Factory.INSTANCE);
|
||||
}
|
||||
|
||||
public LocalContact findContactByUID(String uid) throws ContactsStorageException, FileNotFoundException {
|
||||
LocalContact[] contacts = (LocalContact[])queryContacts(LocalContact.COLUMN_UID + "=?", new String[] { uid });
|
||||
if (contacts.length == 0)
|
||||
throw new FileNotFoundException();
|
||||
return contacts[0];
|
||||
}
|
||||
|
||||
@Override
|
||||
public LocalResource[] getAll() throws ContactsStorageException {
|
||||
List<LocalResource> all = new LinkedList<>();
|
||||
Collections.addAll(all, (LocalResource[])queryContacts(null, null));
|
||||
if (includeGroups)
|
||||
Collections.addAll(all, (LocalResource[])queryGroups(null, null));
|
||||
return all.toArray(new LocalResource[all.size()]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of local contacts/groups which have been deleted locally. (DELETED != 0).
|
||||
*/
|
||||
@Override
|
||||
public LocalResource[] getDeleted() throws ContactsStorageException {
|
||||
List<LocalResource> deleted = new LinkedList<>();
|
||||
Collections.addAll(deleted, getDeletedContacts());
|
||||
if (includeGroups)
|
||||
Collections.addAll(deleted, getDeletedGroups());
|
||||
return deleted.toArray(new LocalResource[deleted.size()]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of local contacts/groups which have been changed locally (DIRTY != 0).
|
||||
*/
|
||||
@Override
|
||||
public LocalResource[] getDirty() throws ContactsStorageException {
|
||||
List<LocalResource> dirty = new LinkedList<>();
|
||||
Collections.addAll(dirty, getDirtyContacts());
|
||||
if (includeGroups)
|
||||
Collections.addAll(dirty, getDirtyGroups());
|
||||
return dirty.toArray(new LocalResource[dirty.size()]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of local contacts which don't have a file name yet.
|
||||
*/
|
||||
@Override
|
||||
public LocalResource[] getWithoutFileName() throws ContactsStorageException {
|
||||
List<LocalResource> nameless = new LinkedList<>();
|
||||
Collections.addAll(nameless, (LocalContact[])queryContacts(AndroidContact.COLUMN_FILENAME + " IS NULL", null));
|
||||
if (includeGroups)
|
||||
Collections.addAll(nameless, (LocalGroup[])queryGroups(AndroidGroup.COLUMN_FILENAME + " IS NULL", null));
|
||||
return nameless.toArray(new LocalResource[nameless.size()]);
|
||||
}
|
||||
|
||||
public void deleteAll() throws ContactsStorageException {
|
||||
try {
|
||||
provider.delete(syncAdapterURI(RawContacts.CONTENT_URI), null, null);
|
||||
provider.delete(syncAdapterURI(Groups.CONTENT_URI), null, null);
|
||||
} catch(RemoteException e) {
|
||||
throw new ContactsStorageException("Couldn't delete all local contacts and groups", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public LocalContact[] getDeletedContacts() throws ContactsStorageException {
|
||||
return (LocalContact[])queryContacts(RawContacts.DELETED + "!= 0", null);
|
||||
}
|
||||
|
||||
public LocalContact[] getDirtyContacts() throws ContactsStorageException {
|
||||
return (LocalContact[])queryContacts(RawContacts.DIRTY + "!= 0", null);
|
||||
}
|
||||
|
||||
public LocalGroup[] getDeletedGroups() throws ContactsStorageException {
|
||||
return (LocalGroup[])queryGroups(Groups.DELETED + "!= 0", null);
|
||||
}
|
||||
|
||||
public LocalGroup[] getDirtyGroups() throws ContactsStorageException {
|
||||
return (LocalGroup[])queryGroups(Groups.DIRTY + "!= 0", null);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
public long findOrCreateGroup(@NonNull String title) throws ContactsStorageException {
|
||||
try {
|
||||
@Cleanup Cursor cursor = provider.query(syncAdapterURI(Groups.CONTENT_URI),
|
||||
new String[] { Groups._ID },
|
||||
Groups.TITLE + "=?", new String[] { title },
|
||||
null);
|
||||
if (cursor != null && cursor.moveToNext())
|
||||
return cursor.getLong(0);
|
||||
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(Groups.TITLE, title);
|
||||
Uri uri = provider.insert(syncAdapterURI(Groups.CONTENT_URI), values);
|
||||
return ContentUris.parseId(uri);
|
||||
} catch(RemoteException e) {
|
||||
throw new ContactsStorageException("Couldn't find local contact group", e);
|
||||
}
|
||||
}
|
||||
|
||||
public void removeEmptyGroups() throws ContactsStorageException {
|
||||
// find groups without members
|
||||
/** should be done using {@link Groups.SUMMARY_COUNT}, but it's not implemented in Android yet */
|
||||
for (LocalGroup group : (LocalGroup[])queryGroups(null, null))
|
||||
if (group.getMembers().length == 0)
|
||||
group.delete();
|
||||
}
|
||||
|
||||
public void removeGroups() throws ContactsStorageException {
|
||||
try {
|
||||
provider.delete(syncAdapterURI(Groups.CONTENT_URI), null, null);
|
||||
} catch(RemoteException e) {
|
||||
throw new ContactsStorageException("Couldn't remove all groups", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// SYNC STATE
|
||||
|
||||
@SuppressWarnings("ParcelClassLoader,Recycle")
|
||||
protected void readSyncState() throws ContactsStorageException {
|
||||
@Cleanup("recycle") Parcel parcel = Parcel.obtain();
|
||||
byte[] raw = getSyncState();
|
||||
syncState.clear();
|
||||
if (raw != null) {
|
||||
parcel.unmarshall(raw, 0, raw.length);
|
||||
parcel.setDataPosition(0);
|
||||
syncState.putAll(parcel.readBundle());
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("Recycle")
|
||||
protected void writeSyncState() throws ContactsStorageException {
|
||||
@Cleanup("recycle") Parcel parcel = Parcel.obtain();
|
||||
parcel.writeBundle(syncState);
|
||||
setSyncState(parcel.marshall());
|
||||
}
|
||||
|
||||
public String getURL() throws ContactsStorageException {
|
||||
synchronized (syncState) {
|
||||
readSyncState();
|
||||
return syncState.getString(SYNC_STATE_URL);
|
||||
}
|
||||
}
|
||||
|
||||
public void setURL(String url) throws ContactsStorageException {
|
||||
synchronized (syncState) {
|
||||
readSyncState();
|
||||
syncState.putString(SYNC_STATE_URL, url);
|
||||
writeSyncState();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCTag() throws ContactsStorageException {
|
||||
synchronized (syncState) {
|
||||
readSyncState();
|
||||
return syncState.getString(SYNC_STATE_CTAG);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setCTag(String cTag) throws ContactsStorageException {
|
||||
synchronized (syncState) {
|
||||
readSyncState();
|
||||
syncState.putString(SYNC_STATE_CTAG, cTag);
|
||||
writeSyncState();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,357 @@
|
||||
/*
|
||||
* 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 android.accounts.Account
|
||||
import android.accounts.AccountManager
|
||||
import android.content.*
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.RemoteException
|
||||
import android.provider.ContactsContract
|
||||
import android.provider.ContactsContract.CommonDataKinds.GroupMembership
|
||||
import android.provider.ContactsContract.Groups
|
||||
import android.provider.ContactsContract.RawContacts
|
||||
import android.util.Base64
|
||||
import at.bitfire.davdroid.App
|
||||
import at.bitfire.davdroid.DavUtils
|
||||
import at.bitfire.davdroid.log.Logger
|
||||
import at.bitfire.davdroid.model.CollectionInfo
|
||||
import at.bitfire.vcard4android.*
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.FileNotFoundException
|
||||
import java.util.*
|
||||
import java.util.logging.Level
|
||||
|
||||
class LocalAddressBook(
|
||||
private val context: Context,
|
||||
account: Account,
|
||||
provider: ContentProviderClient?
|
||||
): AndroidAddressBook<LocalContact, LocalGroup>(account, provider, LocalContact.Factory, LocalGroup.Factory), LocalCollection<LocalResource> {
|
||||
|
||||
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_CTAG = "ctag"
|
||||
|
||||
@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), App.addressBookAccountType)
|
||||
if (!accountManager.addAccountExplicitly(account, null, initialUserData(mainAccount, info.url)))
|
||||
throw ContactsStorageException("Couldn't create address book account")
|
||||
|
||||
val addressBook = LocalAddressBook(context, account, provider)
|
||||
ContentResolver.setSyncAutomatically(account, ContactsContract.AUTHORITY, true)
|
||||
return addressBook
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
@Throws(ContactsStorageException::class)
|
||||
fun find(context: Context, provider: ContentProviderClient, mainAccount: Account?): List<LocalAddressBook> {
|
||||
val accountManager = AccountManager.get(context)
|
||||
|
||||
val result = LinkedList<LocalAddressBook>()
|
||||
accountManager.getAccountsByType(App.addressBookAccountType)
|
||||
.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(")")
|
||||
return sb.toString()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun initialUserData(mainAccount: Account, url: String): Bundle {
|
||||
val bundle = Bundle(3)
|
||||
bundle.putString(USER_DATA_MAIN_ACCOUNT_NAME, mainAccount.name)
|
||||
bundle.putString(USER_DATA_MAIN_ACCOUNT_TYPE, mainAccount.type)
|
||||
bundle.putString(USER_DATA_URL, url)
|
||||
return bundle
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether contact groups (LocalGroup resources) are included in query results for
|
||||
* {@link #getAll()}, {@link #getDeleted()}, {@link #getDirty()} and
|
||||
* {@link #getWithoutFileName()}.
|
||||
*/
|
||||
var includeGroups = true
|
||||
|
||||
|
||||
/* operations on the collection (address book) itself */
|
||||
|
||||
@Throws(ContactsStorageException::class)
|
||||
fun update(info: CollectionInfo) {
|
||||
val newAccountName = accountName(getMainAccount(), info)
|
||||
if (account.name != newAccountName && Build.VERSION.SDK_INT >= 21) {
|
||||
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)
|
||||
account = future.result
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* 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()
|
||||
}
|
||||
|
||||
@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(ContactsStorageException::class)
|
||||
override fun getDeleted(): List<LocalResource> {
|
||||
val deleted = LinkedList<LocalResource>()
|
||||
deleted.addAll(getDeletedContacts())
|
||||
if (includeGroups)
|
||||
deleted.addAll(getDeletedGroups())
|
||||
return deleted
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(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)
|
||||
}
|
||||
|
||||
if (includeGroups)
|
||||
reallyDirty += getDirtyGroups().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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* special group operations */
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
@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)
|
||||
}
|
||||
}
|
||||
|
||||
@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 */
|
||||
queryGroups(null, null).filter { it.getMembers().isEmpty() }.forEach { group ->
|
||||
Logger.log.log(Level.FINE, "Deleting group", group)
|
||||
group.delete()
|
||||
}
|
||||
}
|
||||
|
||||
@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(): String {
|
||||
val accountManager = AccountManager.get(context)
|
||||
return accountManager.getUserData(account, USER_DATA_URL) ?: throw ContactsStorageException("Address book has no URL")
|
||||
}
|
||||
|
||||
fun setURL(url: String) {
|
||||
val accountManager = AccountManager.get(context)
|
||||
accountManager.setUserData(account, USER_DATA_URL, url)
|
||||
}
|
||||
|
||||
override fun getCTag(): String? {
|
||||
val accountManager = AccountManager.get(context)
|
||||
return accountManager.getUserData(account, USER_DATA_CTAG)
|
||||
}
|
||||
|
||||
override fun setCTag(cTag: String?) {
|
||||
val accountManager = AccountManager.get(context)
|
||||
accountManager.setUserData(account, USER_DATA_CTAG, cTag)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,267 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2013 – 2015 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 android.accounts.Account;
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.ContentProviderClient;
|
||||
import android.content.ContentProviderOperation;
|
||||
import android.content.ContentUris;
|
||||
import android.content.ContentValues;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.RemoteException;
|
||||
import android.provider.CalendarContract;
|
||||
import android.provider.CalendarContract.Calendars;
|
||||
import android.provider.CalendarContract.Events;
|
||||
import android.provider.CalendarContract.Reminders;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import net.fortuna.ical4j.model.component.VTimeZone;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
import at.bitfire.davdroid.App;
|
||||
import at.bitfire.davdroid.DavUtils;
|
||||
import at.bitfire.davdroid.model.CollectionInfo;
|
||||
import at.bitfire.ical4android.AndroidCalendar;
|
||||
import at.bitfire.ical4android.AndroidCalendarFactory;
|
||||
import at.bitfire.ical4android.BatchOperation;
|
||||
import at.bitfire.ical4android.CalendarStorageException;
|
||||
import at.bitfire.ical4android.DateUtils;
|
||||
import at.bitfire.vcard4android.ContactsStorageException;
|
||||
import lombok.Cleanup;
|
||||
|
||||
public class LocalCalendar extends AndroidCalendar implements LocalCollection {
|
||||
|
||||
public static final int defaultColor = 0xFF8bc34a; // light green 500
|
||||
|
||||
public static final String COLUMN_CTAG = Calendars.CAL_SYNC1;
|
||||
|
||||
protected static final int
|
||||
DIRTY_INCREASE_SEQUENCE = 1,
|
||||
DIRTY_DONT_INCREASE_SEQUENCE = 2;
|
||||
|
||||
static String[] BASE_INFO_COLUMNS = new String[] {
|
||||
Events._ID,
|
||||
Events._SYNC_ID,
|
||||
LocalEvent.COLUMN_ETAG
|
||||
};
|
||||
|
||||
@Override
|
||||
protected String[] eventBaseInfoColumns() {
|
||||
return BASE_INFO_COLUMNS;
|
||||
}
|
||||
|
||||
|
||||
protected LocalCalendar(Account account, ContentProviderClient provider, long id) {
|
||||
super(account, provider, LocalEvent.Factory.INSTANCE, id);
|
||||
}
|
||||
|
||||
public static Uri create(@NonNull Account account, @NonNull ContentProviderClient provider, @NonNull CollectionInfo info) throws CalendarStorageException {
|
||||
ContentValues values = valuesFromCollectionInfo(info, true);
|
||||
|
||||
// ACCOUNT_NAME and ACCOUNT_TYPE are required (see docs)! If it's missing, other apps will crash.
|
||||
values.put(Calendars.ACCOUNT_NAME, account.name);
|
||||
values.put(Calendars.ACCOUNT_TYPE, account.type);
|
||||
values.put(Calendars.OWNER_ACCOUNT, account.name);
|
||||
|
||||
// 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);
|
||||
|
||||
return create(account, provider, values);
|
||||
}
|
||||
|
||||
public void update(CollectionInfo info, boolean updateColor) throws CalendarStorageException {
|
||||
update(valuesFromCollectionInfo(info, updateColor));
|
||||
}
|
||||
|
||||
@TargetApi(15)
|
||||
private static ContentValues valuesFromCollectionInfo(CollectionInfo info, boolean withColor) {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(Calendars.NAME, info.url);
|
||||
values.put(Calendars.CALENDAR_DISPLAY_NAME, !TextUtils.isEmpty(info.displayName) ? info.displayName : DavUtils.lastSegmentOfUrl(info.url));
|
||||
|
||||
if (withColor)
|
||||
values.put(Calendars.CALENDAR_COLOR, info.color != null ? info.color : defaultColor);
|
||||
|
||||
if (info.readOnly)
|
||||
values.put(Calendars.CALENDAR_ACCESS_LEVEL, Calendars.CAL_ACCESS_READ);
|
||||
else {
|
||||
values.put(Calendars.CALENDAR_ACCESS_LEVEL, Calendars.CAL_ACCESS_OWNER);
|
||||
values.put(Calendars.CAN_MODIFY_TIME_ZONE, 1);
|
||||
values.put(Calendars.CAN_ORGANIZER_RESPOND, 1);
|
||||
}
|
||||
|
||||
if (!TextUtils.isEmpty(info.timeZone)) {
|
||||
VTimeZone timeZone = DateUtils.parseVTimeZone(info.timeZone);
|
||||
if (timeZone != null && timeZone.getTimeZoneId() != null)
|
||||
values.put(Calendars.CALENDAR_TIME_ZONE, DateUtils.findAndroidTimezoneID(timeZone.getTimeZoneId().getValue()));
|
||||
}
|
||||
values.put(Calendars.ALLOWED_REMINDERS, Reminders.METHOD_ALERT);
|
||||
if (Build.VERSION.SDK_INT >= 15) {
|
||||
values.put(Calendars.ALLOWED_AVAILABILITY, StringUtils.join(new int[] { Reminders.AVAILABILITY_TENTATIVE, Reminders.AVAILABILITY_FREE, Reminders.AVAILABILITY_BUSY }, ","));
|
||||
values.put(Calendars.ALLOWED_ATTENDEE_TYPES, StringUtils.join(new int[] { CalendarContract.Attendees.TYPE_OPTIONAL, CalendarContract.Attendees.TYPE_REQUIRED, CalendarContract.Attendees.TYPE_RESOURCE }, ", "));
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public LocalResource[] getAll() throws CalendarStorageException, ContactsStorageException {
|
||||
return (LocalEvent[])queryEvents(Events.ORIGINAL_ID + " IS NULL", null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public LocalEvent[] getDeleted() throws CalendarStorageException {
|
||||
return (LocalEvent[])queryEvents(Events.DELETED + "!=0 AND " + Events.ORIGINAL_ID + " IS NULL", null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public LocalEvent[] getWithoutFileName() throws CalendarStorageException {
|
||||
return (LocalEvent[])queryEvents(Events._SYNC_ID + " IS NULL AND " + Events.ORIGINAL_ID + " IS NULL", null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public LocalResource[] getDirty() throws CalendarStorageException, FileNotFoundException {
|
||||
List<LocalResource> dirty = new LinkedList<>();
|
||||
|
||||
// get dirty events which are not required to have an increased SEQUENCE value
|
||||
Collections.addAll(dirty, (LocalEvent[])queryEvents(Events.DIRTY + "=" + DIRTY_DONT_INCREASE_SEQUENCE + " AND " + Events.ORIGINAL_ID + " IS NULL", null));
|
||||
|
||||
// get dirty events which are required to have an increased SEQUENCE value
|
||||
for (LocalEvent event : (LocalEvent[])queryEvents(Events.DIRTY + "=" + DIRTY_INCREASE_SEQUENCE + " AND " + Events.ORIGINAL_ID + " IS NULL", null)) {
|
||||
if (event.getEvent().sequence == null) // sequence has not been assigned yet (i.e. this event was just locally created)
|
||||
event.getEvent().sequence = 0;
|
||||
else
|
||||
event.getEvent().sequence++;
|
||||
dirty.add(event);
|
||||
}
|
||||
|
||||
return dirty.toArray(new LocalResource[dirty.size()]);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("Recycle")
|
||||
public String getCTag() throws CalendarStorageException {
|
||||
try {
|
||||
@Cleanup Cursor cursor = provider.query(calendarSyncURI(), new String[] { COLUMN_CTAG }, null, null, null);
|
||||
if (cursor != null && cursor.moveToNext())
|
||||
return cursor.getString(0);
|
||||
} catch (RemoteException e) {
|
||||
throw new CalendarStorageException("Couldn't read local (last known) CTag", e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setCTag(String cTag) throws CalendarStorageException, ContactsStorageException {
|
||||
try {
|
||||
ContentValues values = new ContentValues(1);
|
||||
values.put(COLUMN_CTAG, cTag);
|
||||
provider.update(calendarSyncURI(), values, null, null);
|
||||
} catch (RemoteException e) {
|
||||
throw new CalendarStorageException("Couldn't write local (last known) CTag", e);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("Recycle")
|
||||
public void processDirtyExceptions() throws CalendarStorageException {
|
||||
// process deleted exceptions
|
||||
App.log.info("Processing deleted exceptions");
|
||||
try {
|
||||
@Cleanup Cursor cursor = provider.query(
|
||||
syncAdapterURI(Events.CONTENT_URI),
|
||||
new String[] { Events._ID, Events.ORIGINAL_ID, LocalEvent.COLUMN_SEQUENCE },
|
||||
Events.DELETED + "!=0 AND " + Events.ORIGINAL_ID + " IS NOT NULL", null, null);
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
App.log.fine("Found deleted exception, removing; then re-schuling original event");
|
||||
long id = cursor.getLong(0), // can't be null (by definition)
|
||||
originalID = cursor.getLong(1); // can't be null (by query)
|
||||
|
||||
// get original event's SEQUENCE
|
||||
@Cleanup Cursor cursor2 = provider.query(
|
||||
syncAdapterURI(ContentUris.withAppendedId(Events.CONTENT_URI, originalID)),
|
||||
new String[] { LocalEvent.COLUMN_SEQUENCE },
|
||||
null, null, null);
|
||||
int originalSequence = (cursor2 == null || cursor2.isNull(0)) ? 0 : cursor2.getInt(0);
|
||||
|
||||
BatchOperation batch = new BatchOperation(provider);
|
||||
// re-schedule original event and set it to DIRTY
|
||||
batch.enqueue(new BatchOperation.Operation(
|
||||
ContentProviderOperation.newUpdate(syncAdapterURI(ContentUris.withAppendedId(Events.CONTENT_URI, originalID)))
|
||||
.withValue(LocalEvent.COLUMN_SEQUENCE, originalSequence + 1)
|
||||
.withValue(Events.DIRTY, DIRTY_INCREASE_SEQUENCE)
|
||||
));
|
||||
// remove exception
|
||||
batch.enqueue(new BatchOperation.Operation(
|
||||
ContentProviderOperation.newDelete(syncAdapterURI(ContentUris.withAppendedId(Events.CONTENT_URI, id)))
|
||||
));
|
||||
batch.commit();
|
||||
}
|
||||
} catch (RemoteException e) {
|
||||
throw new CalendarStorageException("Couldn't process locally modified exception", e);
|
||||
}
|
||||
|
||||
// process dirty exceptions
|
||||
App.log.info("Processing dirty exceptions");
|
||||
try {
|
||||
@Cleanup Cursor cursor = provider.query(
|
||||
syncAdapterURI(Events.CONTENT_URI),
|
||||
new String[] { Events._ID, Events.ORIGINAL_ID, LocalEvent.COLUMN_SEQUENCE },
|
||||
Events.DIRTY + "!=0 AND " + Events.ORIGINAL_ID + " IS NOT NULL", null, null);
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
App.log.fine("Found dirty exception, increasing SEQUENCE to re-schedule");
|
||||
long id = cursor.getLong(0), // can't be null (by definition)
|
||||
originalID = cursor.getLong(1); // can't be null (by query)
|
||||
int sequence = cursor.isNull(2) ? 0 : cursor.getInt(2);
|
||||
|
||||
BatchOperation batch = new BatchOperation(provider);
|
||||
// original event to DIRTY
|
||||
batch.enqueue(new BatchOperation.Operation(
|
||||
ContentProviderOperation.newUpdate(syncAdapterURI(ContentUris.withAppendedId(Events.CONTENT_URI, originalID)))
|
||||
.withValue(Events.DIRTY, DIRTY_DONT_INCREASE_SEQUENCE)
|
||||
));
|
||||
// increase SEQUENCE and set DIRTY to 0
|
||||
batch.enqueue(new BatchOperation.Operation(
|
||||
ContentProviderOperation.newUpdate(syncAdapterURI(ContentUris.withAppendedId(Events.CONTENT_URI, id)))
|
||||
.withValue(LocalEvent.COLUMN_SEQUENCE, sequence + 1)
|
||||
.withValue(Events.DIRTY, 0)
|
||||
));
|
||||
batch.commit();
|
||||
}
|
||||
} catch (RemoteException e) {
|
||||
throw new CalendarStorageException("Couldn't process locally modified exception", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static class Factory implements AndroidCalendarFactory {
|
||||
public static final Factory INSTANCE = new Factory();
|
||||
|
||||
@Override
|
||||
public AndroidCalendar newInstance(Account account, ContentProviderClient provider, long id) {
|
||||
return new LocalCalendar(account, provider, id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AndroidCalendar[] newArray(int size) {
|
||||
return new LocalCalendar[size];
|
||||
}
|
||||
}
|
||||
}
|
||||
241
app/src/main/java/at/bitfire/davdroid/resource/LocalCalendar.kt
Normal file
241
app/src/main/java/at/bitfire/davdroid/resource/LocalCalendar.kt
Normal file
@@ -0,0 +1,241 @@
|
||||
/*
|
||||
* 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 android.accounts.Account
|
||||
import android.content.ContentProviderClient
|
||||
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.DavUtils
|
||||
import at.bitfire.davdroid.log.Logger
|
||||
import at.bitfire.davdroid.model.CollectionInfo
|
||||
import at.bitfire.ical4android.*
|
||||
import java.io.FileNotFoundException
|
||||
import java.util.*
|
||||
import java.util.logging.Level
|
||||
|
||||
class LocalCalendar private constructor(
|
||||
account: Account,
|
||||
provider: ContentProviderClient,
|
||||
id: Long
|
||||
): AndroidCalendar<LocalEvent>(account, provider, LocalEvent.Factory, id), LocalCollection<LocalEvent> {
|
||||
|
||||
companion object {
|
||||
|
||||
val defaultColor = 0xFF8bc34a.toInt() // light green 500
|
||||
|
||||
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)
|
||||
|
||||
// ACCOUNT_NAME and ACCOUNT_TYPE are required (see docs)! If it's missing, other apps will crash.
|
||||
values.put(Calendars.ACCOUNT_NAME, account.name)
|
||||
values.put(Calendars.ACCOUNT_TYPE, account.type)
|
||||
values.put(Calendars.OWNER_ACCOUNT, account.name)
|
||||
|
||||
// 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)
|
||||
|
||||
// create calendar colors
|
||||
AndroidCalendar.insertColors(provider, account)
|
||||
|
||||
return uri
|
||||
}
|
||||
|
||||
private fun valuesFromCollectionInfo(info: CollectionInfo, withColor: Boolean): ContentValues {
|
||||
val values = ContentValues()
|
||||
values.put(Calendars.NAME, info.url)
|
||||
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)
|
||||
|
||||
if (info.readOnly)
|
||||
values.put(Calendars.CALENDAR_ACCESS_LEVEL, Calendars.CAL_ACCESS_READ)
|
||||
else {
|
||||
values.put(Calendars.CALENDAR_ACCESS_LEVEL, Calendars.CAL_ACCESS_OWNER)
|
||||
values.put(Calendars.CAN_MODIFY_TIME_ZONE, 1)
|
||||
values.put(Calendars.CAN_ORGANIZER_RESPOND, 1)
|
||||
}
|
||||
|
||||
info.timeZone?.let { tzData ->
|
||||
try {
|
||||
val timeZone = DateUtils.parseVTimeZone(tzData)
|
||||
timeZone.timeZoneId?.let { tzId ->
|
||||
values.put(Calendars.CALENDAR_TIME_ZONE, DateUtils.findAndroidTimezoneID(tzId.value))
|
||||
}
|
||||
} catch(e: IllegalArgumentException) {
|
||||
Logger.log.log(Level.WARNING, "Couldn't parse calendar default time zone", e)
|
||||
}
|
||||
}
|
||||
values.put(Calendars.ALLOWED_REMINDERS, Reminders.METHOD_ALERT)
|
||||
values.put(Calendars.ALLOWED_AVAILABILITY, "${Reminders.AVAILABILITY_TENTATIVE},${Reminders.AVAILABILITY_FREE},${Reminders.AVAILABILITY_BUSY}")
|
||||
values.put(Calendars.ALLOWED_ATTENDEE_TYPES, "${CalendarContract.Attendees.TYPE_OPTIONAL},${CalendarContract.Attendees.TYPE_REQUIRED},${CalendarContract.Attendees.TYPE_RESOURCE}")
|
||||
return values
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
override fun eventBaseInfoColumns() = BASE_INFO_COLUMNS
|
||||
|
||||
|
||||
@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() =
|
||||
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> {
|
||||
val dirty = LinkedList<LocalEvent>()
|
||||
|
||||
// get dirty events which are required to have an increased SEQUENCE value
|
||||
for (localEvent in queryEvents("${Events.DIRTY}!=0 AND ${Events.ORIGINAL_ID} IS NULL", null)) {
|
||||
val event = localEvent.event!!
|
||||
val sequence = event.sequence
|
||||
if (event.sequence == null) // sequence has not been assigned yet (i.e. this event was just locally created)
|
||||
event.sequence = 0
|
||||
else if (localEvent.weAreOrganizer)
|
||||
event.sequence = sequence!! + 1
|
||||
dirty += localEvent
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
@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)
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(CalendarStorageException::class)
|
||||
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)
|
||||
|
||||
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 ->
|
||||
val originalSequence = if (cursor2.isNull(0)) 0 else cursor2.getInt(0)
|
||||
|
||||
// re-schedule original event and set it to DIRTY
|
||||
batch.enqueue(BatchOperation.Operation(
|
||||
ContentProviderOperation.newUpdate(syncAdapterURI(ContentUris.withAppendedId(Events.CONTENT_URI, originalID)))
|
||||
.withValue(LocalEvent.COLUMN_SEQUENCE, originalSequence + 1)
|
||||
.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()
|
||||
}
|
||||
}
|
||||
} catch (e: RemoteException) {
|
||||
throw CalendarStorageException("Couldn't process locally modified exception", e)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
object Factory: AndroidCalendarFactory<LocalCalendar> {
|
||||
|
||||
override fun newInstance(account: Account, provider: ContentProviderClient, id: Long) =
|
||||
LocalCalendar(account, provider, id)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2013 – 2015 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 java.io.FileNotFoundException;
|
||||
|
||||
import at.bitfire.ical4android.CalendarStorageException;
|
||||
import at.bitfire.vcard4android.ContactsStorageException;
|
||||
|
||||
public interface LocalCollection {
|
||||
|
||||
LocalResource[] getDeleted() throws CalendarStorageException, ContactsStorageException;
|
||||
LocalResource[] getWithoutFileName() throws CalendarStorageException, ContactsStorageException;
|
||||
LocalResource[] getDirty() throws CalendarStorageException, ContactsStorageException, FileNotFoundException;
|
||||
|
||||
LocalResource[] getAll() throws CalendarStorageException, ContactsStorageException;
|
||||
|
||||
String getCTag() throws CalendarStorageException, ContactsStorageException;
|
||||
void setCTag(String cTag) throws CalendarStorageException, ContactsStorageException;
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* 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.ical4android.CalendarStorageException
|
||||
import at.bitfire.vcard4android.ContactsStorageException
|
||||
import java.io.FileNotFoundException
|
||||
|
||||
interface LocalCollection<out T: LocalResource> {
|
||||
|
||||
@Throws(CalendarStorageException::class, ContactsStorageException::class)
|
||||
fun getDeleted(): List<T>
|
||||
|
||||
@Throws(CalendarStorageException::class, ContactsStorageException::class)
|
||||
fun getWithoutFileName(): List<T>
|
||||
|
||||
@Throws(CalendarStorageException::class, ContactsStorageException::class, FileNotFoundException::class)
|
||||
fun getDirty(): List<T>
|
||||
|
||||
@Throws(CalendarStorageException::class, ContactsStorageException::class)
|
||||
fun getAll(): List<T>
|
||||
|
||||
@Throws(CalendarStorageException::class, ContactsStorageException::class)
|
||||
fun getCTag(): String?
|
||||
|
||||
@Throws(CalendarStorageException::class, ContactsStorageException::class)
|
||||
fun setCTag(cTag: String?)
|
||||
|
||||
}
|
||||
@@ -1,196 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2013 – 2015 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 android.content.ContentProviderOperation;
|
||||
import android.content.ContentValues;
|
||||
import android.os.RemoteException;
|
||||
import android.provider.ContactsContract;
|
||||
import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
|
||||
import android.provider.ContactsContract.RawContacts.Data;
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import at.bitfire.davdroid.BuildConfig;
|
||||
import at.bitfire.davdroid.model.UnknownProperties;
|
||||
import at.bitfire.vcard4android.AndroidAddressBook;
|
||||
import at.bitfire.vcard4android.AndroidContact;
|
||||
import at.bitfire.vcard4android.AndroidContactFactory;
|
||||
import at.bitfire.vcard4android.BatchOperation;
|
||||
import at.bitfire.vcard4android.CachedGroupMembership;
|
||||
import at.bitfire.vcard4android.Contact;
|
||||
import at.bitfire.vcard4android.ContactsStorageException;
|
||||
import ezvcard.Ezvcard;
|
||||
|
||||
public class LocalContact extends AndroidContact implements LocalResource {
|
||||
static {
|
||||
Contact.productID = "+//IDN bitfire.at//DAVdroid/" + BuildConfig.VERSION_NAME + " vcard4android ez-vcard/" + Ezvcard.VERSION;
|
||||
}
|
||||
|
||||
protected final Set<Long>
|
||||
cachedGroupMemberships = new HashSet<>(),
|
||||
groupMemberships = new HashSet<>();
|
||||
|
||||
|
||||
protected LocalContact(AndroidAddressBook addressBook, long id, String fileName, String eTag) {
|
||||
super(addressBook, id, fileName, eTag);
|
||||
}
|
||||
|
||||
public LocalContact(AndroidAddressBook addressBook, Contact contact, String fileName, String eTag) {
|
||||
super(addressBook, contact, fileName, eTag);
|
||||
}
|
||||
|
||||
public void clearDirty(String eTag) throws ContactsStorageException {
|
||||
try {
|
||||
ContentValues values = new ContentValues(1);
|
||||
values.put(ContactsContract.RawContacts.DIRTY, 0);
|
||||
values.put(COLUMN_ETAG, eTag);
|
||||
addressBook.provider.update(rawContactSyncURI(), values, null, null);
|
||||
|
||||
this.eTag = eTag;
|
||||
} catch (RemoteException e) {
|
||||
throw new ContactsStorageException("Couldn't clear dirty flag", e);
|
||||
}
|
||||
}
|
||||
|
||||
public void updateFileNameAndUID(String uid) throws ContactsStorageException {
|
||||
try {
|
||||
String newFileName = uid + ".vcf";
|
||||
|
||||
ContentValues values = new ContentValues(2);
|
||||
values.put(COLUMN_FILENAME, newFileName);
|
||||
values.put(COLUMN_UID, uid);
|
||||
addressBook.provider.update(rawContactSyncURI(), values, null, null);
|
||||
|
||||
fileName = newFileName;
|
||||
} catch (RemoteException e) {
|
||||
throw new ContactsStorageException("Couldn't update UID", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected void populateData(String mimeType, ContentValues row) {
|
||||
switch (mimeType) {
|
||||
case CachedGroupMembership.CONTENT_ITEM_TYPE:
|
||||
cachedGroupMemberships.add(row.getAsLong(CachedGroupMembership.GROUP_ID));
|
||||
break;
|
||||
case GroupMembership.CONTENT_ITEM_TYPE:
|
||||
groupMemberships.add(row.getAsLong(GroupMembership.GROUP_ROW_ID));
|
||||
break;
|
||||
case UnknownProperties.CONTENT_ITEM_TYPE:
|
||||
contact.unknownProperties = row.getAsString(UnknownProperties.UNKNOWN_PROPERTIES);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void insertDataRows(BatchOperation batch) throws ContactsStorageException {
|
||||
super.insertDataRows(batch);
|
||||
|
||||
if (contact.unknownProperties != null) {
|
||||
final BatchOperation.Operation op;
|
||||
final ContentProviderOperation.Builder builder = ContentProviderOperation.newInsert(dataSyncURI());
|
||||
if (id == null) {
|
||||
op = new BatchOperation.Operation(builder, UnknownProperties.RAW_CONTACT_ID, 0);
|
||||
} else {
|
||||
op = new BatchOperation.Operation(builder);
|
||||
builder.withValue(UnknownProperties.RAW_CONTACT_ID, id);
|
||||
}
|
||||
builder .withValue(UnknownProperties.MIMETYPE, UnknownProperties.CONTENT_ITEM_TYPE)
|
||||
.withValue(UnknownProperties.UNKNOWN_PROPERTIES, contact.unknownProperties);
|
||||
batch.enqueue(op);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
public void addToGroup(BatchOperation batch, long groupID) {
|
||||
assertID();
|
||||
batch.enqueue(new BatchOperation.Operation(
|
||||
ContentProviderOperation.newInsert(dataSyncURI())
|
||||
.withValue(GroupMembership.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE)
|
||||
.withValue(GroupMembership.RAW_CONTACT_ID, id)
|
||||
.withValue(GroupMembership.GROUP_ROW_ID, groupID)
|
||||
));
|
||||
|
||||
batch.enqueue(new BatchOperation.Operation(
|
||||
ContentProviderOperation.newInsert(dataSyncURI())
|
||||
.withValue(CachedGroupMembership.MIMETYPE, CachedGroupMembership.CONTENT_ITEM_TYPE)
|
||||
.withValue(CachedGroupMembership.RAW_CONTACT_ID, id)
|
||||
.withValue(CachedGroupMembership.GROUP_ID, groupID)
|
||||
.withYieldAllowed(true)
|
||||
));
|
||||
}
|
||||
|
||||
public void removeGroupMemberships(BatchOperation batch) {
|
||||
assertID();
|
||||
batch.enqueue(new BatchOperation.Operation(
|
||||
ContentProviderOperation.newDelete(dataSyncURI())
|
||||
.withSelection(
|
||||
Data.RAW_CONTACT_ID + "=? AND " + Data.MIMETYPE + " IN (?,?)",
|
||||
new String[] { String.valueOf(id), GroupMembership.CONTENT_ITEM_TYPE, CachedGroupMembership.CONTENT_ITEM_TYPE }
|
||||
)
|
||||
.withYieldAllowed(true)
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the IDs of all groups the contact was member of (cached memberships).
|
||||
* 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
|
||||
*/
|
||||
@NonNull
|
||||
public Set<Long> getCachedGroupMemberships() throws ContactsStorageException, FileNotFoundException {
|
||||
getContact();
|
||||
return cachedGroupMemberships;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
@NonNull
|
||||
public Set<Long> getGroupMemberships() throws ContactsStorageException, FileNotFoundException {
|
||||
getContact();
|
||||
return groupMemberships;
|
||||
}
|
||||
|
||||
|
||||
// factory
|
||||
|
||||
static class Factory extends AndroidContactFactory {
|
||||
static final Factory INSTANCE = new Factory();
|
||||
|
||||
@Override
|
||||
public LocalContact newInstance(AndroidAddressBook addressBook, long id, String fileName, String eTag) {
|
||||
return new LocalContact(addressBook, id, fileName, eTag);
|
||||
}
|
||||
|
||||
@Override
|
||||
public LocalContact newInstance(AndroidAddressBook addressBook, Contact contact, String fileName, String eTag) {
|
||||
return new LocalContact(addressBook, contact, fileName, eTag);
|
||||
}
|
||||
|
||||
@Override
|
||||
public LocalContact[] newArray(int size) {
|
||||
return new LocalContact[size];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
273
app/src/main/java/at/bitfire/davdroid/resource/LocalContact.kt
Normal file
273
app/src/main/java/at/bitfire/davdroid/resource/LocalContact.kt
Normal file
@@ -0,0 +1,273 @@
|
||||
/*
|
||||
* 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 android.content.ContentProviderOperation
|
||||
import android.content.ContentValues
|
||||
import android.os.Build
|
||||
import android.os.RemoteException
|
||||
import android.provider.ContactsContract
|
||||
import android.provider.ContactsContract.CommonDataKinds.GroupMembership
|
||||
import android.provider.ContactsContract.RawContacts.Data
|
||||
import at.bitfire.davdroid.BuildConfig
|
||||
import at.bitfire.davdroid.log.Logger
|
||||
import at.bitfire.davdroid.model.UnknownProperties
|
||||
import at.bitfire.vcard4android.*
|
||||
import ezvcard.Ezvcard
|
||||
import java.io.FileNotFoundException
|
||||
import java.util.*
|
||||
|
||||
class LocalContact: AndroidContact, LocalResource {
|
||||
|
||||
companion object {
|
||||
|
||||
init {
|
||||
Contact.productID = "+//IDN bitfire.at//DAVdroid/" + BuildConfig.VERSION_NAME + " ez-vcard/" + Ezvcard.VERSION
|
||||
}
|
||||
|
||||
val COLUMN_HASHCODE = ContactsContract.RawContacts.SYNC3
|
||||
|
||||
}
|
||||
|
||||
private val cachedGroupMemberships = HashSet<Long>()
|
||||
private val groupMemberships = HashSet<Long>()
|
||||
|
||||
|
||||
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 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)
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(ContactsStorageException::class)
|
||||
override fun clearDirty(eTag: String?) {
|
||||
try {
|
||||
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
|
||||
} 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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Throws(ContactsStorageException::class)
|
||||
override fun populateData(mimeType: String, row: ContentValues) {
|
||||
when (mimeType) {
|
||||
CachedGroupMembership.CONTENT_ITEM_TYPE ->
|
||||
cachedGroupMemberships.add(row.getAsLong(CachedGroupMembership.GROUP_ID))
|
||||
GroupMembership.CONTENT_ITEM_TYPE ->
|
||||
groupMemberships.add(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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@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)
|
||||
}
|
||||
} catch(e: FileNotFoundException) {
|
||||
throw ContactsStorageException("Couldn't insert data rows", e)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Calculates a hash code from the contact's data (VCard) and group memberships.
|
||||
* 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")
|
||||
|
||||
// reset contact so that getContact() reads from database
|
||||
contact = null
|
||||
|
||||
// groupMemberships is filled by getContact()
|
||||
val dataHash = contact!!.hashCode()
|
||||
val groupHash = groupMemberships.hashCode()
|
||||
Logger.log.finest("Calculated data hash = $dataHash, group memberships hash = $groupHash")
|
||||
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)
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun addToGroup(batch: BatchOperation, groupID: Long) {
|
||||
batch.enqueue(BatchOperation.Operation(
|
||||
ContentProviderOperation.newInsert(dataSyncURI())
|
||||
.withValue(GroupMembership.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE)
|
||||
.withValue(GroupMembership.RAW_CONTACT_ID, id)
|
||||
.withValue(GroupMembership.GROUP_ROW_ID, groupID)
|
||||
))
|
||||
groupMemberships.add(groupID)
|
||||
|
||||
batch.enqueue(BatchOperation.Operation(
|
||||
ContentProviderOperation.newInsert(dataSyncURI())
|
||||
.withValue(CachedGroupMembership.MIMETYPE, CachedGroupMembership.CONTENT_ITEM_TYPE)
|
||||
.withValue(CachedGroupMembership.RAW_CONTACT_ID, id)
|
||||
.withValue(CachedGroupMembership.GROUP_ID, groupID)
|
||||
.withYieldAllowed(true)
|
||||
))
|
||||
cachedGroupMemberships.add(groupID)
|
||||
}
|
||||
|
||||
fun removeGroupMemberships(batch: BatchOperation) {
|
||||
batch.enqueue(BatchOperation.Operation(
|
||||
ContentProviderOperation.newDelete(dataSyncURI())
|
||||
.withSelection(
|
||||
Data.RAW_CONTACT_ID + "=? AND " + Data.MIMETYPE + " IN (?,?)",
|
||||
arrayOf(id.toString(), GroupMembership.CONTENT_ITEM_TYPE, CachedGroupMembership.CONTENT_ITEM_TYPE)
|
||||
)
|
||||
.withYieldAllowed(true)
|
||||
))
|
||||
groupMemberships.clear()
|
||||
cachedGroupMemberships.clear()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the IDs of all groups the contact was member of (cached memberships).
|
||||
* 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::class, ContactsStorageException::class)
|
||||
fun getCachedGroupMemberships(): Set<Long> {
|
||||
contact
|
||||
return cachedGroupMemberships
|
||||
}
|
||||
|
||||
/**
|
||||
* 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::class, ContactsStorageException::class)
|
||||
fun getGroupMemberships(): Set<Long> {
|
||||
contact
|
||||
return groupMemberships
|
||||
}
|
||||
|
||||
|
||||
// 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)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,146 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2013 – 2015 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 android.annotation.TargetApi;
|
||||
import android.content.ContentProviderOperation;
|
||||
import android.content.ContentValues;
|
||||
import android.os.Build;
|
||||
import android.os.RemoteException;
|
||||
import android.provider.CalendarContract;
|
||||
import android.provider.CalendarContract.Events;
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import net.fortuna.ical4j.model.property.ProdId;
|
||||
|
||||
import at.bitfire.davdroid.BuildConfig;
|
||||
import at.bitfire.ical4android.AndroidCalendar;
|
||||
import at.bitfire.ical4android.AndroidEvent;
|
||||
import at.bitfire.ical4android.AndroidEventFactory;
|
||||
import at.bitfire.ical4android.CalendarStorageException;
|
||||
import at.bitfire.ical4android.Event;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
@TargetApi(17)
|
||||
public class LocalEvent extends AndroidEvent implements LocalResource {
|
||||
static {
|
||||
Event.prodId = new ProdId("+//IDN bitfire.at//DAVdroid/" + BuildConfig.VERSION_NAME + " ical4android ical4j/2.x");
|
||||
}
|
||||
|
||||
static final String COLUMN_ETAG = CalendarContract.Events.SYNC_DATA1,
|
||||
COLUMN_UID = Build.VERSION.SDK_INT >= 17 ? Events.UID_2445 : Events.SYNC_DATA2,
|
||||
COLUMN_SEQUENCE = CalendarContract.Events.SYNC_DATA3;
|
||||
|
||||
@Getter protected String fileName;
|
||||
@Getter @Setter protected String eTag;
|
||||
|
||||
public LocalEvent(@NonNull AndroidCalendar calendar, Event event, String fileName, String eTag) {
|
||||
super(calendar, event);
|
||||
this.fileName = fileName;
|
||||
this.eTag = eTag;
|
||||
}
|
||||
|
||||
protected LocalEvent(@NonNull AndroidCalendar calendar, long id, ContentValues baseInfo) {
|
||||
super(calendar, id, baseInfo);
|
||||
if (baseInfo != null) {
|
||||
fileName = baseInfo.getAsString(Events._SYNC_ID);
|
||||
eTag = baseInfo.getAsString(COLUMN_ETAG);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* process LocalEvent-specific fields */
|
||||
|
||||
@Override
|
||||
protected void populateEvent(ContentValues values) {
|
||||
super.populateEvent(values);
|
||||
fileName = values.getAsString(Events._SYNC_ID);
|
||||
eTag = values.getAsString(COLUMN_ETAG);
|
||||
event.uid = values.getAsString(COLUMN_UID);
|
||||
|
||||
event.sequence = values.getAsInteger(COLUMN_SEQUENCE);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void buildEvent(Event recurrence, ContentProviderOperation.Builder builder) {
|
||||
super.buildEvent(recurrence, builder);
|
||||
|
||||
boolean buildException = recurrence != null;
|
||||
Event eventToBuild = buildException ? recurrence : event;
|
||||
|
||||
builder .withValue(COLUMN_UID, event.uid)
|
||||
.withValue(COLUMN_SEQUENCE, eventToBuild.sequence)
|
||||
.withValue(CalendarContract.Events.DIRTY, 0)
|
||||
.withValue(CalendarContract.Events.DELETED, 0);
|
||||
|
||||
if (buildException)
|
||||
builder.withValue(Events.ORIGINAL_SYNC_ID, fileName);
|
||||
else
|
||||
builder .withValue(Events._SYNC_ID, fileName)
|
||||
.withValue(COLUMN_ETAG, eTag);
|
||||
}
|
||||
|
||||
|
||||
/* custom queries */
|
||||
|
||||
public void updateFileNameAndUID(String uid) throws CalendarStorageException {
|
||||
try {
|
||||
String newFileName = uid + ".ics";
|
||||
|
||||
ContentValues values = new ContentValues(2);
|
||||
values.put(Events._SYNC_ID, newFileName);
|
||||
values.put(COLUMN_UID, uid);
|
||||
calendar.provider.update(eventSyncURI(), values, null, null);
|
||||
|
||||
fileName = newFileName;
|
||||
if (event != null)
|
||||
event.uid = uid;
|
||||
|
||||
} catch (RemoteException e) {
|
||||
throw new CalendarStorageException("Couldn't update UID", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearDirty(String eTag) throws CalendarStorageException {
|
||||
try {
|
||||
ContentValues values = new ContentValues(2);
|
||||
values.put(CalendarContract.Events.DIRTY, 0);
|
||||
values.put(COLUMN_ETAG, eTag);
|
||||
if (event != null)
|
||||
values.put(COLUMN_SEQUENCE, event.sequence);
|
||||
calendar.provider.update(eventSyncURI(), values, null, null);
|
||||
|
||||
this.eTag = eTag;
|
||||
} catch (RemoteException e) {
|
||||
throw new CalendarStorageException("Couldn't update UID", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static class Factory implements AndroidEventFactory {
|
||||
static final Factory INSTANCE = new Factory();
|
||||
|
||||
@Override
|
||||
public AndroidEvent newInstance(AndroidCalendar calendar, long id, ContentValues baseInfo) {
|
||||
return new LocalEvent(calendar, id, baseInfo);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AndroidEvent newInstance(AndroidCalendar calendar, Event event) {
|
||||
return new LocalEvent(calendar, event, null, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AndroidEvent[] newArray(int size) {
|
||||
return new LocalEvent[size];
|
||||
}
|
||||
}
|
||||
}
|
||||
148
app/src/main/java/at/bitfire/davdroid/resource/LocalEvent.kt
Normal file
148
app/src/main/java/at/bitfire/davdroid/resource/LocalEvent.kt
Normal file
@@ -0,0 +1,148 @@
|
||||
/*
|
||||
* 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 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 {
|
||||
|
||||
companion object {
|
||||
init {
|
||||
iCalendar.prodId = ProdId("+//IDN bitfire.at//DAVdroid/" + BuildConfig.VERSION_NAME + " ical4j/2.x")
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
override var fileName: String? = null
|
||||
override var eTag: String? = null
|
||||
|
||||
var weAreOrganizer = true
|
||||
|
||||
|
||||
constructor(calendar: AndroidCalendar<*>, event: Event, fileName: String?, eTag: String?): super(calendar, event) {
|
||||
this.fileName = fileName
|
||||
this.eTag = eTag
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* 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.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
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(FileNotFoundException::class, CalendarStorageException::class)
|
||||
override fun buildEvent(recurrence: Event?, builder: ContentProviderOperation.Builder) {
|
||||
super.buildEvent(recurrence, builder)
|
||||
val event = requireNotNull(event)
|
||||
|
||||
val buildException = recurrence != null
|
||||
val eventToBuild = recurrence ?: event
|
||||
|
||||
builder .withValue(COLUMN_UID, event.uid)
|
||||
.withValue(COLUMN_SEQUENCE, eventToBuild.sequence)
|
||||
.withValue(CalendarContract.Events.DIRTY, 0)
|
||||
.withValue(CalendarContract.Events.DELETED, 0)
|
||||
|
||||
if (buildException)
|
||||
builder .withValue(Events.ORIGINAL_SYNC_ID, fileName)
|
||||
else
|
||||
builder .withValue(Events._SYNC_ID, fileName)
|
||||
.withValue(COLUMN_ETAG, eTag)
|
||||
}
|
||||
|
||||
|
||||
/* 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)
|
||||
}
|
||||
}
|
||||
|
||||
@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)
|
||||
|
||||
this.eTag = eTag
|
||||
} catch (e: Exception) {
|
||||
throw CalendarStorageException("Couldn't update UID", e)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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)
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,248 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2013 – 2015 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 android.content.ContentProviderOperation;
|
||||
import android.content.ContentUris;
|
||||
import android.content.ContentValues;
|
||||
import android.database.Cursor;
|
||||
import android.os.Parcel;
|
||||
import android.os.RemoteException;
|
||||
import android.provider.ContactsContract;
|
||||
import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
|
||||
import android.provider.ContactsContract.Groups;
|
||||
import android.provider.ContactsContract.RawContacts;
|
||||
import android.provider.ContactsContract.RawContacts.Data;
|
||||
|
||||
import org.apache.commons.lang3.ArrayUtils;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import at.bitfire.dav4android.Constants;
|
||||
import at.bitfire.vcard4android.AndroidAddressBook;
|
||||
import at.bitfire.vcard4android.AndroidGroup;
|
||||
import at.bitfire.vcard4android.AndroidGroupFactory;
|
||||
import at.bitfire.vcard4android.BatchOperation;
|
||||
import at.bitfire.vcard4android.CachedGroupMembership;
|
||||
import at.bitfire.vcard4android.Contact;
|
||||
import at.bitfire.vcard4android.ContactsStorageException;
|
||||
import lombok.Cleanup;
|
||||
import lombok.ToString;
|
||||
|
||||
@ToString(callSuper=true)
|
||||
public class LocalGroup extends AndroidGroup implements LocalResource {
|
||||
/** marshalled list of member UIDs, as sent by server */
|
||||
public static final String COLUMN_PENDING_MEMBERS = Groups.SYNC3;
|
||||
|
||||
public LocalGroup(AndroidAddressBook addressBook, long id, String fileName, String eTag) {
|
||||
super(addressBook, id, fileName, eTag);
|
||||
}
|
||||
|
||||
public LocalGroup(AndroidAddressBook addressBook, Contact contact, String fileName, String eTag) {
|
||||
super(addressBook, contact, fileName, eTag);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void clearDirty(String eTag) throws ContactsStorageException {
|
||||
assertID();
|
||||
|
||||
ContentValues values = new ContentValues(2);
|
||||
values.put(Groups.DIRTY, 0);
|
||||
values.put(COLUMN_ETAG, this.eTag = eTag);
|
||||
update(values);
|
||||
|
||||
// update cached group memberships
|
||||
BatchOperation batch = new BatchOperation(addressBook.provider);
|
||||
|
||||
// delete cached group memberships
|
||||
batch.enqueue(new BatchOperation.Operation(
|
||||
ContentProviderOperation.newDelete(addressBook.syncAdapterURI(ContactsContract.Data.CONTENT_URI))
|
||||
.withSelection(
|
||||
CachedGroupMembership.MIMETYPE + "=? AND " + CachedGroupMembership.GROUP_ID + "=?",
|
||||
new String[] { CachedGroupMembership.CONTENT_ITEM_TYPE, String.valueOf(id) }
|
||||
)
|
||||
));
|
||||
|
||||
// insert updated cached group memberships
|
||||
for (long member : getMembers())
|
||||
batch.enqueue(new BatchOperation.Operation(
|
||||
ContentProviderOperation.newInsert(addressBook.syncAdapterURI(ContactsContract.Data.CONTENT_URI))
|
||||
.withValue(CachedGroupMembership.MIMETYPE, CachedGroupMembership.CONTENT_ITEM_TYPE)
|
||||
.withValue(CachedGroupMembership.RAW_CONTACT_ID, member)
|
||||
.withValue(CachedGroupMembership.GROUP_ID, id)
|
||||
.withYieldAllowed(true)
|
||||
));
|
||||
|
||||
batch.commit();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateFileNameAndUID(String uid) throws ContactsStorageException {
|
||||
String newFileName = uid + ".vcf";
|
||||
|
||||
ContentValues values = new ContentValues(2);
|
||||
values.put(COLUMN_FILENAME, newFileName);
|
||||
values.put(COLUMN_UID, uid);
|
||||
update(values);
|
||||
|
||||
fileName = newFileName;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ContentValues contentValues() {
|
||||
ContentValues values = super.contentValues();
|
||||
|
||||
@Cleanup("recycle") Parcel members = Parcel.obtain();
|
||||
members.writeStringList(contact.members);
|
||||
values.put(COLUMN_PENDING_MEMBERS, members.marshall());
|
||||
|
||||
return values;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Marks all members of the current group as dirty.
|
||||
*/
|
||||
public void markMembersDirty() throws ContactsStorageException {
|
||||
assertID();
|
||||
BatchOperation batch = new BatchOperation(addressBook.provider);
|
||||
|
||||
for (long member : getMembers())
|
||||
batch.enqueue(new BatchOperation.Operation(
|
||||
ContentProviderOperation.newUpdate(addressBook.syncAdapterURI(ContentUris.withAppendedId(RawContacts.CONTENT_URI, member)))
|
||||
.withValue(RawContacts.DIRTY, 1)
|
||||
.withYieldAllowed(true)
|
||||
));
|
||||
|
||||
batch.commit();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
public static void applyPendingMemberships(LocalAddressBook addressBook) throws ContactsStorageException {
|
||||
try {
|
||||
@Cleanup Cursor cursor = addressBook.provider.query(
|
||||
addressBook.syncAdapterURI(Groups.CONTENT_URI),
|
||||
new String[] { Groups._ID, COLUMN_PENDING_MEMBERS },
|
||||
COLUMN_PENDING_MEMBERS + " IS NOT NULL", new String[] {},
|
||||
null
|
||||
);
|
||||
|
||||
BatchOperation batch = new BatchOperation(addressBook.provider);
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
long id = cursor.getLong(0);
|
||||
Constants.log.fine("Assigning members to group " + id);
|
||||
|
||||
// delete all memberships and cached memberships for this group
|
||||
batch.enqueue(new BatchOperation.Operation(
|
||||
ContentProviderOperation.newDelete(addressBook.syncAdapterURI(ContactsContract.Data.CONTENT_URI))
|
||||
.withSelection(
|
||||
"(" + GroupMembership.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?) OR (" +
|
||||
CachedGroupMembership.MIMETYPE + "=? AND " + CachedGroupMembership.GROUP_ID + "=?)",
|
||||
new String[] { GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(id), CachedGroupMembership.CONTENT_ITEM_TYPE, String.valueOf(id) })
|
||||
.withYieldAllowed(true)
|
||||
));
|
||||
|
||||
// extract list of member UIDs
|
||||
List<String> members = new LinkedList<>();
|
||||
byte[] raw = cursor.getBlob(1);
|
||||
@Cleanup("recycle") Parcel parcel = Parcel.obtain();
|
||||
parcel.unmarshall(raw, 0, raw.length);
|
||||
parcel.setDataPosition(0);
|
||||
parcel.readStringList(members);
|
||||
|
||||
// insert memberships
|
||||
for (String uid : members) {
|
||||
Constants.log.fine("Assigning member: " + uid);
|
||||
try {
|
||||
LocalContact member = addressBook.findContactByUID(uid);
|
||||
member.addToGroup(batch, id);
|
||||
} catch(FileNotFoundException e) {
|
||||
Constants.log.log(Level.WARNING, "Group member not found: " + uid, e);
|
||||
}
|
||||
}
|
||||
|
||||
// remove pending memberships
|
||||
batch.enqueue(new BatchOperation.Operation(
|
||||
ContentProviderOperation.newUpdate(addressBook.syncAdapterURI(ContentUris.withAppendedId(Groups.CONTENT_URI, id)))
|
||||
.withValue(COLUMN_PENDING_MEMBERS, null)
|
||||
.withYieldAllowed(true)
|
||||
));
|
||||
|
||||
batch.commit();
|
||||
}
|
||||
} catch(RemoteException e) {
|
||||
throw new ContactsStorageException("Couldn't get pending memberships", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// helpers
|
||||
|
||||
private void assertID() {
|
||||
if (id == null)
|
||||
throw new IllegalStateException("Group has not been saved yet");
|
||||
}
|
||||
|
||||
/**
|
||||
* Lists all members of this group.
|
||||
* @return list of all members' raw contact IDs
|
||||
* @throws ContactsStorageException on contact provider errors
|
||||
*/
|
||||
protected long[] getMembers() throws ContactsStorageException {
|
||||
assertID();
|
||||
List<Long> members = new LinkedList<>();
|
||||
try {
|
||||
@Cleanup Cursor cursor = addressBook.provider.query(
|
||||
addressBook.syncAdapterURI(ContactsContract.Data.CONTENT_URI),
|
||||
new String[] { Data.RAW_CONTACT_ID },
|
||||
GroupMembership.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
|
||||
new String[] { GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(id) },
|
||||
null
|
||||
);
|
||||
while (cursor != null && cursor.moveToNext())
|
||||
members.add(cursor.getLong(0));
|
||||
} catch(RemoteException e) {
|
||||
throw new ContactsStorageException("Couldn't list group members", e);
|
||||
}
|
||||
return ArrayUtils.toPrimitive(members.toArray(new Long[members.size()]));
|
||||
}
|
||||
|
||||
|
||||
// factory
|
||||
|
||||
static class Factory extends AndroidGroupFactory {
|
||||
static final Factory INSTANCE = new Factory();
|
||||
|
||||
@Override
|
||||
public LocalGroup newInstance(AndroidAddressBook addressBook, long id, String fileName, String eTag) {
|
||||
return new LocalGroup(addressBook, id, fileName, eTag);
|
||||
}
|
||||
|
||||
@Override
|
||||
public LocalGroup newInstance(AndroidAddressBook addressBook, Contact contact, String fileName, String eTag) {
|
||||
return new LocalGroup(addressBook, contact, fileName, eTag);
|
||||
}
|
||||
|
||||
@Override
|
||||
public LocalGroup[] newArray(int size) {
|
||||
return new LocalGroup[size];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
242
app/src/main/java/at/bitfire/davdroid/resource/LocalGroup.kt
Normal file
242
app/src/main/java/at/bitfire/davdroid/resource/LocalGroup.kt
Normal file
@@ -0,0 +1,242 @@
|
||||
/*
|
||||
* 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 android.content.ContentProviderOperation
|
||||
import android.content.ContentUris
|
||||
import android.content.ContentValues
|
||||
import android.os.Build
|
||||
import android.os.Parcel
|
||||
import android.os.RemoteException
|
||||
import android.provider.ContactsContract
|
||||
import android.provider.ContactsContract.CommonDataKinds.GroupMembership
|
||||
import android.provider.ContactsContract.Groups
|
||||
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 {
|
||||
|
||||
companion object {
|
||||
|
||||
/** marshaled list of member UIDs, as sent by server */
|
||||
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")
|
||||
|
||||
// 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()
|
||||
}
|
||||
}
|
||||
} 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)
|
||||
|
||||
|
||||
@Throws(ContactsStorageException::class)
|
||||
override fun clearDirty(eTag: String?) {
|
||||
val id = requireNotNull(id)
|
||||
|
||||
val values = ContentValues(2)
|
||||
values.put(Groups.DIRTY, 0)
|
||||
values.put(COLUMN_ETAG, eTag)
|
||||
this.eTag = eTag
|
||||
update(values)
|
||||
|
||||
// update cached group memberships
|
||||
val batch = BatchOperation(addressBook.provider!!)
|
||||
|
||||
// delete cached group memberships
|
||||
batch.enqueue(BatchOperation.Operation(
|
||||
ContentProviderOperation.newDelete(addressBook.syncAdapterURI(ContactsContract.Data.CONTENT_URI))
|
||||
.withSelection(
|
||||
CachedGroupMembership.MIMETYPE + "=? AND " + CachedGroupMembership.GROUP_ID + "=?",
|
||||
arrayOf(CachedGroupMembership.CONTENT_ITEM_TYPE, id.toString())
|
||||
)
|
||||
))
|
||||
|
||||
// insert updated cached group memberships
|
||||
for (member in getMembers())
|
||||
batch.enqueue(BatchOperation.Operation(
|
||||
ContentProviderOperation.newInsert(addressBook.syncAdapterURI(ContactsContract.Data.CONTENT_URI))
|
||||
.withValue(CachedGroupMembership.MIMETYPE, CachedGroupMembership.CONTENT_ITEM_TYPE)
|
||||
.withValue(CachedGroupMembership.RAW_CONTACT_ID, member)
|
||||
.withValue(CachedGroupMembership.GROUP_ID, id)
|
||||
.withYieldAllowed(true)
|
||||
))
|
||||
|
||||
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!!)
|
||||
|
||||
for (member in getMembers())
|
||||
batch.enqueue(BatchOperation.Operation(
|
||||
ContentProviderOperation.newUpdate(addressBook.syncAdapterURI(ContentUris.withAppendedId(RawContacts.CONTENT_URI, member)))
|
||||
.withValue(RawContacts.DIRTY, 1)
|
||||
.withYieldAllowed(true)
|
||||
))
|
||||
|
||||
batch.commit()
|
||||
}
|
||||
|
||||
|
||||
// helpers
|
||||
|
||||
/**
|
||||
* Lists all members of this group.
|
||||
* @return list of all members' raw contact IDs
|
||||
* @throws ContactsStorageException on contact provider errorst
|
||||
*/
|
||||
@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)
|
||||
}
|
||||
return members
|
||||
}
|
||||
|
||||
|
||||
// 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)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2013 – 2015 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.ical4android.CalendarStorageException;
|
||||
import at.bitfire.vcard4android.ContactsStorageException;
|
||||
|
||||
public interface LocalResource {
|
||||
|
||||
Long getId();
|
||||
|
||||
String getFileName();
|
||||
String getETag();
|
||||
|
||||
int delete() throws CalendarStorageException, ContactsStorageException;
|
||||
|
||||
void updateFileNameAndUID(String uuid) throws CalendarStorageException, ContactsStorageException;
|
||||
void clearDirty(String eTag) throws CalendarStorageException, ContactsStorageException;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* 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.ical4android.CalendarStorageException
|
||||
import at.bitfire.vcard4android.ContactsStorageException
|
||||
|
||||
interface LocalResource {
|
||||
|
||||
val id: Long?
|
||||
|
||||
var fileName: String?
|
||||
var eTag: String?
|
||||
|
||||
@Throws(CalendarStorageException::class, ContactsStorageException::class)
|
||||
fun delete(): Int
|
||||
|
||||
@Throws(CalendarStorageException::class, ContactsStorageException::class)
|
||||
fun prepareForUpload()
|
||||
|
||||
@Throws(CalendarStorageException::class, ContactsStorageException::class)
|
||||
fun clearDirty(eTag: String?)
|
||||
|
||||
}
|
||||
@@ -1,138 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2013 – 2015 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 android.content.ContentProviderOperation;
|
||||
import android.content.ContentValues;
|
||||
import android.os.RemoteException;
|
||||
import android.provider.CalendarContract.Events;
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import net.fortuna.ical4j.model.property.ProdId;
|
||||
|
||||
import org.dmfs.provider.tasks.TaskContract.Tasks;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.text.ParseException;
|
||||
|
||||
import at.bitfire.davdroid.BuildConfig;
|
||||
import at.bitfire.ical4android.AndroidTask;
|
||||
import at.bitfire.ical4android.AndroidTaskFactory;
|
||||
import at.bitfire.ical4android.AndroidTaskList;
|
||||
import at.bitfire.ical4android.CalendarStorageException;
|
||||
import at.bitfire.ical4android.Task;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
public class LocalTask extends AndroidTask implements LocalResource {
|
||||
static {
|
||||
Task.prodId = new ProdId("+//IDN bitfire.at//DAVdroid/" + BuildConfig.VERSION_NAME + " ical4android ical4j/2.x");
|
||||
}
|
||||
|
||||
static final String COLUMN_ETAG = Tasks.SYNC1,
|
||||
COLUMN_UID = Tasks.SYNC2,
|
||||
COLUMN_SEQUENCE = Tasks.SYNC3;
|
||||
|
||||
@Getter protected String fileName;
|
||||
@Getter @Setter protected String eTag;
|
||||
|
||||
public LocalTask(@NonNull AndroidTaskList taskList, Task task, String fileName, String eTag) {
|
||||
super(taskList, task);
|
||||
this.fileName = fileName;
|
||||
this.eTag = eTag;
|
||||
}
|
||||
|
||||
protected LocalTask(@NonNull AndroidTaskList taskList, long id, ContentValues baseInfo) {
|
||||
super(taskList, id);
|
||||
if (baseInfo != null) {
|
||||
fileName = baseInfo.getAsString(Events._SYNC_ID);
|
||||
eTag = baseInfo.getAsString(COLUMN_ETAG);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* process LocalTask-specific fields */
|
||||
|
||||
@Override
|
||||
protected void populateTask(ContentValues values) throws FileNotFoundException, RemoteException, ParseException {
|
||||
super.populateTask(values);
|
||||
|
||||
fileName = values.getAsString(Events._SYNC_ID);
|
||||
eTag = values.getAsString(COLUMN_ETAG);
|
||||
task.uid = values.getAsString(COLUMN_UID);
|
||||
|
||||
task.sequence = values.getAsInteger(COLUMN_SEQUENCE);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void buildTask(ContentProviderOperation.Builder builder, boolean update) {
|
||||
super.buildTask(builder, update);
|
||||
builder .withValue(Tasks._SYNC_ID, fileName)
|
||||
.withValue(COLUMN_UID, task.uid)
|
||||
.withValue(COLUMN_SEQUENCE, task.sequence)
|
||||
.withValue(COLUMN_ETAG, eTag);
|
||||
}
|
||||
|
||||
|
||||
/* custom queries */
|
||||
|
||||
public void updateFileNameAndUID(String uid) throws CalendarStorageException {
|
||||
try {
|
||||
String newFileName = uid + ".ics";
|
||||
|
||||
ContentValues values = new ContentValues(2);
|
||||
values.put(Tasks._SYNC_ID, newFileName);
|
||||
values.put(COLUMN_UID, uid);
|
||||
taskList.provider.client.update(taskSyncURI(), values, null, null);
|
||||
|
||||
fileName = newFileName;
|
||||
if (task != null)
|
||||
task.uid = uid;
|
||||
|
||||
} catch (RemoteException e) {
|
||||
throw new CalendarStorageException("Couldn't update UID", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearDirty(String eTag) throws CalendarStorageException {
|
||||
try {
|
||||
ContentValues values = new ContentValues(2);
|
||||
values.put(Tasks._DIRTY, 0);
|
||||
values.put(COLUMN_ETAG, eTag);
|
||||
if (task != null)
|
||||
values.put(COLUMN_SEQUENCE, task.sequence);
|
||||
taskList.provider.client.update(taskSyncURI(), values, null, null);
|
||||
|
||||
this.eTag = eTag;
|
||||
} catch (RemoteException e) {
|
||||
throw new CalendarStorageException("Couldn't update _DIRTY/ETag/SEQUENCE", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static class Factory implements AndroidTaskFactory {
|
||||
static final Factory INSTANCE = new Factory();
|
||||
|
||||
@Override
|
||||
public LocalTask newInstance(AndroidTaskList taskList, long id, ContentValues baseInfo) {
|
||||
return new LocalTask(taskList, id, baseInfo);
|
||||
}
|
||||
|
||||
@Override
|
||||
public LocalTask newInstance(AndroidTaskList taskList, Task task) {
|
||||
return new LocalTask(taskList, task, null, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public LocalTask[] newArray(int size) {
|
||||
return new LocalTask[size];
|
||||
}
|
||||
}
|
||||
}
|
||||
117
app/src/main/java/at/bitfire/davdroid/resource/LocalTask.kt
Normal file
117
app/src/main/java/at/bitfire/davdroid/resource/LocalTask.kt
Normal file
@@ -0,0 +1,117 @@
|
||||
/*
|
||||
* 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 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 java.util.*
|
||||
|
||||
class LocalTask: AndroidTask, LocalResource {
|
||||
|
||||
companion object {
|
||||
val COLUMN_ETAG = Tasks.SYNC1
|
||||
val COLUMN_UID = Tasks.SYNC2
|
||||
val COLUMN_SEQUENCE = Tasks.SYNC3
|
||||
}
|
||||
|
||||
override var fileName: String? = null
|
||||
override var eTag: String? = null
|
||||
|
||||
constructor(taskList: AndroidTaskList<*>, task: Task, fileName: String?, eTag: String?): super(taskList, task) {
|
||||
this.fileName = fileName
|
||||
this.eTag = eTag
|
||||
}
|
||||
|
||||
private constructor(taskList: AndroidTaskList<*>, id: Long, baseInfo: ContentValues?): super(taskList, id) {
|
||||
baseInfo?.let {
|
||||
fileName = it.getAsString(Events._SYNC_ID)
|
||||
eTag = it.getAsString(COLUMN_ETAG)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* 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)
|
||||
}
|
||||
|
||||
|
||||
/* custom queries */
|
||||
|
||||
@Throws(CalendarStorageException::class)
|
||||
override fun prepareForUpload() {
|
||||
try {
|
||||
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)
|
||||
|
||||
fileName = newFileName
|
||||
|
||||
task!!.uid = uid
|
||||
} catch (e: Exception) {
|
||||
throw CalendarStorageException("Couldn't update UID", e)
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(CalendarStorageException::class)
|
||||
override fun clearDirty(eTag: String?) {
|
||||
try {
|
||||
val values = ContentValues(2)
|
||||
values.put(Tasks._DIRTY, 0)
|
||||
values.put(COLUMN_ETAG, eTag)
|
||||
|
||||
values.put(COLUMN_SEQUENCE, task!!.sequence)
|
||||
taskList.provider.client.update(taskSyncURI(), values, null, null)
|
||||
|
||||
this.eTag = eTag
|
||||
} catch (e: Exception) {
|
||||
throw CalendarStorageException("Couldn't update _DIRTY/ETag/SEQUENCE", e)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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)
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,167 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2013 – 2015 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 android.accounts.Account;
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.RemoteException;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import org.dmfs.provider.tasks.TaskContract.TaskLists;
|
||||
import org.dmfs.provider.tasks.TaskContract.Tasks;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
|
||||
import at.bitfire.davdroid.DavUtils;
|
||||
import at.bitfire.davdroid.model.CollectionInfo;
|
||||
import at.bitfire.ical4android.AndroidTaskList;
|
||||
import at.bitfire.ical4android.AndroidTaskListFactory;
|
||||
import at.bitfire.ical4android.CalendarStorageException;
|
||||
import at.bitfire.ical4android.TaskProvider;
|
||||
import lombok.Cleanup;
|
||||
|
||||
public class LocalTaskList extends AndroidTaskList implements LocalCollection {
|
||||
|
||||
public static final int defaultColor = 0xFFC3EA6E; // "DAVdroid green"
|
||||
|
||||
public static final String COLUMN_CTAG = TaskLists.SYNC_VERSION;
|
||||
|
||||
static String[] BASE_INFO_COLUMNS = new String[] {
|
||||
Tasks._ID,
|
||||
Tasks._SYNC_ID,
|
||||
LocalTask.COLUMN_ETAG
|
||||
};
|
||||
|
||||
// can be cached, because after installing OpenTasks, you have to re-install DAVdroid anyway
|
||||
private static Boolean tasksProviderAvailable;
|
||||
|
||||
|
||||
@Override
|
||||
protected String[] taskBaseInfoColumns() {
|
||||
return BASE_INFO_COLUMNS;
|
||||
}
|
||||
|
||||
|
||||
protected LocalTaskList(Account account, TaskProvider provider, long id) {
|
||||
super(account, provider, LocalTask.Factory.INSTANCE, id);
|
||||
}
|
||||
|
||||
public static Uri create(Account account, TaskProvider provider, CollectionInfo info) throws CalendarStorageException {
|
||||
ContentValues values = valuesFromCollectionInfo(info, true);
|
||||
values.put(TaskLists.OWNER, account.name);
|
||||
values.put(TaskLists.SYNC_ENABLED, 1);
|
||||
values.put(TaskLists.VISIBLE, 1);
|
||||
return create(account, provider, values);
|
||||
}
|
||||
|
||||
public void update(CollectionInfo info, boolean updateColor) throws CalendarStorageException {
|
||||
update(valuesFromCollectionInfo(info, updateColor));
|
||||
}
|
||||
|
||||
private static ContentValues valuesFromCollectionInfo(CollectionInfo info, boolean withColor) {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(TaskLists._SYNC_ID, info.url);
|
||||
values.put(TaskLists.LIST_NAME, !TextUtils.isEmpty(info.displayName) ? info.displayName : DavUtils.lastSegmentOfUrl(info.url));
|
||||
|
||||
if (withColor)
|
||||
values.put(TaskLists.LIST_COLOR, info.color != null ? info.color : defaultColor);
|
||||
|
||||
return values;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public LocalTask[] getAll() throws CalendarStorageException {
|
||||
return (LocalTask[])queryTasks(null, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public LocalTask[] getDeleted() throws CalendarStorageException {
|
||||
return (LocalTask[])queryTasks(Tasks._DELETED + "!=0", null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public LocalTask[] getWithoutFileName() throws CalendarStorageException {
|
||||
return (LocalTask[])queryTasks(Tasks._SYNC_ID + " IS NULL", null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public LocalResource[] getDirty() throws CalendarStorageException, FileNotFoundException {
|
||||
LocalTask[] tasks = (LocalTask[])queryTasks(Tasks._DIRTY + "!=0", null);
|
||||
if (tasks != null)
|
||||
for (LocalTask task : tasks) {
|
||||
if (task.getTask().sequence == null) // sequence has not been assigned yet (i.e. this task was just locally created)
|
||||
task.getTask().sequence = 0;
|
||||
else
|
||||
task.getTask().sequence++;
|
||||
}
|
||||
return tasks;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("Recycle")
|
||||
public String getCTag() throws CalendarStorageException {
|
||||
try {
|
||||
@Cleanup Cursor cursor = provider.client.query(taskListSyncUri(), new String[] { COLUMN_CTAG }, null, null, null);
|
||||
if (cursor != null && cursor.moveToNext())
|
||||
return cursor.getString(0);
|
||||
} catch (RemoteException e) {
|
||||
throw new CalendarStorageException("Couldn't read local (last known) CTag", e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setCTag(String cTag) throws CalendarStorageException {
|
||||
try {
|
||||
ContentValues values = new ContentValues(1);
|
||||
values.put(COLUMN_CTAG, cTag);
|
||||
provider.client.update(taskListSyncUri(), values, null, null);
|
||||
} catch (RemoteException e) {
|
||||
throw new CalendarStorageException("Couldn't write local (last known) CTag", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// helpers
|
||||
|
||||
public static boolean tasksProviderAvailable(@NonNull Context context) {
|
||||
if (tasksProviderAvailable != null)
|
||||
return tasksProviderAvailable;
|
||||
else {
|
||||
if (Build.VERSION.SDK_INT >= 23)
|
||||
return context.getPackageManager().resolveContentProvider(TaskProvider.ProviderName.OpenTasks.authority, 0) != null;
|
||||
else {
|
||||
@Cleanup TaskProvider provider = TaskProvider.acquire(context.getContentResolver(), TaskProvider.ProviderName.OpenTasks);
|
||||
return tasksProviderAvailable = (provider != null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static class Factory implements AndroidTaskListFactory {
|
||||
public static final Factory INSTANCE = new Factory();
|
||||
|
||||
@Override
|
||||
public AndroidTaskList newInstance(Account account, TaskProvider provider, long id) {
|
||||
return new LocalTaskList(account, provider, id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AndroidTaskList[] newArray(int size) {
|
||||
return new LocalTaskList[size];
|
||||
}
|
||||
}
|
||||
}
|
||||
164
app/src/main/java/at/bitfire/davdroid/resource/LocalTaskList.kt
Normal file
164
app/src/main/java/at/bitfire/davdroid/resource/LocalTaskList.kt
Normal file
@@ -0,0 +1,164 @@
|
||||
/*
|
||||
* 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 android.accounts.Account
|
||||
import android.content.ContentProviderClient
|
||||
import android.content.ContentResolver
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import at.bitfire.davdroid.DavUtils
|
||||
import at.bitfire.davdroid.model.CollectionInfo
|
||||
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
|
||||
|
||||
class LocalTaskList private constructor(
|
||||
account: Account,
|
||||
provider: TaskProvider,
|
||||
id: Long
|
||||
): AndroidTaskList<LocalTask>(account, provider, LocalTask.Factory, id), LocalCollection<LocalTask> {
|
||||
|
||||
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 }
|
||||
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)
|
||||
values.put(TaskLists.SYNC_ENABLED, 1)
|
||||
values.put(TaskLists.VISIBLE, 1)
|
||||
return create(account, provider, values)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
@Throws(Exception::class)
|
||||
fun onRenameAccount(resolver: ContentResolver, oldName: String, newName: String) {
|
||||
var client: ContentProviderClient? = null
|
||||
try {
|
||||
client = resolver.acquireContentProviderClient(TaskProvider.ProviderName.OpenTasks.authority)
|
||||
client?.use {
|
||||
val values = ContentValues(1)
|
||||
values.put(Tasks.ACCOUNT_NAME, newName)
|
||||
it.update(Tasks.getContentUri(TaskProvider.ProviderName.OpenTasks.authority), values, "${Tasks.ACCOUNT_NAME}=?", arrayOf(oldName))
|
||||
}
|
||||
} finally {
|
||||
if (Build.VERSION.SDK_INT >= 24)
|
||||
client?.close()
|
||||
else
|
||||
client?.release()
|
||||
}
|
||||
}
|
||||
|
||||
private fun valuesFromCollectionInfo(info: CollectionInfo, withColor: Boolean): ContentValues {
|
||||
val values = ContentValues(3)
|
||||
values.put(TaskLists._SYNC_ID, info.url)
|
||||
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)
|
||||
|
||||
return values
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
override fun taskBaseInfoColumns() = BASE_INFO_COLUMNS
|
||||
|
||||
|
||||
@Throws(CalendarStorageException::class)
|
||||
fun update(info: CollectionInfo, updateColor: Boolean) {
|
||||
update(valuesFromCollectionInfo(info, updateColor));
|
||||
}
|
||||
|
||||
|
||||
@Throws(CalendarStorageException::class)
|
||||
override fun getAll() = queryTasks(null, 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> {
|
||||
val tasks = queryTasks("${Tasks._DIRTY}!=0", null)
|
||||
for (localTask in tasks) {
|
||||
val task = requireNotNull(localTask.task)
|
||||
val sequence = task.sequence
|
||||
if (sequence == null) // sequence has not been assigned yet (i.e. this task was just locally created)
|
||||
task.sequence = 0
|
||||
else
|
||||
task.sequence = sequence + 1
|
||||
}
|
||||
return tasks
|
||||
}
|
||||
|
||||
|
||||
@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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
object Factory: AndroidTaskListFactory<LocalTaskList> {
|
||||
|
||||
override fun newInstance(account: Account, provider: TaskProvider, id: Long) =
|
||||
LocalTaskList(account, provider, id)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2013 – 2015 Ricki Hirner (bitfire web engineering).
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the GNU Public License v3.0
|
||||
* which accompanies this distribution, and is available at
|
||||
* http://www.gnu.org/licenses/gpl.html
|
||||
*/
|
||||
package at.bitfire.davdroid.syncadapter;
|
||||
|
||||
import android.accounts.AbstractAccountAuthenticator;
|
||||
import android.accounts.Account;
|
||||
import android.accounts.AccountAuthenticatorResponse;
|
||||
import android.accounts.AccountManager;
|
||||
import android.accounts.NetworkErrorException;
|
||||
import android.app.Service;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.os.IBinder;
|
||||
|
||||
import at.bitfire.davdroid.ui.setup.LoginActivity;
|
||||
|
||||
public class AccountAuthenticatorService extends Service {
|
||||
|
||||
private AccountAuthenticator accountAuthenticator;
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
accountAuthenticator = new AccountAuthenticator(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
if (intent.getAction().equals(android.accounts.AccountManager.ACTION_AUTHENTICATOR_INTENT))
|
||||
return accountAuthenticator.getIBinder();
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
private static class AccountAuthenticator extends AbstractAccountAuthenticator {
|
||||
final Context context;
|
||||
|
||||
public AccountAuthenticator(Context context) {
|
||||
super(context);
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Bundle addAccount(AccountAuthenticatorResponse response, String accountType, String authTokenType,
|
||||
String[] requiredFeatures, Bundle options) throws NetworkErrorException {
|
||||
Intent intent = new Intent(context, LoginActivity.class);
|
||||
intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response);
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putParcelable(AccountManager.KEY_INTENT, intent);
|
||||
return bundle;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Bundle confirmCredentials(AccountAuthenticatorResponse response, Account account, Bundle options) throws NetworkErrorException {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Bundle editProperties(AccountAuthenticatorResponse response, String accountType) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Bundle getAuthToken(AccountAuthenticatorResponse response, Account account, String authTokenType, Bundle options) throws NetworkErrorException {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAuthTokenLabel(String authTokenType) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Bundle hasFeatures(AccountAuthenticatorResponse response, Account account, String[] features) throws NetworkErrorException {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Bundle updateCredentials(AccountAuthenticatorResponse response, Account account, String authTokenType, Bundle options) throws NetworkErrorException {
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Copyright © Ricki Hirner (bitfire web engineering).
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the GNU Public License v3.0
|
||||
* which accompanies this distribution, and is available at
|
||||
* http://www.gnu.org/licenses/gpl.html
|
||||
*/
|
||||
package at.bitfire.davdroid.syncadapter
|
||||
|
||||
import android.accounts.AbstractAccountAuthenticator
|
||||
import android.accounts.Account
|
||||
import android.accounts.AccountAuthenticatorResponse
|
||||
import android.accounts.AccountManager
|
||||
import android.app.Service
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import at.bitfire.davdroid.ui.setup.LoginActivity
|
||||
|
||||
class AccountAuthenticatorService: Service() {
|
||||
|
||||
private lateinit var accountAuthenticator: AccountAuthenticator
|
||||
|
||||
override fun onCreate() {
|
||||
accountAuthenticator = AccountAuthenticator(this)
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent?) =
|
||||
accountAuthenticator.iBinder.takeIf { intent?.action == android.accounts.AccountManager.ACTION_AUTHENTICATOR_INTENT }
|
||||
|
||||
|
||||
private class AccountAuthenticator(
|
||||
val context: Context
|
||||
): AbstractAccountAuthenticator(context) {
|
||||
|
||||
override fun addAccount(response: AccountAuthenticatorResponse?, accountType: String?, authTokenType: String?, requiredFeatures: Array<String>?, options: Bundle?): Bundle {
|
||||
val intent = Intent(context, LoginActivity::class.java)
|
||||
intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response)
|
||||
val bundle = Bundle(1)
|
||||
bundle.putParcelable(AccountManager.KEY_INTENT, intent)
|
||||
return bundle
|
||||
}
|
||||
|
||||
override fun editProperties(response: AccountAuthenticatorResponse?, accountType: String?) = null
|
||||
override fun getAuthTokenLabel(p0: String?) = null
|
||||
override fun confirmCredentials(p0: AccountAuthenticatorResponse?, p1: Account?, p2: Bundle?) = null
|
||||
override fun updateCredentials(p0: AccountAuthenticatorResponse?, p1: Account?, p2: String?, p3: Bundle?) = null
|
||||
override fun getAuthToken(p0: AccountAuthenticatorResponse?, p1: Account?, p2: String?, p3: Bundle?) = null
|
||||
override fun hasFeatures(p0: AccountAuthenticatorResponse?, p1: Account?, p2: Array<out String>?) = null
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* Copyright © Ricki Hirner (bitfire web engineering).
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the GNU Public License v3.0
|
||||
* which accompanies this distribution, and is available at
|
||||
* http://www.gnu.org/licenses/gpl.html
|
||||
*/
|
||||
|
||||
package at.bitfire.davdroid.syncadapter
|
||||
|
||||
import android.content.ContentProvider
|
||||
import android.content.ContentValues
|
||||
import android.net.Uri
|
||||
|
||||
class AddressBookProvider: ContentProvider() {
|
||||
|
||||
override fun onCreate() = false
|
||||
override fun insert(p0: Uri?, p1: ContentValues?) = null
|
||||
override fun query(p0: Uri?, p1: Array<out String>?, p2: String?, p3: Array<out String>?, p4: String?) = null
|
||||
override fun update(p0: Uri?, p1: ContentValues?, p2: String?, p3: Array<out String>?) = 0
|
||||
override fun delete(p0: Uri?, p1: String?, p2: Array<out String>?) = 0
|
||||
override fun getType(p0: Uri?) = null
|
||||
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
/*
|
||||
* Copyright © Ricki Hirner (bitfire web engineering).
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the GNU Public License v3.0
|
||||
* which accompanies this distribution, and is available at
|
||||
* http://www.gnu.org/licenses/gpl.html
|
||||
*/
|
||||
package at.bitfire.davdroid.syncadapter
|
||||
|
||||
import android.accounts.Account
|
||||
import android.accounts.AccountManager
|
||||
import android.content.*
|
||||
import android.database.DatabaseUtils
|
||||
import android.os.Bundle
|
||||
import android.provider.ContactsContract
|
||||
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
|
||||
import at.bitfire.davdroid.model.ServiceDB.Collections
|
||||
import at.bitfire.davdroid.resource.LocalAddressBook
|
||||
import at.bitfire.vcard4android.ContactsStorageException
|
||||
import java.util.logging.Level
|
||||
|
||||
class AddressBooksSyncAdapterService: SyncAdapterService() {
|
||||
|
||||
override fun syncAdapter() = AddressBooksSyncAdapter(this)
|
||||
|
||||
|
||||
protected class AddressBooksSyncAdapter(
|
||||
context: Context
|
||||
): SyncAdapter(context) {
|
||||
|
||||
override fun sync(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
|
||||
}
|
||||
|
||||
try {
|
||||
val settings = AccountSettings(context, 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))
|
||||
return
|
||||
|
||||
updateLocalAddressBooks(contactsProvider, account)
|
||||
|
||||
val accountManager = AccountManager.get(context)
|
||||
for (addressBookAccount in accountManager.getAccountsByType(context.getString(R.string.account_type_address_book))) {
|
||||
Logger.log.log(Level.INFO, "Running sync for address book", addressBookAccount)
|
||||
val syncExtras = Bundle(extras)
|
||||
syncExtras.putBoolean(ContentResolver.SYNC_EXTRAS_IGNORE_SETTINGS, true)
|
||||
syncExtras.putBoolean(ContentResolver.SYNC_EXTRAS_IGNORE_BACKOFF, true)
|
||||
ContentResolver.requestSync(addressBookAccount, ContactsContract.AUTHORITY, syncExtras)
|
||||
}
|
||||
} 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) {
|
||||
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 ->
|
||||
if (c.moveToNext())
|
||||
c.getLong(0)
|
||||
else
|
||||
null
|
||||
}
|
||||
|
||||
fun remoteAddressBooks(service: Long?): MutableMap<String, CollectionInfo> {
|
||||
val collections = mutableMapOf<String, CollectionInfo>()
|
||||
service?.let {
|
||||
db.query(Collections._TABLE, null,
|
||||
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)
|
||||
val info = CollectionInfo(values)
|
||||
collections[info.url] = info
|
||||
}
|
||||
}
|
||||
}
|
||||
return collections
|
||||
}
|
||||
|
||||
// enumerate remote and local address books
|
||||
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()
|
||||
} 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
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,235 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2013 – 2015 Ricki Hirner (bitfire web engineering).
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the GNU Public License v3.0
|
||||
* which accompanies this distribution, and is available at
|
||||
* http://www.gnu.org/licenses/gpl.html
|
||||
*/
|
||||
|
||||
package at.bitfire.davdroid.syncadapter;
|
||||
|
||||
import android.accounts.Account;
|
||||
import android.content.Context;
|
||||
import android.content.SyncResult;
|
||||
import android.os.Bundle;
|
||||
|
||||
import org.apache.commons.codec.Charsets;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.Calendar;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import at.bitfire.dav4android.DavCalendar;
|
||||
import at.bitfire.dav4android.DavResource;
|
||||
import at.bitfire.dav4android.exception.DavException;
|
||||
import at.bitfire.dav4android.exception.HttpException;
|
||||
import at.bitfire.dav4android.property.CalendarData;
|
||||
import at.bitfire.dav4android.property.GetCTag;
|
||||
import at.bitfire.dav4android.property.GetContentType;
|
||||
import at.bitfire.dav4android.property.GetETag;
|
||||
import at.bitfire.davdroid.AccountSettings;
|
||||
import at.bitfire.davdroid.App;
|
||||
import at.bitfire.davdroid.ArrayUtils;
|
||||
import at.bitfire.davdroid.Constants;
|
||||
import at.bitfire.davdroid.InvalidAccountException;
|
||||
import at.bitfire.davdroid.R;
|
||||
import at.bitfire.davdroid.resource.LocalCalendar;
|
||||
import at.bitfire.davdroid.resource.LocalEvent;
|
||||
import at.bitfire.davdroid.resource.LocalResource;
|
||||
import at.bitfire.ical4android.CalendarStorageException;
|
||||
import at.bitfire.ical4android.Event;
|
||||
import at.bitfire.ical4android.InvalidCalendarException;
|
||||
import at.bitfire.vcard4android.ContactsStorageException;
|
||||
import lombok.Cleanup;
|
||||
import okhttp3.HttpUrl;
|
||||
import okhttp3.MediaType;
|
||||
import okhttp3.RequestBody;
|
||||
import okhttp3.ResponseBody;
|
||||
|
||||
/**
|
||||
* Synchronization manager for CalDAV collections; handles events ({@code VEVENT}).
|
||||
*/
|
||||
public class CalendarSyncManager extends SyncManager {
|
||||
|
||||
protected static final int MAX_MULTIGET = 20;
|
||||
|
||||
|
||||
public CalendarSyncManager(Context context, Account account, AccountSettings settings, Bundle extras, String authority, SyncResult result, LocalCalendar calendar) throws InvalidAccountException {
|
||||
super(context, account, settings, extras, authority, result, "calendar/" + calendar.getId());
|
||||
localCollection = calendar;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int notificationId() {
|
||||
return Constants.NOTIFICATION_CALENDAR_SYNC;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getSyncErrorTitle() {
|
||||
return context.getString(R.string.sync_error_calendar, account.name);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected void prepare() {
|
||||
collectionURL = HttpUrl.parse(localCalendar().getName());
|
||||
davCollection = new DavCalendar(httpClient, collectionURL);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void queryCapabilities() throws DavException, IOException, HttpException {
|
||||
davCollection.propfind(0, GetCTag.NAME);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void prepareDirty() throws CalendarStorageException, ContactsStorageException {
|
||||
super.prepareDirty();
|
||||
|
||||
localCalendar().processDirtyExceptions();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected RequestBody prepareUpload(LocalResource resource) throws IOException, CalendarStorageException {
|
||||
LocalEvent local = (LocalEvent)resource;
|
||||
App.log.log(Level.FINE, "Preparing upload of event " + local.getFileName(), local.getEvent());
|
||||
|
||||
ByteArrayOutputStream os = new ByteArrayOutputStream();
|
||||
local.getEvent().write(os);
|
||||
|
||||
return RequestBody.create(
|
||||
DavCalendar.MIME_ICALENDAR,
|
||||
os.toByteArray()
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void listRemote() throws IOException, HttpException, DavException {
|
||||
// calculate time range limits
|
||||
Date limitStart = null;
|
||||
Integer pastDays = settings.getTimeRangePastDays();
|
||||
if (pastDays != null) {
|
||||
Calendar calendar = Calendar.getInstance();
|
||||
calendar.add(Calendar.DAY_OF_MONTH, -pastDays);
|
||||
limitStart = calendar.getTime();
|
||||
}
|
||||
|
||||
// fetch list of remote VEVENTs and build hash table to index file name
|
||||
davCalendar().calendarQuery("VEVENT", limitStart, null);
|
||||
|
||||
remoteResources = new HashMap<>(davCollection.members.size());
|
||||
for (DavResource iCal : davCollection.members) {
|
||||
String fileName = iCal.fileName();
|
||||
App.log.fine("Found remote VEVENT: " + fileName);
|
||||
remoteResources.put(fileName, iCal);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void downloadRemote() throws IOException, HttpException, DavException, CalendarStorageException {
|
||||
App.log.info("Downloading " + toDownload.size() + " events (" + MAX_MULTIGET + " at once)");
|
||||
|
||||
// download new/updated iCalendars from server
|
||||
for (DavResource[] bunch : ArrayUtils.partition(toDownload.toArray(new DavResource[toDownload.size()]), MAX_MULTIGET)) {
|
||||
if (Thread.interrupted())
|
||||
return;
|
||||
App.log.info("Downloading " + StringUtils.join(bunch, ", "));
|
||||
|
||||
if (bunch.length == 1) {
|
||||
// only one contact, use GET
|
||||
DavResource remote = bunch[0];
|
||||
|
||||
ResponseBody body = remote.get("text/calendar");
|
||||
|
||||
// CalDAV servers MUST return ETag on GET [https://tools.ietf.org/html/rfc4791#section-5.3.4]
|
||||
GetETag eTag = (GetETag)remote.properties.get(GetETag.NAME);
|
||||
if (eTag == null || StringUtils.isEmpty(eTag.eTag))
|
||||
throw new DavException("Received CalDAV GET response without ETag for " + remote.location);
|
||||
|
||||
Charset charset = Charsets.UTF_8;
|
||||
MediaType contentType = body.contentType();
|
||||
if (contentType != null)
|
||||
charset = contentType.charset(Charsets.UTF_8);
|
||||
|
||||
@Cleanup InputStream stream = body.byteStream();
|
||||
processVEvent(remote.fileName(), eTag.eTag, stream, charset);
|
||||
|
||||
} else {
|
||||
// multiple contacts, use multi-get
|
||||
List<HttpUrl> urls = new LinkedList<>();
|
||||
for (DavResource remote : bunch)
|
||||
urls.add(remote.location);
|
||||
davCalendar().multiget(urls.toArray(new HttpUrl[urls.size()]));
|
||||
|
||||
// process multiget results
|
||||
for (DavResource remote : davCollection.members) {
|
||||
String eTag;
|
||||
GetETag getETag = (GetETag)remote.properties.get(GetETag.NAME);
|
||||
if (getETag != null)
|
||||
eTag = getETag.eTag;
|
||||
else
|
||||
throw new DavException("Received multi-get response without ETag");
|
||||
|
||||
Charset charset = Charsets.UTF_8;
|
||||
GetContentType getContentType = (GetContentType)remote.properties.get(GetContentType.NAME);
|
||||
if (getContentType != null && getContentType.type != null) {
|
||||
MediaType type = MediaType.parse(getContentType.type);
|
||||
if (type != null)
|
||||
charset = type.charset(Charsets.UTF_8);
|
||||
}
|
||||
|
||||
CalendarData calendarData = (CalendarData)remote.properties.get(CalendarData.NAME);
|
||||
if (calendarData == null || calendarData.iCalendar == null)
|
||||
throw new DavException("Received multi-get response without address data");
|
||||
|
||||
@Cleanup InputStream stream = new ByteArrayInputStream(calendarData.iCalendar.getBytes());
|
||||
processVEvent(remote.fileName(), eTag, stream, charset);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// helpers
|
||||
|
||||
private LocalCalendar localCalendar() { return ((LocalCalendar)localCollection); }
|
||||
private DavCalendar davCalendar() { return (DavCalendar)davCollection; }
|
||||
|
||||
private void processVEvent(String fileName, String eTag, InputStream stream, Charset charset) throws IOException, CalendarStorageException {
|
||||
Event[] events;
|
||||
try {
|
||||
events = Event.fromStream(stream, charset);
|
||||
} catch (InvalidCalendarException e) {
|
||||
App.log.log(Level.SEVERE, "Received invalid iCalendar, ignoring", e);
|
||||
return;
|
||||
}
|
||||
|
||||
if (events.length == 1) {
|
||||
Event newData = events[0];
|
||||
|
||||
// delete local event, if it exists
|
||||
LocalEvent localEvent = (LocalEvent)localResources.get(fileName);
|
||||
if (localEvent != null) {
|
||||
App.log.info("Updating " + fileName + " in local calendar");
|
||||
localEvent.setETag(eTag);
|
||||
localEvent.update(newData);
|
||||
syncResult.stats.numUpdates++;
|
||||
} else {
|
||||
App.log.info("Adding " + fileName + " to local calendar");
|
||||
localEvent = new LocalEvent(localCalendar(), newData, fileName, eTag);
|
||||
localEvent.add();
|
||||
syncResult.stats.numInserts++;
|
||||
}
|
||||
} else
|
||||
App.log.severe("Received VCALENDAR with not exactly one VEVENT with UID, but without RECURRENCE-ID; ignoring " + fileName);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
/*
|
||||
* Copyright © Ricki Hirner (bitfire web engineering).
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the GNU Public License v3.0
|
||||
* which accompanies this distribution, and is available at
|
||||
* http://www.gnu.org/licenses/gpl.html
|
||||
*/
|
||||
|
||||
package at.bitfire.davdroid.syncadapter
|
||||
|
||||
import android.accounts.Account
|
||||
import android.content.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.exception.DavException
|
||||
import at.bitfire.dav4android.property.CalendarData
|
||||
import at.bitfire.dav4android.property.GetCTag
|
||||
import at.bitfire.dav4android.property.GetETag
|
||||
import at.bitfire.davdroid.AccountSettings
|
||||
import at.bitfire.davdroid.Constants
|
||||
import at.bitfire.davdroid.R
|
||||
import at.bitfire.davdroid.log.Logger
|
||||
import at.bitfire.davdroid.resource.LocalCalendar
|
||||
import at.bitfire.davdroid.resource.LocalEvent
|
||||
import at.bitfire.davdroid.resource.LocalResource
|
||||
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
|
||||
import java.util.*
|
||||
import java.util.logging.Level
|
||||
|
||||
/**
|
||||
* Synchronization manager for CalDAV collections; handles events ({@code VEVENT}).
|
||||
*/
|
||||
class CalendarSyncManager(
|
||||
context: Context,
|
||||
account: Account,
|
||||
settings: 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)!!
|
||||
|
||||
|
||||
override fun prepare(): Boolean {
|
||||
collectionURL = HttpUrl.parse(localCalendar.name ?: return false) ?: return false
|
||||
davCollection = DavCalendar(httpClient, collectionURL)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun queryCapabilities() {
|
||||
davCollection.propfind(0, GetCTag.NAME)
|
||||
}
|
||||
|
||||
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() {
|
||||
// calculate time range limits
|
||||
var limitStart: Date? = null
|
||||
settings.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
|
||||
}
|
||||
|
||||
currentDavResource = null
|
||||
}
|
||||
|
||||
override fun downloadRemote() {
|
||||
Logger.log.info("Downloading ${toDownload.size} events ($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 ->
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
currentDavResource = null
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// helpers
|
||||
|
||||
private fun davCalendar() = davCollection as DavCalendar
|
||||
|
||||
private fun processVEvent(fileName: String, eTag: String, reader: Reader) {
|
||||
val events: List<Event>
|
||||
try {
|
||||
events = Event.fromReader(reader)
|
||||
} catch (e: InvalidCalendarException) {
|
||||
Logger.log.log(Level.SEVERE, "Received invalid iCalendar, ignoring", e)
|
||||
return
|
||||
}
|
||||
|
||||
if (events.size == 1) {
|
||||
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++
|
||||
}
|
||||
} else
|
||||
Logger.log.severe("Received VCALENDAR with not exactly one VEVENT with UID, but without RECURRENCE-ID; ignoring $fileName")
|
||||
|
||||
currentLocalResource = null
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,146 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2013 – 2015 Ricki Hirner (bitfire web engineering).
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the GNU Public License v3.0
|
||||
* which accompanies this distribution, and is available at
|
||||
* http://www.gnu.org/licenses/gpl.html
|
||||
*/
|
||||
package at.bitfire.davdroid.syncadapter;
|
||||
|
||||
import android.accounts.Account;
|
||||
import android.content.AbstractThreadedSyncAdapter;
|
||||
import android.content.ContentProviderClient;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.content.SyncResult;
|
||||
import android.database.Cursor;
|
||||
import android.database.DatabaseUtils;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteOpenHelper;
|
||||
import android.os.Bundle;
|
||||
import android.provider.CalendarContract;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import at.bitfire.davdroid.AccountSettings;
|
||||
import at.bitfire.davdroid.App;
|
||||
import at.bitfire.davdroid.InvalidAccountException;
|
||||
import at.bitfire.davdroid.model.CollectionInfo;
|
||||
import at.bitfire.davdroid.model.ServiceDB;
|
||||
import at.bitfire.davdroid.model.ServiceDB.Collections;
|
||||
import at.bitfire.davdroid.model.ServiceDB.Services;
|
||||
import at.bitfire.davdroid.resource.LocalCalendar;
|
||||
import at.bitfire.ical4android.CalendarStorageException;
|
||||
import lombok.Cleanup;
|
||||
|
||||
public class CalendarsSyncAdapterService extends SyncAdapterService {
|
||||
|
||||
@Override
|
||||
protected AbstractThreadedSyncAdapter syncAdapter() {
|
||||
return new SyncAdapter(this);
|
||||
}
|
||||
|
||||
|
||||
private static class SyncAdapter extends SyncAdapterService.SyncAdapter {
|
||||
|
||||
public SyncAdapter(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sync(Account account, Bundle extras, String authority, ContentProviderClient provider, SyncResult syncResult) {
|
||||
try {
|
||||
AccountSettings settings = new AccountSettings(getContext(), account);
|
||||
if (!extras.containsKey(ContentResolver.SYNC_EXTRAS_MANUAL) && !checkSyncConditions(settings))
|
||||
return;
|
||||
|
||||
updateLocalCalendars(provider, account, settings);
|
||||
|
||||
for (LocalCalendar calendar : (LocalCalendar[])LocalCalendar.find(account, provider, LocalCalendar.Factory.INSTANCE, CalendarContract.Calendars.SYNC_EVENTS + "!=0", null)) {
|
||||
App.log.info("Synchronizing calendar #" + calendar.getId() + ", URL: " + calendar.getName());
|
||||
CalendarSyncManager syncManager = new CalendarSyncManager(getContext(), account, settings, extras, authority, syncResult, calendar);
|
||||
syncManager.performSync();
|
||||
}
|
||||
} catch (CalendarStorageException e) {
|
||||
App.log.log(Level.SEVERE, "Couldn't enumerate local calendars", e);
|
||||
syncResult.databaseError = true;
|
||||
} catch (InvalidAccountException e) {
|
||||
App.log.log(Level.SEVERE, "Couldn't get account settings", e);
|
||||
}
|
||||
|
||||
App.log.info("Calendar sync complete");
|
||||
}
|
||||
|
||||
private void updateLocalCalendars(ContentProviderClient provider, Account account, AccountSettings settings) throws CalendarStorageException {
|
||||
SQLiteOpenHelper dbHelper = new ServiceDB.OpenHelper(getContext());
|
||||
try {
|
||||
// enumerate remote and local calendars
|
||||
SQLiteDatabase db = dbHelper.getReadableDatabase();
|
||||
Long service = getService(db, account);
|
||||
Map<String, CollectionInfo> remote = remoteCalendars(db, service);
|
||||
|
||||
LocalCalendar[] local = (LocalCalendar[])LocalCalendar.find(account, provider, LocalCalendar.Factory.INSTANCE, null, null);
|
||||
|
||||
boolean updateColors = settings.getManageCalendarColors();
|
||||
|
||||
// delete obsolete local calendar
|
||||
for (LocalCalendar calendar : local) {
|
||||
String url = calendar.getName();
|
||||
if (!remote.containsKey(url)) {
|
||||
App.log.fine("Deleting obsolete local calendar " + url);
|
||||
calendar.delete();
|
||||
} else {
|
||||
// remote CollectionInfo found for this local collection, update data
|
||||
CollectionInfo info = remote.get(url);
|
||||
App.log.fine("Updating local calendar " + url + " with " + info);
|
||||
calendar.update(info, updateColors);
|
||||
// we already have a local calendar for this remote collection, don't take into consideration anymore
|
||||
remote.remove(url);
|
||||
}
|
||||
}
|
||||
|
||||
// create new local calendars
|
||||
for (String url : remote.keySet()) {
|
||||
CollectionInfo info = remote.get(url);
|
||||
App.log.info("Adding local calendar list " + info);
|
||||
LocalCalendar.create(account, provider, info);
|
||||
}
|
||||
} finally {
|
||||
dbHelper.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
Long getService(@NonNull SQLiteDatabase db, @NonNull Account account) {
|
||||
@Cleanup Cursor c = db.query(Services._TABLE, new String[] { Services.ID },
|
||||
Services.ACCOUNT_NAME + "=? AND " + Services.SERVICE + "=?", new String[] { account.name, Services.SERVICE_CALDAV }, null, null, null);
|
||||
if (c.moveToNext())
|
||||
return c.getLong(0);
|
||||
else
|
||||
return null;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private Map<String, CollectionInfo> remoteCalendars(@NonNull SQLiteDatabase db, Long service) {
|
||||
Map<String, CollectionInfo> collections = new LinkedHashMap<>();
|
||||
if (service != null) {
|
||||
@Cleanup Cursor cursor = db.query(Collections._TABLE, null,
|
||||
Collections.SERVICE_ID + "=? AND " + Collections.SUPPORTS_VEVENT + "!=0 AND " + Collections.SYNC,
|
||||
new String[] { String.valueOf(service) }, null, null, null);
|
||||
while (cursor.moveToNext()) {
|
||||
ContentValues values = new ContentValues();
|
||||
DatabaseUtils.cursorRowToContentValues(cursor, values);
|
||||
CollectionInfo info = CollectionInfo.fromDB(values);
|
||||
collections.put(info.url, info);
|
||||
}
|
||||
}
|
||||
return collections;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
/*
|
||||
* Copyright © Ricki Hirner (bitfire web engineering).
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the GNU Public License v3.0
|
||||
* which accompanies this distribution, and is available at
|
||||
* http://www.gnu.org/licenses/gpl.html
|
||||
*/
|
||||
package at.bitfire.davdroid.syncadapter
|
||||
|
||||
import android.accounts.Account
|
||||
import android.content.*
|
||||
import android.database.DatabaseUtils
|
||||
import android.os.Bundle
|
||||
import android.provider.CalendarContract
|
||||
import at.bitfire.davdroid.AccountSettings
|
||||
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.LocalCalendar
|
||||
import at.bitfire.ical4android.AndroidCalendar
|
||||
import java.util.logging.Level
|
||||
|
||||
class CalendarsSyncAdapterService: SyncAdapterService() {
|
||||
|
||||
override fun syncAdapter() = SyncAdapter(this)
|
||||
|
||||
|
||||
protected class SyncAdapter(
|
||||
context: Context
|
||||
): SyncAdapterService.SyncAdapter(context) {
|
||||
|
||||
override fun sync(account: Account, extras: Bundle, authority: String, provider: ContentProviderClient, syncResult: SyncResult) {
|
||||
try {
|
||||
val settings = AccountSettings(context, 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))
|
||||
return
|
||||
|
||||
updateLocalCalendars(provider, account, settings)
|
||||
|
||||
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, settings, extras, authority, syncResult, provider, calendar)
|
||||
.performSync()
|
||||
}
|
||||
} catch(e: Exception) {
|
||||
Logger.log.log(Level.SEVERE, "Couldn't sync calendars", e)
|
||||
}
|
||||
|
||||
Logger.log.info("Calendar sync complete")
|
||||
}
|
||||
|
||||
private fun updateLocalCalendars(provider: ContentProviderClient, account: Account, settings: AccountSettings) {
|
||||
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_CALDAV), null, null, null)?.use { c ->
|
||||
if (c.moveToNext())
|
||||
c.getLong(0)
|
||||
else
|
||||
null
|
||||
}
|
||||
|
||||
fun remoteCalendars(service: Long?): MutableMap<String, CollectionInfo> {
|
||||
val collections = mutableMapOf<String, CollectionInfo>()
|
||||
service?.let {
|
||||
db.query(Collections._TABLE, null,
|
||||
"${Collections.SERVICE_ID}=? AND ${Collections.SUPPORTS_VEVENT}!=0 AND ${Collections.SYNC}",
|
||||
arrayOf(service.toString()), null, null, null).use { cursor ->
|
||||
while (cursor.moveToNext()) {
|
||||
val values = ContentValues(cursor.columnCount)
|
||||
DatabaseUtils.cursorRowToContentValues(cursor, values)
|
||||
val info = CollectionInfo(values)
|
||||
collections[info.url] = info
|
||||
}
|
||||
}
|
||||
}
|
||||
return collections
|
||||
}
|
||||
|
||||
// enumerate remote and local calendars
|
||||
val service = getService()
|
||||
val remote = remoteCalendars(service)
|
||||
|
||||
// delete/update local calendars
|
||||
val updateColors = settings.getManageCalendarColors()
|
||||
|
||||
for (calendar in AndroidCalendar.find(account, provider, LocalCalendar.Factory, null, null))
|
||||
calendar.name?.let { url ->
|
||||
val info = remote[url]
|
||||
if (info == null) {
|
||||
Logger.log.log(Level.INFO, "Deleting obsolete local calendar", url)
|
||||
calendar.delete()
|
||||
} else {
|
||||
// remote CollectionInfo found for this local collection, update data
|
||||
Logger.log.log(Level.FINE, "Updating local calendar $url", info)
|
||||
calendar.update(info, updateColors)
|
||||
// we already have a local calendar for this remote collection, don't take into consideration anymore
|
||||
remote -= url
|
||||
}
|
||||
}
|
||||
|
||||
// create new local calendars
|
||||
for ((_, info) in remote) {
|
||||
Logger.log.log(Level.INFO, "Adding local calendar", info)
|
||||
LocalCalendar.create(account, provider, info)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2013 – 2015 Ricki Hirner (bitfire web engineering).
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the GNU Public License v3.0
|
||||
* which accompanies this distribution, and is available at
|
||||
* http://www.gnu.org/licenses/gpl.html
|
||||
*/
|
||||
package at.bitfire.davdroid.syncadapter;
|
||||
|
||||
import android.accounts.Account;
|
||||
import android.content.AbstractThreadedSyncAdapter;
|
||||
import android.content.ContentProviderClient;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.content.SyncResult;
|
||||
import android.database.Cursor;
|
||||
import android.database.DatabaseUtils;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteOpenHelper;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import java.util.logging.Level;
|
||||
|
||||
import at.bitfire.davdroid.AccountSettings;
|
||||
import at.bitfire.davdroid.App;
|
||||
import at.bitfire.davdroid.InvalidAccountException;
|
||||
import at.bitfire.davdroid.model.CollectionInfo;
|
||||
import at.bitfire.davdroid.model.ServiceDB;
|
||||
import at.bitfire.davdroid.model.ServiceDB.Collections;
|
||||
import lombok.Cleanup;
|
||||
|
||||
public class ContactsSyncAdapterService extends SyncAdapterService {
|
||||
|
||||
@Override
|
||||
protected AbstractThreadedSyncAdapter syncAdapter() {
|
||||
return new ContactsSyncAdapter(this);
|
||||
}
|
||||
|
||||
|
||||
private static class ContactsSyncAdapter extends SyncAdapter {
|
||||
|
||||
public ContactsSyncAdapter(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sync(Account account, Bundle extras, String authority, ContentProviderClient provider, SyncResult syncResult) {
|
||||
SQLiteOpenHelper dbHelper = new ServiceDB.OpenHelper(getContext());
|
||||
try {
|
||||
AccountSettings settings = new AccountSettings(getContext(), account);
|
||||
if (!extras.containsKey(ContentResolver.SYNC_EXTRAS_MANUAL) && !checkSyncConditions(settings))
|
||||
return;
|
||||
|
||||
SQLiteDatabase db = dbHelper.getReadableDatabase();
|
||||
Long service = getService(db, account);
|
||||
if (service != null) {
|
||||
CollectionInfo remote = remoteAddressBook(db, service);
|
||||
if (remote != null)
|
||||
try {
|
||||
ContactsSyncManager syncManager = new ContactsSyncManager(getContext(), account, settings, extras, authority, provider, syncResult, remote);
|
||||
syncManager.performSync();
|
||||
} catch(InvalidAccountException e) {
|
||||
App.log.log(Level.SEVERE, "Couldn't get account settings", e);
|
||||
}
|
||||
else
|
||||
App.log.info("No address book collection selected for synchronization");
|
||||
} else
|
||||
App.log.info("No CardDAV service found in DB");
|
||||
} catch (InvalidAccountException e) {
|
||||
App.log.log(Level.SEVERE, "Couldn't get account settings", e);
|
||||
} finally {
|
||||
dbHelper.close();
|
||||
}
|
||||
|
||||
App.log.info("Address book sync complete");
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private Long getService(@NonNull SQLiteDatabase db, @NonNull Account account) {
|
||||
@Cleanup Cursor c = db.query(ServiceDB.Services._TABLE, new String[] { ServiceDB.Services.ID },
|
||||
ServiceDB.Services.ACCOUNT_NAME + "=? AND " + ServiceDB.Services.SERVICE + "=?", new String[] { account.name, ServiceDB.Services.SERVICE_CARDDAV }, null, null, null);
|
||||
if (c.moveToNext())
|
||||
return c.getLong(0);
|
||||
else
|
||||
return null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private CollectionInfo remoteAddressBook(@NonNull SQLiteDatabase db, long service) {
|
||||
@Cleanup Cursor c = db.query(Collections._TABLE, null,
|
||||
Collections.SERVICE_ID + "=? AND " + Collections.SYNC, new String[] { String.valueOf(service) }, null, null, null);
|
||||
if (c.moveToNext()) {
|
||||
ContentValues values = new ContentValues();
|
||||
DatabaseUtils.cursorRowToContentValues(c, values);
|
||||
return CollectionInfo.fromDB(values);
|
||||
} else
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Copyright © Ricki Hirner (bitfire web engineering).
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the GNU Public License v3.0
|
||||
* which accompanies this distribution, and is available at
|
||||
* http://www.gnu.org/licenses/gpl.html
|
||||
*/
|
||||
|
||||
package at.bitfire.davdroid.syncadapter
|
||||
|
||||
import android.accounts.Account
|
||||
import android.content.ContentProviderClient
|
||||
import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
import android.content.SyncResult
|
||||
import android.os.Bundle
|
||||
import at.bitfire.davdroid.AccountSettings
|
||||
import at.bitfire.davdroid.log.Logger
|
||||
import at.bitfire.davdroid.resource.LocalAddressBook
|
||||
import java.util.logging.Level
|
||||
|
||||
class ContactsSyncAdapterService: SyncAdapterService() {
|
||||
|
||||
override fun syncAdapter() = ContactsSyncAdapter(this)
|
||||
|
||||
|
||||
protected class ContactsSyncAdapter(
|
||||
context: Context
|
||||
): SyncAdapter(context) {
|
||||
|
||||
override fun sync(account: Account, extras: Bundle, authority: String, provider: ContentProviderClient, syncResult: SyncResult) {
|
||||
try {
|
||||
val addressBook = LocalAddressBook(context, account, provider)
|
||||
|
||||
val settings = AccountSettings(context, addressBook.getMainAccount())
|
||||
if (!extras.containsKey(ContentResolver.SYNC_EXTRAS_MANUAL) && !checkSyncConditions(settings))
|
||||
return
|
||||
|
||||
Logger.log.info("Synchronizing address book: ${addressBook.getURL()}")
|
||||
Logger.log.info("Taking settings from: ${addressBook.getMainAccount()}")
|
||||
|
||||
ContactsSyncManager(context, account, settings, extras, authority, syncResult, provider, addressBook)
|
||||
.performSync()
|
||||
} catch(e: Exception) {
|
||||
Logger.log.log(Level.SEVERE, "Couldn't sync contacts", e)
|
||||
}
|
||||
|
||||
Logger.log.info("Contacts sync complete")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,528 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2013 – 2015 Ricki Hirner (bitfire web engineering).
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the GNU Public License v3.0
|
||||
* which accompanies this distribution, and is available at
|
||||
* http://www.gnu.org/licenses/gpl.html
|
||||
*/
|
||||
|
||||
package at.bitfire.davdroid.syncadapter;
|
||||
|
||||
import android.accounts.Account;
|
||||
import android.content.ContentProviderClient;
|
||||
import android.content.ContentProviderOperation;
|
||||
import android.content.ContentUris;
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.content.SyncResult;
|
||||
import android.database.Cursor;
|
||||
import android.os.Bundle;
|
||||
import android.os.RemoteException;
|
||||
import android.provider.ContactsContract;
|
||||
import android.provider.ContactsContract.Groups;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import org.apache.commons.codec.Charsets;
|
||||
import org.apache.commons.collections4.SetUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import at.bitfire.dav4android.DavAddressBook;
|
||||
import at.bitfire.dav4android.DavResource;
|
||||
import at.bitfire.dav4android.exception.DavException;
|
||||
import at.bitfire.dav4android.exception.HttpException;
|
||||
import at.bitfire.dav4android.property.AddressData;
|
||||
import at.bitfire.dav4android.property.GetCTag;
|
||||
import at.bitfire.dav4android.property.GetContentType;
|
||||
import at.bitfire.dav4android.property.GetETag;
|
||||
import at.bitfire.dav4android.property.SupportedAddressData;
|
||||
import at.bitfire.davdroid.AccountSettings;
|
||||
import at.bitfire.davdroid.App;
|
||||
import at.bitfire.davdroid.ArrayUtils;
|
||||
import at.bitfire.davdroid.Constants;
|
||||
import at.bitfire.davdroid.HttpClient;
|
||||
import at.bitfire.davdroid.InvalidAccountException;
|
||||
import at.bitfire.davdroid.R;
|
||||
import at.bitfire.davdroid.model.CollectionInfo;
|
||||
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.ical4android.CalendarStorageException;
|
||||
import at.bitfire.vcard4android.BatchOperation;
|
||||
import at.bitfire.vcard4android.Contact;
|
||||
import at.bitfire.vcard4android.ContactsStorageException;
|
||||
import at.bitfire.vcard4android.GroupMethod;
|
||||
import ezvcard.VCardVersion;
|
||||
import ezvcard.util.IOUtils;
|
||||
import lombok.Cleanup;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import okhttp3.HttpUrl;
|
||||
import okhttp3.MediaType;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.RequestBody;
|
||||
import okhttp3.Response;
|
||||
import okhttp3.ResponseBody;
|
||||
|
||||
/**
|
||||
* <p>Synchronization manager for CardDAV collections; handles contacts and groups.</p>
|
||||
*
|
||||
* <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>
|
||||
*/
|
||||
public class ContactsSyncManager extends SyncManager {
|
||||
protected static final int MAX_MULTIGET = 10;
|
||||
|
||||
final private ContentProviderClient provider;
|
||||
final private CollectionInfo remote;
|
||||
|
||||
private boolean hasVCard4;
|
||||
private GroupMethod groupMethod;
|
||||
|
||||
|
||||
public ContactsSyncManager(Context context, Account account, AccountSettings settings, Bundle extras, String authority, ContentProviderClient provider, SyncResult result, CollectionInfo remote) throws InvalidAccountException {
|
||||
super(context, account, settings, extras, authority, result, "addressBook");
|
||||
this.provider = provider;
|
||||
this.remote = remote;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int notificationId() {
|
||||
return Constants.NOTIFICATION_CONTACTS_SYNC;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getSyncErrorTitle() {
|
||||
return context.getString(R.string.sync_error_contacts, account.name);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected void prepare() throws ContactsStorageException {
|
||||
// prepare local address book
|
||||
localCollection = new LocalAddressBook(account, provider);
|
||||
LocalAddressBook localAddressBook = localAddressBook();
|
||||
|
||||
String url = remote.url;
|
||||
String lastUrl = localAddressBook.getURL();
|
||||
if (!url.equals(lastUrl)) {
|
||||
App.log.info("Selected address book has changed from " + lastUrl + " to " + url + ", deleting all local contacts");
|
||||
localAddressBook.deleteAll();
|
||||
}
|
||||
|
||||
// set up Contacts Provider Settings
|
||||
ContentValues values = new ContentValues(2);
|
||||
values.put(ContactsContract.Settings.SHOULD_SYNC, 1);
|
||||
values.put(ContactsContract.Settings.UNGROUPED_VISIBLE, 1);
|
||||
localAddressBook.updateSettings(values);
|
||||
|
||||
collectionURL = HttpUrl.parse(url);
|
||||
davCollection = new DavAddressBook(httpClient, collectionURL);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void queryCapabilities() throws DavException, IOException, HttpException {
|
||||
// prepare remote address book
|
||||
davCollection.propfind(0, SupportedAddressData.NAME, GetCTag.NAME);
|
||||
SupportedAddressData supportedAddressData = (SupportedAddressData)davCollection.properties.get(SupportedAddressData.NAME);
|
||||
hasVCard4 = supportedAddressData != null && supportedAddressData.hasVCard4();
|
||||
App.log.info("Server advertises VCard/4 support: " + hasVCard4);
|
||||
|
||||
groupMethod = settings.getGroupMethod();
|
||||
App.log.info("Contact group method: " + groupMethod);
|
||||
|
||||
localAddressBook().includeGroups = groupMethod == GroupMethod.GROUP_VCARDS;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void prepareDirty() throws CalendarStorageException, ContactsStorageException {
|
||||
super.prepareDirty();
|
||||
|
||||
LocalAddressBook addressBook = localAddressBook();
|
||||
|
||||
if (groupMethod == GroupMethod.CATEGORIES) {
|
||||
/* groups memberships are represented as contact CATEGORIES */
|
||||
|
||||
// groups with DELETED=1: set all members to dirty, then remove group
|
||||
for (LocalGroup group : addressBook.getDeletedGroups()) {
|
||||
App.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();
|
||||
}
|
||||
|
||||
// groups with DIRTY=1: mark all memberships as dirty, then clean DIRTY flag of group
|
||||
for (LocalGroup group : addressBook.getDirtyGroups()) {
|
||||
App.log.fine("Marking members of modified group " + group + " as dirty");
|
||||
group.markMembersDirty();
|
||||
group.clearDirty(null);
|
||||
}
|
||||
} else {
|
||||
/* groups as separate VCards: there are group contacts and individual contacts */
|
||||
|
||||
// mark groups with changed members as dirty
|
||||
BatchOperation batch = new BatchOperation(addressBook.provider);
|
||||
for (LocalContact contact : addressBook.getDirtyContacts())
|
||||
try {
|
||||
App.log.fine("Looking for changed group memberships of contact " + contact.getFileName());
|
||||
Set<Long> cachedGroups = contact.getCachedGroupMemberships(),
|
||||
currentGroups = contact.getGroupMemberships();
|
||||
for (Long groupID : SetUtils.disjunction(cachedGroups, currentGroups)) {
|
||||
App.log.fine("Marking group as dirty: " + groupID);
|
||||
batch.enqueue(new BatchOperation.Operation(
|
||||
ContentProviderOperation.newUpdate(addressBook.syncAdapterURI(ContentUris.withAppendedId(Groups.CONTENT_URI, groupID)))
|
||||
.withValue(Groups.DIRTY, 1)
|
||||
.withYieldAllowed(true)
|
||||
));
|
||||
}
|
||||
} catch(FileNotFoundException ignored) {
|
||||
}
|
||||
batch.commit();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected RequestBody prepareUpload(@NonNull LocalResource resource) throws IOException, ContactsStorageException {
|
||||
final Contact contact;
|
||||
if (resource instanceof LocalContact) {
|
||||
LocalContact local = ((LocalContact)resource);
|
||||
contact = local.getContact();
|
||||
|
||||
if (groupMethod == GroupMethod.CATEGORIES) {
|
||||
// add groups as CATEGORIES
|
||||
for (long groupID : local.getGroupMemberships()) {
|
||||
try {
|
||||
@Cleanup Cursor c = provider.query(
|
||||
localAddressBook().syncAdapterURI(ContentUris.withAppendedId(Groups.CONTENT_URI, groupID)),
|
||||
new String[] { Groups.TITLE },
|
||||
null, null,
|
||||
null
|
||||
);
|
||||
if (c != null && c.moveToNext()) {
|
||||
String title = c.getString(0);
|
||||
if (!TextUtils.isEmpty(title))
|
||||
contact.categories.add(title);
|
||||
}
|
||||
} catch(RemoteException e) {
|
||||
throw new ContactsStorageException("Couldn't find group for adding CATEGORIES", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (resource instanceof LocalGroup)
|
||||
contact = ((LocalGroup)resource).getContact();
|
||||
else
|
||||
throw new IllegalArgumentException("Argument must be LocalContact or LocalGroup");
|
||||
|
||||
App.log.log(Level.FINE, "Preparing upload of VCard " + resource.getFileName(), contact);
|
||||
|
||||
ByteArrayOutputStream os = new ByteArrayOutputStream();
|
||||
contact.write(hasVCard4 ? VCardVersion.V4_0 : VCardVersion.V3_0, groupMethod, settings.getVCardRFC6868(), os);
|
||||
|
||||
return RequestBody.create(
|
||||
hasVCard4 ? DavAddressBook.MIME_VCARD4 : DavAddressBook.MIME_VCARD3_UTF8,
|
||||
os.toByteArray()
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void listRemote() throws IOException, HttpException, DavException {
|
||||
// fetch list of remote VCards and build hash table to index file name
|
||||
|
||||
try {
|
||||
davAddressBook().addressbookQuery();
|
||||
} catch(HttpException e) {
|
||||
/* non-successful responses to CARDDAV:addressbook-query with empty filter, tested on 2015/10/21
|
||||
* fastmail.com 403 Forbidden (DAV:error CARDDAV:supported-filter)
|
||||
* mailbox.org (OpenXchange) 400 Bad Request
|
||||
* SOGo 207 Multi-status, but without entries http://www.sogo.nu/bugs/view.php?id=3370
|
||||
* Zimbra ZCS 500 Server Error https://bugzilla.zimbra.com/show_bug.cgi?id=101902
|
||||
*/
|
||||
if (e.status == 400 || e.status == 403 || e.status == 500 || e.status == 501) {
|
||||
App.log.log(Level.WARNING, "Server error on REPORT addressbook-query, falling back to PROPFIND", e);
|
||||
davAddressBook().propfind(1, GetETag.NAME);
|
||||
} else
|
||||
// no defined fallback, pass through exception
|
||||
throw e;
|
||||
}
|
||||
|
||||
remoteResources = new HashMap<>(davCollection.members.size());
|
||||
for (DavResource vCard : davCollection.members) {
|
||||
String fileName = vCard.fileName();
|
||||
App.log.fine("Found remote VCard: " + fileName);
|
||||
remoteResources.put(fileName, vCard);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void downloadRemote() throws IOException, HttpException, DavException, ContactsStorageException {
|
||||
App.log.info("Downloading " + toDownload.size() + " contacts (" + MAX_MULTIGET + " at once)");
|
||||
|
||||
// prepare downloader which may be used to download external resource like contact photos
|
||||
Contact.Downloader downloader = new ResourceDownloader(collectionURL);
|
||||
|
||||
// download new/updated VCards from server
|
||||
for (DavResource[] bunch : ArrayUtils.partition(toDownload.toArray(new DavResource[toDownload.size()]), MAX_MULTIGET)) {
|
||||
if (Thread.interrupted())
|
||||
return;
|
||||
|
||||
App.log.info("Downloading " + StringUtils.join(bunch, ", "));
|
||||
|
||||
if (bunch.length == 1) {
|
||||
// only one contact, use GET
|
||||
DavResource remote = bunch[0];
|
||||
|
||||
ResponseBody 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]
|
||||
GetETag eTag = (GetETag)remote.properties.get(GetETag.NAME);
|
||||
if (eTag == null || StringUtils.isEmpty(eTag.eTag))
|
||||
throw new DavException("Received CardDAV GET response without ETag for " + remote.location);
|
||||
|
||||
Charset charset = Charsets.UTF_8;
|
||||
MediaType contentType = body.contentType();
|
||||
if (contentType != null)
|
||||
charset = contentType.charset(Charsets.UTF_8);
|
||||
|
||||
@Cleanup InputStream stream = body.byteStream();
|
||||
processVCard(remote.fileName(), eTag.eTag, stream, charset, downloader);
|
||||
|
||||
} else {
|
||||
// multiple contacts, use multi-get
|
||||
List<HttpUrl> urls = new LinkedList<>();
|
||||
for (DavResource remote : bunch)
|
||||
urls.add(remote.location);
|
||||
davAddressBook().multiget(urls.toArray(new HttpUrl[urls.size()]), hasVCard4);
|
||||
|
||||
// process multiget results
|
||||
for (DavResource remote : davCollection.members) {
|
||||
String eTag;
|
||||
GetETag getETag = (GetETag)remote.properties.get(GetETag.NAME);
|
||||
if (getETag != null)
|
||||
eTag = getETag.eTag;
|
||||
else
|
||||
throw new DavException("Received multi-get response without ETag");
|
||||
|
||||
Charset charset = Charsets.UTF_8;
|
||||
GetContentType getContentType = (GetContentType)remote.properties.get(GetContentType.NAME);
|
||||
if (getContentType != null && getContentType.type != null) {
|
||||
MediaType type = MediaType.parse(getContentType.type);
|
||||
if (type != null)
|
||||
charset = type.charset(Charsets.UTF_8);
|
||||
}
|
||||
|
||||
AddressData addressData = (AddressData)remote.properties.get(AddressData.NAME);
|
||||
if (addressData == null || addressData.vCard == null)
|
||||
throw new DavException("Received multi-get response without address data");
|
||||
|
||||
@Cleanup InputStream stream = new ByteArrayInputStream(addressData.vCard.getBytes());
|
||||
processVCard(remote.fileName(), eTag, stream, charset, downloader);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void postProcess() throws CalendarStorageException, ContactsStorageException {
|
||||
if (groupMethod == GroupMethod.CATEGORIES) {
|
||||
/* VCard3 group handling: groups memberships are represented as contact CATEGORIES */
|
||||
|
||||
// remove empty groups
|
||||
App.log.info("Removing empty groups");
|
||||
localAddressBook().removeEmptyGroups();
|
||||
|
||||
} else {
|
||||
/* VCard4 group handling: there are group contacts and individual contacts */
|
||||
App.log.info("Assigning memberships of downloaded contact groups");
|
||||
LocalGroup.applyPendingMemberships(localAddressBook());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void saveSyncState() throws CalendarStorageException, ContactsStorageException {
|
||||
super.saveSyncState();
|
||||
((LocalAddressBook)localCollection).setURL(remote.url);
|
||||
}
|
||||
|
||||
|
||||
// helpers
|
||||
|
||||
private DavAddressBook davAddressBook() { return (DavAddressBook)davCollection; }
|
||||
private LocalAddressBook localAddressBook() { return (LocalAddressBook)localCollection; }
|
||||
|
||||
private void processVCard(String fileName, String eTag, InputStream stream, Charset charset, Contact.Downloader downloader) throws IOException, ContactsStorageException {
|
||||
App.log.info("Processing CardDAV resource " + fileName);
|
||||
Contact[] contacts = Contact.fromStream(stream, charset, downloader);
|
||||
if (contacts.length == 0) {
|
||||
App.log.warning("Received VCard without data, ignoring");
|
||||
return;
|
||||
} else if (contacts.length > 1)
|
||||
App.log.warning("Received multiple VCards, using first one");
|
||||
|
||||
final Contact newData = contacts[0];
|
||||
|
||||
if (groupMethod == GroupMethod.CATEGORIES && newData.group) {
|
||||
groupMethod = GroupMethod.GROUP_VCARDS;
|
||||
App.log.warning("Received group VCard although group method is CATEGORIES. Deleting all groups; new group method: " + groupMethod);
|
||||
localAddressBook().removeGroups();
|
||||
settings.setGroupMethod(groupMethod);
|
||||
}
|
||||
|
||||
// update local contact, if it exists
|
||||
LocalResource local = localResources.get(fileName);
|
||||
if (local != null) {
|
||||
App.log.log(Level.INFO, "Updating " + fileName + " in local address book", newData);
|
||||
|
||||
if (local instanceof LocalGroup && newData.group) {
|
||||
// update group
|
||||
LocalGroup group = (LocalGroup)local;
|
||||
group.eTag = eTag;
|
||||
group.updateFromServer(newData);
|
||||
syncResult.stats.numUpdates++;
|
||||
|
||||
} else if (local instanceof LocalContact && !newData.group) {
|
||||
// update contact
|
||||
LocalContact contact = (LocalContact)local;
|
||||
contact.eTag = eTag;
|
||||
contact.update(newData);
|
||||
syncResult.stats.numUpdates++;
|
||||
|
||||
} else {
|
||||
// group has become an individual contact or vice versa
|
||||
try {
|
||||
local.delete();
|
||||
local = null;
|
||||
} catch(CalendarStorageException e) {
|
||||
// CalendarStorageException is not used by LocalGroup and LocalContact
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (local == null) {
|
||||
if (newData.group) {
|
||||
App.log.log(Level.INFO, "Creating local group", newData);
|
||||
LocalGroup group = new LocalGroup(localAddressBook(), newData, fileName, eTag);
|
||||
group.create();
|
||||
|
||||
local = group;
|
||||
} else {
|
||||
App.log.log(Level.INFO, "Creating local contact", newData);
|
||||
LocalContact contact = new LocalContact(localAddressBook(), newData, fileName, eTag);
|
||||
contact.create();
|
||||
|
||||
local = contact;
|
||||
}
|
||||
syncResult.stats.numInserts++;
|
||||
}
|
||||
|
||||
if (groupMethod == GroupMethod.CATEGORIES && local instanceof LocalContact) {
|
||||
// VCard3: update group memberships from CATEGORIES
|
||||
LocalContact contact = (LocalContact)local;
|
||||
|
||||
BatchOperation batch = new BatchOperation(provider);
|
||||
contact.removeGroupMemberships(batch);
|
||||
|
||||
for (String category : contact.getContact().categories) {
|
||||
long groupID = localAddressBook().findOrCreateGroup(category);
|
||||
contact.addToGroup(batch, groupID);
|
||||
}
|
||||
|
||||
batch.commit();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// downloader helper class
|
||||
|
||||
@RequiredArgsConstructor
|
||||
private class ResourceDownloader implements Contact.Downloader {
|
||||
final HttpUrl baseUrl;
|
||||
|
||||
@Override
|
||||
public byte[] download(String url, String accepts) {
|
||||
HttpUrl httpUrl = HttpUrl.parse(url);
|
||||
|
||||
if (httpUrl == null) {
|
||||
App.log.log(Level.SEVERE, "Invalid external resource URL", url);
|
||||
return null;
|
||||
}
|
||||
|
||||
String host = httpUrl.host();
|
||||
if (host == null) {
|
||||
App.log.log(Level.SEVERE, "External resource URL doesn't specify a host name", url);
|
||||
return null;
|
||||
}
|
||||
|
||||
OkHttpClient resourceClient = HttpClient.create();
|
||||
|
||||
// authenticate only against a certain host, and only upon request
|
||||
resourceClient = HttpClient.addAuthentication(resourceClient, baseUrl.host(), settings.username(), settings.password());
|
||||
|
||||
// allow redirects
|
||||
resourceClient = resourceClient.newBuilder()
|
||||
.followRedirects(true)
|
||||
.build();
|
||||
|
||||
try {
|
||||
Response response = resourceClient.newCall(new Request.Builder()
|
||||
.get()
|
||||
.url(httpUrl)
|
||||
.build()).execute();
|
||||
|
||||
ResponseBody body = response.body();
|
||||
if (body != null) {
|
||||
@Cleanup InputStream stream = body.byteStream();
|
||||
if (response.isSuccessful() && stream != null) {
|
||||
return IOUtils.toByteArray(stream);
|
||||
} else
|
||||
App.log.severe("Couldn't download external resource");
|
||||
}
|
||||
} catch(IOException e) {
|
||||
App.log.log(Level.SEVERE, "Couldn't download external resource", e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user