Merge with origin/master

pull/2/head
R Hirner 10 years ago
parent 661738c866
commit 0cc23e3ecf

@ -15,8 +15,9 @@ android {
buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
minifyEnabled false
// minifyEnabled true
// proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
}
}
dexOptions {

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<lint>
<issue id="ExtraTranslation" severity="warning" />
<issue id="InvalidPackage" severity="warning" />
<issue id="MissingTranslation" severity="warning" />
</lint>

@ -1,142 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project name="davdroid" default="help">
<!-- The local.properties file is created and updated by the 'android' tool.
It contains the path to the SDK. It should *NOT* be checked into
Version Control Systems. -->
<property file="local.properties" />
<!-- The ant.properties file can be created by you. It is only edited by the
'android' tool to add properties to it.
This is the place to change some Ant specific build properties.
Here are some properties you may want to change/update:
source.dir
The name of the source directory. Default is 'src'.
out.dir
The name of the output directory. Default is 'bin'.
For other overridable properties, look at the beginning of the rules
files in the SDK, at tools/ant/build.xml
Properties related to the SDK location or the project target should
be updated using the 'android' tool with the 'update' action.
This file is an integral part of the build system for your
application and should be checked into Version Control Systems.
-->
<property file="ant.properties" />
<!-- if sdk.dir was not set from one of the property file, then
get it from the ANDROID_HOME env var.
This must be done before we load project.properties since
the proguard config can use sdk.dir -->
<property environment="env" />
<condition property="sdk.dir" value="${env.ANDROID_HOME}">
<isset property="env.ANDROID_HOME" />
</condition>
<!-- The project.properties file is created and updated by the 'android'
tool, as well as ADT.
This contains project specific properties such as project target, and library
dependencies. Lower level build properties are stored in ant.properties
(or in .classpath for Eclipse projects).
This file is an integral part of the build system for your
application and should be checked into Version Control Systems. -->
<loadproperties srcFile="project.properties" />
<!-- quick check on sdk.dir -->
<fail
message="sdk.dir is missing. Make sure to generate local.properties using 'android update project' or to inject it through the ANDROID_HOME environment variable."
unless="sdk.dir"
/>
<!--
Import per project custom build rules if present at the root of the project.
This is the place to put custom intermediary targets such as:
-pre-build
-pre-compile
-post-compile (This is typically used for code obfuscation.
Compiled code location: ${out.classes.absolute.dir}
If this is not done in place, override ${out.dex.input.absolute.dir})
-post-package
-post-build
-pre-clean
-->
<import file="custom_rules.xml" optional="true" />
<!-- Import the actual build file.
To customize existing targets, there are two options:
- Customize only one target:
- copy/paste the target into this file, *before* the
<import> task.
- customize it to your needs.
- Customize the whole content of build.xml
- copy/paste the content of the rules files (minus the top node)
into this file, replacing the <import> task.
- customize to your needs.
***********************
****** IMPORTANT ******
***********************
In all cases you must update the value of version-tag below to read 'custom' instead of an integer,
in order to avoid having your file be overridden by tools such as "android update project"
-->
<!-- version-tag: custom -->
<target name="-compile" depends="-pre-build, -build-setup, -code-gen, -pre-compile">
<do-only-if-manifest-hasCode elseText="hasCode = false. Skipping...">
<!-- merge the project's own classpath and the tested project's classpath -->
<path id="project.javac.classpath">
<fileset dir="compile-libs" includes="*.jar" />
<path refid="project.all.jars.path" />
<path refid="tested.project.classpath" />
<path path="${java.compiler.classpath}" />
</path>
<javac encoding="${java.encoding}"
source="${java.source}" target="${java.target}"
debug="true" extdirs="" includeantruntime="false"
destdir="${out.classes.absolute.dir}"
bootclasspathref="project.target.class.path"
verbose="${verbose}"
classpathref="project.javac.classpath"
fork="${need.javac.fork}">
<src path="${source.absolute.dir}" />
<src path="${gen.absolute.dir}" />
<compilerarg line="${java.compilerargs}" />
</javac>
<!-- if the project is a library then we generate a jar file -->
<if condition="${project.is.library}">
<then>
<echo level="info">Creating library output jar file...</echo>
<property name="out.library.jar.file" location="${out.absolute.dir}/classes.jar" />
<if>
<condition>
<length string="${android.package.excludes}" trim="true" when="greater" length="0" />
</condition>
<then>
<echo level="info">Custom jar packaging exclusion: ${android.package.excludes}</echo>
</then>
</if>
<propertybyreplace name="project.app.package.path" input="${project.app.package}" replace="." with="/" />
<jar destfile="${out.library.jar.file}">
<fileset dir="${out.classes.absolute.dir}"
includes="**/*.class"
excludes="${project.app.package.path}/R.class ${project.app.package.path}/R$*.class ${project.app.package.path}/BuildConfig.class"/>
<fileset dir="${source.absolute.dir}" excludes="**/*.java ${android.package.excludes}" />
</jar>
</then>
</if>
</do-only-if-manifest-hasCode>
</target>
<import file="${sdk.dir}/tools/ant/build.xml" />
</project>

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

@ -1,28 +0,0 @@
# To enable ProGuard in your project, edit project.properties
# to define the proguard.config property as described in that file.
#
# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in ${sdk.dir}/tools/proguard/proguard-android.txt
# You can edit the include path and order by changing the ProGuard
# include property in project.properties.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# Add any project specific keep options here:
-dontusemixedcaseclassnames
-dontskipnonpubliclibraryclassmembers
-dontwarn edu.emory.mathcs.backport.**
-dontwarn ezvcard.**
-dontwarn net.fortuna.**
-dontwarn org.apache.**
-dontwarn org.simpleframework.xml.**
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}

@ -1,14 +0,0 @@
# This file is automatically generated by Android Tools.
# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
#
# This file must be checked in Version Control Systems.
#
# To customize properties used by the Ant build system edit
# "ant.properties", and override values to adapt the script to your
# project structure.
#
# To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home):
#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt
# Project target.
target=android-21

@ -1,33 +0,0 @@
/*******************************************************************************
* Copyright (c) 2014 Ricki Hirner (bitfire web engineering).
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Public License v3.0
* which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/gpl.html
******************************************************************************/
package at.bitfire.davdroid;
import java.lang.reflect.Array;
public class ArrayUtils {
@SuppressWarnings("unchecked")
public static <T> T[][] partition(T[] bigArray, int max) {
int nItems = bigArray.length;
int nPartArrays = (nItems + max-1)/max;
T[][] partArrays = (T[][])Array.newInstance(bigArray.getClass().getComponentType(), nPartArrays, 0);
// nItems is now the number of remaining items
for (int i = 0; nItems > 0; i++) {
int n = (nItems < max) ? nItems : max;
partArrays[i] = (T[])Array.newInstance(bigArray.getClass().getComponentType(), n);
System.arraycopy(bigArray, i*max, partArrays[i], 0, n);
nItems -= n;
}
return partArrays;
}
}

@ -1,18 +0,0 @@
/*******************************************************************************
* Copyright (c) 2014 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.6.8",
ACCOUNT_TYPE = "bitfire.at.davdroid",
WEB_URL_HELP = "https://davdroid.bitfire.at/configuration?pk_campaign=davdroid-app",
SETTING_DISABLE_COMPRESSION = "disable_compression",
SETTING_NETWORK_LOGGING = "network_logging";
}

@ -1,82 +0,0 @@
/*******************************************************************************
* Copyright (c) 2014 Ricki Hirner (bitfire web engineering).
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Public License v3.0
* which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/gpl.html
******************************************************************************/
package at.bitfire.davdroid;
import android.app.Activity;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.provider.Settings;
import android.text.Html;
import android.text.method.LinkMovementMethod;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.widget.TextView;
import at.bitfire.davdroid.syncadapter.AddAccountActivity;
import at.bitfire.davdroid.syncadapter.GeneralSettingsActivity;
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
TextView tvWorkaround = (TextView)findViewById(R.id.text_workaround);
if (fromPlayStore()) {
tvWorkaround.setVisibility(View.VISIBLE);
tvWorkaround.setText(Html.fromHtml(getString(R.string.html_main_workaround)));
tvWorkaround.setMovementMethod(LinkMovementMethod.getInstance());
}
TextView tvInfo = (TextView)findViewById(R.id.text_info);
tvInfo.setText(Html.fromHtml(getString(R.string.html_main_info, Constants.APP_VERSION)));
tvInfo.setMovementMethod(LinkMovementMethod.getInstance());
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.main_activity, menu);
return true;
}
public void addAccount(MenuItem item) {
Intent intent = new Intent(this, AddAccountActivity.class);
startActivity(intent);
}
public void showDebugSettings(MenuItem item) {
Intent intent = new Intent(this, GeneralSettingsActivity.class);
startActivity(intent);
}
public void showSyncSettings(MenuItem item) {
Intent intent = new Intent(Settings.ACTION_SYNC_SETTINGS);
startActivity(intent);
}
public void showWebsite(MenuItem item) {
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(Uri.parse(Constants.WEB_URL_HELP + "&pk_kwd=main-activity"));
startActivity(intent);
}
private boolean fromPlayStore() {
try {
return "com.android.vending".equals(getPackageManager().getInstallerPackageName("at.bitfire.davdroid"));
} catch(IllegalArgumentException e) {
}
return false;
}
}

@ -1,98 +0,0 @@
/*******************************************************************************
* Copyright (c) 2014 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.MalformedURLException;
import java.net.URL;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import android.annotation.SuppressLint;
import android.util.Log;
@SuppressLint("DefaultLocale")
public class URLUtils {
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 URL ensureTrailingSlash(URL href) {
if (!href.getPath().endsWith("/"))
try {
URL newURL = new URL(href, href.getPath() + "/");
// "@" is the only character that is not encoded
//newURL = new URI(newURI.toString().replaceAll("@", "%40"));
Log.d(TAG, "Implicitly appending trailing slash to collection " + href + " -> " + newURL);
return newURL;
} catch (MalformedURLException e) {
Log.e(TAG, "Couldn't append trailing slash to collection URI", e);
}
return href;
}
/** handles invalid URLs/paths as good as possible **/
public static String sanitize(String original) {
if (original == null)
return null;
Pattern p = Pattern.compile("^((https?:)?//([^/]+))?(.*)", Pattern.CASE_INSENSITIVE);
// $1: "http://hostname" or "https://hostname" or "//hostname" or empty (hostname may end with":port")
// $2: "http:" or "https:" or empty
// $3: hostname (may end with ":port") or empty
// $4: path or empty
Matcher m = p.matcher(original);
if (m.matches()) {
String schema = m.group(2),
host = m.group(3),
path = m.group(4);
if (host != null)
// sanitize host name (don't replace "[", "]", ":" for IP address literals and port numbers)
// "@" should be used for user name/password, but this case shouldn't appear in our URLs
for (char c : new char[] { '@', ' ', '<', '>', '"', '#', '{', '}', '|', '\\', '^', '~', '`' })
host = host.replace(String.valueOf(c), "%" + Integer.toHexString(c));
if (path != null)
// rewrite reserved characters:
// ":" and "@" may be used in the host part but not in the path
// rewrite unsafe characters:
// " ", "<", ">", """, "#", "{", "}", "|", "\", "^", "~", "["], "]", "`"
// do not rewrite "%" because we assume that URLs should be already encoded correctly
for (char c : new char[] { ':', '@', ' ', '<', '>', '"', '#', '{', '}', '|', '\\', '^', '~', '[', ']', '`' })
path = path.replace(String.valueOf(c), "%" + Integer.toHexString(c));
String url = (schema != null) ? schema : "";
if (host != null)
url = url + "//" + host;
if (path != null)
url = url + path;
if (!url.equals(original))
Log.w(TAG, "Trying to repair invalid URL: " + original + " -> " + url);
return url;
} else {
Log.w(TAG, "Couldn't sanitize URL: " + original);
return original;
}
}
}

@ -1,37 +0,0 @@
/*******************************************************************************
* Copyright (c) 2014 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.MalformedURLException;
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";
@Override
protected String memberContentType() {
return "text/calendar";
}
@Override
protected DavMultiget.Type multiGetType() {
return DavMultiget.Type.CALENDAR;
}
@Override
protected Event newResourceSkeleton(String name, String ETag) {
return new Event(name, ETag);
}
public CalDavCalendar(CloseableHttpClient httpClient, String baseURL, String user, String password, boolean preemptiveAuth) throws MalformedURLException {
super(httpClient, baseURL, user, password, preemptiveAuth);
}
}

@ -1,37 +0,0 @@
/*******************************************************************************
* Copyright (c) 2014 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.MalformedURLException;
import at.bitfire.davdroid.webdav.DavMultiget;
import ch.boye.httpclientandroidlib.impl.client.CloseableHttpClient;
public class CardDavAddressBook extends RemoteCollection<Contact> {
//private final static String TAG = "davdroid.CardDavAddressBook";
@Override
protected String memberContentType() {
return Contact.MIME_TYPE;
}
@Override
protected DavMultiget.Type multiGetType() {
return DavMultiget.Type.ADDRESS_BOOK;
}
@Override
protected Contact newResourceSkeleton(String name, String ETag) {
return new Contact(name, ETag);
}
public CardDavAddressBook(CloseableHttpClient httpClient, String baseURL, String user, String password, boolean preemptiveAuth) throws MalformedURLException {
super(httpClient, baseURL, user, password, preemptiveAuth);
}
}

@ -1,410 +0,0 @@
/*******************************************************************************
* Copyright (c) 2014 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.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.VCardVersion;
import ezvcard.ValidationWarnings;
import ezvcard.parameter.EmailType;
import ezvcard.parameter.ImageType;
import ezvcard.parameter.TelephoneType;
import ezvcard.property.Address;
import ezvcard.property.Anniversary;
import ezvcard.property.Birthday;
import ezvcard.property.Categories;
import ezvcard.property.Email;
import ezvcard.property.FormattedName;
import ezvcard.property.Impp;
import ezvcard.property.Logo;
import ezvcard.property.Nickname;
import ezvcard.property.Note;
import ezvcard.property.Organization;
import ezvcard.property.Photo;
import ezvcard.property.RawProperty;
import ezvcard.property.Revision;
import ezvcard.property.Role;
import ezvcard.property.Sound;
import ezvcard.property.Source;
import ezvcard.property.StructuredName;
import ezvcard.property.Telephone;
import ezvcard.property.Title;
import ezvcard.property.Uid;
import ezvcard.property.Url;
/**
* Represents a contact. Locally, this is a Contact in the Android
* device; remote, this is a VCard.
*/
@ToString(callSuper = true)
public class Contact extends Resource {
private final static String TAG = "davdroid.Contact";
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",
PROPERTY_PHONETIC_MIDDLE_NAME = "X-PHONETIC-MIDDLE-NAME",
PROPERTY_PHONETIC_LAST_NAME = "X-PHONETIC-LAST-NAME",
PROPERTY_SIP = "X-SIP";
public final static EmailType EMAIL_TYPE_MOBILE = EmailType.get("X-MOBILE");
public final static TelephoneType
PHONE_TYPE_CALLBACK = TelephoneType.get("X-CALLBACK"),
PHONE_TYPE_COMPANY_MAIN = TelephoneType.get("X-COMPANY_MAIN"),
PHONE_TYPE_RADIO = TelephoneType.get("X-RADIO"),
PHONE_TYPE_ASSISTANT = TelephoneType.get("X-ASSISTANT"),
PHONE_TYPE_MMS = TelephoneType.get("X-MMS");
@Getter @Setter private String unknownProperties;
@Getter @Setter private boolean starred;
@Getter @Setter private String displayName, nickName;
@Getter @Setter private String prefix, givenName, middleName, familyName, suffix;
@Getter @Setter private String phoneticGivenName, phoneticMiddleName, phoneticFamilyName;
@Getter @Setter private String note;
@Getter @Setter private Organization organization;
@Getter @Setter private String jobTitle, jobDescription;
@Getter @Setter private byte[] photo;
@Getter @Setter private Anniversary anniversary;
@Getter @Setter private Birthday birthDay;
@Getter private List<Telephone> phoneNumbers = new LinkedList<Telephone>();
@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>();
/* instance methods */
public Contact(String name, String ETag) {
super(name, ETag);
}
public Contact(long localID, String resourceName, String eTag) {
super(localID, resourceName, eTag);
}
@Override
public void initialize() {
generateUID();
name = uid + ".vcf";
}
protected void generateUID() {
uid = UUID.randomUUID().toString();
}
/* VCard methods */
@Override
public void parseEntity(InputStream is) throws IOException {
VCard vcard = Ezvcard.parse(is).first();
if (vcard == null)
return;
// now work through all supported properties
// supported properties are removed from the VCard after parsing
// so that only unknown properties are left and can be stored separately
// UID
Uid uid = vcard.getUid();
if (uid != null) {
this.uid = uid.getValue();
vcard.removeProperties(Uid.class);
} else {
Log.w(TAG, "Received VCard without UID, generating new one");
generateUID();
}
// X-DAVDROID-STARRED
RawProperty starred = vcard.getExtendedProperty(PROPERTY_STARRED);
if (starred != null && starred.getValue() != null) {
this.starred = starred.getValue().equals("1");
vcard.removeExtendedProperty(PROPERTY_STARRED);
} else
this.starred = false;
// FN
FormattedName fn = vcard.getFormattedName();
if (fn != null) {
displayName = fn.getValue();
vcard.removeProperties(FormattedName.class);
} else
Log.w(TAG, "Received invalid VCard without FN (formatted name) property");
// N
StructuredName n = vcard.getStructuredName();
if (n != null) {
prefix = StringUtils.join(n.getPrefixes(), " ");
givenName = n.getGiven();
middleName = StringUtils.join(n.getAdditional(), " ");
familyName = n.getFamily();
suffix = StringUtils.join(n.getSuffixes(), " ");
vcard.removeProperties(StructuredName.class);
}
// phonetic names
RawProperty
phoneticFirstName = vcard.getExtendedProperty(PROPERTY_PHONETIC_FIRST_NAME),
phoneticMiddleName = vcard.getExtendedProperty(PROPERTY_PHONETIC_MIDDLE_NAME),
phoneticLastName = vcard.getExtendedProperty(PROPERTY_PHONETIC_LAST_NAME);
if (phoneticFirstName != null) {
phoneticGivenName = phoneticFirstName.getValue();
vcard.removeExtendedProperty(PROPERTY_PHONETIC_FIRST_NAME);
}
if (phoneticMiddleName != null) {
this.phoneticMiddleName = phoneticMiddleName.getValue();
vcard.removeExtendedProperty(PROPERTY_PHONETIC_MIDDLE_NAME);
}
if (phoneticLastName != null) {
phoneticFamilyName = phoneticLastName.getValue();
vcard.removeExtendedProperty(PROPERTY_PHONETIC_LAST_NAME);
}
// TEL
phoneNumbers = vcard.getTelephoneNumbers();
vcard.removeProperties(Telephone.class);
// EMAIL
emails = vcard.getEmails();
vcard.removeProperties(Email.class);
// PHOTO
for (Photo photo : vcard.getPhotos()) {
this.photo = photo.getData();
vcard.removeProperties(Photo.class);
break;
}
// ORG
organization = vcard.getOrganization();
vcard.removeProperties(Organization.class);
// TITLE
for (Title title : vcard.getTitles()) {
jobTitle = title.getValue();
vcard.removeProperties(Title.class);
break;
}
// ROLE
for (Role role : vcard.getRoles()) {
this.jobDescription = role.getValue();
vcard.removeProperties(Role.class);
break;
}
// IMPP
impps = vcard.getImpps();
vcard.removeProperties(Impp.class);
// NICKNAME
Nickname nicknames = vcard.getNickname();
if (nicknames != null) {
if (nicknames.getValues() != null)
nickName = StringUtils.join(nicknames.getValues(), ", ");
vcard.removeProperties(Nickname.class);
}
// NOTE
List<String> notes = new LinkedList<String>();
for (Note note : vcard.getNotes())
notes.add(note.getValue());
if (!notes.isEmpty())
note = StringUtils.join(notes, "\n---\n");
vcard.removeProperties(Note.class);
// ADR
addresses = vcard.getAddresses();
vcard.removeProperties(Address.class);
// CATEGORY
Categories categories = vcard.getCategories();
if (categories != null)
this.categories = categories.getValues();
vcard.removeProperties(Categories.class);
// URL
for (Url url : vcard.getUrls())
URLs.add(url.getValue());
vcard.removeProperties(Url.class);
// BDAY
birthDay = vcard.getBirthday();
vcard.removeProperties(Birthday.class);
// ANNIVERSARY
anniversary = vcard.getAnniversary();
vcard.removeProperties(Anniversary.class);
// X-SIP
for (RawProperty sip : vcard.getExtendedProperties(PROPERTY_SIP))
impps.add(new Impp("sip", sip.getValue()));
vcard.removeExtendedProperty(PROPERTY_SIP);
// remove binary properties because of potential OutOfMemory / TransactionTooLarge exceptions
vcard.removeProperties(Logo.class);
vcard.removeProperties(Sound.class);
// remove properties that don't apply anymore
vcard.removeProperties(Revision.class);
vcard.removeProperties(Source.class);
// store all remaining properties into unknownProperties
if (!vcard.getProperties().isEmpty() || !vcard.getExtendedProperties().isEmpty())
try {
unknownProperties = vcard.write();
} catch(Exception e) {
Log.w(TAG, "Couldn't store unknown properties (maybe illegal syntax), dropping them");
}
}
@Override
public ByteArrayOutputStream toEntity() throws IOException {
VCard vcard = null;
try {
if (unknownProperties != null)
vcard = Ezvcard.parse(unknownProperties).first();
} catch (Exception e) {
Log.w(TAG, "Couldn't parse original property set, beginning from scratch");
}
if (vcard == null)
vcard = new VCard();
if (uid != null)
vcard.setUid(new Uid(uid));
else
Log.wtf(TAG, "Generating VCard without UID");
if (starred)
vcard.setExtendedProperty(PROPERTY_STARRED, "1");
if (displayName != null)
vcard.setFormattedName(displayName);
else if (organization != null && organization.getValues() != null && organization.getValues().get(0) != null)
vcard.setFormattedName(organization.getValues().get(0));
else
Log.w(TAG, "No FN (formatted name) available to generate VCard");
// N
if (familyName != null || middleName != null || givenName != null) {
StructuredName n = new StructuredName();
if (prefix != null)
for (String p : StringUtils.split(prefix))
n.addPrefix(p);
n.setGiven(givenName);
if (middleName != null)
for (String middle : StringUtils.split(middleName))
n.addAdditional(middle);
n.setFamily(familyName);
if (suffix != null)
for (String s : StringUtils.split(suffix))
n.addSuffix(s);
vcard.setStructuredName(n);
}
// phonetic names
if (phoneticGivenName != null)
vcard.addExtendedProperty(PROPERTY_PHONETIC_FIRST_NAME, phoneticGivenName);
if (phoneticMiddleName != null)
vcard.addExtendedProperty(PROPERTY_PHONETIC_MIDDLE_NAME, phoneticMiddleName);
if (phoneticFamilyName != null)
vcard.addExtendedProperty(PROPERTY_PHONETIC_LAST_NAME, phoneticFamilyName);
// TEL
for (Telephone phoneNumber : phoneNumbers)
vcard.addTelephoneNumber(phoneNumber);
// EMAIL
for (Email email : emails)
vcard.addEmail(email);
// ORG, TITLE, ROLE
if (organization != null)
vcard.setOrganization(organization);
if (jobTitle != null)
vcard.addTitle(jobTitle);
if (jobDescription != null)
vcard.addRole(jobDescription);
// IMPP
for (Impp impp : impps)
vcard.addImpp(impp);
// NICKNAME
if (!StringUtils.isBlank(nickName))
vcard.setNickname(nickName);
// NOTE
if (!StringUtils.isBlank(note))
vcard.addNote(note);
// ADR
for (Address address : addresses)
vcard.addAddress(address);
// CATEGORY
if (!categories.isEmpty())
vcard.setCategories(categories.toArray(new String[0]));
// URL
for (String url : URLs)
vcard.addUrl(url);
// ANNIVERSARY
if (anniversary != null)
vcard.setAnniversary(anniversary);
// BDAY
if (birthDay != null)
vcard.setBirthday(birthDay);
// PHOTO
if (photo != null)
vcard.addPhoto(new Photo(photo, ImageType.JPEG));
// PRODID, REV
vcard.setProductId("DAVdroid/" + Constants.APP_VERSION + " (ez-vcard/" + Ezvcard.VERSION + ")");
vcard.setRevision(Revision.now());
// validate and print warnings
ValidationWarnings warnings = vcard.validate(VCardVersion.V3_0);
if (!warnings.isEmpty())
Log.w(TAG, "Created potentially invalid VCard! " + warnings);
ByteArrayOutputStream os = new ByteArrayOutputStream();
Ezvcard
.write(vcard)
.version(VCardVersion.V3_0)
.versionStrict(false)
.prodId(false) // we provide our own PRODID
.go(os);
return os;
}
}

@ -1,299 +0,0 @@
/*******************************************************************************
* Copyright (c) 2014 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.Closeable;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.LinkedList;
import java.util.List;
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 android.content.Context;
import android.util.Log;
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 ch.boye.httpclientandroidlib.HttpException;
import ch.boye.httpclientandroidlib.impl.client.CloseableHttpClient;
import ezvcard.VCardVersion;
public class DavResourceFinder implements Closeable {
private final static String TAG = "davdroid.DavResourceFinder";
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(true, true);
}
@Override
public void close() throws IOException {
httpClient.close();
}
public void findResources(ServerInfo serverInfo) throws URISyntaxException, DavException, HttpException, IOException {
// CardDAV
WebDavResource principal = getCurrentUserPrincipal(serverInfo, "carddav");
if (principal != null) {
serverInfo.setCardDAV(true);
principal.propfind(Mode.HOME_SETS);
String pathAddressBooks = principal.getAddressbookHomeSet();
if (pathAddressBooks != null) {
Log.i(TAG, "Found address book home set: " + pathAddressBooks);
WebDavResource homeSetAddressBooks = new WebDavResource(principal, pathAddressBooks);
if (checkHomesetCapabilities(homeSetAddressBooks, "addressbook")) {
homeSetAddressBooks.propfind(Mode.CARDDAV_COLLECTIONS);
List<ServerInfo.ResourceInfo> addressBooks = new LinkedList<ServerInfo.ResourceInfo>();
if (homeSetAddressBooks.getMembers() != null)
for (WebDavResource resource : homeSetAddressBooks.getMembers())
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
principal = getCurrentUserPrincipal(serverInfo, "caldav");
if (principal != null) {
serverInfo.setCalDAV(true);
principal.propfind(Mode.HOME_SETS);
String pathCalendars = principal.getCalendarHomeSet();
if (pathCalendars != null) {
Log.i(TAG, "Found calendar home set: " + pathCalendars);
WebDavResource homeSetCalendars = new WebDavResource(principal, pathCalendars);
if (checkHomesetCapabilities(homeSetCalendars, "calendar-access")) {
homeSetCalendars.propfind(Mode.CALDAV_COLLECTIONS);
List<ServerInfo.ResourceInfo> calendars = new LinkedList<ServerInfo.ResourceInfo>();
if (homeSetCalendars.getMembers() != null)
for (WebDavResource resource : homeSetCalendars.getMembers())
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 URL getInitialContextURL(ServerInfo serverInfo, String serviceName) throws URISyntaxException, MalformedURLException {
String scheme = null,
domain = null;
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);
// 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");
}
if (port != -1)
return new URL(scheme, domain, port, path);
else
return new URL(scheme, domain, path);
}
/**
* 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 resource 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 {
URL initialURL = getInitialContextURL(serverInfo, serviceName);
if (initialURL != null) {
// 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)
return new WebDavResource(wellKnown, wellKnown.getCurrentUserPrincipal());
} catch (NotAuthorizedException e) {
Log.w(TAG, "Not authorized for well-known " + serviceName + " service detection", e);
throw e;
} catch (URISyntaxException e) {
Log.w(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);
}
// fall back to user-given initial context path
try {
base.propfind(Mode.CURRENT_USER_PRINCIPAL);
if (base.getCurrentUserPrincipal() != null)
return new WebDavResource(base, base.getCurrentUserPrincipal());
} 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);
}
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,368 +0,0 @@
/*******************************************************************************
* Copyright (c) 2014 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 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;
import net.fortuna.ical4j.model.Component;
import net.fortuna.ical4j.model.ComponentList;
import net.fortuna.ical4j.model.Date;
import net.fortuna.ical4j.model.DateTime;
import net.fortuna.ical4j.model.DefaultTimeZoneRegistryFactory;
import net.fortuna.ical4j.model.Property;
import net.fortuna.ical4j.model.PropertyList;
import net.fortuna.ical4j.model.TimeZoneRegistry;
import net.fortuna.ical4j.model.ValidationException;
import net.fortuna.ical4j.model.component.VAlarm;
import net.fortuna.ical4j.model.component.VEvent;
import net.fortuna.ical4j.model.component.VTimeZone;
import net.fortuna.ical4j.model.parameter.Value;
import net.fortuna.ical4j.model.property.Attendee;
import net.fortuna.ical4j.model.property.Clazz;
import net.fortuna.ical4j.model.property.DateProperty;
import net.fortuna.ical4j.model.property.Description;
import net.fortuna.ical4j.model.property.DtEnd;
import net.fortuna.ical4j.model.property.DtStart;
import net.fortuna.ical4j.model.property.Duration;
import net.fortuna.ical4j.model.property.ExDate;
import net.fortuna.ical4j.model.property.ExRule;
import net.fortuna.ical4j.model.property.LastModified;
import net.fortuna.ical4j.model.property.Location;
import net.fortuna.ical4j.model.property.Organizer;
import net.fortuna.ical4j.model.property.ProdId;
import net.fortuna.ical4j.model.property.RDate;
import net.fortuna.ical4j.model.property.RRule;
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.SimpleHostInfo;
import net.fortuna.ical4j.util.UidGenerator;
import android.text.format.Time;
import android.util.Log;
import at.bitfire.davdroid.Constants;
import at.bitfire.davdroid.syncadapter.DavSyncAdapter;
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 private String summary, location, description;
@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 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);
}
public Event(String name, String ETag) {
super(name, ETag);
}
public Event(long localID, String name, String ETag) {
super(localID, name, ETag);
}
@Override
public void initialize() {
generateUID();
name = uid.replace("@", "_") + ".ics";
}
protected void generateUID() {
UidGenerator generator = new UidGenerator(new SimpleHostInfo(DavSyncAdapter.getAndroidID()), String.valueOf(android.os.Process.myPid()));
uid = generator.generateUid().getValue();
}
@Override
@SuppressWarnings("unchecked")
public void parseEntity(@NonNull InputStream entity) throws IOException, InvalidResourceException {
net.fortuna.ical4j.model.Calendar ical;
try {
CalendarBuilder builder = new CalendarBuilder();
ical = builder.build(entity);
if (ical == null)
throw new InvalidResourceException("No iCalendar found");
} catch (ParserException e) {
throw new InvalidResourceException(e);
}
// event
ComponentList events = ical.getComponents(Component.VEVENT);
if (events == null || events.isEmpty())
throw new InvalidResourceException("No VEVENT found");
VEvent event = (VEvent)events.get(0);
if (event.getUid() != null)
uid = event.getUid().getValue();
else {
Log.w(TAG, "Received VEVENT without UID, generating new one");
generateUID();
}
if ((dtStart = event.getStartDate()) == null || (dtEnd = event.getEndDate()) == null)
throw new InvalidResourceException("Invalid start time/end time/duration");
if (hasTime(dtStart)) {
validateTimeZone(dtStart);
validateTimeZone(dtEnd);
}
// all-day events and "events on that day":
// * related UNIX times must be in UTC
// * must have a duration (set to one day if missing)
if (!hasTime(dtStart) && !dtEnd.getDate().after(dtStart.getDate())) {
Log.i(TAG, "Repairing iCal: DTEND := DTSTART+1");
Calendar c = Calendar.getInstance(TimeZone.getTimeZone(Time.TIMEZONE_UTC));
c.setTime(dtStart.getDate());
c.add(Calendar.DATE, 1);
dtEnd.setDate(new Date(c.getTimeInMillis()));
}
rrule = (RRule)event.getProperty(Property.RRULE);
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;
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)
forPublic = true;
else if (classification == Clazz.CONFIDENTIAL || classification == Clazz.PRIVATE)
forPublic = false;
}
this.alarms = event.getAlarms();
}
@Override
@SuppressWarnings("unchecked")
public ByteArrayOutputStream toEntity() throws IOException {
net.fortuna.ical4j.model.Calendar ical = new net.fortuna.ical4j.model.Calendar();
ical.getProperties().add(Version.VERSION_2_0);
ical.getProperties().add(new ProdId("-//bitfire web engineering//DAVdroid " + Constants.APP_VERSION + " (ical4j 1.0.x)//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);
// 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());
CalendarOutputter output = new CalendarOutputter(false);
ByteArrayOutputStream os = new ByteArrayOutputStream();
try {
output.output(ical, os);
} catch (ValidationException e) {
Log.e(TAG, "Generated invalid iCalendar");
}
return os;
}
public long getDtStartInMillis() {
return dtStart.getDate().getTime();
}
public String getDtStartTzID() {
return getTzId(dtStart);
}
public void setDtStart(long tsStart, String tzID) {
if (tzID == null) { // all-day
dtStart = new DtStart(new Date(tsStart));
} else {
DateTime start = new DateTime(tsStart);
start.setTimeZone(tzRegistry.getTimeZone(tzID));
dtStart = new DtStart(start);
}
}
public long getDtEndInMillis() {
return dtEnd.getDate().getTime();
}
public String getDtEndTzID() {
return getTzId(dtEnd);
}
public void setDtEnd(long tsEnd, String tzID) {
if (tzID == null) { // all-day
dtEnd = new DtEnd(new Date(tsEnd));
} else {
DateTime end = new DateTime(tsEnd);
end.setTimeZone(tzRegistry.getTimeZone(tzID));
dtEnd = new DtEnd(end);
}
}
// helpers
public boolean isAllDay() {
return !hasTime(dtStart);
}
protected static boolean hasTime(DateProperty date) {
return date.getDate() instanceof DateTime;
}
protected static String getTzId(DateProperty date) {
if (date.isUtc() || !hasTime(date))
return Time.TIMEZONE_UTC;
else if (date.getTimeZone() != null)
return date.getTimeZone().getID();
else if (date.getParameter(Value.TZID) != null)
return date.getParameter(Value.TZID).getValue();
// fallback
return Time.TIMEZONE_UTC;
}
/* 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));
}
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,20 +0,0 @@
/*******************************************************************************
* Copyright (c) 2014 Ricki Hirner (bitfire web engineering).
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Public License v3.0
* which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/gpl.html
******************************************************************************/
package at.bitfire.davdroid.resource;
public class InvalidResourceException extends Exception {
private static final long serialVersionUID = 1593585432655578220L;
public InvalidResourceException(String message) {
super(message);
}
public InvalidResourceException(Throwable throwable) {
super(throwable);
}
}

File diff suppressed because it is too large Load Diff

@ -1,612 +0,0 @@
/*******************************************************************************
* Copyright (c) 2014 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;
import android.content.ContentProviderClient;
import android.content.ContentProviderOperation;
import android.content.ContentProviderOperation.Builder;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.database.Cursor;
import android.database.DatabaseUtils;
import android.net.Uri;
import android.os.Build;
import android.os.RemoteException;
import android.provider.CalendarContract;
import android.provider.CalendarContract.Attendees;
import android.provider.CalendarContract.Calendars;
import android.provider.CalendarContract.Events;
import android.provider.CalendarContract.Reminders;
import android.provider.ContactsContract;
import android.util.Log;
/**
* Represents a locally stored calendar, containing Events.
* Communicates with the Android Contacts Provider which uses an SQLite
* database to store the contacts.
*/
public class LocalCalendar extends LocalCollection<Event> {
private static final String TAG = "davdroid.LocalCalendar";
@Getter protected long id;
@Getter protected String url;
protected static String COLLECTION_COLUMN_CTAG = Calendars.CAL_SYNC1;
/* database fields */
@Override
protected Uri entriesURI() {
return syncAdapterURI(Events.CONTENT_URI);
}
protected String entryColumnAccountType() { return Events.ACCOUNT_TYPE; }
protected String entryColumnAccountName() { return Events.ACCOUNT_NAME; }
protected String entryColumnParentID() { return Events.CALENDAR_ID; }
protected String entryColumnID() { return Events._ID; }
protected String entryColumnRemoteName() { return Events._SYNC_ID; }
protected String entryColumnETag() { return Events.SYNC_DATA1; }
protected String entryColumnDirty() { return Events.DIRTY; }
protected String entryColumnDeleted() { return Events.DELETED; }
@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
protected String entryColumnUID() {
return (android.os.Build.VERSION.SDK_INT >= 17) ?
Events.UID_2445 : Events.SYNC_DATA2;
}
/* class methods, constructor */
@SuppressLint("InlinedApi")
public static void create(Account account, ContentResolver resolver, ServerInfo.ResourceInfo info) throws LocalStorageException {
ContentProviderClient client = resolver.acquireContentProviderClient(CalendarContract.AUTHORITY);
if (client == null)
throw new LocalStorageException("No Calendar Provider found (Calendar app disabled?)");
int color = 0xFFC3EA6E; // fallback: "DAVdroid green"
if (info.getColor() != null) {
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);
int color_alpha = m.group(2) != null ? (Integer.parseInt(m.group(2), 16) & 0xFF) : 0xFF;
color = (color_alpha << 24) | color_rgb;
}
}
ContentValues values = new ContentValues();
values.put(Calendars.ACCOUNT_NAME, account.name);
values.put(Calendars.ACCOUNT_TYPE, account.type);
values.put(Calendars.NAME, info.getURL());
values.put(Calendars.CALENDAR_DISPLAY_NAME, info.getTitle());
values.put(Calendars.CALENDAR_COLOR, color);
values.put(Calendars.OWNER_ACCOUNT, account.name);
values.put(Calendars.SYNC_EVENTS, 1);
values.put(Calendars.VISIBLE, 1);
values.put(Calendars.ALLOWED_REMINDERS, Reminders.METHOD_ALERT);
if (info.isReadOnly())
values.put(Calendars.CALENDAR_ACCESS_LEVEL, Calendars.CAL_ACCESS_READ);
else {
values.put(Calendars.CALENDAR_ACCESS_LEVEL, Calendars.CAL_ACCESS_OWNER);
values.put(Calendars.CAN_ORGANIZER_RESPOND, 1);
values.put(Calendars.CAN_MODIFY_TIME_ZONE, 1);
}
if (android.os.Build.VERSION.SDK_INT >= 15) {
values.put(Calendars.ALLOWED_AVAILABILITY, Events.AVAILABILITY_BUSY + "," + Events.AVAILABILITY_FREE + "," + Events.AVAILABILITY_TENTATIVE);
values.put(Calendars.ALLOWED_ATTENDEE_TYPES, Attendees.TYPE_NONE + "," + Attendees.TYPE_OPTIONAL + "," + Attendees.TYPE_REQUIRED + "," + Attendees.TYPE_RESOURCE);
}
if (info.getTimezone() != null)
values.put(Calendars.CALENDAR_TIME_ZONE, info.getTimezone());
Log.i(TAG, "Inserting calendar: " + values.toString() + " -> " + calendarsURI(account).toString());
try {
client.insert(calendarsURI(account), values);
} catch(RemoteException e) {
throw new LocalStorageException(e);
}
}
public static LocalCalendar[] findAll(Account account, ContentProviderClient providerClient) throws RemoteException {
@Cleanup Cursor cursor = providerClient.query(calendarsURI(account),
new String[] { Calendars._ID, Calendars.NAME },
Calendars.DELETED + "=0 AND " + Calendars.SYNC_EVENTS + "=1", null, null);
LinkedList<LocalCalendar> calendars = new LinkedList<LocalCalendar>();
while (cursor != null && cursor.moveToNext())
calendars.add(new LocalCalendar(account, providerClient, cursor.getInt(0), cursor.getString(1)));
return calendars.toArray(new LocalCalendar[0]);
}
public LocalCalendar(Account account, ContentProviderClient providerClient, long id, String url) throws RemoteException {
super(account, providerClient);
this.id = id;
this.url = url;
}
/* collection operations */
@Override
public String getCTag() throws LocalStorageException {
try {
@Cleanup Cursor c = providerClient.query(ContentUris.withAppendedId(calendarsURI(), id),
new String[] { COLLECTION_COLUMN_CTAG }, null, null, null);
if (c.moveToFirst()) {
return c.getString(0);
} else
throw new LocalStorageException("Couldn't query calendar CTag");
} catch(RemoteException e) {
throw new LocalStorageException(e);
}
}
@Override
public void setCTag(String cTag) throws LocalStorageException {
ContentValues values = new ContentValues(1);
values.put(COLLECTION_COLUMN_CTAG, cTag);
try {
providerClient.update(ContentUris.withAppendedId(calendarsURI(), id), values, null, null);
} catch(RemoteException e) {
throw new LocalStorageException(e);
}
}
/* create/update/delete */
public Event newResource(long localID, String resourceName, String eTag) {
return new Event(localID, resourceName, eTag);
}
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)
.build());
}
/* methods for populating the data object from the content provider */
@Override
public void populate(Resource resource) throws LocalStorageException {
Event e = (Event)resource;
try {
@Cleanup Cursor cursor = providerClient.query(ContentUris.withAppendedId(entriesURI(), e.getLocalID()),
new String[] {
/* 0 */ Events.TITLE, Events.EVENT_LOCATION, Events.DESCRIPTION,
/* 3 */ Events.DTSTART, Events.DTEND, Events.EVENT_TIMEZONE, Events.EVENT_END_TIMEZONE, Events.ALL_DAY,
/* 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
}, null, null, null);
if (cursor != null && cursor.moveToNext()) {
e.setUid(cursor.getString(17));
e.setSummary(cursor.getString(0));
e.setLocation(cursor.getString(1));
e.setDescription(cursor.getString(2));
boolean allDay = cursor.getInt(7) != 0;
long tsStart = cursor.getLong(3),
tsEnd = cursor.getLong(4);
String duration = cursor.getString(18);
String tzId = null;
if (allDay) {
e.setDtStart(tsStart, null);
// provide only DTEND and not DURATION for all-day events
if (tsEnd == 0) {
Dur dur = new Dur(duration);
java.util.Date dEnd = dur.getTime(new java.util.Date(tsStart));
tsEnd = dEnd.getTime();
}
e.setDtEnd(tsEnd, null);
} else {
// use the start time zone for the end time, too
// because apps like Samsung Planner allow the user to change "the" time zone but change the start time zone only
tzId = cursor.getString(5);
e.setDtStart(tsStart, tzId);
if (tsEnd != 0)
e.setDtEnd(tsEnd, tzId);
else if (!StringUtils.isEmpty(duration))
e.setDuration(new Duration(new Dur(duration)));
}
// recurrence
try {
String strRRule = cursor.getString(10);
if (!StringUtils.isEmpty(strRRule))
e.setRrule(new RRule(strRRule));
String strRDate = cursor.getString(11);
if (!StringUtils.isEmpty(strRDate)) {
RDate rDate = new RDate();
rDate.setValue(strRDate);
e.setRdate(rDate);
}
String strExRule = cursor.getString(12);
if (!StringUtils.isEmpty(strExRule)) {
ExRule exRule = new ExRule();
exRule.setValue(strExRule);
e.setExrule(exRule);
}
String strExDate = cursor.getString(13);
if (!StringUtils.isEmpty(strExDate)) {
// ignored, see https://code.google.com/p/android/issues/detail?id=21426
ExDate exDate = new ExDate();
exDate.setValue(strExDate);
e.setExdate(exDate);
}
} catch (ParseException ex) {
Log.w(TAG, "Couldn't parse recurrence rules, ignoring", ex);
} catch (IllegalArgumentException ex) {
Log.w(TAG, "Invalid recurrence rules, ignoring", ex);
}
// status
switch (cursor.getInt(8)) {
case Events.STATUS_CONFIRMED:
e.setStatus(Status.VEVENT_CONFIRMED);
break;
case Events.STATUS_TENTATIVE:
e.setStatus(Status.VEVENT_TENTATIVE);
break;
case Events.STATUS_CANCELED:
e.setStatus(Status.VEVENT_CANCELLED);
}
// availability
e.setOpaque(cursor.getInt(19) != Events.AVAILABILITY_FREE);
// attendees
if (cursor.getInt(14) != 0) { // has attendees
try {
e.setOrganizer(new Organizer(new URI("mailto", cursor.getString(15), null)));
} catch (URISyntaxException ex) {
Log.e(TAG, "Error when creating ORGANIZER URI, ignoring", ex);
}
populateAttendees(e);
}
// classification
switch (cursor.getInt(9)) {
case Events.ACCESS_CONFIDENTIAL:
case Events.ACCESS_PRIVATE:
e.setForPublic(false);
break;
case Events.ACCESS_PUBLIC:
e.setForPublic(true);
}
populateReminders(e);
} else
throw new RecordNotFoundException();
} catch(RemoteException ex) {
throw new LocalStorageException(ex);
}
}
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[] {
/* 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);
while (c != null && c.moveToNext()) {
try {
Attendee attendee = new Attendee(new URI("mailto", c.getString(0), null));
ParameterList params = attendee.getParameters();
String cn = c.getString(1);
if (cn != null)
params.add(new Cn(cn));
// type
int type = c.getInt(2);
params.add((type == Attendees.TYPE_RESOURCE) ? CuType.RESOURCE : CuType.INDIVIDUAL);
// role
int relationship = c.getInt(3);
switch (relationship) {
case Attendees.RELATIONSHIP_ORGANIZER:
params.add(Role.CHAIR);
break;
case Attendees.RELATIONSHIP_ATTENDEE:
case Attendees.RELATIONSHIP_PERFORMER:
case Attendees.RELATIONSHIP_SPEAKER:
params.add((type == Attendees.TYPE_REQUIRED) ? Role.REQ_PARTICIPANT : Role.OPT_PARTICIPANT);
break;
case Attendees.RELATIONSHIP_NONE:
params.add(Role.NON_PARTICIPANT);
}
// status
switch (c.getInt(4)) {
case Attendees.ATTENDEE_STATUS_INVITED:
params.add(PartStat.NEEDS_ACTION);
break;
case Attendees.ATTENDEE_STATUS_ACCEPTED:
params.add(PartStat.ACCEPTED);
break;
case Attendees.ATTENDEE_STATUS_DECLINED:
params.add(PartStat.DECLINED);
break;
case Attendees.ATTENDEE_STATUS_TENTATIVE:
params.add(PartStat.TENTATIVE);
break;
}
e.addAttendee(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()
.appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true")
.build();
@Cleanup Cursor c = providerClient.query(remindersUri, new String[] {
/* 0 */ Reminders.MINUTES, Reminders.METHOD
}, Reminders.EVENT_ID + "=?", new String[] { String.valueOf(e.getLocalID()) }, null);
while (c != null && c.moveToNext()) {
VAlarm alarm = new VAlarm(new Dur(0, 0, -c.getInt(0), 0));
PropertyList props = alarm.getProperties();
switch (c.getInt(1)) {
/*case Reminders.METHOD_EMAIL:
props.add(Action.EMAIL);
break;*/
default:
props.add(Action.DISPLAY);
props.add(new Description(e.getSummary()));
}
e.addAlarm(alarm);
}
}
/* content builder methods */
@Override
protected Builder buildEntry(Builder builder, Resource resource) {
Event event = (Event)resource;
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())
.withValue(Events.HAS_ATTENDEE_DATA, event.getAttendees().isEmpty() ? 0 : 1)
.withValue(Events.GUESTS_CAN_INVITE_OTHERS, 1)
.withValue(Events.GUESTS_CAN_MODIFY, 1)
.withValue(Events.GUESTS_CAN_SEE_GUESTS, 1);
boolean recurring = false;
if (event.getRrule() != null) {
recurring = true;
builder = builder.withValue(Events.RRULE, event.getRrule().getValue());
}
if (event.getRdate() != null) {
recurring = true;
builder = builder.withValue(Events.RDATE, event.getRdate().getValue());
}
if (event.getExrule() != null)
builder = builder.withValue(Events.EXRULE, event.getExrule().getValue());
if (event.getExdate() != null)
builder = builder.withValue(Events.EXDATE, event.getExdate().getValue());
// set either DTEND for single-time events or DURATION for recurring events
// because that's the way Android likes it (see docs)
if (recurring) {
// calculate DURATION from start and end date
Duration duration = new Duration(event.getDtStart().getDate(), event.getDtEnd().getDate());
builder = builder.withValue(Events.DURATION, duration.getValue());
} else {
builder = builder
.withValue(Events.DTEND, event.getDtEndInMillis())
.withValue(Events.EVENT_END_TIMEZONE, event.getDtEndTzID());
}
if (event.getSummary() != null)
builder = builder.withValue(Events.TITLE, event.getSummary());
if (event.getLocation() != null)
builder = builder.withValue(Events.EVENT_LOCATION, event.getLocation());
if (event.getDescription() != null)
builder = builder.withValue(Events.DESCRIPTION, event.getDescription());
if (event.getOrganizer() != null && event.getOrganizer().getCalAddress() != null) {
URI organizer = event.getOrganizer().getCalAddress();
if (organizer.getScheme() != null && organizer.getScheme().equalsIgnoreCase("mailto"))
builder = builder.withValue(Events.ORGANIZER, organizer.getSchemeSpecificPart());
}
Status status = event.getStatus();
if (status != null) {
int statusCode = Events.STATUS_TENTATIVE;
if (status == Status.VEVENT_CONFIRMED)
statusCode = Events.STATUS_CONFIRMED;
else if (status == Status.VEVENT_CANCELLED)
statusCode = Events.STATUS_CANCELED;
builder = builder.withValue(Events.STATUS, statusCode);
}
builder = builder.withValue(Events.AVAILABILITY, event.isOpaque() ? Events.AVAILABILITY_BUSY : Events.AVAILABILITY_FREE);
if (event.getForPublic() != null)
builder = builder.withValue(Events.ACCESS_LEVEL, event.getForPublic() ? Events.ACCESS_PUBLIC : Events.ACCESS_PRIVATE);
return builder;
}
@Override
protected void addDataRows(Resource resource, long localID, int backrefIdx) {
Event event = (Event)resource;
for (Attendee attendee : event.getAttendees())
pendingOperations.add(buildAttendee(newDataInsertBuilder(Attendees.CONTENT_URI, Attendees.EVENT_ID, localID, backrefIdx), attendee).build());
for (VAlarm alarm : event.getAlarms())
pendingOperations.add(buildReminder(newDataInsertBuilder(Reminders.CONTENT_URI, Reminders.EVENT_ID, localID, backrefIdx), alarm).build());
}
@Override
protected void removeDataRows(Resource resource) {
Event event = (Event)resource;
pendingOperations.add(ContentProviderOperation.newDelete(syncAdapterURI(Attendees.CONTENT_URI))
.withSelection(Attendees.EVENT_ID + "=?",
new String[] { String.valueOf(event.getLocalID()) }).build());
pendingOperations.add(ContentProviderOperation.newDelete(syncAdapterURI(Reminders.CONTENT_URI))
.withSelection(Reminders.EVENT_ID + "=?",
new String[] { String.valueOf(event.getLocalID()) }).build());
}
@SuppressLint("InlinedApi")
protected Builder buildAttendee(Builder builder, Attendee attendee) {
Uri member = Uri.parse(attendee.getValue());
String email = member.getSchemeSpecificPart();
Cn cn = (Cn)attendee.getParameter(Parameter.CN);
if (cn != null)
builder = builder.withValue(Attendees.ATTENDEE_NAME, cn.getValue());
int type = Attendees.TYPE_NONE;
CuType cutype = (CuType)attendee.getParameter(Parameter.CUTYPE);
if (cutype == CuType.RESOURCE)
type = Attendees.TYPE_RESOURCE;
else {
Role role = (Role)attendee.getParameter(Parameter.ROLE);
int relationship;
if (role == Role.CHAIR)
relationship = Attendees.RELATIONSHIP_ORGANIZER;
else {
relationship = Attendees.RELATIONSHIP_ATTENDEE;
if (role == Role.OPT_PARTICIPANT)
type = Attendees.TYPE_OPTIONAL;
else if (role == Role.REQ_PARTICIPANT)
type = Attendees.TYPE_REQUIRED;
}
builder = builder.withValue(Attendees.ATTENDEE_RELATIONSHIP, relationship);
}
int status = Attendees.ATTENDEE_STATUS_NONE;
PartStat partStat = (PartStat)attendee.getParameter(Parameter.PARTSTAT);
if (partStat == null || partStat == PartStat.NEEDS_ACTION)
status = Attendees.ATTENDEE_STATUS_INVITED;
else if (partStat == PartStat.ACCEPTED)
status = Attendees.ATTENDEE_STATUS_ACCEPTED;
else if (partStat == PartStat.DECLINED)
status = Attendees.ATTENDEE_STATUS_DECLINED;
else if (partStat == PartStat.TENTATIVE)
status = Attendees.ATTENDEE_STATUS_TENTATIVE;
return builder
.withValue(Attendees.ATTENDEE_EMAIL, email)
.withValue(Attendees.ATTENDEE_TYPE, type)
.withValue(Attendees.ATTENDEE_STATUS, status);
}
protected Builder buildReminder(Builder builder, VAlarm alarm) {
int minutes = 0;
Dur duration;
if (alarm.getTrigger() != null && (duration = alarm.getTrigger().getDuration()) != null)
minutes = duration.getDays() * 24*60 + duration.getHours()*60 + duration.getMinutes();
Log.d(TAG, "Adding alarm " + minutes + " min before");
return builder
.withValue(Reminders.METHOD, Reminders.METHOD_ALERT)
.withValue(Reminders.MINUTES, minutes);
}
/* private helper methods */
protected static Uri calendarsURI(Account account) {
return Calendars.CONTENT_URI.buildUpon().appendQueryParameter(Calendars.ACCOUNT_NAME, account.name)
.appendQueryParameter(Calendars.ACCOUNT_TYPE, account.type)
.appendQueryParameter(CalendarContract.CALLER_IS_SYNCADAPTER, "true").build();
}
protected Uri calendarsURI() {
return calendarsURI(account);
}
}

@ -1,361 +0,0 @@
/*******************************************************************************
* Copyright (c) 2014 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;
import android.content.ContentProviderOperation.Builder;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.OperationApplicationException;
import android.database.Cursor;
import android.net.Uri;
import android.os.RemoteException;
import android.provider.CalendarContract;
import android.util.Log;
/**
* Represents a locally-stored synchronizable collection (for instance, the
* address book or a calendar). Manages a CTag that stores the last known
* remote CTag (the remote CTag changes whenever something in the remote collection changes).
*
* @param <T> Subtype of Resource that can be stored in the collection
*/
public abstract class LocalCollection<T extends Resource> {
private static final String TAG = "davdroid.LocalCollection";
protected Account account;
protected ContentProviderClient providerClient;
protected ArrayList<ContentProviderOperation> pendingOperations = new ArrayList<ContentProviderOperation>();
// database fields
/** base Uri of the collection's entries (for instance, Events.CONTENT_URI);
* apply syncAdapterURI() before returning a value */
abstract protected Uri entriesURI();
/** column name of the type of the account the entry belongs to */
abstract protected String entryColumnAccountType();
/** column name of the name of the account the entry belongs to */
abstract protected String entryColumnAccountName();
/** column name of the collection ID the entry belongs to */
abstract protected String entryColumnParentID();
/** column name of an entry's ID */
abstract protected String entryColumnID();
/** column name of an entry's file name on the WebDAV server */
abstract protected String entryColumnRemoteName();
/** column name of an entry's last ETag on the WebDAV server; null if entry hasn't been uploaded yet */
abstract protected String entryColumnETag();
/** column name of an entry's "dirty" flag (managed by content provider) */
abstract protected String entryColumnDirty();
/** column name of an entry's "deleted" flag (managed by content provider) */
abstract protected String entryColumnDeleted();
/** column name of an entry's UID */
abstract protected String entryColumnUID();
LocalCollection(Account account, ContentProviderClient providerClient) {
this.account = account;
this.providerClient = providerClient;
}
// collection operations
/** gets the ID if the collection (for instance, ID of the Android calendar) */
abstract public long getId();
/** gets the CTag of the collection */
abstract public String getCTag() throws LocalStorageException;
/** sets the CTag of the collection */
abstract public void setCTag(String cTag) throws LocalStorageException;
// content provider (= database) querying
/**
* Finds new resources (resources which haven't been uploaded yet).
* New resources are 1) dirty, and 2) don't have an ETag yet.
*
* @return IDs of new resources
* @throws LocalStorageException when the content provider couldn't be queried
*/
public long[] findNew() throws LocalStorageException {
String where = entryColumnDirty() + "=1 AND " + entryColumnETag() + " IS NULL";
if (entryColumnParentID() != null)
where += " AND " + entryColumnParentID() + "=" + String.valueOf(getId());
try {
@Cleanup Cursor cursor = providerClient.query(entriesURI(),
new String[] { entryColumnID() },
where, null, null);
if (cursor == null)
throw new LocalStorageException("Couldn't query new records");
long[] fresh = new long[cursor.getCount()];
for (int idx = 0; cursor.moveToNext(); idx++) {
long id = cursor.getLong(0);
// new record: generate UID + remote file name so that we can upload
T resource = findById(id, false);
resource.initialize();
// write generated UID + remote file name into database
ContentValues values = new ContentValues(2);
values.put(entryColumnUID(), resource.getUid());
values.put(entryColumnRemoteName(), resource.getName());
providerClient.update(ContentUris.withAppendedId(entriesURI(), id), values, null, null);
fresh[idx] = id;
}
return fresh;
} catch(RemoteException ex) {
throw new LocalStorageException(ex);
}
}
/**
* Finds updated resources (resources which have already been uploaded, but have changed locally).
* Updated resources are 1) dirty, and 2) already have an ETag.
*
* @return IDs of updated resources
* @throws LocalStorageException when the content provider couldn't be queried
*/
public long[] findUpdated() throws LocalStorageException {
String where = entryColumnDirty() + "=1 AND " + entryColumnETag() + " IS NOT NULL";
if (entryColumnParentID() != null)
where += " AND " + entryColumnParentID() + "=" + String.valueOf(getId());
try {
@Cleanup Cursor cursor = providerClient.query(entriesURI(),
new String[] { entryColumnID(), entryColumnRemoteName(), entryColumnETag() },
where, null, null);
if (cursor == null)
throw new LocalStorageException("Couldn't query updated records");
long[] updated = new long[cursor.getCount()];
for (int idx = 0; cursor.moveToNext(); idx++)
updated[idx] = cursor.getLong(0);
return updated;
} catch(RemoteException ex) {
throw new LocalStorageException(ex);
}
}
/**
* Finds deleted resources (resources which have been marked for deletion).
* Deleted resources have the "deleted" flag set.
*
* @return IDs of deleted resources
* @throws LocalStorageException when the content provider couldn't be queried
*/
public long[] findDeleted() throws LocalStorageException {
String where = entryColumnDeleted() + "=1";
if (entryColumnParentID() != null)
where += " AND " + entryColumnParentID() + "=" + String.valueOf(getId());
try {
@Cleanup Cursor cursor = providerClient.query(entriesURI(),
new String[] { entryColumnID(), entryColumnRemoteName(), entryColumnETag() },
where, null, null);
if (cursor == null)
throw new LocalStorageException("Couldn't query dirty records");
long deleted[] = new long[cursor.getCount()];
for (int idx = 0; cursor.moveToNext(); idx++)
deleted[idx] = cursor.getLong(0);
return deleted;
} catch(RemoteException ex) {
throw new LocalStorageException(ex);
}
}
/**
* Finds a specific resource by ID.
* @param localID ID of the resource
* @param populate true: populates all data fields (for instance, contact or event details);
* false: only remote file name and ETag are populated
* @return resource with either ID/remote file/name/ETag or all fields populated
* @throws RecordNotFoundException when the resource couldn't be found
* @throws LocalStorageException when the content provider couldn't be queried
*/
public T findById(long localID, boolean populate) throws LocalStorageException {
try {
@Cleanup Cursor cursor = providerClient.query(ContentUris.withAppendedId(entriesURI(), localID),
new String[] { entryColumnRemoteName(), entryColumnETag() }, null, null, null);
if (cursor != null && cursor.moveToNext()) {
T resource = newResource(localID, cursor.getString(0), cursor.getString(1));
if (populate)
populate(resource);
return resource;
} else
throw new RecordNotFoundException();
} catch(RemoteException ex) {
throw new LocalStorageException(ex);
}
}
/**
* Finds a specific resource by remote file name.
* @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
* @return resource with either ID/remote file/name/ETag or all fields populated
* @throws RecordNotFoundException when the resource couldn't be found
* @throws LocalStorageException when the content provider couldn't be queried
*/
public T findByRemoteName(String remoteName, boolean populate) throws LocalStorageException {
try {
@Cleanup Cursor cursor = providerClient.query(entriesURI(),
new String[] { entryColumnID(), entryColumnRemoteName(), entryColumnETag() },
entryColumnRemoteName() + "=?", new String[] { remoteName }, null);
if (cursor != null && cursor.moveToNext()) {
T resource = newResource(cursor.getLong(0), cursor.getString(1), cursor.getString(2));
if (populate)
populate(resource);
return resource;
} else
throw new RecordNotFoundException();
} catch(RemoteException ex) {
throw new LocalStorageException(ex);
}
}
/** populates all data fields from the content provider */
public abstract void populate(Resource record) throws LocalStorageException;
// create/update/delete
/**
* Creates a new resource object in memory. No content provider operations involved.
* @param localID the ID of the resource
* @param resourceName the (remote) file name of the resource
* @param ETag of the resource
* @return the new resource object */
abstract public T newResource(long localID, String resourceName, String eTag);
/** Enqueues adding the resource (including all data) to the local collection. Requires commit(). */
public void add(Resource resource) {
int idx = pendingOperations.size();
pendingOperations.add(
buildEntry(ContentProviderOperation.newInsert(entriesURI()), resource)
.withYieldAllowed(true)
.build());
addDataRows(resource, -1, idx);
}
/** Enqueues updating an existing resource in the local collection. The resource will be found by
* the remote file name and all data will be updated. Requires commit(). */
public void updateByRemoteName(Resource remoteResource) throws LocalStorageException {
T localResource = findByRemoteName(remoteResource.getName(), false);
pendingOperations.add(
buildEntry(ContentProviderOperation.newUpdate(ContentUris.withAppendedId(entriesURI(), localResource.getLocalID())), remoteResource)
.withValue(entryColumnETag(), remoteResource.getETag())
.withYieldAllowed(true)
.build());
removeDataRows(localResource);
addDataRows(remoteResource, localResource.getLocalID(), -1);
}
/** Enqueues deleting a resource from the local collection. Requires commit(). */
public void delete(Resource resource) {
pendingOperations.add(ContentProviderOperation
.newDelete(ContentUris.withAppendedId(entriesURI(), resource.getLocalID()))
.withYieldAllowed(true)
.build());
}
/**
* Enqueues deleting all resources except the give ones from the local collection. Requires commit().
* @param remoteResources resources with these remote file names will be kept
*/
public abstract void deleteAllExceptRemoteNames(Resource[] remoteResources);
/** 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);
ContentValues values = new ContentValues(1);
values.put(entryColumnETag(), eTag);
try {
providerClient.update(ContentUris.withAppendedId(entriesURI(), res.getLocalID()), values, null, new String[] {});
} catch (RemoteException e) {
throw new LocalStorageException(e);
}
}
/** Enqueues removing the dirty flag from a locally-stored resource. Requires commit(). */
public void clearDirty(Resource resource) {
pendingOperations.add(ContentProviderOperation
.newUpdate(ContentUris.withAppendedId(entriesURI(), resource.getLocalID()))
.withValue(entryColumnDirty(), 0)
.build());
}
/** Commits enqueued operations to the content provider (for batch operations). */
public void commit() throws LocalStorageException {
if (!pendingOperations.isEmpty())
try {
Log.d(TAG, "Committing " + pendingOperations.size() + " operations");
providerClient.applyBatch(pendingOperations);
pendingOperations.clear();
} catch (RemoteException ex) {
throw new LocalStorageException(ex);
} catch(OperationApplicationException ex) {
throw new LocalStorageException(ex);
}
}
// helpers
protected void queueOperation(Builder builder) {
if (builder != null)
pendingOperations.add(builder.build());
}
/** Appends account type, name and CALLER_IS_SYNCADAPTER to an Uri. */
protected Uri syncAdapterURI(Uri baseURI) {
return baseURI.buildUpon()
.appendQueryParameter(entryColumnAccountType(), account.type)
.appendQueryParameter(entryColumnAccountName(), account.name)
.appendQueryParameter(CalendarContract.CALLER_IS_SYNCADAPTER, "true")
.build();
}
protected Builder newDataInsertBuilder(Uri dataUri, String refFieldName, long raw_ref_id, Integer backrefIdx) {
Builder builder = ContentProviderOperation.newInsert(syncAdapterURI(dataUri));
if (backrefIdx != -1)
return builder.withValueBackReference(refFieldName, backrefIdx);
else
return builder.withValue(refFieldName, raw_ref_id);
}
// content builders
/**
* Builds the main entry (for instance, a ContactsContract.RawContacts row) from a resource.
* The entry is built for insertion to the location identified by entriesURI().
*
* @param builder Builder to be extended by all resource data that can be stored without extra data rows.
*/
protected abstract Builder buildEntry(Builder builder, Resource resource);
/** Enqueues adding extra data rows of the resource to the local collection. */
protected abstract void addDataRows(Resource resource, long localID, int backrefIdx);
/** Enqueues removing all extra data rows of the resource from the local collection. */
protected abstract void removeDataRows(Resource resource);
}

@ -1,31 +0,0 @@
/*******************************************************************************
* Copyright (c) 2014 Ricki Hirner (bitfire web engineering).
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Public License v3.0
* which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/gpl.html
******************************************************************************/
package at.bitfire.davdroid.resource;
public class LocalStorageException extends Exception {
private static final long serialVersionUID = -7787658815291629529L;
private static final String detailMessage = "Couldn't access local content provider";
public LocalStorageException(String detailMessage, Throwable throwable) {
super(detailMessage, throwable);
}
public LocalStorageException(String detailMessage) {
super(detailMessage);
}
public LocalStorageException(Throwable throwable) {
super(detailMessage, throwable);
}
public LocalStorageException() {
super(detailMessage);
}
}

@ -1,28 +0,0 @@
/*******************************************************************************
* Copyright (c) 2014 Ricki Hirner (bitfire web engineering).
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Public License v3.0
* which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/gpl.html
******************************************************************************/
package at.bitfire.davdroid.resource;
/**
* Thrown when a local record (for instance, Contact with ID 12345) should be read
* but could not be found.
*/
public class RecordNotFoundException extends LocalStorageException {
private static final long serialVersionUID = 4961024282198632578L;
private static final String detailMessage = "Record not found in local content provider";
RecordNotFoundException(Throwable ex) {
super(detailMessage, ex);
}
RecordNotFoundException() {
super(detailMessage);
}
}

@ -1,181 +0,0 @@
/*******************************************************************************
* Copyright (c) 2014 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.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
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.webdav.DavException;
import at.bitfire.davdroid.webdav.DavMultiget;
import at.bitfire.davdroid.webdav.DavNoContentException;
import at.bitfire.davdroid.webdav.HttpException;
import at.bitfire.davdroid.webdav.HttpPropfind;
import at.bitfire.davdroid.webdav.WebDavResource;
import at.bitfire.davdroid.webdav.WebDavResource.PutMode;
import ch.boye.httpclientandroidlib.impl.client.CloseableHttpClient;
import ezvcard.io.text.VCardParseException;
/**
* Represents a remotely stored synchronizable collection (collection as in
* WebDAV terminology).
*
* @param <T> Subtype of Resource that can be stored in the collection
*/
public abstract class RemoteCollection<T extends Resource> {
private static final String TAG = "davdroid.RemoteCollection";
CloseableHttpClient httpClient;
@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 MalformedURLException {
this.httpClient = httpClient;
collection = new WebDavResource(httpClient, new URL(baseURL), user, password, preemptiveAuth);
}
/* collection operations */
public String getCTag() throws URISyntaxException, IOException, HttpException {
try {
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 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())
resources.add(newResourceSkeleton(member.getName(), member.getETag()));
}
return resources.toArray(new Resource[0]);
}
@SuppressWarnings("unchecked")
public Resource[] multiGet(Resource[] resources) throws URISyntaxException, IOException, DavException, HttpException {
try {
if (resources.length == 1)
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);
foundResources.add(resource);
} else
Log.e(TAG, "Ignoring entity without content");
} catch (InvalidResourceException e) {
Log.e(TAG, "Ignoring unparseable entity in multi-response", e);
}
}
return foundResources.toArray(new Resource[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 URISyntaxException, IOException, HttpException, DavException, InvalidResourceException {
WebDavResource member = new WebDavResource(collection, resource.getName());
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);
try {
resource.parseEntity(is);
} catch(VCardParseException e) {
throw new InvalidResourceException(e);
}
return resource;
}
// returns ETag of the created resource, if returned by server
public String add(Resource res) throws URISyntaxException, IOException, HttpException, 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 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 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;
}
}

@ -1,46 +0,0 @@
/*******************************************************************************
* Copyright (c) 2014 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 lombok.Getter;
import lombok.Setter;
import lombok.ToString;
/**
* Represents a resource that can be contained in a LocalCollection or RemoteCollection
* for synchronization by WebDAV.
*/
@ToString
public abstract class Resource {
@Getter @Setter protected String name, ETag;
@Getter @Setter protected String uid;
@Getter protected long localID;
public Resource(String name, String ETag) {
this.name = name;
this.ETag = ETag;
}
public Resource(long localID, String name, String ETag) {
this(name, ETag);
this.localID = localID;
}
/** initializes UID and remote file name (required for first upload) */
public abstract void initialize();
/** fills the resource data from an input stream (for instance, .vcf file for Contact) */
public abstract void parseEntity(InputStream entity) throws IOException, InvalidResourceException;
/** writes the resource data to an output stream (for instance, .vcf file for Contact) */
public abstract ByteArrayOutputStream toEntity() throws IOException;
}

@ -1,68 +0,0 @@
/*******************************************************************************
* Copyright (c) 2014 Ricki Hirner (bitfire web engineering).
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Public License v3.0
* which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/gpl.html
******************************************************************************/
package at.bitfire.davdroid.resource;
import java.io.Serializable;
import java.net.URI;
import java.util.LinkedList;
import java.util.List;
import ezvcard.VCardVersion;
import lombok.Data;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor(suppressConstructorProperties=true)
@Data
public class ServerInfo implements Serializable {
private static final long serialVersionUID = 6744847358282980437L;
enum Scheme {
HTTP, HTTPS, MAILTO
}
final private URI baseURI;
final private String userName, password;
final boolean authPreemptive;
private String errorMessage;
private boolean calDAV = false, cardDAV = false;
private List<ResourceInfo>
addressBooks = new LinkedList<ResourceInfo>(),
calendars = new LinkedList<ResourceInfo>();
public boolean hasEnabledCalendars() {
for (ResourceInfo calendar : calendars)
if (calendar.enabled)
return true;
return false;
}
@RequiredArgsConstructor(suppressConstructorProperties=true)
@Data
public static class ResourceInfo implements Serializable {
private static final long serialVersionUID = -5516934508229552112L;
public enum Type {
ADDRESS_BOOK,
CALENDAR
}
boolean enabled = false;
final Type type;
final boolean readOnly;
final String URL, title, description, color;
VCardVersion vCardVersion;
String timezone;
}
}

@ -1,86 +0,0 @@
/*******************************************************************************
* Copyright (c) 2014 Ricki Hirner (bitfire web engineering).
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Public License v3.0
* which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/gpl.html
******************************************************************************/
package at.bitfire.davdroid.syncadapter;
import android.accounts.AbstractAccountAuthenticator;
import android.accounts.Account;
import android.accounts.AccountAuthenticatorResponse;
import android.accounts.AccountManager;
import android.accounts.NetworkErrorException;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.os.IBinder;
public class AccountAuthenticatorService extends Service {
private static AccountAuthenticator accountAuthenticator;
private AccountAuthenticator getAuthenticator() {
if (accountAuthenticator != null)
return accountAuthenticator;
return accountAuthenticator = new AccountAuthenticator(this);
}
@Override
public IBinder onBind(Intent intent) {
if (intent.getAction().equals(android.accounts.AccountManager.ACTION_AUTHENTICATOR_INTENT))
return getAuthenticator().getIBinder();
return null;
}
private static class AccountAuthenticator extends AbstractAccountAuthenticator {
Context context;
public AccountAuthenticator(Context context) {
super(context);
this.context = context;
}
@Override
public Bundle addAccount(AccountAuthenticatorResponse response, String accountType, String authTokenType,
String[] requiredFeatures, Bundle options) throws NetworkErrorException {
Intent intent = new Intent(context, AddAccountActivity.class);
intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response);
Bundle bundle = new Bundle();
bundle.putParcelable(AccountManager.KEY_INTENT, intent);
return bundle;
}
@Override
public Bundle confirmCredentials(AccountAuthenticatorResponse response, Account account, Bundle options) throws NetworkErrorException {
return null;
}
@Override
public Bundle editProperties(AccountAuthenticatorResponse response, String accountType) {
return null;
}
@Override
public Bundle getAuthToken(AccountAuthenticatorResponse response, Account account, String authTokenType, Bundle options) throws NetworkErrorException {
return null;
}
@Override
public String getAuthTokenLabel(String authTokenType) {
return null;
}
@Override
public Bundle hasFeatures(AccountAuthenticatorResponse response, Account account, String[] features) throws NetworkErrorException {
return null;
}
@Override
public Bundle updateCredentials(AccountAuthenticatorResponse response, Account account, String authTokenType, Bundle options) throws NetworkErrorException {
return null;
}
}
}

@ -1,147 +0,0 @@
/*******************************************************************************
* Copyright (c) 2014 Ricki Hirner (bitfire web engineering).
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Public License v3.0
* which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/gpl.html
******************************************************************************/
package at.bitfire.davdroid.syncadapter;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.app.Fragment;
import android.content.ContentResolver;
import android.os.Bundle;
import android.provider.CalendarContract;
import android.provider.ContactsContract;
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 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;
public class AccountDetailsFragment extends Fragment implements TextWatcher {
public static final String KEY_SERVER_INFO = "server_info";
ServerInfo serverInfo;
EditText editAccountName;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View v = inflater.inflate(R.layout.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())
textAccountNameInfo.setVisibility(View.GONE);
setHasOptionsMenu(true);
return v;
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
inflater.inflate(R.menu.account_details, menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.add_account:
addAccount();
break;
default:
return false;
}
return true;
}
// actions
void addAccount() {
ServerInfo serverInfo = (ServerInfo)getArguments().getSerializable(KEY_SERVER_INFO);
String accountName = editAccountName.getText().toString();
AccountManager accountManager = AccountManager.get(getActivity());
Account account = new Account(accountName, Constants.ACCOUNT_TYPE);
Bundle userData = AccountSettings.createBundle(serverInfo);
boolean syncContacts = false;
for (ServerInfo.ResourceInfo addressBook : serverInfo.getAddressBooks())
if (addressBook.isEnabled()) {
ContentResolver.setIsSyncable(account, ContactsContract.AUTHORITY, 1);
syncContacts = true;
continue;
}
if (syncContacts) {
ContentResolver.setIsSyncable(account, ContactsContract.AUTHORITY, 1);
ContentResolver.setSyncAutomatically(account, ContactsContract.AUTHORITY, true);
} else
ContentResolver.setIsSyncable(account, ContactsContract.AUTHORITY, 0);
if (accountManager.addAccountExplicitly(account, serverInfo.getPassword(), userData)) {
// account created, now create calendars
boolean syncCalendars = false;
for (ServerInfo.ResourceInfo calendar : serverInfo.getCalendars())
if (calendar.isEnabled())
try {
LocalCalendar.create(account, getActivity().getContentResolver(), calendar);
syncCalendars = true;
} catch (LocalStorageException e) {
Toast.makeText(getActivity(), "Couldn't create calendar(s): " + e.getMessage(), Toast.LENGTH_LONG).show();
}
if (syncCalendars) {
ContentResolver.setIsSyncable(account, CalendarContract.AUTHORITY, 1);
ContentResolver.setSyncAutomatically(account, CalendarContract.AUTHORITY, true);
} else
ContentResolver.setIsSyncable(account, CalendarContract.AUTHORITY, 0);
getActivity().finish();
} else
Toast.makeText(getActivity(), "Couldn't create account (account with this name already existing?)", Toast.LENGTH_LONG).show();
}
// input validation
@Override
public void onPrepareOptionsMenu(Menu menu) {
boolean ok = false;
ok = editAccountName.getText().length() > 0;
MenuItem item = menu.findItem(R.id.add_account);
item.setEnabled(ok);
}
@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) {
}
}

@ -1,183 +0,0 @@
/*******************************************************************************
* Copyright (c) 2014 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.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.provider.CalendarContract;
import android.provider.CalendarContract.Calendars;
import android.util.Log;
import at.bitfire.davdroid.resource.ServerInfo;
import ezvcard.VCardVersion;
public class AccountSettings {
private final static String TAG = "davdroid.AccountSettings";
private final static int CURRENT_VERSION = 1;
private final static String
KEY_SETTINGS_VERSION = "version",
KEY_USERNAME = "user_name",
KEY_AUTH_PREEMPTIVE = "auth_preemptive",
KEY_ADDRESSBOOK_URL = "addressbook_url",
KEY_ADDRESSBOOK_CTAG = "addressbook_ctag",
KEY_ADDRESSBOOK_VCARD_VERSION = "addressbook_vcard_version";
Context context;
AccountManager accountManager;
Account account;
public AccountSettings(Context context, Account account) {
this.context = context;
this.account = account;
accountManager = AccountManager.get(context);
synchronized(AccountSettings.class) {
int version = 0;
try {
version = Integer.parseInt(accountManager.getUserData(account, KEY_SETTINGS_VERSION));
} catch(NumberFormatException e) {
}
if (version < CURRENT_VERSION)
update(version);
}
}
public static Bundle createBundle(ServerInfo serverInfo) {
Bundle bundle = new Bundle();
bundle.putString(KEY_SETTINGS_VERSION, String.valueOf(CURRENT_VERSION));
bundle.putString(KEY_USERNAME, serverInfo.getUserName());
bundle.putString(KEY_AUTH_PREEMPTIVE, Boolean.toString(serverInfo.isAuthPreemptive()));
for (ServerInfo.ResourceInfo addressBook : serverInfo.getAddressBooks())
if (addressBook.isEnabled()) {
bundle.putString(KEY_ADDRESSBOOK_URL, addressBook.getURL());
bundle.putString(KEY_ADDRESSBOOK_VCARD_VERSION, addressBook.getVCardVersion().getVersion());
continue;
}
return bundle;
}
// general settings
public String getUserName() {
return accountManager.getUserData(account, KEY_USERNAME);
}
public String getPassword() {
return accountManager.getPassword(account);
}
public boolean getPreemptiveAuth() {
return Boolean.parseBoolean(accountManager.getUserData(account, KEY_AUTH_PREEMPTIVE));
}
// address book (CardDAV) settings
public String getAddressBookURL() {
return accountManager.getUserData(account, KEY_ADDRESSBOOK_URL);
}
public String getAddressBookCTag() {
return accountManager.getUserData(account, KEY_ADDRESSBOOK_CTAG);
}
public void setAddressBookCTag(String cTag) {
accountManager.setUserData(account, KEY_ADDRESSBOOK_CTAG, cTag);
}
public VCardVersion getAddressBookVCardVersion() {
VCardVersion version = VCardVersion.V3_0;
String versionStr = accountManager.getUserData(account, KEY_ADDRESSBOOK_VCARD_VERSION);
if (versionStr != null)
version = VCardVersion.valueOfByStr(versionStr);
return version;
}
// update from previous account settings
private void update(int fromVersion) {
Log.i(TAG, "Account settings must be updated from v" + fromVersion + " to v" + CURRENT_VERSION);
for (int toVersion = CURRENT_VERSION; toVersion > fromVersion; toVersion--)
update(fromVersion, toVersion);
}
private void update(int fromVersion, int toVersion) {
Log.i(TAG, "Updating account settings from v" + fromVersion + " to " + toVersion);
try {
if (fromVersion == 0 && toVersion == 1)
update_0_1();
else
Log.wtf(TAG, "Don't know how to update settings from v" + fromVersion + " to v" + toVersion);
} catch(Exception e) {
Log.e(TAG, "Couldn't update account settings (DAVdroid will probably crash)!", e);
}
}
private void update_0_1() throws URISyntaxException {
String v0_principalURL = accountManager.getUserData(account, "principal_url"),
v0_addressBookPath = accountManager.getUserData(account, "addressbook_path");
Log.d(TAG, "Old principal URL = " + v0_principalURL);
Log.d(TAG, "Old address book path = " + v0_addressBookPath);
URI principalURI = new URI(v0_principalURL);
// update address book
if (v0_addressBookPath != null) {
String addressBookURL = principalURI.resolve(v0_addressBookPath).toASCIIString();
Log.d(TAG, "New address book URL = " + addressBookURL);
accountManager.setUserData(account, "addressbook_url", addressBookURL);
}
// update calendars
ContentResolver resolver = context.getContentResolver();
Uri calendars = Calendars.CONTENT_URI.buildUpon()
.appendQueryParameter(Calendars.ACCOUNT_NAME, account.name)
.appendQueryParameter(Calendars.ACCOUNT_TYPE, account.type)
.appendQueryParameter(CalendarContract.CALLER_IS_SYNCADAPTER, "true").build();
@Cleanup Cursor cursor = resolver.query(calendars, new String[] { Calendars._ID, Calendars.NAME }, null, null, null);
while (cursor != null && cursor.moveToNext()) {
int id = cursor.getInt(0);
String v0_path = cursor.getString(1),
v1_url = principalURI.resolve(v0_path).toASCIIString();
Log.d(TAG, "Updating calendar #" + id + " name: " + v0_path + " -> " + v1_url);
Uri calendar = ContentUris.appendId(Calendars.CONTENT_URI.buildUpon()
.appendQueryParameter(Calendars.ACCOUNT_NAME, account.name)
.appendQueryParameter(Calendars.ACCOUNT_TYPE, account.type)
.appendQueryParameter(CalendarContract.CALLER_IS_SYNCADAPTER, "true"), id).build();
ContentValues newValues = new ContentValues(1);
newValues.put(Calendars.NAME, v1_url);
if (resolver.update(calendar, newValues, null, null) != 1)
Log.e(TAG, "Number of modified calendars != 1");
}
Log.d(TAG, "Cleaning old principal URL and address book path");
accountManager.setUserData(account, "principal_url", null);
accountManager.setUserData(account, "addressbook_path", null);
Log.d(TAG, "Updated settings successfully!");
accountManager.setUserData(account, KEY_SETTINGS_VERSION, "1");
}
}

@ -1,46 +0,0 @@
/*******************************************************************************
* Copyright (c) 2014 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.app.Activity;
import android.content.Intent;
import android.net.Uri;
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;
public class AddAccountActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.add_account);
if (savedInstanceState == null) { // first call
getFragmentManager().beginTransaction()
.add(R.id.fragment_container, new LoginTypeFragment(), "login_type")
.commit();
}
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.add_account, menu);
return true;
}
public void showHelp(MenuItem item) {
startActivityForResult(new Intent(Intent.ACTION_VIEW, Uri.parse(Constants.WEB_URL_HELP)), 0);
}
}

@ -1,81 +0,0 @@
/*******************************************************************************
* Copyright (c) 2014 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.MalformedURLException;
import java.util.HashMap;
import java.util.Map;
import android.accounts.Account;
import android.app.Service;
import android.content.ContentProviderClient;
import android.content.Context;
import android.content.Intent;
import android.os.IBinder;
import android.os.RemoteException;
import android.util.Log;
import at.bitfire.davdroid.resource.CalDavCalendar;
import at.bitfire.davdroid.resource.LocalCalendar;
import at.bitfire.davdroid.resource.LocalCollection;
import at.bitfire.davdroid.resource.RemoteCollection;
public class CalendarsSyncAdapterService extends Service {
private static SyncAdapter syncAdapter;
@Override
public void onCreate() {
if (syncAdapter == null)
syncAdapter = new SyncAdapter(getApplicationContext());
}
@Override
public void onDestroy() {
syncAdapter.close();
syncAdapter = null;
}
@Override
public IBinder onBind(Intent intent) {
return syncAdapter.getSyncAdapterBinder();
}
private static class SyncAdapter extends DavSyncAdapter {
private final static String TAG = "davdroid.CalendarsSyncAdapter";
private SyncAdapter(Context context) {
super(context);
}
@Override
protected Map<LocalCollection<?>, RemoteCollection<?>> getSyncPairs(Account account, ContentProviderClient provider) {
AccountSettings settings = new AccountSettings(getContext(), account);
String userName = settings.getUserName(),
password = settings.getPassword();
boolean preemptive = settings.getPreemptiveAuth();
try {
Map<LocalCollection<?>, RemoteCollection<?>> map = new HashMap<LocalCollection<?>, RemoteCollection<?>>();
for (LocalCalendar calendar : LocalCalendar.findAll(account, provider)) {
RemoteCollection<?> dav = new CalDavCalendar(httpClient, calendar.getUrl(), userName, password, preemptive);
map.put(calendar, dav);
}
return map;
} catch (RemoteException ex) {
Log.e(TAG, "Couldn't find local calendars", ex);
} catch (MalformedURLException ex) {
Log.e(TAG, "Couldn't build calendar URI", ex);
}
return null;
}
}
}

@ -1,82 +0,0 @@
/*******************************************************************************
* Copyright (c) 2014 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.MalformedURLException;
import java.util.HashMap;
import java.util.Map;
import android.accounts.Account;
import android.app.Service;
import android.content.ContentProviderClient;
import android.content.Context;
import android.content.Intent;
import android.os.IBinder;
import android.util.Log;
import at.bitfire.davdroid.resource.CardDavAddressBook;
import at.bitfire.davdroid.resource.LocalAddressBook;
import at.bitfire.davdroid.resource.LocalCollection;
import at.bitfire.davdroid.resource.RemoteCollection;
public class ContactsSyncAdapterService extends Service {
private static ContactsSyncAdapter syncAdapter;
@Override
public void onCreate() {
if (syncAdapter == null)
syncAdapter = new ContactsSyncAdapter(getApplicationContext());
}
@Override
public void onDestroy() {
syncAdapter.close();
syncAdapter = null;
}
@Override
public IBinder onBind(Intent intent) {
return syncAdapter.getSyncAdapterBinder();
}
private static class ContactsSyncAdapter extends DavSyncAdapter {
private final static String TAG = "davdroid.ContactsSyncAdapter";
private ContactsSyncAdapter(Context context) {
super(context);
}
@Override
protected Map<LocalCollection<?>, RemoteCollection<?>> getSyncPairs(Account account, ContentProviderClient provider) {
AccountSettings settings = new AccountSettings(getContext(), account);
String userName = settings.getUserName(),
password = settings.getPassword();
boolean preemptive = settings.getPreemptiveAuth();
String addressBookURL = settings.getAddressBookURL();
if (addressBookURL == null)
return null;
try {
LocalCollection<?> database = new LocalAddressBook(account, provider, settings);
RemoteCollection<?> dav = new CardDavAddressBook(httpClient, addressBookURL, userName, password, preemptive);
Map<LocalCollection<?>, RemoteCollection<?>> map = new HashMap<LocalCollection<?>, RemoteCollection<?>>();
map.put(database, dav);
return map;
} catch (MalformedURLException ex) {
Log.e(TAG, "Couldn't build address book URI", ex);
}
return null;
}
}
}

@ -1,166 +0,0 @@
/*******************************************************************************
* Copyright (c) 2014 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.net.URISyntaxException;
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.content.AbstractThreadedSyncAdapter;
import android.content.ContentProviderClient;
import android.content.ContentResolver;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.SyncResult;
import android.os.AsyncTask;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.provider.Settings;
import android.util.Log;
import at.bitfire.davdroid.Constants;
import at.bitfire.davdroid.resource.LocalCollection;
import at.bitfire.davdroid.resource.LocalStorageException;
import at.bitfire.davdroid.resource.RemoteCollection;
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;
public abstract class DavSyncAdapter extends AbstractThreadedSyncAdapter implements Closeable {
private final static String TAG = "davdroid.DavSyncAdapter";
@Getter private static String androidID;
protected AccountManager accountManager;
/* We use one static httpClient for
* - all sync adapters (CalendarsSyncAdapter, ContactsSyncAdapter)
* - and all threads (= accounts) of each sync adapter
* so that HttpClient's threaded pool management can do its best.
*/
protected static CloseableHttpClient httpClient;
/* One static read/write lock pair for the static httpClient:
* Use the READ lock when httpClient will only be called (to prevent it from being unset while being used).
* Use the WRITE lock when httpClient will be modified (set/unset). */
private final static ReentrantReadWriteLock httpClientLock = new ReentrantReadWriteLock();
public DavSyncAdapter(Context context) {
super(context, true);
synchronized(this) {
if (androidID == null)
androidID = Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID);
}
accountManager = AccountManager.get(context);
}
@Override
public void close() {
Log.d(TAG, "Closing httpClient");
// may be called from a GUI thread, so we need an AsyncTask
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
try {
httpClientLock.writeLock().lock();
if (httpClient != null) {
httpClient.close();
httpClient = null;
}
httpClientLock.writeLock().unlock();
} catch (IOException e) {
Log.w(TAG, "Couldn't close HTTP client", e);
}
return null;
}
}.execute();
}
protected abstract Map<LocalCollection<?>, RemoteCollection<?>> getSyncPairs(Account account, ContentProviderClient provider);
@Override
public void onPerformSync(Account account, Bundle extras, String authority, ContentProviderClient provider, SyncResult syncResult) {
Log.i(TAG, "Performing sync for authority " + authority);
// set class loader for iCal4j ResourceLoader
Thread.currentThread().setContextClassLoader(getContext().getClassLoader());
// create httpClient, if necessary
httpClientLock.writeLock().lock();
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)
);
}
// 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();
// TODO use VCard 4.0 if possible
AccountSettings accountSettings = new AccountSettings(getContext(), account);
Log.d(TAG, "Server supports VCard version " + accountSettings.getAddressBookVCardVersion());
try {
// get local <-> remote collection pairs
Map<LocalCollection<?>, RemoteCollection<?>> syncCollections = getSyncPairs(account, provider);
if (syncCollections == null)
Log.i(TAG, "Nothing to synchronize");
else
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);
} catch (HttpException ex) {
if (ex.getCode() == HttpStatus.SC_UNAUTHORIZED) {
Log.e(TAG, "HTTP Unauthorized " + ex.getCode(), ex);
syncResult.stats.numAuthExceptions++;
} else if (ex.isClientError()) {
Log.e(TAG, "Hard HTTP error " + ex.getCode(), ex);
syncResult.stats.numParseExceptions++;
} else {
Log.w(TAG, "Soft HTTP error " + ex.getCode() + " (Android will try again later)", ex);
syncResult.stats.numIoExceptions++;
}
} catch (LocalStorageException ex) {
syncResult.databaseError = true;
Log.e(TAG, "Local storage (content provider) exception", ex);
} catch (IOException ex) {
syncResult.stats.numIoExceptions++;
Log.e(TAG, "I/O error (Android will try again later)", ex);
} catch (URISyntaxException ex) {
Log.e(TAG, "Invalid URI (file name) syntax", ex);
}
} finally {
// allow httpClient shutdown
httpClientLock.readLock().unlock();
}
Log.i(TAG, "Sync complete for " + authority);
}
}

@ -1,54 +0,0 @@
/*******************************************************************************
* Copyright (c) 2014 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.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.preference.PreferenceFragment;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import at.bitfire.davdroid.R;
public class GeneralSettingsActivity extends Activity {
final static String URL_REPORT_ISSUE = "https://github.com/bitfireAT/davdroid/blob/master/CONTRIBUTING.md#reporting-issues";
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getFragmentManager().beginTransaction()
.replace(android.R.id.content, new GeneralSettingsFragment())
.commit();
}
public void reportIssue(MenuItem item) {
startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(URL_REPORT_ISSUE)));
}
public static class GeneralSettingsFragment extends PreferenceFragment {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getPreferenceManager().setSharedPreferencesMode(Context.MODE_MULTI_PROCESS);
addPreferencesFromResource(R.xml.general_settings);
setHasOptionsMenu(true);
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
inflater.inflate(R.menu.debug_settings, menu);
}
}
}

@ -1,111 +0,0 @@
/*******************************************************************************
* Copyright (c) 2014 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 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 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.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 passwordOk = editPassword.getText().length() > 0,
emailOk = false;
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) {
}
}

@ -1,57 +0,0 @@
/*******************************************************************************
* Copyright (c) 2014 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.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.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.fragment_container, loginFragment)
.addToBackStack(null)
.commitAllowingStateLoss();
return true;
default:
return false;
}
}
}

@ -1,149 +0,0 @@
/*******************************************************************************
* Copyright (c) 2014 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 org.apache.commons.lang.StringUtils;
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 at.bitfire.davdroid.R;
import at.bitfire.davdroid.URLUtils;
public class LoginURLFragment extends Fragment implements TextWatcher {
protected String scheme;
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.login_url, container, false);
// protocol selection spinner
textHttpWarning = (TextView) v.findViewById(R.id.http_warning);
Spinner spnrScheme = (Spinner) v.findViewById(R.id.login_scheme);
spnrScheme.setOnItemSelectedListener(new OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
scheme = parent.getAdapter().getItem(position).toString();
textHttpWarning.setVisibility(scheme.equals("https://") ? View.GONE : View.VISIBLE);
}
@Override
public void onNothingSelected(AdapterView<?> parent) {
scheme = null;
}
});
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();
String host_path = editBaseURI.getText().toString();
args.putString(QueryServerDialogFragment.EXTRA_BASE_URI, URLUtils.sanitize(scheme + host_path));
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;
}
// input validation
@Override
public void onPrepareOptionsMenu(Menu menu) {
boolean ok =
editUserName.getText().length() > 0 &&
editPassword.getText().length() > 0;
if (ok)
// check host name
try {
URI uri = new URI(URLUtils.sanitize(scheme + editBaseURI.getText().toString()));
if (StringUtils.isBlank(uri.getHost()))
ok = false;
} catch (URISyntaxException e) {
ok = false;
}
MenuItem item = menu.findItem(R.id.next);
item.setEnabled(ok);
}
@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) {
}
}

@ -1,129 +0,0 @@
/*******************************************************************************
* Copyright (c) 2014 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.net.URISyntaxException;
import lombok.Cleanup;
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 at.bitfire.davdroid.R;
import at.bitfire.davdroid.resource.DavResourceFinder;
import at.bitfire.davdroid.resource.ServerInfo;
import at.bitfire.davdroid.webdav.DavException;
import ch.boye.httpclientandroidlib.HttpException;
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) {
View v = inflater.inflate(R.layout.query_server, container, false);
return v;
}
@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.fragment_container, 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) {
serverInfo.setErrorMessage(getContext().getString(R.string.exception_io, e.getLocalizedMessage()));
} 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,161 +0,0 @@
/*******************************************************************************
* Copyright (c) 2014 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 lombok.Getter;
import android.annotation.SuppressLint;
import android.content.Context;
import android.text.Html;
import android.view.LayoutInflater;
import android.view.View;
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 at.bitfire.davdroid.resource.ServerInfo.ResourceInfo.Type;
public class SelectCollectionsAdapter extends BaseAdapter implements ListAdapter {
final static int TYPE_ADDRESS_BOOKS_HEADING = 0,
TYPE_ADDRESS_BOOKS_ROW = 1,
TYPE_CALENDARS_HEADING = 2,
TYPE_CALENDARS_ROW = 3;
protected Context context;
protected ServerInfo serverInfo;
@Getter protected int nAddressBooks, nCalendars;
public SelectCollectionsAdapter(Context context, ServerInfo serverInfo) {
this.context = context;
this.serverInfo = serverInfo;
nAddressBooks = (serverInfo.getAddressBooks() == null) ? 0 : serverInfo.getAddressBooks().size();
nCalendars = (serverInfo.getCalendars() == null) ? 0 : serverInfo.getCalendars().size();
}
// item data
@Override
public int getCount() {
return nAddressBooks + nCalendars + 2;
}
@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);
return null;
}
@Override
public boolean hasStableIds() {
return true;
}
@Override
public long getItemId(int position) {
return position;
}
// item views
@Override
public int getViewTypeCount() {
return 4;
}
@Override
public int getItemViewType(int position) {
if (position == 0)
return TYPE_ADDRESS_BOOKS_HEADING;
else if (position <= nAddressBooks)
return TYPE_ADDRESS_BOOKS_ROW;
else if (position == nAddressBooks + 1)
return TYPE_CALENDARS_HEADING;
else if (position <= nAddressBooks + nCalendars + 1)
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 (v == null) {
LayoutInflater inflater = LayoutInflater.from(parent.getContext());
switch (getItemViewType(position)) {
case TYPE_ADDRESS_BOOKS_HEADING:
v = inflater.inflate(R.layout.address_books_heading, parent, false);
break;
case TYPE_ADDRESS_BOOKS_ROW:
v = inflater.inflate(android.R.layout.simple_list_item_single_choice, null);
v.setPadding(0, 8, 0, 8);
break;
case TYPE_CALENDARS_HEADING:
v = inflater.inflate(R.layout.calendars_heading, parent, false);
break;
case TYPE_CALENDARS_ROW:
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)v, R.drawable.addressbook, (ServerInfo.ResourceInfo)getItem(position));
break;
case TYPE_CALENDARS_ROW:
setContent((CheckedTextView)v, R.drawable.calendar, (ServerInfo.ResourceInfo)getItem(position));
}
return v;
}
protected void setContent(CheckedTextView view, int collectionIcon, ServerInfo.ResourceInfo info) {
// set layout and icons
view.setCompoundDrawablesWithIntrinsicBounds(collectionIcon, 0, info.isReadOnly() ? R.drawable.ic_read_only : 0, 0);
view.setCompoundDrawablePadding(10);
// set text
String title = info.getTitle();
if (title == null) // unnamed collection
title = context.getString((info.getType() == Type.ADDRESS_BOOK) ?
R.string.setup_address_book : R.string.setup_calendar);
title = "<b>" + title + "</b>";
if (info.isReadOnly())
title = title + " (" + context.getString(R.string.setup_read_only) + ")";
String description = info.getDescription();
if (description == null)
description = info.getURL();
// FIXME escape HTML
view.setText(Html.fromHtml(title + "<br/>" + description));
}
@Override
public boolean areAllItemsEnabled() {
return false;
}
@Override
public boolean isEnabled(int position) {
int type = getItemViewType(position);
return (type == TYPE_ADDRESS_BOOKS_ROW || type == TYPE_CALENDARS_ROW);
}
}

@ -1,127 +0,0 @@
/*******************************************************************************
* Copyright (c) 2014 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.app.ListFragment;
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.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);
setHasOptionsMenu(true);
return v;
}
@Override
public void onDestroyView() {
super.onDestroyView();
setListAdapter(null);
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
final ListView listView = getListView();
listView.setPadding(20, 30, 20, 30);
View header = getActivity().getLayoutInflater().inflate(R.layout.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);
setListAdapter(adapter);
listView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE);
listView.setOnItemClickListener(new OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
int itemPosition = position - 1; // one list header view at pos. 0
if (adapter.getItemViewType(itemPosition) == SelectCollectionsAdapter.TYPE_ADDRESS_BOOKS_ROW) {
// unselect all other address books
for (int pos = 1; pos <= adapter.getNAddressBooks(); pos++)
if (pos != itemPosition)
listView.setItemChecked(pos + 1, false);
}
getActivity().invalidateOptionsMenu();
}
});
}
@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:
ServerInfo serverInfo = (ServerInfo)getArguments().getSerializable(KEY_SERVER_INFO);
// synchronize only selected collections
for (ServerInfo.ResourceInfo addressBook : serverInfo.getAddressBooks())
addressBook.setEnabled(false);
for (ServerInfo.ResourceInfo calendar : serverInfo.getCalendars())
calendar.setEnabled(false);
ListAdapter adapter = getListView().getAdapter();
for (long id : getListView().getCheckedItemIds()) {
int position = (int)id + 1; // +1 because header view is inserted at pos. 0
ServerInfo.ResourceInfo info = (ServerInfo.ResourceInfo)adapter.getItem(position);
info.setEnabled(true);
}
// pass to "account details" fragment
AccountDetailsFragment accountDetails = new AccountDetailsFragment();
Bundle arguments = new Bundle();
arguments.putSerializable(SelectCollectionsFragment.KEY_SERVER_INFO, serverInfo);
accountDetails.setArguments(arguments);
getFragmentManager().beginTransaction()
.replace(R.id.fragment_container, accountDetails)
.addToBackStack(null)
.commitAllowingStateLoss();
break;
default:
return false;
}
return true;
}
// input validation
@Override
public void onPrepareOptionsMenu(Menu menu) {
boolean ok = false;
try {
ok = getListView().getCheckedItemCount() > 0;
} catch(IllegalStateException e) {
}
MenuItem item = menu.findItem(R.id.next);
item.setEnabled(ok);
}
}

@ -1,214 +0,0 @@
/*******************************************************************************
* Copyright (c) 2014 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.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;
import at.bitfire.davdroid.resource.RecordNotFoundException;
import at.bitfire.davdroid.resource.RemoteCollection;
import at.bitfire.davdroid.resource.Resource;
import at.bitfire.davdroid.webdav.DavException;
import at.bitfire.davdroid.webdav.HttpException;
import at.bitfire.davdroid.webdav.NotFoundException;
import at.bitfire.davdroid.webdav.PreconditionFailedException;
public class SyncManager {
private static final String TAG = "davdroid.SyncManager";
private static final int MAX_MULTIGET_RESOURCES = 35;
protected LocalCollection<? extends Resource> local;
protected RemoteCollection<? extends Resource> remote;
public SyncManager(LocalCollection<? extends Resource> local, RemoteCollection<? extends Resource> remote) {
this.local = local;
this.remote = remote;
}
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(),
updatedRemotely = pushDirty();
syncResult.stats.numEntries = deletedRemotely + addedRemotely + updatedRemotely;
// PHASE 2A: check if there's a reason to do a sync with remote (= forced sync or remote CTag changed)
boolean fetchCollection = syncResult.stats.numEntries > 0;
if (manualSync) {
Log.i(TAG, "Synchronization forced");
fetchCollection = true;
}
if (!fetchCollection) {
String currentCTag = remote.getCTag(),
lastCTag = local.getCTag();
Log.d(TAG, "Last local CTag = " + lastCTag + "; current remote CTag = " + currentCTag);
if (currentCTag == null || !currentCTag.equals(lastCTag))
fetchCollection = true;
}
if (!fetchCollection) {
Log.i(TAG, "No local changes and CTags match, no need to sync");
return;
}
// PHASE 2B: detect details of remote changes
Log.i(TAG, "Fetching remote resource list");
Set<Resource> remotelyAdded = new HashSet<Resource>(),
remotelyUpdated = new HashSet<Resource>();
Resource[] remoteResources = remote.getMemberETags();
for (Resource remoteResource : remoteResources) {
try {
Resource localResource = local.findByRemoteName(remoteResource.getName(), false);
if (localResource.getETag() == null || !localResource.getETag().equals(remoteResource.getETag()))
remotelyUpdated.add(remoteResource);
} catch(RecordNotFoundException e) {
remotelyAdded.add(remoteResource);
}
}
// PHASE 3: pull remote changes from server
syncResult.stats.numInserts = pullNew(remotelyAdded.toArray(new Resource[0]));
syncResult.stats.numUpdates = pullChanged(remotelyUpdated.toArray(new Resource[0]));
syncResult.stats.numEntries += syncResult.stats.numInserts + syncResult.stats.numUpdates;
Log.i(TAG, "Removing non-dirty resources that are not present remotely anymore");
local.deleteAllExceptRemoteNames(remoteResources);
local.commit();
// update collection CTag
Log.i(TAG, "Sync complete, fetching new CTag");
local.setCTag(remote.getCTag());
}
private int pushDeleted() throws URISyntaxException, LocalStorageException, IOException, HttpException {
int count = 0;
long[] deletedIDs = local.findDeleted();
try {
Log.i(TAG, "Remotely removing " + deletedIDs.length + " deleted resource(s) (if not changed)");
for (long id : deletedIDs)
try {
Resource res = local.findById(id, false);
if (res.getName() != null) // is this resource even present remotely?
try {
remote.delete(res);
} catch(NotFoundException e) {
Log.i(TAG, "Locally-deleted resource has already been removed from server");
} catch(PreconditionFailedException e) {
Log.i(TAG, "Locally-deleted resource has been changed on the server in the meanwhile");
}
// always delete locally so that the record with the DELETED flag doesn't cause another deletion attempt
local.delete(res);
count++;
} catch (RecordNotFoundException e) {
Log.wtf(TAG, "Couldn't read locally-deleted record", e);
}
} finally {
local.commit();
}
return count;
}
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)");
try {
for (long id : newIDs)
try {
Resource res = local.findById(id, true);
String eTag = remote.add(res);
if (eTag != null)
local.updateETag(res, eTag);
local.clearDirty(res);
count++;
} catch(PreconditionFailedException e) {
Log.i(TAG, "Didn't overwrite existing resource with other content");
} catch (ValidationException e) {
Log.e(TAG, "Couldn't create entity for adding: " + e.toString());
} catch (RecordNotFoundException e) {
Log.wtf(TAG, "Couldn't read new record", e);
}
} finally {
local.commit();
}
return count;
}
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)");
try {
for (long id : dirtyIDs) {
try {
Resource res = local.findById(id, true);
String eTag = remote.update(res);
if (eTag != null)
local.updateETag(res, eTag);
local.clearDirty(res);
count++;
} catch(PreconditionFailedException e) {
Log.i(TAG, "Locally changed resource has been changed on the server in the meanwhile");
} catch (ValidationException e) {
Log.e(TAG, "Couldn't create entity for updating: " + e.toString());
} catch (RecordNotFoundException e) {
Log.e(TAG, "Couldn't read dirty record", e);
}
}
} finally {
local.commit();
}
return count;
}
private int pullNew(Resource[] resourcesToAdd) throws URISyntaxException, LocalStorageException, IOException, HttpException, DavException {
int count = 0;
Log.i(TAG, "Fetching " + resourcesToAdd.length + " new remote resource(s)");
for (Resource[] resources : ArrayUtils.partition(resourcesToAdd, MAX_MULTIGET_RESOURCES))
for (Resource res : remote.multiGet(resources)) {
Log.d(TAG, "Adding " + res.getName());
local.add(res);
local.commit();
count++;
}
return count;
}
private int pullChanged(Resource[] resourcesToUpdate) throws URISyntaxException, LocalStorageException, IOException, HttpException, DavException {
int count = 0;
Log.i(TAG, "Fetching " + resourcesToUpdate.length + " updated remote resource(s)");
for (Resource[] resources : ArrayUtils.partition(resourcesToUpdate, MAX_MULTIGET_RESOURCES))
for (Resource res : remote.multiGet(resources)) {
Log.i(TAG, "Updating " + res.getName());
local.updateByRemoteName(res);
local.commit();
count++;
}
return count;
}
}

@ -1,62 +0,0 @@
/*******************************************************************************
* Copyright (c) 2014 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;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.TextView;
import at.bitfire.davdroid.webdav.WebDavResource;
public class WebDavResourceAdapter extends BaseAdapter {
protected int viewId;
protected LayoutInflater inflater;
WebDavResource[] items;
public WebDavResourceAdapter(Context context, int textViewResourceId, List<WebDavResource> objects) {
viewId = textViewResourceId;
inflater = LayoutInflater.from(context);
items = objects.toArray(new WebDavResource[0]);
}
@Override
public View getView(int position, View view, ViewGroup parent) {
WebDavResource item = items[position];
View itemView = (View)inflater.inflate(viewId, null);
TextView textName = (TextView) itemView.findViewById(android.R.id.text1);
textName.setText(item.getDisplayName());
TextView textDescription = (TextView) itemView.findViewById(android.R.id.text2);
String description = item.getDescription();
if (description == null)
description = item.getLocation().getPath();
textDescription.setText(description);
return itemView;
}
@Override
public int getCount() {
return items.length;
}
@Override
public Object getItem(int position) {
return items[position];
}
@Override
public long getItemId(int position) {
return position;
}
}

@ -1,21 +0,0 @@
/*******************************************************************************
* Copyright (c) 2014 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;
import org.simpleframework.xml.NamespaceList;
import org.simpleframework.xml.Root;
@Root(name="addressbook-multiget")
@NamespaceList({
@Namespace(reference="DAV:"),
@Namespace(prefix="CD",reference="urn:ietf:params:xml:ns:carddav")
})
@Namespace(prefix="CD",reference="urn:ietf:params:xml:ns:carddav")
public class DavAddressbookMultiget extends DavMultiget {
}

@ -1,21 +0,0 @@
/*******************************************************************************
* Copyright (c) 2014 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;
import org.simpleframework.xml.NamespaceList;
import org.simpleframework.xml.Root;
@Root(name="calendar-multiget")
@NamespaceList({
@Namespace(reference="DAV:"),
@Namespace(prefix="C",reference="urn:ietf:params:xml:ns:caldav")
})
@Namespace(prefix="C",reference="urn:ietf:params:xml:ns:caldav")
public class DavCalendarMultiget extends DavMultiget {
}

@ -1,25 +0,0 @@
/*******************************************************************************
* Copyright (c) 2014 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 {
private static final long serialVersionUID = -2118919144443165706L;
final private static String prefix = "Invalid DAV response: ";
/* used to indiciate DAV protocol errors */
public DavException(String message) {
super(prefix + message);
}
public DavException(String message, Throwable ex) {
super(prefix + message, ex);
}
}

@ -1,26 +0,0 @@
/*******************************************************************************
* Copyright (c) 2014 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;
import org.simpleframework.xml.Root;
import org.simpleframework.xml.Text;
@Root(name="href")
@Namespace(prefix="D",reference="DAV:")
public class DavHref {
@Text
String href;
DavHref() {
}
public DavHref(String href) {
this.href = href;
}
}

@ -1,72 +0,0 @@
/*******************************************************************************
* Copyright (c) 2014 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 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";
private final static RequestConfig defaultRqConfig;
private final static Registry<ConnectionSocketFactory> socketFactoryRegistry;
static {
socketFactoryRegistry = RegistryBuilder.<ConnectionSocketFactory> create()
.register("http", PlainConnectionSocketFactory.getSocketFactory())
.register("https", TlsSniSocketFactory.INSTANCE)
.build();
// use request defaults from AndroidHttpClient
defaultRqConfig = RequestConfig.copy(RequestConfig.DEFAULT)
.setConnectTimeout(20*1000)
.setSocketTimeout(45*1000)
.setStaleConnectionCheckEnabled(false)
.build();
}
public static CloseableHttpClient create(boolean disableCompression, boolean logTraffic) {
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)
.setDefaultRequestConfig(defaultRqConfig)
.setRetryHandler(DavHttpRequestRetryHandler.INSTANCE)
.setRedirectStrategy(DavRedirectStrategy.INSTANCE)
.setUserAgent("DAVdroid/" + Constants.APP_VERSION)
.disableCookieManagement();
if (disableCompression) {
Log.d(TAG, "Disabling compression for debugging purposes");
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();
}
}

@ -1,35 +0,0 @@
/*******************************************************************************
* Copyright (c) 2014 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.Locale;
import org.apache.commons.lang.ArrayUtils;
import ch.boye.httpclientandroidlib.HttpRequest;
import ch.boye.httpclientandroidlib.impl.client.DefaultHttpRequestRetryHandler;
public class DavHttpRequestRetryHandler extends DefaultHttpRequestRetryHandler {
final static DavHttpRequestRetryHandler INSTANCE = new DavHttpRequestRetryHandler();
// see http://www.iana.org/assignments/http-methods/http-methods.xhtml
private final static String idempotentMethods[] = {
"DELETE", "GET", "HEAD", "MKCALENDAR", "MKCOL", "OPTIONS", "PROPFIND", "PROPPATCH",
"PUT", "REPORT", "SEARCH", "TRACE"
};
public DavHttpRequestRetryHandler() {
super(/* retry count */ 3, /* retry already sent requests? */ false);
}
@Override
protected boolean handleAsIdempotent(final HttpRequest request) {
final String method = request.getRequestLine().getMethod().toUpperCase(Locale.ROOT);
return ArrayUtils.contains(idempotentMethods, method);
}
}

@ -1,18 +0,0 @@
/*******************************************************************************
* Copyright (c) 2014 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 {
private static final long serialVersionUID = -7199786680939975667L;
/* used to indicate that the server doesn't support DAV */
public DavIncapableException(String msg) {
super(msg);
}
}

@ -1,48 +0,0 @@
/*******************************************************************************
* Copyright (c) 2014 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;
@Order(elements={"prop","href"})
public class DavMultiget {
public enum Type {
ADDRESS_BOOK,
CALENDAR
}
@Element
DavProp prop;
@ElementList(inline=true)
List<DavHref> hrefs;
public static DavMultiget newRequest(Type type, String names[]) {
DavMultiget multiget = (type == Type.ADDRESS_BOOK) ? new DavAddressbookMultiget() : new DavCalendarMultiget();
multiget.prop = new DavProp();
multiget.prop.getetag = new DavProp.GetETag();
if (type == Type.ADDRESS_BOOK)
multiget.prop.addressData = new DavProp.AddressData();
else if (type == Type.CALENDAR)
multiget.prop.calendarData = new DavProp.CalendarData();
multiget.hrefs = new ArrayList<DavHref>(names.length);
for (String name : names)
multiget.hrefs.add(new DavHref(name));
return multiget;
}
}

@ -1,21 +0,0 @@
/*******************************************************************************
* Copyright (c) 2014 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;
@Namespace(reference="DAV:")
@Root(strict=false)
public class DavMultistatus {
@ElementList(inline=true,entry="response",required=false)
List<DavResponse> response;
}

@ -1,18 +0,0 @@
/*******************************************************************************
* Copyright (c) 2014 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 {
private static final long serialVersionUID = 6256645020350945477L;
private final static String message = "HTTP response entity (content) expected but not received";
public DavNoContentException() {
super(message);
}
}

@ -1,18 +0,0 @@
/*******************************************************************************
* Copyright (c) 2014 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 {
private static final long serialVersionUID = -3600405724694229828L;
private final static String message = "207 Multi-Status expected but not received";
public DavNoMultiStatusException() {
super(message);
}
}

@ -1,211 +0,0 @@
/*******************************************************************************
* Copyright (c) 2014 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;
import org.simpleframework.xml.Namespace;
import org.simpleframework.xml.Root;
import org.simpleframework.xml.Text;
@Namespace(prefix="D",reference="DAV:")
@Root(strict=false)
public class DavProp {
/* RFC 4918 WebDAV */
@Element(required=false)
ResourceType resourcetype;
@Element(required=false)
DisplayName displayname;
@Element(required=false)
GetCTag getctag;
@Element(required=false)
GetETag getetag;
@Root(strict=false)
public static class ResourceType {
@Element(required=false)
@Getter private Collection collection;
public static class Collection { }
@Element(required=false)
@Getter private Addressbook addressbook;
@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 DisplayName {
@Text(required=false)
@Getter private String displayName;
}
@Namespace(prefix="CS",reference="http://calendarserver.org/ns/")
public static class GetCTag {
@Text(required=false)
@Getter private String CTag;
}
public static class GetETag {
@Text(required=false)
@Getter private String ETag;
}
/* RFC 5397 WebDAV Current Principal Extension */
@Element(required=false,name="current-user-principal")
CurrentUserPrincipal currentUserPrincipal;
public static class CurrentUserPrincipal {
@Element(required=false)
@Getter private DavHref href;
}
/* RFC 3744 WebDAV Access Control Protocol */
@ElementList(required=false,name="current-user-privilege-set",entry="privilege")
List<Privilege> currentUserPrivilegeSet;
public static class Privilege {
@Element(required=false)
@Getter private PrivAll all;
@Element(required=false)
@Getter private PrivBind bind;
@Element(required=false)
@Getter private PrivUnbind unbind;
@Element(required=false)
@Getter private PrivWrite write;
@Element(required=false,name="write-content")
@Getter private PrivWriteContent writeContent;
public static class PrivAll { }
public static class PrivBind { }
public static class PrivUnbind { }
public static class PrivWrite { }
public static class PrivWriteContent { }
}
/* RFC 4791 CalDAV, RFC 6352 CardDAV */
@Element(required=false,name="addressbook-home-set")
AddressbookHomeSet addressbookHomeSet;
@Element(required=false,name="calendar-home-set")
CalendarHomeSet calendarHomeSet;
@Element(required=false,name="addressbook-description")
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")
CalendarDescription calendarDescription;
@Element(required=false,name="calendar-color")
CalendarColor calendarColor;
@Element(required=false,name="calendar-timezone")
CalendarTimezone calendarTimezone;
@Namespace(prefix="C",reference="urn:ietf:params:xml:ns:caldav")
@ElementList(required=false,name="supported-calendar-component-set",entry="comp")
List<Comp> supportedCalendarComponentSet;
@Element(name="address-data",required=false)
AddressData addressData;
@Element(name="calendar-data",required=false)
CalendarData calendarData;
@Namespace(prefix="CD",reference="urn:ietf:params:xml:ns:carddav")
public static class AddressbookHomeSet {
@Element(required=false)
@Getter private DavHref href;
}
@Namespace(prefix="C",reference="urn:ietf:params:xml:ns:caldav")
public static class CalendarHomeSet {
@Element(required=false)
@Getter private DavHref href;
}
@Namespace(prefix="CD",reference="urn:ietf:params:xml:ns:carddav")
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 CalendarDescription {
@Text(required=false)
@Getter private String description;
}
@Namespace(prefix="A",reference="http://apple.com/ns/ical/")
public static class CalendarColor {
@Text(required=false)
@Getter private String color;
}
@Namespace(prefix="C",reference="urn:ietf:params:xml:ns:caldav")
public static class CalendarTimezone {
@Text(required=false)
@Getter private String timezone;
}
@Namespace(prefix="C",reference="urn:ietf:params:xml:ns:caldav")
public static class Comp {
@Attribute
@Getter String name;
}
@Namespace(prefix="CD",reference="urn:ietf:params:xml:ns:carddav")
public static class AddressData {
@Text(required=false)
@Getter String vcard;
}
@Namespace(prefix="C",reference="urn:ietf:params:xml:ns:caldav")
public static class CalendarData {
@Text(required=false)
@Getter String ical;
}
}

@ -1,19 +0,0 @@
/*******************************************************************************
* Copyright (c) 2014 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;
import org.simpleframework.xml.Namespace;
import org.simpleframework.xml.Root;
@Namespace(reference="DAV:")
@Root(name="propfind")
public class DavPropfind {
@Element(required=false)
protected DavProp prop;
}

@ -1,20 +0,0 @@
/*******************************************************************************
* Copyright (c) 2014 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;
import org.simpleframework.xml.Root;
@Root(strict=false,name="propstat")
public class DavPropstat {
@Element
DavProp prop;
@Element
String status;
}

@ -1,109 +0,0 @@
/*******************************************************************************
* Copyright (c) 2014 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.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import android.util.Log;
import ch.boye.httpclientandroidlib.Header;
import ch.boye.httpclientandroidlib.HttpHost;
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.client.utils.URIUtils;
import ch.boye.httpclientandroidlib.protocol.HttpContext;
/**
* 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();
protected final static String REDIRECTABLE_METHODS[] = {
"OPTIONS", "GET", "PUT", "DELETE"
};
@Override
public HttpUriRequest getRedirect(HttpRequest request, HttpResponse response, HttpContext context) throws ProtocolException {
RequestLine line = request.getRequestLine();
String location = getLocation(request, response, context).toString();
Log.i(TAG, "Following redirection: " + line.getMethod() + " " + line.getUri() + " -> " + location);
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
.build();
}
/**
* Determines whether a response indicates a redirection and if it does, whether to follow this redirection.
* PROPFIND and REPORT must handle redirections explicitely because multi-status processing requires knowledge of the content location.
* @return true for 3xx responses on OPTIONS, GET, PUT, DELETE requests that have a valid Location header; false otherwise
*/
@Override
public boolean isRedirected(HttpRequest request, HttpResponse response, HttpContext context) throws ProtocolException {
if (response.getStatusLine().getStatusCode()/100 == 3) {
boolean redirectable = false;
for (String method : REDIRECTABLE_METHODS)
if (method.equalsIgnoreCase(request.getRequestLine().getMethod())) {
redirectable = true;
break;
}
return redirectable && getLocation(request, response, context) != null;
}
return false;
}
/**
* Gets the destination of a redirection
* @return absolute URL of new location; null if not available
*/
static URL getLocation(HttpRequest request, HttpResponse response, HttpContext context) {
Header locationHdr = response.getFirstHeader("Location");
if (locationHdr == null) {
Log.e(TAG, "Received redirection without Location header, ignoring");
return null;
}
try {
URI location = new URI(locationHdr.getValue());
// some servers don't return absolute URLs as required by RFC 2616
if (!location.isAbsolute()) {
Log.w(TAG, "Received invalid redirection to relative URL, repairing");
URI originalURI = new URI(request.getRequestLine().getUri());
if (!originalURI.isAbsolute()) {
final HttpHost target = HttpClientContext.adapt(context).getTargetHost();
if (target != null)
originalURI = URIUtils.rewriteURI(originalURI, target);
else
return null;
}
return new URL(originalURI.toURL(), location.toString());
}
return location.toURL();
} catch (URISyntaxException e) {
Log.e(TAG, "Received redirection from/to invalid URI, ignoring", e);
} catch (MalformedURLException e) {
Log.e(TAG, "Received redirection from/to invalid URL, ignoring", e);
}
return null;
}
}

@ -1,25 +0,0 @@
/*******************************************************************************
* Copyright (c) 2014 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;
@Root(strict=false)
public class DavResponse {
@Element
@Getter DavHref href;
@ElementList(inline=true)
@Getter List<DavPropstat> propstat;
}

@ -1,26 +0,0 @@
/*******************************************************************************
* Copyright (c) 2014 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 {
private static final long serialVersionUID = -4805778240079377401L;
@Getter private int code;
HttpException(int code, String message) {
super(message);
this.code = code;
}
public boolean isClientError() {
return code/100 == 4;
}
}

@ -1,102 +0,0 @@
/*******************************************************************************
* Copyright (c) 2014 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.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 {
private static final String TAG = "davdroid.HttpPropfind";
public final static String METHOD_NAME = "PROPFIND";
public enum Mode {
CURRENT_USER_PRINCIPAL,
HOME_SETS,
CARDDAV_COLLECTIONS,
CALDAV_COLLECTIONS,
COLLECTION_CTAG,
MEMBERS_ETAG
}
HttpPropfind(URI uri) {
setURI(uri);
}
HttpPropfind(URI uri, Mode mode) {
this(uri);
DavPropfind propfind = new DavPropfind();
propfind.prop = new DavProp();
int depth = 0;
switch (mode) {
case CURRENT_USER_PRINCIPAL:
propfind.prop.currentUserPrincipal = new DavProp.CurrentUserPrincipal();
break;
case HOME_SETS:
propfind.prop.addressbookHomeSet = new DavProp.AddressbookHomeSet();
propfind.prop.calendarHomeSet = new DavProp.CalendarHomeSet();
break;
case CARDDAV_COLLECTIONS:
depth = 1;
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.GetCTag();
break;
case MEMBERS_ETAG:
depth = 1;
propfind.prop.getctag = new DavProp.GetCTag();
propfind.prop.getetag = new DavProp.GetETag();
break;
}
try {
Serializer serializer = new Persister();
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) {
Log.e(TAG, "Couldn't prepare PROPFIND request for " + uri, ex);
abort();
}
}
@Override
public String getMethod() {
return METHOD_NAME;
}
}

@ -1,38 +0,0 @@
/*******************************************************************************
* Copyright (c) 2014 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.net.URI;
import ch.boye.httpclientandroidlib.client.methods.HttpEntityEnclosingRequestBase;
import ch.boye.httpclientandroidlib.entity.StringEntity;
public class HttpReport extends HttpEntityEnclosingRequestBase {
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", "0");
setEntity(new StringEntity(entity, "UTF-8"));
}
@Override
public String getMethod() {
return METHOD_NAME;
}
}

@ -1,19 +0,0 @@
/*******************************************************************************
* Copyright (c) 2014 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.http.HttpStatus;
public class NotAuthorizedException extends HttpException {
private static final long serialVersionUID = 2490525047224413586L;
public NotAuthorizedException(String reason) {
super(HttpStatus.SC_UNAUTHORIZED, reason);
}
}

@ -1,18 +0,0 @@
/*******************************************************************************
* Copyright (c) 2014 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.http.HttpStatus;
public class NotFoundException extends HttpException {
private static final long serialVersionUID = 1565961502781880483L;
public NotFoundException(String reason) {
super(HttpStatus.SC_NOT_FOUND, reason);
}
}

@ -1,18 +0,0 @@
/*******************************************************************************
* Copyright (c) 2014 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.http.HttpStatus;
public class PreconditionFailedException extends HttpException {
private static final long serialVersionUID = 102282229174086113L;
public PreconditionFailedException(String reason) {
super(HttpStatus.SC_PRECONDITION_FAILED, reason);
}
}

@ -1,186 +0,0 @@
/*******************************************************************************
* Copyright (c) 2014 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.UnknownHostException;
import java.util.Arrays;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLPeerUnverifiedException;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSocket;
import org.apache.commons.lang.StringUtils;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.net.SSLCertificateSocketFactory;
import android.os.Build;
import android.util.Log;
import ch.boye.httpclientandroidlib.HttpHost;
import ch.boye.httpclientandroidlib.conn.socket.LayeredConnectionSocketFactory;
import ch.boye.httpclientandroidlib.conn.ssl.BrowserCompatHostnameVerifier;
import ch.boye.httpclientandroidlib.protocol.HttpContext;
public class TlsSniSocketFactory implements LayeredConnectionSocketFactory {
private static final String TAG = "davdroid.SNISocketFactory";
final static TlsSniSocketFactory INSTANCE = new TlsSniSocketFactory();
private final static SSLCertificateSocketFactory sslSocketFactory =
(SSLCertificateSocketFactory)SSLCertificateSocketFactory.getDefault(0);
private final static HostnameVerifier hostnameVerifier = new BrowserCompatHostnameVerifier();
/*
For SSL connections without HTTP(S) proxy:
1) createSocket() is called
2) connectSocket() is called which creates a new SSL connection
2a) SNI is set up, and then
2b) the connection is established, hands are shaken and certificate/host name are verified
Layered sockets are used with HTTP(S) proxies:
1) a new plain socket is created by the HTTP library
2) the plain socket is connected to http://proxy:8080
3) a CONNECT request is sent to the proxy and the response is parsed
4) now, createLayeredSocket() is called which wraps an SSL socket around the proxy connection,
doing all the set-up and verfication
4a) Because SSLSocket.createSocket(socket, ...) always does a handshake without allowing
to set up SNI before, *** SNI is not available for layered connections *** (unless
active by Android's defaults, which it isn't at the moment).
*/
@Override
public Socket createSocket(HttpContext context) throws IOException {
SSLSocket ssl = (SSLSocket)sslSocketFactory.createSocket();
setReasonableEncryption(ssl);
return ssl;
}
@Override
public Socket connectSocket(int timeout, Socket plain, HttpHost host, InetSocketAddress remoteAddr, InetSocketAddress localAddr, HttpContext context) throws IOException {
Log.d(TAG, "Preparing direct SSL connection (without proxy) to " + host);
// we'll rather use an SSLSocket directly
plain.close();
// create a plain SSL socket, but don't do hostname/certificate verification yet
SSLSocket ssl = (SSLSocket)sslSocketFactory.createSocket(remoteAddr.getAddress(), host.getPort());
setReasonableEncryption(ssl);
// connect, set SNI, shake hands, verify, print connection info
connectWithSNI(ssl, host.getHostName());
return ssl;
}
@Override
public Socket createLayeredSocket(Socket plain, String host, int port, HttpContext context) throws IOException, UnknownHostException {
Log.d(TAG, "Preparing layered SSL connection (over proxy) to " + host);
// create a layered SSL socket, but don't do hostname/certificate verification yet
SSLSocket ssl = (SSLSocket)sslSocketFactory.createSocket(plain, host, port, true);
setReasonableEncryption(ssl);
// already connected, but verify host name again and print some connection info
Log.w(TAG, "Setting SNI/TLSv1.2 will silently fail because the handshake is already done");
connectWithSNI(ssl, host);
return ssl;
}
@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
private void connectWithSNI(SSLSocket ssl, String host) throws SSLPeerUnverifiedException {
// - set SNI host name
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
Log.d(TAG, "Using documented SNI with host name " + host);
sslSocketFactory.setHostname(ssl, host);
} else {
Log.d(TAG, "No documented SNI support on Android <4.2, trying with reflection");
try {
java.lang.reflect.Method setHostnameMethod = ssl.getClass().getMethod("setHostname", String.class);
setHostnameMethod.invoke(ssl, host);
} catch (Exception e) {
Log.w(TAG, "SNI not useable", e);
}
}
// verify hostname and certificate
SSLSession session = ssl.getSession();
if (!hostnameVerifier.verify(host, session))
throw new SSLPeerUnverifiedException("Cannot verify hostname: " + host);
Log.d(TAG, "Established " + session.getProtocol() + " connection with " + session.getPeerHost() +
" using " + session.getCipherSuite());
}
@SuppressLint("DefaultLocale")
private void setReasonableEncryption(SSLSocket ssl) {
// set reasonable SSL/TLS settings before the handshake
// Android 5.0+ (API level21) provides reasonable default settings
// but it still allows SSLv3
// https://developer.android.com/about/versions/android-5.0-changes.html#ssl
// - enable all supported protocols (enables TLSv1.1 and TLSv1.2 on Android <5.0, if available)
// - remove all SSL versions (especially SSLv3) because they're insecure now
List<String> protocols = new LinkedList<String>();
for (String protocol : ssl.getSupportedProtocols())
if (!protocol.toUpperCase().contains("SSL"))
protocols.add(protocol);
Log.v(TAG, "Setting allowed TLS protocols: " + StringUtils.join(protocols, ", "));
ssl.setEnabledProtocols(protocols.toArray(new String[0]));
if (android.os.Build.VERSION.SDK_INT < 21) {
// choose secure cipher suites
List<String> allowedCiphers = Arrays.asList(new String[] {
// allowed secure ciphers according to NIST.SP.800-52r1.pdf Section 3.3.1 (see docs directory)
// TLS 1.2
"TLS_RSA_WITH_AES_256_GCM_SHA384",
"TLS_RSA_WITH_AES_128_GCM_SHA256",
"TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256",
"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
"TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256",
"TLS_ECHDE_RSA_WITH_AES_128_GCM_SHA256",
// maximum interoperability
"TLS_RSA_WITH_3DES_EDE_CBC_SHA",
"TLS_RSA_WITH_AES_128_CBC_SHA",
// additionally
"TLS_RSA_WITH_AES_256_CBC_SHA",
"TLS_ECDHE_ECDSA_WITH_3DES_EDE_CBC_SHA",
"TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA",
"TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA",
"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA",
});
List<String> availableCiphers = Arrays.asList(ssl.getSupportedCipherSuites());
// preferred ciphers = allowed Ciphers \ availableCiphers
HashSet<String> preferredCiphers = new HashSet<String>(allowedCiphers);
preferredCiphers.retainAll(availableCiphers);
// add preferred ciphers to enabled ciphers
// for maximum security, preferred ciphers should *replace* enabled ciphers,
// but I guess for the security level of DAVdroid, disabling of insecure
// ciphers should be a server-side task
HashSet<String> enabledCiphers = preferredCiphers;
enabledCiphers.addAll(new HashSet<String>(Arrays.asList(ssl.getEnabledCipherSuites())));
Log.v(TAG, "Setting allowed TLS ciphers: " + StringUtils.join(enabledCiphers, ", "));
ssl.setEnabledCipherSuites(enabledCiphers.toArray(new String[0]));
}
}
}

@ -1,584 +0,0 @@
/*******************************************************************************
* Copyright (c) 2014 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.io.InputStream;
import java.io.StringWriter;
import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import lombok.Cleanup;
import lombok.Getter;
import lombok.ToString;
import org.apache.commons.lang.StringUtils;
import org.simpleframework.xml.Serializer;
import org.simpleframework.xml.core.Persister;
import android.util.Log;
import at.bitfire.davdroid.URLUtils;
import at.bitfire.davdroid.resource.Event;
import at.bitfire.davdroid.webdav.DavProp.Comp;
import ch.boye.httpclientandroidlib.Header;
import ch.boye.httpclientandroidlib.HttpEntity;
import ch.boye.httpclientandroidlib.HttpHost;
import ch.boye.httpclientandroidlib.HttpResponse;
import ch.boye.httpclientandroidlib.HttpStatus;
import ch.boye.httpclientandroidlib.StatusLine;
import ch.boye.httpclientandroidlib.auth.AuthScope;
import ch.boye.httpclientandroidlib.auth.UsernamePasswordCredentials;
import ch.boye.httpclientandroidlib.client.AuthCache;
import ch.boye.httpclientandroidlib.client.methods.CloseableHttpResponse;
import ch.boye.httpclientandroidlib.client.methods.HttpDelete;
import ch.boye.httpclientandroidlib.client.methods.HttpGet;
import ch.boye.httpclientandroidlib.client.methods.HttpOptions;
import ch.boye.httpclientandroidlib.client.methods.HttpPut;
import ch.boye.httpclientandroidlib.client.protocol.HttpClientContext;
import ch.boye.httpclientandroidlib.entity.ByteArrayEntity;
import ch.boye.httpclientandroidlib.impl.auth.BasicScheme;
import ch.boye.httpclientandroidlib.impl.client.BasicAuthCache;
import ch.boye.httpclientandroidlib.impl.client.BasicCredentialsProvider;
import ch.boye.httpclientandroidlib.impl.client.CloseableHttpClient;
import ch.boye.httpclientandroidlib.message.BasicLineParser;
import ch.boye.httpclientandroidlib.util.EntityUtils;
import ezvcard.VCardVersion;
/**
* Represents a WebDAV resource (file or collection).
* This class is used for all CalDAV/CardDAV communcation.
*/
@ToString
public class WebDavResource {
private static final String TAG = "davdroid.WebDavResource";
public enum Property {
CURRENT_USER_PRINCIPAL, // resource detection
ADDRESSBOOK_HOMESET, CALENDAR_HOMESET,
CONTENT_TYPE, READ_ONLY, // WebDAV (common)
DISPLAY_NAME, DESCRIPTION, ETAG,
IS_COLLECTION, CTAG, // collections
IS_CALENDAR, COLOR, TIMEZONE, // CalDAV
IS_ADDRESSBOOK, VCARD_VERSION // CardDAV
}
public enum PutMode {
ADD_DONT_OVERWRITE,
UPDATE_DONT_OVERWRITE
}
// location of this resource
@Getter protected URL location;
// DAV capabilities (DAV: header) and allowed DAV methods (set for OPTIONS request)
protected Set<String> capabilities = new HashSet<String>(),
methods = new HashSet<String>();
// DAV properties
protected HashMap<Property, String> properties = new HashMap<Property, String>();
@Getter protected List<String> supportedComponents;
// list of members (only for collections)
@Getter protected List<WebDavResource> members;
// content (available after GET)
@Getter protected byte[] content;
protected CloseableHttpClient httpClient;
protected HttpClientContext context;
public WebDavResource(CloseableHttpClient httpClient, URL baseURL) {
this.httpClient = httpClient;
location = baseURL;
context = HttpClientContext.create();
context.setCredentialsProvider(new BasicCredentialsProvider());
}
public WebDavResource(CloseableHttpClient httpClient, URL baseURL, String username, String password, boolean preemptive) {
this(httpClient, baseURL);
HttpHost host = new HttpHost(baseURL.getHost(), baseURL.getPort(), baseURL.getProtocol());
context.getCredentialsProvider().setCredentials(AuthScope.ANY, new UsernamePasswordCredentials(username, password));
if (preemptive) {
Log.d(TAG, "Using preemptive authentication (not compatible with Digest auth)");
AuthCache authCache = context.getAuthCache();
if (authCache == null)
authCache = new BasicAuthCache();
authCache.put(host, new BasicScheme());
context.setAuthCache(authCache);
}
}
WebDavResource(WebDavResource parent) { // copy constructor: based on existing WebDavResource, reuse settings
httpClient = parent.httpClient;
context = parent.context;
location = parent.location;
}
protected WebDavResource(WebDavResource parent, URL url) {
this(parent);
location = url;
}
public WebDavResource(WebDavResource parent, String member) throws MalformedURLException {
this(parent);
location = new URL(parent.location, URLUtils.sanitize(member));
}
public WebDavResource(WebDavResource parent, String member, String ETag) throws MalformedURLException {
this(parent, member);
properties.put(Property.ETAG, ETag);
}
/* feature detection */
public void options() throws URISyntaxException, IOException, HttpException {
HttpOptions options = new HttpOptions(location.toURI());
CloseableHttpResponse response = httpClient.execute(options, context);
try {
checkResponse(response);
Header[] allowHeaders = response.getHeaders("Allow");
for (Header allowHeader : allowHeaders)
methods.addAll(Arrays.asList(allowHeader.getValue().split(", ?")));
Header[] capHeaders = response.getHeaders("DAV");
for (Header capHeader : capHeaders)
capabilities.addAll(Arrays.asList(capHeader.getValue().split(", ?")));
} finally {
response.close();
}
}
public boolean supportsDAV(String capability) {
return capabilities.contains(capability);
}
public boolean supportsMethod(String method) {
return methods.contains(method);
}
/* file hierarchy methods */
public String getName() {
String[] names = StringUtils.split(location.getPath(), "/");
return names[names.length - 1];
}
/* property methods */
public String getCurrentUserPrincipal() {
return properties.get(Property.CURRENT_USER_PRINCIPAL);
}
public String getAddressbookHomeSet() {
return properties.get(Property.ADDRESSBOOK_HOMESET);
}
public String getCalendarHomeSet() {
return properties.get(Property.CALENDAR_HOMESET);
}
public String getContentType() {
return properties.get(Property.CONTENT_TYPE);
}
public void setContentType(String mimeType) {
properties.put(Property.CONTENT_TYPE, mimeType);
}
public boolean isReadOnly() {
return properties.containsKey(Property.READ_ONLY);
}
public String getDisplayName() {
return properties.get(Property.DISPLAY_NAME);
}
public String getDescription() {
return properties.get(Property.DESCRIPTION);
}
public String getCTag() {
return properties.get(Property.CTAG);
}
public void invalidateCTag() {
properties.remove(Property.CTAG);
}
public String getETag() {
return properties.get(Property.ETAG);
}
public boolean isCalendar() {
return properties.containsKey(Property.IS_CALENDAR);
}
public String getColor() {
return properties.get(Property.COLOR);
}
public String getTimezone() {
return properties.get(Property.TIMEZONE);
}
public boolean isAddressBook() {
return properties.containsKey(Property.IS_ADDRESSBOOK);
}
public VCardVersion getVCardVersion() {
String versionStr = properties.get(Property.VCARD_VERSION);
return (versionStr != null) ? VCardVersion.valueOfByStr(versionStr) : null;
}
/* collection operations */
public void propfind(HttpPropfind.Mode mode) throws URISyntaxException, IOException, DavException, HttpException {
CloseableHttpResponse response = null;
// processMultiStatus() requires knowledge of the actual content location,
// so we have to handle redirections manually and create a new request for the new location
for (int i = context.getRequestConfig().getMaxRedirects(); i > 0; i--) {
HttpPropfind propfind = new HttpPropfind(location.toURI(), mode);
response = httpClient.execute(propfind, context);
if (response.getStatusLine().getStatusCode()/100 == 3) {
location = DavRedirectStrategy.getLocation(propfind, response, context);
Log.i(TAG, "Redirection on PROPFIND; trying again at new content URL: " + location);
// don't forget to throw away the unneeded response content
HttpEntity entity = response.getEntity();
if (entity != null) { @Cleanup InputStream content = entity.getContent(); }
} else
break; // answer was NOT a redirection, continue
}
if (response == null)
throw new DavNoContentException();
try {
checkResponse(response); // will also handle Content-Location
processMultiStatus(response);
} finally {
response.close();
}
}
public void multiGet(DavMultiget.Type type, String[] names) throws URISyntaxException, IOException, DavException, HttpException {
CloseableHttpResponse response = null;
// processMultiStatus() requires knowledge of the actual content location,
// so we have to handle redirections manually and create a new request for the new location
for (int i = context.getRequestConfig().getMaxRedirects(); i > 0; i--) {
// build multi-get XML request
List<String> hrefs = new LinkedList<String>();
for (String name : names)
hrefs.add(new URL(location, name).getPath());
DavMultiget multiget = DavMultiget.newRequest(type, hrefs.toArray(new String[0]));
StringWriter writer = new StringWriter();
try {
Serializer serializer = new Persister();
serializer.write(multiget, writer);
} catch (Exception ex) {
Log.e(TAG, "Couldn't create XML multi-get request", ex);
throw new DavException("Couldn't create multi-get request");
}
// submit REPORT request
HttpReport report = new HttpReport(location.toURI(), writer.toString());
response = httpClient.execute(report, context);
if (response.getStatusLine().getStatusCode()/100 == 3) {
location = DavRedirectStrategy.getLocation(report, response, context);
Log.i(TAG, "Redirection on REPORT multi-get; trying again at new content URL: " + location);
// don't forget to throw away the unneeded response content
HttpEntity entity = response.getEntity();
if (entity != null) { @Cleanup InputStream content = entity.getContent(); }
} else
break; // answer was NOT a redirection, continue
}
if (response == null)
throw new DavNoContentException();
try {
checkResponse(response); // will also handle Content-Location
processMultiStatus(response);
} finally {
response.close();
}
}
/* resource operations */
public void get(String acceptedType) throws URISyntaxException, IOException, HttpException, DavException {
HttpGet get = new HttpGet(location.toURI());
get.addHeader("Accept", acceptedType);
CloseableHttpResponse response = httpClient.execute(get, context);
try {
checkResponse(response);
HttpEntity entity = response.getEntity();
if (entity == null)
throw new DavNoContentException();
content = EntityUtils.toByteArray(entity);
} finally {
response.close();
}
}
// returns the ETag of the created/updated resource, if available (null otherwise)
public String put(byte[] data, PutMode mode) throws URISyntaxException, IOException, HttpException {
HttpPut put = new HttpPut(location.toURI());
put.setEntity(new ByteArrayEntity(data));
switch (mode) {
case ADD_DONT_OVERWRITE:
put.addHeader("If-None-Match", "*");
break;
case UPDATE_DONT_OVERWRITE:
put.addHeader("If-Match", (getETag() != null) ? getETag() : "*");
break;
}
if (getContentType() != null)
put.addHeader("Content-Type", getContentType());
CloseableHttpResponse response = httpClient.execute(put, context);
try {
checkResponse(response);
Header eTag = response.getLastHeader("ETag");
if (eTag != null)
return eTag.getValue();
} finally {
response.close();
}
return null;
}
public void delete() throws URISyntaxException, IOException, HttpException {
HttpDelete delete = new HttpDelete(location.toURI());
if (getETag() != null)
delete.addHeader("If-Match", getETag());
CloseableHttpResponse response = httpClient.execute(delete, context);
try {
checkResponse(response);
} finally {
response.close();
}
}
/* helpers */
protected void checkResponse(HttpResponse response) throws HttpException {
checkResponse(response.getStatusLine());
// handle Content-Location header (see RFC 4918 5.2 Collection Resources)
Header contentLocationHdr = response.getFirstHeader("Content-Location");
if (contentLocationHdr != null)
try {
// Content-Location was set, update location correspondingly
location = new URL(location, contentLocationHdr.getValue());
Log.d(TAG, "Set Content-Location to " + location);
} catch (MalformedURLException e) {
Log.w(TAG, "Ignoring invalid Content-Location", e);
}
}
protected static void checkResponse(StatusLine statusLine) throws HttpException {
int code = statusLine.getStatusCode();
if (code/100 == 1 || code/100 == 2) // everything OK
return;
String reason = code + " " + statusLine.getReasonPhrase();
switch (code) {
case HttpStatus.SC_UNAUTHORIZED:
throw new NotAuthorizedException(reason);
case HttpStatus.SC_NOT_FOUND:
throw new NotFoundException(reason);
case HttpStatus.SC_PRECONDITION_FAILED:
throw new PreconditionFailedException(reason);
default:
throw new HttpException(code, reason);
}
}
protected void processMultiStatus(HttpResponse response) throws IOException, HttpException, DavException {
if (response.getStatusLine().getStatusCode() != HttpStatus.SC_MULTI_STATUS)
throw new DavNoMultiStatusException();
HttpEntity entity = response.getEntity();
if (entity == null)
throw new DavNoContentException();
@Cleanup InputStream content = entity.getContent();
DavMultistatus multiStatus;
try {
Serializer serializer = new Persister();
multiStatus = serializer.read(DavMultistatus.class, content, false);
} catch (Exception ex) {
throw new DavException("Couldn't parse Multi-Status response on REPORT multi-get", ex);
}
if (multiStatus.response == null) // empty response
throw new DavNoContentException();
// member list will be built from response
List<WebDavResource> members = new LinkedList<WebDavResource>();
// iterate through all resources (either ourselves or member)
for (DavResponse singleResponse : multiStatus.response) {
URL href;
try {
href = new URL(location, URLUtils.sanitize(singleResponse.getHref().href));
} catch(IllegalArgumentException ex) {
Log.w(TAG, "Ignoring illegal member URI in multi-status response", ex);
continue;
}
Log.d(TAG, "Processing multi-status element: " + href);
// process known properties
HashMap<Property, String> properties = new HashMap<Property, String>();
List<String> supportedComponents = null;
byte[] data = null;
for (DavPropstat singlePropstat : singleResponse.getPropstat()) {
StatusLine status = BasicLineParser.parseStatusLine(singlePropstat.status, new BasicLineParser());
// ignore information about missing properties etc.
if (status.getStatusCode()/100 != 1 && status.getStatusCode()/100 != 2)
continue;
DavProp prop = singlePropstat.prop;
if (prop.currentUserPrincipal != null && prop.currentUserPrincipal.getHref() != null)
properties.put(Property.CURRENT_USER_PRINCIPAL, prop.currentUserPrincipal.getHref().href);
if (prop.currentUserPrivilegeSet != null) {
// privilege info available
boolean mayAll = false,
mayBind = false,
mayUnbind = false,
mayWrite = false,
mayWriteContent = false;
for (DavProp.Privilege privilege : prop.currentUserPrivilegeSet) {
if (privilege.getAll() != null) mayAll = true;
if (privilege.getBind() != null) mayBind = true;
if (privilege.getUnbind() != null) mayUnbind = true;
if (privilege.getWrite() != null) mayWrite = true;
if (privilege.getWriteContent() != null) mayWriteContent = true;
}
if (!mayAll && !mayWrite && !(mayWriteContent && mayBind && mayUnbind))
properties.put(Property.READ_ONLY, "1");
}
if (prop.addressbookHomeSet != null && prop.addressbookHomeSet.getHref() != null)
properties.put(Property.ADDRESSBOOK_HOMESET, URLUtils.ensureTrailingSlash(prop.addressbookHomeSet.getHref().href));
if (prop.calendarHomeSet != null && prop.calendarHomeSet.getHref() != null)
properties.put(Property.CALENDAR_HOMESET, URLUtils.ensureTrailingSlash(prop.calendarHomeSet.getHref().href));
if (prop.displayname != null)
properties.put(Property.DISPLAY_NAME, prop.displayname.getDisplayName());
if (prop.resourcetype != null) {
if (prop.resourcetype.getCollection() != null) {
properties.put(Property.IS_COLLECTION, "1");
// is a collection, ensure trailing slash
href = URLUtils.ensureTrailingSlash(href);
}
if (prop.resourcetype.getAddressbook() != null) { // CardDAV collection properties
properties.put(Property.IS_ADDRESSBOOK, "1");
if (prop.addressbookDescription != null)
properties.put(Property.DESCRIPTION, prop.addressbookDescription.getDescription());
if (prop.supportedAddressData != null)
for (DavProp.AddressDataType dataType : prop.supportedAddressData)
if ("text/vcard".equalsIgnoreCase(dataType.getContentType()))
// ignore "3.0" as it MUST be supported anyway
if ("4.0".equals(dataType.getVersion()))
properties.put(Property.VCARD_VERSION, VCardVersion.V4_0.getVersion());
}
if (prop.resourcetype.getCalendar() != null) { // CalDAV collection propertioes
properties.put(Property.IS_CALENDAR, "1");
if (prop.calendarDescription != null)
properties.put(Property.DESCRIPTION, prop.calendarDescription.getDescription());
if (prop.calendarColor != null)
properties.put(Property.COLOR, prop.calendarColor.getColor());
if (prop.calendarTimezone != null)
try {
properties.put(Property.TIMEZONE, Event.TimezoneDefToTzId(prop.calendarTimezone.getTimezone()));
} catch(IllegalArgumentException e) {
}
if (prop.supportedCalendarComponentSet != null) {
supportedComponents = new LinkedList<String>();
for (Comp component : prop.supportedCalendarComponentSet)
supportedComponents.add(component.getName());
}
}
}
if (prop.getctag != null)
properties.put(Property.CTAG, prop.getctag.getCTag());
if (prop.getetag != null)
properties.put(Property.ETAG, prop.getetag.getETag());
if (prop.calendarData != null && prop.calendarData.ical != null)
data = prop.calendarData.ical.getBytes();
else if (prop.addressData != null && prop.addressData.vcard != null)
data = prop.addressData.vcard.getBytes();
}
// about which resource is this response?
if (location.equals(href) || URLUtils.ensureTrailingSlash(location).equals(href)) { // about ourselves
this.properties.putAll(properties);
if (supportedComponents != null)
this.supportedComponents = supportedComponents;
this.content = data;
} else { // about a member
WebDavResource member = new WebDavResource(this, href);
member.properties = properties;
member.supportedComponents = supportedComponents;
member.content = data;
members.add(member);
}
}
this.members = members;
}
}

@ -1,6 +0,0 @@
net.fortuna.ical4j.timezone.update.enabled=false
ical4j.unfolding.relaxed=true
ical4j.parsing.relaxed=true
ical4j.compatibility.outlook=true

@ -1,19 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="at.bitfire.davdroid.test"
android:versionCode="1"
android:versionName="1.0" >
<uses-sdk android:minSdkVersion="14" />
<instrumentation
android:name="android.test.InstrumentationTestRunner"
android:targetPackage="at.bitfire.davdroid" />
<application
android:icon="@drawable/ic_launcher"
android:label="@string/app_name" >
<uses-library android:name="android.test.runner" />
</application>
</manifest>

@ -1,11 +0,0 @@
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//hacksw/handcal//NONSGML v1.0//EN
BEGIN:VEVENT
UID:all-day-0sec@example.com
DTSTAMP:20140101T000000Z
DTSTART;VALUE=DATE:19970714
DTEND;VALUE=DATE:19970714
SUMMARY:0 Sec Event
END:VEVENT
END:VCALENDAR

@ -1,11 +0,0 @@
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//hacksw/handcal//NONSGML v1.0//EN
BEGIN:VEVENT
UID:all-day-10days@example.com
DTSTAMP:20140101T000000Z
DTSTART;VALUE=DATE:19970714
DTEND;VALUE=DATE:19970724
SUMMARY:All-Day 10 Days
END:VEVENT
END:VCALENDAR

@ -1,11 +0,0 @@
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//hacksw/handcal//NONSGML v1.0//EN
BEGIN:VEVENT
UID:all-day-1day@example.com
DTSTAMP:20140101T000000Z
DTSTART;VALUE=DATE:19970714
DTEND;VALUE=DATE:19970714
SUMMARY:All-Day 1 Day
END:VEVENT
END:VCALENDAR

@ -1,11 +0,0 @@
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//hacksw/handcal//NONSGML v1.0//EN
BEGIN:VEVENT
UID:event-on-that-day@example.com
DTSTAMP:19970714T170000Z
ORGANIZER;CN=John Doe:MAILTO:john.doe@example.com
DTSTART;VALUE=DATE:19970714
SUMMARY:Bastille Day Party
END:VEVENT
END:VCALENDAR

@ -1,9 +0,0 @@
BEGIN:VCARD
VERSION:3.0
UID:2de59c6cc9
PRODID:-//ownCloud//NONSGML Contacts 0.2.5//EN
REV:2013-12-08T00:04:30+00:00
FN:test mctest
N:mctest;test;;;
IMPP;TYPE=WORK;X-SERVICE-TYPE=jabber:test-without-valid-scheme@test.tld
END:VCARD

@ -1,5 +0,0 @@
BEGIN:VCARD
VERSION:3.0
FN:VCard with invalid unknown properties
X-UNKNOWN@PROPERTY:MUST-NOT_CONTAIN?OTHER*LETTERS;
END:VCARD

@ -1,16 +0,0 @@
BEGIN:VCARD
VERSION:3.0
N:Gump;Forrest;Mr.
FN:Forrest Gump
ORG:Bubba Gump Shrimp Co.
TITLE:Shrimp Man
PHOTO;VALUE=URL;TYPE=GIF:http://www.example.com/dir_photos/my_photo.gif
TEL;TYPE=WORK,VOICE:(111) 555-1212
TEL;TYPE=HOME,VOICE:(404) 555-1212
ADR;TYPE=WORK:;;100 Waters Edge;Baytown;LA;30314;United States of America
LABEL;TYPE=WORK:100 Waters Edge\nBaytown, LA 30314\nUnited States of America
ADR;TYPE=HOME:;;42 Plantation St.;Baytown;LA;30314;United States of America
LABEL;TYPE=HOME:42 Plantation St.\nBaytown, LA 30314\nUnited States of America
EMAIL;TYPE=PREF,INTERNET:forrestgump@example.com
REV:2008-04-24T19:52:43Z
END:VCARD

Binary file not shown.

@ -1,16 +0,0 @@
BEGIN:VCARD
VERSION:3.0
N:Gump;Forrest
FN:Forrest Gump
ORG:Bubba Gump Shrimp Co.
TITLE:Shrimp Man
PHOTO;VALUE=URL;TYPE=GIF:http://www.example.com/dir_photos/my_photo.gif
TEL;TYPE=WORK,VOICE:(111) 555-1212
TEL;TYPE=HOME,VOICE:(404) 555-1212
ADR;TYPE=WORK:;;100 Waters Edge;Baytown;LA;30314;United States of America
LABEL;TYPE=WORK:100 Waters Edge\nBaytown, LA 30314\nUnited States of America
ADR;TYPE=HOME:;;42 Plantation St.;Baytown;LA;30314;United States of America
LABEL;TYPE=HOME:42 Plantation St.\nBaytown, LA 30314\nUnited States of America
EMAIL;TYPE=PREF,INTERNET:forrestgump@example.com
REV:2008-04-24T19:52:43Z
END:VCARD

@ -1,33 +0,0 @@
BEGIN:VCALENDAR
PRODID:-//Ximian//NONSGML Evolution Calendar//EN
VERSION:2.0
METHOD:PUBLISH
BEGIN:VTIMEZONE
TZID:/freeassociation.sourceforge.net/Tzfile/Europe/Vienna
X-LIC-LOCATION:Europe/Vienna
BEGIN:STANDARD
TZNAME:CET
DTSTART:19701027T030000
RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10
TZOFFSETFROM:+0200
TZOFFSETTO:+0100
END:STANDARD
BEGIN:DAYLIGHT
TZNAME:CEST
DTSTART:19700331T020000
RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=3
TZOFFSETFROM:+0100
TZOFFSETTO:+0200
END:DAYLIGHT
END:VTIMEZONE
BEGIN:VEVENT
UID:c252087c-7354-4722-aea9-0e7d86c01a25
DTSTAMP:20130926T151211Z
SUMMARY:Test-Ereignis im schönen Wien
DTSTART;TZID=/freeassociation.sourceforge.net/Tzfile/Europe/Vienna:20131009T170000
DTEND;TZID=/freeassociation.sourceforge.net/Tzfile/Europe/Vienna:20131009T180000
X-RADICALE-NAME:97929342-291a-434e-bf1a-fa1749bf99d0.ics
X-EVOLUTION-CALDAV-HREF:/radicale/rfc2822/default.ics/97929342-291a-434e-bf1a-fa1749bf99d0.ics
X-EVOLUTION-CALDAV-ETAG:\"-3264224243575339985\"
END:VEVENT
END:VCALENDAR

@ -1,20 +0,0 @@
# To enable ProGuard in your project, edit project.properties
# to define the proguard.config property as described in that file.
#
# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in ${sdk.dir}/tools/proguard/proguard-android.txt
# You can edit the include path and order by changing the ProGuard
# include property in project.properties.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# Add any project specific keep options here:
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}

@ -1,14 +0,0 @@
# This file is automatically generated by Android Tools.
# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
#
# This file must be checked in Version Control Systems.
#
# To customize properties used by the Ant build system edit
# "ant.properties", and override values to adapt the script to your
# project structure.
#
# To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home):
#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt
# Project target.
target=android-19

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">DavdroidTest</string>
</resources>

@ -1 +0,0 @@
node_modules

@ -1,6 +0,0 @@
{"plugins":[
"assets",
"redirect",
"dav",
"dav-invalid"
]}

@ -1,12 +0,0 @@
var RoboHydraHeadFilesystem = require("robohydra").heads.RoboHydraHeadFilesystem;
exports.getBodyParts = function(conf) {
return {
heads: [
new RoboHydraHeadFilesystem({
mountPath: '/assets/',
documentRoot: '../assets'
})
]
};
};

@ -1,51 +0,0 @@
var roboHydraHeadDAV = require("../headdav");
exports.getBodyParts = function(conf) {
return {
heads: [
/* address-book home set */
new RoboHydraHeadDAV({
path: "/dav-invalid/addressbooks/user%40domain/",
handler: function(req,res,next) {
if (req.method == "PROPFIND" && req.rawBody.toString().match(/addressbook-description/)) {
res.statusCode = 207;
res.write('\<?xml version="1.0" encoding="utf-8" ?>\
<multistatus xmlns="DAV:">\
<response>\
<href>/dav/addressbooks/user@domain/My Contacts:1.vcf/</href>\
<propstat>\
<prop xmlns:CARD="urn:ietf:params:xml:ns:carddav">\
<resourcetype>\
<collection/>\
<CARD:addressbook/>\
</resourcetype>\
<CARD:addressbook-description>\
Address Book with dubious characters in path\
</CARD:addressbook-description>\
</prop>\
<status>HTTP/1.1 200 OK</status>\
</propstat>\
</response>\
<response>\
<href>HTTPS://example.com/user@domain/absolute-url.vcf</href>\
<propstat>\
<prop xmlns:CARD="urn:ietf:params:xml:ns:carddav">\
<resourcetype>\
<collection/>\
<CARD:addressbook/>\
</resourcetype>\
<CARD:addressbook-description>\
Address Book with absolute URL and at sign in path\
</CARD:addressbook-description>\
</prop>\
<status>HTTP/1.1 200 OK</status>\
</propstat>\
</response>\
</multistatus>\
');
}
}
})
]
};
};

@ -1,269 +0,0 @@
var roboHydraHeadDAV = require("../headdav");
exports.getBodyParts = function(conf) {
return {
heads: [
/* 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/)) {
res.statusCode = 207;
res.write('\<?xml version="1.0" encoding="utf-8" ?>\
<multistatus xmlns="DAV:">\
<response>\
<href>' + req.url + '</href> \
<propstat>\
<prop>\
<CARD:addressbook-home-set xmlns:CARD="urn:ietf:params:xml:ns:carddav">\
<href>/dav/addressbooks/test</href>\
</CARD:addressbook-home-set>\
<CAL:calendar-home-set xmlns:CAL="urn:ietf:params:xml:ns:caldav">\
<href>/dav/calendars/test/</href>\
</CAL:calendar-home-set>\
</prop>\
<status>HTTP/1.1 200 OK</status>\
</propstat>\
</response>\
</multistatus>\
');
}
}
}),
/* address-book home set */
new RoboHydraHeadDAV({
path: "/dav/addressbooks/test/",
handler: function(req,res,next) {
if (!req.url.match(/\/$/)) {
res.statusCode = 302;
res.headers['location'] = "/dav/addressbooks/test/";
}
else if (req.method == "PROPFIND" && req.rawBody.toString().match(/addressbook-description/)) {
res.statusCode = 207;
res.write('\<?xml version="1.0" encoding="utf-8" ?>\
<multistatus xmlns="DAV:">\
<response>\
<href>/dav/addressbooks/test/useless-member</href>\
<propstat>\
<prop>\
<resourcetype/>\
</prop>\
<status>HTTP/1.1 200 OK</status>\
</propstat>\
</response>\
<response>\
<href>/dav/addressbooks/test/default-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: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>\
</multistatus>\
');
}
}
}),
/* calendar home set */
new RoboHydraHeadDAV({
path: "/dav/calendars/test/",
handler: function(req,res,next) {
if (req.method == "PROPFIND" && req.rawBody.toString().match(/calendar-description/)) {
res.statusCode = 207;
res.write('\<?xml version="1.0" encoding="utf-8" ?>\
<multistatus xmlns="DAV:" xmlns:CAL="urn:ietf:params:xml:ns:caldav">\
<response>\
<href>/dav/calendars/test/shared.forbidden</href>\
<propstat>\
<prop>\
<resourcetype/>\
</prop>\
<status>HTTP/1.1 403 Forbidden</status>\
</propstat>\
</response>\
<response>\
<href>/dav/calendars/test/private.ics</href>\
<propstat>\
<prop>\
<resourcetype>\
<collection/>\
<CAL:calendar/>\
</resourcetype>\
<displayname>Private Calendar</displayname>\
<CAL:calendar-description>This is my private calendar.</CAL:calendar-description>\
</prop>\
<status>HTTP/1.1 200 OK</status>\
</propstat>\
</response>\
<response>\
<href>/dav/calendars/test/work.ics</href>\
<propstat>\
<prop>\
<resourcetype>\
<collection/>\
<CAL:calendar/>\
</resourcetype>\
<current-user-privilege-set>\
<privilege><read/></privilege>\
</current-user-privilege-set>\
<displayname>Work Calendar</displayname>\
<A:calendar-color xmlns:A="http://apple.com/ns/ical/">0xFF00FF</A:calendar-color>\
</prop>\
<status>HTTP/1.1 200 OK</status>\
</propstat>\
</response>\
</multistatus>\
');
}
}
}),
/* non-existing file */
new RoboHydraHeadDAV({
path: "/dav/collection/new.file",
handler: function(req,res,next) {
if (req.method == "PUT") {
if (req.headers['if-match']) /* can't overwrite new file */
res.statusCode = 412;
else {
res.statusCode = 201;
res.headers["ETag"] = "has-just-been-created";
}
} else if (req.method == "DELETE")
res.statusCode = 404;
}
}),
/* existing file */
new RoboHydraHeadDAV({
path: "/dav/collection/existing.file",
handler: function(req,res,next) {
if (req.method == "PUT") {
if (req.headers['if-none-match']) /* requested "don't overwrite", but this file exists */
res.statusCode = 412;
else {
res.statusCode = 204;
res.headers["ETag"] = "has-just-been-updated";
}
} else if (req.method == "DELETE")
res.statusCode = 204;
}
}),
/* address-book multiget */
new RoboHydraHeadDAV({
path: "/dav/addressbooks/default.vcf/",
handler: function(req,res,next) {
if (req.method == "REPORT" && req.rawBody.toString().match(/addressbook-multiget[\s\S]+<prop>[\s\S]+<href>/m)) {
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/1.vcf</href>\
<propstat>\
<prop>\
<getetag/>\
<CARD:address-data>BEGIN:VCARD\
VERSION:3.0\
NICKNAME:MULTIGET1\
UID:1\
END:VCARD\
</CARD:address-data>\
</prop>\
<status>HTTP/1.1 200 OK</status>\
</propstat>\
</response>\
<response>\
<href>/dav/addressbooks/default.vcf/2.vcf</href>\
<propstat>\
<prop>\
<getetag/>\
<CARD:address-data>BEGIN:VCARD\
VERSION:3.0\
NICKNAME:MULTIGET2\
UID:2\
END:VCARD\
</CARD:address-data>\
</prop>\
<status>HTTP/1.1 200 OK</status>\
</propstat>\
</response>\
</multistatus>\
');
}
}
}),
]
};
};

@ -1,57 +0,0 @@
var roboHydra = require("robohydra"),
roboHydraHeads = roboHydra.heads,
roboHydraHead = roboHydraHeads.RoboHydraHead;
RoboHydraHeadDAV = roboHydraHeads.roboHydraHeadType({
name: 'WebDAV Server',
mandatoryProperties: [ 'path' ],
optionalProperties: [ 'handler' ],
parentPropBuilder: function() {
var myHandler = this.handler;
return {
path: this.path,
handler: function(req,res,next) {
// default DAV behavior
res.headers['DAV'] = 'addressbook, calendar-access';
res.statusCode = 500;
// verify Accept header
var accept = req.headers['accept'];
if (req.method == "GET" && (accept == undefined || !accept.match(/text\/(calendar|vcard|xml)/)) ||
(req.method == "PROPFIND" || req.method == "REPORT") && (accept == undefined || accept != "text/xml"))
res.statusCode = 406;
// DAV operations that work on all URLs
else if (req.method == "OPTIONS") {
res.statusCode = 204;
res.headers['Allow'] = 'OPTIONS, PROPFIND, GET, PUT, DELETE, REPORT';
} else if (req.method == "PROPFIND" && req.rawBody.toString().match(/current-user-principal/)) {
res.statusCode = 207;
res.write('\<?xml version="1.0" encoding="utf-8" ?>\
<multistatus xmlns="DAV:">\
<response>\
<href>' + req.url + '</href> \
<propstat>\
<prop>\
<current-user-principal>\
<href>/dav/principals/users/test</href>\
</current-user-principal>\
</prop>\
<status>HTTP/1.1 200 OK</status>\
</propstat>\
</response>\
</multistatus>\
');
} else if (typeof myHandler != 'undefined')
myHandler(req,res,next);
res.end();
}
}
}
});
module.exports = RoboHydraHeadDAV;

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save