Compare commits
132 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8afc55dff3 | ||
|
|
9a63dd4693 | ||
|
|
2683012ec3 | ||
|
|
a79a59f409 | ||
|
|
81eabf5961 | ||
|
|
495cdf7c7e | ||
|
|
f6eee6c910 | ||
|
|
a405d07baf | ||
|
|
2696e64a83 | ||
|
|
6d6835c3b7 | ||
|
|
0eb6a56ef1 | ||
|
|
df335335d2 | ||
|
|
35011445e0 | ||
|
|
08789bbb2c | ||
|
|
001b445222 | ||
|
|
7d5ed0bd11 | ||
|
|
392e9f963e | ||
|
|
8d5f815be5 | ||
|
|
100aa665f5 | ||
|
|
caf7adb18a | ||
|
|
13b8c86ce3 | ||
|
|
a15185372d | ||
|
|
03d6dfef02 | ||
|
|
a021b973fe | ||
|
|
7e592d7647 | ||
|
|
38a4f3f53c | ||
|
|
986213dda5 | ||
|
|
d98a9d3673 | ||
|
|
12cb0ff8fe | ||
|
|
3a8f17cc2e | ||
|
|
9b52e51e4e | ||
|
|
be235c39b0 | ||
|
|
1acd2e1a55 | ||
|
|
23dba581e1 | ||
|
|
b29756aff2 | ||
|
|
4e2809910c | ||
|
|
9394c10d29 | ||
|
|
6d55de7c1d | ||
|
|
933f99b563 | ||
|
|
aeca582a7c | ||
|
|
6c998c31c3 | ||
|
|
1a796ade60 | ||
|
|
9e082d930b | ||
|
|
a7115ad39c | ||
|
|
4eb03f78f5 | ||
|
|
a6a7420935 | ||
|
|
3cba163c3b | ||
|
|
ca0ad612a7 | ||
|
|
897ede7582 | ||
|
|
6f5748c464 | ||
|
|
0423e00ffd | ||
|
|
ac46b8f45f | ||
|
|
142e229ff3 | ||
|
|
e65a1b5f16 | ||
|
|
bfbd4569ed | ||
|
|
a52e3507e7 | ||
|
|
49f7bbd8da | ||
|
|
8a75552a4c | ||
|
|
5ce03abad6 | ||
|
|
a7e8221e26 | ||
|
|
0cc23e3ecf | ||
|
|
661738c866 | ||
|
|
d56175652c | ||
|
|
760632302d | ||
|
|
5b21cb4938 | ||
|
|
03a64a43a3 | ||
|
|
1ad25a748a | ||
|
|
51ac34790c | ||
|
|
bc8d63f233 | ||
|
|
9452204f5c | ||
|
|
e98f0bd1ed | ||
|
|
dba35a5296 | ||
|
|
751e2eb5a4 | ||
|
|
4bf613c28b | ||
|
|
3e87f045d5 | ||
|
|
3f1740bc1c | ||
|
|
c8da282db0 | ||
|
|
6f0b9421c1 | ||
|
|
4b250dcb66 | ||
|
|
b6386c1b95 | ||
|
|
378214f62a | ||
|
|
c10fc6dfd1 | ||
|
|
cc1a1741f9 | ||
|
|
83eecc0c72 | ||
|
|
cfc71542f5 | ||
|
|
487509cb0c | ||
|
|
31f3fc80ef | ||
|
|
2e6a3efd25 | ||
|
|
2f5622edaf | ||
|
|
d73f4c5937 | ||
|
|
4c2e66d44c | ||
|
|
8d4c353d8c | ||
|
|
7bfcb2d8ad | ||
|
|
e3a7c7092e | ||
|
|
65376c2d42 | ||
|
|
58efd9ba03 | ||
|
|
fd4e5921ea | ||
|
|
6d9fc8734f | ||
|
|
7501f87d03 | ||
|
|
4205bf34fd | ||
|
|
25c1a1ad51 | ||
|
|
69c94c8d93 | ||
|
|
e9901f38f5 | ||
|
|
b8a728bdb9 | ||
|
|
1ec1db3045 | ||
|
|
b2bd53f36f | ||
|
|
3601678fe8 | ||
|
|
7cdf71fcdc | ||
|
|
97ec7ca721 | ||
|
|
9e49de1116 | ||
|
|
7e20bcd6f5 | ||
|
|
428c09c390 | ||
|
|
c1bd4e2156 | ||
|
|
0b5aa52642 | ||
|
|
ebe13cef5e | ||
|
|
5e5da95e23 | ||
|
|
07ec3c1a0a | ||
|
|
b8e6ff4627 | ||
|
|
732fe8151b | ||
|
|
aeaff0a0e6 | ||
|
|
7a3867b95c | ||
|
|
6b52d03002 | ||
|
|
a286b485e3 | ||
|
|
88972aed1d | ||
|
|
225ffc07cf | ||
|
|
1235a5e45a | ||
|
|
bdee53b5ab | ||
|
|
2c79ae20e5 | ||
|
|
75e5a59948 | ||
|
|
02d72e4f51 | ||
|
|
9a809c9761 | ||
|
|
d712238700 |
99
.gitignore
vendored
@@ -1,61 +1,86 @@
|
||||
### ANDROID
|
||||
# Created by https://www.gitignore.io
|
||||
|
||||
# built application files
|
||||
### Android ###
|
||||
# Built application files
|
||||
*.apk
|
||||
*.ap_
|
||||
|
||||
# files for the dex VM
|
||||
# Files for the Dalvik VM
|
||||
*.dex
|
||||
|
||||
# Java class files
|
||||
*.class
|
||||
|
||||
# generated files
|
||||
# Generated files
|
||||
bin/
|
||||
doc/javadoc/
|
||||
gen/
|
||||
|
||||
# Gradle files
|
||||
.gradle/
|
||||
build/
|
||||
|
||||
# Local configuration file (sdk path, etc)
|
||||
local.properties
|
||||
|
||||
# Eclipse project files
|
||||
.classpath
|
||||
.project
|
||||
|
||||
# Proguard folder generated by Eclipse
|
||||
proguard/
|
||||
|
||||
# Intellij project files
|
||||
# Log Files
|
||||
*.log
|
||||
|
||||
|
||||
### Intellij ###
|
||||
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm
|
||||
|
||||
*.iml
|
||||
|
||||
## Directory-based project format:
|
||||
.idea/
|
||||
# if you remove the above rule, at least ignore the following:
|
||||
|
||||
# User-specific stuff:
|
||||
# .idea/workspace.xml
|
||||
# .idea/tasks.xml
|
||||
# .idea/dictionaries
|
||||
|
||||
# Sensitive or high-churn files:
|
||||
# .idea/dataSources.ids
|
||||
# .idea/dataSources.xml
|
||||
# .idea/sqlDataSources.xml
|
||||
# .idea/dynamic.xml
|
||||
# .idea/uiDesigner.xml
|
||||
|
||||
# Gradle:
|
||||
# .idea/gradle.xml
|
||||
# .idea/libraries
|
||||
|
||||
# Mongo Explorer plugin:
|
||||
# .idea/mongoSettings.xml
|
||||
|
||||
## File-based project format:
|
||||
*.ipr
|
||||
*.iws
|
||||
.idea/
|
||||
|
||||
## Plugin-specific files:
|
||||
|
||||
# IntelliJ
|
||||
out/
|
||||
|
||||
# mpeltonen/sbt-idea plugin
|
||||
.idea_modules/
|
||||
|
||||
# JIRA plugin
|
||||
atlassian-ide-plugin.xml
|
||||
|
||||
# Crashlytics plugin (for Android Studio and IntelliJ)
|
||||
com_crashlytics_export_strings.xml
|
||||
crashlytics.properties
|
||||
crashlytics-build.properties
|
||||
|
||||
|
||||
### ECLIPSE
|
||||
### Gradle ###
|
||||
.gradle
|
||||
build/
|
||||
|
||||
*.pydevproject
|
||||
.metadata
|
||||
bin/**
|
||||
tmp/**
|
||||
tmp/**/*
|
||||
*.tmp
|
||||
*.bak
|
||||
*.swp
|
||||
*~.nib
|
||||
local.properties
|
||||
.classpath
|
||||
.settings/
|
||||
.loadpath
|
||||
|
||||
# External tool builders
|
||||
.externalToolBuilders/
|
||||
|
||||
# Locally stored "Eclipse launch configurations"
|
||||
*.launch
|
||||
|
||||
# CDT-specific
|
||||
.cproject
|
||||
|
||||
# PDT-specific
|
||||
.buildpath
|
||||
# Ignore Gradle GUI config
|
||||
gradle-app.setting
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
DAVDROID
|
||||
========
|
||||
|
||||
Please see the [DAVdroid Web site](http://davdroid.bitfire.at) for
|
||||
Please see the [DAVdroid Web site](https://davdroid.bitfire.at) for
|
||||
detailled information about DAVdroid.
|
||||
|
||||
DAVdroid is licensed under the [GPLv3 License](COPYING).
|
||||
@@ -13,9 +13,9 @@ Twitter: [@davdroidapp](https://twitter.com/davdroidapp)
|
||||
USED THIRD-PARTY LIBRARIES
|
||||
==========================
|
||||
|
||||
* [Apache HttpClient](http://hc.apache.org) ([httpclientandroidlib](https://code.google.com/p/httpclientandroidlib/) flavour) – [Apache License](http://www.apache.org/licenses/)
|
||||
* [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)
|
||||
* [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)
|
||||
* [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)
|
||||
|
||||
2
app/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
build
|
||||
target
|
||||
69
app/build.gradle
Normal file
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* 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
|
||||
*/
|
||||
|
||||
apply plugin: 'com.android.application'
|
||||
|
||||
android {
|
||||
compileSdkVersion 21
|
||||
buildToolsVersion '21.1.2'
|
||||
|
||||
defaultConfig {
|
||||
applicationId "at.bitfire.davdroid"
|
||||
minSdkVersion 14
|
||||
targetSdkVersion 21
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
debug {
|
||||
minifyEnabled false
|
||||
}
|
||||
release {
|
||||
minifyEnabled true
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
|
||||
}
|
||||
}
|
||||
lintOptions {
|
||||
abortOnError false
|
||||
}
|
||||
|
||||
packagingOptions {
|
||||
exclude 'META-INF/LICENSE.txt'
|
||||
exclude 'META-INF/NOTICE.txt'
|
||||
}
|
||||
}
|
||||
|
||||
configurations.all {
|
||||
exclude module: 'commons-logging' // undocumented part of Android
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// Apache Commons
|
||||
compile 'commons-lang:commons-lang:2.6'
|
||||
compile 'commons-io:commons-io:2.4'
|
||||
// Lombok for useful @helpers
|
||||
provided 'org.projectlombok:lombok:1.14.8'
|
||||
// ical4j for parsing/generating iCalendars
|
||||
compile 'org.mnode.ical4j:ical4j:1.0.6'
|
||||
// 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 'dnsjava:dnsjava:2.1.6'
|
||||
// HttpClient 4.3, Android flavour for WebDAV operations
|
||||
compile 'org.apache.httpcomponents:httpclient-android:4.3.5.1'
|
||||
// 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'
|
||||
}
|
||||
}
|
||||
13
app/lint.xml
Normal file
@@ -0,0 +1,13 @@
|
||||
<?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
|
||||
-->
|
||||
|
||||
<lint>
|
||||
<issue id="InvalidPackage" severity="ignore" />
|
||||
<issue id="MissingTranslation" severity="warning" />
|
||||
</lint>
|
||||
34
app/proguard-rules.txt
Normal file
@@ -0,0 +1,34 @@
|
||||
|
||||
# ProGuard usage for DAVdroid:
|
||||
# shrinking yes - main reason for using ProGuard
|
||||
# optimization no - too risky
|
||||
# obfuscation no - DAVdroid is open-source
|
||||
# preverification no (Android default)
|
||||
|
||||
-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 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 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)
|
||||
|
||||
# dnsjava
|
||||
-dontwarn sun.net.spi.nameservice.** # not available on Android
|
||||
|
||||
# DAVdroid
|
||||
-keep class at.bitfire.davdroid.** { *; } # all DAVdroid code is required
|
||||
BIN
app/src/androidTest/assets/davdroid-logo-192.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
17
app/src/androidTest/assets/recurring-with-exception1.ics
Normal file
@@ -0,0 +1,17 @@
|
||||
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
|
||||
@@ -4,7 +4,7 @@ N:Gump;Forrest;Mr.
|
||||
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
|
||||
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
|
||||
@@ -13,4 +13,4 @@ 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
|
||||
END:VCARD
|
||||
@@ -0,0 +1,14 @@
|
||||
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,11 +1,11 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2014 Ricki Hirner (bitfire web engineering).
|
||||
/*
|
||||
* 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
|
||||
******************************************************************************/
|
||||
package at.bitfire.davdroid.test;
|
||||
*/
|
||||
package at.bitfire.davdroid;
|
||||
import java.util.Arrays;
|
||||
|
||||
import junit.framework.TestCase;
|
||||
@@ -1,11 +1,11 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2014 Ricki Hirner (bitfire web engineering).
|
||||
/*
|
||||
* 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
|
||||
******************************************************************************/
|
||||
package at.bitfire.davdroid.test;
|
||||
*/
|
||||
package at.bitfire.davdroid;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
@@ -15,7 +15,6 @@ import net.fortuna.ical4j.data.ParserException;
|
||||
import android.content.res.AssetManager;
|
||||
import android.test.InstrumentationTestCase;
|
||||
import at.bitfire.davdroid.resource.Contact;
|
||||
import ezvcard.VCardException;
|
||||
import ezvcard.property.Impp;
|
||||
|
||||
public class ContactTest extends InstrumentationTestCase {
|
||||
@@ -25,7 +24,7 @@ public class ContactTest extends InstrumentationTestCase {
|
||||
assetMgr = getInstrumentation().getContext().getResources().getAssets();
|
||||
}
|
||||
|
||||
public void testIMPP() throws VCardException, IOException {
|
||||
public void testIMPP() throws IOException {
|
||||
Contact c = parseVCard("impp.vcf");
|
||||
assertEquals("test mctest", c.getDisplayName());
|
||||
|
||||
@@ -52,11 +51,11 @@ public class ContactTest extends InstrumentationTestCase {
|
||||
}
|
||||
|
||||
|
||||
private Contact parseVCard(String fileName) throws VCardException, IOException {
|
||||
private Contact parseVCard(String fileName) throws IOException {
|
||||
@Cleanup InputStream in = assetMgr.open(fileName, AssetManager.ACCESS_STREAMING);
|
||||
|
||||
Contact c = new Contact(fileName, null);
|
||||
c.parseEntity(in);
|
||||
c.parseEntity(in, null);
|
||||
|
||||
return c;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* 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
|
||||
*/
|
||||
|
||||
package at.bitfire.davdroid;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
* 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
|
||||
*/
|
||||
package at.bitfire.davdroid;
|
||||
|
||||
import java.net.URI;
|
||||
|
||||
import junit.framework.TestCase;
|
||||
import at.bitfire.davdroid.URIUtils;
|
||||
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,28 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2014 Ricki Hirner (bitfire web engineering).
|
||||
/*
|
||||
* 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
|
||||
******************************************************************************/
|
||||
package at.bitfire.davdroid.resource.test;
|
||||
*/
|
||||
package at.bitfire.davdroid.resource;
|
||||
|
||||
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.property.Email;
|
||||
import ezvcard.property.Telephone;
|
||||
import lombok.Cleanup;
|
||||
import android.content.res.AssetManager;
|
||||
import android.test.InstrumentationTestCase;
|
||||
|
||||
import org.apache.commons.io.IOUtils;
|
||||
|
||||
import at.bitfire.davdroid.resource.Contact;
|
||||
import at.bitfire.davdroid.resource.InvalidResourceException;
|
||||
|
||||
@@ -47,8 +55,12 @@ public class ContactTest extends InstrumentationTestCase {
|
||||
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, InvalidResourceException {
|
||||
Contact c = parseVCF("invalid-unknown-properties.vcf");
|
||||
assertEquals("VCard with invalid unknown properties", c.getDisplayName());
|
||||
@@ -59,7 +71,12 @@ public class ContactTest extends InstrumentationTestCase {
|
||||
protected Contact parseVCF(String fname) throws IOException, InvalidResourceException {
|
||||
@Cleanup InputStream in = assetMgr.open(fname, AssetManager.ACCESS_STREAMING);
|
||||
Contact c = new Contact(fname, null);
|
||||
c.parseEntity(in);
|
||||
c.parseEntity(in, new Resource.AssetDownloader() {
|
||||
@Override
|
||||
public byte[] download(URI uri) throws URISyntaxException, IOException, HttpException, DavException {
|
||||
return IOUtils.toByteArray(uri);
|
||||
}
|
||||
});
|
||||
return c;
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2014 Ricki Hirner (bitfire web engineering).
|
||||
/*
|
||||
* 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
|
||||
******************************************************************************/
|
||||
package at.bitfire.davdroid.resource.test;
|
||||
*/
|
||||
package at.bitfire.davdroid.resource;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
@@ -21,24 +21,31 @@ import at.bitfire.davdroid.resource.InvalidResourceException;
|
||||
public class EventTest extends InstrumentationTestCase {
|
||||
AssetManager assetMgr;
|
||||
|
||||
Event eViennaEvolution,
|
||||
eOnThatDay, eAllDay1Day, eAllDay10Days, eAllDay0Sec;
|
||||
Event eOnThatDay, eAllDay1Day, eAllDay10Days, eAllDay0Sec;
|
||||
|
||||
public void setUp() throws IOException, InvalidResourceException {
|
||||
assetMgr = getInstrumentation().getContext().getResources().getAssets();
|
||||
|
||||
eViennaEvolution = parseCalendar("vienna-evolution.ics");
|
||||
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");
|
||||
|
||||
//assertEquals("Test-Ereignis im schönen Wien", e.getSummary());
|
||||
}
|
||||
|
||||
|
||||
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 {
|
||||
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());
|
||||
@@ -73,11 +80,60 @@ public class EventTest extends InstrumentationTestCase {
|
||||
assertEquals(Time.TIMEZONE_UTC, eAllDay0Sec.getDtEndTzID());
|
||||
}
|
||||
|
||||
public void testTimezoneDefToTzId() {
|
||||
// test valid definition
|
||||
final String VTIMEZONE_SAMPLE = // taken from RFC 4791, 5.2.2. CALDAV:calendar-timezone Property
|
||||
"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";
|
||||
assertEquals("US-Eastern", Event.TimezoneDefToTzId(VTIMEZONE_SAMPLE));
|
||||
|
||||
// test null value
|
||||
try {
|
||||
Event.TimezoneDefToTzId(null);
|
||||
fail();
|
||||
} catch(IllegalArgumentException e) {
|
||||
assert(true);
|
||||
}
|
||||
|
||||
// test invalid time zone
|
||||
try {
|
||||
Event.TimezoneDefToTzId("/* invalid content */");
|
||||
fail();
|
||||
} catch(IllegalArgumentException e) {
|
||||
assert(true);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
e.parseEntity(in, null);
|
||||
return e;
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,25 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2014 Ricki Hirner (bitfire web engineering).
|
||||
/*
|
||||
* 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
|
||||
******************************************************************************/
|
||||
package at.bitfire.davdroid.resource.test;
|
||||
*/
|
||||
package at.bitfire.davdroid.resource;
|
||||
|
||||
import java.util.Calendar;
|
||||
|
||||
import lombok.Cleanup;
|
||||
|
||||
import android.Manifest;
|
||||
import android.accounts.Account;
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.ContentProviderClient;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.ContentUris;
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
@@ -26,6 +30,8 @@ import android.provider.CalendarContract.Calendars;
|
||||
import android.provider.CalendarContract.Events;
|
||||
import android.provider.CalendarContract.Reminders;
|
||||
import android.test.InstrumentationTestCase;
|
||||
import android.test.IsolatedContext;
|
||||
import android.test.mock.MockContentResolver;
|
||||
import android.util.Log;
|
||||
import at.bitfire.davdroid.resource.LocalCalendar;
|
||||
import at.bitfire.davdroid.resource.LocalStorageException;
|
||||
@@ -33,20 +39,23 @@ import at.bitfire.davdroid.resource.LocalStorageException;
|
||||
public class LocalCalendarTest extends InstrumentationTestCase {
|
||||
|
||||
private static final String
|
||||
TAG = "davroid.LocalCalendarTest",
|
||||
TAG = "davdroid.test",
|
||||
accountName = "at.bitfire.davdroid.test",
|
||||
calendarName = "DAVdroid_Test";
|
||||
|
||||
|
||||
Context targetContext;
|
||||
|
||||
ContentProviderClient providerClient;
|
||||
Account testAccount = new Account(calendarName, CalendarContract.ACCOUNT_TYPE_LOCAL);
|
||||
LocalCalendar testCalendar;
|
||||
|
||||
|
||||
|
||||
|
||||
// helpers
|
||||
|
||||
private Uri syncAdapterURI(Uri uri) {
|
||||
return uri.buildUpon()
|
||||
.appendQueryParameter(Calendars.ACCOUNT_NAME, calendarName)
|
||||
.appendQueryParameter(Calendars.ACCOUNT_TYPE, CalendarContract.ACCOUNT_TYPE_LOCAL)
|
||||
.appendQueryParameter(Calendars.ACCOUNT_NAME, accountName)
|
||||
.appendQueryParameter(CalendarContract.CALLER_IS_SYNCADAPTER, "true").
|
||||
build();
|
||||
}
|
||||
@@ -72,20 +81,21 @@ public class LocalCalendarTest extends InstrumentationTestCase {
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1)
|
||||
protected void setUp() throws Exception {
|
||||
ContentResolver resolver = getInstrumentation().getContext().getContentResolver();
|
||||
providerClient = resolver.acquireContentProviderClient(CalendarContract.AUTHORITY);
|
||||
|
||||
long id;
|
||||
targetContext = getInstrumentation().getTargetContext();
|
||||
targetContext.enforceCallingOrSelfPermission(Manifest.permission.READ_CALENDAR, "No privileges for enumerating calendars");
|
||||
|
||||
providerClient = targetContext.getContentResolver().acquireContentProviderClient(CalendarContract.AUTHORITY);
|
||||
|
||||
long calendarId;
|
||||
|
||||
@Cleanup Cursor cursor = providerClient.query(Calendars.CONTENT_URI,
|
||||
new String[] { Calendars._ID },
|
||||
Calendars.ACCOUNT_TYPE + "=? AND " + Calendars.NAME + "=?",
|
||||
new String[] { CalendarContract.ACCOUNT_TYPE_LOCAL, calendarName },
|
||||
new String[]{ Calendars._ID },
|
||||
Calendars.ACCOUNT_TYPE + "=? AND " + Calendars.ACCOUNT_NAME + "=? AND " + Calendars.NAME + "=?",
|
||||
new String[] { CalendarContract.ACCOUNT_TYPE_LOCAL, accountName, calendarName },
|
||||
null);
|
||||
if (cursor.moveToNext()) {
|
||||
// found local test calendar
|
||||
id = cursor.getLong(0);
|
||||
Log.d(TAG, "Found test calendar with ID " + id);
|
||||
if (cursor != null && cursor.moveToNext()) {
|
||||
calendarId = cursor.getLong(0);
|
||||
Log.i(TAG, "Found test calendar with ID " + calendarId);
|
||||
|
||||
} else {
|
||||
// no local test calendar found, create
|
||||
@@ -106,11 +116,11 @@ public class LocalCalendarTest extends InstrumentationTestCase {
|
||||
|
||||
Uri calendarURI = providerClient.insert(syncAdapterURI(Calendars.CONTENT_URI), values);
|
||||
|
||||
id = ContentUris.parseId(calendarURI);
|
||||
Log.d(TAG, "Created test calendar with ID " + id);
|
||||
calendarId = ContentUris.parseId(calendarURI);
|
||||
Log.d(TAG, "Created test calendar with ID " + calendarId);
|
||||
}
|
||||
|
||||
testCalendar = new LocalCalendar(testAccount, providerClient, id, null);
|
||||
testCalendar = new LocalCalendar(testAccount, providerClient, calendarId, null);
|
||||
}
|
||||
|
||||
protected void tearDown() throws Exception {
|
||||
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* 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
|
||||
*/
|
||||
|
||||
package at.bitfire.davdroid.syncadapter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.util.List;
|
||||
|
||||
import android.test.InstrumentationTestCase;
|
||||
import at.bitfire.davdroid.resource.DavResourceFinder;
|
||||
import at.bitfire.davdroid.resource.ServerInfo;
|
||||
import at.bitfire.davdroid.resource.ServerInfo.ResourceInfo;
|
||||
import at.bitfire.davdroid.TestConstants;
|
||||
import ezvcard.VCardVersion;
|
||||
|
||||
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());
|
||||
// first one
|
||||
ResourceInfo collection = collections.get(0);
|
||||
assertEquals(TestConstants.roboHydra.resolve("/dav/addressbooks/test/default-v4.vcf/").toString(), collection.getURL());
|
||||
assertEquals("Default Address Book", collection.getDescription());
|
||||
assertEquals(VCardVersion.V4_0, collection.getVCardVersion());
|
||||
// second one
|
||||
collection = collections.get(1);
|
||||
assertEquals("https://my.server/absolute:uri/my-address-book/", collection.getURL());
|
||||
assertEquals("Absolute URI VCard3 Book", collection.getDescription());
|
||||
assertEquals(VCardVersion.V3_0, collection.getVCardVersion());
|
||||
|
||||
/*** 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,81 @@
|
||||
/*
|
||||
* 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
|
||||
*/
|
||||
|
||||
package at.bitfire.davdroid.webdav;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import junit.framework.TestCase;
|
||||
import at.bitfire.davdroid.TestConstants;
|
||||
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;
|
||||
|
||||
public class DavRedirectStrategyTest extends TestCase {
|
||||
|
||||
CloseableHttpClient httpClient;
|
||||
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());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
/*
|
||||
* 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
|
||||
*/
|
||||
|
||||
package at.bitfire.davdroid.webdav;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.Socket;
|
||||
import java.net.SocketAddress;
|
||||
import java.security.cert.CertPathValidatorException;
|
||||
|
||||
import javax.net.ssl.SSLException;
|
||||
import javax.net.ssl.SSLHandshakeException;
|
||||
import javax.net.ssl.SSLPeerUnverifiedException;
|
||||
import javax.net.ssl.SSLSocket;
|
||||
|
||||
import android.util.Log;
|
||||
import junit.framework.TestCase;
|
||||
|
||||
import org.apache.commons.lang.ArrayUtils;
|
||||
import org.apache.commons.lang.exception.ExceptionUtils;
|
||||
import org.apache.http.HttpHost;
|
||||
|
||||
import lombok.Cleanup;
|
||||
|
||||
public class TlsSniSocketFactoryTest extends TestCase {
|
||||
private static final String TAG = "davdroid.TlsSniSocketFactoryTest";
|
||||
|
||||
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[] = factory.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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,255 @@
|
||||
/*
|
||||
* 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
|
||||
*/
|
||||
package at.bitfire.davdroid.webdav;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.net.URI;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import javax.net.ssl.SSLPeerUnverifiedException;
|
||||
|
||||
import ezvcard.VCardVersion;
|
||||
import lombok.Cleanup;
|
||||
|
||||
import org.apache.commons.io.IOUtils;
|
||||
|
||||
import android.content.res.AssetManager;
|
||||
import android.test.InstrumentationTestCase;
|
||||
import at.bitfire.davdroid.TestConstants;
|
||||
import at.bitfire.davdroid.webdav.HttpPropfind.Mode;
|
||||
import at.bitfire.davdroid.webdav.WebDavResource.PutMode;
|
||||
import org.apache.http.impl.client.CloseableHttpClient;
|
||||
|
||||
// tests require running robohydra!
|
||||
|
||||
public class WebDavResourceTest extends InstrumentationTestCase {
|
||||
static byte[] SAMPLE_CONTENT = new byte[] { 1, 2, 3, 4, 5 };
|
||||
|
||||
final static String PATH_SIMPLE_FILE = "collection/new.file";
|
||||
|
||||
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.getCurrentUserPrincipal());
|
||||
|
||||
WebDavResource simpleFile = new WebDavResource(davAssets, "test.random");
|
||||
try {
|
||||
simpleFile.propfind(HttpPropfind.Mode.CURRENT_USER_PRINCIPAL);
|
||||
fail();
|
||||
|
||||
} catch(DavException ex) {
|
||||
}
|
||||
assertNull(simpleFile.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.getAddressbookHomeSet());
|
||||
assertEquals(new URI("/dav/calendars/test/"), dav.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.isAddressBook());
|
||||
|
||||
// the second one is a VCard4-capable address book (referenced by relative URI)
|
||||
ab = dav.getMembers().get(1);
|
||||
assertEquals(TestConstants.roboHydra.resolve("/dav/addressbooks/test/default-v4.vcf/"), ab.getLocation());
|
||||
assertEquals("default-v4.vcf", ab.getName());
|
||||
assertTrue(ab.isAddressBook());
|
||||
assertEquals(VCardVersion.V4_0, ab.getVCardVersion());
|
||||
|
||||
// the third one is a (non-VCard4-capable) 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.isAddressBook());
|
||||
assertNull(ab.getVCardVersion());
|
||||
}
|
||||
|
||||
public void testPropfindCalendars() throws Exception {
|
||||
WebDavResource dav = new WebDavResource(davCollection, "calendars/test");
|
||||
dav.propfind(Mode.CALDAV_COLLECTIONS);
|
||||
assertEquals(3, dav.getMembers().size());
|
||||
assertEquals("0xFF00FF", dav.getMembers().get(2).getColor());
|
||||
for (WebDavResource member : dav.getMembers()) {
|
||||
if (member.getName().contains(".ics"))
|
||||
assertTrue(member.isCalendar());
|
||||
else
|
||||
assertFalse(member.isCalendar());
|
||||
assertFalse(member.isAddressBook());
|
||||
}
|
||||
}
|
||||
|
||||
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, path);
|
||||
davSlash.propfind(Mode.CARDDAV_COLLECTIONS);
|
||||
assertEquals(new URI(principalOK), davSlash.getCurrentUserPrincipal());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* test normal HTTP/WebDAV */
|
||||
|
||||
public void testPropfindRedirection() throws Exception {
|
||||
// PROPFIND redirection
|
||||
WebDavResource redirected = new WebDavResource(baseDAV, "/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.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());
|
||||
// both contacts have content
|
||||
for (WebDavResource member : davAddressBook.getMembers())
|
||||
assertNotNull(member.getContent());
|
||||
}
|
||||
|
||||
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
|
||||
try {
|
||||
davNonExistingFile.put(SAMPLE_CONTENT, PutMode.UPDATE_DONT_OVERWRITE);
|
||||
fail();
|
||||
} catch(PreconditionFailedException 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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
Before Width: | Height: | Size: 9.2 KiB After Width: | Height: | Size: 9.2 KiB |
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 5.1 KiB After Width: | Height: | Size: 5.1 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
14
app/src/androidTest/res/values/strings.xml
Normal file
@@ -0,0 +1,14 @@
|
||||
<?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
|
||||
-->
|
||||
|
||||
<resources>
|
||||
|
||||
<string name="app_name">DavdroidTest</string>
|
||||
|
||||
</resources>
|
||||
@@ -1,6 +1,5 @@
|
||||
{"plugins":[
|
||||
"assets",
|
||||
"redirect",
|
||||
"dav-default",
|
||||
"dav-invalid"
|
||||
"dav"
|
||||
]}
|
||||
@@ -3,22 +3,73 @@ var roboHydraHeadDAV = require("../headdav");
|
||||
exports.getBodyParts = function(conf) {
|
||||
return {
|
||||
heads: [
|
||||
/* base URL */
|
||||
new RoboHydraHeadDAV({
|
||||
path: "/dav/",
|
||||
handler: function(req,res,next) { }
|
||||
}),
|
||||
/* base URL, provide default DAV here */
|
||||
new RoboHydraHeadDAV({ path: "/dav/" }),
|
||||
|
||||
/* 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>\
|
||||
');
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
||||
/* principal URL */
|
||||
new RoboHydraHeadDAV({
|
||||
path: "/dav/principals/users/test",
|
||||
handler: function(req,res,next) {
|
||||
if (req.method == "PROPFIND" && req.rawBody.toString().match(/home-set/)) {
|
||||
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>' + req.url + '</href> \
|
||||
<href>/dav/principals/users/t%65st</href> \
|
||||
<propstat>\
|
||||
<prop>\
|
||||
<CARD:addressbook-home-set xmlns:CARD="urn:ietf:params:xml:ns:carddav">\
|
||||
@@ -59,16 +110,31 @@ exports.getBodyParts = function(conf) {
|
||||
</propstat>\
|
||||
</response>\
|
||||
<response>\
|
||||
<href>/dav/addressbooks/test/default.vcf/</href>\
|
||||
<href>/dav/addressbooks/test/default-v4.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>\
|
||||
<CARD:addressbook-description>Default Address Book</CARD:addressbook-description>\
|
||||
<CARD:supported-address-data>\
|
||||
<CARD:address-data-type content-type="text/vcard" version="3.0" />\
|
||||
<CARD:address-data-type content-type="text/vcard" version="4.0" />\
|
||||
</CARD:supported-address-data>\
|
||||
</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 VCard3 Book</CARD:addressbook-description>\
|
||||
</prop>\
|
||||
<status>HTTP/1.1 200 OK</status>\
|
||||
</propstat>\
|
||||
@@ -83,7 +149,7 @@ exports.getBodyParts = function(conf) {
|
||||
new RoboHydraHeadDAV({
|
||||
path: "/dav/calendars/test/",
|
||||
handler: function(req,res,next) {
|
||||
if (req.method == "PROPFIND" && req.rawBody.toString().match(/addressbook-description/)) {
|
||||
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">\
|
||||
@@ -104,6 +170,8 @@ exports.getBodyParts = function(conf) {
|
||||
<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>\
|
||||
@@ -116,6 +184,10 @@ exports.getBodyParts = function(conf) {
|
||||
<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>\
|
||||
@@ -165,10 +237,21 @@ exports.getBodyParts = function(conf) {
|
||||
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)) {
|
||||
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>\
|
||||
@@ -185,7 +268,7 @@ exports.getBodyParts = function(conf) {
|
||||
</propstat>\
|
||||
</response>\
|
||||
<response>\
|
||||
<href>/dav/addressbooks/default.vcf/2.vcf</href>\
|
||||
<href>/dav/addressbooks/default.vcf/2:3%40my%2540pc.vcf</href>\
|
||||
<propstat>\
|
||||
<prop>\
|
||||
<getetag/>\
|
||||
@@ -4,7 +4,8 @@ var roboHydra = require("robohydra"),
|
||||
|
||||
RoboHydraHeadDAV = roboHydraHeads.roboHydraHeadType({
|
||||
name: 'WebDAV Server',
|
||||
mandatoryProperties: [ 'path', 'handler' ],
|
||||
mandatoryProperties: [ 'path' ],
|
||||
optionalProperties: [ 'handler' ],
|
||||
|
||||
parentPropBuilder: function() {
|
||||
var myHandler = this.handler;
|
||||
@@ -15,10 +16,16 @@ RoboHydraHeadDAV = roboHydraHeads.roboHydraHeadType({
|
||||
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
|
||||
if (req.method == "OPTIONS") {
|
||||
else if (req.method == "OPTIONS") {
|
||||
res.statusCode = 204;
|
||||
res.headers['Allow'] = 'OPTIONS, PROPFIND, GET, PUT, DELETE';
|
||||
res.headers['Allow'] = 'OPTIONS, PROPFIND, GET, PUT, DELETE, REPORT';
|
||||
|
||||
} else if (req.method == "PROPFIND" && req.rawBody.toString().match(/current-user-principal/)) {
|
||||
res.statusCode = 207;
|
||||
@@ -38,7 +45,7 @@ RoboHydraHeadDAV = roboHydraHeads.roboHydraHeadType({
|
||||
</multistatus>\
|
||||
');
|
||||
|
||||
} else
|
||||
} else if (typeof myHandler != 'undefined')
|
||||
myHandler(req,res,next);
|
||||
|
||||
res.end();
|
||||
57
app/src/androidTest/robohydra/plugins/redirect/index.js
Normal file
@@ -0,0 +1,57 @@
|
||||
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
|
||||
})
|
||||
|
||||
]
|
||||
};
|
||||
};
|
||||
28
app/src/androidTest/robohydra/plugins/simple.js
Normal file
@@ -0,0 +1,28 @@
|
||||
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,27 +1,38 @@
|
||||
<?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
|
||||
-->
|
||||
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="at.bitfire.davdroid"
|
||||
android:versionCode="38"
|
||||
android:versionName="0.6" android:installLocation="internalOnly">
|
||||
android:versionCode="59" android:versionName="0.7.3"
|
||||
android:installLocation="internalOnly">
|
||||
|
||||
<uses-sdk
|
||||
android:minSdkVersion="14"
|
||||
android:targetSdkVersion="19" />
|
||||
android:targetSdkVersion="21" />
|
||||
|
||||
<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" />
|
||||
|
||||
<application
|
||||
android:allowBackup="false"
|
||||
android:allowBackup="true"
|
||||
android:icon="@drawable/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/AppTheme"
|
||||
android:process=":sync" >
|
||||
android:process=":sync">
|
||||
|
||||
<service
|
||||
android:name=".syncadapter.AccountAuthenticatorService"
|
||||
android:exported="false" >
|
||||
@@ -60,7 +71,7 @@
|
||||
</service>
|
||||
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:name=".ui.MainActivity"
|
||||
android:label="@string/app_name" >
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
@@ -68,12 +79,21 @@
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".syncadapter.AddAccountActivity"
|
||||
android:name=".ui.setup.AddAccountActivity"
|
||||
android:excludeFromRecents="true" >
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".syncadapter.GeneralSettingsActivity" >
|
||||
android:name=".ui.settings.SettingsActivity"
|
||||
android:label="@string/settings_title" >
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MANAGE_NETWORK_USAGE" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
<activity
|
||||
android:name=".ui.settings.AccountActivity"
|
||||
android:label="@string/settings_title" >
|
||||
</activity>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
@@ -1,10 +1,10 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2014 Ricki Hirner (bitfire web engineering).
|
||||
/*
|
||||
* 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
|
||||
******************************************************************************/
|
||||
*/
|
||||
package at.bitfire.davdroid;
|
||||
|
||||
import java.lang.reflect.Array;
|
||||
16
app/src/main/java/at/bitfire/davdroid/Constants.java
Normal file
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* 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
|
||||
*/
|
||||
package at.bitfire.davdroid;
|
||||
|
||||
public class Constants {
|
||||
public static final String
|
||||
APP_VERSION = "0.7.3",
|
||||
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";
|
||||
}
|
||||
84
app/src/main/java/at/bitfire/davdroid/URIUtils.java
Normal file
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
* 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
|
||||
*/
|
||||
package at.bitfire.davdroid;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import org.apache.commons.codec.EncoderException;
|
||||
import org.apache.commons.codec.net.URLCodec;
|
||||
import org.apache.commons.lang.StringEscapeUtils;
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
|
||||
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
|
||||
* @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 URL " + original + " -> " + normalized.toASCIIString());
|
||||
return normalized;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,16 +1,17 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2014 Ricki Hirner (bitfire web engineering).
|
||||
/*
|
||||
* 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
|
||||
******************************************************************************/
|
||||
*/
|
||||
package at.bitfire.davdroid.resource;
|
||||
|
||||
import org.apache.http.impl.client.CloseableHttpClient;
|
||||
|
||||
import java.net.URISyntaxException;
|
||||
|
||||
import at.bitfire.davdroid.webdav.DavMultiget;
|
||||
import ch.boye.httpclientandroidlib.impl.client.CloseableHttpClient;
|
||||
|
||||
public class CalDavCalendar extends RemoteCollection<Event> {
|
||||
//private final static String TAG = "davdroid.CalDavCalendar";
|
||||
@@ -1,15 +1,16 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2014 Ricki Hirner (bitfire web engineering).
|
||||
/*
|
||||
* 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
|
||||
******************************************************************************/
|
||||
*/
|
||||
package at.bitfire.davdroid.resource;
|
||||
|
||||
import org.apache.http.impl.client.CloseableHttpClient;
|
||||
|
||||
import java.net.URISyntaxException;
|
||||
|
||||
import ch.boye.httpclientandroidlib.impl.client.CloseableHttpClient;
|
||||
import at.bitfire.davdroid.webdav.DavMultiget;
|
||||
|
||||
public class CardDavAddressBook extends RemoteCollection<Contact> {
|
||||
@@ -17,7 +18,7 @@ public class CardDavAddressBook extends RemoteCollection<Contact> {
|
||||
|
||||
@Override
|
||||
protected String memberContentType() {
|
||||
return "text/vcard";
|
||||
return Contact.MIME_TYPE;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -1,30 +1,30 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2014 Ricki Hirner (bitfire web engineering).
|
||||
/*
|
||||
* 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
|
||||
******************************************************************************/
|
||||
*/
|
||||
package at.bitfire.davdroid.resource;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import net.fortuna.ical4j.model.property.ProdId;
|
||||
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URI;
|
||||
import java.net.URL;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.ToString;
|
||||
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
|
||||
import android.util.Log;
|
||||
import at.bitfire.davdroid.Constants;
|
||||
import ezvcard.Ezvcard;
|
||||
import ezvcard.VCard;
|
||||
import ezvcard.VCardException;
|
||||
import ezvcard.VCardVersion;
|
||||
import ezvcard.ValidationWarnings;
|
||||
import ezvcard.parameter.EmailType;
|
||||
@@ -33,6 +33,7 @@ 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;
|
||||
@@ -41,6 +42,7 @@ 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.Revision;
|
||||
import ezvcard.property.Role;
|
||||
@@ -51,6 +53,9 @@ 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;
|
||||
|
||||
|
||||
/**
|
||||
@@ -61,6 +66,8 @@ import ezvcard.property.Url;
|
||||
public class Contact extends Resource {
|
||||
private final static String TAG = "davdroid.Contact";
|
||||
|
||||
public final static String MIME_TYPE = "text/vcard";
|
||||
|
||||
public final static String
|
||||
PROPERTY_STARRED = "X-DAVDROID-STARRED",
|
||||
PROPERTY_PHONETIC_FIRST_NAME = "X-PHONETIC-FIRST-NAME",
|
||||
@@ -97,6 +104,7 @@ public class Contact extends Resource {
|
||||
@Getter private List<Email> emails = new LinkedList<Email>();
|
||||
@Getter private List<Impp> impps = new LinkedList<Impp>();
|
||||
@Getter private List<Address> addresses = new LinkedList<Address>();
|
||||
@Getter private List<String> categories = new LinkedList<String>();
|
||||
@Getter private List<String> URLs = new LinkedList<String>();
|
||||
|
||||
|
||||
@@ -124,8 +132,9 @@ public class Contact extends Resource {
|
||||
|
||||
/* VCard methods */
|
||||
|
||||
@Override
|
||||
public void parseEntity(InputStream is) throws IOException, VCardException {
|
||||
@SuppressWarnings("LoopStatementThatDoesntLoop")
|
||||
@Override
|
||||
public void parseEntity(InputStream is, AssetDownloader downloader) throws IOException {
|
||||
VCard vcard = Ezvcard.parse(is).first();
|
||||
if (vcard == null)
|
||||
return;
|
||||
@@ -200,6 +209,14 @@ public class Contact extends Resource {
|
||||
// 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;
|
||||
}
|
||||
@@ -244,6 +261,12 @@ public class Contact extends Resource {
|
||||
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());
|
||||
@@ -265,6 +288,7 @@ public class Contact extends Resource {
|
||||
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
|
||||
@@ -305,7 +329,7 @@ public class Contact extends Resource {
|
||||
Log.w(TAG, "No FN (formatted name) available to generate VCard");
|
||||
|
||||
// N
|
||||
if (familyName != null || middleName != null || givenName != null) {
|
||||
if (prefix != null || familyName != null || middleName != null || givenName != null || suffix != null) {
|
||||
StructuredName n = new StructuredName();
|
||||
if (prefix != null)
|
||||
for (String p : StringUtils.split(prefix))
|
||||
@@ -361,6 +385,10 @@ public class Contact extends Resource {
|
||||
for (Address address : addresses)
|
||||
vcard.addAddress(address);
|
||||
|
||||
// CATEGORY
|
||||
if (!categories.isEmpty())
|
||||
vcard.setCategories(categories.toArray(new String[0]));
|
||||
|
||||
// URL
|
||||
for (String url : URLs)
|
||||
vcard.addUrl(url);
|
||||
@@ -377,7 +405,7 @@ public class Contact extends Resource {
|
||||
vcard.addPhoto(new Photo(photo, ImageType.JPEG));
|
||||
|
||||
// PRODID, REV
|
||||
vcard.setProdId("DAVdroid/" + Constants.APP_VERSION + " (ez-vcard/" + Ezvcard.VERSION + ")");
|
||||
vcard.setProductId("DAVdroid/" + Constants.APP_VERSION + " (ez-vcard/" + Ezvcard.VERSION + ")");
|
||||
vcard.setRevision(Revision.now());
|
||||
|
||||
// validate and print warnings
|
||||
@@ -0,0 +1,326 @@
|
||||
/*
|
||||
* 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
|
||||
*/
|
||||
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.MalformedURLException;
|
||||
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;
|
||||
import ezvcard.VCardVersion;
|
||||
|
||||
public class DavResourceFinder implements Closeable {
|
||||
private final static String TAG = "davdroid.ResourceFinder";
|
||||
|
||||
protected Context context;
|
||||
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.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)
|
||||
if (resource.isAddressBook()) {
|
||||
Log.i(TAG, "Found address book: " + resource.getLocation().getPath());
|
||||
ServerInfo.ResourceInfo info = new ServerInfo.ResourceInfo(
|
||||
ServerInfo.ResourceInfo.Type.ADDRESS_BOOK,
|
||||
resource.isReadOnly(),
|
||||
resource.getLocation().toString(),
|
||||
resource.getDisplayName(),
|
||||
resource.getDescription(), resource.getColor()
|
||||
);
|
||||
|
||||
VCardVersion version = resource.getVCardVersion();
|
||||
if (version == null)
|
||||
version = VCardVersion.V3_0; // VCard 3.0 MUST be supported
|
||||
info.setVCardVersion(version);
|
||||
|
||||
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.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<>();
|
||||
for (WebDavResource resource : possibleCalendars)
|
||||
if (resource.isCalendar()) {
|
||||
Log.i(TAG, "Found calendar: " + resource.getLocation().getPath());
|
||||
if (resource.getSupportedComponents() != null) {
|
||||
// CALDAV:supported-calendar-component-set available
|
||||
boolean supportsEvents = false;
|
||||
for (String supportedComponent : resource.getSupportedComponents())
|
||||
if (supportedComponent.equalsIgnoreCase("VEVENT"))
|
||||
supportsEvents = true;
|
||||
if (!supportsEvents) { // ignore collections without VEVENT support
|
||||
Log.i(TAG, "Ignoring this calendar because of missing VEVENT support");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
ServerInfo.ResourceInfo info = new ServerInfo.ResourceInfo(
|
||||
ServerInfo.ResourceInfo.Type.CALENDAR,
|
||||
resource.isReadOnly(),
|
||||
resource.getLocation().toString(),
|
||||
resource.getDisplayName(),
|
||||
resource.getDescription(), resource.getColor()
|
||||
);
|
||||
info.setTimezone(resource.getTimezone());
|
||||
calendars.add(info);
|
||||
}
|
||||
serverInfo.setCalendars(calendars);
|
||||
} 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
|
||||
* @throws MalformedURLException when the user-given URI is invalid
|
||||
*/
|
||||
public URI getInitialContextURL(ServerInfo serverInfo, String serviceName) throws URISyntaxException, MalformedURLException {
|
||||
String scheme = null,
|
||||
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.getCurrentUserPrincipal() != null) {
|
||||
URI principal = wellKnown.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.getCurrentUserPrincipal() != null) {
|
||||
URI principal = base.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,25 +1,15 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2014 Ricki Hirner (bitfire web engineering).
|
||||
/*
|
||||
* 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
|
||||
******************************************************************************/
|
||||
*/
|
||||
package at.bitfire.davdroid.resource;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.StringReader;
|
||||
import java.util.Calendar;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.SimpleTimeZone;
|
||||
import java.util.TimeZone;
|
||||
import android.text.format.Time;
|
||||
import android.util.Log;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.NonNull;
|
||||
import lombok.Setter;
|
||||
import net.fortuna.ical4j.data.CalendarBuilder;
|
||||
import net.fortuna.ical4j.data.CalendarOutputter;
|
||||
import net.fortuna.ical4j.data.ParserException;
|
||||
@@ -51,48 +41,74 @@ import net.fortuna.ical4j.model.property.Organizer;
|
||||
import net.fortuna.ical4j.model.property.ProdId;
|
||||
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.CompatibilityHints;
|
||||
import net.fortuna.ical4j.util.SimpleHostInfo;
|
||||
import net.fortuna.ical4j.util.UidGenerator;
|
||||
import android.text.format.Time;
|
||||
import android.util.Log;
|
||||
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.StringReader;
|
||||
import java.util.Calendar;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.SimpleTimeZone;
|
||||
import java.util.TimeZone;
|
||||
|
||||
import at.bitfire.davdroid.Constants;
|
||||
import at.bitfire.davdroid.syncadapter.DavSyncAdapter;
|
||||
import lombok.Getter;
|
||||
import lombok.NonNull;
|
||||
import lombok.Setter;
|
||||
|
||||
|
||||
public class Event extends Resource {
|
||||
private final static String TAG = "davdroid.Event";
|
||||
|
||||
public final static String MIME_TYPE = "text/calendar";
|
||||
|
||||
private final static TimeZoneRegistry tzRegistry = new DefaultTimeZoneRegistryFactory().createRegistry();
|
||||
|
||||
@Getter @Setter protected RecurrenceId recurrenceId;
|
||||
|
||||
@Getter @Setter protected String summary, location, description;
|
||||
|
||||
@Getter @Setter private String summary, location, description;
|
||||
@Getter protected DtStart dtStart;
|
||||
@Getter protected DtEnd dtEnd;
|
||||
@Getter @Setter protected Duration duration;
|
||||
@Getter @Setter protected RDate rdate;
|
||||
@Getter @Setter protected RRule rrule;
|
||||
@Getter @Setter protected ExDate exdate;
|
||||
@Getter @Setter protected ExRule exrule;
|
||||
@Getter protected List<Event> exceptions = new LinkedList<>();
|
||||
|
||||
@Getter @Setter protected Boolean forPublic;
|
||||
@Getter @Setter protected Status status;
|
||||
|
||||
@Getter private DtStart dtStart;
|
||||
@Getter private DtEnd dtEnd;
|
||||
@Getter @Setter private Duration duration;
|
||||
@Getter @Setter private RDate rdate;
|
||||
@Getter @Setter private RRule rrule;
|
||||
@Getter @Setter private ExDate exdate;
|
||||
@Getter @Setter private ExRule exrule;
|
||||
@Getter @Setter protected boolean opaque;
|
||||
|
||||
@Getter @Setter private Boolean forPublic;
|
||||
@Getter @Setter private Status status;
|
||||
|
||||
@Getter @Setter private boolean opaque;
|
||||
|
||||
@Getter @Setter private Organizer organizer;
|
||||
@Getter private List<Attendee> attendees = new LinkedList<Attendee>();
|
||||
public void addAttendee(Attendee attendee) {
|
||||
attendees.add(attendee);
|
||||
}
|
||||
|
||||
@Getter private List<VAlarm> alarms = new LinkedList<VAlarm>();
|
||||
public void addAlarm(VAlarm alarm) {
|
||||
alarms.add(alarm);
|
||||
@Getter @Setter protected Organizer organizer;
|
||||
@Getter protected List<Attendee> attendees = new LinkedList<Attendee>();
|
||||
|
||||
@Getter protected List<VAlarm> alarms = new LinkedList<VAlarm>();
|
||||
|
||||
static {
|
||||
CompatibilityHints.setHintEnabled(CompatibilityHints.KEY_RELAXED_UNFOLDING, true);
|
||||
CompatibilityHints.setHintEnabled(CompatibilityHints.KEY_RELAXED_PARSING, true);
|
||||
CompatibilityHints.setHintEnabled(CompatibilityHints.KEY_OUTLOOK_COMPATIBILITY, true);
|
||||
|
||||
// disable automatic time-zone updates (causes unnecessary network traffic for most people)
|
||||
System.setProperty("net.fortuna.ical4j.timezone.update.enabled", "false");
|
||||
}
|
||||
|
||||
|
||||
@@ -119,7 +135,7 @@ public class Event extends Resource {
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public void parseEntity(@NonNull InputStream entity) throws IOException, InvalidResourceException {
|
||||
public void parseEntity(@NonNull InputStream entity, AssetDownloader downloader) throws IOException, InvalidResourceException {
|
||||
net.fortuna.ical4j.model.Calendar ical;
|
||||
try {
|
||||
CalendarBuilder builder = new CalendarBuilder();
|
||||
@@ -131,19 +147,44 @@ public class Event extends Resource {
|
||||
throw new InvalidResourceException(e);
|
||||
}
|
||||
|
||||
// event
|
||||
ComponentList events = ical.getComponents(Component.VEVENT);
|
||||
if (events == null || events.isEmpty())
|
||||
throw new InvalidResourceException("No VEVENT found");
|
||||
VEvent event = (VEvent)events.get(0);
|
||||
|
||||
|
||||
// find master VEVENT (the one that is not an exception, i.e. the one without RECURRENCE-ID)
|
||||
VEvent master = null;
|
||||
for (Object objEvent : events) {
|
||||
VEvent event = (VEvent)objEvent;
|
||||
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 (Object objEvent : events) {
|
||||
VEvent event = (VEvent)objEvent;
|
||||
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");
|
||||
|
||||
@@ -151,7 +192,7 @@ public class Event extends Resource {
|
||||
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)
|
||||
@@ -162,29 +203,26 @@ public class Event extends Resource {
|
||||
c.add(Calendar.DATE, 1);
|
||||
dtEnd.setDate(new Date(c.getTimeInMillis()));
|
||||
}
|
||||
|
||||
|
||||
rrule = (RRule)event.getProperty(Property.RRULE);
|
||||
rdate = (RDate)event.getProperty(Property.RDATE);
|
||||
exrule = (ExRule)event.getProperty(Property.EXRULE);
|
||||
exdate = (ExDate)event.getProperty(Property.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 = true;
|
||||
if (event.getTransparency() == Transp.TRANSPARENT)
|
||||
opaque = false;
|
||||
|
||||
opaque = event.getTransparency() != Transp.TRANSPARENT;
|
||||
|
||||
organizer = event.getOrganizer();
|
||||
for (Object o : event.getProperties(Property.ATTENDEE))
|
||||
attendees.add((Attendee)o);
|
||||
|
||||
|
||||
Clazz classification = event.getClassification();
|
||||
if (classification != null) {
|
||||
if (classification == Clazz.PUBLIC)
|
||||
@@ -192,70 +230,51 @@ public class Event extends Resource {
|
||||
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(new ProdId("-//bitfire web engineering//DAVdroid " + Constants.APP_VERSION + "//EN"));
|
||||
|
||||
VEvent event = new VEvent();
|
||||
PropertyList props = event.getProperties();
|
||||
|
||||
if (uid != null)
|
||||
props.add(new Uid(uid));
|
||||
|
||||
props.add(dtStart);
|
||||
if (dtEnd != null)
|
||||
props.add(dtEnd);
|
||||
if (duration != null)
|
||||
props.add(duration);
|
||||
|
||||
if (rrule != null)
|
||||
props.add(rrule);
|
||||
if (rdate != null)
|
||||
props.add(rdate);
|
||||
if (exrule != null)
|
||||
props.add(exrule);
|
||||
if (exdate != null)
|
||||
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());
|
||||
ical.getComponents().add(event);
|
||||
ical.getProperties().add(new ProdId("-//bitfire web engineering//DAVdroid " + Constants.APP_VERSION + " (ical4j 1.0.x)//EN"));
|
||||
|
||||
// "master event" (without exceptions)
|
||||
ComponentList components = ical.getComponents();
|
||||
VEvent master = toVEvent();
|
||||
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();
|
||||
|
||||
// set UID to UID of master event
|
||||
vException.getProperties().add(master.getProperty(Property.UID));
|
||||
|
||||
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
|
||||
net.fortuna.ical4j.model.TimeZone
|
||||
tzStart = (dtStart == null ? null : dtStart.getTimeZone()),
|
||||
tzEnd = (dtEnd == null ? null : dtEnd.getTimeZone());
|
||||
if (tzStart != null)
|
||||
ical.getComponents().add(tzStart.getVTimeZone());
|
||||
if (tzEnd != null && tzEnd != tzStart)
|
||||
ical.getComponents().add(tzEnd.getVTimeZone());
|
||||
for (net.fortuna.ical4j.model.TimeZone timeZone : usedTimeZones)
|
||||
ical.getComponents().add(timeZone.getVTimeZone());
|
||||
|
||||
CalendarOutputter output = new CalendarOutputter(false);
|
||||
ByteArrayOutputStream os = new ByteArrayOutputStream();
|
||||
@@ -267,6 +286,55 @@ public class Event extends Resource {
|
||||
return os;
|
||||
}
|
||||
|
||||
protected VEvent toVEvent() {
|
||||
VEvent event = new VEvent();
|
||||
PropertyList props = event.getProperties();
|
||||
|
||||
if (uid != null)
|
||||
props.add(new Uid(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);
|
||||
if (rdate != null)
|
||||
props.add(rdate);
|
||||
if (exrule != null)
|
||||
props.add(exrule);
|
||||
if (exdate != null)
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
public long getDtStartInMillis() {
|
||||
return dtStart.getDate().getTime();
|
||||
@@ -330,35 +398,54 @@ public class Event extends Resource {
|
||||
|
||||
/* guess matching Android timezone ID */
|
||||
protected static void validateTimeZone(DateProperty date) {
|
||||
if (date.isUtc() || !hasTime(date))
|
||||
return;
|
||||
|
||||
String tzID = getTzId(date);
|
||||
if (tzID == null)
|
||||
return;
|
||||
|
||||
String localTZ = Time.TIMEZONE_UTC;
|
||||
|
||||
String availableTZs[] = SimpleTimeZone.getAvailableIDs();
|
||||
for (String availableTZ : availableTZs)
|
||||
if (tzID.indexOf(availableTZ, 0) != -1) {
|
||||
localTZ = availableTZ;
|
||||
break;
|
||||
}
|
||||
|
||||
Log.d(TAG, "Assuming time zone " + localTZ + " for " + tzID);
|
||||
date.setTimeZone(tzRegistry.getTimeZone(localTZ));
|
||||
}
|
||||
if (date.isUtc() || !hasTime(date))
|
||||
return;
|
||||
|
||||
public static String TimezoneDefToTzId(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);
|
||||
return timezone.getTimeZoneId().getValue();
|
||||
} catch (Exception ex) {
|
||||
Log.w(TAG, "Can't understand time zone definition", ex);
|
||||
String tzID = getTzId(date);
|
||||
if (tzID == null)
|
||||
return;
|
||||
|
||||
String localTZ = null;
|
||||
String availableTZs[] = SimpleTimeZone.getAvailableIDs();
|
||||
|
||||
// first, try to find an exact match (case insensitive)
|
||||
for (String availableTZ : availableTZs)
|
||||
if (tzID.equalsIgnoreCase(availableTZ)) {
|
||||
localTZ = availableTZ;
|
||||
break;
|
||||
}
|
||||
|
||||
// if that doesn't work, try to find something else that matches
|
||||
if (localTZ == null) {
|
||||
Log.w(TAG, "Coulnd't find time zone with matching identifiers, trying to guess");
|
||||
for (String availableTZ : availableTZs)
|
||||
if (StringUtils.indexOfIgnoreCase(tzID, availableTZ) != -1) {
|
||||
localTZ = availableTZ;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// if that doesn't work, use UTC as fallback
|
||||
if (localTZ == null) {
|
||||
Log.e(TAG, "Couldn't identify time zone, using UTC as fallback");
|
||||
localTZ = Time.TIMEZONE_UTC;
|
||||
}
|
||||
return null;
|
||||
|
||||
Log.d(TAG, "Assuming time zone " + localTZ + " for " + tzID);
|
||||
date.setTimeZone(tzRegistry.getTimeZone(localTZ));
|
||||
}
|
||||
|
||||
public static String TimezoneDefToTzId(String timezoneDef) throws IllegalArgumentException {
|
||||
try {
|
||||
if (timezoneDef != null) {
|
||||
CalendarBuilder builder = new CalendarBuilder();
|
||||
net.fortuna.ical4j.model.Calendar cal = builder.build(new StringReader(timezoneDef));
|
||||
VTimeZone timezone = (VTimeZone)cal.getComponent(VTimeZone.VTIMEZONE);
|
||||
return timezone.getTimeZoneId().getValue();
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
Log.w(TAG, "Can't understand time zone definition, ignoring", ex);
|
||||
}
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2014 Ricki Hirner (bitfire web engineering).
|
||||
/*
|
||||
* 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
|
||||
******************************************************************************/
|
||||
*/
|
||||
package at.bitfire.davdroid.resource;
|
||||
|
||||
public class InvalidResourceException extends Exception {
|
||||
@@ -1,12 +1,45 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2014 Ricki Hirner (bitfire web engineering).
|
||||
/*
|
||||
* 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
|
||||
******************************************************************************/
|
||||
*/
|
||||
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.ContentUris;
|
||||
import android.content.ContentValues;
|
||||
import android.content.res.AssetFileDescriptor;
|
||||
import android.database.Cursor;
|
||||
import android.database.DatabaseUtils;
|
||||
import android.net.Uri;
|
||||
import android.os.RemoteException;
|
||||
import android.provider.ContactsContract.CommonDataKinds;
|
||||
import android.provider.ContactsContract.CommonDataKinds.Email;
|
||||
import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
|
||||
import android.provider.ContactsContract.CommonDataKinds.Im;
|
||||
import android.provider.ContactsContract.CommonDataKinds.Nickname;
|
||||
import android.provider.ContactsContract.CommonDataKinds.Note;
|
||||
import android.provider.ContactsContract.CommonDataKinds.Organization;
|
||||
import android.provider.ContactsContract.CommonDataKinds.Phone;
|
||||
import android.provider.ContactsContract.CommonDataKinds.Photo;
|
||||
import android.provider.ContactsContract.CommonDataKinds.SipAddress;
|
||||
import android.provider.ContactsContract.CommonDataKinds.StructuredName;
|
||||
import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
|
||||
import android.provider.ContactsContract.CommonDataKinds.Website;
|
||||
import android.provider.ContactsContract.Data;
|
||||
import android.provider.ContactsContract.Groups;
|
||||
import android.provider.ContactsContract.RawContacts;
|
||||
import android.util.Log;
|
||||
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.apache.commons.lang.WordUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.text.ParseException;
|
||||
@@ -18,38 +51,7 @@ import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
|
||||
import lombok.Cleanup;
|
||||
|
||||
import android.accounts.Account;
|
||||
import android.content.ContentProviderClient;
|
||||
import android.content.ContentProviderOperation;
|
||||
import android.content.ContentProviderOperation.Builder;
|
||||
import android.content.ContentUris;
|
||||
import android.content.res.AssetFileDescriptor;
|
||||
import android.database.Cursor;
|
||||
import android.database.DatabaseUtils;
|
||||
import android.net.Uri;
|
||||
import android.os.RemoteException;
|
||||
import android.provider.ContactsContract.CommonDataKinds;
|
||||
import android.provider.ContactsContract.CommonDataKinds.Email;
|
||||
import android.provider.ContactsContract.CommonDataKinds.Im;
|
||||
import android.provider.ContactsContract.CommonDataKinds.Nickname;
|
||||
import android.provider.ContactsContract.CommonDataKinds.Note;
|
||||
import android.provider.ContactsContract.CommonDataKinds.Organization;
|
||||
import android.provider.ContactsContract.CommonDataKinds.Phone;
|
||||
import android.provider.ContactsContract.CommonDataKinds.Photo;
|
||||
import android.provider.ContactsContract.CommonDataKinds.SipAddress;
|
||||
import android.provider.ContactsContract.CommonDataKinds.StructuredName;
|
||||
import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
|
||||
import android.provider.ContactsContract.CommonDataKinds.Website;
|
||||
import android.provider.ContactsContract.Data;
|
||||
import android.provider.ContactsContract.RawContacts;
|
||||
import android.util.Log;
|
||||
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.apache.commons.lang.WordUtils;
|
||||
|
||||
import at.bitfire.davdroid.syncadapter.AccountSettings;
|
||||
import ezvcard.parameter.AddressType;
|
||||
import ezvcard.parameter.EmailType;
|
||||
import ezvcard.parameter.ImppType;
|
||||
@@ -60,8 +62,7 @@ import ezvcard.property.Birthday;
|
||||
import ezvcard.property.DateOrTimeProperty;
|
||||
import ezvcard.property.Impp;
|
||||
import ezvcard.property.Telephone;
|
||||
|
||||
import at.bitfire.davdroid.syncadapter.AccountSettings;
|
||||
import lombok.Cleanup;
|
||||
|
||||
|
||||
public class LocalAddressBook extends LocalCollection<Contact> {
|
||||
@@ -142,6 +143,33 @@ public class LocalAddressBook extends LocalCollection<Contact> {
|
||||
.build());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void commit() throws LocalStorageException {
|
||||
super.commit();
|
||||
|
||||
// update group details for groups we have just created
|
||||
Uri groupsUri = syncAdapterURI(Groups.CONTENT_URI);
|
||||
try {
|
||||
// newly created groups don't have a TITLE
|
||||
@Cleanup Cursor cursor = providerClient.query(groupsUri,
|
||||
new String[] { Groups.SOURCE_ID },
|
||||
Groups.TITLE + " IS NULL", null, null
|
||||
);
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
// found group, set TITLE to SOURCE_ID and other details
|
||||
String sourceID = cursor.getString(0);
|
||||
pendingOperations.add(ContentProviderOperation.newUpdate(groupsUri)
|
||||
.withSelection(Groups.SOURCE_ID + "=?", new String[] { sourceID })
|
||||
.withValue(Groups.TITLE, sourceID)
|
||||
.withValue(Groups.GROUP_VISIBLE, 1)
|
||||
.build());
|
||||
super.commit();
|
||||
}
|
||||
} catch (RemoteException e) {
|
||||
throw new LocalStorageException("Couldn't update group names", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* methods for populating the data object from the content provider */
|
||||
|
||||
@@ -168,6 +196,7 @@ public class LocalAddressBook extends LocalCollection<Contact> {
|
||||
populateNickname(c);
|
||||
populateNote(c);
|
||||
populatePostalAddresses(c);
|
||||
populateCategories(c);
|
||||
populateURLs(c);
|
||||
populateEvents(c);
|
||||
populateSipAddress(c);
|
||||
@@ -462,6 +491,57 @@ public class LocalAddressBook extends LocalCollection<Contact> {
|
||||
}
|
||||
}
|
||||
|
||||
protected void populateCategories(Contact c) throws RemoteException {
|
||||
@Cleanup Cursor cursorMemberships = providerClient.query(dataURI(),
|
||||
new String[] { GroupMembership.GROUP_ROW_ID, GroupMembership.GROUP_SOURCE_ID },
|
||||
GroupMembership.RAW_CONTACT_ID + "=? AND " + Data.MIMETYPE + "=?",
|
||||
new String[] { String.valueOf(c.getLocalID()), GroupMembership.CONTENT_ITEM_TYPE }, null);
|
||||
List<String> categories = c.getCategories();
|
||||
while (cursorMemberships != null && cursorMemberships.moveToNext()) {
|
||||
long rowID = cursorMemberships.getLong(0);
|
||||
String sourceID = cursorMemberships.getString(1);
|
||||
|
||||
// either a row ID or a source ID must be available
|
||||
String where, whereArg;
|
||||
if (sourceID == null) {
|
||||
where = Groups._ID + "=?";
|
||||
whereArg = String.valueOf(rowID);
|
||||
} else {
|
||||
where = Groups.SOURCE_ID + "=?";
|
||||
whereArg = sourceID;
|
||||
}
|
||||
where += " AND " + Groups.DELETED + "=0"; // ignore deleted groups
|
||||
Log.d(TAG, "Populating group from " + where + " " + whereArg);
|
||||
|
||||
// fetch group
|
||||
@Cleanup Cursor cursorGroups = providerClient.query(Groups.CONTENT_URI,
|
||||
new String[] { Groups.TITLE },
|
||||
where, new String[] { whereArg }, null
|
||||
);
|
||||
if (cursorGroups != null && cursorGroups.moveToNext()) {
|
||||
String title = cursorGroups.getString(0);
|
||||
|
||||
if (sourceID == null) { // Group wasn't created by DAVdroid
|
||||
// SOURCE_ID IS NULL <=> _ID IS NOT NULL
|
||||
Log.d(TAG, "Setting SOURCE_ID of non-DAVdroid group to title: " + title);
|
||||
|
||||
ContentValues v = new ContentValues(1);
|
||||
v.put(Groups.SOURCE_ID, title);
|
||||
v.put(Groups.GROUP_IS_READ_ONLY, 0);
|
||||
v.put(Groups.GROUP_VISIBLE, 1);
|
||||
providerClient.update(syncAdapterURI(Groups.CONTENT_URI), v, Groups._ID + "=?", new String[] { String.valueOf(rowID) });
|
||||
|
||||
sourceID = title;
|
||||
}
|
||||
|
||||
// add group to CATEGORIES
|
||||
if (sourceID != null)
|
||||
categories.add(sourceID);
|
||||
} else
|
||||
Log.d(TAG, "Group not found (maybe deleted)");
|
||||
}
|
||||
}
|
||||
|
||||
protected void populateURLs(Contact c) throws RemoteException {
|
||||
@Cleanup Cursor cursor = providerClient.query(dataURI(), new String[] { Website.URL },
|
||||
Website.RAW_CONTACT_ID + "=? AND " + Data.MIMETYPE + "=?",
|
||||
@@ -498,20 +578,24 @@ public class LocalAddressBook extends LocalCollection<Contact> {
|
||||
SipAddress.RAW_CONTACT_ID + "=? AND " + Data.MIMETYPE + "=?",
|
||||
new String[] { String.valueOf(c.getLocalID()), SipAddress.CONTENT_ITEM_TYPE }, null);
|
||||
if (cursor != null && cursor.moveToNext()) {
|
||||
Impp impp = new Impp("sip:" + cursor.getString(0));
|
||||
switch (cursor.getInt(1)) {
|
||||
case SipAddress.TYPE_HOME:
|
||||
impp.addType(ImppType.HOME);
|
||||
break;
|
||||
case SipAddress.TYPE_WORK:
|
||||
impp.addType(ImppType.WORK);
|
||||
break;
|
||||
case SipAddress.TYPE_CUSTOM:
|
||||
String customType = cursor.getString(2);
|
||||
if (!StringUtils.isEmpty(customType))
|
||||
impp.addType(ImppType.get(labelToXName(customType)));
|
||||
try {
|
||||
Impp impp = new Impp("sip:" + cursor.getString(0));
|
||||
switch (cursor.getInt(1)) {
|
||||
case SipAddress.TYPE_HOME:
|
||||
impp.addType(ImppType.HOME);
|
||||
break;
|
||||
case SipAddress.TYPE_WORK:
|
||||
impp.addType(ImppType.WORK);
|
||||
break;
|
||||
case SipAddress.TYPE_CUSTOM:
|
||||
String customType = cursor.getString(2);
|
||||
if (!StringUtils.isEmpty(customType))
|
||||
impp.addType(ImppType.get(labelToXName(customType)));
|
||||
}
|
||||
c.getImpps().add(impp);
|
||||
} catch(IllegalArgumentException e) {
|
||||
Log.e(TAG, "Illegal SIP URI", e);
|
||||
}
|
||||
c.getImpps().add(impp);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -529,7 +613,7 @@ public class LocalAddressBook extends LocalCollection<Contact> {
|
||||
.withValue(entryColumnUID(), contact.getUid())
|
||||
.withValue(entryColumnETag(), contact.getETag())
|
||||
.withValue(COLUMN_UNKNOWN_PROPERTIES, contact.getUnknownProperties())
|
||||
.withValue(RawContacts.STARRED, contact.isStarred());
|
||||
.withValue(RawContacts.STARRED, contact.isStarred() ? 1 : 0);
|
||||
}
|
||||
|
||||
|
||||
@@ -562,7 +646,8 @@ public class LocalAddressBook extends LocalCollection<Contact> {
|
||||
for (Address address : contact.getAddresses())
|
||||
queueOperation(buildAddress(newDataInsertBuilder(localID, backrefIdx), address));
|
||||
|
||||
// TODO group membership
|
||||
for (String category : contact.getCategories())
|
||||
queueOperation(buildGroupMembership(newDataInsertBuilder(localID, backrefIdx), category));
|
||||
|
||||
for (String url : contact.getURLs())
|
||||
queueOperation(buildURL(newDataInsertBuilder(localID, backrefIdx), url));
|
||||
@@ -698,7 +783,7 @@ public class LocalAddressBook extends LocalCollection<Contact> {
|
||||
.withValue(Email.ADDRESS, email.getValue())
|
||||
.withValue(Email.TYPE, typeCode)
|
||||
.withValue(Email.IS_PRIMARY, is_primary ? 1 : 0)
|
||||
.withValue(Phone.IS_SUPER_PRIMARY, is_primary ? 1 : 0);;
|
||||
.withValue(Phone.IS_SUPER_PRIMARY, is_primary ? 1 : 0);
|
||||
if (typeLabel != null)
|
||||
builder = builder.withValue(Email.LABEL, typeLabel);
|
||||
return builder;
|
||||
@@ -870,6 +955,12 @@ public class LocalAddressBook extends LocalCollection<Contact> {
|
||||
builder = builder.withValue(StructuredPostal.LABEL, typeLabel);
|
||||
return builder;
|
||||
}
|
||||
|
||||
protected Builder buildGroupMembership(Builder builder, String group) {
|
||||
return builder
|
||||
.withValue(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE)
|
||||
.withValue(GroupMembership.GROUP_SOURCE_ID, group);
|
||||
}
|
||||
|
||||
protected Builder buildURL(Builder builder, String url) {
|
||||
return builder
|
||||
@@ -1,44 +1,12 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2014 Ricki Hirner (bitfire web engineering).
|
||||
/*
|
||||
* 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
|
||||
******************************************************************************/
|
||||
*/
|
||||
package at.bitfire.davdroid.resource;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.text.ParseException;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import lombok.Cleanup;
|
||||
import lombok.Getter;
|
||||
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.Status;
|
||||
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
|
||||
import android.accounts.Account;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.annotation.TargetApi;
|
||||
@@ -60,7 +28,48 @@ import android.provider.CalendarContract.Events;
|
||||
import android.provider.CalendarContract.Reminders;
|
||||
import android.provider.ContactsContract;
|
||||
import android.util.Log;
|
||||
import at.bitfire.davdroid.syncadapter.ServerInfo;
|
||||
|
||||
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.TimeZone;
|
||||
import net.fortuna.ical4j.model.TimeZoneRegistry;
|
||||
import net.fortuna.ical4j.model.TimeZoneRegistryFactory;
|
||||
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.util.TimeZones;
|
||||
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.URL;
|
||||
import java.text.ParseException;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import lombok.Cleanup;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* Represents a locally stored calendar, containing Events.
|
||||
@@ -111,7 +120,7 @@ public class LocalCalendar extends LocalCollection<Event> {
|
||||
|
||||
int color = 0xFFC3EA6E; // fallback: "DAVdroid green"
|
||||
if (info.getColor() != null) {
|
||||
Pattern p = Pattern.compile("#(\\p{XDigit}{6})(\\p{XDigit}{2})?");
|
||||
Pattern p = Pattern.compile("#?(\\p{XDigit}{6})(\\p{XDigit}{2})?");
|
||||
Matcher m = p.matcher(info.getColor());
|
||||
if (m.find()) {
|
||||
int color_rgb = Integer.parseInt(m.group(1), 16);
|
||||
@@ -119,7 +128,7 @@ public class LocalCalendar extends LocalCollection<Event> {
|
||||
color = (color_alpha << 24) | color_rgb;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(Calendars.ACCOUNT_NAME, account.name);
|
||||
values.put(Calendars.ACCOUNT_TYPE, account.type);
|
||||
@@ -170,6 +179,7 @@ public class LocalCalendar extends LocalCollection<Event> {
|
||||
super(account, providerClient);
|
||||
this.id = id;
|
||||
this.url = url;
|
||||
sqlFilter = "ORIGINAL_ID IS NULL";
|
||||
}
|
||||
|
||||
|
||||
@@ -200,6 +210,25 @@ public class LocalCalendar extends LocalCollection<Event> {
|
||||
}
|
||||
}
|
||||
|
||||
@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 */
|
||||
|
||||
@@ -208,26 +237,61 @@ public class LocalCalendar extends LocalCollection<Event> {
|
||||
}
|
||||
|
||||
public void deleteAllExceptRemoteNames(Resource[] remoteResources) {
|
||||
String where;
|
||||
|
||||
if (remoteResources.length != 0) {
|
||||
List<String> sqlFileNames = new LinkedList<String>();
|
||||
for (Resource res : remoteResources)
|
||||
sqlFileNames.add(DatabaseUtils.sqlEscapeString(res.getName()));
|
||||
where = entryColumnRemoteName() + " NOT IN (" + StringUtils.join(sqlFileNames, ",") + ")";
|
||||
} else
|
||||
where = entryColumnRemoteName() + " IS NOT NULL";
|
||||
|
||||
Builder builder = ContentProviderOperation.newDelete(entriesURI())
|
||||
.withSelection(entryColumnParentID() + "=? AND (" + where + ")", new String[] { String.valueOf(id) });
|
||||
pendingOperations.add(builder
|
||||
.withYieldAllowed(true)
|
||||
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()
|
||||
);
|
||||
}
|
||||
|
||||
@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[] { Long.toString(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[] { Long.toString(resource.getLocalID()) })
|
||||
.build()
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/* methods for populating the data object from the content provider */
|
||||
|
||||
|
||||
@Override
|
||||
public void populate(Resource resource) throws LocalStorageException {
|
||||
@@ -241,7 +305,8 @@ public class LocalCalendar extends LocalCollection<Event> {
|
||||
/* 8 */ Events.STATUS, Events.ACCESS_LEVEL,
|
||||
/* 10 */ Events.RRULE, Events.RDATE, Events.EXRULE, Events.EXDATE,
|
||||
/* 14 */ Events.HAS_ATTENDEE_DATA, Events.ORGANIZER, Events.SELF_ATTENDEE_STATUS,
|
||||
/* 17 */ entryColumnUID(), Events.DURATION, Events.AVAILABILITY
|
||||
/* 17 */ entryColumnUID(), Events.DURATION, Events.AVAILABILITY,
|
||||
/* 20 */ Events.ORIGINAL_ALL_DAY, Events.ORIGINAL_INSTANCE_TIME
|
||||
}, null, null, null);
|
||||
if (cursor != null && cursor.moveToNext()) {
|
||||
e.setUid(cursor.getString(17));
|
||||
@@ -309,7 +374,21 @@ public class LocalCalendar extends LocalCollection<Event> {
|
||||
} catch (IllegalArgumentException ex) {
|
||||
Log.w(TAG, "Invalid recurrence rules, ignoring", ex);
|
||||
}
|
||||
|
||||
|
||||
// recurrence exceptions
|
||||
if (!cursor.isNull(21)) {
|
||||
long originalInstanceTime = cursor.getLong(21);
|
||||
boolean originalAllDay = cursor.getInt(20) != 0;
|
||||
Date originalDate = originalAllDay ?
|
||||
new Date(originalInstanceTime) :
|
||||
new DateTime(originalInstanceTime);
|
||||
if (originalDate instanceof DateTime)
|
||||
((DateTime)originalDate).setUtc(true);
|
||||
e.setRecurrenceId(new RecurrenceId(originalDate));
|
||||
} else
|
||||
// this event may have exceptions
|
||||
populateExceptions(e);
|
||||
|
||||
// status
|
||||
switch (cursor.getInt(8)) {
|
||||
case Events.STATUS_CONFIRMED:
|
||||
@@ -353,15 +432,35 @@ public class LocalCalendar extends LocalCollection<Event> {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void populateExceptions(Event e) throws RemoteException {
|
||||
Uri exceptionsUri = Events.CONTENT_URI.buildUpon()
|
||||
.appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true")
|
||||
.build();
|
||||
@Cleanup Cursor c = providerClient.query(exceptionsUri, new String[] {
|
||||
/* 0 */ 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);
|
||||
Log.i(TAG, "Found exception ID " + exceptionId + " of original ID " + e.getLocalID());
|
||||
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 populateAttendees(Event e) throws RemoteException {
|
||||
Uri attendeesUri = Attendees.CONTENT_URI.buildUpon()
|
||||
.appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true")
|
||||
.build();
|
||||
@Cleanup Cursor c = providerClient.query(attendeesUri, new String[] {
|
||||
@Cleanup Cursor c = providerClient.query(attendeesUri, new String[]{
|
||||
/* 0 */ Attendees.ATTENDEE_EMAIL, Attendees.ATTENDEE_NAME, Attendees.ATTENDEE_TYPE,
|
||||
/* 3 */ Attendees.ATTENDEE_RELATIONSHIP, Attendees.STATUS
|
||||
}, Attendees.EVENT_ID + "=?", new String[] { String.valueOf(e.getLocalID()) }, null);
|
||||
}, Attendees.EVENT_ID + "=?", new String[]{String.valueOf(e.getLocalID())}, null);
|
||||
while (c != null && c.moveToNext()) {
|
||||
try {
|
||||
Attendee attendee = new Attendee(new URI("mailto", c.getString(0), null));
|
||||
@@ -406,13 +505,13 @@ public class LocalCalendar extends LocalCollection<Event> {
|
||||
break;
|
||||
}
|
||||
|
||||
e.addAttendee(attendee);
|
||||
e.getAttendees().add(attendee);
|
||||
} catch (URISyntaxException ex) {
|
||||
Log.e(TAG, "Couldn't parse attendee information, ignoring", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void populateReminders(Event e) throws RemoteException {
|
||||
// reminders
|
||||
Uri remindersUri = Reminders.CONTENT_URI.buildUpon()
|
||||
@@ -433,10 +532,10 @@ public class LocalCalendar extends LocalCollection<Event> {
|
||||
props.add(Action.DISPLAY);
|
||||
props.add(new Description(e.getSummary()));
|
||||
}
|
||||
e.addAlarm(alarm);
|
||||
e.getAlarms().add(alarm);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* content builder methods */
|
||||
|
||||
@@ -446,9 +545,6 @@ public class LocalCalendar extends LocalCollection<Event> {
|
||||
|
||||
builder = builder
|
||||
.withValue(Events.CALENDAR_ID, id)
|
||||
.withValue(entryColumnRemoteName(), event.getName())
|
||||
.withValue(entryColumnETag(), event.getETag())
|
||||
.withValue(entryColumnUID(), event.getUid())
|
||||
.withValue(Events.ALL_DAY, event.isAllDay() ? 1 : 0)
|
||||
.withValue(Events.DTSTART, event.getDtStartInMillis())
|
||||
.withValue(Events.EVENT_TIMEZONE, event.getDtStartTzID())
|
||||
@@ -456,7 +552,23 @@ public class LocalCalendar extends LocalCollection<Event> {
|
||||
.withValue(Events.GUESTS_CAN_INVITE_OTHERS, 1)
|
||||
.withValue(Events.GUESTS_CAN_MODIFY, 1)
|
||||
.withValue(Events.GUESTS_CAN_SEE_GUESTS, 1);
|
||||
|
||||
|
||||
RecurrenceId recurrenceId = event.getRecurrenceId();
|
||||
if (recurrenceId == null) {
|
||||
// this event is a "master event" (not an exception)
|
||||
builder = builder
|
||||
.withValue(entryColumnRemoteName(), event.getName())
|
||||
.withValue(entryColumnETag(), event.getETag())
|
||||
.withValue(entryColumnUID(), event.getUid());
|
||||
} else {
|
||||
builder = 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;
|
||||
@@ -518,8 +630,13 @@ public class LocalCalendar extends LocalCollection<Event> {
|
||||
@Override
|
||||
protected void addDataRows(Resource resource, long localID, int backrefIdx) {
|
||||
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());
|
||||
}
|
||||
@@ -527,14 +644,49 @@ public class LocalCalendar extends LocalCollection<Event> {
|
||||
@Override
|
||||
protected void removeDataRows(Resource resource) {
|
||||
Event event = (Event)resource;
|
||||
// delete exceptions
|
||||
pendingOperations.add(ContentProviderOperation.newDelete(entriesURI())
|
||||
.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);
|
||||
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.
|
||||
|
||||
RecurrenceId recurrenceId = exception.getRecurrenceId();
|
||||
Date date = recurrenceId.getDate();
|
||||
|
||||
boolean originalAllDay = master.isAllDay();
|
||||
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) {
|
||||
@@ -1,15 +1,12 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2014 Ricki Hirner (bitfire web engineering).
|
||||
/*
|
||||
* 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
|
||||
******************************************************************************/
|
||||
*/
|
||||
package at.bitfire.davdroid.resource;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
import lombok.Cleanup;
|
||||
import android.accounts.Account;
|
||||
import android.content.ContentProviderClient;
|
||||
import android.content.ContentProviderOperation;
|
||||
@@ -23,6 +20,10 @@ import android.os.RemoteException;
|
||||
import android.provider.CalendarContract;
|
||||
import android.util.Log;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
import lombok.Cleanup;
|
||||
|
||||
/**
|
||||
* Represents a locally-stored synchronizable collection (for instance, the
|
||||
* address book or a calendar). Manages a CTag that stores the last known
|
||||
@@ -31,7 +32,7 @@ import android.util.Log;
|
||||
* @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.LocalCollection";
|
||||
private static final String TAG = "davdroid.Collection";
|
||||
|
||||
protected Account account;
|
||||
protected ContentProviderClient providerClient;
|
||||
@@ -65,6 +66,9 @@ public abstract class LocalCollection<T extends Resource> {
|
||||
|
||||
/** column name of an entry's UID */
|
||||
abstract protected String entryColumnUID();
|
||||
|
||||
/** SQL filter expression */
|
||||
String sqlFilter;
|
||||
|
||||
|
||||
LocalCollection(Account account, ContentProviderClient providerClient) {
|
||||
@@ -88,6 +92,7 @@ public abstract class LocalCollection<T extends Resource> {
|
||||
/**
|
||||
* 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
|
||||
@@ -96,6 +101,8 @@ public abstract class LocalCollection<T extends Resource> {
|
||||
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() },
|
||||
@@ -126,7 +133,8 @@ public abstract class LocalCollection<T extends Resource> {
|
||||
|
||||
/**
|
||||
* Finds updated resources (resources which have already been uploaded, but have changed locally).
|
||||
* Updated resources are 1) dirty, and 2) already have an ETag.
|
||||
* 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
|
||||
@@ -135,6 +143,8 @@ public abstract class LocalCollection<T extends Resource> {
|
||||
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() },
|
||||
@@ -154,6 +164,7 @@ public abstract class LocalCollection<T extends Resource> {
|
||||
/**
|
||||
* 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
|
||||
@@ -162,6 +173,8 @@ public abstract class LocalCollection<T extends Resource> {
|
||||
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() },
|
||||
@@ -179,7 +192,7 @@ public abstract class LocalCollection<T extends Resource> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a specific resource by ID.
|
||||
* 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
|
||||
@@ -190,7 +203,7 @@ public abstract class LocalCollection<T extends Resource> {
|
||||
public T findById(long localID, boolean populate) throws LocalStorageException {
|
||||
try {
|
||||
@Cleanup Cursor cursor = providerClient.query(ContentUris.withAppendedId(entriesURI(), localID),
|
||||
new String[] { entryColumnRemoteName(), entryColumnETag() }, null, null, null);
|
||||
new String[] { entryColumnRemoteName(), entryColumnETag() }, sqlFilter, null, null);
|
||||
if (cursor != null && cursor.moveToNext()) {
|
||||
T resource = newResource(localID, cursor.getString(0), cursor.getString(1));
|
||||
if (populate)
|
||||
@@ -204,7 +217,7 @@ public abstract class LocalCollection<T extends Resource> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a specific resource by remote file name.
|
||||
* Finds a specific resource by remote file name. Only records matching sqlFilter are taken into account.
|
||||
* @param localID 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
|
||||
@@ -213,10 +226,13 @@ public abstract class LocalCollection<T extends Resource> {
|
||||
* @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() },
|
||||
entryColumnRemoteName() + "=?", new String[] { remoteName }, null);
|
||||
where, new String[] { remoteName }, null);
|
||||
if (cursor != null && cursor.moveToNext()) {
|
||||
T resource = newResource(cursor.getLong(0), cursor.getString(1), cursor.getString(2));
|
||||
if (populate)
|
||||
@@ -284,7 +300,7 @@ public abstract class LocalCollection<T extends Resource> {
|
||||
|
||||
/** 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 + " to " + eTag);
|
||||
Log.d(TAG, "Setting ETag of local resource " + res.getName() + " to " + eTag);
|
||||
|
||||
ContentValues values = new ContentValues(1);
|
||||
values.put(entryColumnETag(), eTag);
|
||||
@@ -334,7 +350,7 @@ public abstract class LocalCollection<T extends Resource> {
|
||||
.build();
|
||||
}
|
||||
|
||||
protected Builder newDataInsertBuilder(Uri dataUri, String refFieldName, long raw_ref_id, Integer backrefIdx) {
|
||||
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);
|
||||
@@ -1,10 +1,10 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2014 Ricki Hirner (bitfire web engineering).
|
||||
/*
|
||||
* 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
|
||||
******************************************************************************/
|
||||
*/
|
||||
package at.bitfire.davdroid.resource;
|
||||
|
||||
public class LocalStorageException extends Exception {
|
||||
@@ -1,10 +1,10 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2014 Ricki Hirner (bitfire web engineering).
|
||||
/*
|
||||
* 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
|
||||
******************************************************************************/
|
||||
*/
|
||||
package at.bitfire.davdroid.resource;
|
||||
|
||||
/**
|
||||
@@ -1,25 +1,31 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2014 Ricki Hirner (bitfire web engineering).
|
||||
/*
|
||||
* 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
|
||||
******************************************************************************/
|
||||
*/
|
||||
package at.bitfire.davdroid.resource;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import net.fortuna.ical4j.model.ValidationException;
|
||||
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.apache.http.client.utils.URIUtilsHC4;
|
||||
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.net.URL;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
import lombok.Cleanup;
|
||||
import lombok.Getter;
|
||||
import net.fortuna.ical4j.model.ValidationException;
|
||||
import android.util.Log;
|
||||
import at.bitfire.davdroid.URIUtils;
|
||||
import at.bitfire.davdroid.webdav.DavException;
|
||||
import at.bitfire.davdroid.webdav.DavMultiget;
|
||||
import at.bitfire.davdroid.webdav.DavNoContentException;
|
||||
@@ -27,7 +33,9 @@ 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 ch.boye.httpclientandroidlib.impl.client.CloseableHttpClient;
|
||||
import ezvcard.io.text.VCardParseException;
|
||||
import lombok.Cleanup;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* Represents a remotely stored synchronizable collection (collection as in
|
||||
@@ -37,36 +45,40 @@ import ch.boye.httpclientandroidlib.impl.client.CloseableHttpClient;
|
||||
*/
|
||||
public abstract class RemoteCollection<T extends Resource> {
|
||||
private static final String TAG = "davdroid.RemoteCollection";
|
||||
|
||||
|
||||
CloseableHttpClient httpClient;
|
||||
@Getter WebDavResource collection;
|
||||
URI baseURI;
|
||||
@Getter WebDavResource collection;
|
||||
|
||||
abstract protected String memberContentType();
|
||||
|
||||
abstract protected DavMultiget.Type multiGetType();
|
||||
|
||||
abstract protected T newResourceSkeleton(String name, String ETag);
|
||||
|
||||
|
||||
public RemoteCollection(CloseableHttpClient httpClient, String baseURL, String user, String password, boolean preemptiveAuth) throws URISyntaxException {
|
||||
this.httpClient = httpClient;
|
||||
|
||||
collection = new WebDavResource(httpClient, new URI(baseURL), user, password, preemptiveAuth);
|
||||
|
||||
baseURI = URIUtils.parseURI(baseURL, false);
|
||||
collection = new WebDavResource(httpClient, baseURI, user, password, preemptiveAuth);
|
||||
}
|
||||
|
||||
|
||||
/* collection operations */
|
||||
|
||||
public String getCTag() throws IOException, HttpException {
|
||||
public String getCTag() throws URISyntaxException, IOException, HttpException {
|
||||
try {
|
||||
if (collection.getCTag() == null && collection.getMembers() == null) // not already fetched
|
||||
if (collection.getCTag() == null && collection.getMembers() == null) // not already fetched
|
||||
collection.propfind(HttpPropfind.Mode.COLLECTION_CTAG);
|
||||
} catch (DavException e) {
|
||||
return null;
|
||||
}
|
||||
return collection.getCTag();
|
||||
}
|
||||
|
||||
public Resource[] getMemberETags() throws IOException, DavException, HttpException {
|
||||
|
||||
public Resource[] getMemberETags() throws URISyntaxException, IOException, DavException, HttpException {
|
||||
collection.propfind(HttpPropfind.Mode.MEMBERS_ETAG);
|
||||
|
||||
|
||||
List<T> resources = new LinkedList<T>();
|
||||
if (collection.getMembers() != null) {
|
||||
for (WebDavResource member : collection.getMembers())
|
||||
@@ -74,30 +86,30 @@ public abstract class RemoteCollection<T extends Resource> {
|
||||
}
|
||||
return resources.toArray(new Resource[0]);
|
||||
}
|
||||
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public Resource[] multiGet(Resource[] resources) throws IOException, DavException, HttpException {
|
||||
public Resource[] multiGet(Resource[] resources) throws URISyntaxException, IOException, DavException, HttpException {
|
||||
try {
|
||||
if (resources.length == 1)
|
||||
return (T[]) new Resource[] { get(resources[0]) };
|
||||
|
||||
return (T[]) new Resource[]{get(resources[0])};
|
||||
|
||||
Log.i(TAG, "Multi-getting " + resources.length + " remote resource(s)");
|
||||
|
||||
|
||||
LinkedList<String> names = new LinkedList<String>();
|
||||
for (Resource resource : resources)
|
||||
names.add(resource.getName());
|
||||
|
||||
|
||||
LinkedList<T> foundResources = new LinkedList<T>();
|
||||
collection.multiGet(multiGetType(), names.toArray(new String[0]));
|
||||
if (collection.getMembers() == null)
|
||||
throw new DavNoContentException();
|
||||
|
||||
|
||||
for (WebDavResource member : collection.getMembers()) {
|
||||
T resource = newResourceSkeleton(member.getName(), member.getETag());
|
||||
try {
|
||||
if (member.getContent() != null) {
|
||||
@Cleanup InputStream is = new ByteArrayInputStream(member.getContent());
|
||||
resource.parseEntity(is);
|
||||
resource.parseEntity(is, getDownloader());
|
||||
foundResources.add(resource);
|
||||
} else
|
||||
Log.e(TAG, "Ignoring entity without content");
|
||||
@@ -105,63 +117,99 @@ public abstract class RemoteCollection<T extends Resource> {
|
||||
Log.e(TAG, "Ignoring unparseable entity in multi-response", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return foundResources.toArray(new Resource[0]);
|
||||
} 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 IOException, HttpException, DavException, InvalidResourceException {
|
||||
public Resource get(Resource resource) throws URISyntaxException, IOException, HttpException, DavException, InvalidResourceException {
|
||||
WebDavResource member = new WebDavResource(collection, resource.getName());
|
||||
member.get();
|
||||
|
||||
|
||||
if (resource instanceof Contact)
|
||||
member.get(Contact.MIME_TYPE);
|
||||
else if (resource instanceof Event)
|
||||
member.get(Event.MIME_TYPE);
|
||||
else {
|
||||
Log.wtf(TAG, "Should fetch something, but neither contact nor calendar");
|
||||
throw new InvalidResourceException("Didn't now which MIME type to accept");
|
||||
}
|
||||
|
||||
byte[] data = member.getContent();
|
||||
if (data == null)
|
||||
throw new DavNoContentException();
|
||||
|
||||
|
||||
@Cleanup InputStream is = new ByteArrayInputStream(data);
|
||||
resource.parseEntity(is);
|
||||
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 IOException, HttpException, ValidationException {
|
||||
public String add(Resource res) throws URISyntaxException, IOException, HttpException, ValidationException {
|
||||
WebDavResource member = new WebDavResource(collection, res.getName(), res.getETag());
|
||||
member.setContentType(memberContentType());
|
||||
|
||||
|
||||
@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.invalidateCTag();
|
||||
|
||||
|
||||
return eTag;
|
||||
}
|
||||
|
||||
public void delete(Resource res) throws IOException, HttpException {
|
||||
public void delete(Resource res) throws URISyntaxException, IOException, HttpException {
|
||||
WebDavResource member = new WebDavResource(collection, res.getName(), res.getETag());
|
||||
member.delete();
|
||||
|
||||
|
||||
collection.invalidateCTag();
|
||||
}
|
||||
|
||||
|
||||
// returns ETag of the updated resource, if returned by server
|
||||
public String update(Resource res) throws IOException, HttpException, ValidationException {
|
||||
public String update(Resource res) throws URISyntaxException, IOException, HttpException, ValidationException {
|
||||
WebDavResource member = new WebDavResource(collection, res.getName(), res.getETag());
|
||||
member.setContentType(memberContentType());
|
||||
|
||||
|
||||
@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.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");
|
||||
|
||||
if (uri.getScheme().equalsIgnoreCase(baseURI.getScheme()) &&
|
||||
uri.getAuthority().equalsIgnoreCase(baseURI.getAuthority())) {
|
||||
// resource is on same server, send Authorization
|
||||
WebDavResource file = new WebDavResource(collection, uri);
|
||||
file.get("image/*");
|
||||
return file.getContent();
|
||||
} else {
|
||||
// resource is on an external server, don't send Authorization
|
||||
return IOUtils.toByteArray(uri);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,21 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2014 Ricki Hirner (bitfire web engineering).
|
||||
/*
|
||||
* 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
|
||||
******************************************************************************/
|
||||
*/
|
||||
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 java.net.URL;
|
||||
|
||||
import at.bitfire.davdroid.webdav.DavException;
|
||||
import at.bitfire.davdroid.webdav.HttpException;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.ToString;
|
||||
@@ -39,8 +44,17 @@ public abstract class Resource {
|
||||
/** 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) */
|
||||
public abstract void parseEntity(InputStream entity) throws IOException, InvalidResourceException;
|
||||
/** 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;
|
||||
|
||||
/** writes the resource data to an output stream (for instance, .vcf file for Contact) */
|
||||
public abstract ByteArrayOutputStream toEntity() throws IOException;
|
||||
|
||||
|
||||
public interface AssetDownloader {
|
||||
public byte[] download(URI url) throws URISyntaxException, IOException, HttpException, DavException;
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,19 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2014 Ricki Hirner (bitfire web engineering).
|
||||
/*
|
||||
* 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
|
||||
******************************************************************************/
|
||||
package at.bitfire.davdroid.syncadapter;
|
||||
*/
|
||||
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 ezvcard.VCardVersion;
|
||||
import lombok.Data;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
@@ -19,7 +22,11 @@ import lombok.RequiredArgsConstructor;
|
||||
public class ServerInfo implements Serializable {
|
||||
private static final long serialVersionUID = 6744847358282980437L;
|
||||
|
||||
final private String providedURL;
|
||||
enum Scheme {
|
||||
HTTP, HTTPS, MAILTO
|
||||
}
|
||||
|
||||
final private URI baseURI;
|
||||
final private String userName, password;
|
||||
final boolean authPreemptive;
|
||||
|
||||
@@ -30,6 +37,7 @@ public class ServerInfo implements Serializable {
|
||||
addressBooks = new LinkedList<ResourceInfo>(),
|
||||
calendars = new LinkedList<ResourceInfo>();
|
||||
|
||||
|
||||
public boolean hasEnabledCalendars() {
|
||||
for (ResourceInfo calendar : calendars)
|
||||
if (calendar.enabled)
|
||||
@@ -43,7 +51,7 @@ public class ServerInfo implements Serializable {
|
||||
public static class ResourceInfo implements Serializable {
|
||||
private static final long serialVersionUID = -5516934508229552112L;
|
||||
|
||||
enum Type {
|
||||
public enum Type {
|
||||
ADDRESS_BOOK,
|
||||
CALENDAR
|
||||
}
|
||||
@@ -52,8 +60,27 @@ public class ServerInfo implements Serializable {
|
||||
|
||||
final Type type;
|
||||
final boolean readOnly;
|
||||
final String URL, title, description, color;
|
||||
|
||||
|
||||
final String URL, // absolute URL of resource
|
||||
title,
|
||||
description,
|
||||
color;
|
||||
|
||||
VCardVersion vCardVersion;
|
||||
|
||||
String timezone;
|
||||
|
||||
|
||||
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,10 +1,10 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2014 Ricki Hirner (bitfire web engineering).
|
||||
/*
|
||||
* 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
|
||||
******************************************************************************/
|
||||
*/
|
||||
package at.bitfire.davdroid.syncadapter;
|
||||
|
||||
import android.accounts.AbstractAccountAuthenticator;
|
||||
@@ -18,6 +18,8 @@ import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.os.IBinder;
|
||||
|
||||
import at.bitfire.davdroid.ui.setup.AddAccountActivity;
|
||||
|
||||
public class AccountAuthenticatorService extends Service {
|
||||
private static AccountAuthenticator accountAuthenticator;
|
||||
|
||||
@@ -54,8 +56,7 @@ public class AccountAuthenticatorService extends Service {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Bundle confirmCredentials(AccountAuthenticatorResponse response, Account account, Bundle options)
|
||||
throws NetworkErrorException {
|
||||
public Bundle confirmCredentials(AccountAuthenticatorResponse response, Account account, Bundle options) throws NetworkErrorException {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -65,8 +66,7 @@ public class AccountAuthenticatorService extends Service {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Bundle getAuthToken(AccountAuthenticatorResponse response, Account account, String authTokenType, Bundle options)
|
||||
throws NetworkErrorException {
|
||||
public Bundle getAuthToken(AccountAuthenticatorResponse response, Account account, String authTokenType, Bundle options) throws NetworkErrorException {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -76,14 +76,12 @@ public class AccountAuthenticatorService extends Service {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Bundle hasFeatures(AccountAuthenticatorResponse response, Account account, String[] features)
|
||||
throws NetworkErrorException {
|
||||
public Bundle hasFeatures(AccountAuthenticatorResponse response, Account account, String[] features) throws NetworkErrorException {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Bundle updateCredentials(AccountAuthenticatorResponse response, Account account, String authTokenType,
|
||||
Bundle options) throws NetworkErrorException {
|
||||
public Bundle updateCredentials(AccountAuthenticatorResponse response, Account account, String authTokenType, Bundle options) throws NetworkErrorException {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,29 +1,35 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2014 Ricki Hirner (bitfire web engineering).
|
||||
/*
|
||||
* 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
|
||||
******************************************************************************/
|
||||
*/
|
||||
package at.bitfire.davdroid.syncadapter;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
|
||||
import lombok.Cleanup;
|
||||
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.provider.ContactsContract;
|
||||
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";
|
||||
|
||||
@@ -35,8 +41,11 @@ public class AccountSettings {
|
||||
KEY_AUTH_PREEMPTIVE = "auth_preemptive",
|
||||
|
||||
KEY_ADDRESSBOOK_URL = "addressbook_url",
|
||||
KEY_ADDRESSBOOK_CTAG = "addressbook_ctag";
|
||||
|
||||
KEY_ADDRESSBOOK_CTAG = "addressbook_ctag",
|
||||
KEY_ADDRESSBOOK_VCARD_VERSION = "addressbook_vcard_version";
|
||||
|
||||
public final static long SYNC_INTERVAL_MANUALLY = -1;
|
||||
|
||||
Context context;
|
||||
AccountManager accountManager;
|
||||
Account account;
|
||||
@@ -68,27 +77,78 @@ public class AccountSettings {
|
||||
for (ServerInfo.ResourceInfo addressBook : serverInfo.getAddressBooks())
|
||||
if (addressBook.isEnabled()) {
|
||||
bundle.putString(KEY_ADDRESSBOOK_URL, addressBook.getURL());
|
||||
bundle.putString(KEY_ADDRESSBOOK_VCARD_VERSION, addressBook.getVCardVersion().getVersion());
|
||||
continue;
|
||||
}
|
||||
return bundle;
|
||||
}
|
||||
|
||||
|
||||
// general settings
|
||||
|
||||
// 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 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 getContactsSyncInterval() {
|
||||
if (ContentResolver.getIsSyncable(account, ContactsContract.AUTHORITY) <= 0)
|
||||
return null;
|
||||
|
||||
if (ContentResolver.getSyncAutomatically(account, ContactsContract.AUTHORITY)) {
|
||||
List<PeriodicSync> syncs = ContentResolver.getPeriodicSyncs(account, ContactsContract.AUTHORITY);
|
||||
if (syncs.isEmpty())
|
||||
return SYNC_INTERVAL_MANUALLY;
|
||||
else
|
||||
return syncs.get(0).period;
|
||||
} else
|
||||
return SYNC_INTERVAL_MANUALLY;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public void setContactsSyncInterval(long seconds) {
|
||||
if (seconds == SYNC_INTERVAL_MANUALLY) {
|
||||
ContentResolver.setSyncAutomatically(account, ContactsContract.AUTHORITY, false);
|
||||
} else {
|
||||
ContentResolver.setSyncAutomatically(account, ContactsContract.AUTHORITY, true);
|
||||
ContentResolver.addPeriodicSync(account, ContactsContract.AUTHORITY, new Bundle(), seconds);
|
||||
}
|
||||
}
|
||||
|
||||
public Long getCalendarsSyncInterval() {
|
||||
if (ContentResolver.getIsSyncable(account, CalendarContract.AUTHORITY) <= 0)
|
||||
return null;
|
||||
|
||||
if (ContentResolver.getSyncAutomatically(account, CalendarContract.AUTHORITY)) {
|
||||
List<PeriodicSync> syncs = ContentResolver.getPeriodicSyncs(account, CalendarContract.AUTHORITY);
|
||||
if (syncs.isEmpty())
|
||||
return SYNC_INTERVAL_MANUALLY;
|
||||
else
|
||||
return syncs.get(0).period;
|
||||
} else
|
||||
return SYNC_INTERVAL_MANUALLY;
|
||||
}
|
||||
|
||||
public void setCalendarsSyncInterval(long seconds) {
|
||||
if (seconds == SYNC_INTERVAL_MANUALLY) {
|
||||
ContentResolver.setSyncAutomatically(account, CalendarContract.AUTHORITY, false);
|
||||
} else {
|
||||
ContentResolver.setSyncAutomatically(account, CalendarContract.AUTHORITY, true);
|
||||
ContentResolver.addPeriodicSync(account, CalendarContract.AUTHORITY, new Bundle(), seconds);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// address book (CardDAV) settings
|
||||
|
||||
public String getAddressBookURL() {
|
||||
@@ -103,6 +163,14 @@ public class AccountSettings {
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
// update from previous account settings
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2014 Ricki Hirner (bitfire web engineering).
|
||||
/*
|
||||
* 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
|
||||
******************************************************************************/
|
||||
*/
|
||||
package at.bitfire.davdroid.syncadapter;
|
||||
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import android.accounts.Account;
|
||||
import android.app.Service;
|
||||
import android.content.ContentProviderClient;
|
||||
@@ -19,6 +15,11 @@ import android.content.Intent;
|
||||
import android.os.IBinder;
|
||||
import android.os.RemoteException;
|
||||
import android.util.Log;
|
||||
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import at.bitfire.davdroid.resource.CalDavCalendar;
|
||||
import at.bitfire.davdroid.resource.LocalCalendar;
|
||||
import at.bitfire.davdroid.resource.LocalCollection;
|
||||
@@ -1,16 +1,12 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2014 Ricki Hirner (bitfire web engineering).
|
||||
/*
|
||||
* 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
|
||||
******************************************************************************/
|
||||
*/
|
||||
package at.bitfire.davdroid.syncadapter;
|
||||
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import android.accounts.Account;
|
||||
import android.app.Service;
|
||||
import android.content.ContentProviderClient;
|
||||
@@ -18,6 +14,11 @@ import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.IBinder;
|
||||
import android.util.Log;
|
||||
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import at.bitfire.davdroid.resource.CardDavAddressBook;
|
||||
import at.bitfire.davdroid.resource.LocalAddressBook;
|
||||
import at.bitfire.davdroid.resource.LocalCollection;
|
||||
@@ -1,50 +1,64 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2014 Ricki Hirner (bitfire web engineering).
|
||||
/*
|
||||
* 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
|
||||
******************************************************************************/
|
||||
*/
|
||||
package at.bitfire.davdroid.syncadapter;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.locks.ReentrantReadWriteLock;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
import org.apache.http.HttpStatus;
|
||||
|
||||
import android.accounts.Account;
|
||||
import android.accounts.AccountManager;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.annotation.TargetApi;
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.AbstractThreadedSyncAdapter;
|
||||
import android.content.ContentProviderClient;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.SyncResult;
|
||||
import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.provider.Settings;
|
||||
import android.util.Log;
|
||||
|
||||
import org.apache.commons.lang.exception.ExceptionUtils;
|
||||
import org.apache.http.HttpStatus;
|
||||
import org.apache.http.impl.client.CloseableHttpClient;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.locks.ReentrantReadWriteLock;
|
||||
|
||||
import javax.net.ssl.SSLException;
|
||||
|
||||
import at.bitfire.davdroid.Constants;
|
||||
import at.bitfire.davdroid.R;
|
||||
import at.bitfire.davdroid.resource.LocalCollection;
|
||||
import at.bitfire.davdroid.resource.LocalStorageException;
|
||||
import at.bitfire.davdroid.resource.RemoteCollection;
|
||||
import at.bitfire.davdroid.ui.settings.AccountActivity;
|
||||
import at.bitfire.davdroid.webdav.DavException;
|
||||
import at.bitfire.davdroid.webdav.DavHttpClient;
|
||||
import at.bitfire.davdroid.webdav.HttpException;
|
||||
import ch.boye.httpclientandroidlib.impl.client.CloseableHttpClient;
|
||||
import lombok.Getter;
|
||||
|
||||
public abstract class DavSyncAdapter extends AbstractThreadedSyncAdapter implements Closeable {
|
||||
private final static String TAG = "davdroid.DavSyncAdapter";
|
||||
|
||||
@Getter private static String androidID;
|
||||
|
||||
protected AccountManager accountManager;
|
||||
|
||||
|
||||
protected Context context;
|
||||
|
||||
/* We use one static httpClient for
|
||||
* - all sync adapters (CalendarsSyncAdapter, ContactsSyncAdapter)
|
||||
* - and all threads (= accounts) of each sync adapter
|
||||
@@ -65,8 +79,8 @@ public abstract class DavSyncAdapter extends AbstractThreadedSyncAdapter impleme
|
||||
if (androidID == null)
|
||||
androidID = Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID);
|
||||
}
|
||||
|
||||
accountManager = AccountManager.get(context);
|
||||
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -93,8 +107,8 @@ public abstract class DavSyncAdapter extends AbstractThreadedSyncAdapter impleme
|
||||
}
|
||||
|
||||
protected abstract Map<LocalCollection<?>, RemoteCollection<?>> getSyncPairs(Account account, ContentProviderClient provider);
|
||||
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
|
||||
@Override
|
||||
public void onPerformSync(Account account, Bundle extras, String authority, ContentProviderClient provider, SyncResult syncResult) {
|
||||
Log.i(TAG, "Performing sync for authority " + authority);
|
||||
@@ -107,18 +121,21 @@ public abstract class DavSyncAdapter extends AbstractThreadedSyncAdapter impleme
|
||||
if (httpClient == null) {
|
||||
Log.d(TAG, "Creating new DavHttpClient");
|
||||
SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(getContext());
|
||||
httpClient = DavHttpClient.create(
|
||||
settings.getBoolean(Constants.SETTING_DISABLE_COMPRESSION, false),
|
||||
settings.getBoolean(Constants.SETTING_NETWORK_LOGGING, false)
|
||||
);
|
||||
httpClient = DavHttpClient.create();
|
||||
}
|
||||
|
||||
// prevent httpClient shutdown until we're ready by holding a read lock
|
||||
// acquiring read lock before releasing write lock will downgrade the write lock to a read lock
|
||||
httpClientLock.readLock().lock();
|
||||
httpClientLock.writeLock().unlock();
|
||||
|
||||
try {
|
||||
|
||||
// TODO use VCard 4.0 if possible
|
||||
AccountSettings accountSettings = new AccountSettings(getContext(), account);
|
||||
Log.d(TAG, "Server supports VCard version " + accountSettings.getAddressBookVCardVersion());
|
||||
|
||||
Exception exceptionToShow = null; // exception to show notification for
|
||||
Intent exceptionIntent = null; // what shall happen when clicking on the exception notification
|
||||
try {
|
||||
// get local <-> remote collection pairs
|
||||
Map<LocalCollection<?>, RemoteCollection<?>> syncCollections = getSyncPairs(account, provider);
|
||||
if (syncCollections == null)
|
||||
@@ -127,35 +144,68 @@ public abstract class DavSyncAdapter extends AbstractThreadedSyncAdapter impleme
|
||||
try {
|
||||
for (Map.Entry<LocalCollection<?>, RemoteCollection<?>> entry : syncCollections.entrySet())
|
||||
new SyncManager(entry.getKey(), entry.getValue()).synchronize(extras.containsKey(ContentResolver.SYNC_EXTRAS_MANUAL), syncResult);
|
||||
|
||||
|
||||
} catch (DavException ex) {
|
||||
syncResult.stats.numParseExceptions++;
|
||||
Log.e(TAG, "Invalid DAV response", ex);
|
||||
|
||||
exceptionToShow = ex;
|
||||
|
||||
} catch (HttpException ex) {
|
||||
if (ex.getCode() == HttpStatus.SC_UNAUTHORIZED) {
|
||||
Log.e(TAG, "HTTP Unauthorized " + ex.getCode(), ex);
|
||||
syncResult.stats.numAuthExceptions++;
|
||||
syncResult.stats.numAuthExceptions++; // hard error
|
||||
|
||||
exceptionToShow = ex;
|
||||
exceptionIntent = new Intent(context, AccountActivity.class);
|
||||
exceptionIntent.putExtra(AccountActivity.EXTRA_ACCOUNT, account);
|
||||
} else if (ex.isClientError()) {
|
||||
Log.e(TAG, "Hard HTTP error " + ex.getCode(), ex);
|
||||
syncResult.stats.numParseExceptions++;
|
||||
syncResult.stats.numParseExceptions++; // hard error
|
||||
exceptionToShow = ex;
|
||||
} else {
|
||||
Log.w(TAG, "Soft HTTP error " + ex.getCode() + " (Android will try again later)", ex);
|
||||
syncResult.stats.numIoExceptions++;
|
||||
syncResult.stats.numIoExceptions++; // soft error
|
||||
}
|
||||
|
||||
} catch (LocalStorageException ex) {
|
||||
syncResult.databaseError = true;
|
||||
syncResult.databaseError = true; // hard error
|
||||
Log.e(TAG, "Local storage (content provider) exception", ex);
|
||||
exceptionToShow = ex;
|
||||
} catch (IOException ex) {
|
||||
syncResult.stats.numIoExceptions++;
|
||||
syncResult.stats.numIoExceptions++; // soft error
|
||||
Log.e(TAG, "I/O error (Android will try again later)", ex);
|
||||
if (ex instanceof SSLException) // always notify on SSL/TLS errors
|
||||
exceptionToShow = ex;
|
||||
} catch (URISyntaxException ex) {
|
||||
syncResult.stats.numParseExceptions++; // hard error
|
||||
Log.e(TAG, "Invalid URI (file name) syntax", ex);
|
||||
exceptionToShow = ex;
|
||||
}
|
||||
} finally {
|
||||
// allow httpClient shutdown
|
||||
httpClientLock.readLock().unlock();
|
||||
}
|
||||
|
||||
|
||||
// show sync errors as notification
|
||||
if (exceptionToShow != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
|
||||
if (exceptionIntent == null)
|
||||
exceptionIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(Constants.WEB_URL_VIEW_LOGS));
|
||||
|
||||
PendingIntent contentIntent = PendingIntent.getActivity(context, 0, exceptionIntent, 0);
|
||||
Notification.Builder builder = new Notification.Builder(context)
|
||||
.setSmallIcon(R.drawable.ic_launcher)
|
||||
.setPriority(Notification.PRIORITY_LOW)
|
||||
.setOnlyAlertOnce(true)
|
||||
.setWhen(System.currentTimeMillis())
|
||||
.setContentTitle(context.getString(R.string.sync_error_title))
|
||||
.setContentText(exceptionToShow.getLocalizedMessage())
|
||||
.setContentInfo(account.name)
|
||||
.setStyle(new Notification.BigTextStyle().bigText(account.name + ":\n" + ExceptionUtils.getFullStackTrace(exceptionToShow)))
|
||||
.setContentIntent(contentIntent);
|
||||
|
||||
NotificationManager notificationManager = (NotificationManager)context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
notificationManager.notify(account.name.hashCode(), builder.build());
|
||||
}
|
||||
|
||||
Log.i(TAG, "Sync complete for " + authority);
|
||||
}
|
||||
|
||||
@@ -1,19 +1,22 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2014 Ricki Hirner (bitfire web engineering).
|
||||
/*
|
||||
* 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
|
||||
******************************************************************************/
|
||||
*/
|
||||
package at.bitfire.davdroid.syncadapter;
|
||||
|
||||
import android.content.SyncResult;
|
||||
import android.util.Log;
|
||||
|
||||
import net.fortuna.ical4j.model.ValidationException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import net.fortuna.ical4j.model.ValidationException;
|
||||
import android.content.SyncResult;
|
||||
import android.util.Log;
|
||||
import at.bitfire.davdroid.ArrayUtils;
|
||||
import at.bitfire.davdroid.resource.LocalCollection;
|
||||
import at.bitfire.davdroid.resource.LocalStorageException;
|
||||
@@ -40,7 +43,7 @@ public class SyncManager {
|
||||
}
|
||||
|
||||
|
||||
public void synchronize(boolean manualSync, SyncResult syncResult) throws LocalStorageException, IOException, HttpException, DavException {
|
||||
public void synchronize(boolean manualSync, SyncResult syncResult) throws URISyntaxException, LocalStorageException, IOException, HttpException, DavException {
|
||||
// PHASE 1: push local changes to server
|
||||
int deletedRemotely = pushDeleted(),
|
||||
addedRemotely = pushNew(),
|
||||
@@ -98,7 +101,7 @@ public class SyncManager {
|
||||
}
|
||||
|
||||
|
||||
private int pushDeleted() throws LocalStorageException, IOException, HttpException {
|
||||
private int pushDeleted() throws URISyntaxException, LocalStorageException, IOException, HttpException {
|
||||
int count = 0;
|
||||
long[] deletedIDs = local.findDeleted();
|
||||
|
||||
@@ -129,7 +132,7 @@ public class SyncManager {
|
||||
return count;
|
||||
}
|
||||
|
||||
private int pushNew() throws LocalStorageException, IOException, HttpException {
|
||||
private int pushNew() throws URISyntaxException, LocalStorageException, IOException, HttpException {
|
||||
int count = 0;
|
||||
long[] newIDs = local.findNew();
|
||||
Log.i(TAG, "Uploading " + newIDs.length + " new resource(s) (if not existing)");
|
||||
@@ -155,7 +158,7 @@ public class SyncManager {
|
||||
return count;
|
||||
}
|
||||
|
||||
private int pushDirty() throws LocalStorageException, IOException, HttpException {
|
||||
private int pushDirty() throws URISyntaxException, LocalStorageException, IOException, HttpException {
|
||||
int count = 0;
|
||||
long[] dirtyIDs = local.findUpdated();
|
||||
Log.i(TAG, "Uploading " + dirtyIDs.length + " modified resource(s) (if not changed)");
|
||||
@@ -182,7 +185,7 @@ public class SyncManager {
|
||||
return count;
|
||||
}
|
||||
|
||||
private int pullNew(Resource[] resourcesToAdd) throws LocalStorageException, IOException, HttpException, DavException {
|
||||
private int pullNew(Resource[] resourcesToAdd) throws URISyntaxException, LocalStorageException, IOException, HttpException, DavException {
|
||||
int count = 0;
|
||||
Log.i(TAG, "Fetching " + resourcesToAdd.length + " new remote resource(s)");
|
||||
|
||||
@@ -196,7 +199,7 @@ public class SyncManager {
|
||||
return count;
|
||||
}
|
||||
|
||||
private int pullChanged(Resource[] resourcesToUpdate) throws LocalStorageException, IOException, HttpException, DavException {
|
||||
private int pullChanged(Resource[] resourcesToUpdate) throws URISyntaxException, LocalStorageException, IOException, HttpException, DavException {
|
||||
int count = 0;
|
||||
Log.i(TAG, "Fetching " + resourcesToUpdate.length + " updated remote resource(s)");
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2014 Ricki Hirner (bitfire web engineering).
|
||||
/*
|
||||
* 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
|
||||
******************************************************************************/
|
||||
package at.bitfire.davdroid;
|
||||
*/
|
||||
package at.bitfire.davdroid.ui;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
@@ -19,7 +19,11 @@ import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
import at.bitfire.davdroid.syncadapter.GeneralSettingsActivity;
|
||||
|
||||
import at.bitfire.davdroid.Constants;
|
||||
import at.bitfire.davdroid.R;
|
||||
import at.bitfire.davdroid.ui.setup.AddAccountActivity;
|
||||
import at.bitfire.davdroid.ui.settings.SettingsActivity;
|
||||
|
||||
public class MainActivity extends Activity {
|
||||
|
||||
@@ -27,7 +31,7 @@ public class MainActivity extends Activity {
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
setContentView(R.layout.activity_main);
|
||||
setContentView(R.layout.main_activity);
|
||||
|
||||
TextView tvWorkaround = (TextView)findViewById(R.id.text_workaround);
|
||||
if (fromPlayStore()) {
|
||||
@@ -50,15 +54,13 @@ public class MainActivity extends Activity {
|
||||
|
||||
|
||||
public void addAccount(MenuItem item) {
|
||||
Intent intent = new Intent(Settings.ACTION_ADD_ACCOUNT);
|
||||
startActivity(intent);
|
||||
}
|
||||
|
||||
public void showDebugSettings(MenuItem item) {
|
||||
Intent intent = new Intent(this, GeneralSettingsActivity.class);
|
||||
startActivity(intent);
|
||||
startActivity(new Intent(this, AddAccountActivity.class));
|
||||
}
|
||||
|
||||
public void showSettings(MenuItem item) {
|
||||
startActivity(new Intent(this, SettingsActivity.class));
|
||||
}
|
||||
|
||||
public void showSyncSettings(MenuItem item) {
|
||||
Intent intent = new Intent(Settings.ACTION_SYNC_SETTINGS);
|
||||
startActivity(intent);
|
||||
@@ -75,7 +77,7 @@ public class MainActivity extends Activity {
|
||||
try {
|
||||
return "com.android.vending".equals(getPackageManager().getInstallerPackageName("at.bitfire.davdroid"));
|
||||
} catch(IllegalArgumentException e) {
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* 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
|
||||
*/
|
||||
|
||||
package at.bitfire.davdroid.ui.settings;
|
||||
|
||||
import android.accounts.Account;
|
||||
import android.app.Activity;
|
||||
import android.app.FragmentManager;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
|
||||
import at.bitfire.davdroid.R;
|
||||
|
||||
public class AccountActivity extends Activity {
|
||||
public static final String EXTRA_ACCOUNT = "account";
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
setContentView(R.layout.settings_account_activity);
|
||||
|
||||
final FragmentManager fm = getFragmentManager();
|
||||
|
||||
AccountFragment fragment = (AccountFragment)fm.findFragmentById(R.id.account_fragment);
|
||||
if (fragment == null) {
|
||||
fragment = new AccountFragment();
|
||||
final Bundle args = new Bundle(1);
|
||||
Account account = getIntent().getExtras().getParcelable(EXTRA_ACCOUNT);
|
||||
args.putParcelable(AccountFragment.ARG_ACCOUNT, account);
|
||||
fragment.setArguments(args);
|
||||
|
||||
getFragmentManager().beginTransaction()
|
||||
.add(R.id.account_fragment, fragment)
|
||||
.commit();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
/*
|
||||
* 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
|
||||
*/
|
||||
|
||||
package at.bitfire.davdroid.ui.settings;
|
||||
|
||||
import android.accounts.Account;
|
||||
import android.os.Bundle;
|
||||
import android.preference.CheckBoxPreference;
|
||||
import android.preference.EditTextPreference;
|
||||
import android.preference.ListPreference;
|
||||
import android.preference.Preference;
|
||||
import android.preference.PreferenceFragment;
|
||||
|
||||
import at.bitfire.davdroid.R;
|
||||
import at.bitfire.davdroid.syncadapter.AccountSettings;
|
||||
import lombok.Setter;
|
||||
|
||||
public class AccountFragment extends PreferenceFragment {
|
||||
final static String ARG_ACCOUNT = "account";
|
||||
|
||||
Account account;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
addPreferencesFromResource(R.xml.settings_account_prefs);
|
||||
|
||||
account = getArguments().getParcelable(ARG_ACCOUNT);
|
||||
readFromAccount();
|
||||
}
|
||||
|
||||
public void readFromAccount() {
|
||||
final AccountSettings settings = new AccountSettings(getActivity(), account);
|
||||
|
||||
final EditTextPreference prefUserName = (EditTextPreference)findPreference("username");
|
||||
prefUserName.setSummary(settings.getUserName());
|
||||
prefUserName.setText(settings.getUserName());
|
||||
prefUserName.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
|
||||
@Override
|
||||
public boolean onPreferenceChange(Preference preference, Object newValue) {
|
||||
settings.setUserName((String)newValue);
|
||||
readFromAccount();
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
final EditTextPreference prefPassword = (EditTextPreference)findPreference("password");
|
||||
prefPassword.setText(settings.getPassword());
|
||||
prefPassword.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
|
||||
@Override
|
||||
public boolean onPreferenceChange(Preference preference, Object newValue) {
|
||||
settings.setPassword((String)newValue);
|
||||
readFromAccount();
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
final CheckBoxPreference prefPreemptive = (CheckBoxPreference)findPreference("preemptive");
|
||||
prefPreemptive.setChecked(settings.getPreemptiveAuth());
|
||||
prefPreemptive.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
|
||||
@Override
|
||||
public boolean onPreferenceChange(Preference preference, Object newValue) {
|
||||
settings.setPreemptiveAuth((Boolean)newValue);
|
||||
readFromAccount();
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
final ListPreference prefSyncContacts = (ListPreference)findPreference("sync_interval_contacts");
|
||||
final Long syncIntervalContacts = settings.getContactsSyncInterval();
|
||||
if (syncIntervalContacts != null) {
|
||||
prefSyncContacts.setValue(syncIntervalContacts.toString());
|
||||
if (syncIntervalContacts == AccountSettings.SYNC_INTERVAL_MANUALLY)
|
||||
prefSyncContacts.setSummary(R.string.settings_sync_summary_manually);
|
||||
else
|
||||
prefSyncContacts.setSummary(getString(R.string.settings_sync_summary_periodically, syncIntervalContacts / 60));
|
||||
prefSyncContacts.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
|
||||
@Override
|
||||
public boolean onPreferenceChange(Preference preference, Object newValue) {
|
||||
settings.setContactsSyncInterval(Long.parseLong((String)newValue));
|
||||
readFromAccount();
|
||||
return true;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
prefSyncContacts.setEnabled(false);
|
||||
prefSyncContacts.setSummary(R.string.settings_sync_summary_not_available);
|
||||
}
|
||||
|
||||
final ListPreference prefSyncCalendars = (ListPreference)findPreference("sync_interval_calendars");
|
||||
final Long syncIntervalCalendars = settings.getCalendarsSyncInterval();
|
||||
if (syncIntervalCalendars != null) {
|
||||
prefSyncCalendars.setValue(syncIntervalCalendars.toString());
|
||||
if (syncIntervalCalendars == AccountSettings.SYNC_INTERVAL_MANUALLY)
|
||||
prefSyncCalendars.setSummary(R.string.settings_sync_summary_manually);
|
||||
else
|
||||
prefSyncCalendars.setSummary(getString(R.string.settings_sync_summary_periodically, syncIntervalCalendars / 60));
|
||||
prefSyncCalendars.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
|
||||
@Override
|
||||
public boolean onPreferenceChange(Preference preference, Object newValue) {
|
||||
settings.setCalendarsSyncInterval(Long.parseLong((String)newValue));
|
||||
readFromAccount();
|
||||
return true;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
prefSyncCalendars.setEnabled(false);
|
||||
prefSyncCalendars.setSummary(R.string.settings_sync_summary_not_available);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* 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
|
||||
*/
|
||||
|
||||
package at.bitfire.davdroid.ui.settings;
|
||||
|
||||
import android.accounts.Account;
|
||||
import android.app.Activity;
|
||||
import android.app.FragmentTransaction;
|
||||
import android.app.ListFragment;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.widget.ListView;
|
||||
|
||||
import at.bitfire.davdroid.R;
|
||||
|
||||
public class SettingsActivity extends Activity {
|
||||
private final static String KEY_SELECTED_ACCOUNT = "selected_account";
|
||||
|
||||
boolean tabletLayout;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
setContentView(R.layout.settings_activity);
|
||||
|
||||
tabletLayout = findViewById(R.id.right_pane) != null;
|
||||
if (tabletLayout) {
|
||||
SettingsScopeFragment scope = (SettingsScopeFragment)getFragmentManager().findFragmentById(R.id.settings_scope);
|
||||
scope.setLayout(true);
|
||||
}
|
||||
}
|
||||
|
||||
void showAccountSettings(Account account) {
|
||||
if (tabletLayout) {
|
||||
AccountFragment fragment = new AccountFragment();
|
||||
|
||||
Bundle args = new Bundle(1);
|
||||
args.putParcelable(AccountFragment.ARG_ACCOUNT, account);
|
||||
fragment.setArguments(args);
|
||||
|
||||
getFragmentManager().beginTransaction()
|
||||
.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN)
|
||||
.replace(R.id.right_pane, fragment)
|
||||
.commit();
|
||||
} else { // phone layout
|
||||
Intent intent = new Intent(getApplicationContext(), AccountActivity.class);
|
||||
intent.putExtra(AccountActivity.EXTRA_ACCOUNT, account);
|
||||
startActivity(intent);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* 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
|
||||
*/
|
||||
|
||||
package at.bitfire.davdroid.ui.settings;
|
||||
|
||||
import android.accounts.Account;
|
||||
import android.accounts.AccountManager;
|
||||
import android.app.ListFragment;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.ListView;
|
||||
|
||||
import at.bitfire.davdroid.Constants;
|
||||
import at.bitfire.davdroid.R;
|
||||
|
||||
public class SettingsScopeFragment extends ListFragment {
|
||||
Account[] accounts;
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
final AccountManager manager = AccountManager.get(getActivity());
|
||||
accounts = manager.getAccountsByType(Constants.ACCOUNT_TYPE);
|
||||
|
||||
final String[] accountNames = new String[accounts.length];
|
||||
for (int i = 0; i < accounts.length; i++)
|
||||
accountNames[i] = accounts[i].name;
|
||||
setListAdapter(new ArrayAdapter<String>(getActivity(), android.R.layout.simple_list_item_activated_1, accountNames));
|
||||
|
||||
return super.onCreateView(inflater, container, savedInstanceState);
|
||||
}
|
||||
|
||||
public void setLayout(boolean tabletLayout) {
|
||||
if (tabletLayout)
|
||||
getListView().setChoiceMode(ListView.CHOICE_MODE_SINGLE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
setEmptyText(getString(R.string.settings_no_accounts));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onListItemClick(ListView l, View v, int position, long id) {
|
||||
l.clearChoices();
|
||||
((SettingsActivity)getActivity()).showAccountSettings(accounts[position]);
|
||||
l.setItemChecked(position, true);
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2014 Ricki Hirner (bitfire web engineering).
|
||||
/*
|
||||
* 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
|
||||
******************************************************************************/
|
||||
package at.bitfire.davdroid.syncadapter;
|
||||
*/
|
||||
package at.bitfire.davdroid.ui.setup;
|
||||
|
||||
import android.accounts.Account;
|
||||
import android.accounts.AccountManager;
|
||||
@@ -25,10 +25,13 @@ import android.view.ViewGroup;
|
||||
import android.widget.EditText;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import at.bitfire.davdroid.Constants;
|
||||
import at.bitfire.davdroid.R;
|
||||
import at.bitfire.davdroid.resource.LocalCalendar;
|
||||
import at.bitfire.davdroid.resource.LocalStorageException;
|
||||
import at.bitfire.davdroid.resource.ServerInfo;
|
||||
import at.bitfire.davdroid.syncadapter.AccountSettings;
|
||||
|
||||
public class AccountDetailsFragment extends Fragment implements TextWatcher {
|
||||
public static final String KEY_SERVER_INFO = "server_info";
|
||||
@@ -40,12 +43,13 @@ public class AccountDetailsFragment extends Fragment implements TextWatcher {
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
View v = inflater.inflate(R.layout.account_details, container, false);
|
||||
View v = inflater.inflate(R.layout.setup_account_details, container, false);
|
||||
|
||||
serverInfo = (ServerInfo)getArguments().getSerializable(KEY_SERVER_INFO);
|
||||
|
||||
editAccountName = (EditText)v.findViewById(R.id.account_name);
|
||||
editAccountName.addTextChangedListener(this);
|
||||
editAccountName.setText(serverInfo.getUserName());
|
||||
|
||||
TextView textAccountNameInfo = (TextView)v.findViewById(R.id.account_name_info);
|
||||
if (!serverInfo.hasEnabledCalendars())
|
||||
@@ -58,7 +62,7 @@ public class AccountDetailsFragment extends Fragment implements TextWatcher {
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
||||
inflater.inflate(R.menu.account_details, menu);
|
||||
inflater.inflate(R.menu.setup_account_details, menu);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -1,11 +1,11 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2014 Ricki Hirner (bitfire web engineering).
|
||||
/*
|
||||
* 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
|
||||
******************************************************************************/
|
||||
package at.bitfire.davdroid.syncadapter;
|
||||
*/
|
||||
package at.bitfire.davdroid.ui.setup;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
@@ -14,6 +14,7 @@ import android.os.Bundle;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
|
||||
import at.bitfire.davdroid.Constants;
|
||||
import at.bitfire.davdroid.R;
|
||||
|
||||
@@ -23,11 +24,11 @@ public class AddAccountActivity extends Activity {
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
setContentView(R.layout.add_account);
|
||||
setContentView(R.layout.setup_add_account);
|
||||
|
||||
if (savedInstanceState == null) { // first call
|
||||
getFragmentManager().beginTransaction()
|
||||
.add(R.id.fragment_container, new EnterCredentialsFragment(), "enter_credentials")
|
||||
.add(R.id.right_pane, new LoginTypeFragment())
|
||||
.commit();
|
||||
}
|
||||
}
|
||||
@@ -35,7 +36,7 @@ public class AddAccountActivity extends Activity {
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
MenuInflater inflater = getMenuInflater();
|
||||
inflater.inflate(R.menu.add_account, menu);
|
||||
inflater.inflate(R.menu.setup_add_account, menu);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
/*
|
||||
* 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
|
||||
*/
|
||||
package at.bitfire.davdroid.ui.setup;
|
||||
|
||||
import android.app.DialogFragment;
|
||||
import android.app.Fragment;
|
||||
import android.app.FragmentTransaction;
|
||||
import android.os.Bundle;
|
||||
import android.text.Editable;
|
||||
import android.text.TextWatcher;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.EditText;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
|
||||
import at.bitfire.davdroid.R;
|
||||
|
||||
public class LoginEmailFragment extends Fragment implements TextWatcher {
|
||||
|
||||
protected EditText editEmail, editPassword;
|
||||
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
View v = inflater.inflate(R.layout.setup_login_email, container, false);
|
||||
|
||||
editEmail = (EditText)v.findViewById(R.id.email_address);
|
||||
editEmail.addTextChangedListener(this);
|
||||
editPassword = (EditText)v.findViewById(R.id.password);
|
||||
editPassword.addTextChangedListener(this);
|
||||
|
||||
setHasOptionsMenu(true);
|
||||
return v;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
||||
inflater.inflate(R.menu.only_next, menu);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case R.id.next:
|
||||
FragmentTransaction ft = getFragmentManager().beginTransaction();
|
||||
|
||||
Bundle args = new Bundle();
|
||||
String email = editEmail.getText().toString();
|
||||
args.putString(QueryServerDialogFragment.EXTRA_BASE_URI, "mailto:" + email);
|
||||
args.putString(QueryServerDialogFragment.EXTRA_USER_NAME, email);
|
||||
args.putString(QueryServerDialogFragment.EXTRA_PASSWORD, editPassword.getText().toString());
|
||||
args.putBoolean(QueryServerDialogFragment.EXTRA_AUTH_PREEMPTIVE, true);
|
||||
|
||||
DialogFragment dialog = new QueryServerDialogFragment();
|
||||
dialog.setArguments(args);
|
||||
dialog.show(ft, QueryServerDialogFragment.class.getName());
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
// input validation
|
||||
|
||||
@Override
|
||||
public void onPrepareOptionsMenu(Menu menu) {
|
||||
boolean emailOk = false,
|
||||
passwordOk = editPassword.getText().length() > 0;
|
||||
|
||||
String email = editEmail.getText().toString();
|
||||
try {
|
||||
URI uri = new URI("mailto:" + email);
|
||||
if (uri.isOpaque()) {
|
||||
int pos = email.lastIndexOf("@");
|
||||
if (pos != -1)
|
||||
emailOk = !email.substring(pos+1).isEmpty();
|
||||
}
|
||||
} catch (URISyntaxException e) {
|
||||
// invalid mailto: URI
|
||||
}
|
||||
|
||||
MenuItem item = menu.findItem(R.id.next);
|
||||
item.setEnabled(emailOk && passwordOk);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||
getActivity().invalidateOptionsMenu();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable s) {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* 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
|
||||
*/
|
||||
package at.bitfire.davdroid.ui.setup;
|
||||
|
||||
import android.app.Fragment;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.RadioButton;
|
||||
|
||||
import at.bitfire.davdroid.R;
|
||||
|
||||
public class LoginTypeFragment extends Fragment {
|
||||
|
||||
protected RadioButton btnTypeEmail, btnTypeURL;
|
||||
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
View v = inflater.inflate(R.layout.setup_login_type, container, false);
|
||||
|
||||
btnTypeEmail = (RadioButton)v.findViewById(R.id.login_type_email);
|
||||
btnTypeURL = (RadioButton)v.findViewById(R.id.login_type_url);
|
||||
|
||||
setHasOptionsMenu(true);
|
||||
|
||||
return v;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
||||
inflater.inflate(R.menu.only_next, menu);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case R.id.next:
|
||||
Fragment loginFragment = btnTypeEmail.isChecked() ? new LoginEmailFragment() : new LoginURLFragment();
|
||||
getFragmentManager().beginTransaction()
|
||||
.replace(R.id.right_pane, loginFragment)
|
||||
.addToBackStack(null)
|
||||
.commitAllowingStateLoss();
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
/*
|
||||
* 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
|
||||
*/
|
||||
package at.bitfire.davdroid.ui.setup;
|
||||
|
||||
import android.app.DialogFragment;
|
||||
import android.app.Fragment;
|
||||
import android.app.FragmentTransaction;
|
||||
import android.os.Bundle;
|
||||
import android.text.Editable;
|
||||
import android.text.TextWatcher;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.AdapterView.OnItemSelectedListener;
|
||||
import android.widget.Button;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.EditText;
|
||||
import android.widget.Spinner;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
|
||||
import at.bitfire.davdroid.R;
|
||||
|
||||
public class LoginURLFragment extends Fragment implements TextWatcher {
|
||||
|
||||
protected Spinner spnrScheme;
|
||||
protected TextView textHttpWarning;
|
||||
protected EditText editBaseURI, editUserName, editPassword;
|
||||
protected CheckBox checkboxPreemptive;
|
||||
protected Button btnNext;
|
||||
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
View v = inflater.inflate(R.layout.setup_login_url, container, false);
|
||||
|
||||
// protocol selection spinner
|
||||
textHttpWarning = (TextView)v.findViewById(R.id.http_warning);
|
||||
|
||||
spnrScheme = (Spinner)v.findViewById(R.id.login_scheme);
|
||||
spnrScheme.setOnItemSelectedListener(new OnItemSelectedListener() {
|
||||
@Override
|
||||
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
|
||||
String scheme = parent.getAdapter().getItem(position).toString();
|
||||
textHttpWarning.setVisibility(scheme.equals("https://") ? View.GONE : View.VISIBLE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNothingSelected(AdapterView<?> parent) {
|
||||
}
|
||||
});
|
||||
spnrScheme.setSelection(1); // HTTPS
|
||||
|
||||
// other input fields
|
||||
editBaseURI = (EditText)v.findViewById(R.id.login_host_path);
|
||||
editBaseURI.addTextChangedListener(this);
|
||||
|
||||
editUserName = (EditText)v.findViewById(R.id.userName);
|
||||
editUserName.addTextChangedListener(this);
|
||||
|
||||
editPassword = (EditText)v.findViewById(R.id.password);
|
||||
editPassword.addTextChangedListener(this);
|
||||
|
||||
checkboxPreemptive = (CheckBox) v.findViewById(R.id.auth_preemptive);
|
||||
|
||||
// hook into action bar
|
||||
setHasOptionsMenu(true);
|
||||
|
||||
return v;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
||||
inflater.inflate(R.menu.only_next, menu);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case R.id.next:
|
||||
FragmentTransaction ft = getFragmentManager().beginTransaction();
|
||||
|
||||
Bundle args = new Bundle();
|
||||
try {
|
||||
args.putString(QueryServerDialogFragment.EXTRA_BASE_URI, getBaseURI().toString());
|
||||
} catch (URISyntaxException e) {
|
||||
}
|
||||
args.putString(QueryServerDialogFragment.EXTRA_USER_NAME, editUserName.getText().toString());
|
||||
args.putString(QueryServerDialogFragment.EXTRA_PASSWORD, editPassword.getText().toString());
|
||||
args.putBoolean(QueryServerDialogFragment.EXTRA_AUTH_PREEMPTIVE, checkboxPreemptive.isChecked());
|
||||
|
||||
DialogFragment dialog = new QueryServerDialogFragment();
|
||||
dialog.setArguments(args);
|
||||
dialog.show(ft, QueryServerDialogFragment.class.getName());
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
private URI getBaseURI() throws URISyntaxException {
|
||||
String scheme = spnrScheme.getSelectedItem().toString(),
|
||||
host_path = editBaseURI.getText().toString();
|
||||
return new URI(scheme + host_path);
|
||||
}
|
||||
|
||||
|
||||
// input validation
|
||||
|
||||
@Override
|
||||
public void onPrepareOptionsMenu(Menu menu) {
|
||||
boolean usernameOk = editUserName.getText().length() > 0,
|
||||
passwordOk = editPassword.getText().length() > 0,
|
||||
urlOk = false;
|
||||
|
||||
// check host name
|
||||
try {
|
||||
if (!StringUtils.isBlank(getBaseURI().getHost()))
|
||||
urlOk = true;
|
||||
} catch (Exception e) {
|
||||
}
|
||||
|
||||
MenuItem item = menu.findItem(R.id.next);
|
||||
item.setEnabled(usernameOk && passwordOk && urlOk);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||
getActivity().invalidateOptionsMenu();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable s) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
/*
|
||||
* 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
|
||||
*/
|
||||
package at.bitfire.davdroid.ui.setup;
|
||||
|
||||
import android.app.DialogFragment;
|
||||
import android.app.LoaderManager.LoaderCallbacks;
|
||||
import android.content.AsyncTaskLoader;
|
||||
import android.content.Context;
|
||||
import android.content.Loader;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.apache.commons.lang.exception.ExceptionUtils;
|
||||
import org.apache.http.HttpException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.security.cert.CertPathValidatorException;
|
||||
|
||||
import at.bitfire.davdroid.R;
|
||||
import at.bitfire.davdroid.resource.DavResourceFinder;
|
||||
import at.bitfire.davdroid.resource.ServerInfo;
|
||||
import at.bitfire.davdroid.webdav.DavException;
|
||||
import lombok.Cleanup;
|
||||
|
||||
public class QueryServerDialogFragment extends DialogFragment implements LoaderCallbacks<ServerInfo> {
|
||||
private static final String TAG = "davdroid.QueryServerDialogFragment";
|
||||
public static final String
|
||||
EXTRA_BASE_URI = "base_uri",
|
||||
EXTRA_USER_NAME = "user_name",
|
||||
EXTRA_PASSWORD = "password",
|
||||
EXTRA_AUTH_PREEMPTIVE = "auth_preemptive";
|
||||
|
||||
ProgressBar progressBar;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setStyle(DialogFragment.STYLE_NO_TITLE, android.R.style.Theme_Holo_Light_Dialog);
|
||||
setCancelable(false);
|
||||
|
||||
Loader<ServerInfo> loader = getLoaderManager().initLoader(0, getArguments(), this);
|
||||
if (savedInstanceState == null) // http://code.google.com/p/android/issues/detail?id=14944
|
||||
loader.forceLoad();
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
return inflater.inflate(R.layout.setup_query_server, container, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Loader<ServerInfo> onCreateLoader(int id, Bundle args) {
|
||||
Log.i(TAG, "onCreateLoader");
|
||||
return new ServerInfoLoader(getActivity(), args);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadFinished(Loader<ServerInfo> loader, ServerInfo serverInfo) {
|
||||
if (serverInfo.getErrorMessage() != null)
|
||||
Toast.makeText(getActivity(), serverInfo.getErrorMessage(), Toast.LENGTH_LONG).show();
|
||||
else {
|
||||
SelectCollectionsFragment selectCollections = new SelectCollectionsFragment();
|
||||
Bundle arguments = new Bundle();
|
||||
arguments.putSerializable(SelectCollectionsFragment.KEY_SERVER_INFO, serverInfo);
|
||||
selectCollections.setArguments(arguments);
|
||||
|
||||
getFragmentManager().beginTransaction()
|
||||
.replace(R.id.right_pane, selectCollections)
|
||||
.addToBackStack(null)
|
||||
.commitAllowingStateLoss();
|
||||
}
|
||||
|
||||
getDialog().dismiss();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoaderReset(Loader<ServerInfo> arg0) {
|
||||
}
|
||||
|
||||
|
||||
static class ServerInfoLoader extends AsyncTaskLoader<ServerInfo> {
|
||||
private static final String TAG = "davdroid.ServerInfoLoader";
|
||||
final Bundle args;
|
||||
final Context context;
|
||||
|
||||
public ServerInfoLoader(Context context, Bundle args) {
|
||||
super(context);
|
||||
this.context = context;
|
||||
this.args = args;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ServerInfo loadInBackground() {
|
||||
ServerInfo serverInfo = new ServerInfo(
|
||||
URI.create(args.getString(EXTRA_BASE_URI)),
|
||||
args.getString(EXTRA_USER_NAME),
|
||||
args.getString(EXTRA_PASSWORD),
|
||||
args.getBoolean(EXTRA_AUTH_PREEMPTIVE)
|
||||
);
|
||||
|
||||
try {
|
||||
@Cleanup DavResourceFinder finder = new DavResourceFinder(context);
|
||||
finder.findResources(serverInfo);
|
||||
} catch (URISyntaxException e) {
|
||||
serverInfo.setErrorMessage(getContext().getString(R.string.exception_uri_syntax, e.getMessage()));
|
||||
} catch (IOException e) {
|
||||
// general message
|
||||
serverInfo.setErrorMessage(getContext().getString(R.string.exception_io, e.getLocalizedMessage()));
|
||||
// overwrite by more specific message, if possible
|
||||
if (ExceptionUtils.indexOfType(e, CertPathValidatorException.class) != -1)
|
||||
serverInfo.setErrorMessage(getContext().getString(R.string.exception_cert_path_validation, e.getMessage()));
|
||||
} catch (HttpException e) {
|
||||
Log.e(TAG, "HTTP error while querying server info", e);
|
||||
serverInfo.setErrorMessage(getContext().getString(R.string.exception_http, e.getLocalizedMessage()));
|
||||
} catch (DavException e) {
|
||||
Log.e(TAG, "DAV error while querying server info", e);
|
||||
serverInfo.setErrorMessage(getContext().getString(R.string.exception_incapable_resource, e.getLocalizedMessage()));
|
||||
}
|
||||
|
||||
return serverInfo;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,13 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2014 Ricki Hirner (bitfire web engineering).
|
||||
/*
|
||||
* 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
|
||||
******************************************************************************/
|
||||
package at.bitfire.davdroid.syncadapter;
|
||||
*/
|
||||
package at.bitfire.davdroid.ui.setup;
|
||||
|
||||
import lombok.Getter;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.text.Html;
|
||||
import android.view.LayoutInflater;
|
||||
@@ -16,7 +16,10 @@ import android.view.ViewGroup;
|
||||
import android.widget.BaseAdapter;
|
||||
import android.widget.CheckedTextView;
|
||||
import android.widget.ListAdapter;
|
||||
|
||||
import at.bitfire.davdroid.R;
|
||||
import at.bitfire.davdroid.resource.ServerInfo;
|
||||
import lombok.Getter;
|
||||
|
||||
public class SelectCollectionsAdapter extends BaseAdapter implements ListAdapter {
|
||||
final static int TYPE_ADDRESS_BOOKS_HEADING = 0,
|
||||
@@ -26,7 +29,7 @@ public class SelectCollectionsAdapter extends BaseAdapter implements ListAdapter
|
||||
|
||||
protected Context context;
|
||||
protected ServerInfo serverInfo;
|
||||
@Getter protected int nAddressBooks, nCalendars;
|
||||
@Getter protected int nAddressBooks, nAddressbookHeadings, nCalendars, nCalendarHeadings;
|
||||
|
||||
|
||||
public SelectCollectionsAdapter(Context context, ServerInfo serverInfo) {
|
||||
@@ -34,7 +37,9 @@ public class SelectCollectionsAdapter extends BaseAdapter implements ListAdapter
|
||||
|
||||
this.serverInfo = serverInfo;
|
||||
nAddressBooks = (serverInfo.getAddressBooks() == null) ? 0 : serverInfo.getAddressBooks().size();
|
||||
nAddressbookHeadings = (nAddressBooks == 0) ? 0 : 1;
|
||||
nCalendars = (serverInfo.getCalendars() == null) ? 0 : serverInfo.getCalendars().size();
|
||||
nCalendarHeadings = (nCalendars == 0) ? 0 : 1;
|
||||
}
|
||||
|
||||
|
||||
@@ -42,15 +47,17 @@ public class SelectCollectionsAdapter extends BaseAdapter implements ListAdapter
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
return nAddressBooks + nCalendars + 2;
|
||||
return nAddressbookHeadings + nAddressBooks + nCalendarHeadings + nCalendars;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getItem(int position) {
|
||||
if (position > 0 && position <= nAddressBooks)
|
||||
return serverInfo.getAddressBooks().get(position - 1);
|
||||
else if (position > nAddressBooks + 1)
|
||||
return serverInfo.getCalendars().get(position - nAddressBooks - 2);
|
||||
if (position >= nAddressbookHeadings &&
|
||||
position < (nAddressbookHeadings + nAddressBooks))
|
||||
return serverInfo.getAddressBooks().get(position - nAddressbookHeadings);
|
||||
else if (position >= (nAddressbookHeadings + nAddressBooks + nCalendarHeadings) &&
|
||||
(position < (nAddressbookHeadings + nAddressBooks + nCalendarHeadings + nCalendars)))
|
||||
return serverInfo.getCalendars().get(position - (nAddressbookHeadings + nAddressBooks + nCalendarHeadings));
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -74,60 +81,64 @@ public class SelectCollectionsAdapter extends BaseAdapter implements ListAdapter
|
||||
|
||||
@Override
|
||||
public int getItemViewType(int position) {
|
||||
if (position == 0)
|
||||
if ((nAddressbookHeadings != 0) && (position == 0))
|
||||
return TYPE_ADDRESS_BOOKS_HEADING;
|
||||
else if (position <= nAddressBooks)
|
||||
else if ((nAddressbookHeadings != 0) && (position > 0) && (position < nAddressbookHeadings + nAddressBooks))
|
||||
return TYPE_ADDRESS_BOOKS_ROW;
|
||||
else if (position == nAddressBooks + 1)
|
||||
else if ((nCalendars != 0) && (position == nAddressbookHeadings + nAddressBooks))
|
||||
return TYPE_CALENDARS_HEADING;
|
||||
else if (position <= nAddressBooks + nCalendars + 1)
|
||||
else if ((nCalendars != 0) && (position > nAddressbookHeadings + nAddressBooks) && (position < nAddressbookHeadings + nAddressBooks + nCalendarHeadings + nCalendars))
|
||||
return TYPE_CALENDARS_ROW;
|
||||
else
|
||||
return IGNORE_ITEM_VIEW_TYPE;
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressLint("InflateParams")
|
||||
public View getView(int position, View convertView, ViewGroup parent) {
|
||||
View v = convertView;
|
||||
|
||||
// step 1: get view (either by creating or recycling)
|
||||
if (convertView == null) {
|
||||
if (v == null) {
|
||||
LayoutInflater inflater = LayoutInflater.from(parent.getContext());
|
||||
switch (getItemViewType(position)) {
|
||||
case TYPE_ADDRESS_BOOKS_HEADING:
|
||||
convertView = inflater.inflate(R.layout.address_books_heading, parent, false);
|
||||
v = inflater.inflate(R.layout.setup_address_books_heading, parent, false);
|
||||
break;
|
||||
case TYPE_ADDRESS_BOOKS_ROW:
|
||||
convertView = inflater.inflate(android.R.layout.simple_list_item_single_choice, null);
|
||||
v = inflater.inflate(android.R.layout.simple_list_item_single_choice, null);
|
||||
v.setPadding(0, 8, 0, 8);
|
||||
break;
|
||||
case TYPE_CALENDARS_HEADING:
|
||||
convertView = inflater.inflate(R.layout.calendars_heading, parent, false);
|
||||
v = inflater.inflate(R.layout.setup_calendars_heading, parent, false);
|
||||
break;
|
||||
case TYPE_CALENDARS_ROW:
|
||||
convertView = inflater.inflate(android.R.layout.simple_list_item_multiple_choice, null);
|
||||
v = inflater.inflate(android.R.layout.simple_list_item_multiple_choice, null);
|
||||
v.setPadding(0, 8, 0, 8);
|
||||
}
|
||||
}
|
||||
|
||||
// step 2: fill view with content
|
||||
switch (getItemViewType(position)) {
|
||||
case TYPE_ADDRESS_BOOKS_ROW:
|
||||
setContent((CheckedTextView)convertView, R.drawable.addressbook, (ServerInfo.ResourceInfo)getItem(position));
|
||||
setContent((CheckedTextView)v, R.drawable.addressbook, (ServerInfo.ResourceInfo)getItem(position));
|
||||
break;
|
||||
case TYPE_CALENDARS_ROW:
|
||||
setContent((CheckedTextView)convertView, R.drawable.calendar, (ServerInfo.ResourceInfo)getItem(position));
|
||||
setContent((CheckedTextView)v, R.drawable.calendar, (ServerInfo.ResourceInfo)getItem(position));
|
||||
}
|
||||
|
||||
return convertView;
|
||||
return v;
|
||||
}
|
||||
|
||||
protected void setContent(CheckedTextView view, int collectionIcon, ServerInfo.ResourceInfo info) {
|
||||
// set layout and icons
|
||||
view.setPadding(10, 10, 10, 10);
|
||||
view.setCompoundDrawablesWithIntrinsicBounds(collectionIcon, 0, info.isReadOnly() ? R.drawable.ic_read_only : 0, 0);
|
||||
view.setCompoundDrawablePadding(10);
|
||||
|
||||
// set text
|
||||
String title = "<b>" + info.getTitle() + "</b>";
|
||||
if (info.isReadOnly())
|
||||
title = title + " (" + context.getString(R.string.read_only) + ")";
|
||||
title = title + " (" + context.getString(R.string.setup_read_only) + ")";
|
||||
|
||||
String description = info.getDescription();
|
||||
if (description == null)
|
||||
@@ -1,11 +1,11 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2014 Ricki Hirner (bitfire web engineering).
|
||||
/*
|
||||
* 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
|
||||
******************************************************************************/
|
||||
package at.bitfire.davdroid.syncadapter;
|
||||
*/
|
||||
package at.bitfire.davdroid.ui.setup;
|
||||
|
||||
import android.app.ListFragment;
|
||||
import android.os.Bundle;
|
||||
@@ -19,11 +19,14 @@ import android.widget.AdapterView;
|
||||
import android.widget.AdapterView.OnItemClickListener;
|
||||
import android.widget.ListAdapter;
|
||||
import android.widget.ListView;
|
||||
|
||||
import at.bitfire.davdroid.R;
|
||||
import at.bitfire.davdroid.resource.ServerInfo;
|
||||
|
||||
public class SelectCollectionsFragment extends ListFragment {
|
||||
public static final String KEY_SERVER_INFO = "server_info";
|
||||
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
View v = super.onCreateView(inflater, container, savedInstanceState);
|
||||
@@ -44,8 +47,8 @@ public class SelectCollectionsFragment extends ListFragment {
|
||||
final ListView listView = getListView();
|
||||
listView.setPadding(20, 30, 20, 30);
|
||||
|
||||
View header = getActivity().getLayoutInflater().inflate(R.layout.select_collections_header, null);
|
||||
listView.addHeaderView(header);
|
||||
View header = getActivity().getLayoutInflater().inflate(R.layout.setup_select_collections_header, getListView(), false);
|
||||
listView.addHeaderView(header, getListView(), false);
|
||||
|
||||
final ServerInfo serverInfo = (ServerInfo)getArguments().getSerializable(KEY_SERVER_INFO);
|
||||
final SelectCollectionsAdapter adapter = new SelectCollectionsAdapter(view.getContext(), serverInfo);
|
||||
@@ -70,7 +73,7 @@ public class SelectCollectionsFragment extends ListFragment {
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
||||
inflater.inflate(R.menu.select_collections, menu);
|
||||
inflater.inflate(R.menu.only_next, menu);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -99,7 +102,7 @@ public class SelectCollectionsFragment extends ListFragment {
|
||||
accountDetails.setArguments(arguments);
|
||||
|
||||
getFragmentManager().beginTransaction()
|
||||
.replace(R.id.fragment_container, accountDetails)
|
||||
.replace(R.id.right_pane, accountDetails)
|
||||
.addToBackStack(null)
|
||||
.commitAllowingStateLoss();
|
||||
break;
|
||||
@@ -1,13 +1,11 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2014 Ricki Hirner (bitfire web engineering).
|
||||
/*
|
||||
* 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
|
||||
******************************************************************************/
|
||||
package at.bitfire.davdroid.syncadapter;
|
||||
|
||||
import java.util.List;
|
||||
*/
|
||||
package at.bitfire.davdroid.ui.setup;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.LayoutInflater;
|
||||
@@ -15,6 +13,9 @@ import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.BaseAdapter;
|
||||
import android.widget.TextView;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import at.bitfire.davdroid.webdav.WebDavResource;
|
||||
|
||||
public class WebDavResourceAdapter extends BaseAdapter {
|
||||
@@ -1,10 +1,10 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2014 Ricki Hirner (bitfire web engineering).
|
||||
/*
|
||||
* 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
|
||||
******************************************************************************/
|
||||
*/
|
||||
package at.bitfire.davdroid.webdav;
|
||||
|
||||
import org.simpleframework.xml.Namespace;
|
||||
@@ -1,10 +1,10 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2014 Ricki Hirner (bitfire web engineering).
|
||||
/*
|
||||
* 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
|
||||
******************************************************************************/
|
||||
*/
|
||||
package at.bitfire.davdroid.webdav;
|
||||
|
||||
import org.simpleframework.xml.Namespace;
|
||||
@@ -1,10 +1,10 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2014 Ricki Hirner (bitfire web engineering).
|
||||
/*
|
||||
* 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
|
||||
******************************************************************************/
|
||||
*/
|
||||
package at.bitfire.davdroid.webdav;
|
||||
|
||||
public class DavException extends Exception {
|
||||
@@ -1,10 +1,10 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2014 Ricki Hirner (bitfire web engineering).
|
||||
/*
|
||||
* 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
|
||||
******************************************************************************/
|
||||
*/
|
||||
package at.bitfire.davdroid.webdav;
|
||||
|
||||
import org.simpleframework.xml.Namespace;
|
||||
@@ -1,24 +1,26 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2014 Ricki Hirner (bitfire web engineering).
|
||||
/*
|
||||
* 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
|
||||
******************************************************************************/
|
||||
*/
|
||||
package at.bitfire.davdroid.webdav;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import org.apache.http.client.config.RequestConfig;
|
||||
import org.apache.http.config.Registry;
|
||||
import org.apache.http.config.RegistryBuilder;
|
||||
import org.apache.http.conn.socket.ConnectionSocketFactory;
|
||||
import org.apache.http.conn.socket.PlainConnectionSocketFactory;
|
||||
import org.apache.http.impl.client.CloseableHttpClient;
|
||||
import org.apache.http.impl.client.HttpClientBuilder;
|
||||
import org.apache.http.impl.client.HttpClients;
|
||||
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
|
||||
|
||||
import at.bitfire.davdroid.Constants;
|
||||
import ch.boye.httpclientandroidlib.client.config.RequestConfig;
|
||||
import ch.boye.httpclientandroidlib.config.Registry;
|
||||
import ch.boye.httpclientandroidlib.config.RegistryBuilder;
|
||||
import ch.boye.httpclientandroidlib.conn.socket.ConnectionSocketFactory;
|
||||
import ch.boye.httpclientandroidlib.conn.socket.PlainConnectionSocketFactory;
|
||||
import ch.boye.httpclientandroidlib.impl.client.CloseableHttpClient;
|
||||
import ch.boye.httpclientandroidlib.impl.client.HttpClientBuilder;
|
||||
import ch.boye.httpclientandroidlib.impl.client.HttpClients;
|
||||
import ch.boye.httpclientandroidlib.impl.conn.ManagedHttpClientConnectionFactory;
|
||||
import ch.boye.httpclientandroidlib.impl.conn.PoolingHttpClientConnectionManager;
|
||||
|
||||
|
||||
public class DavHttpClient {
|
||||
private final static String TAG = "davdroid.DavHttpClient";
|
||||
@@ -29,7 +31,7 @@ public class DavHttpClient {
|
||||
static {
|
||||
socketFactoryRegistry = RegistryBuilder.<ConnectionSocketFactory> create()
|
||||
.register("http", PlainConnectionSocketFactory.getSocketFactory())
|
||||
.register("https", TlsSniSocketFactory.INSTANCE)
|
||||
.register("https", TlsSniSocketFactory.getSocketFactory())
|
||||
.build();
|
||||
|
||||
// use request defaults from AndroidHttpClient
|
||||
@@ -41,12 +43,12 @@ public class DavHttpClient {
|
||||
}
|
||||
|
||||
|
||||
public static CloseableHttpClient create(boolean disableCompression, boolean logTraffic) {
|
||||
public static CloseableHttpClient create() {
|
||||
PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(socketFactoryRegistry);
|
||||
// limits per DavHttpClient (= per DavSyncAdapter extends AbstractThreadedSyncAdapter)
|
||||
connectionManager.setMaxTotal(3); // max. 3 connections in total
|
||||
connectionManager.setDefaultMaxPerRoute(2); // max. 2 connections per host
|
||||
|
||||
|
||||
HttpClientBuilder builder = HttpClients.custom()
|
||||
.useSystemProperties()
|
||||
.setConnectionManager(connectionManager)
|
||||
@@ -56,17 +58,12 @@ public class DavHttpClient {
|
||||
.setUserAgent("DAVdroid/" + Constants.APP_VERSION)
|
||||
.disableCookieManagement();
|
||||
|
||||
if (disableCompression) {
|
||||
Log.d(TAG, "Disabling compression for debugging purposes");
|
||||
if (Log.isLoggable("Wire", Log.DEBUG)) {
|
||||
Log.i(TAG, "Wire logging active, disabling HTTP compression");
|
||||
builder = builder.disableContentCompression();
|
||||
}
|
||||
|
||||
if (logTraffic)
|
||||
Log.d(TAG, "Logging network traffic for debugging purposes");
|
||||
ManagedHttpClientConnectionFactory.INSTANCE.wirelog.enableDebug(logTraffic);
|
||||
ManagedHttpClientConnectionFactory.INSTANCE.log.enableDebug(logTraffic);
|
||||
|
||||
return builder.build();
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,20 +1,19 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2014 Ricki Hirner (bitfire web engineering).
|
||||
/*
|
||||
* 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
|
||||
******************************************************************************/
|
||||
*/
|
||||
package at.bitfire.davdroid.webdav;
|
||||
|
||||
import org.apache.commons.lang.ArrayUtils;
|
||||
import org.apache.http.HttpRequest;
|
||||
import org.apache.http.impl.client.DefaultHttpRequestRetryHandlerHC4;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
import org.apache.commons.lang.ArrayUtils;
|
||||
|
||||
import ch.boye.httpclientandroidlib.HttpRequest;
|
||||
import ch.boye.httpclientandroidlib.impl.client.DefaultHttpRequestRetryHandler;
|
||||
|
||||
public class DavHttpRequestRetryHandler extends DefaultHttpRequestRetryHandler {
|
||||
public class DavHttpRequestRetryHandler extends DefaultHttpRequestRetryHandlerHC4 {
|
||||
final static DavHttpRequestRetryHandler INSTANCE = new DavHttpRequestRetryHandler();
|
||||
|
||||
// see http://www.iana.org/assignments/http-methods/http-methods.xhtml
|
||||
@@ -1,10 +1,10 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2014 Ricki Hirner (bitfire web engineering).
|
||||
/*
|
||||
* 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
|
||||
******************************************************************************/
|
||||
*/
|
||||
package at.bitfire.davdroid.webdav;
|
||||
|
||||
public class DavIncapableException extends DavException {
|
||||
@@ -1,19 +1,19 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2014 Ricki Hirner (bitfire web engineering).
|
||||
/*
|
||||
* 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
|
||||
******************************************************************************/
|
||||
*/
|
||||
package at.bitfire.davdroid.webdav;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.simpleframework.xml.Element;
|
||||
import org.simpleframework.xml.ElementList;
|
||||
import org.simpleframework.xml.Order;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@Order(elements={"prop","href"})
|
||||
public class DavMultiget {
|
||||
public enum Type {
|
||||
@@ -32,12 +32,12 @@ public class DavMultiget {
|
||||
DavMultiget multiget = (type == Type.ADDRESS_BOOK) ? new DavAddressbookMultiget() : new DavCalendarMultiget();
|
||||
|
||||
multiget.prop = new DavProp();
|
||||
multiget.prop.getetag = new DavProp.DavPropGetETag();
|
||||
multiget.prop.getetag = new DavProp.GetETag();
|
||||
|
||||
if (type == Type.ADDRESS_BOOK)
|
||||
multiget.prop.addressData = new DavProp.DavPropAddressData();
|
||||
multiget.prop.addressData = new DavProp.AddressData();
|
||||
else if (type == Type.CALENDAR)
|
||||
multiget.prop.calendarData = new DavProp.DavPropCalendarData();
|
||||
multiget.prop.calendarData = new DavProp.CalendarData();
|
||||
|
||||
multiget.hrefs = new ArrayList<DavHref>(names.length);
|
||||
for (String name : names)
|
||||
@@ -1,18 +1,18 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2014 Ricki Hirner (bitfire web engineering).
|
||||
/*
|
||||
* 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
|
||||
******************************************************************************/
|
||||
*/
|
||||
package at.bitfire.davdroid.webdav;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.simpleframework.xml.ElementList;
|
||||
import org.simpleframework.xml.Namespace;
|
||||
import org.simpleframework.xml.Root;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Namespace(reference="DAV:")
|
||||
@Root(strict=false)
|
||||
public class DavMultistatus {
|
||||
@@ -1,10 +1,10 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2014 Ricki Hirner (bitfire web engineering).
|
||||
/*
|
||||
* 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
|
||||
******************************************************************************/
|
||||
*/
|
||||
package at.bitfire.davdroid.webdav;
|
||||
|
||||
public class DavNoContentException extends DavException {
|
||||
@@ -1,10 +1,10 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2014 Ricki Hirner (bitfire web engineering).
|
||||
/*
|
||||
* 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
|
||||
******************************************************************************/
|
||||
*/
|
||||
package at.bitfire.davdroid.webdav;
|
||||
|
||||
public class DavNoMultiStatusException extends DavException {
|
||||
@@ -1,16 +1,12 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2014 Ricki Hirner (bitfire web engineering).
|
||||
/*
|
||||
* 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
|
||||
******************************************************************************/
|
||||
*/
|
||||
package at.bitfire.davdroid.webdav;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
import org.simpleframework.xml.Attribute;
|
||||
import org.simpleframework.xml.Element;
|
||||
import org.simpleframework.xml.ElementList;
|
||||
@@ -18,6 +14,10 @@ import org.simpleframework.xml.Namespace;
|
||||
import org.simpleframework.xml.Root;
|
||||
import org.simpleframework.xml.Text;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
@Namespace(prefix="D",reference="DAV:")
|
||||
@Root(strict=false)
|
||||
public class DavProp {
|
||||
@@ -25,43 +25,46 @@ public class DavProp {
|
||||
/* RFC 4918 WebDAV */
|
||||
|
||||
@Element(required=false)
|
||||
DavPropResourceType resourcetype;
|
||||
ResourceType resourcetype;
|
||||
|
||||
@Element(required=false)
|
||||
DavPropDisplayName displayname;
|
||||
DisplayName displayname;
|
||||
|
||||
@Element(required=false)
|
||||
DavPropGetCTag getctag;
|
||||
GetCTag getctag;
|
||||
|
||||
@Element(required=false)
|
||||
DavPropGetETag getetag;
|
||||
GetETag getetag;
|
||||
|
||||
@Root(strict=false)
|
||||
public static class DavPropResourceType {
|
||||
public static class ResourceType {
|
||||
@Element(required=false)
|
||||
@Getter private Collection collection;
|
||||
public static class Collection { }
|
||||
|
||||
@Element(required=false)
|
||||
@Getter private Addressbook addressbook;
|
||||
@Element(required=false)
|
||||
@Getter private Calendar calendar;
|
||||
|
||||
@Namespace(prefix="CD",reference="urn:ietf:params:xml:ns:carddav")
|
||||
public static class Addressbook { }
|
||||
|
||||
@Element(required=false)
|
||||
@Getter private Calendar calendar;
|
||||
@Namespace(prefix="C",reference="urn:ietf:params:xml:ns:caldav")
|
||||
public static class Calendar { }
|
||||
}
|
||||
|
||||
public static class DavPropDisplayName {
|
||||
public static class DisplayName {
|
||||
@Text(required=false)
|
||||
@Getter private String displayName;
|
||||
}
|
||||
|
||||
@Namespace(prefix="CS",reference="http://calendarserver.org/ns/")
|
||||
public static class DavPropGetCTag {
|
||||
public static class GetCTag {
|
||||
@Text(required=false)
|
||||
@Getter private String CTag;
|
||||
}
|
||||
|
||||
public static class DavPropGetETag {
|
||||
public static class GetETag {
|
||||
@Text(required=false)
|
||||
@Getter private String ETag;
|
||||
}
|
||||
@@ -70,9 +73,9 @@ public class DavProp {
|
||||
/* RFC 5397 WebDAV Current Principal Extension */
|
||||
|
||||
@Element(required=false,name="current-user-principal")
|
||||
DavCurrentUserPrincipal currentUserPrincipal;
|
||||
CurrentUserPrincipal currentUserPrincipal;
|
||||
|
||||
public static class DavCurrentUserPrincipal {
|
||||
public static class CurrentUserPrincipal {
|
||||
@Element(required=false)
|
||||
@Getter private DavHref href;
|
||||
}
|
||||
@@ -81,9 +84,9 @@ public class DavProp {
|
||||
/* RFC 3744 WebDAV Access Control Protocol */
|
||||
|
||||
@ElementList(required=false,name="current-user-privilege-set",entry="privilege")
|
||||
List<DavPropPrivilege> currentUserPrivilegeSet;
|
||||
List<Privilege> currentUserPrivilegeSet;
|
||||
|
||||
public static class DavPropPrivilege {
|
||||
public static class Privilege {
|
||||
@Element(required=false)
|
||||
@Getter private PrivAll all;
|
||||
|
||||
@@ -111,84 +114,97 @@ public class DavProp {
|
||||
/* RFC 4791 CalDAV, RFC 6352 CardDAV */
|
||||
|
||||
@Element(required=false,name="addressbook-home-set")
|
||||
DavAddressbookHomeSet addressbookHomeSet;
|
||||
AddressbookHomeSet addressbookHomeSet;
|
||||
|
||||
@Element(required=false,name="calendar-home-set")
|
||||
DavCalendarHomeSet calendarHomeSet;
|
||||
CalendarHomeSet calendarHomeSet;
|
||||
|
||||
@Element(required=false,name="addressbook-description")
|
||||
DavPropAddressbookDescription addressbookDescription;
|
||||
AddressbookDescription addressbookDescription;
|
||||
|
||||
@Namespace(prefix="CD",reference="urn:ietf:params:xml:ns:carddav")
|
||||
@ElementList(required=false,name="supported-address-data",entry="address-data-type")
|
||||
List<AddressDataType> supportedAddressData;
|
||||
|
||||
@Element(required=false,name="calendar-description")
|
||||
DavPropCalendarDescription calendarDescription;
|
||||
CalendarDescription calendarDescription;
|
||||
|
||||
@Element(required=false,name="calendar-color")
|
||||
DavPropCalendarColor calendarColor;
|
||||
CalendarColor calendarColor;
|
||||
|
||||
@Element(required=false,name="calendar-timezone")
|
||||
DavPropCalendarTimezone calendarTimezone;
|
||||
CalendarTimezone calendarTimezone;
|
||||
|
||||
@Namespace(prefix="C",reference="urn:ietf:params:xml:ns:caldav")
|
||||
@ElementList(required=false,name="supported-calendar-component-set",entry="comp")
|
||||
List<DavPropComp> supportedCalendarComponentSet;
|
||||
List<Comp> supportedCalendarComponentSet;
|
||||
|
||||
@Element(name="address-data",required=false)
|
||||
DavPropAddressData addressData;
|
||||
AddressData addressData;
|
||||
|
||||
@Element(name="calendar-data",required=false)
|
||||
DavPropCalendarData calendarData;
|
||||
CalendarData calendarData;
|
||||
|
||||
|
||||
@Namespace(prefix="CD",reference="urn:ietf:params:xml:ns:carddav")
|
||||
public static class DavAddressbookHomeSet {
|
||||
public static class AddressbookHomeSet {
|
||||
@Element(required=false)
|
||||
@Getter private DavHref href;
|
||||
}
|
||||
|
||||
@Namespace(prefix="C",reference="urn:ietf:params:xml:ns:caldav")
|
||||
public static class DavCalendarHomeSet {
|
||||
public static class CalendarHomeSet {
|
||||
@Element(required=false)
|
||||
@Getter private DavHref href;
|
||||
}
|
||||
|
||||
@Namespace(prefix="CD",reference="urn:ietf:params:xml:ns:carddav")
|
||||
public static class DavPropAddressbookDescription {
|
||||
public static class AddressbookDescription {
|
||||
@Text(required=false)
|
||||
@Getter private String description;
|
||||
}
|
||||
|
||||
@Namespace(prefix="CD",reference="urn:ietf:params:xml:ns:carddav")
|
||||
public static class AddressDataType {
|
||||
@Attribute(name="content-type")
|
||||
@Getter private String contentType;
|
||||
|
||||
@Attribute
|
||||
@Getter private String version;
|
||||
}
|
||||
|
||||
@Namespace(prefix="C",reference="urn:ietf:params:xml:ns:caldav")
|
||||
public static class DavPropCalendarDescription {
|
||||
public static class CalendarDescription {
|
||||
@Text(required=false)
|
||||
@Getter private String description;
|
||||
}
|
||||
|
||||
@Namespace(prefix="A",reference="http://apple.com/ns/ical/")
|
||||
public static class DavPropCalendarColor {
|
||||
public static class CalendarColor {
|
||||
@Text(required=false)
|
||||
@Getter private String color;
|
||||
}
|
||||
|
||||
@Namespace(prefix="C",reference="urn:ietf:params:xml:ns:caldav")
|
||||
public static class DavPropCalendarTimezone {
|
||||
public static class CalendarTimezone {
|
||||
@Text(required=false)
|
||||
@Getter private String timezone;
|
||||
}
|
||||
|
||||
@Namespace(prefix="C",reference="urn:ietf:params:xml:ns:caldav")
|
||||
public static class DavPropComp {
|
||||
public static class Comp {
|
||||
@Attribute
|
||||
@Getter String name;
|
||||
}
|
||||
|
||||
@Namespace(prefix="CD",reference="urn:ietf:params:xml:ns:carddav")
|
||||
public static class DavPropAddressData {
|
||||
public static class AddressData {
|
||||
@Text(required=false)
|
||||
@Getter String vcard;
|
||||
}
|
||||
|
||||
@Namespace(prefix="C",reference="urn:ietf:params:xml:ns:caldav")
|
||||
public static class DavPropCalendarData {
|
||||
public static class CalendarData {
|
||||
@Text(required=false)
|
||||
@Getter String ical;
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2014 Ricki Hirner (bitfire web engineering).
|
||||
/*
|
||||
* 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
|
||||
******************************************************************************/
|
||||
*/
|
||||
package at.bitfire.davdroid.webdav;
|
||||
|
||||
import org.simpleframework.xml.Element;
|
||||
@@ -1,10 +1,10 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2014 Ricki Hirner (bitfire web engineering).
|
||||
/*
|
||||
* 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
|
||||
******************************************************************************/
|
||||
*/
|
||||
package at.bitfire.davdroid.webdav;
|
||||
|
||||
import org.simpleframework.xml.Element;
|
||||
@@ -1,26 +1,37 @@
|
||||
/*
|
||||
* 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
|
||||
*/
|
||||
package at.bitfire.davdroid.webdav;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import org.apache.http.Header;
|
||||
import org.apache.http.HttpHost;
|
||||
import org.apache.http.HttpRequest;
|
||||
import org.apache.http.HttpResponse;
|
||||
import org.apache.http.ProtocolException;
|
||||
import org.apache.http.RequestLine;
|
||||
import org.apache.http.client.RedirectStrategy;
|
||||
import org.apache.http.client.methods.HttpUriRequest;
|
||||
import org.apache.http.client.methods.RequestBuilder;
|
||||
import org.apache.http.client.protocol.HttpClientContext;
|
||||
import org.apache.http.protocol.HttpContext;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
|
||||
import android.util.Log;
|
||||
import ch.boye.httpclientandroidlib.Header;
|
||||
import ch.boye.httpclientandroidlib.HttpRequest;
|
||||
import ch.boye.httpclientandroidlib.HttpResponse;
|
||||
import ch.boye.httpclientandroidlib.ProtocolException;
|
||||
import ch.boye.httpclientandroidlib.RequestLine;
|
||||
import ch.boye.httpclientandroidlib.client.RedirectStrategy;
|
||||
import ch.boye.httpclientandroidlib.client.methods.HttpUriRequest;
|
||||
import ch.boye.httpclientandroidlib.client.methods.RequestBuilder;
|
||||
import ch.boye.httpclientandroidlib.client.protocol.HttpClientContext;
|
||||
import ch.boye.httpclientandroidlib.protocol.HttpContext;
|
||||
import at.bitfire.davdroid.URIUtils;
|
||||
|
||||
/**
|
||||
* Custom Redirect Strategy that handles 30x for CalDAV/CardDAV-specific requests correctly
|
||||
*/
|
||||
public class DavRedirectStrategy implements RedirectStrategy {
|
||||
private final static String TAG = "davdroid.DavRedirectStrategy";
|
||||
final static DavRedirectStrategy INSTANCE = new DavRedirectStrategy();
|
||||
public final static DavRedirectStrategy INSTANCE = new DavRedirectStrategy();
|
||||
|
||||
protected final static String REDIRECTABLE_METHODS[] = {
|
||||
"OPTIONS", "GET", "PUT", "DELETE"
|
||||
@@ -33,8 +44,9 @@ public class DavRedirectStrategy implements RedirectStrategy {
|
||||
|
||||
String location = getLocation(request, response, context).toString();
|
||||
Log.i(TAG, "Following redirection: " + line.getMethod() + " " + line.getUri() + " -> " + location);
|
||||
|
||||
return RequestBuilder.copy(request)
|
||||
|
||||
return RequestBuilder
|
||||
.copy(request)
|
||||
.setUri(location)
|
||||
.removeHeaders("Content-Length") // Content-Length will be set again automatically, if required;
|
||||
// remove it now to avoid duplicate header
|
||||
@@ -71,22 +83,24 @@ public class DavRedirectStrategy implements RedirectStrategy {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
URI location = new URI(locationHdr.getValue());
|
||||
URI location = URIUtils.parseURI(locationHdr.getValue(), false);
|
||||
|
||||
// some servers don't return absolute URLs as required by RFC 2616
|
||||
if (!location.isAbsolute()) {
|
||||
Log.w(TAG, "Received invalid redirection with relative URL, repairing");
|
||||
|
||||
// determine original URL
|
||||
final HttpClientContext clientContext = HttpClientContext.adapt(context);
|
||||
final URI originalURI = new URI(clientContext.getTargetHost() + request.getRequestLine().getUri());
|
||||
|
||||
// determine new location relative to original URL
|
||||
location = originalURI.resolve(location);
|
||||
Log.w(TAG, "Received invalid redirection to relative URL, repairing");
|
||||
URI originalURI = URIUtils.parseURI(request.getRequestLine().getUri(), false);
|
||||
if (!originalURI.isAbsolute()) {
|
||||
final HttpHost target = HttpClientContext.adapt(context).getTargetHost();
|
||||
if (target != null)
|
||||
originalURI = org.apache.http.client.utils.URIUtilsHC4.rewriteURI(originalURI, target);
|
||||
else
|
||||
return null;
|
||||
}
|
||||
return originalURI.resolve(location);
|
||||
}
|
||||
return location;
|
||||
} catch (URISyntaxException e) {
|
||||
Log.e(TAG, "Received redirection from/to invalid URL, ignoring", e);
|
||||
Log.e(TAG, "Received redirection from/to invalid URI, ignoring", e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -1,20 +1,20 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2014 Ricki Hirner (bitfire web engineering).
|
||||
/*
|
||||
* 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
|
||||
******************************************************************************/
|
||||
*/
|
||||
package at.bitfire.davdroid.webdav;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
import org.simpleframework.xml.Element;
|
||||
import org.simpleframework.xml.ElementList;
|
||||
import org.simpleframework.xml.Root;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
@Root(strict=false)
|
||||
public class DavResponse {
|
||||
@Element
|
||||
@@ -1,15 +1,15 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2014 Ricki Hirner (bitfire web engineering).
|
||||
/*
|
||||
* 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
|
||||
******************************************************************************/
|
||||
*/
|
||||
package at.bitfire.davdroid.webdav;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
public class HttpException extends ch.boye.httpclientandroidlib.HttpException {
|
||||
public class HttpException extends org.apache.http.HttpException {
|
||||
private static final long serialVersionUID = -4805778240079377401L;
|
||||
|
||||
@Getter private int code;
|
||||
@@ -1,24 +1,24 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2014 Ricki Hirner (bitfire web engineering).
|
||||
/*
|
||||
* 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
|
||||
******************************************************************************/
|
||||
*/
|
||||
package at.bitfire.davdroid.webdav;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import org.apache.http.client.methods.HttpEntityEnclosingRequestBaseHC4;
|
||||
import org.apache.http.entity.StringEntity;
|
||||
import org.simpleframework.xml.Serializer;
|
||||
import org.simpleframework.xml.core.Persister;
|
||||
|
||||
import java.io.StringWriter;
|
||||
import java.net.URI;
|
||||
import java.util.LinkedList;
|
||||
|
||||
import org.simpleframework.xml.Serializer;
|
||||
import org.simpleframework.xml.core.Persister;
|
||||
|
||||
import android.util.Log;
|
||||
import ch.boye.httpclientandroidlib.client.methods.HttpEntityEnclosingRequestBase;
|
||||
import ch.boye.httpclientandroidlib.entity.StringEntity;
|
||||
|
||||
public class HttpPropfind extends HttpEntityEnclosingRequestBase {
|
||||
public class HttpPropfind extends HttpEntityEnclosingRequestBaseHC4 {
|
||||
private static final String TAG = "davdroid.HttpPropfind";
|
||||
|
||||
public final static String METHOD_NAME = "PROPFIND";
|
||||
@@ -26,7 +26,8 @@ public class HttpPropfind extends HttpEntityEnclosingRequestBase {
|
||||
public enum Mode {
|
||||
CURRENT_USER_PRINCIPAL,
|
||||
HOME_SETS,
|
||||
MEMBERS_COLLECTIONS,
|
||||
CARDDAV_COLLECTIONS,
|
||||
CALDAV_COLLECTIONS,
|
||||
COLLECTION_CTAG,
|
||||
MEMBERS_ETAG
|
||||
}
|
||||
@@ -38,8 +39,6 @@ public class HttpPropfind extends HttpEntityEnclosingRequestBase {
|
||||
|
||||
HttpPropfind(URI uri, Mode mode) {
|
||||
this(uri);
|
||||
|
||||
setHeader("Content-Type", "text/xml; charset=UTF-8");
|
||||
|
||||
DavPropfind propfind = new DavPropfind();
|
||||
propfind.prop = new DavProp();
|
||||
@@ -47,30 +46,37 @@ public class HttpPropfind extends HttpEntityEnclosingRequestBase {
|
||||
int depth = 0;
|
||||
switch (mode) {
|
||||
case CURRENT_USER_PRINCIPAL:
|
||||
propfind.prop.currentUserPrincipal = new DavProp.DavCurrentUserPrincipal();
|
||||
propfind.prop.currentUserPrincipal = new DavProp.CurrentUserPrincipal();
|
||||
break;
|
||||
case HOME_SETS:
|
||||
propfind.prop.addressbookHomeSet = new DavProp.DavAddressbookHomeSet();
|
||||
propfind.prop.calendarHomeSet = new DavProp.DavCalendarHomeSet();
|
||||
propfind.prop.addressbookHomeSet = new DavProp.AddressbookHomeSet();
|
||||
propfind.prop.calendarHomeSet = new DavProp.CalendarHomeSet();
|
||||
break;
|
||||
case MEMBERS_COLLECTIONS:
|
||||
case CARDDAV_COLLECTIONS:
|
||||
depth = 1;
|
||||
propfind.prop.displayname = new DavProp.DavPropDisplayName();
|
||||
propfind.prop.resourcetype = new DavProp.DavPropResourceType();
|
||||
propfind.prop.currentUserPrivilegeSet = new LinkedList<DavProp.DavPropPrivilege>();
|
||||
propfind.prop.addressbookDescription = new DavProp.DavPropAddressbookDescription();
|
||||
propfind.prop.calendarDescription = new DavProp.DavPropCalendarDescription();
|
||||
propfind.prop.calendarColor = new DavProp.DavPropCalendarColor();
|
||||
propfind.prop.calendarTimezone = new DavProp.DavPropCalendarTimezone();
|
||||
propfind.prop.supportedCalendarComponentSet = new LinkedList<DavProp.DavPropComp>();
|
||||
propfind.prop.displayname = new DavProp.DisplayName();
|
||||
propfind.prop.resourcetype = new DavProp.ResourceType();
|
||||
propfind.prop.currentUserPrivilegeSet = new LinkedList<DavProp.Privilege>();
|
||||
propfind.prop.addressbookDescription = new DavProp.AddressbookDescription();
|
||||
propfind.prop.supportedAddressData = new LinkedList<DavProp.AddressDataType>();
|
||||
break;
|
||||
case CALDAV_COLLECTIONS:
|
||||
depth = 1;
|
||||
propfind.prop.displayname = new DavProp.DisplayName();
|
||||
propfind.prop.resourcetype = new DavProp.ResourceType();
|
||||
propfind.prop.currentUserPrivilegeSet = new LinkedList<DavProp.Privilege>();
|
||||
propfind.prop.calendarDescription = new DavProp.CalendarDescription();
|
||||
propfind.prop.calendarColor = new DavProp.CalendarColor();
|
||||
propfind.prop.calendarTimezone = new DavProp.CalendarTimezone();
|
||||
propfind.prop.supportedCalendarComponentSet = new LinkedList<DavProp.Comp>();
|
||||
break;
|
||||
case COLLECTION_CTAG:
|
||||
propfind.prop.getctag = new DavProp.DavPropGetCTag();
|
||||
propfind.prop.getctag = new DavProp.GetCTag();
|
||||
break;
|
||||
case MEMBERS_ETAG:
|
||||
depth = 1;
|
||||
propfind.prop.getctag = new DavProp.DavPropGetCTag();
|
||||
propfind.prop.getetag = new DavProp.DavPropGetETag();
|
||||
propfind.prop.getctag = new DavProp.GetCTag();
|
||||
propfind.prop.getetag = new DavProp.GetETag();
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -79,6 +85,8 @@ public class HttpPropfind extends HttpEntityEnclosingRequestBase {
|
||||
StringWriter writer = new StringWriter();
|
||||
serializer.write(propfind, writer);
|
||||
|
||||
setHeader("Content-Type", "text/xml; charset=UTF-8");
|
||||
setHeader("Accept", "text/xml");
|
||||
setHeader("Depth", String.valueOf(depth));
|
||||
setEntity(new StringEntity(writer.toString(), "UTF-8"));
|
||||
} catch(Exception ex) {
|
||||
46
app/src/main/java/at/bitfire/davdroid/webdav/HttpReport.java
Normal file
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* 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
|
||||
*/
|
||||
package at.bitfire.davdroid.webdav;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import org.apache.http.client.methods.HttpEntityEnclosingRequestBaseHC4;
|
||||
import org.apache.http.entity.StringEntity;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URI;
|
||||
|
||||
public class HttpReport extends HttpEntityEnclosingRequestBaseHC4 {
|
||||
private static final String TAG = "davdroid.HttpEntityEncloseRequestBase";
|
||||
|
||||
public final static String METHOD_NAME = "REPORT";
|
||||
|
||||
|
||||
HttpReport(URI uri) {
|
||||
setURI(uri);
|
||||
}
|
||||
|
||||
HttpReport(URI uri, String entity) {
|
||||
this(uri);
|
||||
|
||||
setHeader("Content-Type", "text/xml; charset=UTF-8");
|
||||
setHeader("Accept", "text/xml");
|
||||
setHeader("Depth", "1");
|
||||
|
||||
try {
|
||||
setEntity(new StringEntity(entity, "UTF-8"));
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
Log.wtf(TAG, "String entity doesn't support UTF-8");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMethod() {
|
||||
return METHOD_NAME;
|
||||
}
|
||||
}
|
||||