mirror of
https://github.com/bitfireAT/davx5-ose.git
synced 2026-01-06 05:47:50 -05:00
Compare commits
167 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ad8c832819 | ||
|
|
389af2b738 | ||
|
|
be2e15e463 | ||
|
|
c7c13520f9 | ||
|
|
317144630c | ||
|
|
34bc27fa79 | ||
|
|
210735a500 | ||
|
|
b30733c64b | ||
|
|
91234a688f | ||
|
|
5675e544b5 | ||
|
|
42a261b84e | ||
|
|
0d1825cbf3 | ||
|
|
9b8fc983cd | ||
|
|
3bde3758fc | ||
|
|
fd1f59d124 | ||
|
|
9886507b7d | ||
|
|
144643d6af | ||
|
|
14875f63ea | ||
|
|
28e567cf78 | ||
|
|
7997606550 | ||
|
|
fb0552de46 | ||
|
|
03c15a6924 | ||
|
|
c3b2929f88 | ||
|
|
eb2537a278 | ||
|
|
0b9727cca6 | ||
|
|
61231b4233 | ||
|
|
59252d7471 | ||
|
|
6ffa6fa9a7 | ||
|
|
03ee9a037b | ||
|
|
7ab13d648e | ||
|
|
25c54cce62 | ||
|
|
f0e45c71f5 | ||
|
|
fa528a64e9 | ||
|
|
c6aed90c96 | ||
|
|
2280f899ee | ||
|
|
a283cbbae5 | ||
|
|
bb95a25b91 | ||
|
|
f1ccd01708 | ||
|
|
c498225064 | ||
|
|
879b137cfc | ||
|
|
84379f7ee1 | ||
|
|
a594fd3d14 | ||
|
|
100b78a6a4 | ||
|
|
758711acb2 | ||
|
|
c90b6075db | ||
|
|
7109915e6e | ||
|
|
e8cf9fd5ab | ||
|
|
3a49815220 | ||
|
|
96881bd986 | ||
|
|
c08a0bdc43 | ||
|
|
773b2ee992 | ||
|
|
c2181c55d3 | ||
|
|
8449684dd2 | ||
|
|
28e7c91658 | ||
|
|
51867c5f3f | ||
|
|
1786b73ac6 | ||
|
|
1df3ddbe74 | ||
|
|
5ee8d76b34 | ||
|
|
5723225475 | ||
|
|
f73f6ca43c | ||
|
|
753c4b05a5 | ||
|
|
2e34fa686d | ||
|
|
a735564bc1 | ||
|
|
552f6b6936 | ||
|
|
50f7006e59 | ||
|
|
6ac5fe0204 | ||
|
|
19bfe5c5f2 | ||
|
|
212cd8ddb0 | ||
|
|
c30195d9ba | ||
|
|
3ca063416e | ||
|
|
940d622402 | ||
|
|
814abc60ed | ||
|
|
220ba4b151 | ||
|
|
777e124b54 | ||
|
|
f32493986b | ||
|
|
5025a61cd1 | ||
|
|
89a516bfd1 | ||
|
|
af71ed8bc5 | ||
|
|
fc29988dc6 | ||
|
|
77c947da14 | ||
|
|
ff901ce91f | ||
|
|
85a6b68a56 | ||
|
|
89050d88c6 | ||
|
|
ba0350c83d | ||
|
|
515969c4b8 | ||
|
|
9a8d29e774 | ||
|
|
2880b05b5d | ||
|
|
d6cff63f2d | ||
|
|
be6aa1b6a2 | ||
|
|
9ec4a4015d | ||
|
|
9dbc32d30b | ||
|
|
b63fc70cfb | ||
|
|
0142e63257 | ||
|
|
aaa7d71ae3 | ||
|
|
4adf3001ac | ||
|
|
5ccdafa074 | ||
|
|
fce2b85991 | ||
|
|
e5ebf10dc0 | ||
|
|
0f0acd62a3 | ||
|
|
2414b42867 | ||
|
|
243dac9952 | ||
|
|
12248b8bb9 | ||
|
|
d872bd06e5 | ||
|
|
065aa3fc84 | ||
|
|
20ee4e03f3 | ||
|
|
241e15404f | ||
|
|
4a00ba647d | ||
|
|
8d00814eaf | ||
|
|
c665744c31 | ||
|
|
2ef278c336 | ||
|
|
34de8431ae | ||
|
|
9d19d9757c | ||
|
|
81d13576e8 | ||
|
|
6f429328ef | ||
|
|
6465d83da4 | ||
|
|
0f5f39a9fe | ||
|
|
3e2459c85c | ||
|
|
8f52bf160e | ||
|
|
661276450c | ||
|
|
c93a89348e | ||
|
|
93464ccf8c | ||
|
|
3646a561c6 | ||
|
|
da9410c1b5 | ||
|
|
82f80fed1c | ||
|
|
94770fb0c8 | ||
|
|
9ddcec5624 | ||
|
|
4b5cb30762 | ||
|
|
58f05986c9 | ||
|
|
dd50f10c58 | ||
|
|
d3c1688407 | ||
|
|
80231dd44b | ||
|
|
4ecca76a95 | ||
|
|
410a04dc11 | ||
|
|
7fc01503d5 | ||
|
|
18542adb2c | ||
|
|
e34abf291e | ||
|
|
20bc5af4a3 | ||
|
|
f344bd3c28 | ||
|
|
419d732195 | ||
|
|
0c819c842b | ||
|
|
d348f54deb | ||
|
|
c2e9b27831 | ||
|
|
808958a69b | ||
|
|
bd77a5be63 | ||
|
|
ab34def8b0 | ||
|
|
d024cdb495 | ||
|
|
4f7f3b851a | ||
|
|
7f4b4855a0 | ||
|
|
bc2d1ba96d | ||
|
|
0bc1a8178a | ||
|
|
d0b928a93d | ||
|
|
b0163e16cd | ||
|
|
98899ab27b | ||
|
|
e4e1053f77 | ||
|
|
bcd2e8d4da | ||
|
|
f7700ba8aa | ||
|
|
a198309df5 | ||
|
|
c1a26fbbb7 | ||
|
|
5bf3aad575 | ||
|
|
97ae121331 | ||
|
|
31f5be01b4 | ||
|
|
d7fff8a760 | ||
|
|
faeb3b7dd0 | ||
|
|
fc1874af85 | ||
|
|
be80b6fde8 | ||
|
|
072c763dec | ||
|
|
6ad74c79f0 |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -85,6 +85,8 @@ build/
|
||||
# Ignore Gradle GUI config
|
||||
gradle-app.setting
|
||||
|
||||
|
||||
### external libs ###
|
||||
.svn
|
||||
|
||||
# Javadoc
|
||||
javadoc/
|
||||
|
||||
13
.gitmodules
vendored
Normal file
13
.gitmodules
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
[submodule "dav4android"]
|
||||
path = dav4android
|
||||
url = https://gitlab.com/bitfireAT/dav4android.git
|
||||
[submodule "ical4android"]
|
||||
path = ical4android
|
||||
url = https://gitlab.com/bitfireAT/ical4android.git
|
||||
[submodule "vcard4android"]
|
||||
path = vcard4android
|
||||
url = https://gitlab.com/bitfireAT/vcard4android.git
|
||||
[submodule "MemorizingTrustManager"]
|
||||
path = MemorizingTrustManager
|
||||
url = https://github.com/ge0rg/MemorizingTrustManager
|
||||
ignore = dirty
|
||||
@@ -1,58 +0,0 @@
|
||||
|
||||
DAVdroid is free and open-source software, licensed under the [GPLv3 License](COPYING).
|
||||
If you like our project, please contribute to it.
|
||||
|
||||
# How to contribute
|
||||
|
||||
## Reporting issues
|
||||
|
||||
An issue might be a bug, an enhancement request or something in between. If you think you
|
||||
have found a bug or if you want to request some enhancement, please:
|
||||
|
||||
1. Read the [Configuration](http://davdroid.bitfire.at/configuration) and [FAQ](http://davdroid.bitfire.at/faq/)
|
||||
pages carefully. The most common issues/usage challenges are explained there.
|
||||
2. Search the Web for the problem, maybe ask competent friends or in forums.
|
||||
3. Browse through the [open issues](https://github.com/rfc2822/davdroid/issues). You can
|
||||
also search the issues in the search field on top of the page. Please have a look
|
||||
into the closed issues, too, because many requests have already been handled (and can't/won't
|
||||
be fixed, for instance).
|
||||
4. **[Fetch verbose logs](https://github.com/rfc2822/davdroid/wiki/How-to-view-the-logs) and prepare
|
||||
them. Remove `Authorization: Basic xxxxxx` headers and other private data.** Extracting the
|
||||
logs may be cumbersome work in the first time, but it's absolutely necessary in order to
|
||||
handle your issue.
|
||||
5. [Create a new issue](https://github.com/rfc2822/davdroid/issues/new), containing
|
||||
* a useful summary of the problem ("Crash when syncing contacts with large photos" instead of "CRASH PLEASE HELP"),
|
||||
* your DAVdroid version and source ("DAVdroid 0.5.10 from F-Droid"),
|
||||
* your Android version and device model ("Samsung Galaxy S2 running Android 4.4.2 (CyanogenMod 11-20140504-SNAPSHOT-M6-i9100)"),
|
||||
* your CalDAV/CardDAV server software, version and hosting information ("OwnCloud 6, hosted on virtual server"),
|
||||
* a problem description, including **instructions on how to reproduce the problem** (we need to
|
||||
reproduce the problem before we can fix it!),
|
||||
* **verbose logs including the network traffic** (see step before). Enquote the logs with three backticks ```
|
||||
before and after, or post them onto http://gist.github.com and provide a link.
|
||||
|
||||
|
||||
## Pull requests
|
||||
|
||||
We're very happy about pull requests for
|
||||
|
||||
* source code,
|
||||
* documentation,
|
||||
* translation (strings).
|
||||
|
||||
However, if you want to contribute source code, please talk with us in the
|
||||
corresponding issue before because will only merge pull requests that
|
||||
|
||||
* match our product goals,
|
||||
* have the necessary code quality,
|
||||
* don't interfere with other near-term future development.
|
||||
|
||||
However, feel free to fork the repository and do your changes anyway
|
||||
(that's why it's open-source). Just don't expect your strategic changes to be
|
||||
merged if there's no consensus in the issue before.
|
||||
|
||||
|
||||
## Donations
|
||||
|
||||
If you want to support this project, please also consider [donating to DAVdroid](http://davdroid.bitfire.at/donate)
|
||||
or [purchasing it in one of the commercial stores](http://davdroid.bitfire.at/download).
|
||||
|
||||
1
MemorizingTrustManager
Submodule
1
MemorizingTrustManager
Submodule
Submodule MemorizingTrustManager added at b6a3d558e4
29
README.md
29
README.md
@@ -1,21 +1,34 @@
|
||||
|
||||
DAVDROID
|
||||
DAVdroid
|
||||
========
|
||||
|
||||
Please see the [DAVdroid Web site](https://davdroid.bitfire.at) for
|
||||
detailled information about DAVdroid.
|
||||
comprehensive information about DAVdroid.
|
||||
|
||||
DAVdroid is licensed under the [GPLv3 License](COPYING).
|
||||
DAVdroid is licensed under the [GPLv3 License](LICENSE).
|
||||
|
||||
Twitter: [@davdroidapp](https://twitter.com/davdroidapp)
|
||||
News and updates: [@davdroidapp](https://twitter.com/davdroidapp)
|
||||
|
||||
Help and discussion: [DAVdroid forums](https://davdroid.bitfire.at/forums)
|
||||
|
||||
Parts of DAVdroid have been outsourced into these libraries:
|
||||
|
||||
* [dav4android](https://gitlab.com/bitfireAT/dav4android) – WebDAV/CalDav/CardDAV framework
|
||||
* [ical4android](https://gitlab.com/bitfireAT/ical4android) – iCalendar processing and Calendar Provider access
|
||||
* [vcard4android](https://gitlab.com/bitfireAT/vcard4android) – VCard processing and Contacts Provider access
|
||||
|
||||
[](https://flattr.com/submit/auto?user_id=bitfire&url=https://davdroid.bitfire.at&title=DAVdroid&category=software)
|
||||
|
||||
|
||||
USED THIRD-PARTY LIBRARIES
|
||||
==========================
|
||||
|
||||
* [Apache HttpClient](http://hc.apache.org) (Android port) – [Apache License](http://www.apache.org/licenses/)
|
||||
* [iCal4j](http://ical4j.sourceforge.net/) – [New BSD License](http://sourceforge.net/p/ical4j/ical4j/ci/default/tree/LICENSE)
|
||||
Those libraries are used by DAVdroid (alphabetically):
|
||||
|
||||
* [dnsjava](http://www.xbill.org/dnsjava/) – [BSD License](http://www.xbill.org/dnsjava/dnsjava-current/LICENSE)
|
||||
* [ez-vcard](https://code.google.com/p/ez-vcard/) – [New BSD License](http://opensource.org/licenses/BSD-3-Clause)
|
||||
* [Simple XML Serialization](http://simple.sourceforge.net/) – [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0)
|
||||
* [iCal4j](http://ical4j.sourceforge.net/) – [New BSD License](http://sourceforge.net/p/ical4j/ical4j/ci/default/tree/LICENSE)
|
||||
* [MemorizingTrustManager](https://github.com/ge0rg/MemorizingTrustManager) – [MIT License](https://raw.githubusercontent.com/ge0rg/MemorizingTrustManager/master/LICENSE.txt)
|
||||
* [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)
|
||||
* [dnsjava](http://www.xbill.org/dnsjava/) – [BSD license](http://www.xbill.org/dnsjava/dnsjava-current/LICENSE)
|
||||
* [SLF4J](http://www.slf4j.org/) – [MIT License](http://www.slf4j.org/license.html)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2013 – 2015 Ricki Hirner (bitfire web engineering).
|
||||
* Copyright (c) 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
|
||||
@@ -9,13 +9,18 @@
|
||||
apply plugin: 'com.android.application'
|
||||
|
||||
android {
|
||||
compileSdkVersion 22
|
||||
buildToolsVersion '22.0.1'
|
||||
compileSdkVersion 23
|
||||
buildToolsVersion "23.0.3"
|
||||
|
||||
defaultConfig {
|
||||
applicationId "at.bitfire.davdroid"
|
||||
minSdkVersion 14
|
||||
targetSdkVersion 22
|
||||
targetSdkVersion 23
|
||||
|
||||
versionCode 103
|
||||
versionName "1.1"
|
||||
|
||||
buildConfigField "long", "buildTime", System.currentTimeMillis() + "L"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
@@ -27,11 +32,22 @@ android {
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
|
||||
}
|
||||
}
|
||||
lintOptions {
|
||||
abortOnError false
|
||||
disable 'ExtraTranslation'
|
||||
}
|
||||
|
||||
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 'Recycle' // doesn't understand Lombok's @Cleanup
|
||||
disable 'RtlEnabled'
|
||||
disable 'RtlHardcoded'
|
||||
disable 'Typos'
|
||||
}
|
||||
packagingOptions {
|
||||
exclude 'LICENSE'
|
||||
exclude 'META-INF/LICENSE.txt'
|
||||
@@ -39,38 +55,26 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
configurations.all {
|
||||
exclude module: 'commons-logging' // undocumented part of Android
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// Apache Commons
|
||||
compile 'org.apache.commons:commons-lang3:3.4'
|
||||
compile 'commons-io:commons-io:2.4'
|
||||
// Lombok for useful @helpers
|
||||
provided 'org.projectlombok:lombok:1.16.4'
|
||||
// ical4j for parsing/generating iCalendars
|
||||
compile('org.mnode.ical4j:ical4j:2.0-alpha1') {
|
||||
// we don't need content builders, see https://github.com/ical4j/ical4j/wiki/Groovy
|
||||
exclude group: 'org.codehaus.groovy', module: 'groovy-all'
|
||||
}
|
||||
compile('org.slf4j:slf4j-android:1.7.12') // slf4j is used by ical4j
|
||||
// ez-vcard for parsing/generating VCards
|
||||
compile('com.googlecode.ez-vcard:ez-vcard:0.9.6') {
|
||||
// hCard functionality not needed
|
||||
exclude group: 'org.jsoup', module: 'jsoup'
|
||||
exclude group: 'org.freemarker', module: 'freemarker'
|
||||
// jCard functionality not needed
|
||||
exclude group: 'com.fasterxml.jackson.core', module: 'jackson-core'
|
||||
}
|
||||
// dnsjava for querying SRV/TXT records
|
||||
compile project(':dav4android')
|
||||
compile project(':ical4android')
|
||||
compile project(':vcard4android')
|
||||
|
||||
compile 'com.android.support:appcompat-v7:23.+'
|
||||
compile 'com.android.support:cardview-v7:23.+'
|
||||
compile 'com.android.support:design:23.+'
|
||||
compile 'com.android.support:preference-v14:23.+'
|
||||
|
||||
compile 'com.github.yukuku:ambilwarna:2.0.1'
|
||||
compile project(':MemorizingTrustManager')
|
||||
|
||||
compile 'dnsjava:dnsjava:2.1.7'
|
||||
// HttpClient 4.3, Android flavour for WebDAV operations
|
||||
//compile 'org.apache.httpcomponents:httpclient-android:4.3.5.1'
|
||||
compile project(':lib:httpclient-android')
|
||||
// SimpleXML for parsing and generating WebDAV messages
|
||||
compile('org.simpleframework:simple-xml:2.7.1') {
|
||||
exclude group: 'stax', module: 'stax-api'
|
||||
exclude group: 'xpp3', module: 'xpp3'
|
||||
}
|
||||
compile 'org.apache.commons:commons-lang3:3.4'
|
||||
compile 'org.apache.commons:commons-collections4:4.1'
|
||||
provided 'org.projectlombok:lombok:1.16.8'
|
||||
|
||||
// for tests
|
||||
testCompile 'junit:junit:4.12'
|
||||
testCompile 'com.squareup.okhttp3:mockwebserver:3.3.1'
|
||||
androidTestCompile 'com.squareup.okhttp3:mockwebserver:3.3.1'
|
||||
}
|
||||
|
||||
@@ -7,31 +7,30 @@
|
||||
|
||||
-dontobfuscate
|
||||
|
||||
|
||||
# SimpleXML
|
||||
-keep class org.simpleframework.** { *; } # keep all interfaces etc. to allow reflection
|
||||
-dontwarn com.bea.xml.stream.** # StAX API not used
|
||||
-dontwarn javax.xml.stream.**
|
||||
|
||||
# ez-vcard
|
||||
-dontwarn com.fasterxml.jackson.** # Jackson JSON Processor (for jCards) 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)
|
||||
|
||||
# ical4j: ignore unused dynamic libraries
|
||||
-dontwarn aQute.**
|
||||
-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)
|
||||
|
||||
# okhttp
|
||||
-dontwarn java.nio.file.** # not available on Android
|
||||
-dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement
|
||||
|
||||
# MemorizingTrustManager
|
||||
-dontwarn de.duenndns.ssl.MemorizingTrustManager
|
||||
|
||||
# dnsjava
|
||||
-dontwarn sun.net.spi.nameservice.** # not available on Android
|
||||
|
||||
# DAVdroid
|
||||
-keep class at.bitfire.davdroid.** { *; } # all DAVdroid code is required
|
||||
|
||||
# unneeded libraries
|
||||
-dontwarn aQute.**
|
||||
# DAVdroid + libs
|
||||
-keep class at.bitfire.** { *; } # all DAVdroid code is required
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
PRODID:-//hacksw/handcal//NONSGML v1.0//EN
|
||||
BEGIN:VEVENT
|
||||
UID:all-day-0sec@example.com
|
||||
DTSTAMP:20140101T000000Z
|
||||
DTSTART;VALUE=DATE:19970714
|
||||
DTEND;VALUE=DATE:19970714
|
||||
SUMMARY:0 Sec Event
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
||||
@@ -1,11 +0,0 @@
|
||||
BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
PRODID:-//hacksw/handcal//NONSGML v1.0//EN
|
||||
BEGIN:VEVENT
|
||||
UID:all-day-10days@example.com
|
||||
DTSTAMP:20140101T000000Z
|
||||
DTSTART;VALUE=DATE:19970714
|
||||
DTEND;VALUE=DATE:19970724
|
||||
SUMMARY:All-Day 10 Days
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
||||
@@ -1,11 +0,0 @@
|
||||
BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
PRODID:-//hacksw/handcal//NONSGML v1.0//EN
|
||||
BEGIN:VEVENT
|
||||
UID:all-day-1day@example.com
|
||||
DTSTAMP:20140101T000000Z
|
||||
DTSTART;VALUE=DATE:19970714
|
||||
DTEND;VALUE=DATE:19970714
|
||||
SUMMARY:All-Day 1 Day
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 23 KiB |
@@ -1,11 +0,0 @@
|
||||
BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
PRODID:-//hacksw/handcal//NONSGML v1.0//EN
|
||||
BEGIN:VEVENT
|
||||
UID:event-on-that-day@example.com
|
||||
DTSTAMP:19970714T170000Z
|
||||
ORGANIZER;CN=John Doe:MAILTO:john.doe@example.com
|
||||
DTSTART;VALUE=DATE:19970714
|
||||
SUMMARY:Bastille Day Party
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
||||
@@ -1,9 +0,0 @@
|
||||
BEGIN:VCARD
|
||||
VERSION:3.0
|
||||
UID:2de59c6cc9
|
||||
PRODID:-//ownCloud//NONSGML Contacts 0.2.5//EN
|
||||
REV:2013-12-08T00:04:30+00:00
|
||||
FN:test mctest
|
||||
N:mctest;test;;;
|
||||
IMPP;TYPE=WORK;X-SERVICE-TYPE=jabber:test-without-valid-scheme@test.tld
|
||||
END:VCARD
|
||||
@@ -1,5 +0,0 @@
|
||||
BEGIN:VCARD
|
||||
VERSION:3.0
|
||||
FN:VCard with invalid unknown properties
|
||||
X-UNKNOWN@PROPERTY:MUST-NOT_CONTAIN?OTHER*LETTERS;
|
||||
END:VCARD
|
||||
@@ -1,17 +0,0 @@
|
||||
BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
BEGIN:VEVENT
|
||||
UID:fcb42e4d-bc6e-4499-97f0-6616a02da7bc
|
||||
SUMMARY:Recurring event with one exception
|
||||
RRULE:FREQ=DAILY;COUNT=5
|
||||
DTSTART;VALUE=DATE:20150501
|
||||
DTEND;VALUE=DATE:20150502
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
UID:fcb42e4d-bc6e-4499-97f0-6616a02da7bc
|
||||
RECURRENCE-ID;VALUE=DATE:20150503
|
||||
DTSTART;VALUE=DATE:20150503
|
||||
DTEND;VALUE=DATE:20150504
|
||||
SUMMARY:Another summary for the third day
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
||||
@@ -1,16 +0,0 @@
|
||||
BEGIN:VCARD
|
||||
VERSION:3.0
|
||||
N:Gump;Forrest;Mr.
|
||||
FN:Forrest Gump
|
||||
ORG:Bubba Gump Shrimp Co.
|
||||
TITLE:Shrimp Man
|
||||
PHOTO;VALUE=URL;TYPE=PNG:http://192.168.0.11:3000/assets/davdroid-logo-192.png
|
||||
TEL;TYPE=WORK,VOICE:(111) 555-1212
|
||||
TEL;TYPE=HOME,VOICE:(404) 555-1212
|
||||
ADR;TYPE=WORK:;;100 Waters Edge;Baytown;LA;30314;United States of America
|
||||
LABEL;TYPE=WORK:100 Waters Edge\nBaytown, LA 30314\nUnited States of America
|
||||
ADR;TYPE=HOME:;;42 Plantation St.;Baytown;LA;30314;United States of America
|
||||
LABEL;TYPE=HOME:42 Plantation St.\nBaytown, LA 30314\nUnited States of America
|
||||
EMAIL;TYPE=PREF,INTERNET:forrestgump@example.com
|
||||
REV:2008-04-24T19:52:43Z
|
||||
END:VCARD
|
||||
Binary file not shown.
@@ -1,14 +0,0 @@
|
||||
BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
PRODID:Blabla
|
||||
BEGIN:VEVENT
|
||||
CLASS:PUBLIC
|
||||
CREATED;VALUE=DATE-TIME:20131008T205713
|
||||
LAST-MODIFIED;VALUE=DATE-TIME:20131008T205740
|
||||
SUMMARY:online Anmeldung
|
||||
DESCRIPTION:http://www.tgbornheim.de/index.php?sessionid=&page=&id=&sportce
|
||||
ntergroup=&day=6
|
||||
UID:b99c41704b
|
||||
DTSTART;VALUE=DATE-TIME;TZID=Europe/Berlin:20131019T060000
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
||||
@@ -1,16 +0,0 @@
|
||||
BEGIN:VCARD
|
||||
VERSION:3.0
|
||||
N:Gump;Forrest
|
||||
FN:Forrest Gump
|
||||
ORG:Bubba Gump Shrimp Co.
|
||||
TITLE:Shrimp Man
|
||||
PHOTO;VALUE=URL;TYPE=GIF:http://www.example.com/dir_photos/my_photo.gif
|
||||
TEL;TYPE=WORK,VOICE:(111) 555-1212
|
||||
TEL;TYPE=HOME,VOICE:(404) 555-1212
|
||||
ADR;TYPE=WORK:;;100 Waters Edge;Baytown;LA;30314;United States of America
|
||||
LABEL;TYPE=WORK:100 Waters Edge\nBaytown, LA 30314\nUnited States of America
|
||||
ADR;TYPE=HOME:;;42 Plantation St.;Baytown;LA;30314;United States of America
|
||||
LABEL;TYPE=HOME:42 Plantation St.\nBaytown, LA 30314\nUnited States of America
|
||||
EMAIL;TYPE=PREF,INTERNET:forrestgump@example.com
|
||||
REV:2008-04-24T19:52:43Z
|
||||
END:VCARD
|
||||
@@ -1,33 +0,0 @@
|
||||
BEGIN:VCALENDAR
|
||||
PRODID:-//Ximian//NONSGML Evolution Calendar//EN
|
||||
VERSION:2.0
|
||||
METHOD:PUBLISH
|
||||
BEGIN:VTIMEZONE
|
||||
TZID:/freeassociation.sourceforge.net/Tzfile/Europe/Vienna
|
||||
X-LIC-LOCATION:Europe/Vienna
|
||||
BEGIN:STANDARD
|
||||
TZNAME:CET
|
||||
DTSTART:19701027T030000
|
||||
RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10
|
||||
TZOFFSETFROM:+0200
|
||||
TZOFFSETTO:+0100
|
||||
END:STANDARD
|
||||
BEGIN:DAYLIGHT
|
||||
TZNAME:CEST
|
||||
DTSTART:19700331T020000
|
||||
RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=3
|
||||
TZOFFSETFROM:+0100
|
||||
TZOFFSETTO:+0200
|
||||
END:DAYLIGHT
|
||||
END:VTIMEZONE
|
||||
BEGIN:VEVENT
|
||||
UID:c252087c-7354-4722-aea9-0e7d86c01a25
|
||||
DTSTAMP:20130926T151211Z
|
||||
SUMMARY:Test-Ereignis im schönen Wien
|
||||
DTSTART;TZID=/freeassociation.sourceforge.net/Tzfile/Europe/Vienna:20131009T170000
|
||||
DTEND;TZID=/freeassociation.sourceforge.net/Tzfile/Europe/Vienna:20131009T180000
|
||||
X-RADICALE-NAME:97929342-291a-434e-bf1a-fa1749bf99d0.ics
|
||||
X-EVOLUTION-CALDAV-HREF:/radicale/rfc2822/default.ics/97929342-291a-434e-bf1a-fa1749bf99d0.ics
|
||||
X-EVOLUTION-CALDAV-ETAG:\"-3264224243575339985\"
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
||||
@@ -1,39 +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 junit.framework.TestCase;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
|
||||
public class ArrayUtilsTest extends TestCase {
|
||||
|
||||
public void testPartition() {
|
||||
// n == 0
|
||||
assertTrue(Arrays.deepEquals(
|
||||
new Long[0][0],
|
||||
ArrayUtils.partition(new Long[] { }, 5)));
|
||||
|
||||
// n < max
|
||||
assertTrue(Arrays.deepEquals(
|
||||
new Long[][] { { 1l, 2l } },
|
||||
ArrayUtils.partition(new Long[] { 1l, 2l }, 5)));
|
||||
|
||||
// n == max
|
||||
assertTrue(Arrays.deepEquals(
|
||||
new Long[][] { { 1l, 2l }, { 3l, 4l } },
|
||||
ArrayUtils.partition(new Long[] { 1l, 2l, 3l, 4l }, 2)));
|
||||
|
||||
// n > max
|
||||
assertTrue(Arrays.deepEquals(
|
||||
new Long[][] { { 1l, 2l, 3l, 4l, 5l }, { 6l, 7l, 8l, 9l, 10l }, { 11l } },
|
||||
ArrayUtils.partition(new Long[] { 1l, 2l, 3l, 4l, 5l, 6l, 7l, 8l, 9l, 10l, 11l }, 5)));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,78 +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 junit.framework.TestCase;
|
||||
|
||||
import net.fortuna.ical4j.model.DateList;
|
||||
import net.fortuna.ical4j.model.TimeZone;
|
||||
import net.fortuna.ical4j.model.parameter.Value;
|
||||
import net.fortuna.ical4j.model.property.ExDate;
|
||||
import net.fortuna.ical4j.model.property.RDate;
|
||||
|
||||
import java.text.ParseException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class DateUtilsTest extends TestCase {
|
||||
private static final String tzIdVienna = "Europe/Vienna";
|
||||
|
||||
public void testRecurrenceSetsToAndroidString() throws ParseException {
|
||||
// one entry without time zone (implicitly UTC)
|
||||
final List<RDate> list = new ArrayList<>(2);
|
||||
list.add(new RDate(new DateList("20150101T103010Z,20150102T103020Z", Value.DATE_TIME)));
|
||||
assertEquals("20150101T103010Z,20150102T103020Z", DateUtils.recurrenceSetsToAndroidString(list, false));
|
||||
|
||||
// two entries (previous one + this), both with time zone Vienna
|
||||
list.add(new RDate(new DateList("20150103T113030,20150704T123040", Value.DATE_TIME)));
|
||||
final TimeZone tz = DateUtils.tzRegistry.getTimeZone(tzIdVienna);
|
||||
for (RDate rdate : list)
|
||||
rdate.setTimeZone(tz);
|
||||
assertEquals("20150101T103010Z,20150102T103020Z,20150103T103030Z,20150704T103040Z", DateUtils.recurrenceSetsToAndroidString(list, false));
|
||||
|
||||
// DATEs (without time) have to be converted to <date>T000000Z for Android
|
||||
list.clear();
|
||||
list.add(new RDate(new DateList("20150101,20150702", Value.DATE)));
|
||||
assertEquals("20150101T000000Z,20150702T000000Z", DateUtils.recurrenceSetsToAndroidString(list, true));
|
||||
|
||||
// DATE-TIME (floating time or UTC) recurrences for all-day events have to converted to <date>T000000Z for Android
|
||||
list.clear();
|
||||
list.add(new RDate(new DateList("20150101T000000,20150702T000000Z", Value.DATE_TIME)));
|
||||
assertEquals("20150101T000000Z,20150702T000000Z", DateUtils.recurrenceSetsToAndroidString(list, true));
|
||||
}
|
||||
|
||||
public void testAndroidStringToRecurrenceSets() throws ParseException {
|
||||
// list of UTC times
|
||||
ExDate exDate = (ExDate)DateUtils.androidStringToRecurrenceSet("20150101T103010Z,20150702T103020Z", ExDate.class, false);
|
||||
DateList exDates = exDate.getDates();
|
||||
assertEquals(Value.DATE_TIME, exDates.getType());
|
||||
assertTrue(exDates.isUtc());
|
||||
assertEquals(2, exDates.size());
|
||||
assertEquals(1420108210000L, exDates.get(0).getTime());
|
||||
assertEquals(1435833020000L, exDates.get(1).getTime());
|
||||
|
||||
// list of time zone times
|
||||
exDate = (ExDate)DateUtils.androidStringToRecurrenceSet(tzIdVienna + ";20150101T103010,20150702T103020", ExDate.class, false);
|
||||
exDates = exDate.getDates();
|
||||
assertEquals(Value.DATE_TIME, exDates.getType());
|
||||
assertEquals(DateUtils.tzRegistry.getTimeZone(tzIdVienna), exDates.getTimeZone());
|
||||
assertEquals(2, exDates.size());
|
||||
assertEquals(1420104610000L, exDates.get(0).getTime());
|
||||
assertEquals(1435825820000L, exDates.get(1).getTime());
|
||||
|
||||
// list of dates
|
||||
exDate = (ExDate)DateUtils.androidStringToRecurrenceSet("20150101T103010Z,20150702T103020Z", ExDate.class, true);
|
||||
exDates = exDate.getDates();
|
||||
assertEquals(Value.DATE, exDates.getType());
|
||||
assertEquals(2, exDates.size());
|
||||
assertEquals("20150101", exDates.get(0).toString());
|
||||
assertEquals("20150702", exDates.get(1).toString());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* 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.test.InstrumentationTestCase;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.Socket;
|
||||
|
||||
import javax.net.ssl.SSLSocket;
|
||||
|
||||
import de.duenndns.ssl.MemorizingTrustManager;
|
||||
import okhttp3.mockwebserver.MockWebServer;
|
||||
|
||||
public class SSLSocketFactoryCompatTest extends InstrumentationTestCase {
|
||||
|
||||
SSLSocketFactoryCompat factory;
|
||||
MockWebServer server = new MockWebServer();
|
||||
|
||||
@Override
|
||||
protected void setUp() throws Exception {
|
||||
factory = new SSLSocketFactoryCompat(new MemorizingTrustManager(getInstrumentation().getTargetContext().getApplicationContext()));
|
||||
server.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void tearDown() throws Exception {
|
||||
server.shutdown();
|
||||
}
|
||||
|
||||
|
||||
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"));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,27 +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.util.Log;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
|
||||
public class TestConstants {
|
||||
public static final String ROBOHYDRA_BASE = "http://192.168.0.11:3000/";
|
||||
|
||||
public static URI roboHydra;
|
||||
static {
|
||||
try {
|
||||
roboHydra = new URI(ROBOHYDRA_BASE);
|
||||
} catch(URISyntaxException e) {
|
||||
Log.wtf("davdroid.test.Constants", "Invalid RoboHydra base URL");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,70 +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 junit.framework.TestCase;
|
||||
|
||||
import java.net.URI;
|
||||
|
||||
|
||||
public class URLUtilsTest extends TestCase {
|
||||
|
||||
/* RFC 1738 p17 HTTP URLs:
|
||||
hpath = hsegment *[ "/" hsegment ]
|
||||
hsegment = *[ uchar | ";" | ":" | "@" | "&" | "=" ]
|
||||
uchar = unreserved | escape
|
||||
unreserved = alpha | digit | safe | extra
|
||||
alpha = lowalpha | hialpha
|
||||
lowalpha = ...
|
||||
hialpha = ...
|
||||
digit = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" |
|
||||
"8" | "9"
|
||||
safe = "$" | "-" | "_" | "." | "+"
|
||||
extra = "!" | "*" | "'" | "(" | ")" | ","
|
||||
escape = "%" hex hex
|
||||
*/
|
||||
|
||||
|
||||
public void testEnsureTrailingSlash() throws Exception {
|
||||
assertEquals("/test/", URIUtils.ensureTrailingSlash("/test"));
|
||||
assertEquals("/test/", URIUtils.ensureTrailingSlash("/test/"));
|
||||
|
||||
String withoutSlash = "http://www.test.example/dav/collection",
|
||||
withSlash = withoutSlash + "/";
|
||||
assertEquals(new URI(withSlash), URIUtils.ensureTrailingSlash(new URI(withoutSlash)));
|
||||
assertEquals(new URI(withSlash), URIUtils.ensureTrailingSlash(new URI(withSlash)));
|
||||
}
|
||||
|
||||
public void testParseURI() throws Exception {
|
||||
// don't escape valid characters
|
||||
String validPath = "/;:@&=$-_.+!*'(),";
|
||||
assertEquals(new URI("https://www.test.example:123" + validPath), URIUtils.parseURI("https://www.test.example:123" + validPath, false));
|
||||
assertEquals(new URI(validPath), URIUtils.parseURI(validPath, true));
|
||||
|
||||
// keep literal IPv6 addresses (only in host name)
|
||||
assertEquals(new URI("https://[1:2::1]/"), URIUtils.parseURI("https://[1:2::1]/", false));
|
||||
|
||||
// "~" as home directory (valid)
|
||||
assertEquals(new URI("http://www.test.example/~user1/"), URIUtils.parseURI("http://www.test.example/~user1/", false));
|
||||
assertEquals(new URI("/~user1/"), URIUtils.parseURI("/%7euser1/", true));
|
||||
|
||||
// "@" in path names (valid)
|
||||
assertEquals(new URI("http://www.test.example/user@server.com/"), URIUtils.parseURI("http://www.test.example/user@server.com/", false));
|
||||
assertEquals(new URI("/user@server.com/"), URIUtils.parseURI("/user%40server.com/", true));
|
||||
assertEquals(new URI("user@server.com"), URIUtils.parseURI("user%40server.com", true));
|
||||
|
||||
// ":" in path names (valid)
|
||||
assertEquals(new URI("http://www.test.example/my:cal.ics"), URIUtils.parseURI("http://www.test.example/my:cal.ics", false));
|
||||
assertEquals(new URI("/my:cal.ics"), URIUtils.parseURI("/my%3Acal.ics", true));
|
||||
assertEquals(new URI(null, null, "my:cal.ics", null, null), URIUtils.parseURI("my%3Acal.ics", true));
|
||||
|
||||
// common invalid path names
|
||||
assertEquals(new URI(null, null, "my cal.ics", null, null), URIUtils.parseURI("my cal.ics", true));
|
||||
assertEquals(new URI(null, null, "{1234}.vcf", null, null), URIUtils.parseURI("{1234}.vcf", true));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
/*
|
||||
* 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 junit.framework.TestCase;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import at.bitfire.dav4android.DavResource;
|
||||
import at.bitfire.dav4android.exception.DavException;
|
||||
import at.bitfire.dav4android.exception.HttpException;
|
||||
import at.bitfire.dav4android.property.ResourceType;
|
||||
import at.bitfire.davdroid.HttpClient;
|
||||
import at.bitfire.davdroid.model.ServiceDB.Collections;
|
||||
import okhttp3.mockwebserver.MockResponse;
|
||||
import okhttp3.mockwebserver.MockWebServer;
|
||||
|
||||
public class CollectionInfoTest extends TestCase {
|
||||
|
||||
MockWebServer server = new MockWebServer();
|
||||
|
||||
public void testFromDavResource() throws IOException, HttpException, DavException {
|
||||
// r/w address book
|
||||
server.enqueue(new MockResponse()
|
||||
.setResponseCode(207)
|
||||
.setBody("<multistatus xmlns='DAV:' xmlns:CARD='urn:ietf:params:xml:ns:carddav'>" +
|
||||
"<response>" +
|
||||
" <href>/</href>" +
|
||||
" <propstat><prop>" +
|
||||
" <resourcetype><collection/><CARD:addressbook/></resourcetype>" +
|
||||
" <displayname>My Contacts</displayname>" +
|
||||
" <CARD:addressbook-description>My Contacts Description</CARD:addressbook-description>" +
|
||||
" </prop></propstat>" +
|
||||
"</response>" +
|
||||
"</multistatus>"));
|
||||
|
||||
DavResource dav = new DavResource(HttpClient.create(), 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);
|
||||
|
||||
// read-only calendar, no display name
|
||||
server.enqueue(new MockResponse()
|
||||
.setResponseCode(207)
|
||||
.setBody("<multistatus xmlns='DAV:' xmlns:CAL='urn:ietf:params:xml:ns:caldav' xmlns:ICAL='http://apple.com/ns/ical/'>" +
|
||||
"<response>" +
|
||||
" <href>/</href>" +
|
||||
" <propstat><prop>" +
|
||||
" <resourcetype><collection/><CAL:calendar/></resourcetype>" +
|
||||
" <current-user-privilege-set><privilege><read/></privilege></current-user-privilege-set>" +
|
||||
" <CAL:calendar-description>My Calendar</CAL:calendar-description>" +
|
||||
" <CAL:calendar-timezone>tzdata</CAL:calendar-timezone>" +
|
||||
" <ICAL:calendar-color>#ff0000</ICAL:calendar-color>" +
|
||||
" </prop></propstat>" +
|
||||
"</response>" +
|
||||
"</multistatus>"));
|
||||
|
||||
dav = new DavResource(HttpClient.create(), 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);
|
||||
}
|
||||
|
||||
public void testFromDB() {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(Collections.ID, 1);
|
||||
values.put(Collections.SERVICE_ID, 1);
|
||||
values.put(Collections.URL, "http://example.com");
|
||||
values.put(Collections.READ_ONLY, 1);
|
||||
values.put(Collections.DISPLAY_NAME, "display name");
|
||||
values.put(Collections.DESCRIPTION, "description");
|
||||
values.put(Collections.COLOR, 0xFFFF0000);
|
||||
values.put(Collections.TIME_ZONE, "tzdata");
|
||||
values.put(Collections.SUPPORTS_VEVENT, 1);
|
||||
values.put(Collections.SUPPORTS_VTODO, 1);
|
||||
values.put(Collections.SYNC, 1);
|
||||
|
||||
CollectionInfo info = 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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,94 +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.res.AssetManager;
|
||||
import android.test.InstrumentationTestCase;
|
||||
|
||||
import org.apache.commons.io.IOUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.Arrays;
|
||||
|
||||
import at.bitfire.davdroid.webdav.DavException;
|
||||
import at.bitfire.davdroid.webdav.HttpException;
|
||||
import ezvcard.VCardVersion;
|
||||
import ezvcard.property.Email;
|
||||
import ezvcard.property.Telephone;
|
||||
import lombok.Cleanup;
|
||||
|
||||
public class ContactTest extends InstrumentationTestCase {
|
||||
AssetManager assetMgr;
|
||||
|
||||
public void setUp() throws IOException, InvalidResourceException {
|
||||
assetMgr = getInstrumentation().getContext().getResources().getAssets();
|
||||
}
|
||||
|
||||
public void testGenerateDifferentVersions() throws Exception {
|
||||
Contact c = new Contact("test.vcf", null);
|
||||
|
||||
// should generate VCard 3.0 by default
|
||||
assertEquals("text/vcard;charset=UTF-8", c.getMimeType());
|
||||
assertTrue(new String(c.toEntity().toByteArray()).contains("VERSION:3.0"));
|
||||
|
||||
// now let's generate VCard 4.0
|
||||
c.setVCardVersion(VCardVersion.V4_0);
|
||||
assertEquals("text/vcard;version=4.0", c.getMimeType());
|
||||
assertTrue(new String(c.toEntity().toByteArray()).contains("VERSION:4.0"));
|
||||
}
|
||||
|
||||
public void testReferenceVCard() throws IOException, InvalidResourceException {
|
||||
Contact c = parseVCF("reference.vcf");
|
||||
assertEquals("Gump", c.getFamilyName());
|
||||
assertEquals("Forrest", c.getGivenName());
|
||||
assertEquals("Forrest Gump", c.getDisplayName());
|
||||
assertEquals("Bubba Gump Shrimp Co.", c.getOrganization().getValues().get(0));
|
||||
assertEquals("Shrimp Man", c.getJobTitle());
|
||||
|
||||
Telephone phone1 = c.getPhoneNumbers().get(0);
|
||||
assertEquals("(111) 555-1212", phone1.getText());
|
||||
assertEquals("WORK", phone1.getParameters("TYPE").get(0));
|
||||
assertEquals("VOICE", phone1.getParameters("TYPE").get(1));
|
||||
|
||||
Telephone phone2 = c.getPhoneNumbers().get(1);
|
||||
assertEquals("(404) 555-1212", phone2.getText());
|
||||
assertEquals("HOME", phone2.getParameters("TYPE").get(0));
|
||||
assertEquals("VOICE", phone2.getParameters("TYPE").get(1));
|
||||
|
||||
Email email = c.getEmails().get(0);
|
||||
assertEquals("forrestgump@example.com", email.getValue());
|
||||
assertEquals("PREF", email.getParameters("TYPE").get(0));
|
||||
assertEquals("INTERNET", email.getParameters("TYPE").get(1));
|
||||
|
||||
@Cleanup InputStream photoStream = assetMgr.open("davdroid-logo-192.png", AssetManager.ACCESS_STREAMING);
|
||||
byte[] expectedPhoto = IOUtils.toByteArray(photoStream);
|
||||
assertTrue(Arrays.equals(c.getPhoto(), expectedPhoto));
|
||||
}
|
||||
|
||||
public void testParseInvalidUnknownProperties() throws IOException {
|
||||
Contact c = parseVCF("invalid-unknown-properties.vcf");
|
||||
assertEquals("VCard with invalid unknown properties", c.getDisplayName());
|
||||
assertNull(c.getUnknownProperties());
|
||||
}
|
||||
|
||||
|
||||
protected Contact parseVCF(String fname) throws IOException {
|
||||
@Cleanup InputStream in = assetMgr.open(fname, AssetManager.ACCESS_STREAMING);
|
||||
Contact c = new Contact(fname, null);
|
||||
c.parseEntity(in, new Resource.AssetDownloader() {
|
||||
@Override
|
||||
public byte[] download(URI uri) throws URISyntaxException, IOException, HttpException, DavException {
|
||||
return IOUtils.toByteArray(uri);
|
||||
}
|
||||
});
|
||||
return c;
|
||||
}
|
||||
}
|
||||
@@ -1,117 +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.res.AssetManager;
|
||||
import android.test.InstrumentationTestCase;
|
||||
|
||||
import net.fortuna.ical4j.data.ParserException;
|
||||
import net.fortuna.ical4j.model.Date;
|
||||
import net.fortuna.ical4j.model.DateTime;
|
||||
import net.fortuna.ical4j.model.TimeZone;
|
||||
import net.fortuna.ical4j.model.property.DtStart;
|
||||
import net.fortuna.ical4j.util.TimeZones;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
import at.bitfire.davdroid.DateUtils;
|
||||
import lombok.Cleanup;
|
||||
|
||||
public class EventTest extends InstrumentationTestCase {
|
||||
protected final TimeZone tzVienna = DateUtils.tzRegistry.getTimeZone("Europe/Vienna");
|
||||
|
||||
AssetManager assetMgr;
|
||||
|
||||
Event eOnThatDay, eAllDay1Day, eAllDay10Days, eAllDay0Sec;
|
||||
|
||||
public void setUp() throws IOException, InvalidResourceException {
|
||||
assetMgr = getInstrumentation().getContext().getResources().getAssets();
|
||||
|
||||
eOnThatDay = parseCalendar("event-on-that-day.ics");
|
||||
eAllDay1Day = parseCalendar("all-day-1day.ics");
|
||||
eAllDay10Days = parseCalendar("all-day-10days.ics");
|
||||
eAllDay0Sec = parseCalendar("all-day-0sec.ics");
|
||||
}
|
||||
|
||||
|
||||
public void testGetTzID() throws Exception {
|
||||
// DATE (without time)
|
||||
assertEquals(TimeZones.UTC_ID, Event.getTzId(new DtStart(new Date("20150101"))));
|
||||
|
||||
// DATE-TIME without time zone (floating time): should be UTC (because net.fortuna.ical4j.timezone.date.floating=false)
|
||||
assertEquals(TimeZones.UTC_ID, Event.getTzId(new DtStart(new DateTime("20150101T000000"))));
|
||||
|
||||
// DATE-TIME without time zone (UTC)
|
||||
assertEquals(TimeZones.UTC_ID, Event.getTzId(new DtStart(new DateTime(1438607288000L))));
|
||||
|
||||
// DATE-TIME with time zone
|
||||
assertEquals(tzVienna.getID(), Event.getTzId(new DtStart(new DateTime("20150101T000000", tzVienna))));
|
||||
}
|
||||
|
||||
|
||||
public void testRecurringWithException() throws Exception {
|
||||
Event event = parseCalendar("recurring-with-exception1.ics");
|
||||
assertTrue(event.isAllDay());
|
||||
|
||||
assertEquals(1, event.getExceptions().size());
|
||||
Event exception = event.getExceptions().get(0);
|
||||
assertEquals("20150503", exception.getRecurrenceId().getValue());
|
||||
assertEquals("Another summary for the third day", exception.getSummary());
|
||||
}
|
||||
|
||||
public void testStartEndTimes() throws IOException, ParserException, InvalidResourceException {
|
||||
// event with start+end date-time
|
||||
Event eViennaEvolution = parseCalendar("vienna-evolution.ics");
|
||||
assertEquals(1381330800000L, eViennaEvolution.getDtStartInMillis());
|
||||
assertEquals("Europe/Vienna", eViennaEvolution.getDtStartTzID());
|
||||
assertEquals(1381334400000L, eViennaEvolution.getDtEndInMillis());
|
||||
assertEquals("Europe/Vienna", eViennaEvolution.getDtEndTzID());
|
||||
}
|
||||
|
||||
public void testStartEndTimesAllDay() throws IOException, ParserException {
|
||||
// event with start date only
|
||||
assertEquals(868838400000L, eOnThatDay.getDtStartInMillis());
|
||||
assertEquals(TimeZones.UTC_ID, eOnThatDay.getDtStartTzID());
|
||||
// DTEND missing in VEVENT, must have been set to DTSTART+1 day
|
||||
assertEquals(868838400000L + 86400000, eOnThatDay.getDtEndInMillis());
|
||||
assertEquals(TimeZones.UTC_ID, eOnThatDay.getDtEndTzID());
|
||||
|
||||
// event with start+end date for all-day event (one day)
|
||||
assertEquals(868838400000L, eAllDay1Day.getDtStartInMillis());
|
||||
assertEquals(TimeZones.UTC_ID, eAllDay1Day.getDtStartTzID());
|
||||
assertEquals(868838400000L + 86400000, eAllDay1Day.getDtEndInMillis());
|
||||
assertEquals(TimeZones.UTC_ID, eAllDay1Day.getDtEndTzID());
|
||||
|
||||
// event with start+end date for all-day event (ten days)
|
||||
assertEquals(868838400000L, eAllDay10Days.getDtStartInMillis());
|
||||
assertEquals(TimeZones.UTC_ID, eAllDay10Days.getDtStartTzID());
|
||||
assertEquals(868838400000L + 10*86400000, eAllDay10Days.getDtEndInMillis());
|
||||
assertEquals(TimeZones.UTC_ID, eAllDay10Days.getDtEndTzID());
|
||||
|
||||
// event with start+end date on some day (invalid 0 sec-event)
|
||||
assertEquals(868838400000L, eAllDay0Sec.getDtStartInMillis());
|
||||
assertEquals(TimeZones.UTC_ID, eAllDay0Sec.getDtStartTzID());
|
||||
// DTEND invalid in VEVENT, must have been set to DTSTART+1 day
|
||||
assertEquals(868838400000L + 86400000, eAllDay0Sec.getDtEndInMillis());
|
||||
assertEquals(TimeZones.UTC_ID, eAllDay0Sec.getDtEndTzID());
|
||||
}
|
||||
|
||||
public void testUnfolding() throws IOException, InvalidResourceException {
|
||||
Event e = parseCalendar("two-line-description-without-crlf.ics");
|
||||
assertEquals("http://www.tgbornheim.de/index.php?sessionid=&page=&id=&sportcentergroup=&day=6", e.getDescription());
|
||||
}
|
||||
|
||||
|
||||
protected Event parseCalendar(String fname) throws IOException, InvalidResourceException {
|
||||
@Cleanup InputStream in = assetMgr.open(fname, AssetManager.ACCESS_STREAMING);
|
||||
Event e = new Event(fname, null);
|
||||
e.parseEntity(in, null);
|
||||
return e;
|
||||
}
|
||||
}
|
||||
@@ -1,227 +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.Manifest;
|
||||
import android.accounts.Account;
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.ContentProviderClient;
|
||||
import android.content.ContentUris;
|
||||
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.provider.CalendarContract;
|
||||
import android.provider.CalendarContract.Calendars;
|
||||
import android.provider.CalendarContract.Events;
|
||||
import android.test.InstrumentationTestCase;
|
||||
import android.util.Log;
|
||||
|
||||
import net.fortuna.ical4j.model.Date;
|
||||
import net.fortuna.ical4j.model.Dur;
|
||||
import net.fortuna.ical4j.model.TimeZone;
|
||||
import net.fortuna.ical4j.model.component.VAlarm;
|
||||
import net.fortuna.ical4j.model.property.DtEnd;
|
||||
import net.fortuna.ical4j.model.property.DtStart;
|
||||
|
||||
import java.text.ParseException;
|
||||
import java.util.Calendar;
|
||||
|
||||
import at.bitfire.davdroid.DateUtils;
|
||||
import lombok.Cleanup;
|
||||
|
||||
public class LocalCalendarTest extends InstrumentationTestCase {
|
||||
|
||||
private static final String
|
||||
TAG = "davdroid.test",
|
||||
accountType = "at.bitfire.davdroid.test",
|
||||
calendarName = "DAVdroid_Test";
|
||||
|
||||
Context targetContext;
|
||||
|
||||
ContentProviderClient providerClient;
|
||||
final Account testAccount = new Account(calendarName, accountType);
|
||||
|
||||
Uri calendarURI;
|
||||
LocalCalendar testCalendar;
|
||||
|
||||
|
||||
// helpers
|
||||
|
||||
private Uri syncAdapterURI(Uri uri) {
|
||||
return uri.buildUpon()
|
||||
.appendQueryParameter(Calendars.ACCOUNT_TYPE, accountType)
|
||||
.appendQueryParameter(Calendars.ACCOUNT_NAME, accountType)
|
||||
.appendQueryParameter(CalendarContract.CALLER_IS_SYNCADAPTER, "true").
|
||||
build();
|
||||
}
|
||||
|
||||
private long insertNewEvent() throws RemoteException {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(Events.CALENDAR_ID, testCalendar.getId());
|
||||
values.put(Events.TITLE, "Test Event");
|
||||
values.put(Events.ALL_DAY, 0);
|
||||
values.put(Events.DTSTART, Calendar.getInstance().getTimeInMillis());
|
||||
values.put(Events.DTEND, Calendar.getInstance().getTimeInMillis());
|
||||
values.put(Events.EVENT_TIMEZONE, "UTC");
|
||||
values.put(Events.DIRTY, 1);
|
||||
return ContentUris.parseId(providerClient.insert(syncAdapterURI(Events.CONTENT_URI), values));
|
||||
}
|
||||
|
||||
private void deleteEvent(long id) throws RemoteException {
|
||||
providerClient.delete(syncAdapterURI(ContentUris.withAppendedId(Events.CONTENT_URI, id)), null, null);
|
||||
}
|
||||
|
||||
|
||||
// initialization
|
||||
|
||||
@Override
|
||||
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1)
|
||||
protected void setUp() throws LocalStorageException, RemoteException {
|
||||
targetContext = getInstrumentation().getTargetContext();
|
||||
targetContext.enforceCallingOrSelfPermission(Manifest.permission.WRITE_CALENDAR, "No privileges for managing calendars");
|
||||
|
||||
providerClient = targetContext.getContentResolver().acquireContentProviderClient(CalendarContract.AUTHORITY);
|
||||
|
||||
prepareTestCalendar();
|
||||
}
|
||||
|
||||
private void prepareTestCalendar() throws LocalStorageException, RemoteException {
|
||||
@Cleanup Cursor cursor = providerClient.query(Calendars.CONTENT_URI, new String[] { Calendars._ID },
|
||||
Calendars.ACCOUNT_TYPE + "=? AND " + Calendars.ACCOUNT_NAME + "=?",
|
||||
new String[] { testAccount.type, testAccount.name }, null);
|
||||
if (cursor != null && cursor.moveToNext())
|
||||
calendarURI = ContentUris.withAppendedId(Calendars.CONTENT_URI, cursor.getLong(0));
|
||||
else {
|
||||
ServerInfo.ResourceInfo info = new ServerInfo.ResourceInfo(ServerInfo.ResourceInfo.Type.CALENDAR, false, null, "Test Calendar", null, null);
|
||||
calendarURI = LocalCalendar.create(testAccount, targetContext.getContentResolver(), info);
|
||||
}
|
||||
|
||||
Log.i(TAG, "Prepared test calendar " + calendarURI);
|
||||
testCalendar = new LocalCalendar(testAccount, providerClient, ContentUris.parseId(calendarURI), null);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void tearDown() throws RemoteException {
|
||||
deleteTestCalendar();
|
||||
}
|
||||
|
||||
protected void deleteTestCalendar() throws RemoteException {
|
||||
Uri uri = ContentUris.withAppendedId(Calendars.CONTENT_URI, testCalendar.id);
|
||||
if (providerClient.delete(uri,null,null)>0)
|
||||
Log.i(TAG,"Deleted test calendar "+uri);
|
||||
else
|
||||
Log.e(TAG,"Couldn't delete test calendar "+uri);
|
||||
}
|
||||
|
||||
|
||||
// tests
|
||||
|
||||
public void testBuildEntry() throws LocalStorageException, ParseException {
|
||||
final String vcardName = "testBuildEntry";
|
||||
|
||||
final TimeZone tzVienna = DateUtils.tzRegistry.getTimeZone("Europe/Vienna");
|
||||
assertNotNull(tzVienna);
|
||||
|
||||
// build and write event to calendar provider
|
||||
Event event = new Event(vcardName, null);
|
||||
event.summary = "Sample event";
|
||||
event.description = "Sample event with date/time";
|
||||
event.location = "Sample location";
|
||||
event.dtStart = new DtStart("20150501T120000", tzVienna);
|
||||
event.dtEnd = new DtEnd("20150501T130000", tzVienna);
|
||||
assertFalse(event.isAllDay());
|
||||
|
||||
// set an alarm one day, two hours, three minutes and four seconds before begin of event
|
||||
event.getAlarms().add(new VAlarm(new Dur(-1, -2, -3, -4)));
|
||||
|
||||
testCalendar.add(event);
|
||||
testCalendar.commit();
|
||||
|
||||
// read and parse event from calendar provider
|
||||
Event event2 = testCalendar.findByRemoteName(vcardName, true);
|
||||
assertNotNull("Couldn't build and insert event", event);
|
||||
// compare with original event
|
||||
try {
|
||||
assertEquals(event.getSummary(), event2.getSummary());
|
||||
assertEquals(event.getDescription(), event2.getDescription());
|
||||
assertEquals(event.getLocation(), event2.getLocation());
|
||||
assertEquals(event.getDtStart(), event2.getDtStart());
|
||||
assertFalse(event2.isAllDay());
|
||||
|
||||
assertEquals(1, event2.getAlarms().size());
|
||||
VAlarm alarm = event2.getAlarms().get(0);
|
||||
assertEquals(event.getSummary(), alarm.getDescription().getValue()); // should be built from event name
|
||||
assertEquals(new Dur(0, 0, -(24*60 + 60*2 + 3), 0), alarm.getTrigger().getDuration()); // calendar provider stores trigger in minutes
|
||||
} finally {
|
||||
testCalendar.delete(event);
|
||||
}
|
||||
}
|
||||
|
||||
public void testBuildAllDayEntry() throws LocalStorageException, ParseException {
|
||||
final String vcardName = "testBuildAllDayEntry";
|
||||
|
||||
// build and write event to calendar provider
|
||||
Event event = new Event(vcardName, null);
|
||||
event.summary = "All-day event";
|
||||
event.description = "All-day event for testing";
|
||||
event.location = "Sample location testBuildAllDayEntry";
|
||||
event.dtStart = new DtStart(new Date("20150501"));
|
||||
event.dtEnd = new DtEnd(new Date("20150502"));
|
||||
assertTrue(event.isAllDay());
|
||||
testCalendar.add(event);
|
||||
testCalendar.commit();
|
||||
|
||||
// read and parse event from calendar provider
|
||||
Event event2 = testCalendar.findByRemoteName(vcardName, true);
|
||||
assertNotNull("Couldn't build and insert event", event);
|
||||
// compare with original event
|
||||
try {
|
||||
assertEquals(event.getSummary(), event2.getSummary());
|
||||
assertEquals(event.getDescription(), event2.getDescription());
|
||||
assertEquals(event.getLocation(), event2.getLocation());
|
||||
assertEquals(event.getDtStart(), event2.getDtStart());
|
||||
assertTrue(event2.isAllDay());
|
||||
} finally {
|
||||
testCalendar.delete(event);
|
||||
}
|
||||
}
|
||||
|
||||
public void testCTags() throws LocalStorageException {
|
||||
assertNull(testCalendar.getCTag());
|
||||
|
||||
final String cTag = "just-modified";
|
||||
testCalendar.setCTag(cTag);
|
||||
|
||||
assertEquals(cTag, testCalendar.getCTag());
|
||||
}
|
||||
|
||||
public void testFindNew() throws LocalStorageException, RemoteException {
|
||||
// at the beginning, there are no dirty events
|
||||
assertTrue(testCalendar.findNew().length == 0);
|
||||
assertTrue(testCalendar.findUpdated().length == 0);
|
||||
|
||||
// insert a "new" event
|
||||
final long id = insertNewEvent();
|
||||
try {
|
||||
// there must be one "new" event now
|
||||
assertTrue(testCalendar.findNew().length == 1);
|
||||
assertTrue(testCalendar.findUpdated().length == 0);
|
||||
|
||||
// nothing has changed, the record must still be "new"
|
||||
// see issue #233
|
||||
assertTrue(testCalendar.findNew().length == 1);
|
||||
assertTrue(testCalendar.findUpdated().length == 0);
|
||||
} finally {
|
||||
deleteEvent(id);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,95 +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 junit.framework.TestCase;
|
||||
|
||||
import net.fortuna.ical4j.data.CalendarBuilder;
|
||||
import net.fortuna.ical4j.model.Date;
|
||||
import net.fortuna.ical4j.model.TimeZone;
|
||||
import net.fortuna.ical4j.model.component.VTimeZone;
|
||||
import net.fortuna.ical4j.model.property.DtStart;
|
||||
|
||||
import java.io.StringReader;
|
||||
|
||||
import at.bitfire.davdroid.DateUtils;
|
||||
|
||||
public class iCalendarTest extends TestCase {
|
||||
protected final TimeZone tzVienna = DateUtils.tzRegistry.getTimeZone("Europe/Vienna");
|
||||
|
||||
public void testTimezoneDefToTzId() {
|
||||
// test valid definition
|
||||
assertEquals("US-Eastern", Event.TimezoneDefToTzId("BEGIN:VCALENDAR\n" +
|
||||
"PRODID:-//Example Corp.//CalDAV Client//EN\n" +
|
||||
"VERSION:2.0\n" +
|
||||
"BEGIN:VTIMEZONE\n" +
|
||||
"TZID:US-Eastern\n" +
|
||||
"LAST-MODIFIED:19870101T000000Z\n" +
|
||||
"BEGIN:STANDARD\n" +
|
||||
"DTSTART:19671029T020000\n" +
|
||||
"RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10\n" +
|
||||
"TZOFFSETFROM:-0400\n" +
|
||||
"TZOFFSETTO:-0500\n" +
|
||||
"TZNAME:Eastern Standard Time (US & Canada)\n" +
|
||||
"END:STANDARD\n" +
|
||||
"BEGIN:DAYLIGHT\n" +
|
||||
"DTSTART:19870405T020000\n" +
|
||||
"RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4\n" +
|
||||
"TZOFFSETFROM:-0500\n" +
|
||||
"TZOFFSETTO:-0400\n" +
|
||||
"TZNAME:Eastern Daylight Time (US & Canada)\n" +
|
||||
"END:DAYLIGHT\n" +
|
||||
"END:VTIMEZONE\n" +
|
||||
"END:VCALENDAR"));
|
||||
|
||||
// test invalid time zone
|
||||
assertNull(iCalendar.TimezoneDefToTzId("/* invalid content */"));
|
||||
|
||||
// test time zone without TZID
|
||||
assertNull(iCalendar.TimezoneDefToTzId("BEGIN:VCALENDAR\n" +
|
||||
"PRODID:-//Inverse inc./SOGo 2.2.10//EN\n" +
|
||||
"VERSION:2.0\n" +
|
||||
"END:VCALENDAR"));
|
||||
}
|
||||
|
||||
public void testValidateTimeZone() throws Exception {
|
||||
assertNotNull(tzVienna);
|
||||
|
||||
// date (no time zone) should be ignored
|
||||
DtStart date = new DtStart(new Date("20150101"));
|
||||
iCalendar.validateTimeZone(date);
|
||||
assertNull(date.getTimeZone());
|
||||
|
||||
// date-time (Europe/Vienna) should be unchanged
|
||||
DtStart dtStart = new DtStart("20150101", tzVienna);
|
||||
iCalendar.validateTimeZone(dtStart);
|
||||
assertEquals(tzVienna, dtStart.getTimeZone());
|
||||
|
||||
// time zone that is not available on Android systems should be changed to system default
|
||||
CalendarBuilder builder = new CalendarBuilder();
|
||||
net.fortuna.ical4j.model.Calendar cal = builder.build(new StringReader("BEGIN:VCALENDAR\n" +
|
||||
"BEGIN:VTIMEZONE\n" +
|
||||
"TZID:CustomTime\n" +
|
||||
"BEGIN:STANDARD\n" +
|
||||
"TZOFFSETFROM:-0400\n" +
|
||||
"TZOFFSETTO:-0500\n" +
|
||||
"DTSTART:19600101T000000\n" +
|
||||
"END:STANDARD\n" +
|
||||
"END:VTIMEZONE\n" +
|
||||
"END:VCALENDAR"));
|
||||
final TimeZone tzCustom = new TimeZone((VTimeZone)cal.getComponent(VTimeZone.VTIMEZONE));
|
||||
dtStart = new DtStart("20150101T000000", tzCustom);
|
||||
iCalendar.validateTimeZone(dtStart);
|
||||
|
||||
final TimeZone tzDefault = DateUtils.tzRegistry.getTimeZone(java.util.TimeZone.getDefault().getID());
|
||||
assertNotNull(tzDefault);
|
||||
assertEquals(tzDefault.getID(), dtStart.getTimeZone().getID());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,82 +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.test.InstrumentationTestCase;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.util.List;
|
||||
|
||||
import at.bitfire.davdroid.TestConstants;
|
||||
import at.bitfire.davdroid.resource.DavResourceFinder;
|
||||
import at.bitfire.davdroid.resource.ServerInfo;
|
||||
import at.bitfire.davdroid.resource.ServerInfo.ResourceInfo;
|
||||
|
||||
public class DavResourceFinderTest extends InstrumentationTestCase {
|
||||
|
||||
DavResourceFinder finder;
|
||||
|
||||
@Override
|
||||
protected void setUp() {
|
||||
finder = new DavResourceFinder(getInstrumentation().getContext());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void tearDown() throws IOException {
|
||||
finder.close();
|
||||
}
|
||||
|
||||
|
||||
public void testFindResourcesRobohydra() throws Exception {
|
||||
ServerInfo info = new ServerInfo(new URI(TestConstants.ROBOHYDRA_BASE), "test", "test", true);
|
||||
finder.findResources(info);
|
||||
|
||||
/*** CardDAV ***/
|
||||
assertTrue(info.isCardDAV());
|
||||
List<ResourceInfo> collections = info.getAddressBooks();
|
||||
// two address books
|
||||
assertEquals(2, collections.size());
|
||||
ResourceInfo collection = collections.get(0);
|
||||
assertEquals(TestConstants.roboHydra.resolve("/dav/addressbooks/test/default.vcf/").toString(), collection.getURL());
|
||||
assertEquals("Default Address Book", collection.getDescription());
|
||||
// second one
|
||||
collection = collections.get(1);
|
||||
assertEquals("https://my.server/absolute:uri/my-address-book/", collection.getURL());
|
||||
assertEquals("Absolute URI VCard Book", collection.getDescription());
|
||||
|
||||
/*** CalDAV ***/
|
||||
assertTrue(info.isCalDAV());
|
||||
collections = info.getCalendars();
|
||||
assertEquals(2, collections.size());
|
||||
|
||||
ResourceInfo resource = collections.get(0);
|
||||
assertEquals("Private Calendar", resource.getTitle());
|
||||
assertEquals("This is my private calendar.", resource.getDescription());
|
||||
assertFalse(resource.isReadOnly());
|
||||
|
||||
resource = collections.get(1);
|
||||
assertEquals("Work Calendar", resource.getTitle());
|
||||
assertTrue(resource.isReadOnly());
|
||||
}
|
||||
|
||||
|
||||
public void testGetInitialContextURL() throws Exception {
|
||||
// without SRV records, but with well-known paths
|
||||
ServerInfo roboHydra = new ServerInfo(new URI(TestConstants.ROBOHYDRA_BASE), "test", "test", true);
|
||||
assertEquals(TestConstants.roboHydra.resolve("/"), finder.getInitialContextURL(roboHydra, "caldav"));
|
||||
assertEquals(TestConstants.roboHydra.resolve("/"), finder.getInitialContextURL(roboHydra, "carddav"));
|
||||
|
||||
// with SRV records and well-known paths
|
||||
ServerInfo iCloud = new ServerInfo(new URI("mailto:test@icloud.com"), "", "", true);
|
||||
assertEquals(new URI("https://contacts.icloud.com/"), finder.getInitialContextURL(iCloud, "carddav"));
|
||||
assertEquals(new URI("https://caldav.icloud.com/"), finder.getInitialContextURL(iCloud, "caldav"));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
/*
|
||||
* 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.test.InstrumentationTestCase;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
|
||||
import at.bitfire.dav4android.DavResource;
|
||||
import at.bitfire.dav4android.exception.DavException;
|
||||
import at.bitfire.dav4android.exception.HttpException;
|
||||
import at.bitfire.dav4android.property.AddressbookHomeSet;
|
||||
import at.bitfire.dav4android.property.ResourceType;
|
||||
import at.bitfire.davdroid.App;
|
||||
import at.bitfire.davdroid.HttpClient;
|
||||
import at.bitfire.davdroid.ui.setup.DavResourceFinder;
|
||||
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 {
|
||||
|
||||
MockWebServer server = new MockWebServer();
|
||||
|
||||
DavResourceFinder finder;
|
||||
OkHttpClient client;
|
||||
LoginCredentials credentials;
|
||||
|
||||
private static final String
|
||||
PATH_NO_DAV = "/nodav",
|
||||
|
||||
PATH_CALDAV = "/caldav",
|
||||
PATH_CARDDAV = "/carddav",
|
||||
PATH_CALDAV_AND_CARDDAV = "/both-caldav-carddav",
|
||||
|
||||
SUBPATH_PRINCIPAL = "/principal",
|
||||
SUBPATH_ADDRESSBOOK_HOMESET = "/addressbooks",
|
||||
SUBPATH_ADDRESSBOOK = "/addressbooks/private-contacts";
|
||||
|
||||
@Override
|
||||
protected void setUp() throws Exception {
|
||||
server.setDispatcher(new TestDispatcher());
|
||||
server.start();
|
||||
|
||||
credentials = new LoginCredentials(URI.create("/"), "mock", "12345", true);
|
||||
finder = new DavResourceFinder(getInstrumentation().getContext(), credentials);
|
||||
|
||||
client = HttpClient.create();
|
||||
client = HttpClient.addAuthentication(client, credentials.userName, credentials.password, credentials.authPreemptive);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void tearDown() throws Exception {
|
||||
server.shutdown();
|
||||
}
|
||||
|
||||
|
||||
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());
|
||||
|
||||
// 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());
|
||||
|
||||
// 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());
|
||||
}
|
||||
|
||||
public void testProvidesService() throws IOException {
|
||||
assertFalse(finder.providesService(server.url(PATH_NO_DAV), DavResourceFinder.Service.CALDAV));
|
||||
assertFalse(finder.providesService(server.url(PATH_NO_DAV), DavResourceFinder.Service.CARDDAV));
|
||||
|
||||
assertTrue(finder.providesService(server.url(PATH_CALDAV), DavResourceFinder.Service.CALDAV));
|
||||
assertFalse(finder.providesService(server.url(PATH_CALDAV), DavResourceFinder.Service.CARDDAV));
|
||||
|
||||
assertTrue(finder.providesService(server.url(PATH_CARDDAV), DavResourceFinder.Service.CARDDAV));
|
||||
assertFalse(finder.providesService(server.url(PATH_CARDDAV), DavResourceFinder.Service.CALDAV));
|
||||
|
||||
assertTrue(finder.providesService(server.url(PATH_CALDAV_AND_CARDDAV), DavResourceFinder.Service.CALDAV));
|
||||
assertTrue(finder.providesService(server.url(PATH_CALDAV_AND_CARDDAV), DavResourceFinder.Service.CARDDAV));
|
||||
}
|
||||
|
||||
public void testGetCurrentUserPrincipal() throws IOException, HttpException, DavException {
|
||||
assertNull(finder.getCurrentUserPrincipal(server.url(PATH_NO_DAV), DavResourceFinder.Service.CALDAV));
|
||||
assertNull(finder.getCurrentUserPrincipal(server.url(PATH_NO_DAV), DavResourceFinder.Service.CARDDAV));
|
||||
|
||||
assertEquals(
|
||||
server.url(PATH_CALDAV + SUBPATH_PRINCIPAL).uri(),
|
||||
finder.getCurrentUserPrincipal(server.url(PATH_CALDAV), DavResourceFinder.Service.CALDAV)
|
||||
);
|
||||
assertNull(finder.getCurrentUserPrincipal(server.url(PATH_CALDAV), DavResourceFinder.Service.CARDDAV));
|
||||
|
||||
assertEquals(
|
||||
server.url(PATH_CARDDAV + SUBPATH_PRINCIPAL).uri(),
|
||||
finder.getCurrentUserPrincipal(server.url(PATH_CARDDAV), DavResourceFinder.Service.CARDDAV)
|
||||
);
|
||||
assertNull(finder.getCurrentUserPrincipal(server.url(PATH_CARDDAV), DavResourceFinder.Service.CALDAV));
|
||||
}
|
||||
|
||||
|
||||
// mock server
|
||||
|
||||
public class TestDispatcher extends Dispatcher {
|
||||
|
||||
@Override
|
||||
public MockResponse dispatch(RecordedRequest rq) throws InterruptedException {
|
||||
if (!checkAuth(rq))
|
||||
return new MockResponse().setResponseCode(401);
|
||||
|
||||
String path = rq.getPath();
|
||||
|
||||
if ("OPTIONS".equalsIgnoreCase(rq.getMethod())) {
|
||||
String dav = null;
|
||||
if (path.startsWith(PATH_CALDAV))
|
||||
dav = "calendar-access";
|
||||
else if (path.startsWith(PATH_CARDDAV))
|
||||
dav = "addressbook";
|
||||
else if (path.startsWith(PATH_CALDAV_AND_CARDDAV))
|
||||
dav = "calendar-access, addressbook";
|
||||
MockResponse response = new MockResponse().setResponseCode(200);
|
||||
if (dav != null)
|
||||
response.addHeader("DAV", dav);
|
||||
return response;
|
||||
|
||||
} else if ("PROPFIND".equalsIgnoreCase(rq.getMethod())) {
|
||||
String props = null;
|
||||
switch (path) {
|
||||
case PATH_CALDAV:
|
||||
case PATH_CARDDAV:
|
||||
props = "<current-user-principal><href>" + path + SUBPATH_PRINCIPAL + "</href></current-user-principal>";
|
||||
break;
|
||||
|
||||
case PATH_CARDDAV + SUBPATH_PRINCIPAL:
|
||||
props = "<CARD:addressbook-home-set>" +
|
||||
" <href>" + PATH_CARDDAV + SUBPATH_ADDRESSBOOK_HOMESET + "</href>" +
|
||||
"</CARD:addressbook-home-set>";
|
||||
break;
|
||||
case PATH_CARDDAV + SUBPATH_ADDRESSBOOK:
|
||||
props = "<resourcetype>" +
|
||||
" <collection/>" +
|
||||
" <CARD:addressbook/>" +
|
||||
"</resourcetype>";
|
||||
break;
|
||||
}
|
||||
App.log.info("Sending props: " + props);
|
||||
return new MockResponse()
|
||||
.setResponseCode(207)
|
||||
.setBody("<multistatus xmlns='DAV:' xmlns:CARD='urn:ietf:params:xml:ns:carddav'>" +
|
||||
"<response>" +
|
||||
" <href>" + rq.getPath() + "</href>" +
|
||||
" <propstat><prop>" + props + "</prop></propstat>" +
|
||||
"</response>" +
|
||||
"</multistatus>");
|
||||
}
|
||||
|
||||
return new MockResponse().setResponseCode(404);
|
||||
}
|
||||
|
||||
private boolean checkAuth(RecordedRequest rq) {
|
||||
return "Basic bW9jazoxMjM0NQ==".equals(rq.getHeader("Authorization"));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,72 +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.webdav;
|
||||
|
||||
import android.test.InstrumentationTestCase;
|
||||
|
||||
import org.apache.http.client.methods.CloseableHttpResponse;
|
||||
import org.apache.http.client.methods.HttpGetHC4;
|
||||
import org.apache.http.client.methods.HttpPostHC4;
|
||||
import org.apache.http.impl.client.CloseableHttpClient;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
|
||||
import at.bitfire.davdroid.TestConstants;
|
||||
|
||||
public class DavHttpClientTest extends InstrumentationTestCase {
|
||||
final static URI testCookieURI = TestConstants.roboHydra.resolve("/dav/testCookieStore");
|
||||
|
||||
CloseableHttpClient httpClient;
|
||||
|
||||
@Override
|
||||
protected void setUp() throws Exception {
|
||||
httpClient = DavHttpClient.create();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void tearDown() throws Exception {
|
||||
httpClient.close();
|
||||
}
|
||||
|
||||
|
||||
public void testCookies() throws IOException {
|
||||
CloseableHttpResponse response = null;
|
||||
|
||||
HttpGetHC4 get = new HttpGetHC4(testCookieURI);
|
||||
get.setHeader("Accept", "text/xml");
|
||||
|
||||
// at first, DavHttpClient doesn't send a cookie
|
||||
try {
|
||||
response = httpClient.execute(get);
|
||||
assertEquals(412, response.getStatusLine().getStatusCode());
|
||||
} finally {
|
||||
if (response != null)
|
||||
response.close();
|
||||
}
|
||||
|
||||
// POST sets a cookie to DavHttpClient
|
||||
try {
|
||||
response = httpClient.execute(new HttpPostHC4(testCookieURI));
|
||||
assertEquals(200, response.getStatusLine().getStatusCode());
|
||||
} finally {
|
||||
if (response != null)
|
||||
response.close();
|
||||
}
|
||||
|
||||
// and now DavHttpClient sends a cookie for GET, too
|
||||
try {
|
||||
response = httpClient.execute(get);
|
||||
assertEquals(200, response.getStatusLine().getStatusCode());
|
||||
} finally {
|
||||
if (response != null)
|
||||
response.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,83 +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.webdav;
|
||||
|
||||
import junit.framework.TestCase;
|
||||
|
||||
import org.apache.http.HttpResponse;
|
||||
import org.apache.http.client.methods.HttpOptions;
|
||||
import org.apache.http.client.methods.HttpUriRequest;
|
||||
import org.apache.http.client.protocol.HttpClientContext;
|
||||
import org.apache.http.impl.client.CloseableHttpClient;
|
||||
import org.apache.http.impl.client.HttpClientBuilder;
|
||||
import org.apache.http.protocol.HttpContext;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import at.bitfire.davdroid.TestConstants;
|
||||
|
||||
public class DavRedirectStrategyTest extends TestCase {
|
||||
|
||||
CloseableHttpClient httpClient;
|
||||
final DavRedirectStrategy strategy = DavRedirectStrategy.INSTANCE;
|
||||
|
||||
@Override
|
||||
protected void setUp() {
|
||||
httpClient = HttpClientBuilder.create()
|
||||
.useSystemProperties()
|
||||
.disableRedirectHandling()
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void tearDown() throws IOException {
|
||||
httpClient.close();
|
||||
}
|
||||
|
||||
|
||||
// happy cases
|
||||
|
||||
public void testNonRedirection() throws Exception {
|
||||
HttpUriRequest request = new HttpOptions(TestConstants.roboHydra);
|
||||
HttpResponse response = httpClient.execute(request);
|
||||
assertFalse(strategy.isRedirected(request, response, null));
|
||||
}
|
||||
|
||||
public void testDefaultRedirection() throws Exception {
|
||||
final String newLocation = "/new-location";
|
||||
|
||||
HttpContext context = HttpClientContext.create();
|
||||
HttpUriRequest request = new HttpOptions(TestConstants.roboHydra.resolve("redirect/301?to=" + newLocation));
|
||||
HttpResponse response = httpClient.execute(request, context);
|
||||
assertTrue(strategy.isRedirected(request, response, context));
|
||||
|
||||
HttpUriRequest redirected = strategy.getRedirect(request, response, context);
|
||||
assertEquals(TestConstants.roboHydra.resolve(newLocation), redirected.getURI());
|
||||
}
|
||||
|
||||
|
||||
// error cases
|
||||
|
||||
public void testMissingLocation() throws Exception {
|
||||
HttpContext context = HttpClientContext.create();
|
||||
HttpUriRequest request = new HttpOptions(TestConstants.roboHydra.resolve("redirect/without-location"));
|
||||
HttpResponse response = httpClient.execute(request, context);
|
||||
assertFalse(strategy.isRedirected(request, response, context));
|
||||
}
|
||||
|
||||
public void testRelativeLocation() throws Exception {
|
||||
HttpContext context = HttpClientContext.create();
|
||||
HttpUriRequest request = new HttpOptions(TestConstants.roboHydra.resolve("redirect/relative"));
|
||||
HttpResponse response = httpClient.execute(request, context);
|
||||
assertTrue(strategy.isRedirected(request, response, context));
|
||||
|
||||
HttpUriRequest redirected = strategy.getRedirect(request, response, context);
|
||||
assertEquals(TestConstants.roboHydra.resolve("/new/location"), redirected.getURI());
|
||||
}
|
||||
}
|
||||
@@ -1,116 +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.webdav;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import junit.framework.TestCase;
|
||||
|
||||
import org.apache.commons.lang3.ArrayUtils;
|
||||
import org.apache.commons.lang3.exception.ExceptionUtils;
|
||||
import org.apache.http.HttpHost;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.Socket;
|
||||
import java.security.cert.CertPathValidatorException;
|
||||
|
||||
import javax.net.ssl.SSLException;
|
||||
import javax.net.ssl.SSLHandshakeException;
|
||||
|
||||
import lombok.Cleanup;
|
||||
|
||||
public class TlsSniSocketFactoryTest extends TestCase {
|
||||
private static final String TAG = "davdroid.TlsSniSocketFactoryTest";
|
||||
|
||||
final TlsSniSocketFactory factory = TlsSniSocketFactory.getSocketFactory();
|
||||
|
||||
private InetSocketAddress sampleTlsEndpoint;
|
||||
|
||||
@Override
|
||||
protected void setUp() {
|
||||
// sni.velox.ch is used to test SNI (without SNI support, the certificate is invalid)
|
||||
sampleTlsEndpoint = new InetSocketAddress("sni.velox.ch", 443);
|
||||
}
|
||||
|
||||
public void testCreateSocket() {
|
||||
try {
|
||||
@Cleanup Socket socket = factory.createSocket(null);
|
||||
assertFalse(socket.isConnected());
|
||||
} catch (IOException e) {
|
||||
fail();
|
||||
}
|
||||
}
|
||||
|
||||
public void testConnectSocket() {
|
||||
try {
|
||||
factory.connectSocket(1000, null, new HttpHost(sampleTlsEndpoint.getHostName()), sampleTlsEndpoint, null, null);
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "I/O exception", e);
|
||||
fail();
|
||||
}
|
||||
}
|
||||
|
||||
public void testCreateLayeredSocket() {
|
||||
try {
|
||||
// connect plain socket first
|
||||
@Cleanup Socket plain = new Socket();
|
||||
plain.connect(sampleTlsEndpoint);
|
||||
assertTrue(plain.isConnected());
|
||||
|
||||
// then create TLS socket on top of it and establish TLS Connection
|
||||
@Cleanup Socket socket = factory.createLayeredSocket(plain, sampleTlsEndpoint.getHostName(), sampleTlsEndpoint.getPort(), null);
|
||||
assertTrue(socket.isConnected());
|
||||
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "I/O exception", e);
|
||||
fail();
|
||||
}
|
||||
}
|
||||
|
||||
public void testProtocolVersions() throws IOException {
|
||||
String enabledProtocols[] = TlsSniSocketFactory.protocols;
|
||||
// SSL (all versions) should be disabled
|
||||
for (String protocol : enabledProtocols)
|
||||
assertFalse(protocol.contains("SSL"));
|
||||
// TLS v1+ should be enabled
|
||||
assertTrue(ArrayUtils.contains(enabledProtocols, "TLSv1"));
|
||||
assertTrue(ArrayUtils.contains(enabledProtocols, "TLSv1.1"));
|
||||
assertTrue(ArrayUtils.contains(enabledProtocols, "TLSv1.2"));
|
||||
}
|
||||
|
||||
|
||||
public void testHostnameNotInCertificate() throws IOException {
|
||||
try {
|
||||
// host with certificate that doesn't match host name
|
||||
// use the IP address as host name because IP addresses are usually not in the certificate subject
|
||||
final String ipHostname = sampleTlsEndpoint.getAddress().getHostAddress();
|
||||
InetSocketAddress host = new InetSocketAddress(ipHostname, 443);
|
||||
@Cleanup Socket socket = factory.connectSocket(0, null, new HttpHost(ipHostname), host, null, null);
|
||||
fail();
|
||||
} catch (SSLException e) {
|
||||
Log.i(TAG, "Expected exception", e);
|
||||
assertFalse(ExceptionUtils.indexOfType(e, SSLException.class) == -1);
|
||||
}
|
||||
}
|
||||
|
||||
public void testUntrustedCertificate() throws IOException {
|
||||
try {
|
||||
// host with certificate that is not trusted by default
|
||||
InetSocketAddress host = new InetSocketAddress("cacert.org", 443);
|
||||
|
||||
@Cleanup Socket socket = factory.connectSocket(0, null, new HttpHost(host.getHostName()), host, null, null);
|
||||
fail();
|
||||
} catch (SSLHandshakeException e) {
|
||||
Log.i(TAG, "Expected exception", e);
|
||||
assertFalse(ExceptionUtils.indexOfType(e, CertPathValidatorException.class) == -1);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,286 +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.webdav;
|
||||
|
||||
import android.content.res.AssetManager;
|
||||
import android.test.InstrumentationTestCase;
|
||||
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.apache.http.impl.client.CloseableHttpClient;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.net.URI;
|
||||
import java.util.Arrays;
|
||||
|
||||
import javax.net.ssl.SSLPeerUnverifiedException;
|
||||
|
||||
import at.bitfire.davdroid.TestConstants;
|
||||
import at.bitfire.davdroid.webdav.HttpPropfind.Mode;
|
||||
import at.bitfire.davdroid.webdav.WebDavResource.PutMode;
|
||||
import ezvcard.VCardVersion;
|
||||
import lombok.Cleanup;
|
||||
|
||||
// tests require running robohydra!
|
||||
|
||||
public class WebDavResourceTest extends InstrumentationTestCase {
|
||||
final static byte[] SAMPLE_CONTENT = new byte[] { 1, 2, 3, 4, 5 };
|
||||
|
||||
AssetManager assetMgr;
|
||||
CloseableHttpClient httpClient;
|
||||
|
||||
WebDavResource baseDAV;
|
||||
WebDavResource davAssets,
|
||||
davCollection, davNonExistingFile, davExistingFile;
|
||||
|
||||
@Override
|
||||
protected void setUp() throws Exception {
|
||||
httpClient = DavHttpClient.create();
|
||||
|
||||
assetMgr = getInstrumentation().getContext().getResources().getAssets();
|
||||
|
||||
baseDAV = new WebDavResource(httpClient, TestConstants.roboHydra.resolve("/dav/"));
|
||||
davAssets = new WebDavResource(httpClient, TestConstants.roboHydra.resolve("assets/"));
|
||||
davCollection = new WebDavResource(httpClient, TestConstants.roboHydra.resolve("dav/"));
|
||||
|
||||
davNonExistingFile = new WebDavResource(davCollection, "collection/new.file");
|
||||
davExistingFile = new WebDavResource(davCollection, "collection/existing.file");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void tearDown() throws Exception {
|
||||
httpClient.close();
|
||||
}
|
||||
|
||||
|
||||
/* test feature detection */
|
||||
|
||||
public void testOptions() throws Exception {
|
||||
String[] davMethods = new String[] { "PROPFIND", "GET", "PUT", "DELETE", "REPORT" },
|
||||
davCapabilities = new String[] { "addressbook", "calendar-access" };
|
||||
|
||||
WebDavResource capable = new WebDavResource(baseDAV);
|
||||
capable.options();
|
||||
for (String davMethod : davMethods)
|
||||
assertTrue(capable.supportsMethod(davMethod));
|
||||
for (String capability : davCapabilities)
|
||||
assertTrue(capable.supportsDAV(capability));
|
||||
}
|
||||
|
||||
public void testPropfindCurrentUserPrincipal() throws Exception {
|
||||
davCollection.propfind(HttpPropfind.Mode.CURRENT_USER_PRINCIPAL);
|
||||
assertEquals(new URI("/dav/principals/users/test"), davCollection.getProperties().getCurrentUserPrincipal());
|
||||
|
||||
WebDavResource simpleFile = new WebDavResource(davAssets, "test.random");
|
||||
try {
|
||||
simpleFile.propfind(HttpPropfind.Mode.CURRENT_USER_PRINCIPAL);
|
||||
fail();
|
||||
|
||||
} catch(DavException ex) {
|
||||
}
|
||||
assertNull(simpleFile.getProperties().getCurrentUserPrincipal());
|
||||
}
|
||||
|
||||
public void testPropfindHomeSets() throws Exception {
|
||||
WebDavResource dav = new WebDavResource(davCollection, "principals/users/test");
|
||||
dav.propfind(HttpPropfind.Mode.HOME_SETS);
|
||||
assertEquals(new URI("/dav/addressbooks/test/"), dav.getProperties().getAddressbookHomeSet());
|
||||
assertEquals(new URI("/dav/calendars/test/"), dav.getProperties().getCalendarHomeSet());
|
||||
}
|
||||
|
||||
public void testPropfindAddressBooks() throws Exception {
|
||||
WebDavResource dav = new WebDavResource(davCollection, "addressbooks/test");
|
||||
dav.propfind(HttpPropfind.Mode.CARDDAV_COLLECTIONS);
|
||||
|
||||
// there should be two address books
|
||||
assertEquals(3, dav.getMembers().size());
|
||||
|
||||
// the first one is not an address book and not even a collection (referenced by relative URI)
|
||||
WebDavResource ab = dav.getMembers().get(0);
|
||||
assertEquals(TestConstants.roboHydra.resolve("/dav/addressbooks/test/useless-member"), ab.getLocation());
|
||||
assertEquals("useless-member", ab.getName());
|
||||
assertFalse(ab.getProperties().isAddressBook());
|
||||
|
||||
// the second one is an address book (referenced by relative URI)
|
||||
ab = dav.getMembers().get(1);
|
||||
assertEquals(TestConstants.roboHydra.resolve("/dav/addressbooks/test/default.vcf/"), ab.getLocation());
|
||||
assertEquals("default.vcf", ab.getName());
|
||||
assertTrue(ab.getProperties().isAddressBook());
|
||||
|
||||
// the third one is an address book (referenced by an absolute URI)
|
||||
ab = dav.getMembers().get(2);
|
||||
assertEquals(new URI("https://my.server/absolute:uri/my-address-book/"), ab.getLocation());
|
||||
assertEquals("my-address-book", ab.getName());
|
||||
assertTrue(ab.getProperties().isAddressBook());
|
||||
}
|
||||
|
||||
public void testPropfindCalendars() throws Exception {
|
||||
WebDavResource dav = new WebDavResource(davCollection, "calendars/test");
|
||||
dav.propfind(Mode.CALDAV_COLLECTIONS);
|
||||
assertEquals(3, dav.getMembers().size());
|
||||
assertEquals(new Integer(0xFFFF00FF), dav.getMembers().get(2).getProperties().getColor());
|
||||
for (WebDavResource member : dav.getMembers()) {
|
||||
if (member.getName().contains(".ics"))
|
||||
assertTrue(member.getProperties().isCalendar());
|
||||
else
|
||||
assertFalse(member.getProperties().isCalendar());
|
||||
assertFalse(member.getProperties().isAddressBook());
|
||||
}
|
||||
}
|
||||
|
||||
public void testPropfindCollectionProperties() throws Exception {
|
||||
WebDavResource dav = new WebDavResource(davCollection, "propfind-collection-properties");
|
||||
dav.propfind(Mode.COLLECTION_PROPERTIES);
|
||||
assertTrue(dav.members.isEmpty());
|
||||
assertTrue(dav.properties.isCollection);
|
||||
assertTrue(dav.properties.isAddressBook);
|
||||
assertNull(dav.properties.displayName);
|
||||
assertNull(dav.properties.color);
|
||||
assertEquals(VCardVersion.V4_0, dav.properties.supportedVCardVersion);
|
||||
}
|
||||
|
||||
public void testPropfindTrailingSlashes() throws Exception {
|
||||
final String principalOK = "/principals/ok";
|
||||
|
||||
String requestPaths[] = {
|
||||
"/dav/collection-response-with-trailing-slash",
|
||||
"/dav/collection-response-with-trailing-slash/",
|
||||
"/dav/collection-response-without-trailing-slash",
|
||||
"/dav/collection-response-without-trailing-slash/"
|
||||
};
|
||||
|
||||
for (String path : requestPaths) {
|
||||
WebDavResource davSlash = new WebDavResource(davCollection, new URI(path));
|
||||
davSlash.propfind(Mode.CARDDAV_COLLECTIONS);
|
||||
assertEquals(new URI(principalOK), davSlash.getProperties().getCurrentUserPrincipal());
|
||||
}
|
||||
}
|
||||
|
||||
public void testStrangeMemberNames() throws Exception {
|
||||
// construct a WebDavResource from a base collection and a member which is an encoded URL (see https://github.com/bitfireAT/davdroid/issues/482)
|
||||
WebDavResource dav = new WebDavResource(davCollection, "http%3A%2F%2Fwww.invalid.example%2Fm8%2Ffeeds%2Fcontacts%2Fmaria.mueller%2540gmail.com%2Fbase%2F5528abc5720cecc.vcf");
|
||||
dav.get("text/vcard");
|
||||
}
|
||||
|
||||
|
||||
/* test normal HTTP/WebDAV */
|
||||
|
||||
public void testPropfindRedirection() throws Exception {
|
||||
// PROPFIND redirection
|
||||
WebDavResource redirected = new WebDavResource(baseDAV, new URI("/redirect/301?to=/dav/"));
|
||||
redirected.propfind(Mode.CURRENT_USER_PRINCIPAL);
|
||||
assertEquals("/dav/", redirected.getLocation().getPath());
|
||||
}
|
||||
|
||||
public void testGet() throws Exception {
|
||||
WebDavResource simpleFile = new WebDavResource(davAssets, "test.random");
|
||||
simpleFile.get("*/*");
|
||||
@Cleanup InputStream is = assetMgr.open("test.random", AssetManager.ACCESS_STREAMING);
|
||||
byte[] expected = IOUtils.toByteArray(is);
|
||||
assertTrue(Arrays.equals(expected, simpleFile.getContent()));
|
||||
}
|
||||
|
||||
public void testGetHttpsWithSni() throws Exception {
|
||||
WebDavResource file = new WebDavResource(httpClient, new URI("https://sni.velox.ch"));
|
||||
|
||||
boolean sniWorking = false;
|
||||
try {
|
||||
file.get("* /*");
|
||||
sniWorking = true;
|
||||
} catch (SSLPeerUnverifiedException e) {
|
||||
}
|
||||
|
||||
assertTrue(sniWorking);
|
||||
}
|
||||
|
||||
public void testMultiGet() throws Exception {
|
||||
WebDavResource davAddressBook = new WebDavResource(davCollection, "addressbooks/default.vcf/");
|
||||
davAddressBook.multiGet(DavMultiget.Type.ADDRESS_BOOK, new String[] { "1.vcf", "2:3@my%40pc.vcf" });
|
||||
// queried address book has a name
|
||||
assertEquals("My Book", davAddressBook.getProperties().getDisplayName());
|
||||
// there are two contacts
|
||||
assertEquals(2, davAddressBook.getMembers().size());
|
||||
// contact file names should be unescaped (yes, it's really named ...%40pc... to check double-encoding)
|
||||
assertEquals("1.vcf", davAddressBook.getMembers().get(0).getName());
|
||||
assertEquals("2:3@my%40pc.vcf", davAddressBook.getMembers().get(1).getName());
|
||||
// all contacts have some content
|
||||
for (WebDavResource member : davAddressBook.getMembers())
|
||||
assertNotNull(member.getContent());
|
||||
}
|
||||
|
||||
public void testMultiGetWith404() throws Exception {
|
||||
WebDavResource davAddressBook = new WebDavResource(davCollection, "addressbooks/default-with-404.vcf/");
|
||||
try {
|
||||
davAddressBook.multiGet(DavMultiget.Type.ADDRESS_BOOK, new String[]{ "notexisting" });
|
||||
fail();
|
||||
} catch(NotFoundException e) {
|
||||
// addressbooks/default.vcf/notexisting doesn't exist,
|
||||
// so server responds with 404 which causes a NotFoundException
|
||||
}
|
||||
}
|
||||
|
||||
public void testPutAddDontOverwrite() throws Exception {
|
||||
// should succeed on a non-existing file
|
||||
assertEquals("has-just-been-created", davNonExistingFile.put(SAMPLE_CONTENT, PutMode.ADD_DONT_OVERWRITE));
|
||||
|
||||
// should fail on an existing file
|
||||
try {
|
||||
davExistingFile.put(SAMPLE_CONTENT, PutMode.ADD_DONT_OVERWRITE);
|
||||
fail();
|
||||
} catch(PreconditionFailedException ex) {
|
||||
}
|
||||
}
|
||||
|
||||
public void testPutUpdateDontOverwrite() throws Exception {
|
||||
// should succeed on an existing file
|
||||
assertEquals("has-just-been-updated", davExistingFile.put(SAMPLE_CONTENT, PutMode.UPDATE_DONT_OVERWRITE));
|
||||
|
||||
// should fail on a non-existing file (resource has been deleted on server, thus server returns 412)
|
||||
try {
|
||||
davNonExistingFile.put(SAMPLE_CONTENT, PutMode.UPDATE_DONT_OVERWRITE);
|
||||
fail();
|
||||
} catch(PreconditionFailedException ex) {
|
||||
}
|
||||
|
||||
// should fail on existing file with wrong ETag (resource has changed on server, thus server returns 409)
|
||||
try {
|
||||
WebDavResource dav = new WebDavResource(davCollection, new URI("collection/existing.file?conflict=1"));
|
||||
dav.put(SAMPLE_CONTENT, PutMode.UPDATE_DONT_OVERWRITE);
|
||||
fail();
|
||||
} catch(ConflictException ex) {
|
||||
}
|
||||
}
|
||||
|
||||
public void testDelete() throws Exception {
|
||||
// should succeed on an existing file
|
||||
davExistingFile.delete();
|
||||
|
||||
// should fail on a non-existing file
|
||||
try {
|
||||
davNonExistingFile.delete();
|
||||
fail();
|
||||
} catch (NotFoundException e) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* test CalDAV/CardDAV */
|
||||
|
||||
|
||||
/* special test */
|
||||
|
||||
public void testGetSpecialURLs() throws Exception {
|
||||
WebDavResource dav = new WebDavResource(davAssets, "member-with:colon.vcf");
|
||||
try {
|
||||
dav.get("*/*");
|
||||
fail();
|
||||
} catch(NotFoundException e) {
|
||||
assertTrue(true);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ Copyright (c) 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
|
||||
-->
|
||||
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical" >
|
||||
|
||||
<TextView
|
||||
style="@style/TextView.Heading"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="20dp"
|
||||
android:text="@string/setup_task_lists" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="10dp"
|
||||
android:layout_marginTop="10dp"
|
||||
android:text="@string/setup_select_task_lists" />
|
||||
|
||||
</LinearLayout>
|
||||
1
app/src/androidTest/robohydra/.gitignore
vendored
1
app/src/androidTest/robohydra/.gitignore
vendored
@@ -1 +0,0 @@
|
||||
node_modules
|
||||
@@ -1,5 +0,0 @@
|
||||
{"plugins":[
|
||||
"assets",
|
||||
"redirect",
|
||||
"dav"
|
||||
]}
|
||||
@@ -1,12 +0,0 @@
|
||||
var RoboHydraHeadFilesystem = require("robohydra").heads.RoboHydraHeadFilesystem;
|
||||
|
||||
exports.getBodyParts = function(conf) {
|
||||
return {
|
||||
heads: [
|
||||
new RoboHydraHeadFilesystem({
|
||||
mountPath: '/assets/',
|
||||
documentRoot: '../assets'
|
||||
})
|
||||
]
|
||||
};
|
||||
};
|
||||
@@ -1,386 +0,0 @@
|
||||
// vim: ts=4:sw=4
|
||||
|
||||
var roboHydraHeadDAV = require("../headdav");
|
||||
|
||||
exports.getBodyParts = function(conf) {
|
||||
return {
|
||||
heads: [
|
||||
/* base URL, provide default DAV here */
|
||||
new RoboHydraHeadDAV({ path: "/dav/" }),
|
||||
|
||||
/* test cookie:
|
||||
* POST /dav/testCookieStore will cause the mock server to set a cookie
|
||||
* GET /dav/testCookieStore will cause the mock server to check the request cookie
|
||||
* and return 412 Precondition failed when it's not set correctly
|
||||
*/
|
||||
new RoboHydraHeadDAV({
|
||||
path: "/dav/testCookieStore",
|
||||
handler: function(req,res,next) {
|
||||
var cookie = 'sess=MY_SESSION_12345';
|
||||
if (req.method == "POST") {
|
||||
res.statusCode = 200;
|
||||
res.headers['Set-Cookie'] = cookie;
|
||||
res.send("Setting cookie");
|
||||
} else {
|
||||
res.statusCode = (req.headers['cookie'] == cookie) ? 200 : 412;
|
||||
res.send("Checking cookie");
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
||||
/* multistatus parsing */
|
||||
new RoboHydraHeadDAV({
|
||||
path: "/dav/collection-response-with-trailing-slash",
|
||||
handler: function(req,res,next) {
|
||||
if (req.method == "PROPFIND") {
|
||||
res.statusCode = 207;
|
||||
res.write('\<?xml version="1.0" encoding="utf-8" ?>\
|
||||
<multistatus xmlns="DAV:">\
|
||||
<response>\
|
||||
<href>/dav/collection-response-with-trailing-slash/</href> \
|
||||
<propstat>\
|
||||
<prop>\
|
||||
<current-user-principal>\
|
||||
<href>/principals/ok</href>\
|
||||
</current-user-principal>\
|
||||
<resourcetype>\
|
||||
<collection/>\
|
||||
</resourcetype>\
|
||||
</prop>\
|
||||
<status>HTTP/1.1 200 OK</status>\
|
||||
</propstat>\
|
||||
</response>\
|
||||
</multistatus>\
|
||||
');
|
||||
}
|
||||
}
|
||||
}),
|
||||
new RoboHydraHeadDAV({
|
||||
path: "/dav/collection-response-without-trailing-slash",
|
||||
handler: function(req,res,next) {
|
||||
if (req.method == "PROPFIND") {
|
||||
res.statusCode = 207;
|
||||
res.write('\<?xml version="1.0" encoding="utf-8" ?>\
|
||||
<multistatus xmlns="DAV:">\
|
||||
<response>\
|
||||
<href>/dav/collection-response-without-trailing-slash</href> \
|
||||
<propstat>\
|
||||
<prop>\
|
||||
<current-user-principal>\
|
||||
<href>/principals/ok</href>\
|
||||
</current-user-principal>\
|
||||
<resourcetype>\
|
||||
<collection/>\
|
||||
</resourcetype>\
|
||||
</prop>\
|
||||
<status>HTTP/1.1 200 OK</status>\
|
||||
</propstat>\
|
||||
</response>\
|
||||
</multistatus>\
|
||||
');
|
||||
}
|
||||
}
|
||||
}),
|
||||
new RoboHydraHeadDAV({
|
||||
path: "/dav/propfind-collection-properties",
|
||||
handler: function(req,res,next) {
|
||||
if (req.method == "PROPFIND") {
|
||||
res.statusCode = 207;
|
||||
res.write('\<?xml version="1.0" encoding="utf-8" ?>\
|
||||
<multistatus xmlns="DAV:" xmlns:CARD="urn:ietf:params:xml:ns:carddav">\
|
||||
<response>\
|
||||
<href>/dav/propfind-collection-properties</href> \
|
||||
<propstat>\
|
||||
<prop>\
|
||||
<resourcetype>\
|
||||
<collection/>\
|
||||
<CARD:addressbook/>\
|
||||
</resourcetype>\
|
||||
<CARD:supported-address-data>\
|
||||
<address-data-type content-type="text/vcard" version="4.0"/>\
|
||||
</CARD:supported-address-data>\
|
||||
</prop>\
|
||||
<status>HTTP/1.1 200 OK</status>\
|
||||
</propstat>\
|
||||
<propstat>\
|
||||
<prop>\
|
||||
<displayname/>\
|
||||
<A:calendar-color xmlns:A="http://apple.com/ns/ical/">0xFF00FF</A:calendar-color>\
|
||||
</prop>\
|
||||
<status>HTTP/1.1 404 Not Found</status>\
|
||||
</propstat>\
|
||||
</response>\
|
||||
</multistatus>\
|
||||
');
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
||||
/* principal URL */
|
||||
new RoboHydraHeadDAV({
|
||||
path: "/dav/principals/users/test",
|
||||
handler: function(req,res,next) {
|
||||
if (req.method == "PROPFIND" && req.rawBody.toString().match(/home-?set/)) {
|
||||
res.statusCode = 207;
|
||||
res.write('\<?xml version="1.0" encoding="utf-8" ?>\
|
||||
<multistatus xmlns="DAV:">\
|
||||
<response>\
|
||||
<href>/dav/principals/users/t%65st</href> \
|
||||
<propstat>\
|
||||
<prop>\
|
||||
<CARD:addressbook-home-set xmlns:CARD="urn:ietf:params:xml:ns:carddav">\
|
||||
<href>/dav/addressbooks/test</href>\
|
||||
</CARD:addressbook-home-set>\
|
||||
<CAL:calendar-home-set xmlns:CAL="urn:ietf:params:xml:ns:caldav">\
|
||||
<href>/dav/calendars/test/</href>\
|
||||
</CAL:calendar-home-set>\
|
||||
</prop>\
|
||||
<status>HTTP/1.1 200 OK</status>\
|
||||
</propstat>\
|
||||
</response>\
|
||||
</multistatus>\
|
||||
');
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
||||
/* address-book home set */
|
||||
new RoboHydraHeadDAV({
|
||||
path: "/dav/addressbooks/test/",
|
||||
handler: function(req,res,next) {
|
||||
if (!req.url.match(/\/$/)) {
|
||||
res.statusCode = 302;
|
||||
res.headers['location'] = "/dav/addressbooks/test/";
|
||||
}
|
||||
else if (req.method == "PROPFIND" && req.rawBody.toString().match(/addressbook-description/)) {
|
||||
res.statusCode = 207;
|
||||
res.write('\<?xml version="1.0" encoding="utf-8" ?>\
|
||||
<multistatus xmlns="DAV:">\
|
||||
<response>\
|
||||
<href>/dav/addressbooks/test/useless-member</href>\
|
||||
<propstat>\
|
||||
<prop>\
|
||||
<resourcetype/>\
|
||||
</prop>\
|
||||
<status>HTTP/1.1 200 OK</status>\
|
||||
</propstat>\
|
||||
</response>\
|
||||
<response>\
|
||||
<href>/dav/addressbooks/test/default.vcf</href>\
|
||||
<propstat>\
|
||||
<prop xmlns:CARD="urn:ietf:params:xml:ns:carddav">\
|
||||
<resourcetype>\
|
||||
<collection/>\
|
||||
<CARD:addressbook/>\
|
||||
</resourcetype>\
|
||||
<CARD:addressbook-description>Default Address Book</CARD:addressbook-description>\
|
||||
</prop>\
|
||||
<status>HTTP/1.1 200 OK</status>\
|
||||
</propstat>\
|
||||
</response>\
|
||||
<response>\
|
||||
<href>https://my.server/absolute:uri/my-address-book</href>\
|
||||
<propstat>\
|
||||
<prop xmlns:CARD="urn:ietf:params:xml:ns:carddav">\
|
||||
<resourcetype>\
|
||||
<collection/>\
|
||||
<CARD:addressbook/>\
|
||||
</resourcetype>\
|
||||
<CARD:addressbook-description>Absolute URI VCard Book</CARD:addressbook-description>\
|
||||
</prop>\
|
||||
<status>HTTP/1.1 200 OK</status>\
|
||||
</propstat>\
|
||||
</response>\
|
||||
</multistatus>\
|
||||
');
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
||||
/* calendar home set */
|
||||
new RoboHydraHeadDAV({
|
||||
path: "/dav/calendars/test/",
|
||||
handler: function(req,res,next) {
|
||||
if (req.method == "PROPFIND" && req.rawBody.toString().match(/calendar-description/)) {
|
||||
res.statusCode = 207;
|
||||
res.write('\<?xml version="1.0" encoding="utf-8" ?>\
|
||||
<multistatus xmlns="DAV:" xmlns:CAL="urn:ietf:params:xml:ns:caldav">\
|
||||
<response>\
|
||||
<href>/dav/calendars/test/shared.forbidden</href>\
|
||||
<propstat>\
|
||||
<prop>\
|
||||
<resourcetype/>\
|
||||
</prop>\
|
||||
<status>HTTP/1.1 403 Forbidden</status>\
|
||||
</propstat>\
|
||||
</response>\
|
||||
<response>\
|
||||
<href>/dav/calendars/test/private.ics</href>\
|
||||
<propstat>\
|
||||
<prop>\
|
||||
<resourcetype>\
|
||||
<collection/>\
|
||||
<CAL:calendar/>\
|
||||
</resourcetype>\
|
||||
<displayname>Private Calendar</displayname>\
|
||||
<CAL:calendar-description>This is my private calendar.</CAL:calendar-description>\
|
||||
</prop>\
|
||||
<status>HTTP/1.1 200 OK</status>\
|
||||
</propstat>\
|
||||
</response>\
|
||||
<response>\
|
||||
<href>/dav/calendars/test/work.ics</href>\
|
||||
<propstat>\
|
||||
<prop>\
|
||||
<resourcetype>\
|
||||
<collection/>\
|
||||
<CAL:calendar/>\
|
||||
</resourcetype>\
|
||||
<current-user-privilege-set>\
|
||||
<privilege><read/></privilege>\
|
||||
</current-user-privilege-set>\
|
||||
<displayname>Work Calendar</displayname>\
|
||||
<A:calendar-color xmlns:A="http://apple.com/ns/ical/">0xFF00FF</A:calendar-color>\
|
||||
</prop>\
|
||||
<status>HTTP/1.1 200 OK</status>\
|
||||
</propstat>\
|
||||
</response>\
|
||||
</multistatus>\
|
||||
');
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
||||
/* non-existing member */
|
||||
new RoboHydraHeadDAV({
|
||||
path: "/dav/collection/new.file",
|
||||
handler: function(req,res,next) {
|
||||
if (req.method == "PUT") {
|
||||
if (req.headers['if-match']) /* can't overwrite new file */
|
||||
res.statusCode = 412;
|
||||
else {
|
||||
res.statusCode = 201;
|
||||
res.headers["ETag"] = "has-just-been-created";
|
||||
}
|
||||
|
||||
} else if (req.method == "DELETE")
|
||||
res.statusCode = 404;
|
||||
}
|
||||
}),
|
||||
|
||||
/* existing member */
|
||||
new RoboHydraHeadDAV({
|
||||
path: "/dav/collection/existing.file",
|
||||
handler: function(req,res,next) {
|
||||
if (req.method == "PUT") {
|
||||
if (req.headers['if-none-match']) /* requested "don't overwrite", but this file exists */
|
||||
res.statusCode = 412;
|
||||
else if (req.headers['if-match'] && req.queryParams && req.queryParams.conflict)
|
||||
/* requested "don't overwrite", but this file exists with newer content */
|
||||
res.statusCode = 409;
|
||||
else {
|
||||
res.statusCode = 204;
|
||||
res.headers["ETag"] = "has-just-been-updated";
|
||||
}
|
||||
|
||||
} else if (req.method == "DELETE")
|
||||
res.statusCode = 204;
|
||||
}
|
||||
}),
|
||||
|
||||
/* existing member with encoded URL as file name */
|
||||
new RoboHydraHeadDAV({
|
||||
path: "/dav/http%253A%252F%252Fwww.invalid.example%252Fm8%252Ffeeds%252Fcontacts%252Fmaria.mueller%252540gmail.com%252Fbase%252F5528abc5720cecc.vcf",
|
||||
handler: function(req,res,next) {
|
||||
if (req.method == "GET")
|
||||
res.statusCode = 200;
|
||||
}
|
||||
}),
|
||||
|
||||
/* address-book multiget */
|
||||
new RoboHydraHeadDAV({
|
||||
path: "/dav/addressbooks/default.vcf/",
|
||||
handler: function(req,res,next) {
|
||||
if (req.method == "REPORT" && req.rawBody.toString().match(/addressbook-multiget[\s\S]+<prop>[\s\S]+<href>/m &&
|
||||
req.rawBody.toString().match(/<href>\/dav\/addressbooks\/default\.vcf\/2:3@my%2540pc\.vcf<\/href>/m))) {
|
||||
res.statusCode = 207;
|
||||
res.write('\<?xml version="1.0" encoding="utf-8" ?>\
|
||||
<multistatus xmlns="DAV:" xmlns:CARD="urn:ietf:params:xml:ns:carddav">\
|
||||
<response>\
|
||||
<href>/dav/addressbooks/default.vcf</href>\
|
||||
<propstat>\
|
||||
<prop>\
|
||||
<resourcetype><collection/></resourcetype>\
|
||||
<displayname>My Book</displayname>\
|
||||
</prop>\
|
||||
<status>HTTP/1.1 200 OK</status>\
|
||||
</propstat>\
|
||||
</response>\
|
||||
<response>\
|
||||
<href>/dav/addressbooks/default.vcf/1.vcf</href>\
|
||||
<propstat>\
|
||||
<prop>\
|
||||
<getetag/>\
|
||||
<CARD:address-data>BEGIN:VCARD\
|
||||
VERSION:3.0\
|
||||
NICKNAME:MULTIGET1\
|
||||
UID:1\
|
||||
END:VCARD\
|
||||
</CARD:address-data>\
|
||||
</prop>\
|
||||
<status>HTTP/1.1 200 OK</status>\
|
||||
</propstat>\
|
||||
</response>\
|
||||
<response>\
|
||||
<href>/dav/addressbooks/default.vcf/2:3%40my%2540pc.vcf</href>\
|
||||
<propstat>\
|
||||
<prop>\
|
||||
<getetag/>\
|
||||
<CARD:address-data>BEGIN:VCARD\
|
||||
VERSION:3.0\
|
||||
NICKNAME:MULTIGET2\
|
||||
UID:2\
|
||||
END:VCARD\
|
||||
</CARD:address-data>\
|
||||
</prop>\
|
||||
<status>HTTP/1.1 200 OK</status>\
|
||||
</propstat>\
|
||||
</response>\
|
||||
</multistatus>\
|
||||
');
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
||||
/* address-book multiget where one resource has 404 Not Found */
|
||||
new RoboHydraHeadDAV({
|
||||
path: "/dav/addressbooks/default-with-404.vcf/",
|
||||
handler: function(req,res,next) {
|
||||
if (req.method == "REPORT" && req.rawBody.toString().match(/addressbook-multiget[\s\S]+<prop>[\s\S]+<href>/m &&
|
||||
req.rawBody.toString().match(/<href>\/dav\/addressbooks\/default-with-404\.vcf\/notexisting<\/href>/m))) {
|
||||
res.statusCode = 207;
|
||||
res.write('\<?xml version="1.0" encoding="utf-8" ?>\
|
||||
<multistatus xmlns="DAV:" xmlns:CARD="urn:ietf:params:xml:ns:carddav">\
|
||||
<response>\
|
||||
<href>/dav/addressbooks/default-with-404.vcf</href>\
|
||||
<propstat>\
|
||||
<prop>\
|
||||
<resourcetype><collection/></resourcetype>\
|
||||
</prop>\
|
||||
<status>HTTP/1.1 200 OK</status>\
|
||||
</propstat>\
|
||||
</response>\
|
||||
<response>\
|
||||
<href>/dav/addressbooks/default-with-404.vcf/notexisting</href>\
|
||||
<status>HTTP/1.1 404 Not Found</status>\
|
||||
</response>\
|
||||
</multistatus>\
|
||||
');
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
||||
|
||||
]
|
||||
};
|
||||
};
|
||||
@@ -1,59 +0,0 @@
|
||||
// vim: ts=4:sw=4
|
||||
|
||||
var roboHydra = require("robohydra"),
|
||||
roboHydraHeads = roboHydra.heads,
|
||||
roboHydraHead = roboHydraHeads.RoboHydraHead;
|
||||
|
||||
RoboHydraHeadDAV = roboHydraHeads.roboHydraHeadType({
|
||||
name: 'WebDAV Server',
|
||||
mandatoryProperties: [ 'path' ],
|
||||
optionalProperties: [ 'handler' ],
|
||||
|
||||
parentPropBuilder: function() {
|
||||
var myHandler = this.handler;
|
||||
return {
|
||||
path: this.path,
|
||||
handler: function(req,res,next) {
|
||||
// default DAV behavior
|
||||
res.headers['DAV'] = 'addressbook, calendar-access';
|
||||
res.statusCode = 500;
|
||||
|
||||
// verify Accept header
|
||||
var accept = req.headers['accept'];
|
||||
if (req.method == "GET" && (accept == undefined || !accept.match(/text\/(calendar|vcard|xml)/)) ||
|
||||
(req.method == "PROPFIND" || req.method == "REPORT") && (accept == undefined || accept != "text/xml"))
|
||||
res.statusCode = 406;
|
||||
|
||||
// DAV operations that work on all URLs
|
||||
else if (req.method == "OPTIONS") {
|
||||
res.statusCode = 204;
|
||||
res.headers['Allow'] = 'OPTIONS, PROPFIND, GET, PUT, DELETE, REPORT';
|
||||
|
||||
} else if (req.method == "PROPFIND" && req.rawBody.toString().match(/current-user-principal/)) {
|
||||
res.statusCode = 207;
|
||||
res.write('\<?xml version="1.0" encoding="utf-8" ?>\
|
||||
<multistatus xmlns="DAV:">\
|
||||
<response>\
|
||||
<href>' + req.url + '</href> \
|
||||
<propstat>\
|
||||
<prop>\
|
||||
<current-user-principal>\
|
||||
<href>/dav/principals/users/test</href>\
|
||||
</current-user-principal>\
|
||||
</prop>\
|
||||
<status>HTTP/1.1 200 OK</status>\
|
||||
</propstat>\
|
||||
</response>\
|
||||
</multistatus>\
|
||||
');
|
||||
|
||||
} else if (typeof myHandler != 'undefined')
|
||||
myHandler(req,res,next);
|
||||
|
||||
res.end();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = RoboHydraHeadDAV;
|
||||
@@ -1,57 +0,0 @@
|
||||
require('../simple');
|
||||
|
||||
var RoboHydraHead = require('robohydra').heads.RoboHydraHead;
|
||||
|
||||
exports.getBodyParts = function(conf) {
|
||||
return {
|
||||
heads: [
|
||||
// well-known URIs
|
||||
new SimpleResponseHead({
|
||||
path: '/.well-known/caldav',
|
||||
status: 302,
|
||||
headers: { Location: '/dav/' }
|
||||
}),
|
||||
new SimpleResponseHead({
|
||||
path: '/.well-known/carddav',
|
||||
status: 302,
|
||||
headers: { Location: '/dav/' }
|
||||
}),
|
||||
|
||||
// generic redirections
|
||||
new RoboHydraHead({
|
||||
path: '/redirect/301',
|
||||
handler: function(req,res,next) {
|
||||
res.statusCode = 301;
|
||||
var location = req.queryParams['to'] || '/assets/test.random';
|
||||
res.headers = {
|
||||
Location: location
|
||||
}
|
||||
res.end();
|
||||
}
|
||||
}),
|
||||
new RoboHydraHead({
|
||||
path: '/redirect/302',
|
||||
handler: function(req,res,next) {
|
||||
res.statusCode = 302;
|
||||
var location = req.queryParams['to'] || '/assets/test.random';
|
||||
res.headers = {
|
||||
Location: location
|
||||
}
|
||||
res.end();
|
||||
}
|
||||
}),
|
||||
|
||||
// special redirections
|
||||
new SimpleResponseHead({
|
||||
path: '/redirect/relative',
|
||||
status: 302,
|
||||
headers: { Location: '/new/location' }
|
||||
}),
|
||||
new SimpleResponseHead({
|
||||
path: '/redirect/without-location',
|
||||
status: 302
|
||||
})
|
||||
|
||||
]
|
||||
};
|
||||
};
|
||||
@@ -1,30 +0,0 @@
|
||||
// vim: ts=4:sw=4
|
||||
|
||||
var roboHydra = require("robohydra"),
|
||||
roboHydraHeads = roboHydra.heads,
|
||||
roboHydraHead = roboHydraHeads.RoboHydraHead;
|
||||
|
||||
SimpleResponseHead = roboHydraHeads.roboHydraHeadType({
|
||||
name: 'Simple HTTP Response',
|
||||
mandatoryProperties: [ 'path', 'status' ],
|
||||
optionalProperties: [ 'headers', 'body' ],
|
||||
|
||||
parentPropBuilder: function() {
|
||||
var head = this;
|
||||
return {
|
||||
path: this.path,
|
||||
handler: function(req,res,next) {
|
||||
res.statusCode = head.status;
|
||||
if (typeof head.headers != 'undefined')
|
||||
res.headers = head.headers;
|
||||
if (typeof head.body != 'undefined')
|
||||
res.write(head.body);
|
||||
else
|
||||
res.write();
|
||||
res.end();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = SimpleResponseHead;
|
||||
@@ -1,2 +0,0 @@
|
||||
#!/bin/sh
|
||||
node_modules/robohydra/bin/robohydra.js davdroid.conf -I plugins
|
||||
@@ -5,107 +5,197 @@
|
||||
~ 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
|
||||
-->
|
||||
-->
|
||||
<manifest package="at.bitfire.davdroid"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:installLocation="internalOnly">
|
||||
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="at.bitfire.davdroid"
|
||||
android:versionCode="69" android:versionName="0.8.2"
|
||||
android:installLocation="internalOnly">
|
||||
<!-- normal permissions -->
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.GET_ACCOUNTS"/>
|
||||
<uses-permission android:name="android.permission.MANAGE_ACCOUNTS"/>
|
||||
<uses-permission android:name="android.permission.READ_SYNC_SETTINGS"/>
|
||||
<uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS"/>
|
||||
|
||||
<uses-sdk
|
||||
android:minSdkVersion="14"
|
||||
android:targetSdkVersion="22" />
|
||||
<!-- legacy permissions -->
|
||||
<uses-permission
|
||||
android:name="android.permission.AUTHENTICATE_ACCOUNTS"
|
||||
android:maxSdkVersion="22"
|
||||
tools:ignore="UnusedAttribute"/>
|
||||
<!--
|
||||
for writing external log files; permission only required for SDK <= 18 because since then,
|
||||
writing to app-private directory doesn't require extra permissions
|
||||
-->
|
||||
<uses-permission
|
||||
android:name="android.permission.READ_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="18"
|
||||
tools:ignore="UnusedAttribute"/>
|
||||
<uses-permission
|
||||
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="18"
|
||||
tools:ignore="UnusedAttribute"/>
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS" />
|
||||
<uses-permission android:name="android.permission.GET_ACCOUNTS" />
|
||||
<uses-permission android:name="android.permission.READ_CONTACTS" />
|
||||
<uses-permission android:name="android.permission.WRITE_CONTACTS" />
|
||||
<uses-permission android:name="android.permission.READ_CALENDAR" />
|
||||
<uses-permission android:name="android.permission.WRITE_CALENDAR" />
|
||||
<uses-permission android:name="android.permission.READ_SYNC_SETTINGS" />
|
||||
<uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS" />
|
||||
<!-- other permissions -->
|
||||
<uses-permission android:name="android.permission.READ_CONTACTS"/>
|
||||
<!-- android.permission-group.CONTACTS -->
|
||||
<uses-permission android:name="android.permission.WRITE_CONTACTS"/>
|
||||
<uses-permission android:name="android.permission.READ_CALENDAR"/>
|
||||
<!-- android.permission-group.CALENDAR -->
|
||||
<uses-permission android:name="android.permission.WRITE_CALENDAR"/>
|
||||
|
||||
<uses-permission android:name="org.dmfs.permission.READ_TASKS" />
|
||||
<uses-permission android:name="org.dmfs.permission.WRITE_TASKS" />
|
||||
<!-- ical4android declares task access permissions -->
|
||||
|
||||
<application
|
||||
android:name=".App"
|
||||
android:allowBackup="true"
|
||||
android:fullBackupContent="false"
|
||||
android:icon="@drawable/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/AppTheme"
|
||||
android:process=":sync">
|
||||
tools:ignore="UnusedAttribute">
|
||||
|
||||
<receiver
|
||||
android:name=".App$ReinitLoggingReceiver"
|
||||
android:exported="false"
|
||||
android:process=":sync">
|
||||
<intent-filter>
|
||||
<action android:name="at.bitfire.davdroid.REINIT_LOGGER"/>
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
<receiver
|
||||
android:name=".AccountSettings$AppUpdatedReceiver"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.PACKAGE_REPLACED" />
|
||||
<data android:scheme="package" android:path="at.bitfire.davdroid" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<service
|
||||
android:name=".syncadapter.AccountAuthenticatorService"
|
||||
android:exported="false" >
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="android.accounts.AccountAuthenticator" />
|
||||
<action android:name="android.accounts.AccountAuthenticator"/>
|
||||
</intent-filter>
|
||||
|
||||
<meta-data
|
||||
android:name="android.accounts.AccountAuthenticator"
|
||||
android:resource="@xml/account_authenticator" />
|
||||
android:resource="@xml/account_authenticator"/>
|
||||
</service>
|
||||
<service
|
||||
android:name=".syncadapter.ContactsSyncAdapterService"
|
||||
android:exported="true" >
|
||||
android:exported="true"
|
||||
android:process=":sync"
|
||||
tools:ignore="ExportedService">
|
||||
<intent-filter>
|
||||
<action android:name="android.content.SyncAdapter" />
|
||||
<action android:name="android.content.SyncAdapter"/>
|
||||
</intent-filter>
|
||||
|
||||
<meta-data
|
||||
android:name="android.content.SyncAdapter"
|
||||
android:resource="@xml/sync_contacts" />
|
||||
android:resource="@xml/sync_contacts"/>
|
||||
<meta-data
|
||||
android:name="android.provider.CONTACTS_STRUCTURE"
|
||||
android:resource="@xml/contacts" />
|
||||
android:resource="@xml/contacts"/>
|
||||
</service>
|
||||
<service
|
||||
android:name=".syncadapter.CalendarsSyncAdapterService"
|
||||
android:exported="true" >
|
||||
android:exported="true"
|
||||
android:process=":sync"
|
||||
tools:ignore="ExportedService">
|
||||
<intent-filter>
|
||||
<action android:name="android.content.SyncAdapter" />
|
||||
<action android:name="android.content.SyncAdapter"/>
|
||||
</intent-filter>
|
||||
|
||||
<meta-data
|
||||
android:name="android.content.SyncAdapter"
|
||||
android:resource="@xml/sync_calendars" />
|
||||
android:resource="@xml/sync_calendars"/>
|
||||
</service>
|
||||
<service
|
||||
android:name=".syncadapter.TasksSyncAdapterService"
|
||||
android:exported="true" >
|
||||
android:exported="true"
|
||||
android:process=":sync"
|
||||
tools:ignore="ExportedService">
|
||||
<intent-filter>
|
||||
<action android:name="android.content.SyncAdapter" />
|
||||
<action android:name="android.content.SyncAdapter"/>
|
||||
</intent-filter>
|
||||
|
||||
<meta-data
|
||||
android:name="android.content.SyncAdapter"
|
||||
android:resource="@xml/sync_tasks" />
|
||||
android:resource="@xml/sync_tasks"/>
|
||||
</service>
|
||||
|
||||
<activity
|
||||
android:name=".ui.MainActivity"
|
||||
android:label="@string/app_name" >
|
||||
<service
|
||||
android:name=".DavService"
|
||||
android:enabled="true">
|
||||
</service>
|
||||
<receiver
|
||||
android:name=".AccountsChangedReceiver"
|
||||
android:enabled="true"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
<action android:name="android.accounts.LOGIN_ACCOUNTS_CHANGED"/>
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<activity
|
||||
android:name=".ui.AccountsActivity"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/AppTheme.NoActionBar">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".ui.setup.AddAccountActivity"
|
||||
android:excludeFromRecents="true" >
|
||||
</activity>
|
||||
android:name=".ui.AboutActivity"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/AppTheme.NoActionBar"
|
||||
android:parentActivityName=".ui.AccountsActivity"/>
|
||||
|
||||
<activity
|
||||
android:name=".ui.settings.SettingsActivity"
|
||||
android:label="@string/settings_title" >
|
||||
android:name=".ui.AppSettingsActivity"
|
||||
android:label="@string/app_settings"
|
||||
android:parentActivityName=".ui.AccountsActivity"/>
|
||||
|
||||
<activity android:name=".ui.PermissionsActivity"
|
||||
android:label="@string/permissions_title"
|
||||
android:parentActivityName=".ui.AccountsActivity"/>
|
||||
|
||||
<activity
|
||||
android:name=".ui.setup.LoginActivity"
|
||||
android:label="@string/login_title"
|
||||
android:parentActivityName=".ui.AccountsActivity">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MANAGE_NETWORK_USAGE" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".ui.settings.AccountActivity"
|
||||
android:label="@string/settings_title"
|
||||
android:parentActivityName=".ui.settings.SettingsActivity" >
|
||||
android:name=".ui.AccountActivity"
|
||||
android:parentActivityName=".ui.AccountsActivity">
|
||||
</activity>
|
||||
<activity android:name=".ui.AccountSettingsActivity"/>
|
||||
<activity android:name=".ui.CreateAddressBookActivity"
|
||||
android:label="@string/create_addressbook"/>
|
||||
<activity android:name=".ui.CreateCalendarActivity"
|
||||
android:label="@string/create_calendar"/>
|
||||
|
||||
<activity
|
||||
android:name=".ui.DebugInfoActivity"
|
||||
android:exported="true"
|
||||
android:label="@string/debug_info_title">
|
||||
</activity>
|
||||
|
||||
<!-- MemorizingTrustManager -->
|
||||
<activity
|
||||
android:name="de.duenndns.ssl.MemorizingActivity"
|
||||
android:theme="@android:style/Theme.Holo.Light.Dialog.NoActionBar"/>
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
||||
153
app/src/main/assets/apache2.html
Normal file
153
app/src/main/assets/apache2.html
Normal file
@@ -0,0 +1,153 @@
|
||||
<h3>Apache License, Version 2.0, January 2004</h3>
|
||||
<p><a href="http://www.apache.org/licenses/">http://www.apache.org/licenses/</a> </p>
|
||||
<p>TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION</p>
|
||||
<p><strong><a name="definitions">1. Definitions</a></strong>.</p>
|
||||
<p>"License" shall mean the terms and conditions for use, reproduction, and
|
||||
distribution as defined by Sections 1 through 9 of this document.</p>
|
||||
<p>"Licensor" shall mean the copyright owner or entity authorized by the
|
||||
copyright owner that is granting the License.</p>
|
||||
<p>"Legal Entity" shall mean the union of the acting entity and all other
|
||||
entities that control, are controlled by, or are under common control with
|
||||
that entity. For the purposes of this definition, "control" means (i) the
|
||||
power, direct or indirect, to cause the direction or management of such
|
||||
entity, whether by contract or otherwise, or (ii) ownership of fifty
|
||||
percent (50%) or more of the outstanding shares, or (iii) beneficial
|
||||
ownership of such entity.</p>
|
||||
<p>"You" (or "Your") shall mean an individual or Legal Entity exercising
|
||||
permissions granted by this License.</p>
|
||||
<p>"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation source,
|
||||
and configuration files.</p>
|
||||
<p>"Object" form shall mean any form resulting from mechanical transformation
|
||||
or translation of a Source form, including but not limited to compiled
|
||||
object code, generated documentation, and conversions to other media types.</p>
|
||||
<p>"Work" shall mean the work of authorship, whether in Source or Object form,
|
||||
made available under the License, as indicated by a copyright notice that
|
||||
is included in or attached to the work (an example is provided in the
|
||||
Appendix below).</p>
|
||||
<p>"Derivative Works" shall mean any work, whether in Source or Object form,
|
||||
that is based on (or derived from) the Work and for which the editorial
|
||||
revisions, annotations, elaborations, or other modifications represent, as
|
||||
a whole, an original work of authorship. For the purposes of this License,
|
||||
Derivative Works shall not include works that remain separable from, or
|
||||
merely link (or bind by name) to the interfaces of, the Work and Derivative
|
||||
Works thereof.</p>
|
||||
<p>"Contribution" shall mean any work of authorship, including the original
|
||||
version of the Work and any modifications or additions to that Work or
|
||||
Derivative Works thereof, that is intentionally submitted to Licensor for
|
||||
inclusion in the Work by the copyright owner or by an individual or Legal
|
||||
Entity authorized to submit on behalf of the copyright owner. For the
|
||||
purposes of this definition, "submitted" means any form of electronic,
|
||||
verbal, or written communication sent to the Licensor or its
|
||||
representatives, including but not limited to communication on electronic
|
||||
mailing lists, source code control systems, and issue tracking systems that
|
||||
are managed by, or on behalf of, the Licensor for the purpose of discussing
|
||||
and improving the Work, but excluding communication that is conspicuously
|
||||
marked or otherwise designated in writing by the copyright owner as "Not a
|
||||
Contribution."</p>
|
||||
<p>"Contributor" shall mean Licensor and any individual or Legal Entity on
|
||||
behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.</p>
|
||||
<p><strong><a name="copyright">2. Grant of Copyright License</a></strong>. Subject to the
|
||||
terms and conditions of this License, each Contributor hereby grants to You
|
||||
a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of, publicly
|
||||
display, publicly perform, sublicense, and distribute the Work and such
|
||||
Derivative Works in Source or Object form.</p>
|
||||
<p><strong><a name="patent">3. Grant of Patent License</a></strong>. Subject to the terms
|
||||
and conditions of this License, each Contributor hereby grants to You a
|
||||
perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made, use,
|
||||
offer to sell, sell, import, and otherwise transfer the Work, where such
|
||||
license applies only to those patent claims licensable by such Contributor
|
||||
that are necessarily infringed by their Contribution(s) alone or by
|
||||
combination of their Contribution(s) with the Work to which such
|
||||
Contribution(s) was submitted. If You institute patent litigation against
|
||||
any entity (including a cross-claim or counterclaim in a lawsuit) alleging
|
||||
that the Work or a Contribution incorporated within the Work constitutes
|
||||
direct or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate as of the
|
||||
date such litigation is filed.</p>
|
||||
<p><strong><a name="redistribution">4. Redistribution</a></strong>. You may reproduce and
|
||||
distribute copies of the Work or Derivative Works thereof in any medium,
|
||||
with or without modifications, and in Source or Object form, provided that
|
||||
You meet the following conditions:</p>
|
||||
|
||||
<p>a. You must give any other recipients of the Work or Derivative Works a
|
||||
copy of this License; and</p>
|
||||
|
||||
<p>b. You must cause any modified files to carry prominent notices stating
|
||||
that You changed the files; and</p>
|
||||
|
||||
<p>c. You must retain, in the Source form of any Derivative Works that You
|
||||
distribute, all copyright, patent, trademark, and attribution notices from
|
||||
the Source form of the Work, excluding those notices that do not pertain to
|
||||
any part of the Derivative Works; and</p>
|
||||
|
||||
<p>d. If the Work includes a "NOTICE" text file as part of its distribution,
|
||||
then any Derivative Works that You distribute must include a readable copy
|
||||
of the attribution notices contained within such NOTICE file, excluding
|
||||
those notices that do not pertain to any part of the Derivative Works, in
|
||||
at least one of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or documentation,
|
||||
if provided along with the Derivative Works; or, within a display generated
|
||||
by the Derivative Works, if and wherever such third-party notices normally
|
||||
appear. The contents of the NOTICE file are for informational purposes only
|
||||
and do not modify the License. You may add Your own attribution notices
|
||||
within Derivative Works that You distribute, alongside or as an addendum to
|
||||
the NOTICE text from the Work, provided that such additional attribution
|
||||
notices cannot be construed as modifying the License.
|
||||
<br/>
|
||||
<br/>
|
||||
You may add Your own copyright statement to Your modifications and may
|
||||
provide additional or different license terms and conditions for use,
|
||||
reproduction, or distribution of Your modifications, or for any such
|
||||
Derivative Works as a whole, provided Your use, reproduction, and
|
||||
distribution of the Work otherwise complies with the conditions stated in
|
||||
this License.
|
||||
</p>
|
||||
|
||||
<p><strong><a name="contributions">5. Submission of Contributions</a></strong>. Unless You
|
||||
explicitly state otherwise, any Contribution intentionally submitted for
|
||||
inclusion in the Work by You to the Licensor shall be under the terms and
|
||||
conditions of this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify the
|
||||
terms of any separate license agreement you may have executed with Licensor
|
||||
regarding such Contributions.</p>
|
||||
<p><strong><a name="trademarks">6. Trademarks</a></strong>. This License does not grant
|
||||
permission to use the trade names, trademarks, service marks, or product
|
||||
names of the Licensor, except as required for reasonable and customary use
|
||||
in describing the origin of the Work and reproducing the content of the
|
||||
NOTICE file.</p>
|
||||
<p><strong><a name="no-warranty">7. Disclaimer of Warranty</a></strong>. Unless required by
|
||||
applicable law or agreed to in writing, Licensor provides the Work (and
|
||||
each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT
|
||||
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including,
|
||||
without limitation, any warranties or conditions of TITLE,
|
||||
NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You
|
||||
are solely responsible for determining the appropriateness of using or
|
||||
redistributing the Work and assume any risks associated with Your exercise
|
||||
of permissions under this License.</p>
|
||||
<p><strong><a name="no-liability">8. Limitation of Liability</a></strong>. In no event and
|
||||
under no legal theory, whether in tort (including negligence), contract, or
|
||||
otherwise, unless required by applicable law (such as deliberate and
|
||||
grossly negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a result
|
||||
of this License or out of the use or inability to use the Work (including
|
||||
but not limited to damages for loss of goodwill, work stoppage, computer
|
||||
failure or malfunction, or any and all other commercial damages or losses),
|
||||
even if such Contributor has been advised of the possibility of such
|
||||
damages.</p>
|
||||
<p><strong><a name="additional">9. Accepting Warranty or Additional Liability</a></strong>.
|
||||
While redistributing the Work or Derivative Works thereof, You may choose
|
||||
to offer, and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this License.
|
||||
However, in accepting such obligations, You may act only on Your own behalf
|
||||
and on Your sole responsibility, not on behalf of any other Contributor,
|
||||
and only if You agree to indemnify, defend, and hold each Contributor
|
||||
harmless for any liability incurred by, or claims asserted against, such
|
||||
Contributor by reason of your accepting any such warranty or additional
|
||||
liability.</p>
|
||||
|
||||
<p>END OF TERMS AND CONDITIONS</p>
|
||||
28
app/src/main/assets/bsd-3clause.html
Normal file
28
app/src/main/assets/bsd-3clause.html
Normal file
@@ -0,0 +1,28 @@
|
||||
<h3>BSD License (3-clause)</h3>
|
||||
|
||||
<p>Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions
|
||||
are met:</p>
|
||||
|
||||
<p>o Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.</p>
|
||||
|
||||
<p>o Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.</p>
|
||||
|
||||
<p>o Neither the name of Ben Fortuna nor the names of any other contributors
|
||||
may be used to endorse or promote products derived from this software
|
||||
without specific prior written permission.</p>
|
||||
|
||||
<p>THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
|
||||
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
||||
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
||||
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.</p>
|
||||
23
app/src/main/assets/bsd.html
Normal file
23
app/src/main/assets/bsd.html
Normal file
@@ -0,0 +1,23 @@
|
||||
<h3>BSD License</h3>
|
||||
|
||||
<p>Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:</p>
|
||||
|
||||
<p>1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.</p>
|
||||
|
||||
<p>2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.</p>
|
||||
|
||||
<p>THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
|
||||
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.</p>
|
||||
|
||||
628
app/src/main/assets/gpl-3.0-standalone.html
Normal file
628
app/src/main/assets/gpl-3.0-standalone.html
Normal file
@@ -0,0 +1,628 @@
|
||||
<h3 style="text-align: center;">GNU GENERAL PUBLIC LICENSE</h3>
|
||||
<p style="text-align: center;">Version 3, 29 June 2007</p>
|
||||
|
||||
<p>Copyright © 2007 Free Software Foundation, Inc.
|
||||
<<a href="http://fsf.org/">http://fsf.org/</a>></p><p>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.</p>
|
||||
|
||||
<h3><a name="preamble"></a>Preamble</h3>
|
||||
|
||||
<p>The GNU General Public License is a free, copyleft license for
|
||||
software and other kinds of works.</p>
|
||||
|
||||
<p>The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
the GNU General Public License is intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users. We, the Free Software Foundation, use the
|
||||
GNU General Public License for most of our software; it applies also to
|
||||
any other work released this way by its authors. You can apply it to
|
||||
your programs, too.</p>
|
||||
|
||||
<p>When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.</p>
|
||||
|
||||
<p>To protect your rights, we need to prevent others from denying you
|
||||
these rights or asking you to surrender the rights. Therefore, you have
|
||||
certain responsibilities if you distribute copies of the software, or if
|
||||
you modify it: responsibilities to respect the freedom of others.</p>
|
||||
|
||||
<p>For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must pass on to the recipients the same
|
||||
freedoms that you received. You must make sure that they, too, receive
|
||||
or can get the source code. And you must show them these terms so they
|
||||
know their rights.</p>
|
||||
|
||||
<p>Developers that use the GNU GPL protect your rights with two steps:
|
||||
(1) assert copyright on the software, and (2) offer you this License
|
||||
giving you legal permission to copy, distribute and/or modify it.</p>
|
||||
|
||||
<p>For the developers' and authors' protection, the GPL clearly explains
|
||||
that there is no warranty for this free software. For both users' and
|
||||
authors' sake, the GPL requires that modified versions be marked as
|
||||
changed, so that their problems will not be attributed erroneously to
|
||||
authors of previous versions.</p>
|
||||
|
||||
<p>Some devices are designed to deny users access to install or run
|
||||
modified versions of the software inside them, although the manufacturer
|
||||
can do so. This is fundamentally incompatible with the aim of
|
||||
protecting users' freedom to change the software. The systematic
|
||||
pattern of such abuse occurs in the area of products for individuals to
|
||||
use, which is precisely where it is most unacceptable. Therefore, we
|
||||
have designed this version of the GPL to prohibit the practice for those
|
||||
products. If such problems arise substantially in other domains, we
|
||||
stand ready to extend this provision to those domains in future versions
|
||||
of the GPL, as needed to protect the freedom of users.</p>
|
||||
|
||||
<p>Finally, every program is threatened constantly by software patents.
|
||||
States should not allow patents to restrict development and use of
|
||||
software on general-purpose computers, but in those that do, we wish to
|
||||
avoid the special danger that patents applied to a free program could
|
||||
make it effectively proprietary. To prevent this, the GPL assures that
|
||||
patents cannot be used to render the program non-free.</p>
|
||||
|
||||
<p>The precise terms and conditions for copying, distribution and
|
||||
modification follow.</p>
|
||||
|
||||
<h3><a name="terms"></a>TERMS AND CONDITIONS</h3>
|
||||
|
||||
<h4><a name="section0"></a>0. Definitions.</h4>
|
||||
|
||||
<p>“This License” refers to version 3 of the GNU General Public License.</p>
|
||||
|
||||
<p>“Copyright” also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.</p>
|
||||
|
||||
<p>“The Program” refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as “you”. “Licensees” and
|
||||
“recipients” may be individuals or organizations.</p>
|
||||
|
||||
<p>To “modify” a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a “modified version” of the
|
||||
earlier work or a work “based on” the earlier work.</p>
|
||||
|
||||
<p>A “covered work” means either the unmodified Program or a work based
|
||||
on the Program.</p>
|
||||
|
||||
<p>To “propagate” a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.</p>
|
||||
|
||||
<p>To “convey” a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.</p>
|
||||
|
||||
<p>An interactive user interface displays “Appropriate Legal Notices”
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.</p>
|
||||
|
||||
<h4><a name="section1"></a>1. Source Code.</h4>
|
||||
|
||||
<p>The “source code” for a work means the preferred form of the work
|
||||
for making modifications to it. “Object code” means any non-source
|
||||
form of a work.</p>
|
||||
|
||||
<p>A “Standard Interface” means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.</p>
|
||||
|
||||
<p>The “System Libraries” of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
“Major Component”, in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.</p>
|
||||
|
||||
<p>The “Corresponding Source” for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.</p>
|
||||
|
||||
<p>The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.</p>
|
||||
|
||||
<p>The Corresponding Source for a work in source code form is that
|
||||
same work.</p>
|
||||
|
||||
<h4><a name="section2"></a>2. Basic Permissions.</h4>
|
||||
|
||||
<p>All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.</p>
|
||||
|
||||
<p>You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.</p>
|
||||
|
||||
<p>Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.</p>
|
||||
|
||||
<h4><a name="section3"></a>3. Protecting Users' Legal Rights From Anti-Circumvention Law.</h4>
|
||||
|
||||
<p>No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.</p>
|
||||
|
||||
<p>When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.</p>
|
||||
|
||||
<h4><a name="section4"></a>4. Conveying Verbatim Copies.</h4>
|
||||
|
||||
<p>You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.</p>
|
||||
|
||||
<p>You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.</p>
|
||||
|
||||
<h4><a name="section5"></a>5. Conveying Modified Source Versions.</h4>
|
||||
|
||||
<p>You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:</p>
|
||||
|
||||
<ul>
|
||||
<li>a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.</li>
|
||||
|
||||
<li>b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
“keep intact all notices”.</li>
|
||||
|
||||
<li>c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.</li>
|
||||
|
||||
<li>d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.</li>
|
||||
</ul>
|
||||
|
||||
<p>A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
“aggregate” if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.</p>
|
||||
|
||||
<h4><a name="section6"></a>6. Conveying Non-Source Forms.</h4>
|
||||
|
||||
<p>You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:</p>
|
||||
|
||||
<ul>
|
||||
<li>a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.</li>
|
||||
|
||||
<li>b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.</li>
|
||||
|
||||
<li>c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.</li>
|
||||
|
||||
<li>d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.</li>
|
||||
|
||||
<li>e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.</li>
|
||||
</ul>
|
||||
|
||||
<p>A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.</p>
|
||||
|
||||
<p>A “User Product” is either (1) a “consumer product”, which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, “normally used” refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.</p>
|
||||
|
||||
<p>“Installation Information” for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.</p>
|
||||
|
||||
<p>If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).</p>
|
||||
|
||||
<p>The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.</p>
|
||||
|
||||
<p>Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.</p>
|
||||
|
||||
<h4><a name="section7"></a>7. Additional Terms.</h4>
|
||||
|
||||
<p>“Additional permissions” are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.</p>
|
||||
|
||||
<p>When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.</p>
|
||||
|
||||
<p>Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:</p>
|
||||
|
||||
<ul>
|
||||
<li>a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or</li>
|
||||
|
||||
<li>b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or</li>
|
||||
|
||||
<li>c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or</li>
|
||||
|
||||
<li>d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or</li>
|
||||
|
||||
<li>e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or</li>
|
||||
|
||||
<li>f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.</li>
|
||||
</ul>
|
||||
|
||||
<p>All other non-permissive additional terms are considered “further
|
||||
restrictions” within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.</p>
|
||||
|
||||
<p>If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.</p>
|
||||
|
||||
<p>Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.</p>
|
||||
|
||||
<h4><a name="section8"></a>8. Termination.</h4>
|
||||
|
||||
<p>You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).</p>
|
||||
|
||||
<p>However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.</p>
|
||||
|
||||
<p>Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.</p>
|
||||
|
||||
<p>Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.</p>
|
||||
|
||||
<h4><a name="section9"></a>9. Acceptance Not Required for Having Copies.</h4>
|
||||
|
||||
<p>You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.</p>
|
||||
|
||||
<h4><a name="section10"></a>10. Automatic Licensing of Downstream Recipients.</h4>
|
||||
|
||||
<p>Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.</p>
|
||||
|
||||
<p>An “entity transaction” is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.</p>
|
||||
|
||||
<p>You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.</p>
|
||||
|
||||
<h4><a name="section11"></a>11. Patents.</h4>
|
||||
|
||||
<p>A “contributor” is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's “contributor version”.</p>
|
||||
|
||||
<p>A contributor's “essential patent claims” are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, “control” includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.</p>
|
||||
|
||||
<p>Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.</p>
|
||||
|
||||
<p>In the following three paragraphs, a “patent license” is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To “grant” such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.</p>
|
||||
|
||||
<p>If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. “Knowingly relying” means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.</p>
|
||||
|
||||
<p>If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.</p>
|
||||
|
||||
<p>A patent license is “discriminatory” if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.</p>
|
||||
|
||||
<p>Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.</p>
|
||||
|
||||
<h4><a name="section12"></a>12. No Surrender of Others' Freedom.</h4>
|
||||
|
||||
<p>If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.</p>
|
||||
|
||||
<h4><a name="section13"></a>13. Use with the GNU Affero General Public License.</h4>
|
||||
|
||||
<p>Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU Affero General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the special requirements of the GNU Affero General Public License,
|
||||
section 13, concerning interaction through a network will apply to the
|
||||
combination as such.</p>
|
||||
|
||||
<h4><a name="section14"></a>14. Revised Versions of this License.</h4>
|
||||
|
||||
<p>The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.</p>
|
||||
|
||||
<p>Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU General
|
||||
Public License “or any later version” applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.</p>
|
||||
|
||||
<p>If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.</p>
|
||||
|
||||
<p>Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.</p>
|
||||
|
||||
<h4><a name="section15"></a>15. Disclaimer of Warranty.</h4>
|
||||
|
||||
<p>THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM “AS IS” WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.</p>
|
||||
|
||||
<h4><a name="section16"></a>16. Limitation of Liability.</h4>
|
||||
|
||||
<p>IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.</p>
|
||||
|
||||
<h4><a name="section17"></a>17. Interpretation of Sections 15 and 16.</h4>
|
||||
|
||||
<p>If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.</p>
|
||||
|
||||
<p>END OF TERMS AND CONDITIONS</p>
|
||||
7
app/src/main/assets/mit.html
Normal file
7
app/src/main/assets/mit.html
Normal file
@@ -0,0 +1,7 @@
|
||||
<h3>The MIT License (MIT)</h3>
|
||||
|
||||
<p>Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:</p>
|
||||
|
||||
<p>The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.</p>
|
||||
|
||||
<p>THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.</p>
|
||||
448
app/src/main/java/at/bitfire/davdroid/AccountSettings.java
Normal file
448
app/src/main/java/at/bitfire/davdroid/AccountSettings.java
Normal file
@@ -0,0 +1,448 @@
|
||||
/*
|
||||
* 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.app.Notification;
|
||||
import android.app.NotificationManager;
|
||||
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.graphics.drawable.BitmapDrawable;
|
||||
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.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_AUTH_PREEMPTIVE = "auth_preemptive",
|
||||
|
||||
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;
|
||||
|
||||
|
||||
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_new_releases_light)
|
||||
.setLargeIcon(((BitmapDrawable)context.getResources().getDrawable(R.drawable.ic_launcher)).getBitmap())
|
||||
.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)
|
||||
.setLocalOnly(true)
|
||||
.build();
|
||||
NotificationManager nm = (NotificationManager)context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
nm.notify(Constants.NOTIFICATION_ACCOUNT_SETTINGS_UPDATED, notify);
|
||||
|
||||
update(version);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static Bundle initialUserData(String userName, boolean preemptive) {
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putString(KEY_SETTINGS_VERSION, String.valueOf(CURRENT_VERSION));
|
||||
bundle.putString(KEY_USERNAME, userName);
|
||||
bundle.putString(KEY_AUTH_PREEMPTIVE, Boolean.toString(preemptive));
|
||||
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); }
|
||||
|
||||
public boolean preemptiveAuth() { return Boolean.parseBoolean(accountManager.getUserData(account, KEY_AUTH_PREEMPTIVE)); }
|
||||
public void preemptiveAuth(boolean preemptive) { accountManager.setUserData(account, KEY_AUTH_PREEMPTIVE, Boolean.toString(preemptive)); }
|
||||
|
||||
|
||||
// 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() {
|
||||
return accountManager.getUserData(account, KEY_VCARD_RFC6868) == null;
|
||||
}
|
||||
|
||||
public void setVCardRFC6868(boolean use) {
|
||||
accountManager.setUserData(account, KEY_VCARD_RFC6868, use ? null : "0");
|
||||
}
|
||||
|
||||
|
||||
// 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() {
|
||||
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) {
|
||||
final String name = method == GroupMethod.GROUP_VCARDS ? null : method.name();
|
||||
accountManager.setUserData(account, KEY_CONTACT_GROUP_METHOD, name);
|
||||
}
|
||||
|
||||
|
||||
// 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")
|
||||
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(Constants.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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* 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.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) {
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
143
app/src/main/java/at/bitfire/davdroid/App.java
Normal file
143
app/src/main/java/at/bitfire/davdroid/App.java
Normal file
@@ -0,0 +1,143 @@
|
||||
/*
|
||||
* 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.app.Application;
|
||||
import android.app.NotificationManager;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.graphics.drawable.BitmapDrawable;
|
||||
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.davdroid.log.LogcatHandler;
|
||||
import at.bitfire.davdroid.log.PlainTextFormatter;
|
||||
import at.bitfire.davdroid.model.ServiceDB;
|
||||
import at.bitfire.davdroid.model.Settings;
|
||||
import de.duenndns.ssl.MemorizingTrustManager;
|
||||
import lombok.Cleanup;
|
||||
import lombok.Getter;
|
||||
import okhttp3.internal.tls.OkHostnameVerifier;
|
||||
|
||||
public class App extends Application {
|
||||
public static final String LOG_TO_EXTERNAL_STORAGE = "logToExternalStorage";
|
||||
|
||||
@Getter
|
||||
private static MemorizingTrustManager memorizingTrustManager;
|
||||
|
||||
@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");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
|
||||
// initialize MemorizingTrustManager
|
||||
memorizingTrustManager = new MemorizingTrustManager(this);
|
||||
sslSocketFactoryCompat = new SSLSocketFactoryCompat(memorizingTrustManager);
|
||||
hostnameVerifier = memorizingTrustManager.wrapHostnameVerifier(OkHostnameVerifier.INSTANCE);
|
||||
|
||||
// initializer logger
|
||||
reinitLogger();
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
NotificationManager nm = (NotificationManager)getSystemService(NOTIFICATION_SERVICE);
|
||||
// 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(((BitmapDrawable)getResources().getDrawable(R.drawable.ic_launcher)).getBitmap())
|
||||
.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);
|
||||
}
|
||||
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -7,14 +7,23 @@
|
||||
*/
|
||||
package at.bitfire.davdroid;
|
||||
|
||||
import net.fortuna.ical4j.model.property.ProdId;
|
||||
import android.net.Uri;
|
||||
|
||||
public class Constants {
|
||||
public static final String
|
||||
APP_VERSION = "0.8.2",
|
||||
ACCOUNT_TYPE = "bitfire.at.davdroid",
|
||||
WEB_URL_HELP = "https://davdroid.bitfire.at/configuration?pk_campaign=davdroid-app",
|
||||
WEB_URL_VIEW_LOGS = "https://github.com/bitfireAT/davdroid/wiki/How-to-view-the-logs";
|
||||
|
||||
public static final ProdId ICAL_PRODID = new ProdId("-//bitfire web engineering//DAVdroid " + Constants.APP_VERSION + " (ical4j 2.0-alpha1)//EN");
|
||||
public static final String
|
||||
ACCOUNT_TYPE = "bitfire.at.davdroid";
|
||||
|
||||
// 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;
|
||||
|
||||
public static final Uri webUri = Uri.parse("https://davdroid.bitfire.at/?pk_campaign=davdroid-app");
|
||||
|
||||
}
|
||||
|
||||
@@ -1,37 +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.util.Log;
|
||||
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class DAVUtils {
|
||||
private static final String TAG = "davdroid.DAVutils";
|
||||
|
||||
public static final int calendarGreen = 0xFFC3EA6E;
|
||||
|
||||
|
||||
public static int CalDAVtoARGBColor(String davColor) {
|
||||
int color = calendarGreen; // fallback: "DAVdroid green"
|
||||
if (davColor != null) {
|
||||
Pattern p = Pattern.compile("#?(\\p{XDigit}{6})(\\p{XDigit}{2})?");
|
||||
Matcher m = p.matcher(davColor);
|
||||
if (m.find()) {
|
||||
int color_rgb = Integer.parseInt(m.group(1), 16);
|
||||
int color_alpha = m.group(2) != null ? (Integer.parseInt(m.group(2), 16) & 0xFF) : 0xFF;
|
||||
color = (color_alpha << 24) | color_rgb;
|
||||
} else
|
||||
Log.w(TAG, "Couldn't parse color " + davColor + ", using DAVdroid green");
|
||||
}
|
||||
return color;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,166 +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.util.Log;
|
||||
|
||||
import net.fortuna.ical4j.model.Date;
|
||||
import net.fortuna.ical4j.model.DateList;
|
||||
import net.fortuna.ical4j.model.DateTime;
|
||||
import net.fortuna.ical4j.model.TimeZone;
|
||||
import net.fortuna.ical4j.model.TimeZoneRegistry;
|
||||
import net.fortuna.ical4j.model.TimeZoneRegistryFactory;
|
||||
import net.fortuna.ical4j.model.parameter.Value;
|
||||
import net.fortuna.ical4j.model.property.DateListProperty;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import java.text.DateFormat;
|
||||
import java.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.SimpleTimeZone;
|
||||
|
||||
public class DateUtils {
|
||||
private final static String TAG = "davdroid.DateUtils";
|
||||
|
||||
public final static TimeZoneRegistry tzRegistry = TimeZoneRegistryFactory.getInstance().createRegistry();
|
||||
|
||||
static {
|
||||
// disable automatic time-zone updates (causes unwanted network traffic)
|
||||
System.setProperty("net.fortuna.ical4j.timezone.update.enabled", "false");
|
||||
}
|
||||
|
||||
|
||||
// time zones
|
||||
|
||||
public static String findAndroidTimezoneID(String tz) {
|
||||
String deviceTZ = null;
|
||||
String availableTZs[] = SimpleTimeZone.getAvailableIDs();
|
||||
|
||||
// first, try to find an exact match (case insensitive)
|
||||
for (String availableTZ : availableTZs)
|
||||
if (availableTZ.equalsIgnoreCase(tz)) {
|
||||
deviceTZ = availableTZ;
|
||||
break;
|
||||
}
|
||||
|
||||
// if that doesn't work, try to find something else that matches
|
||||
if (deviceTZ == null) {
|
||||
Log.w(TAG, "Coulnd't find time zone with matching identifiers, trying to guess");
|
||||
for (String availableTZ : availableTZs)
|
||||
if (StringUtils.indexOfIgnoreCase(tz, availableTZ) != -1) {
|
||||
deviceTZ = availableTZ;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// if that doesn't work, use UTC as fallback
|
||||
if (deviceTZ == null) {
|
||||
final String defaultTZ = TimeZone.getDefault().getID();
|
||||
Log.e(TAG, "Couldn't identify time zone, using system default (" + defaultTZ + ") as fallback");
|
||||
deviceTZ = defaultTZ;
|
||||
}
|
||||
|
||||
return deviceTZ;
|
||||
}
|
||||
|
||||
|
||||
// recurrence sets
|
||||
|
||||
/**
|
||||
* Concatenates, if necessary, multiple RDATE/EXDATE lists and converts them to
|
||||
* a formatted string which Android calendar provider can process.
|
||||
* Android expects this format: "[TZID;]date1,date2,date3" where date is "yyyymmddThhmmss" (when
|
||||
* TZID is given) or "yyyymmddThhmmssZ". We don't use the TZID format here because then we're limited
|
||||
* to one time-zone, while an iCalendar may contain multiple EXDATE/RDATE lines with different time zones.
|
||||
* @param dates one more more lists of RDATE or EXDATE
|
||||
* @param allDay indicates whether the event is an all-day event or not
|
||||
* @return formatted string for Android calendar provider:
|
||||
* - in case of all-day events, all dates/times are returned as yyyymmddT000000Z
|
||||
* - in case of timed events, all dates/times are returned as UTC time: yyyymmddThhmmssZ
|
||||
*/
|
||||
public static String recurrenceSetsToAndroidString(List<? extends DateListProperty> dates, boolean allDay) throws ParseException {
|
||||
List<String> strDates = new LinkedList<>();
|
||||
|
||||
/* rdate/exdate: DATE DATE_TIME
|
||||
all-day store as ...T000000Z cut off time and store as ...T000000Z
|
||||
event with time (ignored) store as ...ThhmmssZ
|
||||
*/
|
||||
final DateFormat dateFormatUtcMidnight = new SimpleDateFormat("yyyyMMdd'T'000000'Z'");
|
||||
|
||||
for (DateListProperty dateListProp : dates) {
|
||||
final Value type = dateListProp.getDates().getType();
|
||||
|
||||
if (Value.DATE_TIME.equals(type)) { // DATE-TIME values will be stored in UTC format for Android
|
||||
if (allDay) {
|
||||
DateList dateList = dateListProp.getDates();
|
||||
for (Date date : dateList)
|
||||
strDates.add(dateFormatUtcMidnight.format(date));
|
||||
} else {
|
||||
dateListProp.setUtc(true);
|
||||
strDates.add(dateListProp.getValue());
|
||||
}
|
||||
|
||||
} else if (Value.DATE.equals(type)) // DATE values have to be converted to DATE-TIME <date>T000000Z for Android
|
||||
for (Date date : dateListProp.getDates())
|
||||
strDates.add(dateFormatUtcMidnight.format(date));
|
||||
}
|
||||
return StringUtils.join(strDates, ",");
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a formatted string as provided by the Android calendar provider and returns a DateListProperty
|
||||
* constructed from these values.
|
||||
* @param dbStr formatted string from Android calendar provider (RDATE/EXDATE field)
|
||||
* expected format: "[TZID;]date1,date2,date3" where date is "yyyymmddThhmmss[Z]"
|
||||
* @param type subclass of DateListProperty, e.g. RDate or ExDate
|
||||
* @param allDay true: list will contain DATE values; false: list will contain DATE_TIME values
|
||||
* @return instance of "type" containing the parsed dates/times from the string
|
||||
*/
|
||||
public static DateListProperty androidStringToRecurrenceSet(String dbStr, Class<? extends DateListProperty> type, boolean allDay) throws ParseException {
|
||||
// 1. split string into time zone and actual dates
|
||||
TimeZone timeZone;
|
||||
String datesStr;
|
||||
final int limiter = dbStr.indexOf(';');
|
||||
if (limiter != -1) { // TZID given
|
||||
timeZone = DateUtils.tzRegistry.getTimeZone(dbStr.substring(0, limiter));
|
||||
datesStr = dbStr.substring(limiter + 1);
|
||||
} else {
|
||||
timeZone = null;
|
||||
datesStr = dbStr;
|
||||
}
|
||||
|
||||
// 2. process date string and generate list of DATEs or DATE-TIMEs
|
||||
DateList dateList;
|
||||
if (allDay) {
|
||||
dateList = new DateList(Value.DATE);
|
||||
for (String s: StringUtils.split(datesStr, ','))
|
||||
dateList.add(new Date(new DateTime(s)));
|
||||
} else {
|
||||
dateList = new DateList(datesStr, Value.DATE_TIME, timeZone);
|
||||
if (timeZone == null)
|
||||
dateList.setUtc(true);
|
||||
}
|
||||
|
||||
// 3. generate requested DateListProperty (RDate/ExDate) from list of DATEs or DATE-TIMEs
|
||||
DateListProperty list;
|
||||
try {
|
||||
list = (DateListProperty)type.getDeclaredConstructor(new Class[] { DateList.class } ).newInstance(dateList);
|
||||
if (dateList.getTimeZone() != null)
|
||||
list.setTimeZone(dateList.getTimeZone());
|
||||
} catch (Exception e) {
|
||||
throw new ParseException("Couldn't create date/time list by reflection", -1);
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
}
|
||||
427
app/src/main/java/at/bitfire/davdroid/DavService.java
Normal file
427
app/src/main/java/at/bitfire/davdroid/DavService.java
Normal file
@@ -0,0 +1,427 @@
|
||||
/*
|
||||
* 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.app.Notification;
|
||||
import android.app.NotificationManager;
|
||||
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.graphics.drawable.BitmapDrawable;
|
||||
import android.os.Binder;
|
||||
import android.os.IBinder;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
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
|
||||
*/
|
||||
|
||||
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(Constants.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);
|
||||
|
||||
NotificationManager nm = (NotificationManager)getSystemService(NOTIFICATION_SERVICE);
|
||||
Notification notify = new NotificationCompat.Builder(DavService.this)
|
||||
.setSmallIcon(R.drawable.ic_error_light)
|
||||
.setLargeIcon(((BitmapDrawable)getResources().getDrawable(R.drawable.ic_launcher)).getBitmap())
|
||||
.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), Constants.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
41
app/src/main/java/at/bitfire/davdroid/DavUtils.java
Normal file
41
app/src/main/java/at/bitfire/davdroid/DavUtils.java
Normal file
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* 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 "/";
|
||||
}
|
||||
|
||||
}
|
||||
157
app/src/main/java/at/bitfire/davdroid/HttpClient.java
Normal file
157
app/src/main/java/at/bitfire/davdroid/HttpClient.java
Normal file
@@ -0,0 +1,157 @@
|
||||
/*
|
||||
* 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 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.BasicDigestAuthenticator;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import okhttp3.Credentials;
|
||||
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 and logging
|
||||
AccountSettings settings = new AccountSettings(context, account);
|
||||
|
||||
if (settings.preemptiveAuth())
|
||||
builder.addNetworkInterceptor(new PreemptiveAuthenticationInterceptor(settings.username(), settings.password()));
|
||||
else
|
||||
builder.authenticator(new BasicDigestAuthenticator(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)
|
||||
builder.sslSocketFactory(App.getSslSocketFactoryCompat(), App.getMemorizingTrustManager());
|
||||
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(MemoryCookieStore.INSTANCE);
|
||||
|
||||
// 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, @NonNull String username, @NonNull String password, boolean preemptive) {
|
||||
if (preemptive)
|
||||
builder.addNetworkInterceptor(new PreemptiveAuthenticationInterceptor(username, password));
|
||||
else
|
||||
builder.authenticator(new BasicDigestAuthenticator(null, username, password));
|
||||
return builder;
|
||||
}
|
||||
|
||||
public static OkHttpClient addAuthentication(@NonNull OkHttpClient client, @NonNull String username, @NonNull String password, boolean preemptive) {
|
||||
OkHttpClient.Builder builder = client.newBuilder();
|
||||
addAuthentication(builder, username, password, preemptive);
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
public static OkHttpClient addAuthentication(@NonNull OkHttpClient client, @NonNull String host, @NonNull String username, @NonNull String password) {
|
||||
return client.newBuilder()
|
||||
.authenticator(new BasicDigestAuthenticator(host, username, password))
|
||||
.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);
|
||||
}
|
||||
}
|
||||
|
||||
@RequiredArgsConstructor
|
||||
static class PreemptiveAuthenticationInterceptor implements Interceptor {
|
||||
final String username, password;
|
||||
|
||||
@Override
|
||||
public Response intercept(Chain chain) throws IOException {
|
||||
App.log.fine("Adding basic authorization header for user " + username);
|
||||
Request request = chain.request().newBuilder()
|
||||
.header("Authorization", Credentials.basic(username, password))
|
||||
.build();
|
||||
return chain.proceed(request);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* 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);
|
||||
}
|
||||
|
||||
}
|
||||
46
app/src/main/java/at/bitfire/davdroid/MemoryCookieStore.java
Normal file
46
app/src/main/java/at/bitfire/davdroid/MemoryCookieStore.java
Normal file
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* 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 java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
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 {
|
||||
|
||||
public static final MemoryCookieStore INSTANCE = new MemoryCookieStore();
|
||||
|
||||
protected final Map<HttpUrl, List<Cookie>> store = new ConcurrentHashMap<>();
|
||||
|
||||
|
||||
@Override
|
||||
public void saveFromResponse(HttpUrl url, List<Cookie> cookies) {
|
||||
store.put(url, cookies);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Cookie> loadForRequest(HttpUrl url) {
|
||||
List<Cookie> cookies = store.get(url);
|
||||
|
||||
if (cookies == null)
|
||||
cookies = Collections.emptyList();
|
||||
|
||||
return cookies;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
/*
|
||||
* 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 de.duenndns.ssl.MemorizingTrustManager;
|
||||
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 MemorizingTrustManager mtm) {
|
||||
try {
|
||||
SSLContext sslContext = SSLContext.getInstance("TLS");
|
||||
sslContext.init(null, new X509TrustManager[] { mtm }, 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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,80 +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.util.Log;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
|
||||
public class URIUtils {
|
||||
private static final String TAG = "davdroid.URIUtils";
|
||||
|
||||
|
||||
public static String ensureTrailingSlash(String href) {
|
||||
if (!href.endsWith("/")) {
|
||||
Log.d(TAG, "Implicitly appending trailing slash to collection " + href);
|
||||
return href + "/";
|
||||
} else
|
||||
return href;
|
||||
}
|
||||
|
||||
public static URI ensureTrailingSlash(URI href) {
|
||||
if (!href.getPath().endsWith("/")) {
|
||||
try {
|
||||
URI newURI = new URI(href.getScheme(), href.getAuthority(), href.getPath() + "/", null, null);
|
||||
Log.d(TAG, "Appended trailing slash to collection " + href + " -> " + newURI);
|
||||
href = newURI;
|
||||
} catch (URISyntaxException e) {
|
||||
}
|
||||
}
|
||||
return href;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Parse a received absolute/relative URL and generate a normalized URI that can be compared.
|
||||
* @param original URI to be parsed, may be absolute or relative. Encoded characters will be decoded!
|
||||
* @param mustBePath true if it's known that original is a path (may contain ":") and not an URI, i.e. ":" is not the scheme separator
|
||||
* @return normalized URI
|
||||
* @throws URISyntaxException
|
||||
*/
|
||||
public static URI parseURI(String original, boolean mustBePath) throws URISyntaxException {
|
||||
if (mustBePath) {
|
||||
// may contain ":"
|
||||
// case 1: "my:file" won't be parsed by URI correctly because it would consider "my" as URI scheme
|
||||
// case 2: "path/my:file" will be parsed by URI correctly
|
||||
// case 3: "my:path/file" won't be parsed by URI correctly because it would consider "my" as URI scheme
|
||||
int idxSlash = original.indexOf('/'),
|
||||
idxColon = original.indexOf(':');
|
||||
if (idxColon != -1) {
|
||||
// colon present
|
||||
if ((idxSlash != -1) && idxSlash < idxColon) // There's a slash, and it's before the colon → everything OK
|
||||
;
|
||||
else // No slash before the colon; we have to put it there
|
||||
original = "./" + original;
|
||||
}
|
||||
}
|
||||
|
||||
// escape some common invalid characters – servers keep sending unescaped crap like "my calendar.ics" or "{guid}.vcf"
|
||||
// this is only a hack, because for instance, "[" may be valid in URLs (IPv6 literal in host name)
|
||||
String repaired = original
|
||||
.replaceAll(" ", "%20")
|
||||
.replaceAll("\\{", "%7B")
|
||||
.replaceAll("\\}", "%7D");
|
||||
if (!repaired.equals(original))
|
||||
Log.w(TAG, "Repaired invalid URL: " + original + " -> " + repaired);
|
||||
|
||||
URI uri = new URI(repaired);
|
||||
URI normalized = new URI(uri.getScheme(), uri.getAuthority(), uri.getPath(), uri.getQuery(), uri.getFragment());
|
||||
Log.v(TAG, "Normalized URI " + original + " -> " + normalized.toASCIIString() + " assuming that it was " +
|
||||
(mustBePath ? "a path name" : "an URI or path name"));
|
||||
return normalized;
|
||||
}
|
||||
|
||||
}
|
||||
59
app/src/main/java/at/bitfire/davdroid/log/LogcatHandler.java
Normal file
59
app/src/main/java/at/bitfire/davdroid/log/LogcatHandler.java
Normal file
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* 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() {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* 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.", "");
|
||||
}
|
||||
|
||||
}
|
||||
40
app/src/main/java/at/bitfire/davdroid/log/StringHandler.java
Normal file
40
app/src/main/java/at/bitfire/davdroid/log/StringHandler.java
Normal file
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* 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();
|
||||
}
|
||||
}
|
||||
163
app/src/main/java/at/bitfire/davdroid/model/CollectionInfo.java
Normal file
163
app/src/main/java/at/bitfire/davdroid/model/CollectionInfo.java
Normal file
@@ -0,0 +1,163 @@
|
||||
/*
|
||||
* 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);
|
||||
}
|
||||
|
||||
}
|
||||
186
app/src/main/java/at/bitfire/davdroid/model/ServiceDB.java
Normal file
186
app/src/main/java/at/bitfire/davdroid/model/ServiceDB.java
Normal file
@@ -0,0 +1,186 @@
|
||||
/*
|
||||
* 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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
45
app/src/main/java/at/bitfire/davdroid/model/Settings.java
Normal file
45
app/src/main/java/at/bitfire/davdroid/model/Settings.java
Normal file
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* 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 });
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
}
|
||||
@@ -1,78 +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.util.Log;
|
||||
|
||||
import org.apache.http.impl.client.CloseableHttpClient;
|
||||
import org.simpleframework.xml.Serializer;
|
||||
import org.simpleframework.xml.core.Persister;
|
||||
|
||||
import java.io.StringWriter;
|
||||
import java.net.URISyntaxException;
|
||||
|
||||
import at.bitfire.davdroid.webdav.DavCalendarQuery;
|
||||
import at.bitfire.davdroid.webdav.DavCompFilter;
|
||||
import at.bitfire.davdroid.webdav.DavFilter;
|
||||
import at.bitfire.davdroid.webdav.DavMultiget;
|
||||
import at.bitfire.davdroid.webdav.DavProp;
|
||||
|
||||
public class CalDavCalendar extends WebDavCollection<Event> {
|
||||
private final static String TAG = "davdroid.CalDAVCalendar";
|
||||
|
||||
public CalDavCalendar(CloseableHttpClient httpClient, String baseURL, String user, String password, boolean preemptiveAuth) throws URISyntaxException {
|
||||
super(httpClient, baseURL, user, password, preemptiveAuth);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String memberAcceptedMimeTypes()
|
||||
{
|
||||
return "text/calendar";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected DavMultiget.Type multiGetType() {
|
||||
return DavMultiget.Type.CALENDAR;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Event newResourceSkeleton(String name, String ETag) {
|
||||
return new Event(name, ETag);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String getMemberETagsQuery() {
|
||||
DavCalendarQuery query = new DavCalendarQuery();
|
||||
|
||||
// prop
|
||||
DavProp prop = new DavProp();
|
||||
prop.setGetetag(new DavProp.GetETag());
|
||||
query.setProp(prop);
|
||||
|
||||
// filter
|
||||
DavFilter filter = new DavFilter();
|
||||
query.setFilter(filter);
|
||||
|
||||
DavCompFilter compFilter = new DavCompFilter("VCALENDAR");
|
||||
filter.setCompFilter(compFilter);
|
||||
|
||||
compFilter.setCompFilter(new DavCompFilter("VEVENT"));
|
||||
|
||||
Serializer serializer = new Persister();
|
||||
StringWriter writer = new StringWriter();
|
||||
try {
|
||||
serializer.write(query, writer);
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Couldn't prepare REPORT query", e);
|
||||
return null;
|
||||
}
|
||||
|
||||
return writer.toString();
|
||||
}
|
||||
}
|
||||
@@ -1,80 +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.util.Log;
|
||||
|
||||
import org.apache.http.impl.client.CloseableHttpClient;
|
||||
import org.simpleframework.xml.Serializer;
|
||||
import org.simpleframework.xml.core.Persister;
|
||||
|
||||
import java.io.StringWriter;
|
||||
import java.net.URISyntaxException;
|
||||
|
||||
import at.bitfire.davdroid.webdav.DavCalendarQuery;
|
||||
import at.bitfire.davdroid.webdav.DavCompFilter;
|
||||
import at.bitfire.davdroid.webdav.DavFilter;
|
||||
import at.bitfire.davdroid.webdav.DavMultiget;
|
||||
import at.bitfire.davdroid.webdav.DavProp;
|
||||
|
||||
public class CalDavTaskList extends WebDavCollection<Task> {
|
||||
private final static String TAG = "davdroid.CalDAVTaskList";
|
||||
|
||||
public CalDavTaskList(CloseableHttpClient httpClient, String baseURL, String user, String password, boolean preemptiveAuth) throws URISyntaxException {
|
||||
super(httpClient, baseURL, user, password, preemptiveAuth);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String memberAcceptedMimeTypes()
|
||||
{
|
||||
return "text/calendar";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected DavMultiget.Type multiGetType() {
|
||||
return DavMultiget.Type.CALENDAR;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Task newResourceSkeleton(String name, String ETag) {
|
||||
return new Task(name, ETag);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String getMemberETagsQuery() {
|
||||
DavCalendarQuery query = new DavCalendarQuery();
|
||||
|
||||
// prop
|
||||
DavProp prop = new DavProp();
|
||||
prop.setGetetag(new DavProp.GetETag());
|
||||
query.setProp(prop);
|
||||
|
||||
// filter
|
||||
DavFilter filter = new DavFilter();
|
||||
query.setFilter(filter);
|
||||
|
||||
DavCompFilter compFilter = new DavCompFilter("VCALENDAR");
|
||||
filter.setCompFilter(compFilter);
|
||||
|
||||
compFilter.setCompFilter(new DavCompFilter("VTODO"));
|
||||
|
||||
Serializer serializer = new Persister();
|
||||
StringWriter writer = new StringWriter();
|
||||
try {
|
||||
serializer.write(query, writer);
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Couldn't prepare REPORT query", e);
|
||||
return null;
|
||||
}
|
||||
|
||||
return writer.toString();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,42 +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 org.apache.http.impl.client.CloseableHttpClient;
|
||||
|
||||
import java.net.URISyntaxException;
|
||||
|
||||
import at.bitfire.davdroid.syncadapter.AccountSettings;
|
||||
import at.bitfire.davdroid.webdav.DavMultiget;
|
||||
import ezvcard.VCardVersion;
|
||||
|
||||
public class CardDavAddressBook extends WebDavCollection<Contact> {
|
||||
AccountSettings accountSettings;
|
||||
|
||||
@Override
|
||||
protected String memberAcceptedMimeTypes() {
|
||||
return "text/vcard;q=0.8, text/vcard;version=4.0";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected DavMultiget.Type multiGetType() {
|
||||
return accountSettings.getAddressBookVCardVersion() == VCardVersion.V4_0 ?
|
||||
DavMultiget.Type.ADDRESS_BOOK_V4 : DavMultiget.Type.ADDRESS_BOOK;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Contact newResourceSkeleton(String name, String ETag) {
|
||||
return new Contact(name, ETag);
|
||||
}
|
||||
|
||||
|
||||
public CardDavAddressBook(AccountSettings settings, CloseableHttpClient httpClient, String baseURL, String user, String password, boolean preemptiveAuth) throws URISyntaxException {
|
||||
super(httpClient, baseURL, user, password, preemptiveAuth);
|
||||
accountSettings = settings;
|
||||
}
|
||||
}
|
||||
@@ -1,453 +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.util.Log;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URI;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import at.bitfire.davdroid.Constants;
|
||||
import ezvcard.Ezvcard;
|
||||
import ezvcard.VCard;
|
||||
import ezvcard.VCardVersion;
|
||||
import ezvcard.ValidationWarnings;
|
||||
import ezvcard.parameter.EmailType;
|
||||
import ezvcard.parameter.ImageType;
|
||||
import ezvcard.parameter.RelatedType;
|
||||
import ezvcard.parameter.TelephoneType;
|
||||
import ezvcard.property.Address;
|
||||
import ezvcard.property.Anniversary;
|
||||
import ezvcard.property.Birthday;
|
||||
import ezvcard.property.Categories;
|
||||
import ezvcard.property.Email;
|
||||
import ezvcard.property.FormattedName;
|
||||
import ezvcard.property.Impp;
|
||||
import ezvcard.property.Logo;
|
||||
import ezvcard.property.Nickname;
|
||||
import ezvcard.property.Note;
|
||||
import ezvcard.property.Organization;
|
||||
import ezvcard.property.Photo;
|
||||
import ezvcard.property.ProductId;
|
||||
import ezvcard.property.RawProperty;
|
||||
import ezvcard.property.Related;
|
||||
import ezvcard.property.Revision;
|
||||
import ezvcard.property.Role;
|
||||
import ezvcard.property.Sound;
|
||||
import ezvcard.property.Source;
|
||||
import ezvcard.property.StructuredName;
|
||||
import ezvcard.property.Telephone;
|
||||
import ezvcard.property.Title;
|
||||
import ezvcard.property.Uid;
|
||||
import ezvcard.property.Url;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.ToString;
|
||||
|
||||
|
||||
/**
|
||||
* Represents a contact. Locally, this is a Contact in the Android
|
||||
* device; remote, this is a VCard.
|
||||
*/
|
||||
@ToString(callSuper = true)
|
||||
public class Contact extends Resource {
|
||||
private final static String TAG = "davdroid.Contact";
|
||||
|
||||
@Getter @Setter protected VCardVersion vCardVersion = VCardVersion.V3_0;
|
||||
|
||||
public final static String
|
||||
PROPERTY_STARRED = "X-DAVDROID-STARRED",
|
||||
PROPERTY_PHONETIC_FIRST_NAME = "X-PHONETIC-FIRST-NAME",
|
||||
PROPERTY_PHONETIC_MIDDLE_NAME = "X-PHONETIC-MIDDLE-NAME",
|
||||
PROPERTY_PHONETIC_LAST_NAME = "X-PHONETIC-LAST-NAME",
|
||||
PROPERTY_SIP = "X-SIP";
|
||||
|
||||
public final static EmailType EMAIL_TYPE_MOBILE = EmailType.get("x-mobile");
|
||||
|
||||
public final static TelephoneType
|
||||
PHONE_TYPE_CALLBACK = TelephoneType.get("x-callback"),
|
||||
PHONE_TYPE_COMPANY_MAIN = TelephoneType.get("x-company_main"),
|
||||
PHONE_TYPE_RADIO = TelephoneType.get("x-radio"),
|
||||
PHONE_TYPE_ASSISTANT = TelephoneType.get("X-assistant"),
|
||||
PHONE_TYPE_MMS = TelephoneType.get("x-mms");
|
||||
public final static RelatedType
|
||||
RELATED_TYPE_BROTHER = RelatedType.get("brother"),
|
||||
RELATED_TYPE_FATHER = RelatedType.get("father"),
|
||||
RELATED_TYPE_MANAGER = RelatedType.get("manager"),
|
||||
RELATED_TYPE_MOTHER = RelatedType.get("mother"),
|
||||
RELATED_TYPE_REFERRED_BY = RelatedType.get("referred-by"),
|
||||
RELATED_TYPE_SISTER = RelatedType.get("sister");
|
||||
|
||||
@Getter @Setter private String unknownProperties;
|
||||
|
||||
@Getter @Setter private boolean starred;
|
||||
|
||||
@Getter @Setter private String displayName, nickName;
|
||||
@Getter @Setter private String prefix, givenName, middleName, familyName, suffix;
|
||||
@Getter @Setter private String phoneticGivenName, phoneticMiddleName, phoneticFamilyName;
|
||||
@Getter @Setter private String note;
|
||||
@Getter @Setter private Organization organization;
|
||||
@Getter @Setter private String jobTitle, jobDescription;
|
||||
|
||||
@Getter @Setter private byte[] photo;
|
||||
|
||||
@Getter @Setter private Anniversary anniversary;
|
||||
@Getter @Setter private Birthday birthDay;
|
||||
|
||||
@Getter private List<Telephone> phoneNumbers = new LinkedList<>();
|
||||
@Getter private List<Email> emails = new LinkedList<>();
|
||||
@Getter private List<Impp> impps = new LinkedList<>();
|
||||
@Getter private List<Address> addresses = new LinkedList<>();
|
||||
@Getter private List<String> categories = new LinkedList<>();
|
||||
@Getter private List<String> URLs = new LinkedList<>();
|
||||
@Getter private List<Related> relations = new LinkedList<>();
|
||||
|
||||
/* instance methods */
|
||||
|
||||
Contact(String name, String ETag) {
|
||||
super(name, ETag);
|
||||
}
|
||||
|
||||
Contact(long localID, String resourceName, String eTag) {
|
||||
super(localID, resourceName, eTag);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
generateUID();
|
||||
name = uid + ".vcf";
|
||||
}
|
||||
|
||||
protected void generateUID() {
|
||||
uid = UUID.randomUUID().toString();
|
||||
}
|
||||
|
||||
|
||||
/* VCard methods */
|
||||
|
||||
@SuppressWarnings("LoopStatementThatDoesntLoop")
|
||||
@Override
|
||||
public void parseEntity(InputStream is, AssetDownloader downloader) throws IOException {
|
||||
VCard vcard = Ezvcard.parse(is).first();
|
||||
if (vcard == null)
|
||||
return;
|
||||
|
||||
// now work through all supported properties
|
||||
// supported properties are removed from the VCard after parsing
|
||||
// so that only unknown properties are left and can be stored separately
|
||||
|
||||
// UID
|
||||
Uid uid = vcard.getUid();
|
||||
if (uid != null) {
|
||||
this.uid = uid.getValue();
|
||||
vcard.removeProperties(Uid.class);
|
||||
} else {
|
||||
Log.w(TAG, "Received VCard without UID, generating new one");
|
||||
generateUID();
|
||||
}
|
||||
|
||||
// X-DAVDROID-STARRED
|
||||
RawProperty starred = vcard.getExtendedProperty(PROPERTY_STARRED);
|
||||
if (starred != null && starred.getValue() != null) {
|
||||
this.starred = starred.getValue().equals("1");
|
||||
vcard.removeExtendedProperty(PROPERTY_STARRED);
|
||||
} else
|
||||
this.starred = false;
|
||||
|
||||
// FN
|
||||
FormattedName fn = vcard.getFormattedName();
|
||||
if (fn != null) {
|
||||
displayName = fn.getValue();
|
||||
vcard.removeProperties(FormattedName.class);
|
||||
} else
|
||||
Log.w(TAG, "Received invalid VCard without FN (formatted name) property");
|
||||
|
||||
// N
|
||||
StructuredName n = vcard.getStructuredName();
|
||||
if (n != null) {
|
||||
prefix = StringUtils.join(n.getPrefixes(), " ");
|
||||
givenName = n.getGiven();
|
||||
middleName = StringUtils.join(n.getAdditional(), " ");
|
||||
familyName = n.getFamily();
|
||||
suffix = StringUtils.join(n.getSuffixes(), " ");
|
||||
vcard.removeProperties(StructuredName.class);
|
||||
}
|
||||
|
||||
// phonetic names
|
||||
RawProperty
|
||||
phoneticFirstName = vcard.getExtendedProperty(PROPERTY_PHONETIC_FIRST_NAME),
|
||||
phoneticMiddleName = vcard.getExtendedProperty(PROPERTY_PHONETIC_MIDDLE_NAME),
|
||||
phoneticLastName = vcard.getExtendedProperty(PROPERTY_PHONETIC_LAST_NAME);
|
||||
if (phoneticFirstName != null) {
|
||||
phoneticGivenName = phoneticFirstName.getValue();
|
||||
vcard.removeExtendedProperty(PROPERTY_PHONETIC_FIRST_NAME);
|
||||
}
|
||||
if (phoneticMiddleName != null) {
|
||||
this.phoneticMiddleName = phoneticMiddleName.getValue();
|
||||
vcard.removeExtendedProperty(PROPERTY_PHONETIC_MIDDLE_NAME);
|
||||
}
|
||||
if (phoneticLastName != null) {
|
||||
phoneticFamilyName = phoneticLastName.getValue();
|
||||
vcard.removeExtendedProperty(PROPERTY_PHONETIC_LAST_NAME);
|
||||
}
|
||||
|
||||
// TEL
|
||||
phoneNumbers = vcard.getTelephoneNumbers();
|
||||
vcard.removeProperties(Telephone.class);
|
||||
|
||||
// EMAIL
|
||||
emails = vcard.getEmails();
|
||||
vcard.removeProperties(Email.class);
|
||||
|
||||
// PHOTO
|
||||
for (Photo photo : vcard.getPhotos()) {
|
||||
this.photo = photo.getData();
|
||||
if (this.photo == null && photo.getUrl() != null)
|
||||
try {
|
||||
URI uri = new URI(photo.getUrl());
|
||||
Log.i(TAG, "Downloading contact photo from " + uri);
|
||||
this.photo = downloader.download(uri);
|
||||
} catch(Exception e) {
|
||||
Log.w(TAG, "Couldn't fetch contact photo", e);
|
||||
}
|
||||
vcard.removeProperties(Photo.class);
|
||||
break;
|
||||
}
|
||||
|
||||
// ORG
|
||||
organization = vcard.getOrganization();
|
||||
vcard.removeProperties(Organization.class);
|
||||
// TITLE
|
||||
for (Title title : vcard.getTitles()) {
|
||||
jobTitle = title.getValue();
|
||||
vcard.removeProperties(Title.class);
|
||||
break;
|
||||
}
|
||||
// ROLE
|
||||
for (Role role : vcard.getRoles()) {
|
||||
this.jobDescription = role.getValue();
|
||||
vcard.removeProperties(Role.class);
|
||||
break;
|
||||
}
|
||||
|
||||
// IMPP
|
||||
impps = vcard.getImpps();
|
||||
vcard.removeProperties(Impp.class);
|
||||
|
||||
// NICKNAME
|
||||
Nickname nicknames = vcard.getNickname();
|
||||
if (nicknames != null) {
|
||||
if (nicknames.getValues() != null)
|
||||
nickName = StringUtils.join(nicknames.getValues(), ", ");
|
||||
vcard.removeProperties(Nickname.class);
|
||||
}
|
||||
|
||||
// NOTE
|
||||
List<String> notes = new LinkedList<>();
|
||||
for (Note note : vcard.getNotes())
|
||||
notes.add(note.getValue());
|
||||
if (!notes.isEmpty())
|
||||
note = StringUtils.join(notes, "\n---\n");
|
||||
vcard.removeProperties(Note.class);
|
||||
|
||||
// ADR
|
||||
addresses = vcard.getAddresses();
|
||||
vcard.removeProperties(Address.class);
|
||||
|
||||
// CATEGORY
|
||||
Categories categories = vcard.getCategories();
|
||||
if (categories != null)
|
||||
this.categories = categories.getValues();
|
||||
vcard.removeProperties(Categories.class);
|
||||
|
||||
// URL
|
||||
for (Url url : vcard.getUrls())
|
||||
URLs.add(url.getValue());
|
||||
vcard.removeProperties(Url.class);
|
||||
|
||||
// BDAY
|
||||
birthDay = vcard.getBirthday();
|
||||
vcard.removeProperties(Birthday.class);
|
||||
// ANNIVERSARY
|
||||
anniversary = vcard.getAnniversary();
|
||||
vcard.removeProperties(Anniversary.class);
|
||||
|
||||
// RELATED
|
||||
for (Related related : vcard.getRelations()) {
|
||||
String text = related.getText();
|
||||
if (!StringUtils.isNotEmpty(text)) {
|
||||
// process only free-form relations with text
|
||||
relations.add(related);
|
||||
vcard.removeProperty(related);
|
||||
}
|
||||
}
|
||||
|
||||
// X-SIP
|
||||
for (RawProperty sip : vcard.getExtendedProperties(PROPERTY_SIP))
|
||||
impps.add(new Impp("sip", sip.getValue()));
|
||||
vcard.removeExtendedProperty(PROPERTY_SIP);
|
||||
|
||||
// remove binary properties because of potential OutOfMemory / TransactionTooLarge exceptions
|
||||
vcard.removeProperties(Logo.class);
|
||||
vcard.removeProperties(Sound.class);
|
||||
// remove properties that don't apply anymore
|
||||
vcard.removeProperties(ProductId.class);
|
||||
vcard.removeProperties(Revision.class);
|
||||
vcard.removeProperties(Source.class);
|
||||
// store all remaining properties into unknownProperties
|
||||
if (!vcard.getProperties().isEmpty() || !vcard.getExtendedProperties().isEmpty())
|
||||
try {
|
||||
unknownProperties = vcard.write();
|
||||
} catch(Exception e) {
|
||||
Log.w(TAG, "Couldn't store unknown properties (maybe illegal syntax), dropping them");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String getMimeType() {
|
||||
if (vCardVersion == VCardVersion.V4_0)
|
||||
return "text/vcard;version=4.0";
|
||||
else
|
||||
return "text/vcard;charset=UTF-8";
|
||||
}
|
||||
|
||||
@Override
|
||||
public ByteArrayOutputStream toEntity() throws IOException {
|
||||
VCard vcard = null;
|
||||
try {
|
||||
if (unknownProperties != null)
|
||||
vcard = Ezvcard.parse(unknownProperties).first();
|
||||
} catch (Exception e) {
|
||||
Log.w(TAG, "Couldn't parse original property set, beginning from scratch");
|
||||
}
|
||||
if (vcard == null)
|
||||
vcard = new VCard();
|
||||
|
||||
if (uid != null)
|
||||
vcard.setUid(new Uid(uid));
|
||||
else
|
||||
Log.wtf(TAG, "Generating VCard without UID");
|
||||
|
||||
if (starred)
|
||||
vcard.setExtendedProperty(PROPERTY_STARRED, "1");
|
||||
|
||||
if (displayName != null)
|
||||
vcard.setFormattedName(displayName);
|
||||
else if (organization != null && organization.getValues() != null && organization.getValues().get(0) != null)
|
||||
vcard.setFormattedName(organization.getValues().get(0));
|
||||
else
|
||||
Log.w(TAG, "No FN (formatted name) available to generate VCard");
|
||||
|
||||
// N
|
||||
if (prefix != null || familyName != null || middleName != null || givenName != null || suffix != null) {
|
||||
StructuredName n = new StructuredName();
|
||||
if (prefix != null)
|
||||
for (String p : StringUtils.split(prefix))
|
||||
n.addPrefix(p);
|
||||
n.setGiven(givenName);
|
||||
if (middleName != null)
|
||||
for (String middle : StringUtils.split(middleName))
|
||||
n.addAdditional(middle);
|
||||
n.setFamily(familyName);
|
||||
if (suffix != null)
|
||||
for (String s : StringUtils.split(suffix))
|
||||
n.addSuffix(s);
|
||||
vcard.setStructuredName(n);
|
||||
}
|
||||
|
||||
// phonetic names
|
||||
if (phoneticGivenName != null)
|
||||
vcard.addExtendedProperty(PROPERTY_PHONETIC_FIRST_NAME, phoneticGivenName);
|
||||
if (phoneticMiddleName != null)
|
||||
vcard.addExtendedProperty(PROPERTY_PHONETIC_MIDDLE_NAME, phoneticMiddleName);
|
||||
if (phoneticFamilyName != null)
|
||||
vcard.addExtendedProperty(PROPERTY_PHONETIC_LAST_NAME, phoneticFamilyName);
|
||||
|
||||
// TEL
|
||||
for (Telephone phoneNumber : phoneNumbers)
|
||||
vcard.addTelephoneNumber(phoneNumber);
|
||||
|
||||
// EMAIL
|
||||
for (Email email : emails)
|
||||
vcard.addEmail(email);
|
||||
|
||||
// ORG, TITLE, ROLE
|
||||
if (organization != null)
|
||||
vcard.setOrganization(organization);
|
||||
if (jobTitle != null)
|
||||
vcard.addTitle(jobTitle);
|
||||
if (jobDescription != null)
|
||||
vcard.addRole(jobDescription);
|
||||
|
||||
// IMPP
|
||||
for (Impp impp : impps)
|
||||
vcard.addImpp(impp);
|
||||
|
||||
// NICKNAME
|
||||
if (!StringUtils.isBlank(nickName))
|
||||
vcard.setNickname(nickName);
|
||||
|
||||
// NOTE
|
||||
if (!StringUtils.isBlank(note))
|
||||
vcard.addNote(note);
|
||||
|
||||
// ADR
|
||||
for (Address address : addresses)
|
||||
vcard.addAddress(address);
|
||||
|
||||
// CATEGORY
|
||||
if (!categories.isEmpty())
|
||||
vcard.setCategories(categories.toArray(new String[categories.size()]));
|
||||
|
||||
// URL
|
||||
for (String url : URLs)
|
||||
vcard.addUrl(url);
|
||||
|
||||
// ANNIVERSARY
|
||||
if (anniversary != null)
|
||||
vcard.setAnniversary(anniversary);
|
||||
// BDAY
|
||||
if (birthDay != null)
|
||||
vcard.setBirthday(birthDay);
|
||||
|
||||
// PHOTO
|
||||
if (photo != null)
|
||||
vcard.addPhoto(new Photo(photo, ImageType.JPEG));
|
||||
|
||||
// REL
|
||||
for (Related related : relations)
|
||||
vcard.addRelated(related);
|
||||
|
||||
// PRODID, REV
|
||||
vcard.setProductId("DAVdroid/" + Constants.APP_VERSION + " (ez-vcard/" + Ezvcard.VERSION + ")");
|
||||
vcard.setRevision(Revision.now());
|
||||
|
||||
// validate and print warnings
|
||||
ValidationWarnings warnings = vcard.validate(vCardVersion);
|
||||
if (!warnings.isEmpty())
|
||||
Log.w(TAG, "Created potentially invalid VCard:\n" + warnings);
|
||||
|
||||
ByteArrayOutputStream os = new ByteArrayOutputStream();
|
||||
Ezvcard
|
||||
.write(vcard)
|
||||
.version(vCardVersion)
|
||||
.versionStrict(false)
|
||||
.prodId(false) // we provide our own PRODID
|
||||
.go(os);
|
||||
return os;
|
||||
}
|
||||
}
|
||||
@@ -1,339 +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.Context;
|
||||
import android.util.Log;
|
||||
|
||||
import org.apache.http.HttpException;
|
||||
import org.apache.http.impl.client.CloseableHttpClient;
|
||||
import org.xbill.DNS.Lookup;
|
||||
import org.xbill.DNS.Record;
|
||||
import org.xbill.DNS.SRVRecord;
|
||||
import org.xbill.DNS.TXTRecord;
|
||||
import org.xbill.DNS.TextParseException;
|
||||
import org.xbill.DNS.Type;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
import at.bitfire.davdroid.R;
|
||||
import at.bitfire.davdroid.webdav.DavException;
|
||||
import at.bitfire.davdroid.webdav.DavHttpClient;
|
||||
import at.bitfire.davdroid.webdav.DavIncapableException;
|
||||
import at.bitfire.davdroid.webdav.HttpPropfind.Mode;
|
||||
import at.bitfire.davdroid.webdav.NotAuthorizedException;
|
||||
import at.bitfire.davdroid.webdav.WebDavResource;
|
||||
|
||||
public class DavResourceFinder implements Closeable {
|
||||
private final static String TAG = "davdroid.ResourceFinder";
|
||||
|
||||
final protected Context context;
|
||||
final protected CloseableHttpClient httpClient;
|
||||
|
||||
|
||||
public DavResourceFinder(Context context) {
|
||||
this.context = context;
|
||||
|
||||
// disable compression and enable network logging for debugging purposes
|
||||
httpClient = DavHttpClient.create();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
httpClient.close();
|
||||
}
|
||||
|
||||
|
||||
public void findResources(ServerInfo serverInfo) throws URISyntaxException, DavException, HttpException, IOException {
|
||||
// CardDAV
|
||||
Log.i(TAG, "*** Starting CardDAV resource detection");
|
||||
WebDavResource principal = getCurrentUserPrincipal(serverInfo, "carddav");
|
||||
URI uriAddressBookHomeSet = null;
|
||||
try {
|
||||
principal.propfind(Mode.HOME_SETS);
|
||||
uriAddressBookHomeSet = principal.getProperties().getAddressbookHomeSet();
|
||||
} catch (Exception e) {
|
||||
Log.i(TAG, "Couldn't find address-book home set", e);
|
||||
}
|
||||
if (uriAddressBookHomeSet != null) {
|
||||
Log.i(TAG, "Found address-book home set: " + uriAddressBookHomeSet);
|
||||
|
||||
WebDavResource homeSetAddressBooks = new WebDavResource(principal, uriAddressBookHomeSet);
|
||||
if (checkHomesetCapabilities(homeSetAddressBooks, "addressbook")) {
|
||||
serverInfo.setCardDAV(true);
|
||||
homeSetAddressBooks.propfind(Mode.CARDDAV_COLLECTIONS);
|
||||
|
||||
List<WebDavResource> possibleAddressBooks = new LinkedList<>();
|
||||
possibleAddressBooks.add(homeSetAddressBooks);
|
||||
if (homeSetAddressBooks.getMembers() != null)
|
||||
possibleAddressBooks.addAll(homeSetAddressBooks.getMembers());
|
||||
|
||||
List<ServerInfo.ResourceInfo> addressBooks = new LinkedList<>();
|
||||
for (WebDavResource resource : possibleAddressBooks) {
|
||||
final WebDavResource.Properties properties = resource.getProperties();
|
||||
if (properties.isAddressBook()) {
|
||||
Log.i(TAG, "Found address book: " + resource.getLocation().getPath());
|
||||
ServerInfo.ResourceInfo info = new ServerInfo.ResourceInfo(
|
||||
ServerInfo.ResourceInfo.Type.ADDRESS_BOOK,
|
||||
properties.isReadOnly(),
|
||||
resource.getLocation().toString(),
|
||||
properties.getDisplayName(),
|
||||
properties.getDescription(), properties.getColor()
|
||||
);
|
||||
|
||||
addressBooks.add(info);
|
||||
}
|
||||
}
|
||||
serverInfo.setAddressBooks(addressBooks);
|
||||
} else
|
||||
Log.w(TAG, "Found address-book home set, but it doesn't advertise CardDAV support");
|
||||
}
|
||||
|
||||
// CalDAV
|
||||
Log.i(TAG, "*** Starting CalDAV resource detection");
|
||||
principal = getCurrentUserPrincipal(serverInfo, "caldav");
|
||||
URI uriCalendarHomeSet = null;
|
||||
try {
|
||||
principal.propfind(Mode.HOME_SETS);
|
||||
uriCalendarHomeSet = principal.getProperties().getCalendarHomeSet();
|
||||
} catch(Exception e) {
|
||||
Log.i(TAG, "Couldn't find calendar home set", e);
|
||||
}
|
||||
if (uriCalendarHomeSet != null) {
|
||||
Log.i(TAG, "Found calendar home set: " + uriCalendarHomeSet);
|
||||
|
||||
WebDavResource homeSetCalendars = new WebDavResource(principal, uriCalendarHomeSet);
|
||||
if (checkHomesetCapabilities(homeSetCalendars, "calendar-access")) {
|
||||
serverInfo.setCalDAV(true);
|
||||
homeSetCalendars.propfind(Mode.CALDAV_COLLECTIONS);
|
||||
|
||||
List<WebDavResource> possibleCalendars = new LinkedList<>();
|
||||
possibleCalendars.add(homeSetCalendars);
|
||||
if (homeSetCalendars.getMembers() != null)
|
||||
possibleCalendars.addAll(homeSetCalendars.getMembers());
|
||||
|
||||
List<ServerInfo.ResourceInfo>
|
||||
calendars = new LinkedList<>(),
|
||||
todoLists = new LinkedList<>();
|
||||
for (WebDavResource resource : possibleCalendars) {
|
||||
final WebDavResource.Properties properties = resource.getProperties();
|
||||
if (properties.isCalendar()) {
|
||||
Log.i(TAG, "Found calendar: " + resource.getLocation().getPath());
|
||||
ServerInfo.ResourceInfo info = new ServerInfo.ResourceInfo(
|
||||
ServerInfo.ResourceInfo.Type.CALENDAR,
|
||||
properties.isReadOnly(),
|
||||
resource.getLocation().toString(),
|
||||
properties.getDisplayName(),
|
||||
properties.getDescription(), properties.getColor()
|
||||
);
|
||||
info.setTimezone(properties.getTimeZone());
|
||||
|
||||
boolean isCalendar = false,
|
||||
isTodoList = false;
|
||||
if (properties.getSupportedComponents() == null) {
|
||||
// no info about supported components, assuming all components are supported
|
||||
isCalendar = true;
|
||||
isTodoList = true;
|
||||
} else {
|
||||
// CALDAV:supported-calendar-component-set available
|
||||
for (String supportedComponent : properties.getSupportedComponents())
|
||||
if ("VEVENT".equalsIgnoreCase(supportedComponent))
|
||||
isCalendar = true;
|
||||
else if ("VTODO".equalsIgnoreCase(supportedComponent))
|
||||
isTodoList = true;
|
||||
|
||||
if (!isCalendar && !isTodoList) {
|
||||
Log.i(TAG, "Ignoring this calendar because it supports neither VEVENT nor VTODO");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// use a copy constructor to allow different "enabled" status for calendars and todo lists
|
||||
if (isCalendar)
|
||||
calendars.add(new ServerInfo.ResourceInfo(info));
|
||||
if (isTodoList)
|
||||
todoLists.add(new ServerInfo.ResourceInfo(info));
|
||||
}
|
||||
}
|
||||
|
||||
serverInfo.setCalendars(calendars);
|
||||
serverInfo.setTodoLists(todoLists);
|
||||
} else
|
||||
Log.w(TAG, "Found calendar home set, but it doesn't advertise CalDAV support");
|
||||
}
|
||||
|
||||
if (!serverInfo.isCalDAV() && !serverInfo.isCardDAV())
|
||||
throw new DavIncapableException(context.getString(R.string.setup_neither_caldav_nor_carddav));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Finds the initial service URL from a given base URI (HTTP[S] or mailto URI, user name, password)
|
||||
* @param serverInfo User-given service information (including base URI, i.e. HTTP[S] URL+user name+password or mailto URI and password)
|
||||
* @param serviceName Service name ("carddav" or "caldav")
|
||||
* @return Initial service URL (HTTP/HTTPS), without user credentials
|
||||
* @throws URISyntaxException when the user-given URI is invalid
|
||||
*/
|
||||
public URI getInitialContextURL(ServerInfo serverInfo, String serviceName) throws URISyntaxException {
|
||||
String scheme,
|
||||
domain;
|
||||
int port = -1;
|
||||
String path = "/";
|
||||
|
||||
URI baseURI = serverInfo.getBaseURI();
|
||||
if ("mailto".equalsIgnoreCase(baseURI.getScheme())) {
|
||||
// mailto URIs
|
||||
String mailbox = serverInfo.getBaseURI().getSchemeSpecificPart();
|
||||
|
||||
// determine service FQDN
|
||||
int pos = mailbox.lastIndexOf("@");
|
||||
if (pos == -1)
|
||||
throw new URISyntaxException(mailbox, "Missing @ sign");
|
||||
|
||||
scheme = "https";
|
||||
domain = mailbox.substring(pos + 1);
|
||||
if (domain.isEmpty())
|
||||
throw new URISyntaxException(mailbox, "Missing domain name");
|
||||
} else {
|
||||
// HTTP(S) URLs
|
||||
scheme = baseURI.getScheme();
|
||||
domain = baseURI.getHost();
|
||||
port = baseURI.getPort();
|
||||
path = baseURI.getPath();
|
||||
}
|
||||
|
||||
// try to determine FQDN and port number using SRV records
|
||||
try {
|
||||
String name = "_" + serviceName + "s._tcp." + domain;
|
||||
Log.d(TAG, "Looking up SRV records for " + name);
|
||||
Record[] records = new Lookup(name, Type.SRV).run();
|
||||
if (records != null && records.length >= 1) {
|
||||
SRVRecord srv = selectSRVRecord(records);
|
||||
|
||||
scheme = "https";
|
||||
domain = srv.getTarget().toString(true);
|
||||
port = srv.getPort();
|
||||
Log.d(TAG, "Found " + serviceName + "s service for " + domain + " -> " + domain + ":" + port);
|
||||
|
||||
if (port == 443) // no reason to explicitly give the default port
|
||||
port = -1;
|
||||
|
||||
// SRV record found, look for TXT record too (for initial context path)
|
||||
records = new Lookup(name, Type.TXT).run();
|
||||
if (records != null && records.length >= 1) {
|
||||
TXTRecord txt = (TXTRecord)records[0];
|
||||
for (Object o : txt.getStrings().toArray()) {
|
||||
String segment = (String)o;
|
||||
if (segment.startsWith("path=")) {
|
||||
path = segment.substring(5);
|
||||
Log.d(TAG, "Found initial context path for " + serviceName + " at " + domain + " -> " + path);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (TextParseException e) {
|
||||
throw new URISyntaxException(domain, "Invalid domain name");
|
||||
}
|
||||
|
||||
return new URI(scheme, null, domain, port, path, null, null);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Detects the current-user-principal for a given WebDavResource. At first, /.well-known/ is tried. Only
|
||||
* if no current-user-principal can be detected for the .well-known location, the given location of the resource
|
||||
* is tried.
|
||||
* @param serverInfo Location that will be queried
|
||||
* @param serviceName Well-known service name ("carddav", "caldav")
|
||||
* @return WebDavResource of current-user-principal for the given service, or null if it can't be found
|
||||
*
|
||||
* TODO: If a TXT record is given, always use it instead of trying .well-known first
|
||||
*/
|
||||
WebDavResource getCurrentUserPrincipal(ServerInfo serverInfo, String serviceName) throws URISyntaxException, IOException, NotAuthorizedException {
|
||||
URI initialURL = getInitialContextURL(serverInfo, serviceName);
|
||||
if (initialURL != null) {
|
||||
Log.i(TAG, "Looking up principal URL for service " + serviceName + "; initial context: " + initialURL);
|
||||
|
||||
// determine base URL (host name and initial context path)
|
||||
WebDavResource base = new WebDavResource(httpClient,
|
||||
initialURL,
|
||||
serverInfo.getUserName(), serverInfo.getPassword(), serverInfo.isAuthPreemptive());
|
||||
|
||||
// look for well-known service (RFC 5785)
|
||||
try {
|
||||
WebDavResource wellKnown = new WebDavResource(base, "/.well-known/" + serviceName);
|
||||
wellKnown.propfind(Mode.CURRENT_USER_PRINCIPAL);
|
||||
if (wellKnown.getProperties().getCurrentUserPrincipal() != null) {
|
||||
URI principal = wellKnown.getProperties().getCurrentUserPrincipal();
|
||||
Log.i(TAG, "Principal URL found from Well-Known URI: " + principal);
|
||||
return new WebDavResource(wellKnown, principal);
|
||||
}
|
||||
} catch (NotAuthorizedException e) {
|
||||
Log.w(TAG, "Not authorized for well-known " + serviceName + " service detection", e);
|
||||
throw e;
|
||||
} catch (URISyntaxException e) {
|
||||
Log.e(TAG, "Well-known" + serviceName + " service detection failed because of invalid URIs", e);
|
||||
} catch (HttpException e) {
|
||||
Log.d(TAG, "Well-known " + serviceName + " service detection failed with HTTP error", e);
|
||||
} catch (DavException e) {
|
||||
Log.w(TAG, "Well-known " + serviceName + " service detection failed with unexpected DAV response", e);
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Well-known " + serviceName + " service detection failed with I/O error", e);
|
||||
}
|
||||
|
||||
// fall back to user-given initial context path
|
||||
Log.d(TAG, "Well-known service detection failed, trying initial context path " + initialURL);
|
||||
try {
|
||||
base.propfind(Mode.CURRENT_USER_PRINCIPAL);
|
||||
if (base.getProperties().getCurrentUserPrincipal() != null) {
|
||||
URI principal = base.getProperties().getCurrentUserPrincipal();
|
||||
Log.i(TAG, "Principal URL found from initial context path: " + principal);
|
||||
return new WebDavResource(base, principal);
|
||||
}
|
||||
} catch (NotAuthorizedException e) {
|
||||
Log.e(TAG, "Not authorized for querying principal", e);
|
||||
throw e;
|
||||
} catch (HttpException e) {
|
||||
Log.e(TAG, "HTTP error when querying principal", e);
|
||||
} catch (DavException e) {
|
||||
Log.e(TAG, "DAV error when querying principal", e);
|
||||
}
|
||||
|
||||
Log.i(TAG, "Couldn't find current-user-principal for service " + serviceName + ", assuming initial context path is principal path");
|
||||
return base;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static boolean checkHomesetCapabilities(WebDavResource resource, String davCapability) throws URISyntaxException, IOException {
|
||||
// check for necessary capabilities
|
||||
try {
|
||||
resource.options();
|
||||
if (resource.supportsDAV(davCapability) &&
|
||||
resource.supportsMethod("PROPFIND")) // check only for methods that MUST be available for home sets
|
||||
return true;
|
||||
} catch(HttpException e) {
|
||||
// for instance, 405 Method not allowed
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
SRVRecord selectSRVRecord(Record[] records) {
|
||||
if (records.length > 1)
|
||||
Log.w(TAG, "Multiple SRV records not supported yet; using first one");
|
||||
return (SRVRecord)records[0];
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,351 +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.text.format.Time;
|
||||
import android.util.Log;
|
||||
|
||||
import net.fortuna.ical4j.data.CalendarBuilder;
|
||||
import net.fortuna.ical4j.data.CalendarOutputter;
|
||||
import net.fortuna.ical4j.data.ParserException;
|
||||
import net.fortuna.ical4j.model.Component;
|
||||
import net.fortuna.ical4j.model.ComponentList;
|
||||
import net.fortuna.ical4j.model.Date;
|
||||
import net.fortuna.ical4j.model.DateTime;
|
||||
import net.fortuna.ical4j.model.Property;
|
||||
import net.fortuna.ical4j.model.PropertyList;
|
||||
import net.fortuna.ical4j.model.ValidationException;
|
||||
import net.fortuna.ical4j.model.component.VAlarm;
|
||||
import net.fortuna.ical4j.model.component.VEvent;
|
||||
import net.fortuna.ical4j.model.property.Attendee;
|
||||
import net.fortuna.ical4j.model.property.Clazz;
|
||||
import net.fortuna.ical4j.model.property.DateProperty;
|
||||
import net.fortuna.ical4j.model.property.Description;
|
||||
import net.fortuna.ical4j.model.property.DtEnd;
|
||||
import net.fortuna.ical4j.model.property.DtStart;
|
||||
import net.fortuna.ical4j.model.property.Duration;
|
||||
import net.fortuna.ical4j.model.property.ExDate;
|
||||
import net.fortuna.ical4j.model.property.ExRule;
|
||||
import net.fortuna.ical4j.model.property.LastModified;
|
||||
import net.fortuna.ical4j.model.property.Location;
|
||||
import net.fortuna.ical4j.model.property.Organizer;
|
||||
import net.fortuna.ical4j.model.property.RDate;
|
||||
import net.fortuna.ical4j.model.property.RRule;
|
||||
import net.fortuna.ical4j.model.property.RecurrenceId;
|
||||
import net.fortuna.ical4j.model.property.Status;
|
||||
import net.fortuna.ical4j.model.property.Summary;
|
||||
import net.fortuna.ical4j.model.property.Transp;
|
||||
import net.fortuna.ical4j.model.property.Uid;
|
||||
import net.fortuna.ical4j.model.property.Version;
|
||||
import net.fortuna.ical4j.util.TimeZones;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.Calendar;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.TimeZone;
|
||||
|
||||
import at.bitfire.davdroid.Constants;
|
||||
import at.bitfire.davdroid.DateUtils;
|
||||
import lombok.Getter;
|
||||
import lombok.NonNull;
|
||||
import lombok.Setter;
|
||||
|
||||
|
||||
public class Event extends iCalendar {
|
||||
private final static String TAG = "davdroid.Event";
|
||||
|
||||
@Getter @Setter protected RecurrenceId recurrenceId;
|
||||
|
||||
@Getter @Setter protected String summary, location, description;
|
||||
|
||||
@Getter protected DtStart dtStart;
|
||||
@Getter protected DtEnd dtEnd;
|
||||
@Getter @Setter protected Duration duration;
|
||||
@Getter protected List<RDate> rdates = new LinkedList<>();
|
||||
@Getter @Setter protected RRule rrule;
|
||||
@Getter protected List<ExDate> exdates = new LinkedList<>();
|
||||
@Getter @Setter protected ExRule exrule;
|
||||
@Getter protected List<Event> exceptions = new LinkedList<>();
|
||||
|
||||
@Getter @Setter protected Boolean forPublic;
|
||||
@Getter @Setter protected Status status;
|
||||
|
||||
@Getter @Setter protected boolean opaque;
|
||||
|
||||
@Getter @Setter protected Organizer organizer;
|
||||
@Getter protected List<Attendee> attendees = new LinkedList<>();
|
||||
|
||||
@Getter protected List<VAlarm> alarms = new LinkedList<>();
|
||||
|
||||
|
||||
public Event(String name, String ETag) {
|
||||
super(name, ETag);
|
||||
}
|
||||
|
||||
public Event(long localID, String name, String ETag) {
|
||||
super(localID, name, ETag);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public void parseEntity(@NonNull InputStream entity, AssetDownloader downloader) throws IOException, InvalidResourceException {
|
||||
net.fortuna.ical4j.model.Calendar ical;
|
||||
try {
|
||||
CalendarBuilder builder = new CalendarBuilder();
|
||||
ical = builder.build(entity);
|
||||
|
||||
if (ical == null)
|
||||
throw new InvalidResourceException("No iCalendar found");
|
||||
} catch (ParserException e) {
|
||||
throw new InvalidResourceException(e);
|
||||
}
|
||||
|
||||
ComponentList events = ical.getComponents(Component.VEVENT);
|
||||
if (events == null || events.isEmpty())
|
||||
throw new InvalidResourceException("No VEVENT found");
|
||||
|
||||
// find master VEVENT (the one that is not an exception, i.e. the one without RECURRENCE-ID)
|
||||
VEvent master = null;
|
||||
for (VEvent event : (Iterable<VEvent>)events)
|
||||
if (event.getRecurrenceId() == null) {
|
||||
master = event;
|
||||
break;
|
||||
}
|
||||
if (master == null)
|
||||
throw new InvalidResourceException("No VEVENT without RECURRENCE-ID found");
|
||||
// set event data from master VEVENT
|
||||
fromVEvent(master);
|
||||
|
||||
// find and process exceptions
|
||||
for (VEvent event : (Iterable<VEvent>)events)
|
||||
if (event.getRecurrenceId() != null) {
|
||||
Event exception = new Event(name, null);
|
||||
exception.fromVEvent(event);
|
||||
exceptions.add(exception);
|
||||
}
|
||||
}
|
||||
|
||||
protected void fromVEvent(VEvent event) throws InvalidResourceException {
|
||||
if (event.getUid() != null)
|
||||
uid = event.getUid().getValue();
|
||||
else {
|
||||
Log.w(TAG, "Received VEVENT without UID, generating new one");
|
||||
generateUID();
|
||||
}
|
||||
recurrenceId = event.getRecurrenceId();
|
||||
|
||||
if ((dtStart = event.getStartDate()) == null || (dtEnd = event.getEndDate()) == null)
|
||||
throw new InvalidResourceException("Invalid start time/end time/duration");
|
||||
|
||||
validateTimeZone(dtStart);
|
||||
validateTimeZone(dtEnd);
|
||||
|
||||
// all-day events and "events on that day":
|
||||
// * related UNIX times must be in UTC
|
||||
// * must have a duration (set to one day if missing)
|
||||
if (!isDateTime(dtStart) && !dtEnd.getDate().after(dtStart.getDate())) {
|
||||
Log.i(TAG, "Repairing iCal: DTEND := DTSTART+1");
|
||||
Calendar c = Calendar.getInstance(TimeZone.getTimeZone(Time.TIMEZONE_UTC));
|
||||
c.setTime(dtStart.getDate());
|
||||
c.add(Calendar.DATE, 1);
|
||||
dtEnd.setDate(new Date(c.getTimeInMillis()));
|
||||
}
|
||||
|
||||
rrule = (RRule)event.getProperty(Property.RRULE);
|
||||
for (RDate rdate : (List<RDate>)(List<?>)event.getProperties(Property.RDATE))
|
||||
rdates.add(rdate);
|
||||
exrule = (ExRule)event.getProperty(Property.EXRULE);
|
||||
for (ExDate exdate : (List<ExDate>)(List<?>)event.getProperties(Property.EXDATE))
|
||||
exdates.add(exdate);
|
||||
|
||||
if (event.getSummary() != null)
|
||||
summary = event.getSummary().getValue();
|
||||
if (event.getLocation() != null)
|
||||
location = event.getLocation().getValue();
|
||||
if (event.getDescription() != null)
|
||||
description = event.getDescription().getValue();
|
||||
|
||||
status = event.getStatus();
|
||||
opaque = event.getTransparency() != Transp.TRANSPARENT;
|
||||
|
||||
organizer = event.getOrganizer();
|
||||
for (Attendee attendee : (List<Attendee>)(List<?>)event.getProperties(Property.ATTENDEE))
|
||||
attendees.add(attendee);
|
||||
|
||||
Clazz classification = event.getClassification();
|
||||
if (classification != null) {
|
||||
if (classification == Clazz.PUBLIC)
|
||||
forPublic = true;
|
||||
else if (classification == Clazz.CONFIDENTIAL || classification == Clazz.PRIVATE)
|
||||
forPublic = false;
|
||||
}
|
||||
|
||||
this.alarms = event.getAlarms();
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public ByteArrayOutputStream toEntity() throws IOException {
|
||||
net.fortuna.ical4j.model.Calendar ical = new net.fortuna.ical4j.model.Calendar();
|
||||
ical.getProperties().add(Version.VERSION_2_0);
|
||||
ical.getProperties().add(Constants.ICAL_PRODID);
|
||||
|
||||
// "master event" (without exceptions)
|
||||
ComponentList components = ical.getComponents();
|
||||
VEvent master = toVEvent(new Uid(uid));
|
||||
components.add(master);
|
||||
|
||||
// remember used time zones
|
||||
Set<net.fortuna.ical4j.model.TimeZone> usedTimeZones = new HashSet<>();
|
||||
if (dtStart != null && dtStart.getTimeZone() != null)
|
||||
usedTimeZones.add(dtStart.getTimeZone());
|
||||
if (dtEnd != null && dtEnd.getTimeZone() != null)
|
||||
usedTimeZones.add(dtEnd.getTimeZone());
|
||||
|
||||
// recurrence exceptions
|
||||
for (Event exception : exceptions) {
|
||||
// create VEVENT for exception
|
||||
VEvent vException = exception.toVEvent(master.getUid());
|
||||
|
||||
components.add(vException);
|
||||
|
||||
// remember used time zones
|
||||
if (exception.dtStart != null && exception.dtStart.getTimeZone() != null)
|
||||
usedTimeZones.add(exception.dtStart.getTimeZone());
|
||||
if (exception.dtEnd != null && exception.dtEnd.getTimeZone() != null)
|
||||
usedTimeZones.add(exception.dtEnd.getTimeZone());
|
||||
}
|
||||
|
||||
// add VTIMEZONE components
|
||||
for (net.fortuna.ical4j.model.TimeZone timeZone : usedTimeZones)
|
||||
ical.getComponents().add(timeZone.getVTimeZone());
|
||||
|
||||
CalendarOutputter output = new CalendarOutputter(false);
|
||||
ByteArrayOutputStream os = new ByteArrayOutputStream();
|
||||
try {
|
||||
output.output(ical, os);
|
||||
} catch (ValidationException e) {
|
||||
Log.e(TAG, "Generated invalid iCalendar");
|
||||
}
|
||||
return os;
|
||||
}
|
||||
|
||||
protected VEvent toVEvent(Uid uid) {
|
||||
VEvent event = new VEvent();
|
||||
PropertyList props = event.getProperties();
|
||||
|
||||
if (uid != null)
|
||||
props.add(uid);
|
||||
if (recurrenceId != null)
|
||||
props.add(recurrenceId);
|
||||
|
||||
props.add(dtStart);
|
||||
if (dtEnd != null)
|
||||
props.add(dtEnd);
|
||||
if (duration != null)
|
||||
props.add(duration);
|
||||
|
||||
if (rrule != null)
|
||||
props.add(rrule);
|
||||
for (RDate rdate : rdates)
|
||||
props.add(rdate);
|
||||
if (exrule != null)
|
||||
props.add(exrule);
|
||||
for (ExDate exdate : exdates)
|
||||
props.add(exdate);
|
||||
|
||||
if (summary != null && !summary.isEmpty())
|
||||
props.add(new Summary(summary));
|
||||
if (location != null && !location.isEmpty())
|
||||
props.add(new Location(location));
|
||||
if (description != null && !description.isEmpty())
|
||||
props.add(new Description(description));
|
||||
|
||||
if (status != null)
|
||||
props.add(status);
|
||||
if (!opaque)
|
||||
props.add(Transp.TRANSPARENT);
|
||||
|
||||
if (organizer != null)
|
||||
props.add(organizer);
|
||||
props.addAll(attendees);
|
||||
|
||||
if (forPublic != null)
|
||||
event.getProperties().add(forPublic ? Clazz.PUBLIC : Clazz.PRIVATE);
|
||||
|
||||
event.getAlarms().addAll(alarms);
|
||||
|
||||
props.add(new LastModified());
|
||||
return event;
|
||||
}
|
||||
|
||||
|
||||
// time helpers
|
||||
|
||||
/**
|
||||
* Returns the time-zone ID for a given date-time, or TIMEZONE_UTC for dates (without time).
|
||||
* TIMEZONE_UTC is also returned for DATE-TIMEs in UTC representation.
|
||||
* @param date DateProperty (DATE or DATE-TIME) whose time-zone information is used
|
||||
*/
|
||||
protected static String getTzId(DateProperty date) {
|
||||
if (isDateTime(date) && !date.isUtc() && date.getTimeZone() != null)
|
||||
return date.getTimeZone().getID();
|
||||
else
|
||||
return TimeZones.UTC_ID;
|
||||
}
|
||||
|
||||
public boolean isAllDay() {
|
||||
return !isDateTime(dtStart);
|
||||
}
|
||||
|
||||
public long getDtStartInMillis() {
|
||||
return dtStart.getDate().getTime();
|
||||
}
|
||||
|
||||
public String getDtStartTzID() {
|
||||
return getTzId(dtStart);
|
||||
}
|
||||
|
||||
public void setDtStart(long tsStart, String tzID) {
|
||||
if (tzID == null) { // all-day
|
||||
dtStart = new DtStart(new Date(tsStart));
|
||||
} else {
|
||||
DateTime start = new DateTime(tsStart);
|
||||
start.setTimeZone(DateUtils.tzRegistry.getTimeZone(tzID));
|
||||
dtStart = new DtStart(start);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public long getDtEndInMillis() {
|
||||
return dtEnd.getDate().getTime();
|
||||
}
|
||||
|
||||
public String getDtEndTzID() {
|
||||
return getTzId(dtEnd);
|
||||
}
|
||||
|
||||
public void setDtEnd(long tsEnd, String tzID) {
|
||||
if (tzID == null) { // all-day
|
||||
dtEnd = new DtEnd(new Date(tsEnd));
|
||||
} else {
|
||||
DateTime end = new DateTime(tsEnd);
|
||||
end.setTimeZone(DateUtils.tzRegistry.getTimeZone(tzID));
|
||||
dtEnd = new DtEnd(end);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,20 +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;
|
||||
|
||||
public class InvalidResourceException extends Exception {
|
||||
private static final long serialVersionUID = 1593585432655578220L;
|
||||
|
||||
public InvalidResourceException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public InvalidResourceException(Throwable throwable) {
|
||||
super(throwable);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -5,755 +5,262 @@
|
||||
* 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.SuppressLint;
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.ContentProviderClient;
|
||||
import android.content.ContentProviderOperation;
|
||||
import android.content.ContentProviderOperation.Builder;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.ContentUris;
|
||||
import android.content.ContentValues;
|
||||
import android.content.Entity;
|
||||
import android.content.EntityIterator;
|
||||
import android.database.Cursor;
|
||||
import android.database.DatabaseUtils;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.RemoteException;
|
||||
import android.provider.CalendarContract;
|
||||
import android.provider.CalendarContract.Attendees;
|
||||
import android.provider.CalendarContract.Calendars;
|
||||
import android.provider.CalendarContract.Events;
|
||||
import android.provider.CalendarContract.Reminders;
|
||||
import android.util.Log;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import net.fortuna.ical4j.model.Date;
|
||||
import net.fortuna.ical4j.model.DateTime;
|
||||
import net.fortuna.ical4j.model.Dur;
|
||||
import net.fortuna.ical4j.model.Parameter;
|
||||
import net.fortuna.ical4j.model.ParameterList;
|
||||
import net.fortuna.ical4j.model.PropertyList;
|
||||
import net.fortuna.ical4j.model.component.VAlarm;
|
||||
import net.fortuna.ical4j.model.parameter.Cn;
|
||||
import net.fortuna.ical4j.model.parameter.CuType;
|
||||
import net.fortuna.ical4j.model.parameter.PartStat;
|
||||
import net.fortuna.ical4j.model.parameter.Role;
|
||||
import net.fortuna.ical4j.model.property.Action;
|
||||
import net.fortuna.ical4j.model.property.Attendee;
|
||||
import net.fortuna.ical4j.model.property.Description;
|
||||
import net.fortuna.ical4j.model.property.Duration;
|
||||
import net.fortuna.ical4j.model.property.ExDate;
|
||||
import net.fortuna.ical4j.model.property.ExRule;
|
||||
import net.fortuna.ical4j.model.property.Organizer;
|
||||
import net.fortuna.ical4j.model.property.RDate;
|
||||
import net.fortuna.ical4j.model.property.RRule;
|
||||
import net.fortuna.ical4j.model.property.RecurrenceId;
|
||||
import net.fortuna.ical4j.model.property.Status;
|
||||
import net.fortuna.ical4j.model.component.VTimeZone;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.text.ParseException;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
import at.bitfire.davdroid.DAVUtils;
|
||||
import at.bitfire.davdroid.DateUtils;
|
||||
import at.bitfire.davdroid.webdav.WebDavResource;
|
||||
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;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* Represents a locally stored calendar, containing Events.
|
||||
* Communicates with the Android Contacts Provider which uses an SQLite
|
||||
* database to store the contacts.
|
||||
*/
|
||||
public class LocalCalendar extends LocalCollection<Event> {
|
||||
private static final String TAG = "davdroid.LocalCalendar";
|
||||
|
||||
@Getter protected String url;
|
||||
@Getter protected long id;
|
||||
|
||||
protected static final String COLLECTION_COLUMN_CTAG = Calendars.CAL_SYNC1;
|
||||
|
||||
|
||||
/* database fields */
|
||||
|
||||
@Override protected Uri entriesURI() { return syncAdapterURI(Events.CONTENT_URI); }
|
||||
@Override protected String entryColumnAccountType() { return Events.ACCOUNT_TYPE; }
|
||||
@Override protected String entryColumnAccountName() { return Events.ACCOUNT_NAME; }
|
||||
@Override protected String entryColumnParentID() { return Events.CALENDAR_ID; }
|
||||
@Override protected String entryColumnID() { return Events._ID; }
|
||||
@Override protected String entryColumnRemoteName() { return Events._SYNC_ID; }
|
||||
@Override protected String entryColumnETag() { return Events.SYNC_DATA1; }
|
||||
@Override protected String entryColumnDirty() { return Events.DIRTY; }
|
||||
@Override protected String entryColumnDeleted() { return Events.DELETED; }
|
||||
|
||||
@Override
|
||||
@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
|
||||
protected String entryColumnUID() {
|
||||
return (android.os.Build.VERSION.SDK_INT >= 17) ?
|
||||
Events.UID_2445 : Events.SYNC_DATA2;
|
||||
}
|
||||
|
||||
|
||||
/* class methods, constructor */
|
||||
|
||||
@SuppressLint("InlinedApi")
|
||||
public static Uri create(Account account, ContentResolver resolver, ServerInfo.ResourceInfo info) throws LocalStorageException {
|
||||
@Cleanup("release") final ContentProviderClient client = resolver.acquireContentProviderClient(CalendarContract.AUTHORITY);
|
||||
if (client == null)
|
||||
throw new LocalStorageException("No Calendar Provider found (Calendar app disabled?)");
|
||||
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(Calendars.ACCOUNT_NAME, account.name);
|
||||
values.put(Calendars.ACCOUNT_TYPE, account.type);
|
||||
values.put(Calendars.NAME, info.getURL());
|
||||
values.put(Calendars.CALENDAR_DISPLAY_NAME, info.getTitle());
|
||||
values.put(Calendars.CALENDAR_COLOR, info.getColor() != null ? info.getColor() : DAVUtils.calendarGreen);
|
||||
values.put(Calendars.OWNER_ACCOUNT, account.name);
|
||||
values.put(Calendars.SYNC_EVENTS, 1);
|
||||
values.put(Calendars.VISIBLE, 1);
|
||||
values.put(Calendars.ALLOWED_REMINDERS, Reminders.METHOD_ALERT);
|
||||
|
||||
if (info.isReadOnly())
|
||||
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_ORGANIZER_RESPOND, 1);
|
||||
values.put(Calendars.CAN_MODIFY_TIME_ZONE, 1);
|
||||
}
|
||||
|
||||
if (android.os.Build.VERSION.SDK_INT >= 15) {
|
||||
values.put(Calendars.ALLOWED_AVAILABILITY, Events.AVAILABILITY_BUSY + "," + Events.AVAILABILITY_FREE + "," + Events.AVAILABILITY_TENTATIVE);
|
||||
values.put(Calendars.ALLOWED_ATTENDEE_TYPES, Attendees.TYPE_NONE + "," + Attendees.TYPE_OPTIONAL + "," + Attendees.TYPE_REQUIRED + "," + Attendees.TYPE_RESOURCE);
|
||||
}
|
||||
|
||||
if (info.getTimezone() != null)
|
||||
values.put(Calendars.CALENDAR_TIME_ZONE, DateUtils.findAndroidTimezoneID(info.getTimezone()));
|
||||
|
||||
Log.i(TAG, "Inserting calendar: " + values.toString());
|
||||
try {
|
||||
return client.insert(calendarsURI(account), values);
|
||||
} catch (RemoteException e) {
|
||||
throw new LocalStorageException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static LocalCalendar[] findAll(Account account, ContentProviderClient providerClient) throws RemoteException {
|
||||
@Cleanup Cursor cursor = providerClient.query(calendarsURI(account),
|
||||
new String[] { Calendars._ID, Calendars.NAME },
|
||||
Calendars.DELETED + "=0 AND " + Calendars.SYNC_EVENTS + "=1", null, null);
|
||||
|
||||
LinkedList<LocalCalendar> calendars = new LinkedList<>();
|
||||
while (cursor != null && cursor.moveToNext())
|
||||
calendars.add(new LocalCalendar(account, providerClient, cursor.getInt(0), cursor.getString(1)));
|
||||
return calendars.toArray(new LocalCalendar[calendars.size()]);
|
||||
}
|
||||
|
||||
public LocalCalendar(Account account, ContentProviderClient providerClient, long id, String url) {
|
||||
super(account, providerClient);
|
||||
this.id = id;
|
||||
this.url = url;
|
||||
sqlFilter = "ORIGINAL_ID IS NULL";
|
||||
}
|
||||
|
||||
|
||||
/* collection operations */
|
||||
|
||||
@Override
|
||||
public String getCTag() throws LocalStorageException {
|
||||
try {
|
||||
@Cleanup Cursor c = providerClient.query(ContentUris.withAppendedId(calendarsURI(), id),
|
||||
new String[] { COLLECTION_COLUMN_CTAG }, null, null, null);
|
||||
if (c != null && c.moveToFirst())
|
||||
return c.getString(0);
|
||||
else
|
||||
throw new LocalStorageException("Couldn't query calendar CTag");
|
||||
} catch(RemoteException e) {
|
||||
throw new LocalStorageException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setCTag(String cTag) throws LocalStorageException {
|
||||
ContentValues values = new ContentValues(1);
|
||||
values.put(COLLECTION_COLUMN_CTAG, cTag);
|
||||
try {
|
||||
providerClient.update(ContentUris.withAppendedId(calendarsURI(), id), values, null, null);
|
||||
} catch(RemoteException e) {
|
||||
throw new LocalStorageException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateMetaData(WebDavResource.Properties properties) throws LocalStorageException {
|
||||
ContentValues values = new ContentValues();
|
||||
|
||||
final String displayName = properties.getDisplayName();
|
||||
if (displayName != null)
|
||||
values.put(Calendars.CALENDAR_DISPLAY_NAME, displayName);
|
||||
|
||||
final Integer color = properties.getColor();
|
||||
if (color != null)
|
||||
values.put(Calendars.CALENDAR_COLOR, color);
|
||||
|
||||
try {
|
||||
if (values.size() > 0)
|
||||
providerClient.update(ContentUris.withAppendedId(calendarsURI(), id), values, null, null);
|
||||
} catch(RemoteException e) {
|
||||
throw new LocalStorageException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public long[] findUpdated() throws LocalStorageException {
|
||||
// mark (recurring) events with changed/deleted exceptions as dirty
|
||||
String where = entryColumnID() + " IN (SELECT DISTINCT " + Events.ORIGINAL_ID + " FROM events WHERE " +
|
||||
Events.ORIGINAL_ID + " IS NOT NULL AND (" + Events.DIRTY + "=1 OR " + Events.DELETED + "=1))";
|
||||
ContentValues dirty = new ContentValues(1);
|
||||
dirty.put(CalendarContract.Events.DIRTY, 1);
|
||||
try {
|
||||
int rows = providerClient.update(entriesURI(), dirty, where, null);
|
||||
if (rows > 0)
|
||||
Log.d(TAG, rows + " event(s) marked as dirty because of dirty/deleted exceptions");
|
||||
} catch (RemoteException e) {
|
||||
Log.e(TAG, "Couldn't mark events with updated exceptions as dirty", e);
|
||||
}
|
||||
|
||||
// new find and return updated (master) events
|
||||
return super.findUpdated();
|
||||
}
|
||||
|
||||
|
||||
/* create/update/delete */
|
||||
|
||||
public Event newResource(long localID, String resourceName, String eTag) {
|
||||
return new Event(localID, resourceName, eTag);
|
||||
}
|
||||
|
||||
public int deleteAllExceptRemoteNames(Resource[] remoteResources) throws LocalStorageException {
|
||||
List<String> sqlFileNames = new LinkedList<>();
|
||||
for (Resource res : remoteResources)
|
||||
sqlFileNames.add(DatabaseUtils.sqlEscapeString(res.getName()));
|
||||
|
||||
// delete master events
|
||||
String where = entryColumnParentID() + "=?";
|
||||
where += sqlFileNames.isEmpty() ?
|
||||
" AND " + entryColumnRemoteName() + " IS NOT NULL" : // don't retain anything (delete all)
|
||||
" AND " + entryColumnRemoteName() + " NOT IN (" + StringUtils.join(sqlFileNames, ",") + ")"; // retain by remote file name
|
||||
if (sqlFilter != null)
|
||||
where += " AND (" + sqlFilter + ")";
|
||||
pendingOperations.add(ContentProviderOperation.newDelete(entriesURI())
|
||||
.withSelection(where, new String[] { String.valueOf(id) })
|
||||
.build());
|
||||
|
||||
// delete exceptions, too
|
||||
where = entryColumnParentID() + "=?";
|
||||
where += sqlFileNames.isEmpty() ?
|
||||
" AND " + Events.ORIGINAL_SYNC_ID + " IS NOT NULL" : // don't retain anything (delete all)
|
||||
" AND " + Events.ORIGINAL_SYNC_ID + " NOT IN (" + StringUtils.join(sqlFileNames, ",") + ")"; // retain by remote file name
|
||||
pendingOperations.add(ContentProviderOperation
|
||||
.newDelete(entriesURI())
|
||||
.withSelection(where, new String[]{String.valueOf(id)})
|
||||
.withYieldAllowed(true)
|
||||
.build()
|
||||
);
|
||||
return commit();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(Resource resource) {
|
||||
super.delete(resource);
|
||||
|
||||
// delete all exceptions of this event, too
|
||||
pendingOperations.add(ContentProviderOperation
|
||||
.newDelete(entriesURI())
|
||||
.withSelection(Events.ORIGINAL_ID + "=?", new String[] { String.valueOf(resource.getLocalID()) })
|
||||
.build()
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearDirty(Resource resource) {
|
||||
super.clearDirty(resource);
|
||||
|
||||
// clear dirty flag of all exceptions of this event
|
||||
pendingOperations.add(ContentProviderOperation
|
||||
.newUpdate(entriesURI())
|
||||
.withValue(Events.DIRTY, 0)
|
||||
.withSelection(Events.ORIGINAL_ID + "=?", new String[]{String.valueOf(resource.getLocalID())})
|
||||
.build()
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/* methods for populating the data object from the content provider */
|
||||
|
||||
@Override
|
||||
public void populate(Resource resource) throws LocalStorageException {
|
||||
Event event = (Event)resource;
|
||||
|
||||
try {
|
||||
@Cleanup EntityIterator iterEvents = CalendarContract.EventsEntity.newEntityIterator(
|
||||
providerClient.query(
|
||||
syncAdapterURI(CalendarContract.EventsEntity.CONTENT_URI),
|
||||
null, Events._ID + "=" + event.getLocalID(),
|
||||
null, null),
|
||||
providerClient
|
||||
);
|
||||
while (iterEvents.hasNext()) {
|
||||
Entity e = iterEvents.next();
|
||||
|
||||
ContentValues values = e.getEntityValues();
|
||||
populateEvent(event, values);
|
||||
|
||||
List<Entity.NamedContentValues> subValues = e.getSubValues();
|
||||
for (Entity.NamedContentValues subValue : subValues) {
|
||||
values = subValue.values;
|
||||
if (Attendees.CONTENT_URI.equals(subValue.uri))
|
||||
populateAttendee(event, values);
|
||||
if (Reminders.CONTENT_URI.equals(subValue.uri))
|
||||
populateReminder(event, values);
|
||||
}
|
||||
|
||||
populateExceptions(event);
|
||||
}
|
||||
} catch (RemoteException ex) {
|
||||
throw new LocalStorageException("Couldn't process locally stored event", ex);
|
||||
}
|
||||
}
|
||||
|
||||
protected void populateEvent(Event e, ContentValues values) {
|
||||
e.setUid(values.getAsString(entryColumnUID()));
|
||||
|
||||
e.setSummary(values.getAsString(Events.TITLE));
|
||||
e.setLocation(values.getAsString(Events.EVENT_LOCATION));
|
||||
e.setDescription(values.getAsString(Events.DESCRIPTION));
|
||||
|
||||
final boolean allDay = values.getAsInteger(Events.ALL_DAY) != 0;
|
||||
final long tsStart = values.getAsLong(Events.DTSTART);
|
||||
final String duration = values.getAsString(Events.DURATION);
|
||||
|
||||
String tzId;
|
||||
Long tsEnd = values.getAsLong(Events.DTEND);
|
||||
if (allDay) {
|
||||
e.setDtStart(tsStart, null);
|
||||
if (tsEnd == null) {
|
||||
Dur dur = new Dur(duration);
|
||||
java.util.Date dEnd = dur.getTime(new java.util.Date(tsStart));
|
||||
tsEnd = dEnd.getTime();
|
||||
}
|
||||
e.setDtEnd(tsEnd, null);
|
||||
|
||||
} else {
|
||||
// use the start time zone for the end time, too
|
||||
// because apps like Samsung Planner allow the user to change "the" time zone but change the start time zone only
|
||||
tzId = values.getAsString(Events.EVENT_TIMEZONE);
|
||||
e.setDtStart(tsStart, tzId);
|
||||
if (tsEnd != null)
|
||||
e.setDtEnd(tsEnd, tzId);
|
||||
else if (!StringUtils.isEmpty(duration))
|
||||
e.setDuration(new Duration(new Dur(duration)));
|
||||
}
|
||||
|
||||
// recurrence
|
||||
try {
|
||||
String strRRule = values.getAsString(Events.RRULE);
|
||||
if (!StringUtils.isEmpty(strRRule))
|
||||
e.setRrule(new RRule(strRRule));
|
||||
|
||||
String strRDate = values.getAsString(Events.RDATE);
|
||||
if (!StringUtils.isEmpty(strRDate)) {
|
||||
RDate rDate = (RDate)DateUtils.androidStringToRecurrenceSet(strRDate, RDate.class, allDay);
|
||||
e.getRdates().add(rDate);
|
||||
}
|
||||
|
||||
String strExRule = values.getAsString(Events.EXRULE);
|
||||
if (!StringUtils.isEmpty(strExRule)) {
|
||||
ExRule exRule = new ExRule();
|
||||
exRule.setValue(strExRule);
|
||||
e.setExrule(exRule);
|
||||
}
|
||||
|
||||
String strExDate = values.getAsString(Events.EXDATE);
|
||||
if (!StringUtils.isEmpty(strExDate)) {
|
||||
ExDate exDate = (ExDate)DateUtils.androidStringToRecurrenceSet(strExDate, ExDate.class, allDay);
|
||||
e.getExdates().add(exDate);
|
||||
}
|
||||
} catch (ParseException ex) {
|
||||
Log.w(TAG, "Couldn't parse recurrence rules, ignoring", ex);
|
||||
} catch (IllegalArgumentException ex) {
|
||||
Log.w(TAG, "Invalid recurrence rules, ignoring", ex);
|
||||
}
|
||||
|
||||
if (values.containsKey(Events.ORIGINAL_INSTANCE_TIME)) {
|
||||
// this event is an exception of a recurring event
|
||||
long originalInstanceTime = values.getAsLong(Events.ORIGINAL_INSTANCE_TIME);
|
||||
|
||||
boolean originalAllDay = false;
|
||||
if (values.containsKey(Events.ORIGINAL_ALL_DAY))
|
||||
originalAllDay = values.getAsInteger(Events.ORIGINAL_ALL_DAY) != 0;
|
||||
|
||||
Date originalDate = originalAllDay ?
|
||||
new Date(originalInstanceTime) :
|
||||
new DateTime(originalInstanceTime);
|
||||
if (originalDate instanceof DateTime)
|
||||
((DateTime)originalDate).setUtc(true);
|
||||
e.setRecurrenceId(new RecurrenceId(originalDate));
|
||||
}
|
||||
|
||||
// status
|
||||
if (values.containsKey(Events.STATUS))
|
||||
switch (values.getAsInteger(Events.STATUS)) {
|
||||
case Events.STATUS_CONFIRMED:
|
||||
e.setStatus(Status.VEVENT_CONFIRMED);
|
||||
break;
|
||||
case Events.STATUS_TENTATIVE:
|
||||
e.setStatus(Status.VEVENT_TENTATIVE);
|
||||
break;
|
||||
case Events.STATUS_CANCELED:
|
||||
e.setStatus(Status.VEVENT_CANCELLED);
|
||||
}
|
||||
|
||||
// availability
|
||||
e.setOpaque(values.getAsInteger(Events.AVAILABILITY) != Events.AVAILABILITY_FREE);
|
||||
|
||||
// set ORGANIZER only when there are attendees
|
||||
if (values.getAsInteger(Events.HAS_ATTENDEE_DATA) != 0 && values.containsKey(Events.ORGANIZER))
|
||||
try {
|
||||
e.setOrganizer(new Organizer(new URI("mailto", values.getAsString(Events.ORGANIZER), null)));
|
||||
} catch (URISyntaxException ex) {
|
||||
Log.e(TAG, "Error when creating ORGANIZER URI, ignoring", ex);
|
||||
}
|
||||
|
||||
// classification
|
||||
switch (values.getAsInteger(Events.ACCESS_LEVEL)) {
|
||||
case Events.ACCESS_CONFIDENTIAL:
|
||||
case Events.ACCESS_PRIVATE:
|
||||
e.setForPublic(false);
|
||||
break;
|
||||
case Events.ACCESS_PUBLIC:
|
||||
e.setForPublic(true);
|
||||
}
|
||||
}
|
||||
|
||||
void populateExceptions(Event e) throws RemoteException {
|
||||
@Cleanup Cursor c = providerClient.query(syncAdapterURI(Events.CONTENT_URI),
|
||||
new String[]{Events._ID, entryColumnRemoteName()},
|
||||
Events.ORIGINAL_ID + "=?", new String[]{ String.valueOf(e.getLocalID()) }, null);
|
||||
while (c != null && c.moveToNext()) {
|
||||
long exceptionId = c.getLong(0);
|
||||
String exceptionRemoteName = c.getString(1);
|
||||
try {
|
||||
Event exception = new Event(exceptionId, exceptionRemoteName, null);
|
||||
populate(exception);
|
||||
e.getExceptions().add(exception);
|
||||
} catch (LocalStorageException ex) {
|
||||
Log.e(TAG, "Couldn't find exception details, ignoring");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void populateAttendee(Event event, ContentValues values) {
|
||||
try {
|
||||
Attendee attendee = new Attendee(new URI("mailto", values.getAsString(Attendees.ATTENDEE_EMAIL), null));
|
||||
ParameterList params = attendee.getParameters();
|
||||
|
||||
String cn = values.getAsString(Attendees.ATTENDEE_NAME);
|
||||
if (cn != null)
|
||||
params.add(new Cn(cn));
|
||||
|
||||
// type
|
||||
int type = values.getAsInteger(Attendees.ATTENDEE_TYPE);
|
||||
params.add((type == Attendees.TYPE_RESOURCE) ? CuType.RESOURCE : CuType.INDIVIDUAL);
|
||||
|
||||
// role
|
||||
int relationship = values.getAsInteger(Attendees.ATTENDEE_RELATIONSHIP);
|
||||
switch (relationship) {
|
||||
case Attendees.RELATIONSHIP_ORGANIZER:
|
||||
params.add(Role.CHAIR);
|
||||
break;
|
||||
case Attendees.RELATIONSHIP_ATTENDEE:
|
||||
case Attendees.RELATIONSHIP_PERFORMER:
|
||||
case Attendees.RELATIONSHIP_SPEAKER:
|
||||
params.add((type == Attendees.TYPE_REQUIRED) ? Role.REQ_PARTICIPANT : Role.OPT_PARTICIPANT);
|
||||
break;
|
||||
case Attendees.RELATIONSHIP_NONE:
|
||||
params.add(Role.NON_PARTICIPANT);
|
||||
}
|
||||
|
||||
// status
|
||||
switch (values.getAsInteger(Attendees.ATTENDEE_STATUS)) {
|
||||
case Attendees.ATTENDEE_STATUS_INVITED:
|
||||
params.add(PartStat.NEEDS_ACTION);
|
||||
break;
|
||||
case Attendees.ATTENDEE_STATUS_ACCEPTED:
|
||||
params.add(PartStat.ACCEPTED);
|
||||
break;
|
||||
case Attendees.ATTENDEE_STATUS_DECLINED:
|
||||
params.add(PartStat.DECLINED);
|
||||
break;
|
||||
case Attendees.ATTENDEE_STATUS_TENTATIVE:
|
||||
params.add(PartStat.TENTATIVE);
|
||||
break;
|
||||
}
|
||||
|
||||
event.getAttendees().add(attendee);
|
||||
} catch (URISyntaxException ex) {
|
||||
Log.e(TAG, "Couldn't parse attendee information, ignoring", ex);
|
||||
}
|
||||
}
|
||||
|
||||
void populateReminder(Event event, ContentValues row) {
|
||||
VAlarm alarm = new VAlarm(new Dur(0, 0, -row.getAsInteger(Reminders.MINUTES), 0));
|
||||
|
||||
PropertyList props = alarm.getProperties();
|
||||
props.add(Action.DISPLAY);
|
||||
props.add(new Description(event.getSummary()));
|
||||
event.getAlarms().add(alarm);
|
||||
}
|
||||
|
||||
|
||||
/* content builder methods */
|
||||
|
||||
@Override
|
||||
protected Builder buildEntry(Builder builder, Resource resource, boolean update) {
|
||||
final Event event = (Event)resource;
|
||||
|
||||
builder .withValue(Events.CALENDAR_ID, id)
|
||||
.withValue(Events.ALL_DAY, event.isAllDay() ? 1 : 0)
|
||||
.withValue(Events.DTSTART, event.getDtStartInMillis())
|
||||
.withValue(Events.EVENT_TIMEZONE, event.getDtStartTzID())
|
||||
.withValue(Events.HAS_ALARM, event.getAlarms().isEmpty() ? 0 : 1)
|
||||
.withValue(Events.HAS_ATTENDEE_DATA, event.getAttendees().isEmpty() ? 0 : 1)
|
||||
.withValue(Events.GUESTS_CAN_INVITE_OTHERS, 1)
|
||||
.withValue(Events.GUESTS_CAN_MODIFY, 1)
|
||||
.withValue(Events.GUESTS_CAN_SEE_GUESTS, 1);
|
||||
|
||||
final RecurrenceId recurrenceId = event.getRecurrenceId();
|
||||
if (recurrenceId == null) {
|
||||
// this event is a "master event" (not an exception)
|
||||
builder .withValue(entryColumnRemoteName(), event.getName())
|
||||
.withValue(entryColumnETag(), event.getETag())
|
||||
.withValue(entryColumnUID(), event.getUid());
|
||||
} else {
|
||||
builder.withValue(Events.ORIGINAL_SYNC_ID, event.getName());
|
||||
|
||||
// ORIGINAL_INSTANCE_TIME and ORIGINAL_ALL_DAY is set in buildExceptions.
|
||||
// It's not possible to use only the RECURRENCE-ID to calculate
|
||||
// ORIGINAL_INSTANCE_TIME and ORIGINAL_ALL_DAY because iCloud sends DATE-TIME
|
||||
// RECURRENCE-IDs even if the original event is an all-day event.
|
||||
}
|
||||
|
||||
boolean recurring = false;
|
||||
if (event.getRrule() != null) {
|
||||
recurring = true;
|
||||
builder.withValue(Events.RRULE, event.getRrule().getValue());
|
||||
}
|
||||
if (!event.getRdates().isEmpty()) {
|
||||
recurring = true;
|
||||
try {
|
||||
builder.withValue(Events.RDATE, DateUtils.recurrenceSetsToAndroidString(event.getRdates(), event.isAllDay()));
|
||||
} catch (ParseException e) {
|
||||
Log.e(TAG, "Couldn't parse RDate(s)", e);
|
||||
}
|
||||
}
|
||||
if (event.getExrule() != null)
|
||||
builder.withValue(Events.EXRULE, event.getExrule().getValue());
|
||||
if (!event.getExdates().isEmpty())
|
||||
try {
|
||||
builder.withValue(Events.EXDATE, DateUtils.recurrenceSetsToAndroidString(event.getExdates(), event.isAllDay()));
|
||||
} catch (ParseException e) {
|
||||
Log.e(TAG, "Couldn't parse ExDate(s)", e);
|
||||
}
|
||||
|
||||
// set either DTEND for single-time events or DURATION for recurring events
|
||||
// because that's the way Android likes it (see docs)
|
||||
if (recurring) {
|
||||
// calculate DURATION from start and end date
|
||||
Duration duration = new Duration(event.getDtStart().getDate(), event.getDtEnd().getDate());
|
||||
builder.withValue(Events.DURATION, duration.getValue());
|
||||
} else
|
||||
builder .withValue(Events.DTEND, event.getDtEndInMillis())
|
||||
.withValue(Events.EVENT_END_TIMEZONE, event.getDtEndTzID());
|
||||
|
||||
if (event.getSummary() != null)
|
||||
builder.withValue(Events.TITLE, event.getSummary());
|
||||
if (event.getLocation() != null)
|
||||
builder.withValue(Events.EVENT_LOCATION, event.getLocation());
|
||||
if (event.getDescription() != null)
|
||||
builder.withValue(Events.DESCRIPTION, event.getDescription());
|
||||
|
||||
if (event.getOrganizer() != null && event.getOrganizer().getCalAddress() != null) {
|
||||
URI organizer = event.getOrganizer().getCalAddress();
|
||||
if (organizer.getScheme() != null && organizer.getScheme().equalsIgnoreCase("mailto"))
|
||||
builder.withValue(Events.ORGANIZER, organizer.getSchemeSpecificPart());
|
||||
}
|
||||
|
||||
Status status = event.getStatus();
|
||||
if (status != null) {
|
||||
int statusCode = Events.STATUS_TENTATIVE;
|
||||
if (status == Status.VEVENT_CONFIRMED)
|
||||
statusCode = Events.STATUS_CONFIRMED;
|
||||
else if (status == Status.VEVENT_CANCELLED)
|
||||
statusCode = Events.STATUS_CANCELED;
|
||||
builder.withValue(Events.STATUS, statusCode);
|
||||
}
|
||||
|
||||
builder.withValue(Events.AVAILABILITY, event.isOpaque() ? Events.AVAILABILITY_BUSY : Events.AVAILABILITY_FREE);
|
||||
|
||||
if (event.getForPublic() != null)
|
||||
builder.withValue(Events.ACCESS_LEVEL, event.getForPublic() ? Events.ACCESS_PUBLIC : Events.ACCESS_PRIVATE);
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected void addDataRows(Resource resource, long localID, int backrefIdx) {
|
||||
final Event event = (Event)resource;
|
||||
|
||||
// add exceptions
|
||||
for (Event exception : event.getExceptions())
|
||||
pendingOperations.add(buildException(newDataInsertBuilder(Events.CONTENT_URI, Events.ORIGINAL_ID, localID, backrefIdx), event, exception).build());
|
||||
// add attendees
|
||||
for (Attendee attendee : event.getAttendees())
|
||||
pendingOperations.add(buildAttendee(newDataInsertBuilder(Attendees.CONTENT_URI, Attendees.EVENT_ID, localID, backrefIdx), attendee).build());
|
||||
// add reminders
|
||||
for (VAlarm alarm : event.getAlarms())
|
||||
pendingOperations.add(buildReminder(newDataInsertBuilder(Reminders.CONTENT_URI, Reminders.EVENT_ID, localID, backrefIdx), alarm).build());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void removeDataRows(Resource resource) {
|
||||
final Event event = (Event)resource;
|
||||
|
||||
// delete exceptions
|
||||
pendingOperations.add(ContentProviderOperation.newDelete(syncAdapterURI(Events.CONTENT_URI))
|
||||
.withSelection(Events.ORIGINAL_ID + "=?", new String[] { String.valueOf(event.getLocalID())}).build());
|
||||
// delete attendees
|
||||
pendingOperations.add(ContentProviderOperation.newDelete(syncAdapterURI(Attendees.CONTENT_URI))
|
||||
.withSelection(Attendees.EVENT_ID + "=?", new String[]{String.valueOf(event.getLocalID())}).build());
|
||||
// delete reminders
|
||||
pendingOperations.add(ContentProviderOperation.newDelete(syncAdapterURI(Reminders.CONTENT_URI))
|
||||
.withSelection(Reminders.EVENT_ID + "=?", new String[]{String.valueOf(event.getLocalID())}).build());
|
||||
}
|
||||
|
||||
|
||||
protected Builder buildException(Builder builder, Event master, Event exception) {
|
||||
buildEntry(builder, exception, false);
|
||||
builder.withValue(Events.ORIGINAL_SYNC_ID, exception.getName());
|
||||
|
||||
// Some servers (iCloud, for instance) return RECURRENCE-ID with DATE-TIME even if
|
||||
// the original event is an all-day event. Workaround: determine value of ORIGINAL_ALL_DAY
|
||||
// by original event type (all-day or not) and not by whether RECURRENCE-ID is DATE or DATE-TIME.
|
||||
|
||||
final RecurrenceId recurrenceId = exception.getRecurrenceId();
|
||||
final boolean originalAllDay = master.isAllDay();
|
||||
|
||||
Date date = recurrenceId.getDate();
|
||||
if (originalAllDay && date instanceof DateTime) {
|
||||
String value = recurrenceId.getValue();
|
||||
if (value.matches("^\\d{8}T\\d{6}$"))
|
||||
try {
|
||||
// no "Z" at the end indicates "local" time
|
||||
// so this is a "local" time, but it should be a ical4j Date without time
|
||||
date = new Date(value.substring(0, 8));
|
||||
} catch (ParseException e) {
|
||||
Log.e(TAG, "Couldn't parse DATE part of DATE-TIME RECURRENCE-ID", e);
|
||||
}
|
||||
}
|
||||
|
||||
builder.withValue(Events.ORIGINAL_INSTANCE_TIME, date.getTime());
|
||||
builder.withValue(Events.ORIGINAL_ALL_DAY, originalAllDay ? 1 : 0);
|
||||
return builder;
|
||||
}
|
||||
|
||||
@SuppressLint("InlinedApi")
|
||||
protected Builder buildAttendee(Builder builder, Attendee attendee) {
|
||||
final Uri member = Uri.parse(attendee.getValue());
|
||||
final String email = member.getSchemeSpecificPart();
|
||||
|
||||
final Cn cn = (Cn)attendee.getParameter(Parameter.CN);
|
||||
if (cn != null)
|
||||
builder.withValue(Attendees.ATTENDEE_NAME, cn.getValue());
|
||||
|
||||
int type = Attendees.TYPE_NONE;
|
||||
|
||||
CuType cutype = (CuType)attendee.getParameter(Parameter.CUTYPE);
|
||||
if (cutype == CuType.RESOURCE)
|
||||
type = Attendees.TYPE_RESOURCE;
|
||||
else {
|
||||
Role role = (Role)attendee.getParameter(Parameter.ROLE);
|
||||
int relationship;
|
||||
if (role == Role.CHAIR)
|
||||
relationship = Attendees.RELATIONSHIP_ORGANIZER;
|
||||
else {
|
||||
relationship = Attendees.RELATIONSHIP_ATTENDEE;
|
||||
if (role == Role.OPT_PARTICIPANT)
|
||||
type = Attendees.TYPE_OPTIONAL;
|
||||
else if (role == Role.REQ_PARTICIPANT)
|
||||
type = Attendees.TYPE_REQUIRED;
|
||||
}
|
||||
builder.withValue(Attendees.ATTENDEE_RELATIONSHIP, relationship);
|
||||
}
|
||||
|
||||
int status = Attendees.ATTENDEE_STATUS_NONE;
|
||||
PartStat partStat = (PartStat)attendee.getParameter(Parameter.PARTSTAT);
|
||||
if (partStat == null || partStat == PartStat.NEEDS_ACTION)
|
||||
status = Attendees.ATTENDEE_STATUS_INVITED;
|
||||
else if (partStat == PartStat.ACCEPTED)
|
||||
status = Attendees.ATTENDEE_STATUS_ACCEPTED;
|
||||
else if (partStat == PartStat.DECLINED)
|
||||
status = Attendees.ATTENDEE_STATUS_DECLINED;
|
||||
else if (partStat == PartStat.TENTATIVE)
|
||||
status = Attendees.ATTENDEE_STATUS_TENTATIVE;
|
||||
|
||||
return builder
|
||||
.withValue(Attendees.ATTENDEE_EMAIL, email)
|
||||
.withValue(Attendees.ATTENDEE_TYPE, type)
|
||||
.withValue(Attendees.ATTENDEE_STATUS, status);
|
||||
}
|
||||
|
||||
protected Builder buildReminder(Builder builder, VAlarm alarm) {
|
||||
int minutes = 0;
|
||||
|
||||
if (alarm.getTrigger() != null) {
|
||||
Dur duration = alarm.getTrigger().getDuration();
|
||||
if (duration != null) {
|
||||
// negative value in TRIGGER means positive value in Reminders.MINUTES and vice versa
|
||||
minutes = -(((duration.getWeeks() * 7 + duration.getDays()) * 24 + duration.getHours()) * 60 + duration.getMinutes());
|
||||
if (duration.isNegative())
|
||||
minutes *= -1;
|
||||
}
|
||||
}
|
||||
|
||||
Log.d(TAG, "Adding alarm " + minutes + " minutes before");
|
||||
|
||||
return builder
|
||||
.withValue(Reminders.METHOD, Reminders.METHOD_ALERT)
|
||||
.withValue(Reminders.MINUTES, minutes);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* private helper methods */
|
||||
|
||||
protected static Uri calendarsURI(Account account) {
|
||||
return Calendars.CONTENT_URI.buildUpon()
|
||||
.appendQueryParameter(Calendars.ACCOUNT_NAME, account.name)
|
||||
.appendQueryParameter(Calendars.ACCOUNT_TYPE, account.type)
|
||||
.appendQueryParameter(CalendarContract.CALLER_IS_SYNCADAPTER, "true")
|
||||
.build();
|
||||
}
|
||||
|
||||
protected Uri calendarsURI() {
|
||||
return calendarsURI(account);
|
||||
}
|
||||
|
||||
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(ContentProviderOperation.newUpdate(
|
||||
syncAdapterURI(ContentUris.withAppendedId(Events.CONTENT_URI, originalID)))
|
||||
.withValue(LocalEvent.COLUMN_SEQUENCE, originalSequence + 1)
|
||||
.withValue(Events.DIRTY, DIRTY_INCREASE_SEQUENCE)
|
||||
.build());
|
||||
// remove exception
|
||||
batch.enqueue(ContentProviderOperation.newDelete(
|
||||
syncAdapterURI(ContentUris.withAppendedId(Events.CONTENT_URI, id))).build());
|
||||
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(ContentProviderOperation.newUpdate(
|
||||
syncAdapterURI(ContentUris.withAppendedId(Events.CONTENT_URI, originalID)))
|
||||
.withValue(Events.DIRTY, DIRTY_DONT_INCREASE_SEQUENCE)
|
||||
.build());
|
||||
// increase SEQUENCE and set DIRTY to 0
|
||||
batch.enqueue(ContentProviderOperation.newUpdate(
|
||||
syncAdapterURI(ContentUris.withAppendedId(Events.CONTENT_URI, id)))
|
||||
.withValue(LocalEvent.COLUMN_SEQUENCE, sequence + 1)
|
||||
.withValue(Events.DIRTY, 0)
|
||||
.build());
|
||||
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];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,426 +5,22 @@
|
||||
* 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.ContentProviderOperation.Builder;
|
||||
import android.content.ContentProviderResult;
|
||||
import android.content.ContentUris;
|
||||
import android.content.ContentValues;
|
||||
import android.content.OperationApplicationException;
|
||||
import android.database.Cursor;
|
||||
import android.database.DatabaseUtils;
|
||||
import android.net.Uri;
|
||||
import android.os.RemoteException;
|
||||
import android.provider.CalendarContract;
|
||||
import android.util.Log;
|
||||
import java.io.FileNotFoundException;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import at.bitfire.ical4android.CalendarStorageException;
|
||||
import at.bitfire.vcard4android.ContactsStorageException;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
public interface LocalCollection {
|
||||
|
||||
import at.bitfire.davdroid.webdav.WebDavResource;
|
||||
import lombok.Cleanup;
|
||||
LocalResource[] getDeleted() throws CalendarStorageException, ContactsStorageException;
|
||||
LocalResource[] getWithoutFileName() throws CalendarStorageException, ContactsStorageException;
|
||||
LocalResource[] getDirty() throws CalendarStorageException, ContactsStorageException, FileNotFoundException;
|
||||
|
||||
/**
|
||||
* Represents a locally-stored synchronizable collection (for instance, the
|
||||
* address book or a calendar). Manages a CTag that stores the last known
|
||||
* remote CTag (the remote CTag changes whenever something in the remote collection changes).
|
||||
*
|
||||
* @param <T> Subtype of Resource that can be stored in the collection
|
||||
*/
|
||||
public abstract class LocalCollection<T extends Resource> {
|
||||
private static final String TAG = "davdroid.Collection";
|
||||
|
||||
final protected Account account;
|
||||
final protected ContentProviderClient providerClient;
|
||||
final protected ArrayList<ContentProviderOperation> pendingOperations = new ArrayList<>();
|
||||
LocalResource[] getAll() throws CalendarStorageException, ContactsStorageException;
|
||||
|
||||
|
||||
// database fields
|
||||
|
||||
/** base Uri of the collection's entries (for instance, Events.CONTENT_URI);
|
||||
* apply syncAdapterURI() before returning a value */
|
||||
abstract protected Uri entriesURI();
|
||||
|
||||
/** column name of the type of the account the entry belongs to */
|
||||
abstract protected String entryColumnAccountType();
|
||||
/** column name of the name of the account the entry belongs to */
|
||||
abstract protected String entryColumnAccountName();
|
||||
|
||||
/** column name of the collection ID the entry belongs to */
|
||||
abstract protected String entryColumnParentID();
|
||||
/** column name of an entry's ID */
|
||||
abstract protected String entryColumnID();
|
||||
/** column name of an entry's file name on the WebDAV server */
|
||||
abstract protected String entryColumnRemoteName();
|
||||
/** column name of an entry's last ETag on the WebDAV server; null if entry hasn't been uploaded yet */
|
||||
abstract protected String entryColumnETag();
|
||||
|
||||
/** column name of an entry's "dirty" flag (managed by content provider) */
|
||||
abstract protected String entryColumnDirty();
|
||||
/** column name of an entry's "deleted" flag (managed by content provider) */
|
||||
abstract protected String entryColumnDeleted();
|
||||
|
||||
/** column name of an entry's UID */
|
||||
abstract protected String entryColumnUID();
|
||||
|
||||
|
||||
/** SQL filter expression */
|
||||
String sqlFilter;
|
||||
|
||||
|
||||
LocalCollection(Account account, ContentProviderClient providerClient) {
|
||||
this.account = account;
|
||||
this.providerClient = providerClient;
|
||||
}
|
||||
|
||||
|
||||
// collection operations
|
||||
|
||||
/** gets the ID if the collection (for instance, ID of the Android calendar) */
|
||||
abstract public long getId();
|
||||
/** sets local stored CTag */
|
||||
abstract public void setCTag(String cTag) throws LocalStorageException;
|
||||
/** gets the CTag of the collection */
|
||||
abstract public String getCTag() throws LocalStorageException;
|
||||
/** update locally stored collection properties (e.g. display name and color) from a WebDavResource */
|
||||
abstract public void updateMetaData(WebDavResource.Properties properties) throws LocalStorageException;
|
||||
|
||||
|
||||
// content provider (= database) querying
|
||||
|
||||
/**
|
||||
* Finds new resources (resources which haven't been uploaded yet).
|
||||
* New resources are 1) dirty, and 2) don't have an ETag yet.
|
||||
* Only records matching sqlFilter will be returned.
|
||||
*
|
||||
* @return IDs of new resources
|
||||
* @throws LocalStorageException when the content provider couldn't be queried
|
||||
*/
|
||||
public long[] findNew() throws LocalStorageException {
|
||||
String where = entryColumnDirty() + "=1 AND " + entryColumnETag() + " IS NULL";
|
||||
if (entryColumnParentID() != null)
|
||||
where += " AND " + entryColumnParentID() + "=" + String.valueOf(getId());
|
||||
if (sqlFilter != null)
|
||||
where += " AND (" + sqlFilter + ")";
|
||||
try {
|
||||
@Cleanup Cursor cursor = providerClient.query(entriesURI(),
|
||||
new String[] { entryColumnID() },
|
||||
where, null, null);
|
||||
if (cursor == null)
|
||||
throw new LocalStorageException("Couldn't query new records");
|
||||
|
||||
long[] fresh = new long[cursor.getCount()];
|
||||
for (int idx = 0; cursor.moveToNext(); idx++) {
|
||||
long id = cursor.getLong(0);
|
||||
|
||||
// new record: we have to generate UID + remote file name for uploading
|
||||
T resource = findById(id, false);
|
||||
resource.initialize();
|
||||
// write generated UID + remote file name into database
|
||||
ContentValues values = new ContentValues(2);
|
||||
values.put(entryColumnUID(), resource.getUid());
|
||||
values.put(entryColumnRemoteName(), resource.getName());
|
||||
providerClient.update(ContentUris.withAppendedId(entriesURI(), id), values, null, null);
|
||||
|
||||
fresh[idx] = id;
|
||||
}
|
||||
return fresh;
|
||||
} catch(RemoteException ex) {
|
||||
throw new LocalStorageException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds updated resources (resources which have already been uploaded, but have changed locally).
|
||||
* Updated resources are 1) dirty, and 2) already have an ETag. Only records matching sqlFilter
|
||||
* will be returned.
|
||||
*
|
||||
* @return IDs of updated resources
|
||||
* @throws LocalStorageException when the content provider couldn't be queried
|
||||
*/
|
||||
public long[] findUpdated() throws LocalStorageException {
|
||||
String where = entryColumnDirty() + "=1 AND " + entryColumnETag() + " IS NOT NULL";
|
||||
if (entryColumnParentID() != null)
|
||||
where += " AND " + entryColumnParentID() + "=" + String.valueOf(getId());
|
||||
if (sqlFilter != null)
|
||||
where += " AND (" + sqlFilter + ")";
|
||||
try {
|
||||
@Cleanup Cursor cursor = providerClient.query(entriesURI(),
|
||||
new String[] { entryColumnID(), entryColumnRemoteName(), entryColumnETag() },
|
||||
where, null, null);
|
||||
if (cursor == null)
|
||||
throw new LocalStorageException("Couldn't query updated records");
|
||||
|
||||
long[] updated = new long[cursor.getCount()];
|
||||
for (int idx = 0; cursor.moveToNext(); idx++)
|
||||
updated[idx] = cursor.getLong(0);
|
||||
return updated;
|
||||
} catch(RemoteException ex) {
|
||||
throw new LocalStorageException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds deleted resources (resources which have been marked for deletion).
|
||||
* Deleted resources have the "deleted" flag set.
|
||||
* Only records matching sqlFilter will be returned.
|
||||
*
|
||||
* @return IDs of deleted resources
|
||||
* @throws LocalStorageException when the content provider couldn't be queried
|
||||
*/
|
||||
public long[] findDeleted() throws LocalStorageException {
|
||||
String where = entryColumnDeleted() + "=1";
|
||||
if (entryColumnParentID() != null)
|
||||
where += " AND " + entryColumnParentID() + "=" + String.valueOf(getId());
|
||||
if (sqlFilter != null)
|
||||
where += " AND (" + sqlFilter + ")";
|
||||
try {
|
||||
@Cleanup Cursor cursor = providerClient.query(entriesURI(),
|
||||
new String[] { entryColumnID(), entryColumnRemoteName(), entryColumnETag() },
|
||||
where, null, null);
|
||||
if (cursor == null)
|
||||
throw new LocalStorageException("Couldn't query dirty records");
|
||||
|
||||
long deleted[] = new long[cursor.getCount()];
|
||||
for (int idx = 0; cursor.moveToNext(); idx++)
|
||||
deleted[idx] = cursor.getLong(0);
|
||||
return deleted;
|
||||
} catch(RemoteException ex) {
|
||||
throw new LocalStorageException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a specific resource by ID. Only records matching sqlFilter are taken into account.
|
||||
* @param localID ID of the resource
|
||||
* @param populate true: populates all data fields (for instance, contact or event details);
|
||||
* false: only remote file name and ETag are populated
|
||||
* @return resource with either ID/remote file/name/ETag or all fields populated
|
||||
* @throws RecordNotFoundException when the resource couldn't be found
|
||||
* @throws LocalStorageException when the content provider couldn't be queried
|
||||
*/
|
||||
public T findById(long localID, boolean populate) throws LocalStorageException {
|
||||
try {
|
||||
@Cleanup Cursor cursor = providerClient.query(ContentUris.withAppendedId(entriesURI(), localID),
|
||||
new String[] { entryColumnRemoteName(), entryColumnETag() }, sqlFilter, null, null);
|
||||
if (cursor != null && cursor.moveToNext()) {
|
||||
T resource = newResource(localID, cursor.getString(0), cursor.getString(1));
|
||||
if (populate)
|
||||
populate(resource);
|
||||
return resource;
|
||||
} else
|
||||
throw new RecordNotFoundException();
|
||||
} catch(RemoteException ex) {
|
||||
throw new LocalStorageException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a specific resource by remote file name. Only records matching sqlFilter are taken into account.
|
||||
* @param remoteName remote file name of the resource
|
||||
* @param populate true: populates all data fields (for instance, contact or event details);
|
||||
* false: only remote file name and ETag are populated
|
||||
* @return resource with either ID/remote file/name/ETag or all fields populated
|
||||
* @throws RecordNotFoundException when the resource couldn't be found
|
||||
* @throws LocalStorageException when the content provider couldn't be queried
|
||||
*/
|
||||
public T findByRemoteName(String remoteName, boolean populate) throws LocalStorageException {
|
||||
String where = entryColumnRemoteName() + "=?";
|
||||
if (sqlFilter != null)
|
||||
where += " AND (" + sqlFilter + ")";
|
||||
try {
|
||||
@Cleanup Cursor cursor = providerClient.query(entriesURI(),
|
||||
new String[] { entryColumnID(), entryColumnRemoteName(), entryColumnETag() },
|
||||
where, new String[] { remoteName }, null);
|
||||
if (cursor != null && cursor.moveToNext()) {
|
||||
T resource = newResource(cursor.getLong(0), cursor.getString(1), cursor.getString(2));
|
||||
if (populate)
|
||||
populate(resource);
|
||||
return resource;
|
||||
} else
|
||||
throw new RecordNotFoundException();
|
||||
} catch(RemoteException ex) {
|
||||
throw new LocalStorageException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
/** populates all data fields from the content provider */
|
||||
public abstract void populate(Resource record) throws LocalStorageException;
|
||||
|
||||
|
||||
// create/update/delete
|
||||
|
||||
/**
|
||||
* Creates a new resource object in memory. No content provider operations involved.
|
||||
* @param localID the ID of the resource
|
||||
* @param resourceName the (remote) file name of the resource
|
||||
* @param eTag ETag of the resource
|
||||
* @return the new resource object */
|
||||
abstract public T newResource(long localID, String resourceName, String eTag);
|
||||
|
||||
/** Adds the resource (including all data) to the local collection.
|
||||
* @param resource Resource to be added
|
||||
*/
|
||||
public void add(Resource resource) throws LocalStorageException {
|
||||
int idx = pendingOperations.size();
|
||||
pendingOperations.add(
|
||||
buildEntry(ContentProviderOperation.newInsert(entriesURI()), resource, false)
|
||||
.withYieldAllowed(true)
|
||||
.build());
|
||||
|
||||
addDataRows(resource, -1, idx);
|
||||
commit();
|
||||
}
|
||||
|
||||
/** Updates an existing resource in the local collection. The resource will be found by
|
||||
* the remote file name and all data will be updated. */
|
||||
public void updateByRemoteName(Resource remoteResource) throws LocalStorageException {
|
||||
T localResource = findByRemoteName(remoteResource.getName(), false);
|
||||
pendingOperations.add(
|
||||
buildEntry(ContentProviderOperation.newUpdate(ContentUris.withAppendedId(entriesURI(), localResource.getLocalID())), remoteResource, true)
|
||||
.withValue(entryColumnETag(), remoteResource.getETag())
|
||||
.withYieldAllowed(true)
|
||||
.build());
|
||||
|
||||
removeDataRows(localResource);
|
||||
addDataRows(remoteResource, localResource.getLocalID(), -1);
|
||||
commit();
|
||||
}
|
||||
|
||||
/** Enqueues deleting a resource from the local collection. Requires commit() to be effective! */
|
||||
public void delete(Resource resource) {
|
||||
pendingOperations.add(ContentProviderOperation
|
||||
.newDelete(ContentUris.withAppendedId(entriesURI(), resource.getLocalID()))
|
||||
.withYieldAllowed(true)
|
||||
.build());
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes all resources except the give ones from the local collection.
|
||||
* @param remoteResources resources with these remote file names will be kept
|
||||
* @return number of deleted resources
|
||||
*/
|
||||
public int deleteAllExceptRemoteNames(Resource[] remoteResources) throws LocalStorageException
|
||||
{
|
||||
final String where;
|
||||
|
||||
if (remoteResources.length != 0) {
|
||||
// delete all except certain entries
|
||||
final List<String> sqlFileNames = new LinkedList<>();
|
||||
for (final Resource res : remoteResources)
|
||||
sqlFileNames.add(DatabaseUtils.sqlEscapeString(res.getName()));
|
||||
where = entryColumnRemoteName() + " NOT IN (" + StringUtils.join(sqlFileNames, ",") + ')';
|
||||
} else
|
||||
// delete all entries
|
||||
where = entryColumnRemoteName() + " IS NOT NULL";
|
||||
|
||||
try {
|
||||
if (entryColumnParentID() != null)
|
||||
// entries have a parent collection (for instance, events which have a calendar)
|
||||
return providerClient.delete(
|
||||
entriesURI(),
|
||||
entryColumnParentID() + "=? AND (" + where + ')', // restrict deletion to parent collection
|
||||
new String[] { String.valueOf(getId()) }
|
||||
);
|
||||
else
|
||||
// entries don't have a parent collection (contacts are stored directly and not within an address book)
|
||||
return providerClient.delete(entriesURI(), where, null);
|
||||
} catch (RemoteException e) {
|
||||
throw new LocalStorageException("Couldn't delete local resources", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/** Updates the locally-known ETag of a resource. */
|
||||
public void updateETag(Resource res, String eTag) throws LocalStorageException {
|
||||
Log.d(TAG, "Setting ETag of local resource " + res.getName() + " to " + eTag);
|
||||
|
||||
ContentValues values = new ContentValues(1);
|
||||
values.put(entryColumnETag(), eTag);
|
||||
try {
|
||||
providerClient.update(ContentUris.withAppendedId(entriesURI(), res.getLocalID()), values, null, new String[] {});
|
||||
} catch (RemoteException e) {
|
||||
throw new LocalStorageException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/** Enqueues removing the dirty flag from a locally-stored resource. Requires commit() to be effective! */
|
||||
public void clearDirty(Resource resource) {
|
||||
pendingOperations.add(ContentProviderOperation
|
||||
.newUpdate(ContentUris.withAppendedId(entriesURI(), resource.getLocalID()))
|
||||
.withValue(entryColumnDirty(), 0)
|
||||
.build());
|
||||
}
|
||||
|
||||
/** Commits enqueued operations to the content provider (for batch operations). */
|
||||
public int commit() throws LocalStorageException {
|
||||
int affected = 0;
|
||||
if (!pendingOperations.isEmpty())
|
||||
try {
|
||||
Log.d(TAG, "Committing " + pendingOperations.size() + " operations ...");
|
||||
ContentProviderResult[] results = providerClient.applyBatch(pendingOperations);
|
||||
for (ContentProviderResult result : results)
|
||||
if (result != null) // will have either .uri or .count set
|
||||
if (result.count != null)
|
||||
affected += result.count;
|
||||
else if (result.uri != null)
|
||||
affected = 1;
|
||||
Log.d(TAG, "... " + affected + " record(s) affected");
|
||||
pendingOperations.clear();
|
||||
} catch(OperationApplicationException | RemoteException ex) {
|
||||
throw new LocalStorageException(ex);
|
||||
}
|
||||
return affected;
|
||||
}
|
||||
|
||||
|
||||
// helpers
|
||||
|
||||
protected void queueOperation(Builder builder) {
|
||||
if (builder != null)
|
||||
pendingOperations.add(builder.build());
|
||||
}
|
||||
|
||||
/** Appends account type, name and CALLER_IS_SYNCADAPTER to an Uri. */
|
||||
protected Uri syncAdapterURI(Uri baseURI) {
|
||||
return baseURI.buildUpon()
|
||||
.appendQueryParameter(entryColumnAccountType(), account.type)
|
||||
.appendQueryParameter(entryColumnAccountName(), account.name)
|
||||
.appendQueryParameter(CalendarContract.CALLER_IS_SYNCADAPTER, "true")
|
||||
.build();
|
||||
}
|
||||
|
||||
protected Builder newDataInsertBuilder(Uri dataUri, String refFieldName, long raw_ref_id, int backrefIdx) {
|
||||
Builder builder = ContentProviderOperation.newInsert(syncAdapterURI(dataUri));
|
||||
if (backrefIdx != -1)
|
||||
return builder.withValueBackReference(refFieldName, backrefIdx);
|
||||
else
|
||||
return builder.withValue(refFieldName, raw_ref_id);
|
||||
}
|
||||
|
||||
|
||||
// content builders
|
||||
|
||||
/**
|
||||
* Builds the main entry (for instance, a ContactsContract.RawContacts row) from a resource.
|
||||
* The entry is built for insertion to the location identified by entriesURI().
|
||||
*
|
||||
* @param builder Builder to be extended by all resource data that can be stored without extra data rows.
|
||||
* @param resource Event, task or note resource whose contents shall be inserted/updated
|
||||
* @param update false when the entry is built the first time (when creating the row), true if it's an update
|
||||
*/
|
||||
protected abstract Builder buildEntry(Builder builder, Resource resource, boolean update);
|
||||
|
||||
/** Enqueues adding extra data rows of the resource to the local collection. */
|
||||
protected abstract void addDataRows(Resource resource, long localID, int backrefIdx);
|
||||
|
||||
/** Enqueues removing all extra data rows of the resource from the local collection. */
|
||||
protected abstract void removeDataRows(Resource resource);
|
||||
String getCTag() throws CalendarStorageException, ContactsStorageException;
|
||||
void setCTag(String cTag) throws CalendarStorageException, ContactsStorageException;
|
||||
}
|
||||
|
||||
194
app/src/main/java/at/bitfire/davdroid/resource/LocalContact.java
Normal file
194
app/src/main/java/at/bitfire/davdroid/resource/LocalContact.java
Normal file
@@ -0,0 +1,194 @@
|
||||
/*
|
||||
* 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) {
|
||||
ContentProviderOperation.Builder builder = ContentProviderOperation.newInsert(dataSyncURI());
|
||||
if (id == null)
|
||||
builder.withValueBackReference(UnknownProperties.RAW_CONTACT_ID, 0);
|
||||
else
|
||||
builder.withValue(UnknownProperties.RAW_CONTACT_ID, id);
|
||||
builder .withValue(UnknownProperties.MIMETYPE, UnknownProperties.CONTENT_ITEM_TYPE)
|
||||
.withValue(UnknownProperties.UNKNOWN_PROPERTIES, contact.unknownProperties);
|
||||
batch.enqueue(builder.build());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
public void addToGroup(BatchOperation batch, long groupID) {
|
||||
assertID();
|
||||
batch.enqueue(ContentProviderOperation
|
||||
.newInsert(dataSyncURI())
|
||||
.withValue(GroupMembership.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE)
|
||||
.withValue(GroupMembership.RAW_CONTACT_ID, id)
|
||||
.withValue(GroupMembership.GROUP_ROW_ID, groupID)
|
||||
.build()
|
||||
);
|
||||
|
||||
batch.enqueue(ContentProviderOperation
|
||||
.newInsert(dataSyncURI())
|
||||
.withValue(CachedGroupMembership.MIMETYPE, CachedGroupMembership.CONTENT_ITEM_TYPE)
|
||||
.withValue(CachedGroupMembership.RAW_CONTACT_ID, id)
|
||||
.withValue(CachedGroupMembership.GROUP_ID, groupID)
|
||||
.build()
|
||||
);
|
||||
}
|
||||
|
||||
public void removeGroupMemberships(BatchOperation batch) {
|
||||
assertID();
|
||||
batch.enqueue(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 }
|
||||
)
|
||||
.build()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
146
app/src/main/java/at/bitfire/davdroid/resource/LocalEvent.java
Normal file
146
app/src/main/java/at/bitfire/davdroid/resource/LocalEvent.java
Normal file
@@ -0,0 +1,146 @@
|
||||
/*
|
||||
* 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];
|
||||
}
|
||||
}
|
||||
}
|
||||
249
app/src/main/java/at/bitfire/davdroid/resource/LocalGroup.java
Normal file
249
app/src/main/java/at/bitfire/davdroid/resource/LocalGroup.java
Normal file
@@ -0,0 +1,249 @@
|
||||
/*
|
||||
* 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 android.provider.ContactsContract.RawContactsEntity;
|
||||
|
||||
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(ContentProviderOperation
|
||||
.newDelete(addressBook.syncAdapterURI(ContactsContract.Data.CONTENT_URI))
|
||||
.withSelection(
|
||||
CachedGroupMembership.MIMETYPE + "=? AND " + CachedGroupMembership.GROUP_ID + "=?",
|
||||
new String[] { CachedGroupMembership.CONTENT_ITEM_TYPE, String.valueOf(id) }
|
||||
).build()
|
||||
);
|
||||
|
||||
// insert updated cached group memberships
|
||||
for (long member : getMembers())
|
||||
batch.enqueue(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)
|
||||
.build()
|
||||
);
|
||||
|
||||
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(ContentProviderOperation
|
||||
.newUpdate(addressBook.syncAdapterURI(ContentUris.withAppendedId(RawContacts.CONTENT_URI, member)))
|
||||
.withValue(RawContacts.DIRTY, 1)
|
||||
.build()
|
||||
);
|
||||
|
||||
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(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) })
|
||||
.build()
|
||||
);
|
||||
|
||||
// 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(ContentProviderOperation
|
||||
.newUpdate(addressBook.syncAdapterURI(ContentUris.withAppendedId(Groups.CONTENT_URI, id)))
|
||||
.withValue(COLUMN_PENDING_MEMBERS, null)
|
||||
.build()
|
||||
);
|
||||
|
||||
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];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
}
|
||||
@@ -1,31 +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;
|
||||
|
||||
public class LocalStorageException extends Exception {
|
||||
private static final long serialVersionUID = -7787658815291629529L;
|
||||
|
||||
private static final String detailMessage = "Couldn't access local content provider";
|
||||
|
||||
|
||||
public LocalStorageException(String detailMessage, Throwable throwable) {
|
||||
super(detailMessage, throwable);
|
||||
}
|
||||
|
||||
public LocalStorageException(String detailMessage) {
|
||||
super(detailMessage);
|
||||
}
|
||||
|
||||
public LocalStorageException(Throwable throwable) {
|
||||
super(detailMessage, throwable);
|
||||
}
|
||||
|
||||
public LocalStorageException() {
|
||||
super(detailMessage);
|
||||
}
|
||||
}
|
||||
138
app/src/main/java/at/bitfire/davdroid/resource/LocalTask.java
Normal file
138
app/src/main/java/at/bitfire/davdroid/resource/LocalTask.java
Normal file
@@ -0,0 +1,138 @@
|
||||
/*
|
||||
* 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];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,387 +9,159 @@
|
||||
package at.bitfire.davdroid.resource;
|
||||
|
||||
import android.accounts.Account;
|
||||
import android.content.ContentProviderClient;
|
||||
import android.content.ContentProviderOperation;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.ContentUris;
|
||||
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.util.Log;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import net.fortuna.ical4j.model.Date;
|
||||
import net.fortuna.ical4j.model.DateTime;
|
||||
import net.fortuna.ical4j.model.Dur;
|
||||
import net.fortuna.ical4j.model.TimeZone;
|
||||
import net.fortuna.ical4j.model.property.Clazz;
|
||||
import net.fortuna.ical4j.model.property.Completed;
|
||||
import net.fortuna.ical4j.model.property.DtStart;
|
||||
import net.fortuna.ical4j.model.property.Due;
|
||||
import net.fortuna.ical4j.model.property.Duration;
|
||||
import net.fortuna.ical4j.model.property.Status;
|
||||
import net.fortuna.ical4j.util.TimeZones;
|
||||
import org.dmfs.provider.tasks.TaskContract.TaskLists;
|
||||
import org.dmfs.provider.tasks.TaskContract.Tasks;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.dmfs.provider.tasks.TaskContract;
|
||||
import java.io.FileNotFoundException;
|
||||
|
||||
import java.util.LinkedList;
|
||||
|
||||
import at.bitfire.davdroid.DAVUtils;
|
||||
import at.bitfire.davdroid.DateUtils;
|
||||
import at.bitfire.davdroid.webdav.WebDavResource;
|
||||
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;
|
||||
import lombok.Getter;
|
||||
|
||||
public class LocalTaskList extends LocalCollection<Task> {
|
||||
private static final String TAG = "davdroid.LocalTaskList";
|
||||
public class LocalTaskList extends AndroidTaskList implements LocalCollection {
|
||||
|
||||
@Getter protected String url;
|
||||
@Getter protected long id;
|
||||
public static final int defaultColor = 0xFFC3EA6E; // "DAVdroid green"
|
||||
|
||||
public static final String TASKS_AUTHORITY = "org.dmfs.tasks";
|
||||
public static final String COLUMN_CTAG = TaskLists.SYNC_VERSION;
|
||||
|
||||
protected static final String COLLECTION_COLUMN_CTAG = TaskContract.TaskLists.SYNC1;
|
||||
static String[] BASE_INFO_COLUMNS = new String[] {
|
||||
Tasks._ID,
|
||||
Tasks._SYNC_ID,
|
||||
LocalTask.COLUMN_ETAG
|
||||
};
|
||||
|
||||
@Override protected Uri entriesURI() { return syncAdapterURI(TaskContract.Tasks.getContentUri(TASKS_AUTHORITY)); }
|
||||
@Override protected String entryColumnAccountType() { return TaskContract.Tasks.ACCOUNT_TYPE; }
|
||||
@Override protected String entryColumnAccountName() { return TaskContract.Tasks.ACCOUNT_NAME; }
|
||||
@Override protected String entryColumnParentID() { return TaskContract.Tasks.LIST_ID; }
|
||||
@Override protected String entryColumnID() { return TaskContract.Tasks._ID; }
|
||||
@Override protected String entryColumnRemoteName() { return TaskContract.Tasks._SYNC_ID; }
|
||||
@Override protected String entryColumnETag() { return TaskContract.Tasks.SYNC1; }
|
||||
@Override protected String entryColumnDirty() { return TaskContract.Tasks._DIRTY; }
|
||||
@Override protected String entryColumnDeleted() { return TaskContract.Tasks._DELETED; }
|
||||
@Override protected String entryColumnUID() { return TaskContract.Tasks.SYNC2; }
|
||||
// can be cached, because after installing OpenTasks, you have to re-install DAVdroid anyway
|
||||
private static Boolean tasksProviderAvailable;
|
||||
|
||||
|
||||
public static Uri create(Account account, ContentResolver resolver, ServerInfo.ResourceInfo info) throws LocalStorageException {
|
||||
@Cleanup("release") final ContentProviderClient client = resolver.acquireContentProviderClient(TASKS_AUTHORITY);
|
||||
if (client == null)
|
||||
throw new LocalStorageException("No tasks provider found");
|
||||
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(TaskContract.TaskLists.ACCOUNT_NAME, account.name);
|
||||
values.put(TaskContract.TaskLists.ACCOUNT_TYPE, account.type);
|
||||
values.put(TaskContract.TaskLists._SYNC_ID, info.getURL());
|
||||
values.put(TaskContract.TaskLists.LIST_NAME, info.getTitle());
|
||||
values.put(TaskContract.TaskLists.LIST_COLOR, info.getColor() != null ? info.getColor() : DAVUtils.calendarGreen);
|
||||
values.put(TaskContract.TaskLists.OWNER, account.name);
|
||||
values.put(TaskContract.TaskLists.ACCESS_LEVEL, 0);
|
||||
values.put(TaskContract.TaskLists.SYNC_ENABLED, 1);
|
||||
values.put(TaskContract.TaskLists.VISIBLE, 1);
|
||||
|
||||
Log.i(TAG, "Inserting task list: " + values.toString());
|
||||
try {
|
||||
return client.insert(taskListsURI(account), values);
|
||||
} catch (RemoteException e) {
|
||||
throw new LocalStorageException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static LocalTaskList[] findAll(Account account, ContentProviderClient providerClient) throws RemoteException {
|
||||
@Cleanup Cursor cursor = providerClient.query(taskListsURI(account),
|
||||
new String[]{TaskContract.TaskLists._ID, TaskContract.TaskLists._SYNC_ID},
|
||||
null, null, null);
|
||||
|
||||
LinkedList<LocalTaskList> taskList = new LinkedList<>();
|
||||
while (cursor != null && cursor.moveToNext())
|
||||
taskList.add(new LocalTaskList(account, providerClient, cursor.getInt(0), cursor.getString(1)));
|
||||
return taskList.toArray(new LocalTaskList[taskList.size()]);
|
||||
}
|
||||
|
||||
public LocalTaskList(Account account, ContentProviderClient providerClient, long id, String url) {
|
||||
super(account, providerClient);
|
||||
this.id = id;
|
||||
this.url = url;
|
||||
}
|
||||
@Override
|
||||
protected String[] taskBaseInfoColumns() {
|
||||
return BASE_INFO_COLUMNS;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String getCTag() throws LocalStorageException {
|
||||
try {
|
||||
@Cleanup Cursor c = providerClient.query(ContentUris.withAppendedId(taskListsURI(account), id),
|
||||
new String[] { COLLECTION_COLUMN_CTAG }, null, null, null);
|
||||
if (c != null && c.moveToFirst())
|
||||
return c.getString(0);
|
||||
else
|
||||
throw new LocalStorageException("Couldn't query task list CTag");
|
||||
} catch(RemoteException e) {
|
||||
throw new LocalStorageException(e);
|
||||
}
|
||||
}
|
||||
protected LocalTaskList(Account account, TaskProvider provider, long id) {
|
||||
super(account, provider, LocalTask.Factory.INSTANCE, id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setCTag(String cTag) throws LocalStorageException {
|
||||
ContentValues values = new ContentValues(1);
|
||||
values.put(COLLECTION_COLUMN_CTAG, cTag);
|
||||
try {
|
||||
providerClient.update(ContentUris.withAppendedId(taskListsURI(account), id), values, null, null);
|
||||
} catch(RemoteException e) {
|
||||
throw new LocalStorageException(e);
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateMetaData(WebDavResource.Properties properties) throws LocalStorageException {
|
||||
ContentValues values = new ContentValues();
|
||||
public void update(CollectionInfo info, boolean updateColor) throws CalendarStorageException {
|
||||
update(valuesFromCollectionInfo(info, updateColor));
|
||||
}
|
||||
|
||||
final String displayName = properties.getDisplayName();
|
||||
if (displayName != null)
|
||||
values.put(TaskContract.TaskLists.LIST_NAME, displayName);
|
||||
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));
|
||||
|
||||
final Integer color = properties.getColor();
|
||||
if (color != null)
|
||||
values.put(TaskContract.TaskLists.LIST_COLOR, color);
|
||||
if (withColor)
|
||||
values.put(TaskLists.LIST_COLOR, info.color != null ? info.color : defaultColor);
|
||||
|
||||
try {
|
||||
if (values.size() > 0)
|
||||
providerClient.update(ContentUris.withAppendedId(taskListsURI(account), id), values, null, null);
|
||||
} catch(RemoteException e) {
|
||||
throw new LocalStorageException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Task newResource(long localID, String resourceName, String eTag) {
|
||||
return new Task(localID, resourceName, eTag);
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void populate(Resource record) throws LocalStorageException {
|
||||
try {
|
||||
@Cleanup final Cursor cursor = providerClient.query(entriesURI(),
|
||||
new String[] {
|
||||
/* 0 */ entryColumnUID(), TaskContract.Tasks.TITLE, TaskContract.Tasks.LOCATION, TaskContract.Tasks.DESCRIPTION, TaskContract.Tasks.URL,
|
||||
/* 5 */ TaskContract.Tasks.CLASSIFICATION, TaskContract.Tasks.STATUS, TaskContract.Tasks.PERCENT_COMPLETE,
|
||||
/* 8 */ TaskContract.Tasks.TZ, TaskContract.Tasks.DTSTART, TaskContract.Tasks.IS_ALLDAY,
|
||||
/* 11 */ TaskContract.Tasks.DUE, TaskContract.Tasks.DURATION, TaskContract.Tasks.COMPLETED,
|
||||
/* 14 */ TaskContract.Tasks.CREATED, TaskContract.Tasks.LAST_MODIFIED, TaskContract.Tasks.PRIORITY
|
||||
}, entryColumnID() + "=?", new String[]{ String.valueOf(record.getLocalID()) }, null);
|
||||
@Override
|
||||
public LocalTask[] getAll() throws CalendarStorageException {
|
||||
return (LocalTask[])queryTasks(null, null);
|
||||
}
|
||||
|
||||
Task task = (Task)record;
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
task.setUid(cursor.getString(0));
|
||||
@Override
|
||||
public LocalTask[] getDeleted() throws CalendarStorageException {
|
||||
return (LocalTask[])queryTasks(Tasks._DELETED + "!=0", null);
|
||||
}
|
||||
|
||||
if (!cursor.isNull(14))
|
||||
task.setCreatedAt(new DateTime(cursor.getLong(14)));
|
||||
if (!cursor.isNull(15))
|
||||
task.setLastModified(new DateTime(cursor.getLong(15)));
|
||||
@Override
|
||||
public LocalTask[] getWithoutFileName() throws CalendarStorageException {
|
||||
return (LocalTask[])queryTasks(Tasks._SYNC_ID + " IS NULL", null);
|
||||
}
|
||||
|
||||
if (!StringUtils.isEmpty(cursor.getString(1)))
|
||||
task.setSummary(cursor.getString(1));
|
||||
|
||||
if (!StringUtils.isEmpty(cursor.getString(2)))
|
||||
task.setLocation(cursor.getString(2));
|
||||
|
||||
if (!StringUtils.isEmpty(cursor.getString(3)))
|
||||
task.setDescription(cursor.getString(3));
|
||||
|
||||
if (!StringUtils.isEmpty(cursor.getString(4)))
|
||||
task.setUrl(cursor.getString(4));
|
||||
|
||||
if (!cursor.isNull(16))
|
||||
task.setPriority(cursor.getInt(16));
|
||||
|
||||
if (!cursor.isNull(5))
|
||||
switch (cursor.getInt(5)) {
|
||||
case TaskContract.Tasks.CLASSIFICATION_PUBLIC:
|
||||
task.setClassification(Clazz.PUBLIC);
|
||||
break;
|
||||
case TaskContract.Tasks.CLASSIFICATION_CONFIDENTIAL:
|
||||
task.setClassification(Clazz.CONFIDENTIAL);
|
||||
break;
|
||||
default:
|
||||
task.setClassification(Clazz.PRIVATE);
|
||||
}
|
||||
|
||||
if (!cursor.isNull(6))
|
||||
switch (cursor.getInt(6)) {
|
||||
case TaskContract.Tasks.STATUS_IN_PROCESS:
|
||||
task.setStatus(Status.VTODO_IN_PROCESS);
|
||||
break;
|
||||
case TaskContract.Tasks.STATUS_COMPLETED:
|
||||
task.setStatus(Status.VTODO_COMPLETED);
|
||||
break;
|
||||
case TaskContract.Tasks.STATUS_CANCELLED:
|
||||
task.setStatus(Status.VTODO_CANCELLED);
|
||||
break;
|
||||
default:
|
||||
task.setStatus(Status.VTODO_NEEDS_ACTION);
|
||||
}
|
||||
if (!cursor.isNull(7))
|
||||
task.setPercentComplete(cursor.getInt(7));
|
||||
|
||||
TimeZone tz = null;
|
||||
if (!cursor.isNull(8))
|
||||
tz = DateUtils.tzRegistry.getTimeZone(cursor.getString(8));
|
||||
|
||||
if (!cursor.isNull(9) && !cursor.isNull(10)) {
|
||||
long ts = cursor.getLong(9);
|
||||
boolean allDay = cursor.getInt(10) != 0;
|
||||
|
||||
Date dt;
|
||||
if (allDay)
|
||||
dt = new Date(ts);
|
||||
else {
|
||||
dt = new DateTime(ts);
|
||||
if (tz != null)
|
||||
((DateTime)dt).setTimeZone(tz);
|
||||
}
|
||||
task.setDtStart(new DtStart(dt));
|
||||
}
|
||||
|
||||
if (!cursor.isNull(11)) {
|
||||
DateTime dt = new DateTime(cursor.getLong(11));
|
||||
if (tz != null)
|
||||
dt.setTimeZone(tz);
|
||||
task.setDue(new Due(dt));
|
||||
}
|
||||
|
||||
if (!cursor.isNull(12))
|
||||
task.setDuration(new Duration(new Dur(cursor.getString(12))));
|
||||
|
||||
if (!cursor.isNull(13))
|
||||
task.setCompletedAt(new Completed(new DateTime(cursor.getLong(13))));
|
||||
}
|
||||
|
||||
} catch (RemoteException e) {
|
||||
throw new LocalStorageException("Couldn't process locally stored task", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ContentProviderOperation.Builder buildEntry(ContentProviderOperation.Builder builder, Resource resource, boolean update) {
|
||||
final Task task = (Task)resource;
|
||||
|
||||
if (!update)
|
||||
builder .withValue(entryColumnParentID(), id)
|
||||
.withValue(entryColumnRemoteName(), task.getName())
|
||||
.withValue(entryColumnDirty(), 0); // _DIRTY is INTEGER DEFAULT 1 in org.dmfs.provider.tasks
|
||||
|
||||
builder.withValue(entryColumnUID(), task.getUid())
|
||||
.withValue(entryColumnETag(), task.getETag())
|
||||
.withValue(TaskContract.Tasks.TITLE, task.getSummary())
|
||||
.withValue(TaskContract.Tasks.LOCATION, task.getLocation())
|
||||
.withValue(TaskContract.Tasks.DESCRIPTION, task.getDescription())
|
||||
.withValue(TaskContract.Tasks.URL, task.getUrl())
|
||||
.withValue(TaskContract.Tasks.PRIORITY, task.getPriority());
|
||||
|
||||
if (task.getCreatedAt() != null)
|
||||
builder.withValue(TaskContract.Tasks.CREATED, task.getCreatedAt().getTime());
|
||||
if (task.getLastModified() != null)
|
||||
builder.withValue(TaskContract.Tasks.LAST_MODIFIED, task.getLastModified().getTime());
|
||||
|
||||
if (task.getClassification() != null) {
|
||||
int classCode = TaskContract.Tasks.CLASSIFICATION_PRIVATE;
|
||||
if (task.getClassification() == Clazz.PUBLIC)
|
||||
classCode = TaskContract.Tasks.CLASSIFICATION_PUBLIC;
|
||||
else if (task.getClassification() == Clazz.CONFIDENTIAL)
|
||||
classCode = TaskContract.Tasks.CLASSIFICATION_CONFIDENTIAL;
|
||||
builder = builder.withValue(TaskContract.Tasks.CLASSIFICATION, classCode);
|
||||
}
|
||||
|
||||
int statusCode = TaskContract.Tasks.STATUS_DEFAULT;
|
||||
if (task.getStatus() != null) {
|
||||
if (task.getStatus() == Status.VTODO_NEEDS_ACTION)
|
||||
statusCode = TaskContract.Tasks.STATUS_NEEDS_ACTION;
|
||||
else if (task.getStatus() == Status.VTODO_IN_PROCESS)
|
||||
statusCode = TaskContract.Tasks.STATUS_IN_PROCESS;
|
||||
else if (task.getStatus() == Status.VTODO_COMPLETED)
|
||||
statusCode = TaskContract.Tasks.STATUS_COMPLETED;
|
||||
else if (task.getStatus() == Status.VTODO_CANCELLED)
|
||||
statusCode = TaskContract.Tasks.STATUS_CANCELLED;
|
||||
}
|
||||
builder .withValue(TaskContract.Tasks.STATUS, statusCode)
|
||||
.withValue(TaskContract.Tasks.PERCENT_COMPLETE, task.getPercentComplete());
|
||||
|
||||
TimeZone tz = null;
|
||||
|
||||
if (task.getDtStart() != null) {
|
||||
Date start = task.getDtStart().getDate();
|
||||
boolean allDay;
|
||||
if (start instanceof DateTime) {
|
||||
allDay = false;
|
||||
tz = ((DateTime)start).getTimeZone();
|
||||
} else
|
||||
allDay = true;
|
||||
long ts = start.getTime();
|
||||
builder .withValue(TaskContract.Tasks.DTSTART, ts)
|
||||
.withValue(TaskContract.Tasks.IS_ALLDAY, allDay ? 1 : 0);
|
||||
}
|
||||
|
||||
if (task.getDue() != null) {
|
||||
Due due = task.getDue();
|
||||
builder.withValue(TaskContract.Tasks.DUE, due.getDate().getTime());
|
||||
if (tz == null)
|
||||
tz = due.getTimeZone();
|
||||
|
||||
} else if (task.getDuration() != null)
|
||||
builder.withValue(TaskContract.Tasks.DURATION, task.getDuration().getValue());
|
||||
|
||||
if (task.getCompletedAt() != null) {
|
||||
Date completed = task.getCompletedAt().getDate();
|
||||
boolean allDay;
|
||||
if (completed instanceof DateTime) {
|
||||
allDay = false;
|
||||
if (tz == null)
|
||||
tz = ((DateTime)completed).getTimeZone();
|
||||
} else {
|
||||
task.getCompletedAt().setUtc(true);
|
||||
allDay = true;
|
||||
}
|
||||
long ts = completed.getTime();
|
||||
builder .withValue(TaskContract.Tasks.COMPLETED, ts)
|
||||
.withValue(TaskContract.Tasks.COMPLETED_IS_ALLDAY, allDay ? 1 : 0);
|
||||
}
|
||||
|
||||
// TZ *must* be provided when DTSTART or DUE is set
|
||||
if ((task.getDtStart() != null || task.getDue() != null) && tz == null)
|
||||
tz = DateUtils.tzRegistry.getTimeZone(TimeZones.GMT_ID);
|
||||
if (tz != null)
|
||||
builder.withValue(TaskContract.Tasks.TZ, DateUtils.findAndroidTimezoneID(tz.getID()));
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void addDataRows(Resource resource, long localID, int backrefIdx) {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void removeDataRows(Resource resource) {
|
||||
}
|
||||
@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;
|
||||
}
|
||||
|
||||
|
||||
// helpers
|
||||
@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;
|
||||
}
|
||||
|
||||
public static boolean isAvailable(Context context) {
|
||||
try {
|
||||
@Cleanup("release") ContentProviderClient client = context.getContentResolver().acquireContentProviderClient(TASKS_AUTHORITY);
|
||||
return client != null;
|
||||
} catch (SecurityException e) {
|
||||
Log.e(TAG, "DAVdroid is not allowed to access tasks", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@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);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Uri syncAdapterURI(Uri baseURI) {
|
||||
return baseURI.buildUpon()
|
||||
.appendQueryParameter(entryColumnAccountType(), account.type)
|
||||
.appendQueryParameter(entryColumnAccountName(), account.name)
|
||||
.appendQueryParameter(TaskContract.CALLER_IS_SYNCADAPTER, "true")
|
||||
.build();
|
||||
}
|
||||
|
||||
protected static Uri taskListsURI(Account account) {
|
||||
return TaskContract.TaskLists.getContentUri(TASKS_AUTHORITY).buildUpon()
|
||||
.appendQueryParameter(TaskContract.TaskLists.ACCOUNT_TYPE, account.type)
|
||||
.appendQueryParameter(TaskContract.TaskLists.ACCOUNT_NAME, account.name)
|
||||
.appendQueryParameter(TaskContract.CALLER_IS_SYNCADAPTER, "true")
|
||||
.build();
|
||||
}
|
||||
// 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];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,24 +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;
|
||||
|
||||
/**
|
||||
* Thrown when a local record (for instance, Contact with ID 12345) should be read
|
||||
* but could not be found.
|
||||
*/
|
||||
public class RecordNotFoundException extends LocalStorageException {
|
||||
private static final long serialVersionUID = 4961024282198632578L;
|
||||
|
||||
private static final String detailMessage = "Record not found in local content provider";
|
||||
|
||||
|
||||
RecordNotFoundException() {
|
||||
super(detailMessage);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,62 +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.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
|
||||
import at.bitfire.davdroid.webdav.DavException;
|
||||
import at.bitfire.davdroid.webdav.HttpException;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.ToString;
|
||||
|
||||
/**
|
||||
* Represents a resource that can be contained in a LocalCollection or RemoteCollection
|
||||
* for synchronization by WebDAV.
|
||||
*/
|
||||
@ToString
|
||||
public abstract class Resource {
|
||||
@Getter @Setter protected String name, ETag;
|
||||
@Getter @Setter protected String uid;
|
||||
@Getter protected long localID;
|
||||
|
||||
public Resource(String name, String ETag) {
|
||||
this.name = name;
|
||||
this.ETag = ETag;
|
||||
}
|
||||
|
||||
public Resource(long localID, String name, String ETag) {
|
||||
this(name, ETag);
|
||||
this.localID = localID;
|
||||
}
|
||||
|
||||
/** initializes UID and remote file name (required for first upload) */
|
||||
public abstract void initialize();
|
||||
|
||||
/** fills the resource data from an input stream (for instance, .vcf file for Contact)
|
||||
* @param entity entity to parse
|
||||
* @param downloader will be used to fetch additional resources like contact images
|
||||
**/
|
||||
public abstract void parseEntity(InputStream entity, AssetDownloader downloader) throws IOException, InvalidResourceException;
|
||||
|
||||
|
||||
/* returns the MIME type that toEntity() will produce */
|
||||
public abstract String getMimeType();
|
||||
|
||||
/** writes the resource data to an output stream (for instance, .vcf file for Contact) */
|
||||
public abstract ByteArrayOutputStream toEntity() throws IOException;
|
||||
|
||||
|
||||
public interface AssetDownloader {
|
||||
byte[] download(URI url) throws URISyntaxException, IOException, HttpException, DavException;
|
||||
}
|
||||
}
|
||||
@@ -1,93 +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.Serializable;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URI;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
@RequiredArgsConstructor(suppressConstructorProperties=true)
|
||||
@Data
|
||||
public class ServerInfo implements Serializable {
|
||||
final private URI baseURI;
|
||||
final private String userName, password;
|
||||
final boolean authPreemptive;
|
||||
|
||||
private String errorMessage;
|
||||
|
||||
private boolean calDAV = false, cardDAV = false;
|
||||
private List<ResourceInfo>
|
||||
addressBooks = new LinkedList<>(),
|
||||
calendars = new LinkedList<>(),
|
||||
todoLists = new LinkedList<>();
|
||||
|
||||
|
||||
public boolean hasEnabledCalendars() {
|
||||
for (ResourceInfo calendar : calendars)
|
||||
if (calendar.enabled)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@RequiredArgsConstructor(suppressConstructorProperties=true)
|
||||
@Data
|
||||
public static class ResourceInfo {
|
||||
public enum Type {
|
||||
ADDRESS_BOOK,
|
||||
CALENDAR
|
||||
}
|
||||
|
||||
boolean enabled = false;
|
||||
|
||||
final Type type;
|
||||
final boolean readOnly;
|
||||
|
||||
final String URL, // absolute URL of resource
|
||||
title,
|
||||
description;
|
||||
final Integer color;
|
||||
|
||||
String timezone;
|
||||
|
||||
|
||||
// copy constructor
|
||||
public ResourceInfo(ResourceInfo src) {
|
||||
enabled = src.enabled;
|
||||
type = src.type;
|
||||
readOnly = src.readOnly;
|
||||
|
||||
URL = src.URL;
|
||||
title = src.title;
|
||||
description = src.description;
|
||||
color = src.color;
|
||||
|
||||
timezone = src.timezone;
|
||||
}
|
||||
|
||||
|
||||
// some logic
|
||||
|
||||
public String getTitle() {
|
||||
if (title == null) {
|
||||
try {
|
||||
java.net.URL url = new java.net.URL(URL);
|
||||
return url.getPath();
|
||||
} catch (MalformedURLException e) {
|
||||
return URL;
|
||||
}
|
||||
} else
|
||||
return title;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,218 +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.util.Log;
|
||||
|
||||
import net.fortuna.ical4j.data.CalendarBuilder;
|
||||
import net.fortuna.ical4j.data.CalendarOutputter;
|
||||
import net.fortuna.ical4j.data.ParserException;
|
||||
import net.fortuna.ical4j.model.Component;
|
||||
import net.fortuna.ical4j.model.ComponentList;
|
||||
import net.fortuna.ical4j.model.DateTime;
|
||||
import net.fortuna.ical4j.model.PropertyList;
|
||||
import net.fortuna.ical4j.model.TimeZone;
|
||||
import net.fortuna.ical4j.model.ValidationException;
|
||||
import net.fortuna.ical4j.model.component.VToDo;
|
||||
import net.fortuna.ical4j.model.property.Clazz;
|
||||
import net.fortuna.ical4j.model.property.Completed;
|
||||
import net.fortuna.ical4j.model.property.Created;
|
||||
import net.fortuna.ical4j.model.property.Description;
|
||||
import net.fortuna.ical4j.model.property.DtStart;
|
||||
import net.fortuna.ical4j.model.property.Due;
|
||||
import net.fortuna.ical4j.model.property.Duration;
|
||||
import net.fortuna.ical4j.model.property.LastModified;
|
||||
import net.fortuna.ical4j.model.property.Location;
|
||||
import net.fortuna.ical4j.model.property.PercentComplete;
|
||||
import net.fortuna.ical4j.model.property.Priority;
|
||||
import net.fortuna.ical4j.model.property.Status;
|
||||
import net.fortuna.ical4j.model.property.Summary;
|
||||
import net.fortuna.ical4j.model.property.Uid;
|
||||
import net.fortuna.ical4j.model.property.Url;
|
||||
import net.fortuna.ical4j.model.property.Version;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import at.bitfire.davdroid.Constants;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
public class Task extends iCalendar {
|
||||
private final static String TAG = "davdroid.Task";
|
||||
|
||||
@Getter @Setter DateTime createdAt;
|
||||
@Getter @Setter DateTime lastModified;
|
||||
|
||||
@Getter @Setter String summary, location, description, url;
|
||||
@Getter @Setter int priority;
|
||||
@Getter @Setter Clazz classification;
|
||||
@Getter @Setter Status status;
|
||||
|
||||
@Getter @Setter DtStart dtStart;
|
||||
@Getter @Setter Due due;
|
||||
@Getter @Setter Duration duration;
|
||||
@Getter @Setter Completed completedAt;
|
||||
@Getter @Setter Integer percentComplete;
|
||||
|
||||
|
||||
public Task(String name, String ETag) {
|
||||
super(name, ETag);
|
||||
}
|
||||
|
||||
public Task(long localId, String name, String ETag)
|
||||
{
|
||||
super(localId, name, ETag);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void parseEntity(InputStream entity, AssetDownloader downloader) throws IOException, InvalidResourceException {
|
||||
net.fortuna.ical4j.model.Calendar ical;
|
||||
try {
|
||||
CalendarBuilder builder = new CalendarBuilder();
|
||||
ical = builder.build(entity);
|
||||
|
||||
if (ical == null)
|
||||
throw new InvalidResourceException("No iCalendar found");
|
||||
} catch (ParserException e) {
|
||||
throw new InvalidResourceException(e);
|
||||
}
|
||||
|
||||
ComponentList notes = ical.getComponents(Component.VTODO);
|
||||
if (notes == null || notes.isEmpty())
|
||||
throw new InvalidResourceException("No VTODO found");
|
||||
VToDo todo = (VToDo)notes.get(0);
|
||||
|
||||
if (todo.getUid() != null)
|
||||
uid = todo.getUid().getValue();
|
||||
else {
|
||||
Log.w(TAG, "Received VTODO without UID, generating new one");
|
||||
generateUID();
|
||||
}
|
||||
|
||||
if (todo.getCreated() != null)
|
||||
createdAt = todo.getCreated().getDateTime();
|
||||
if (todo.getLastModified() != null)
|
||||
lastModified = todo.getLastModified().getDateTime();
|
||||
|
||||
if (todo.getSummary() != null)
|
||||
summary = todo.getSummary().getValue();
|
||||
if (todo.getLocation() != null)
|
||||
location = todo.getLocation().getValue();
|
||||
if (todo.getDescription() != null)
|
||||
description = todo.getDescription().getValue();
|
||||
if (todo.getUrl() != null)
|
||||
url = todo.getUrl().getValue();
|
||||
|
||||
priority = (todo.getPriority() != null) ? todo.getPriority().getLevel() : 0;
|
||||
if (todo.getClassification() != null)
|
||||
classification = todo.getClassification();
|
||||
if (todo.getStatus() != null)
|
||||
status = todo.getStatus();
|
||||
|
||||
if (todo.getDue() != null) {
|
||||
due = todo.getDue();
|
||||
validateTimeZone(due);
|
||||
}
|
||||
if (todo.getDuration() != null)
|
||||
duration = todo.getDuration();
|
||||
if (todo.getStartDate() != null) {
|
||||
dtStart = todo.getStartDate();
|
||||
validateTimeZone(dtStart);
|
||||
}
|
||||
if (todo.getDateCompleted() != null) {
|
||||
completedAt = todo.getDateCompleted();
|
||||
validateTimeZone(completedAt);
|
||||
}
|
||||
if (todo.getPercentComplete() != null)
|
||||
percentComplete = todo.getPercentComplete().getPercentage();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public ByteArrayOutputStream toEntity() throws IOException {
|
||||
final net.fortuna.ical4j.model.Calendar ical = new net.fortuna.ical4j.model.Calendar();
|
||||
ical.getProperties().add(Version.VERSION_2_0);
|
||||
ical.getProperties().add(Constants.ICAL_PRODID);
|
||||
|
||||
final VToDo todo = new VToDo();
|
||||
ical.getComponents().add(todo);
|
||||
final PropertyList props = todo.getProperties();
|
||||
|
||||
if (uid != null)
|
||||
props.add(new Uid(uid));
|
||||
|
||||
if (createdAt != null)
|
||||
props.add(new Created(createdAt));
|
||||
if (lastModified != null)
|
||||
props.add(new LastModified(lastModified));
|
||||
|
||||
if (summary != null)
|
||||
props.add(new Summary(summary));
|
||||
if (location != null)
|
||||
props.add(new Location(location));
|
||||
if (description != null)
|
||||
props.add(new Description(description));
|
||||
if (url != null)
|
||||
try {
|
||||
props.add(new Url(new URI(url)));
|
||||
} catch (URISyntaxException e) {
|
||||
Log.e(TAG, "Ignoring invalid task URL: " + url, e);
|
||||
}
|
||||
if (priority != 0)
|
||||
props.add(new Priority(priority));
|
||||
if (classification != null)
|
||||
props.add(classification);
|
||||
if (status != null)
|
||||
props.add(status);
|
||||
|
||||
// remember used time zones
|
||||
Set<TimeZone> usedTimeZones = new HashSet<>();
|
||||
|
||||
if (due != null) {
|
||||
props.add(due);
|
||||
if (due.getTimeZone() != null)
|
||||
usedTimeZones.add(due.getTimeZone());
|
||||
}
|
||||
if (duration != null)
|
||||
props.add(duration);
|
||||
if (dtStart != null) {
|
||||
props.add(dtStart);
|
||||
if (dtStart.getTimeZone() != null)
|
||||
usedTimeZones.add(dtStart.getTimeZone());
|
||||
}
|
||||
if (completedAt != null) {
|
||||
props.add(completedAt);
|
||||
if (completedAt.getTimeZone() != null)
|
||||
usedTimeZones.add(completedAt.getTimeZone());
|
||||
}
|
||||
if (percentComplete != null)
|
||||
props.add(new PercentComplete(percentComplete));
|
||||
|
||||
// add VTIMEZONE components
|
||||
for (TimeZone timeZone : usedTimeZones)
|
||||
ical.getComponents().add(timeZone.getVTimeZone());
|
||||
|
||||
CalendarOutputter output = new CalendarOutputter(false);
|
||||
ByteArrayOutputStream os = new ByteArrayOutputStream();
|
||||
try {
|
||||
output.output(ical, os);
|
||||
} catch (ValidationException e) {
|
||||
Log.e(TAG, "Generated invalid iCalendar");
|
||||
}
|
||||
return os;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,221 +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.util.Log;
|
||||
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.apache.http.impl.client.CloseableHttpClient;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
import at.bitfire.davdroid.URIUtils;
|
||||
import at.bitfire.davdroid.webdav.DavException;
|
||||
import at.bitfire.davdroid.webdav.DavMultiget;
|
||||
import at.bitfire.davdroid.webdav.DavNoContentException;
|
||||
import at.bitfire.davdroid.webdav.HttpException;
|
||||
import at.bitfire.davdroid.webdav.HttpPropfind;
|
||||
import at.bitfire.davdroid.webdav.WebDavResource;
|
||||
import at.bitfire.davdroid.webdav.WebDavResource.PutMode;
|
||||
import ezvcard.io.text.VCardParseException;
|
||||
import lombok.Cleanup;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* Represents a remotely stored synchronizable collection (collection as in
|
||||
* WebDAV terminology).
|
||||
*
|
||||
* @param <T> Subtype of Resource that can be stored in the collection
|
||||
*/
|
||||
public abstract class WebDavCollection<T extends Resource> {
|
||||
private static final String TAG = "davdroid.resource";
|
||||
|
||||
URI baseURI;
|
||||
@Getter WebDavResource collection;
|
||||
|
||||
abstract protected String memberAcceptedMimeTypes();
|
||||
abstract protected DavMultiget.Type multiGetType();
|
||||
|
||||
abstract protected T newResourceSkeleton(String name, String ETag);
|
||||
|
||||
public WebDavCollection(CloseableHttpClient httpClient, String baseURL, String user, String password, boolean preemptiveAuth) throws URISyntaxException {
|
||||
baseURI = URIUtils.parseURI(baseURL, false);
|
||||
collection = new WebDavResource(httpClient, baseURI, user, password, preemptiveAuth);
|
||||
}
|
||||
|
||||
|
||||
/* collection operations */
|
||||
|
||||
public void getProperties() throws URISyntaxException, IOException, HttpException, DavException {
|
||||
collection.propfind(HttpPropfind.Mode.COLLECTION_PROPERTIES);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns a body for the REPORT request that queries all members of the collection
|
||||
* that should be considered. Allows collections to implement remote filters.
|
||||
* @return body for REPORT request or null if PROPFIND shall be used
|
||||
*/
|
||||
public String getMemberETagsQuery() {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the ETags of the resources in a collection. If davQuery is set, it's required to be a
|
||||
* complete addressbook-query or calendar-query XML and will cause getMemberETags() to use REPORT
|
||||
* instead of PROPFIND.
|
||||
* @return array of Resources where only the resource names and ETags are set
|
||||
*/
|
||||
public Resource[] getMemberETags() throws URISyntaxException, IOException, DavException, HttpException {
|
||||
String query = getMemberETagsQuery();
|
||||
if (query != null)
|
||||
collection.report(query);
|
||||
else
|
||||
collection.propfind(HttpPropfind.Mode.MEMBERS_ETAG);
|
||||
|
||||
List<T> resources = new LinkedList<>();
|
||||
if (collection.getMembers() != null)
|
||||
for (WebDavResource member : collection.getMembers())
|
||||
resources.add(newResourceSkeleton(member.getName(), member.getProperties().getETag()));
|
||||
|
||||
return resources.toArray(new Resource[resources.size()]);
|
||||
|
||||
}
|
||||
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public Resource[] multiGet(Resource[] resources) throws URISyntaxException, IOException, DavException, HttpException {
|
||||
try {
|
||||
if (resources.length == 1)
|
||||
return new Resource[] { get(resources[0]) };
|
||||
|
||||
Log.i(TAG, "Multi-getting " + resources.length + " remote resource(s)");
|
||||
|
||||
LinkedList<String> names = new LinkedList<>();
|
||||
for (Resource resource : resources)
|
||||
names.add(resource.getName());
|
||||
|
||||
LinkedList<T> foundResources = new LinkedList<>();
|
||||
collection.multiGet(multiGetType(), names.toArray(new String[names.size()]));
|
||||
if (collection.getMembers() == null)
|
||||
throw new DavNoContentException();
|
||||
|
||||
for (WebDavResource member : collection.getMembers()) {
|
||||
T resource = newResourceSkeleton(member.getName(), member.getProperties().getETag());
|
||||
try {
|
||||
if (member.getContent() != null) {
|
||||
@Cleanup InputStream is = new ByteArrayInputStream(member.getContent());
|
||||
resource.parseEntity(is, getDownloader());
|
||||
foundResources.add(resource);
|
||||
} else
|
||||
Log.e(TAG, "Ignoring entity without content");
|
||||
} catch (InvalidResourceException e) {
|
||||
Log.e(TAG, "Ignoring unparseable entity in multi-response", e);
|
||||
}
|
||||
}
|
||||
|
||||
return foundResources.toArray(new Resource[foundResources.size()]);
|
||||
} catch (InvalidResourceException e) {
|
||||
Log.e(TAG, "Couldn't parse entity from GET", e);
|
||||
}
|
||||
|
||||
return new Resource[0];
|
||||
}
|
||||
|
||||
|
||||
/* internal member operations */
|
||||
|
||||
public Resource get(Resource resource) throws URISyntaxException, IOException, HttpException, DavException, InvalidResourceException {
|
||||
WebDavResource member = new WebDavResource(collection, resource.getName());
|
||||
|
||||
member.get(memberAcceptedMimeTypes());
|
||||
|
||||
byte[] data = member.getContent();
|
||||
if (data == null)
|
||||
throw new DavNoContentException();
|
||||
|
||||
@Cleanup InputStream is = new ByteArrayInputStream(data);
|
||||
try {
|
||||
resource.parseEntity(is, getDownloader());
|
||||
} catch (VCardParseException e) {
|
||||
throw new InvalidResourceException(e);
|
||||
}
|
||||
return resource;
|
||||
}
|
||||
|
||||
// returns ETag of the created resource, if returned by server
|
||||
public String add(Resource res) throws URISyntaxException, IOException, HttpException {
|
||||
WebDavResource member = new WebDavResource(collection, res.getName(), res.getETag());
|
||||
member.getProperties().setContentType(res.getMimeType());
|
||||
|
||||
@Cleanup ByteArrayOutputStream os = res.toEntity();
|
||||
String eTag = member.put(os.toByteArray(), PutMode.ADD_DONT_OVERWRITE);
|
||||
|
||||
// after a successful upload, the collection has implicitely changed, too
|
||||
collection.getProperties().invalidateCTag();
|
||||
|
||||
return eTag;
|
||||
}
|
||||
|
||||
public void delete(Resource res) throws URISyntaxException, IOException, HttpException {
|
||||
WebDavResource member = new WebDavResource(collection, res.getName(), res.getETag());
|
||||
member.delete();
|
||||
|
||||
collection.getProperties().invalidateCTag();
|
||||
}
|
||||
|
||||
// returns ETag of the updated resource, if returned by server
|
||||
public String update(Resource res) throws URISyntaxException, IOException, HttpException {
|
||||
WebDavResource member = new WebDavResource(collection, res.getName(), res.getETag());
|
||||
member.getProperties().setContentType(res.getMimeType());
|
||||
|
||||
@Cleanup ByteArrayOutputStream os = res.toEntity();
|
||||
String eTag = member.put(os.toByteArray(), PutMode.UPDATE_DONT_OVERWRITE);
|
||||
|
||||
// after a successful upload, the collection has implicitely changed, too
|
||||
collection.getProperties().invalidateCTag();
|
||||
|
||||
return eTag;
|
||||
}
|
||||
|
||||
|
||||
// helpers
|
||||
|
||||
Resource.AssetDownloader getDownloader() {
|
||||
return new Resource.AssetDownloader() {
|
||||
@Override
|
||||
public byte[] download(URI uri) throws URISyntaxException, IOException, HttpException, DavException {
|
||||
if (!uri.isAbsolute())
|
||||
throw new URISyntaxException(uri.toString(), "URI referenced from entity must be absolute");
|
||||
|
||||
// send credentials when asset has same URI authority as baseURI
|
||||
// we have to construct these helper URIs because
|
||||
// "https://server:443" is NOT equal to "https://server" otherwise
|
||||
URI baseUriAuthority = new URI(baseURI.getScheme(), null, baseURI.getHost(), baseURI.getPort(), null, null, null),
|
||||
assetUriAuthority = new URI(uri.getScheme(), null, uri.getHost(), baseURI.getPort(), null, null, null);
|
||||
|
||||
if (baseUriAuthority.equals(assetUriAuthority)) {
|
||||
Log.d(TAG, "Asset is on same server, sending credentials");
|
||||
WebDavResource file = new WebDavResource(collection, uri);
|
||||
file.get("image/*");
|
||||
return file.getContent();
|
||||
|
||||
} else
|
||||
// resource is on an external server, don't send credentials
|
||||
return IOUtils.toByteArray(uri);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,112 +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.util.Log;
|
||||
|
||||
import net.fortuna.ical4j.data.CalendarBuilder;
|
||||
import net.fortuna.ical4j.data.ParserException;
|
||||
import net.fortuna.ical4j.model.DateTime;
|
||||
import net.fortuna.ical4j.model.component.VTimeZone;
|
||||
import net.fortuna.ical4j.model.property.DateProperty;
|
||||
import net.fortuna.ical4j.util.CompatibilityHints;
|
||||
import net.fortuna.ical4j.util.SimpleHostInfo;
|
||||
import net.fortuna.ical4j.util.UidGenerator;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.StringReader;
|
||||
import java.util.TimeZone;
|
||||
|
||||
import at.bitfire.davdroid.DateUtils;
|
||||
import at.bitfire.davdroid.syncadapter.DavSyncAdapter;
|
||||
import lombok.NonNull;
|
||||
|
||||
public abstract class iCalendar extends Resource {
|
||||
static private final String TAG = "DAVdroid.iCal";
|
||||
|
||||
// static ical4j initialization
|
||||
static {
|
||||
CompatibilityHints.setHintEnabled(CompatibilityHints.KEY_RELAXED_UNFOLDING, true);
|
||||
CompatibilityHints.setHintEnabled(CompatibilityHints.KEY_RELAXED_PARSING, true);
|
||||
CompatibilityHints.setHintEnabled(CompatibilityHints.KEY_OUTLOOK_COMPATIBILITY, true);
|
||||
}
|
||||
|
||||
|
||||
public iCalendar(long localID, String name, String ETag) {
|
||||
super(localID, name, ETag);
|
||||
}
|
||||
|
||||
public iCalendar(String name, String ETag) {
|
||||
super(name, ETag);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
generateUID();
|
||||
name = uid + ".ics";
|
||||
}
|
||||
|
||||
protected void generateUID() {
|
||||
UidGenerator generator = new UidGenerator(new SimpleHostInfo(DavSyncAdapter.getAndroidID()), String.valueOf(android.os.Process.myPid()));
|
||||
uid = generator.generateUid().getValue().replace("@", "_");
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String getMimeType() {
|
||||
return "text/calendar";
|
||||
}
|
||||
|
||||
|
||||
// time zone helpers
|
||||
|
||||
protected static boolean isDateTime(DateProperty date) {
|
||||
return date.getDate() instanceof DateTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that a given DateProperty has a time zone with an ID that is available in Android.
|
||||
* @param date DateProperty to validate. Values which are not DATE-TIME will be ignored.
|
||||
*/
|
||||
protected static void validateTimeZone(DateProperty date) {
|
||||
if (isDateTime(date)) {
|
||||
final TimeZone tz = date.getTimeZone();
|
||||
if (tz == null)
|
||||
return;
|
||||
final String tzID = tz.getID();
|
||||
if (tzID == null)
|
||||
return;
|
||||
|
||||
String deviceTzID = DateUtils.findAndroidTimezoneID(tzID);
|
||||
if (!tzID.equals(deviceTzID))
|
||||
date.setTimeZone(DateUtils.tzRegistry.getTimeZone(deviceTzID));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a string with a timezone definition and returns the time zone ID.
|
||||
* @param timezoneDef time zone definition (VCALENDAR with VTIMEZONE component)
|
||||
* @return time zone id (TZID) if VTIMEZONE contains a TZID,
|
||||
* null otherwise
|
||||
*/
|
||||
public static String TimezoneDefToTzId(@NonNull String timezoneDef) {
|
||||
try {
|
||||
CalendarBuilder builder = new CalendarBuilder();
|
||||
net.fortuna.ical4j.model.Calendar cal = builder.build(new StringReader(timezoneDef));
|
||||
VTimeZone timezone = (VTimeZone)cal.getComponent(VTimeZone.VTIMEZONE);
|
||||
if (timezone != null && timezone.getTimeZoneId() != null)
|
||||
return timezone.getTimeZoneId().getValue();
|
||||
} catch (IOException|ParserException e) {
|
||||
Log.e(TAG, "Can't understand time zone definition", e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -18,7 +18,7 @@ import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.os.IBinder;
|
||||
|
||||
import at.bitfire.davdroid.ui.setup.AddAccountActivity;
|
||||
import at.bitfire.davdroid.ui.setup.LoginActivity;
|
||||
|
||||
public class AccountAuthenticatorService extends Service {
|
||||
private static AccountAuthenticator accountAuthenticator;
|
||||
@@ -48,7 +48,7 @@ public class AccountAuthenticatorService extends Service {
|
||||
@Override
|
||||
public Bundle addAccount(AccountAuthenticatorResponse response, String accountType, String authTokenType,
|
||||
String[] requiredFeatures, Bundle options) throws NetworkErrorException {
|
||||
Intent intent = new Intent(context, AddAccountActivity.class);
|
||||
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);
|
||||
@@ -65,7 +65,7 @@ public class AccountAuthenticatorService extends Service {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Override
|
||||
public Bundle getAuthToken(AccountAuthenticatorResponse response, Account account, String authTokenType, Bundle options) throws NetworkErrorException {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1,218 +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.accounts.AccountManager;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.ContentUris;
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.content.PeriodicSync;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.provider.CalendarContract;
|
||||
import android.provider.CalendarContract.Calendars;
|
||||
import android.util.Log;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.List;
|
||||
|
||||
import at.bitfire.davdroid.resource.ServerInfo;
|
||||
import ezvcard.VCardVersion;
|
||||
import lombok.Cleanup;
|
||||
|
||||
public class AccountSettings {
|
||||
private final static String TAG = "davdroid.AccountSettings";
|
||||
|
||||
private final static int CURRENT_VERSION = 1;
|
||||
private final static String
|
||||
KEY_SETTINGS_VERSION = "version",
|
||||
|
||||
KEY_USERNAME = "user_name",
|
||||
KEY_AUTH_PREEMPTIVE = "auth_preemptive",
|
||||
|
||||
KEY_ADDRESSBOOK_URL = "addressbook_url",
|
||||
KEY_ADDRESSBOOK_CTAG = "addressbook_ctag",
|
||||
KEY_ADDRESSBOOK_VCARD_VERSION = "addressbook_vcard_version";
|
||||
|
||||
public final static long SYNC_INTERVAL_MANUALLY = -1;
|
||||
|
||||
final Context context;
|
||||
final AccountManager accountManager;
|
||||
final Account account;
|
||||
|
||||
|
||||
public AccountSettings(Context context, Account account) {
|
||||
this.context = context;
|
||||
this.account = account;
|
||||
|
||||
accountManager = AccountManager.get(context);
|
||||
|
||||
synchronized(AccountSettings.class) {
|
||||
int version = 0;
|
||||
try {
|
||||
version = Integer.parseInt(accountManager.getUserData(account, KEY_SETTINGS_VERSION));
|
||||
} catch(NumberFormatException e) {
|
||||
}
|
||||
if (version < CURRENT_VERSION)
|
||||
update(version);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static Bundle createBundle(ServerInfo serverInfo) {
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putString(KEY_SETTINGS_VERSION, String.valueOf(CURRENT_VERSION));
|
||||
bundle.putString(KEY_USERNAME, serverInfo.getUserName());
|
||||
bundle.putString(KEY_AUTH_PREEMPTIVE, Boolean.toString(serverInfo.isAuthPreemptive()));
|
||||
for (ServerInfo.ResourceInfo addressBook : serverInfo.getAddressBooks())
|
||||
if (addressBook.isEnabled()) {
|
||||
bundle.putString(KEY_ADDRESSBOOK_URL, addressBook.getURL());
|
||||
break;
|
||||
}
|
||||
return bundle;
|
||||
}
|
||||
|
||||
|
||||
// authentication settings
|
||||
|
||||
public String getUserName() {
|
||||
return accountManager.getUserData(account, KEY_USERNAME);
|
||||
}
|
||||
public void setUserName(String userName) { accountManager.setUserData(account, KEY_USERNAME, userName); }
|
||||
|
||||
public String getPassword() {
|
||||
return accountManager.getPassword(account);
|
||||
}
|
||||
public void setPassword(String password) { accountManager.setPassword(account, password); }
|
||||
|
||||
public boolean getPreemptiveAuth() { return Boolean.parseBoolean(accountManager.getUserData(account, KEY_AUTH_PREEMPTIVE)); }
|
||||
public void setPreemptiveAuth(boolean preemptive) { accountManager.setUserData(account, KEY_AUTH_PREEMPTIVE, Boolean.toString(preemptive)); }
|
||||
|
||||
|
||||
// sync. settings
|
||||
|
||||
public Long getSyncInterval(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(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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// address book (CardDAV) settings
|
||||
|
||||
public String getAddressBookURL() {
|
||||
return accountManager.getUserData(account, KEY_ADDRESSBOOK_URL);
|
||||
}
|
||||
|
||||
public String getAddressBookCTag() {
|
||||
return accountManager.getUserData(account, KEY_ADDRESSBOOK_CTAG);
|
||||
}
|
||||
|
||||
public void setAddressBookCTag(String cTag) {
|
||||
accountManager.setUserData(account, KEY_ADDRESSBOOK_CTAG, cTag);
|
||||
}
|
||||
|
||||
public VCardVersion getAddressBookVCardVersion() {
|
||||
VCardVersion version = VCardVersion.V3_0;
|
||||
String versionStr = accountManager.getUserData(account, KEY_ADDRESSBOOK_VCARD_VERSION);
|
||||
if (versionStr != null)
|
||||
version = VCardVersion.valueOfByStr(versionStr);
|
||||
return version;
|
||||
}
|
||||
|
||||
public void setAddressBookVCardVersion(VCardVersion version) {
|
||||
accountManager.setUserData(account, KEY_ADDRESSBOOK_VCARD_VERSION, version.getVersion());
|
||||
}
|
||||
|
||||
|
||||
// update from previous account settings
|
||||
|
||||
private void update(int fromVersion) {
|
||||
Log.i(TAG, "Account settings must be updated from v" + fromVersion + " to v" + CURRENT_VERSION);
|
||||
for (int toVersion = CURRENT_VERSION; toVersion > fromVersion; toVersion--)
|
||||
update(fromVersion, toVersion);
|
||||
}
|
||||
|
||||
private void update(int fromVersion, int toVersion) {
|
||||
Log.i(TAG, "Updating account settings from v" + fromVersion + " to " + toVersion);
|
||||
try {
|
||||
if (fromVersion == 0 && toVersion == 1)
|
||||
update_0_1();
|
||||
else
|
||||
Log.wtf(TAG, "Don't know how to update settings from v" + fromVersion + " to v" + toVersion);
|
||||
} catch(Exception e) {
|
||||
Log.e(TAG, "Couldn't update account settings (DAVdroid will probably crash)!", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void update_0_1() throws URISyntaxException {
|
||||
String v0_principalURL = accountManager.getUserData(account, "principal_url"),
|
||||
v0_addressBookPath = accountManager.getUserData(account, "addressbook_path");
|
||||
Log.d(TAG, "Old principal URL = " + v0_principalURL);
|
||||
Log.d(TAG, "Old address book path = " + v0_addressBookPath);
|
||||
|
||||
URI principalURI = new URI(v0_principalURL);
|
||||
|
||||
// update address book
|
||||
if (v0_addressBookPath != null) {
|
||||
String addressBookURL = principalURI.resolve(v0_addressBookPath).toASCIIString();
|
||||
Log.d(TAG, "New address book URL = " + addressBookURL);
|
||||
accountManager.setUserData(account, "addressbook_url", addressBookURL);
|
||||
}
|
||||
|
||||
// update calendars
|
||||
ContentResolver resolver = context.getContentResolver();
|
||||
Uri calendars = Calendars.CONTENT_URI.buildUpon()
|
||||
.appendQueryParameter(Calendars.ACCOUNT_NAME, account.name)
|
||||
.appendQueryParameter(Calendars.ACCOUNT_TYPE, account.type)
|
||||
.appendQueryParameter(CalendarContract.CALLER_IS_SYNCADAPTER, "true").build();
|
||||
@Cleanup Cursor cursor = resolver.query(calendars, new String[] { Calendars._ID, Calendars.NAME }, null, null, null);
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
int id = cursor.getInt(0);
|
||||
String v0_path = cursor.getString(1),
|
||||
v1_url = principalURI.resolve(v0_path).toASCIIString();
|
||||
Log.d(TAG, "Updating calendar #" + id + " name: " + v0_path + " -> " + v1_url);
|
||||
Uri calendar = ContentUris.appendId(Calendars.CONTENT_URI.buildUpon()
|
||||
.appendQueryParameter(Calendars.ACCOUNT_NAME, account.name)
|
||||
.appendQueryParameter(Calendars.ACCOUNT_TYPE, account.type)
|
||||
.appendQueryParameter(CalendarContract.CALLER_IS_SYNCADAPTER, "true"), id).build();
|
||||
ContentValues newValues = new ContentValues(1);
|
||||
newValues.put(Calendars.NAME, v1_url);
|
||||
if (resolver.update(calendar, newValues, null, null) != 1)
|
||||
Log.e(TAG, "Number of modified calendars != 1");
|
||||
}
|
||||
|
||||
Log.d(TAG, "Cleaning old principal URL and address book path");
|
||||
accountManager.setUserData(account, "principal_url", null);
|
||||
accountManager.setUserData(account, "addressbook_path", null);
|
||||
|
||||
Log.d(TAG, "Updated settings successfully!");
|
||||
accountManager.setUserData(account, KEY_SETTINGS_VERSION, "1");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
/*
|
||||
* 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");
|
||||
String eTag = ((GetETag)remote.properties.get(GetETag.NAME)).eTag;
|
||||
|
||||
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, 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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -8,73 +8,141 @@
|
||||
package at.bitfire.davdroid.syncadapter;
|
||||
|
||||
import android.accounts.Account;
|
||||
import android.app.Service;
|
||||
import android.content.AbstractThreadedSyncAdapter;
|
||||
import android.content.ContentProviderClient;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.IBinder;
|
||||
import android.os.RemoteException;
|
||||
import android.util.Log;
|
||||
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.net.URISyntaxException;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import at.bitfire.davdroid.resource.CalDavCalendar;
|
||||
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.davdroid.resource.LocalCollection;
|
||||
import at.bitfire.davdroid.resource.WebDavCollection;
|
||||
import at.bitfire.ical4android.CalendarStorageException;
|
||||
import lombok.Cleanup;
|
||||
|
||||
public class CalendarsSyncAdapterService extends Service {
|
||||
private static SyncAdapter syncAdapter;
|
||||
public class CalendarsSyncAdapterService extends SyncAdapterService {
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
if (syncAdapter == null)
|
||||
syncAdapter = new SyncAdapter(getApplicationContext());
|
||||
}
|
||||
@Override
|
||||
protected AbstractThreadedSyncAdapter syncAdapter() {
|
||||
return new SyncAdapter(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
syncAdapter.close();
|
||||
syncAdapter = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
return syncAdapter.getSyncAdapterBinder();
|
||||
}
|
||||
|
||||
private static class SyncAdapter extends SyncAdapterService.SyncAdapter {
|
||||
|
||||
private static class SyncAdapter extends DavSyncAdapter {
|
||||
private final static String TAG = "davdroid.CalendarsSync";
|
||||
public SyncAdapter(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
private SyncAdapter(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Map<LocalCollection<?>, WebDavCollection<?>> getSyncPairs(Account account, ContentProviderClient provider) {
|
||||
AccountSettings settings = new AccountSettings(getContext(), account);
|
||||
String userName = settings.getUserName(),
|
||||
password = settings.getPassword();
|
||||
boolean preemptive = settings.getPreemptiveAuth();
|
||||
@Override
|
||||
public void onPerformSync(Account account, Bundle extras, String authority, ContentProviderClient provider, SyncResult syncResult) {
|
||||
super.onPerformSync(account, extras, authority, provider, 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;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
Map<LocalCollection<?>, WebDavCollection<?>> map = new HashMap<>();
|
||||
|
||||
for (LocalCalendar calendar : LocalCalendar.findAll(account, provider)) {
|
||||
WebDavCollection<?> dav = new CalDavCalendar(httpClient, calendar.getUrl(), userName, password, preemptive);
|
||||
map.put(calendar, dav);
|
||||
}
|
||||
return map;
|
||||
} catch (RemoteException ex) {
|
||||
Log.e(TAG, "Couldn't find local calendars", ex);
|
||||
} catch (URISyntaxException ex) {
|
||||
Log.e(TAG, "Couldn't build calendar URI", ex);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,74 +8,100 @@
|
||||
package at.bitfire.davdroid.syncadapter;
|
||||
|
||||
import android.accounts.Account;
|
||||
import android.app.Service;
|
||||
import android.content.AbstractThreadedSyncAdapter;
|
||||
import android.content.ContentProviderClient;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.IBinder;
|
||||
import android.util.Log;
|
||||
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.net.URISyntaxException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import at.bitfire.davdroid.resource.CardDavAddressBook;
|
||||
import at.bitfire.davdroid.resource.LocalAddressBook;
|
||||
import at.bitfire.davdroid.resource.LocalCollection;
|
||||
import at.bitfire.davdroid.resource.WebDavCollection;
|
||||
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 Service {
|
||||
private static ContactsSyncAdapter syncAdapter;
|
||||
public class ContactsSyncAdapterService extends SyncAdapterService {
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
if (syncAdapter == null)
|
||||
syncAdapter = new ContactsSyncAdapter(getApplicationContext());
|
||||
}
|
||||
@Override
|
||||
protected AbstractThreadedSyncAdapter syncAdapter() {
|
||||
return new ContactsSyncAdapter(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
syncAdapter.close();
|
||||
syncAdapter = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
return syncAdapter.getSyncAdapterBinder();
|
||||
}
|
||||
|
||||
private static class ContactsSyncAdapter extends SyncAdapter {
|
||||
|
||||
private static class ContactsSyncAdapter extends DavSyncAdapter {
|
||||
private final static String TAG = "davdroid.ContactsSync";
|
||||
public ContactsSyncAdapter(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
private ContactsSyncAdapter(Context context) {
|
||||
super(context);
|
||||
}
|
||||
@Override
|
||||
public void onPerformSync(Account account, Bundle extras, String authority, ContentProviderClient provider, SyncResult syncResult) {
|
||||
super.onPerformSync(account, extras, authority, provider, syncResult);
|
||||
|
||||
@Override
|
||||
protected Map<LocalCollection<?>, WebDavCollection<?>> getSyncPairs(Account account, ContentProviderClient provider) {
|
||||
AccountSettings settings = new AccountSettings(getContext(), account);
|
||||
String userName = settings.getUserName(),
|
||||
password = settings.getPassword();
|
||||
boolean preemptive = settings.getPreemptiveAuth();
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
String addressBookURL = settings.getAddressBookURL();
|
||||
if (addressBookURL == null)
|
||||
return null;
|
||||
|
||||
try {
|
||||
LocalCollection<?> database = new LocalAddressBook(account, provider, settings);
|
||||
WebDavCollection<?> dav = new CardDavAddressBook(settings, httpClient, addressBookURL, userName, password, preemptive);
|
||||
|
||||
Map<LocalCollection<?>, WebDavCollection<?>> map = new HashMap<>();
|
||||
map.put(database, dav);
|
||||
|
||||
return map;
|
||||
} catch (URISyntaxException ex) {
|
||||
Log.e(TAG, "Couldn't build address book URI", ex);
|
||||
}
|
||||
|
||||
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