mirror of
https://github.com/etesync/android
synced 2024-11-22 07:58:09 +00:00
Merge with origin/master
This commit is contained in:
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>
|
||||
|
142
build.xml
142
build.xml
@ -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.
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
test/robohydra/.gitignore
vendored
1
test/robohydra/.gitignore
vendored
@ -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…
Reference in New Issue
Block a user