mirror of
https://github.com/etesync/android
synced 2024-11-13 03:09:10 +00:00
Remove legacy calendar/task/WebDAV code
This commit is contained in:
parent
c2e9b27831
commit
d348f54deb
@ -50,27 +50,9 @@ configurations.all {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// Apache Commons
|
||||
compile 'org.apache.commons:commons-lang3:3.4'
|
||||
compile 'commons-io:commons-io:2.4'
|
||||
// Lombok for useful @helpers
|
||||
provided 'org.projectlombok:lombok:1.16.6'
|
||||
// ical4j for parsing/generating iCalendars
|
||||
compile('org.mnode.ical4j:ical4j:2.0-beta1') { // update ICAL_PRODID in Constants too!
|
||||
// we don't need content builders, see https://github.com/ical4j/ical4j/wiki/Groovy
|
||||
exclude group: 'org.codehaus.groovy', module: 'groovy-all'
|
||||
}
|
||||
compile('org.slf4j:slf4j-android:1.7.12') // slf4j is used by ical4j
|
||||
// dnsjava for querying SRV/TXT records
|
||||
compile 'dnsjava:dnsjava:2.1.7'
|
||||
// HttpClient 4.3, Android flavour for WebDAV operations
|
||||
compile 'org.apache.httpcomponents:httpclient-android:4.3.5.1'
|
||||
//compile project(':lib:httpclient-android')
|
||||
// SimpleXML for parsing and generating WebDAV messages
|
||||
compile('org.simpleframework:simple-xml:2.7.1') {
|
||||
exclude group: 'stax', module: 'stax-api'
|
||||
exclude group: 'xpp3', module: 'xpp3'
|
||||
}
|
||||
provided 'org.projectlombok:lombok:1.16.6'
|
||||
compile('org.slf4j:slf4j-android:1.7.12')
|
||||
|
||||
compile project(':dav4android')
|
||||
compile project(':vcard4android')
|
||||
|
@ -7,8 +7,6 @@
|
||||
*/
|
||||
package at.bitfire.davdroid;
|
||||
|
||||
import net.fortuna.ical4j.model.property.ProdId;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@ -19,7 +17,7 @@ public class Constants {
|
||||
WEB_URL_HELP = "https://davdroid.bitfire.at/configuration?pk_campaign=davdroid-app",
|
||||
WEB_URL_VIEW_LOGS = "https://github.com/bitfireAT/davdroid/wiki/How-to-view-the-logs";
|
||||
|
||||
public static final ProdId ICAL_PRODID = new ProdId("-//bitfire web engineering//DAVdroid " + BuildConfig.VERSION_CODE + " (ical4j 2.0-beta1)//EN");
|
||||
//public static final ProdId ICAL_PRODID = new ProdId("-//bitfire web engineering//DAVdroid " + BuildConfig.VERSION_CODE + " (ical4j 2.0-beta1)//EN");
|
||||
|
||||
public static final Logger log = LoggerFactory.getLogger("davdroid");
|
||||
}
|
||||
|
@ -1,166 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2013 – 2015 Ricki Hirner (bitfire web engineering).
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the GNU Public License v3.0
|
||||
* which accompanies this distribution, and is available at
|
||||
* http://www.gnu.org/licenses/gpl.html
|
||||
*/
|
||||
|
||||
package at.bitfire.davdroid;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import net.fortuna.ical4j.model.Date;
|
||||
import net.fortuna.ical4j.model.DateList;
|
||||
import net.fortuna.ical4j.model.DateTime;
|
||||
import net.fortuna.ical4j.model.TimeZone;
|
||||
import net.fortuna.ical4j.model.TimeZoneRegistry;
|
||||
import net.fortuna.ical4j.model.TimeZoneRegistryFactory;
|
||||
import net.fortuna.ical4j.model.parameter.Value;
|
||||
import net.fortuna.ical4j.model.property.DateListProperty;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import java.text.DateFormat;
|
||||
import java.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.SimpleTimeZone;
|
||||
|
||||
public class DateUtils {
|
||||
private final static String TAG = "davdroid.DateUtils";
|
||||
|
||||
public static final TimeZoneRegistry tzRegistry = TimeZoneRegistryFactory.getInstance().createRegistry();
|
||||
|
||||
static {
|
||||
// disable automatic time-zone updates (causes unwanted network traffic)
|
||||
System.setProperty("net.fortuna.ical4j.timezone.update.enabled", "false");
|
||||
}
|
||||
|
||||
|
||||
// time zones
|
||||
|
||||
public static String findAndroidTimezoneID(String tz) {
|
||||
String deviceTZ = null;
|
||||
String availableTZs[] = SimpleTimeZone.getAvailableIDs();
|
||||
|
||||
// first, try to find an exact match (case insensitive)
|
||||
for (String availableTZ : availableTZs)
|
||||
if (availableTZ.equalsIgnoreCase(tz)) {
|
||||
deviceTZ = availableTZ;
|
||||
break;
|
||||
}
|
||||
|
||||
// if that doesn't work, try to find something else that matches
|
||||
if (deviceTZ == null) {
|
||||
Log.w(TAG, "Coulnd't find time zone with matching identifiers, trying to guess");
|
||||
for (String availableTZ : availableTZs)
|
||||
if (StringUtils.indexOfIgnoreCase(tz, availableTZ) != -1) {
|
||||
deviceTZ = availableTZ;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// if that doesn't work, use UTC as fallback
|
||||
if (deviceTZ == null) {
|
||||
final String defaultTZ = TimeZone.getDefault().getID();
|
||||
Log.e(TAG, "Couldn't identify time zone, using system default (" + defaultTZ + ") as fallback");
|
||||
deviceTZ = defaultTZ;
|
||||
}
|
||||
|
||||
return deviceTZ;
|
||||
}
|
||||
|
||||
|
||||
// recurrence sets
|
||||
|
||||
/**
|
||||
* Concatenates, if necessary, multiple RDATE/EXDATE lists and converts them to
|
||||
* a formatted string which Android calendar provider can process.
|
||||
* Android expects this format: "[TZID;]date1,date2,date3" where date is "yyyymmddThhmmss" (when
|
||||
* TZID is given) or "yyyymmddThhmmssZ". We don't use the TZID format here because then we're limited
|
||||
* to one time-zone, while an iCalendar may contain multiple EXDATE/RDATE lines with different time zones.
|
||||
* @param dates one more more lists of RDATE or EXDATE
|
||||
* @param allDay indicates whether the event is an all-day event or not
|
||||
* @return formatted string for Android calendar provider:
|
||||
* - in case of all-day events, all dates/times are returned as yyyymmddT000000Z
|
||||
* - in case of timed events, all dates/times are returned as UTC time: yyyymmddThhmmssZ
|
||||
*/
|
||||
public static String recurrenceSetsToAndroidString(List<? extends DateListProperty> dates, boolean allDay) throws ParseException {
|
||||
List<String> strDates = new LinkedList<>();
|
||||
|
||||
/* rdate/exdate: DATE DATE_TIME
|
||||
all-day store as ...T000000Z cut off time and store as ...T000000Z
|
||||
event with time (ignored) store as ...ThhmmssZ
|
||||
*/
|
||||
final DateFormat dateFormatUtcMidnight = new SimpleDateFormat("yyyyMMdd'T'000000'Z'");
|
||||
|
||||
for (DateListProperty dateListProp : dates) {
|
||||
final Value type = dateListProp.getDates().getType();
|
||||
|
||||
if (Value.DATE_TIME.equals(type)) { // DATE-TIME values will be stored in UTC format for Android
|
||||
if (allDay) {
|
||||
DateList dateList = dateListProp.getDates();
|
||||
for (Date date : dateList)
|
||||
strDates.add(dateFormatUtcMidnight.format(date));
|
||||
} else {
|
||||
dateListProp.setUtc(true);
|
||||
strDates.add(dateListProp.getValue());
|
||||
}
|
||||
|
||||
} else if (Value.DATE.equals(type)) // DATE values have to be converted to DATE-TIME <date>T000000Z for Android
|
||||
for (Date date : dateListProp.getDates())
|
||||
strDates.add(dateFormatUtcMidnight.format(date));
|
||||
}
|
||||
return StringUtils.join(strDates, ",");
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a formatted string as provided by the Android calendar provider and returns a DateListProperty
|
||||
* constructed from these values.
|
||||
* @param dbStr formatted string from Android calendar provider (RDATE/EXDATE field)
|
||||
* expected format: "[TZID;]date1,date2,date3" where date is "yyyymmddThhmmss[Z]"
|
||||
* @param type subclass of DateListProperty, e.g. RDate or ExDate
|
||||
* @param allDay true: list will contain DATE values; false: list will contain DATE_TIME values
|
||||
* @return instance of "type" containing the parsed dates/times from the string
|
||||
*/
|
||||
public static DateListProperty androidStringToRecurrenceSet(String dbStr, Class<? extends DateListProperty> type, boolean allDay) throws ParseException {
|
||||
// 1. split string into time zone and actual dates
|
||||
TimeZone timeZone;
|
||||
String datesStr;
|
||||
final int limiter = dbStr.indexOf(';');
|
||||
if (limiter != -1) { // TZID given
|
||||
timeZone = DateUtils.tzRegistry.getTimeZone(dbStr.substring(0, limiter));
|
||||
datesStr = dbStr.substring(limiter + 1);
|
||||
} else {
|
||||
timeZone = null;
|
||||
datesStr = dbStr;
|
||||
}
|
||||
|
||||
// 2. process date string and generate list of DATEs or DATE-TIMEs
|
||||
DateList dateList;
|
||||
if (allDay) {
|
||||
dateList = new DateList(Value.DATE);
|
||||
for (String s: StringUtils.split(datesStr, ','))
|
||||
dateList.add(new Date(new DateTime(s)));
|
||||
} else {
|
||||
dateList = new DateList(datesStr, Value.DATE_TIME, timeZone);
|
||||
if (timeZone == null)
|
||||
dateList.setUtc(true);
|
||||
}
|
||||
|
||||
// 3. generate requested DateListProperty (RDate/ExDate) from list of DATEs or DATE-TIMEs
|
||||
DateListProperty list;
|
||||
try {
|
||||
list = (DateListProperty)type.getDeclaredConstructor(new Class[] { DateList.class } ).newInstance(dateList);
|
||||
if (dateList.getTimeZone() != null)
|
||||
list.setTimeZone(dateList.getTimeZone());
|
||||
} catch (Exception e) {
|
||||
throw new ParseException("Couldn't create date/time list by reflection", -1);
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
}
|
@ -1,80 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2013 – 2015 Ricki Hirner (bitfire web engineering).
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the GNU Public License v3.0
|
||||
* which accompanies this distribution, and is available at
|
||||
* http://www.gnu.org/licenses/gpl.html
|
||||
*/
|
||||
package at.bitfire.davdroid;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
|
||||
public class URIUtils {
|
||||
private static final String TAG = "davdroid.URIUtils";
|
||||
|
||||
|
||||
public static String ensureTrailingSlash(String href) {
|
||||
if (!href.endsWith("/")) {
|
||||
Log.d(TAG, "Implicitly appending trailing slash to collection " + href);
|
||||
return href + "/";
|
||||
} else
|
||||
return href;
|
||||
}
|
||||
|
||||
public static URI ensureTrailingSlash(URI href) {
|
||||
if (!href.getPath().endsWith("/")) {
|
||||
try {
|
||||
URI newURI = new URI(href.getScheme(), href.getAuthority(), href.getPath() + "/", null, null);
|
||||
Log.d(TAG, "Appended trailing slash to collection " + href + " -> " + newURI);
|
||||
href = newURI;
|
||||
} catch (URISyntaxException e) {
|
||||
}
|
||||
}
|
||||
return href;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Parse a received absolute/relative URL and generate a normalized URI that can be compared.
|
||||
* @param original URI to be parsed, may be absolute or relative. Encoded characters will be decoded!
|
||||
* @param mustBePath true if it's known that original is a path (may contain ":") and not an URI, i.e. ":" is not the scheme separator
|
||||
* @return normalized URI
|
||||
* @throws URISyntaxException
|
||||
*/
|
||||
public static URI parseURI(String original, boolean mustBePath) throws URISyntaxException {
|
||||
if (mustBePath) {
|
||||
// may contain ":"
|
||||
// case 1: "my:file" won't be parsed by URI correctly because it would consider "my" as URI scheme
|
||||
// case 2: "path/my:file" will be parsed by URI correctly
|
||||
// case 3: "my:path/file" won't be parsed by URI correctly because it would consider "my" as URI scheme
|
||||
int idxSlash = original.indexOf('/'),
|
||||
idxColon = original.indexOf(':');
|
||||
if (idxColon != -1) {
|
||||
// colon present
|
||||
if ((idxSlash != -1) && idxSlash < idxColon) // There's a slash, and it's before the colon → everything OK
|
||||
;
|
||||
else // No slash before the colon; we have to put it there
|
||||
original = "./" + original;
|
||||
}
|
||||
}
|
||||
|
||||
// escape some common invalid characters – servers keep sending unescaped crap like "my calendar.ics" or "{guid}.vcf"
|
||||
// this is only a hack, because for instance, "[" may be valid in URLs (IPv6 literal in host name)
|
||||
String repaired = original
|
||||
.replaceAll(" ", "%20")
|
||||
.replaceAll("\\{", "%7B")
|
||||
.replaceAll("\\}", "%7D");
|
||||
if (!repaired.equals(original))
|
||||
Log.w(TAG, "Repaired invalid URL: " + original + " -> " + repaired);
|
||||
|
||||
URI uri = new URI(repaired);
|
||||
URI normalized = new URI(uri.getScheme(), uri.getAuthority(), uri.getPath(), uri.getQuery(), uri.getFragment());
|
||||
Log.v(TAG, "Normalized URI " + original + " -> " + normalized.toASCIIString() + " assuming that it was " +
|
||||
(mustBePath ? "a path name" : "an URI or path name"));
|
||||
return normalized;
|
||||
}
|
||||
|
||||
}
|
@ -1,78 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2013 – 2015 Ricki Hirner (bitfire web engineering).
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the GNU Public License v3.0
|
||||
* which accompanies this distribution, and is available at
|
||||
* http://www.gnu.org/licenses/gpl.html
|
||||
*/
|
||||
package at.bitfire.davdroid.resource;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import org.apache.http.impl.client.CloseableHttpClient;
|
||||
import org.simpleframework.xml.Serializer;
|
||||
import org.simpleframework.xml.core.Persister;
|
||||
|
||||
import java.io.StringWriter;
|
||||
import java.net.URISyntaxException;
|
||||
|
||||
import at.bitfire.davdroid.webdav.DavCalendarQuery;
|
||||
import at.bitfire.davdroid.webdav.DavCompFilter;
|
||||
import at.bitfire.davdroid.webdav.DavFilter;
|
||||
import at.bitfire.davdroid.webdav.DavMultiget;
|
||||
import at.bitfire.davdroid.webdav.DavProp;
|
||||
|
||||
public class CalDavCalendar extends WebDavCollection<Event> {
|
||||
private final static String TAG = "davdroid.CalDAVCalendar";
|
||||
|
||||
public CalDavCalendar(CloseableHttpClient httpClient, String baseURL, String user, String password, boolean preemptiveAuth) throws URISyntaxException {
|
||||
super(httpClient, baseURL, user, password, preemptiveAuth);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String memberAcceptedMimeTypes()
|
||||
{
|
||||
return "text/calendar";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected DavMultiget.Type multiGetType() {
|
||||
return DavMultiget.Type.CALENDAR;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Event newResourceSkeleton(String name, String ETag) {
|
||||
return new Event(name, ETag);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String getMemberETagsQuery() {
|
||||
DavCalendarQuery query = new DavCalendarQuery();
|
||||
|
||||
// prop
|
||||
DavProp prop = new DavProp();
|
||||
prop.setGetetag(new DavProp.GetETag());
|
||||
query.setProp(prop);
|
||||
|
||||
// filter
|
||||
DavFilter filter = new DavFilter();
|
||||
query.setFilter(filter);
|
||||
|
||||
DavCompFilter compFilter = new DavCompFilter("VCALENDAR");
|
||||
filter.setCompFilter(compFilter);
|
||||
|
||||
compFilter.setCompFilter(new DavCompFilter("VEVENT"));
|
||||
|
||||
Serializer serializer = new Persister();
|
||||
StringWriter writer = new StringWriter();
|
||||
try {
|
||||
serializer.write(query, writer);
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Couldn't prepare REPORT query", e);
|
||||
return null;
|
||||
}
|
||||
|
||||
return writer.toString();
|
||||
}
|
||||
}
|
@ -1,80 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2013 – 2015 Ricki Hirner (bitfire web engineering).
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the GNU Public License v3.0
|
||||
* which accompanies this distribution, and is available at
|
||||
* http://www.gnu.org/licenses/gpl.html
|
||||
*/
|
||||
|
||||
package at.bitfire.davdroid.resource;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import org.apache.http.impl.client.CloseableHttpClient;
|
||||
import org.simpleframework.xml.Serializer;
|
||||
import org.simpleframework.xml.core.Persister;
|
||||
|
||||
import java.io.StringWriter;
|
||||
import java.net.URISyntaxException;
|
||||
|
||||
import at.bitfire.davdroid.webdav.DavCalendarQuery;
|
||||
import at.bitfire.davdroid.webdav.DavCompFilter;
|
||||
import at.bitfire.davdroid.webdav.DavFilter;
|
||||
import at.bitfire.davdroid.webdav.DavMultiget;
|
||||
import at.bitfire.davdroid.webdav.DavProp;
|
||||
|
||||
public class CalDavTaskList extends WebDavCollection<Task> {
|
||||
private final static String TAG = "davdroid.CalDAVTaskList";
|
||||
|
||||
public CalDavTaskList(CloseableHttpClient httpClient, String baseURL, String user, String password, boolean preemptiveAuth) throws URISyntaxException {
|
||||
super(httpClient, baseURL, user, password, preemptiveAuth);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String memberAcceptedMimeTypes()
|
||||
{
|
||||
return "text/calendar";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected DavMultiget.Type multiGetType() {
|
||||
return DavMultiget.Type.CALENDAR;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Task newResourceSkeleton(String name, String ETag) {
|
||||
return new Task(name, ETag);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String getMemberETagsQuery() {
|
||||
DavCalendarQuery query = new DavCalendarQuery();
|
||||
|
||||
// prop
|
||||
DavProp prop = new DavProp();
|
||||
prop.setGetetag(new DavProp.GetETag());
|
||||
query.setProp(prop);
|
||||
|
||||
// filter
|
||||
DavFilter filter = new DavFilter();
|
||||
query.setFilter(filter);
|
||||
|
||||
DavCompFilter compFilter = new DavCompFilter("VCALENDAR");
|
||||
filter.setCompFilter(compFilter);
|
||||
|
||||
compFilter.setCompFilter(new DavCompFilter("VTODO"));
|
||||
|
||||
Serializer serializer = new Persister();
|
||||
StringWriter writer = new StringWriter();
|
||||
try {
|
||||
serializer.write(query, writer);
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Couldn't prepare REPORT query", e);
|
||||
return null;
|
||||
}
|
||||
|
||||
return writer.toString();
|
||||
}
|
||||
|
||||
}
|
@ -1,353 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2013 – 2015 Ricki Hirner (bitfire web engineering).
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the GNU Public License v3.0
|
||||
* which accompanies this distribution, and is available at
|
||||
* http://www.gnu.org/licenses/gpl.html
|
||||
*/
|
||||
package at.bitfire.davdroid.resource;
|
||||
|
||||
import android.text.format.Time;
|
||||
import android.util.Log;
|
||||
|
||||
import net.fortuna.ical4j.data.CalendarOutputter;
|
||||
import net.fortuna.ical4j.data.ParserException;
|
||||
import net.fortuna.ical4j.model.Component;
|
||||
import net.fortuna.ical4j.model.ComponentList;
|
||||
import net.fortuna.ical4j.model.Date;
|
||||
import net.fortuna.ical4j.model.DateTime;
|
||||
import net.fortuna.ical4j.model.Property;
|
||||
import net.fortuna.ical4j.model.PropertyList;
|
||||
import net.fortuna.ical4j.model.ValidationException;
|
||||
import net.fortuna.ical4j.model.component.VAlarm;
|
||||
import net.fortuna.ical4j.model.component.VEvent;
|
||||
import net.fortuna.ical4j.model.property.Attendee;
|
||||
import net.fortuna.ical4j.model.property.Clazz;
|
||||
import net.fortuna.ical4j.model.property.DateProperty;
|
||||
import net.fortuna.ical4j.model.property.Description;
|
||||
import net.fortuna.ical4j.model.property.DtEnd;
|
||||
import net.fortuna.ical4j.model.property.DtStart;
|
||||
import net.fortuna.ical4j.model.property.Duration;
|
||||
import net.fortuna.ical4j.model.property.ExDate;
|
||||
import net.fortuna.ical4j.model.property.ExRule;
|
||||
import net.fortuna.ical4j.model.property.Location;
|
||||
import net.fortuna.ical4j.model.property.Organizer;
|
||||
import net.fortuna.ical4j.model.property.RDate;
|
||||
import net.fortuna.ical4j.model.property.RRule;
|
||||
import net.fortuna.ical4j.model.property.RecurrenceId;
|
||||
import net.fortuna.ical4j.model.property.Status;
|
||||
import net.fortuna.ical4j.model.property.Summary;
|
||||
import net.fortuna.ical4j.model.property.Transp;
|
||||
import net.fortuna.ical4j.model.property.Uid;
|
||||
import net.fortuna.ical4j.model.property.Version;
|
||||
import net.fortuna.ical4j.util.TimeZones;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.Calendar;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.TimeZone;
|
||||
|
||||
import at.bitfire.davdroid.Constants;
|
||||
import at.bitfire.davdroid.DateUtils;
|
||||
import lombok.Cleanup;
|
||||
import lombok.Getter;
|
||||
import lombok.NonNull;
|
||||
|
||||
|
||||
public class Event extends iCalendar {
|
||||
private final static String TAG = "davdroid.Event";
|
||||
|
||||
protected RecurrenceId recurrenceId;
|
||||
|
||||
protected String summary, location, description;
|
||||
|
||||
protected DtStart dtStart;
|
||||
protected DtEnd dtEnd;
|
||||
|
||||
// lists must not be set to null (because they're iterated using "for"), so only getters are exposed
|
||||
protected Duration duration;
|
||||
@Getter private List<RDate> rdates = new LinkedList<>();
|
||||
protected RRule rrule;
|
||||
@Getter private List<ExDate> exdates = new LinkedList<>();
|
||||
protected ExRule exrule;
|
||||
@Getter private List<Event> exceptions = new LinkedList<>();
|
||||
|
||||
protected Boolean forPublic;
|
||||
protected Status status;
|
||||
|
||||
protected boolean opaque;
|
||||
|
||||
protected Organizer organizer;
|
||||
@Getter private List<Attendee> attendees = new LinkedList<>();
|
||||
|
||||
@Getter private List<VAlarm> alarms = new LinkedList<>();
|
||||
|
||||
|
||||
public Event(String name, String ETag) {
|
||||
super(name, ETag);
|
||||
}
|
||||
|
||||
public Event(long localID, String name, String ETag) {
|
||||
super(localID, name, ETag);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public void parseEntity(@NonNull InputStream entity, Charset charset, AssetDownloader downloader) throws IOException, InvalidResourceException {
|
||||
final net.fortuna.ical4j.model.Calendar ical;
|
||||
try {
|
||||
if (charset != null) {
|
||||
@Cleanup InputStreamReader reader = new InputStreamReader(entity, charset);
|
||||
ical = calendarBuilder.build(reader);
|
||||
} else
|
||||
ical = calendarBuilder.build(entity);
|
||||
|
||||
if (ical == null)
|
||||
throw new InvalidResourceException("No iCalendar found");
|
||||
} catch (ParserException e) {
|
||||
throw new InvalidResourceException(e);
|
||||
}
|
||||
|
||||
ComponentList events = ical.getComponents(Component.VEVENT);
|
||||
if (events == null || events.isEmpty())
|
||||
throw new InvalidResourceException("No VEVENT found");
|
||||
|
||||
// find master VEVENT (the one that is not an exception, i.e. the one without RECURRENCE-ID)
|
||||
VEvent master = null;
|
||||
for (VEvent event : (Iterable<VEvent>)events)
|
||||
if (event.getRecurrenceId() == null) {
|
||||
master = event;
|
||||
break;
|
||||
}
|
||||
if (master == null)
|
||||
throw new InvalidResourceException("No VEVENT without RECURRENCE-ID found");
|
||||
// set event data from master VEVENT
|
||||
fromVEvent(master);
|
||||
|
||||
// find and process exceptions
|
||||
for (VEvent event : (Iterable<VEvent>)events)
|
||||
if (event.getRecurrenceId() != null) {
|
||||
Event exception = new Event(name, null);
|
||||
exception.fromVEvent(event);
|
||||
exceptions.add(exception);
|
||||
}
|
||||
}
|
||||
|
||||
protected void fromVEvent(VEvent event) throws InvalidResourceException {
|
||||
if (event.getUid() != null)
|
||||
uid = event.getUid().getValue();
|
||||
else {
|
||||
Log.w(TAG, "Received VEVENT without UID, generating new one");
|
||||
generateUID();
|
||||
}
|
||||
recurrenceId = event.getRecurrenceId();
|
||||
|
||||
if ((dtStart = event.getStartDate()) == null || (dtEnd = event.getEndDate()) == null)
|
||||
throw new InvalidResourceException("Invalid start time/end time/duration");
|
||||
|
||||
validateTimeZone(dtStart);
|
||||
validateTimeZone(dtEnd);
|
||||
|
||||
// all-day events and "events on that day":
|
||||
// * related UNIX times must be in UTC
|
||||
// * must have a duration (set to one day if missing)
|
||||
if (!isDateTime(dtStart) && !dtEnd.getDate().after(dtStart.getDate())) {
|
||||
Log.i(TAG, "Repairing iCal: DTEND := DTSTART+1");
|
||||
Calendar c = Calendar.getInstance(TimeZone.getTimeZone(Time.TIMEZONE_UTC));
|
||||
c.setTime(dtStart.getDate());
|
||||
c.add(Calendar.DATE, 1);
|
||||
dtEnd.setDate(new Date(c.getTimeInMillis()));
|
||||
}
|
||||
|
||||
rrule = (RRule)event.getProperty(Property.RRULE);
|
||||
for (RDate rdate : (List<RDate>)(List<?>)event.getProperties(Property.RDATE))
|
||||
rdates.add(rdate);
|
||||
exrule = (ExRule)event.getProperty(Property.EXRULE);
|
||||
for (ExDate exdate : (List<ExDate>)(List<?>)event.getProperties(Property.EXDATE))
|
||||
exdates.add(exdate);
|
||||
|
||||
if (event.getSummary() != null)
|
||||
summary = event.getSummary().getValue();
|
||||
if (event.getLocation() != null)
|
||||
location = event.getLocation().getValue();
|
||||
if (event.getDescription() != null)
|
||||
description = event.getDescription().getValue();
|
||||
|
||||
status = event.getStatus();
|
||||
opaque = event.getTransparency() != Transp.TRANSPARENT;
|
||||
|
||||
organizer = event.getOrganizer();
|
||||
for (Attendee attendee : (List<Attendee>)(List<?>)event.getProperties(Property.ATTENDEE))
|
||||
attendees.add(attendee);
|
||||
|
||||
Clazz classification = event.getClassification();
|
||||
if (classification != null) {
|
||||
if (classification == Clazz.PUBLIC)
|
||||
forPublic = true;
|
||||
else if (classification == Clazz.CONFIDENTIAL || classification == Clazz.PRIVATE)
|
||||
forPublic = false;
|
||||
}
|
||||
|
||||
this.alarms = event.getAlarms();
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public ByteArrayOutputStream toEntity() throws IOException {
|
||||
net.fortuna.ical4j.model.Calendar ical = new net.fortuna.ical4j.model.Calendar();
|
||||
ical.getProperties().add(Version.VERSION_2_0);
|
||||
ical.getProperties().add(Constants.ICAL_PRODID);
|
||||
|
||||
// "master event" (without exceptions)
|
||||
ComponentList components = ical.getComponents();
|
||||
VEvent master = toVEvent(new Uid(uid));
|
||||
components.add(master);
|
||||
|
||||
// remember used time zones
|
||||
Set<net.fortuna.ical4j.model.TimeZone> usedTimeZones = new HashSet<>();
|
||||
if (dtStart != null && dtStart.getTimeZone() != null)
|
||||
usedTimeZones.add(dtStart.getTimeZone());
|
||||
if (dtEnd != null && dtEnd.getTimeZone() != null)
|
||||
usedTimeZones.add(dtEnd.getTimeZone());
|
||||
|
||||
// recurrence exceptions
|
||||
for (Event exception : exceptions) {
|
||||
// create VEVENT for exception
|
||||
VEvent vException = exception.toVEvent(master.getUid());
|
||||
|
||||
components.add(vException);
|
||||
|
||||
// remember used time zones
|
||||
if (exception.dtStart != null && exception.dtStart.getTimeZone() != null)
|
||||
usedTimeZones.add(exception.dtStart.getTimeZone());
|
||||
if (exception.dtEnd != null && exception.dtEnd.getTimeZone() != null)
|
||||
usedTimeZones.add(exception.dtEnd.getTimeZone());
|
||||
}
|
||||
|
||||
// add VTIMEZONE components
|
||||
for (net.fortuna.ical4j.model.TimeZone timeZone : usedTimeZones)
|
||||
ical.getComponents().add(timeZone.getVTimeZone());
|
||||
|
||||
CalendarOutputter output = new CalendarOutputter(false);
|
||||
ByteArrayOutputStream os = new ByteArrayOutputStream();
|
||||
try {
|
||||
output.output(ical, os);
|
||||
} catch (ValidationException e) {
|
||||
Log.e(TAG, "Generated invalid iCalendar");
|
||||
}
|
||||
return os;
|
||||
}
|
||||
|
||||
protected VEvent toVEvent(Uid uid) {
|
||||
VEvent event = new VEvent();
|
||||
PropertyList props = event.getProperties();
|
||||
|
||||
if (uid != null)
|
||||
props.add(uid);
|
||||
if (recurrenceId != null)
|
||||
props.add(recurrenceId);
|
||||
|
||||
props.add(dtStart);
|
||||
if (dtEnd != null)
|
||||
props.add(dtEnd);
|
||||
if (duration != null)
|
||||
props.add(duration);
|
||||
|
||||
if (rrule != null)
|
||||
props.add(rrule);
|
||||
for (RDate rdate : rdates)
|
||||
props.add(rdate);
|
||||
if (exrule != null)
|
||||
props.add(exrule);
|
||||
for (ExDate exdate : exdates)
|
||||
props.add(exdate);
|
||||
|
||||
if (summary != null && !summary.isEmpty())
|
||||
props.add(new Summary(summary));
|
||||
if (location != null && !location.isEmpty())
|
||||
props.add(new Location(location));
|
||||
if (description != null && !description.isEmpty())
|
||||
props.add(new Description(description));
|
||||
|
||||
if (status != null)
|
||||
props.add(status);
|
||||
if (!opaque)
|
||||
props.add(Transp.TRANSPARENT);
|
||||
|
||||
if (organizer != null)
|
||||
props.add(organizer);
|
||||
props.addAll(attendees);
|
||||
|
||||
if (forPublic != null)
|
||||
event.getProperties().add(forPublic ? Clazz.PUBLIC : Clazz.PRIVATE);
|
||||
|
||||
event.getAlarms().addAll(alarms);
|
||||
return event;
|
||||
}
|
||||
|
||||
|
||||
// time helpers
|
||||
|
||||
/**
|
||||
* Returns the time-zone ID for a given date-time, or TIMEZONE_UTC for dates (without time).
|
||||
* TIMEZONE_UTC is also returned for DATE-TIMEs in UTC representation.
|
||||
* @param date DateProperty (DATE or DATE-TIME) whose time-zone information is used
|
||||
*/
|
||||
protected static String getTzId(DateProperty date) {
|
||||
if (isDateTime(date) && !date.isUtc() && date.getTimeZone() != null)
|
||||
return date.getTimeZone().getID();
|
||||
else
|
||||
return TimeZones.UTC_ID;
|
||||
}
|
||||
|
||||
public boolean isAllDay() {
|
||||
return !isDateTime(dtStart);
|
||||
}
|
||||
|
||||
public long getDtStartInMillis() {
|
||||
return dtStart.getDate().getTime();
|
||||
}
|
||||
|
||||
public String getDtStartTzID() {
|
||||
return getTzId(dtStart);
|
||||
}
|
||||
|
||||
public void setDtStart(long tsStart, String tzID) {
|
||||
if (tzID == null) { // all-day
|
||||
dtStart = new DtStart(new Date(tsStart));
|
||||
} else {
|
||||
DateTime start = new DateTime(tsStart);
|
||||
start.setTimeZone(DateUtils.tzRegistry.getTimeZone(tzID));
|
||||
dtStart = new DtStart(start);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public long getDtEndInMillis() {
|
||||
return dtEnd.getDate().getTime();
|
||||
}
|
||||
|
||||
public String getDtEndTzID() {
|
||||
return getTzId(dtEnd);
|
||||
}
|
||||
|
||||
public void setDtEnd(long tsEnd, String tzID) {
|
||||
if (tzID == null) { // all-day
|
||||
dtEnd = new DtEnd(new Date(tsEnd));
|
||||
} else {
|
||||
DateTime end = new DateTime(tsEnd);
|
||||
end.setTimeZone(DateUtils.tzRegistry.getTimeZone(tzID));
|
||||
dtEnd = new DtEnd(end);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,786 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2013 – 2015 Ricki Hirner (bitfire web engineering).
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the GNU Public License v3.0
|
||||
* which accompanies this distribution, and is available at
|
||||
* http://www.gnu.org/licenses/gpl.html
|
||||
*/
|
||||
package at.bitfire.davdroid.resource;
|
||||
|
||||
import android.accounts.Account;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.ContentProviderClient;
|
||||
import android.content.ContentProviderOperation;
|
||||
import android.content.ContentProviderOperation.Builder;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.ContentUris;
|
||||
import android.content.ContentValues;
|
||||
import android.content.Entity;
|
||||
import android.content.EntityIterator;
|
||||
import android.database.Cursor;
|
||||
import android.database.DatabaseUtils;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.RemoteException;
|
||||
import android.provider.CalendarContract;
|
||||
import android.provider.CalendarContract.Attendees;
|
||||
import android.provider.CalendarContract.Calendars;
|
||||
import android.provider.CalendarContract.Events;
|
||||
import android.provider.CalendarContract.Reminders;
|
||||
import android.util.Log;
|
||||
|
||||
import net.fortuna.ical4j.model.Date;
|
||||
import net.fortuna.ical4j.model.DateTime;
|
||||
import net.fortuna.ical4j.model.Dur;
|
||||
import net.fortuna.ical4j.model.Parameter;
|
||||
import net.fortuna.ical4j.model.ParameterList;
|
||||
import net.fortuna.ical4j.model.PropertyList;
|
||||
import net.fortuna.ical4j.model.component.VAlarm;
|
||||
import net.fortuna.ical4j.model.parameter.Cn;
|
||||
import net.fortuna.ical4j.model.parameter.CuType;
|
||||
import net.fortuna.ical4j.model.parameter.PartStat;
|
||||
import net.fortuna.ical4j.model.parameter.Role;
|
||||
import net.fortuna.ical4j.model.parameter.Rsvp;
|
||||
import net.fortuna.ical4j.model.property.Action;
|
||||
import net.fortuna.ical4j.model.property.Attendee;
|
||||
import net.fortuna.ical4j.model.property.Description;
|
||||
import net.fortuna.ical4j.model.property.Duration;
|
||||
import net.fortuna.ical4j.model.property.ExDate;
|
||||
import net.fortuna.ical4j.model.property.ExRule;
|
||||
import net.fortuna.ical4j.model.property.Organizer;
|
||||
import net.fortuna.ical4j.model.property.RDate;
|
||||
import net.fortuna.ical4j.model.property.RRule;
|
||||
import net.fortuna.ical4j.model.property.RecurrenceId;
|
||||
import net.fortuna.ical4j.model.property.Status;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.text.DateFormat;
|
||||
import java.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
import at.bitfire.davdroid.DavUtils;
|
||||
import at.bitfire.davdroid.DateUtils;
|
||||
import at.bitfire.davdroid.webdav.WebDavResource;
|
||||
import lombok.Cleanup;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* Represents a locally stored calendar, containing Events.
|
||||
* Communicates with the Android Contacts Provider which uses an SQLite
|
||||
* database to store the contacts.
|
||||
*/
|
||||
public class LocalCalendar extends LocalCollection<Event> {
|
||||
private static final String TAG = "davdroid.LocalCalendar";
|
||||
|
||||
@Getter protected String url;
|
||||
@Getter protected long id;
|
||||
|
||||
protected static final String COLLECTION_COLUMN_CTAG = Calendars.CAL_SYNC1;
|
||||
|
||||
/* database fields */
|
||||
|
||||
@Override protected Uri entriesURI() { return syncAdapterURI(Events.CONTENT_URI); }
|
||||
@Override protected String entryColumnAccountType() { return Events.ACCOUNT_TYPE; }
|
||||
@Override protected String entryColumnAccountName() { return Events.ACCOUNT_NAME; }
|
||||
@Override protected String entryColumnParentID() { return Events.CALENDAR_ID; }
|
||||
@Override protected String entryColumnID() { return Events._ID; }
|
||||
@Override protected String entryColumnRemoteName() { return Events._SYNC_ID; }
|
||||
@Override protected String entryColumnETag() { return Events.SYNC_DATA1; }
|
||||
@Override protected String entryColumnDirty() { return Events.DIRTY; }
|
||||
@Override protected String entryColumnDeleted() { return Events.DELETED; }
|
||||
|
||||
@Override
|
||||
@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
|
||||
protected String entryColumnUID() {
|
||||
return (android.os.Build.VERSION.SDK_INT >= 17) ?
|
||||
Events.UID_2445 : Events.SYNC_DATA2;
|
||||
}
|
||||
|
||||
|
||||
/* class methods, constructor */
|
||||
|
||||
@SuppressLint("InlinedApi")
|
||||
public static Uri create(Account account, ContentResolver resolver, ServerInfo.ResourceInfo info) throws LocalStorageException {
|
||||
@Cleanup("release") final ContentProviderClient client = resolver.acquireContentProviderClient(CalendarContract.AUTHORITY);
|
||||
if (client == null)
|
||||
throw new LocalStorageException("No Calendar Provider found (Calendar app disabled?)");
|
||||
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(Calendars.ACCOUNT_NAME, account.name);
|
||||
values.put(Calendars.ACCOUNT_TYPE, account.type);
|
||||
values.put(Calendars.NAME, info.getURL());
|
||||
values.put(Calendars.CALENDAR_DISPLAY_NAME, info.getTitle());
|
||||
values.put(Calendars.CALENDAR_COLOR, info.getColor() != null ? info.getColor() : DavUtils.calendarGreen);
|
||||
values.put(Calendars.OWNER_ACCOUNT, account.name);
|
||||
values.put(Calendars.SYNC_EVENTS, 1);
|
||||
values.put(Calendars.VISIBLE, 1);
|
||||
values.put(Calendars.ALLOWED_REMINDERS, Reminders.METHOD_ALERT);
|
||||
|
||||
if (info.isReadOnly())
|
||||
values.put(Calendars.CALENDAR_ACCESS_LEVEL, Calendars.CAL_ACCESS_READ);
|
||||
else {
|
||||
values.put(Calendars.CALENDAR_ACCESS_LEVEL, Calendars.CAL_ACCESS_OWNER);
|
||||
values.put(Calendars.CAN_ORGANIZER_RESPOND, 1);
|
||||
values.put(Calendars.CAN_MODIFY_TIME_ZONE, 1);
|
||||
}
|
||||
|
||||
if (android.os.Build.VERSION.SDK_INT >= 15) {
|
||||
values.put(Calendars.ALLOWED_AVAILABILITY, Events.AVAILABILITY_BUSY + "," + Events.AVAILABILITY_FREE + "," + Events.AVAILABILITY_TENTATIVE);
|
||||
values.put(Calendars.ALLOWED_ATTENDEE_TYPES, Attendees.TYPE_NONE + "," + Attendees.TYPE_OPTIONAL + "," + Attendees.TYPE_REQUIRED + "," + Attendees.TYPE_RESOURCE);
|
||||
}
|
||||
|
||||
if (info.getTimezone() != null)
|
||||
values.put(Calendars.CALENDAR_TIME_ZONE, DateUtils.findAndroidTimezoneID(info.getTimezone()));
|
||||
|
||||
Log.i(TAG, "Inserting calendar: " + values.toString());
|
||||
try {
|
||||
return client.insert(calendarsURI(account), values);
|
||||
} catch (RemoteException e) {
|
||||
throw new LocalStorageException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static LocalCalendar[] findAll(Account account, ContentProviderClient providerClient) throws RemoteException {
|
||||
@Cleanup Cursor cursor = providerClient.query(calendarsURI(account),
|
||||
new String[] { Calendars._ID, Calendars.NAME },
|
||||
Calendars.DELETED + "=0 AND " + Calendars.SYNC_EVENTS + "=1", null, null);
|
||||
|
||||
LinkedList<LocalCalendar> calendars = new LinkedList<>();
|
||||
while (cursor != null && cursor.moveToNext())
|
||||
calendars.add(new LocalCalendar(account, providerClient, cursor.getInt(0), cursor.getString(1)));
|
||||
return calendars.toArray(new LocalCalendar[calendars.size()]);
|
||||
}
|
||||
|
||||
public LocalCalendar(Account account, ContentProviderClient providerClient, long id, String url) {
|
||||
super(account, providerClient);
|
||||
this.id = id;
|
||||
this.url = url;
|
||||
sqlFilter = "ORIGINAL_ID IS NULL";
|
||||
}
|
||||
|
||||
|
||||
/* collection operations */
|
||||
|
||||
@Override
|
||||
public String getCTag() throws LocalStorageException {
|
||||
try {
|
||||
@Cleanup Cursor c = providerClient.query(ContentUris.withAppendedId(calendarsURI(), id),
|
||||
new String[] { COLLECTION_COLUMN_CTAG }, null, null, null);
|
||||
if (c != null && c.moveToFirst())
|
||||
return c.getString(0);
|
||||
else
|
||||
throw new LocalStorageException("Couldn't query calendar CTag");
|
||||
} catch(RemoteException e) {
|
||||
throw new LocalStorageException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setCTag(String cTag) throws LocalStorageException {
|
||||
ContentValues values = new ContentValues(1);
|
||||
values.put(COLLECTION_COLUMN_CTAG, cTag);
|
||||
try {
|
||||
providerClient.update(ContentUris.withAppendedId(calendarsURI(), id), values, null, null);
|
||||
} catch(RemoteException e) {
|
||||
throw new LocalStorageException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateMetaData(WebDavResource.Properties properties) throws LocalStorageException {
|
||||
ContentValues values = new ContentValues();
|
||||
|
||||
final String displayName = properties.getDisplayName();
|
||||
if (displayName != null)
|
||||
values.put(Calendars.CALENDAR_DISPLAY_NAME, displayName);
|
||||
|
||||
final Integer color = properties.getColor();
|
||||
if (color != null)
|
||||
values.put(Calendars.CALENDAR_COLOR, color);
|
||||
|
||||
try {
|
||||
if (values.size() > 0)
|
||||
providerClient.update(ContentUris.withAppendedId(calendarsURI(), id), values, null, null);
|
||||
} catch(RemoteException e) {
|
||||
throw new LocalStorageException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public long[] findUpdated() throws LocalStorageException {
|
||||
// mark (recurring) events with changed/deleted exceptions as dirty
|
||||
String where = entryColumnID() + " IN (SELECT DISTINCT " + Events.ORIGINAL_ID + " FROM events WHERE " +
|
||||
Events.ORIGINAL_ID + " IS NOT NULL AND (" + Events.DIRTY + "=1 OR " + Events.DELETED + "=1))";
|
||||
ContentValues dirty = new ContentValues(1);
|
||||
dirty.put(CalendarContract.Events.DIRTY, 1);
|
||||
try {
|
||||
int rows = providerClient.update(entriesURI(), dirty, where, null);
|
||||
if (rows > 0)
|
||||
Log.d(TAG, rows + " event(s) marked as dirty because of dirty/deleted exceptions");
|
||||
} catch (RemoteException e) {
|
||||
Log.e(TAG, "Couldn't mark events with updated exceptions as dirty", e);
|
||||
}
|
||||
|
||||
// new find and return updated (master) events
|
||||
return super.findUpdated();
|
||||
}
|
||||
|
||||
|
||||
/* create/update/delete */
|
||||
|
||||
public Event newResource(long localID, String resourceName, String eTag) {
|
||||
return new Event(localID, resourceName, eTag);
|
||||
}
|
||||
|
||||
public int deleteAllExceptRemoteNames(Resource[] remoteResources) throws LocalStorageException {
|
||||
List<String> sqlFileNames = new LinkedList<>();
|
||||
for (Resource res : remoteResources)
|
||||
sqlFileNames.add(DatabaseUtils.sqlEscapeString(res.getName()));
|
||||
|
||||
// delete master events
|
||||
String where = entryColumnParentID() + "=?";
|
||||
where += sqlFileNames.isEmpty() ?
|
||||
" AND " + entryColumnRemoteName() + " IS NOT NULL" : // don't retain anything (delete all)
|
||||
" AND " + entryColumnRemoteName() + " NOT IN (" + StringUtils.join(sqlFileNames, ",") + ")"; // retain by remote file name
|
||||
if (sqlFilter != null)
|
||||
where += " AND (" + sqlFilter + ")";
|
||||
pendingOperations.add(ContentProviderOperation.newDelete(entriesURI())
|
||||
.withSelection(where, new String[] { String.valueOf(id) })
|
||||
.build());
|
||||
|
||||
// delete exceptions, too
|
||||
where = entryColumnParentID() + "=?";
|
||||
where += sqlFileNames.isEmpty() ?
|
||||
" AND " + Events.ORIGINAL_SYNC_ID + " IS NOT NULL" : // don't retain anything (delete all)
|
||||
" AND " + Events.ORIGINAL_SYNC_ID + " NOT IN (" + StringUtils.join(sqlFileNames, ",") + ")"; // retain by remote file name
|
||||
pendingOperations.add(ContentProviderOperation
|
||||
.newDelete(entriesURI())
|
||||
.withSelection(where, new String[]{String.valueOf(id)})
|
||||
.withYieldAllowed(true)
|
||||
.build()
|
||||
);
|
||||
return commit();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(Resource resource) {
|
||||
super.delete(resource);
|
||||
|
||||
// delete all exceptions of this event, too
|
||||
pendingOperations.add(ContentProviderOperation
|
||||
.newDelete(entriesURI())
|
||||
.withSelection(Events.ORIGINAL_ID + "=?", new String[] { String.valueOf(resource.getLocalID()) })
|
||||
.build()
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearDirty(Resource resource) {
|
||||
super.clearDirty(resource);
|
||||
|
||||
// clear dirty flag of all exceptions of this event
|
||||
pendingOperations.add(ContentProviderOperation
|
||||
.newUpdate(entriesURI())
|
||||
.withValue(Events.DIRTY, 0)
|
||||
.withSelection(Events.ORIGINAL_ID + "=?", new String[]{String.valueOf(resource.getLocalID())})
|
||||
.build()
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/* methods for populating the data object from the content provider */
|
||||
|
||||
@Override
|
||||
public void populate(Resource resource) throws LocalStorageException {
|
||||
Event event = (Event)resource;
|
||||
|
||||
try {
|
||||
@Cleanup EntityIterator iterEvents = CalendarContract.EventsEntity.newEntityIterator(
|
||||
providerClient.query(
|
||||
syncAdapterURI(CalendarContract.EventsEntity.CONTENT_URI),
|
||||
null, Events._ID + "=" + event.getLocalID(),
|
||||
null, null),
|
||||
providerClient
|
||||
);
|
||||
while (iterEvents.hasNext()) {
|
||||
Entity e = iterEvents.next();
|
||||
|
||||
ContentValues values = e.getEntityValues();
|
||||
populateEvent(event, values);
|
||||
|
||||
List<Entity.NamedContentValues> subValues = e.getSubValues();
|
||||
for (Entity.NamedContentValues subValue : subValues) {
|
||||
values = subValue.values;
|
||||
if (Attendees.CONTENT_URI.equals(subValue.uri))
|
||||
populateAttendee(event, values);
|
||||
if (Reminders.CONTENT_URI.equals(subValue.uri))
|
||||
populateReminder(event, values);
|
||||
}
|
||||
|
||||
populateExceptions(event);
|
||||
}
|
||||
} catch (RemoteException ex) {
|
||||
throw new LocalStorageException("Couldn't process locally stored event", ex);
|
||||
}
|
||||
}
|
||||
|
||||
protected void populateEvent(Event e, ContentValues values) {
|
||||
e.setUid(values.getAsString(entryColumnUID()));
|
||||
|
||||
e.summary = values.getAsString(Events.TITLE);
|
||||
e.location = values.getAsString(Events.EVENT_LOCATION);
|
||||
e.description = values.getAsString(Events.DESCRIPTION);
|
||||
|
||||
final boolean allDay = values.getAsInteger(Events.ALL_DAY) != 0;
|
||||
final long tsStart = values.getAsLong(Events.DTSTART);
|
||||
final String duration = values.getAsString(Events.DURATION);
|
||||
|
||||
String tzId;
|
||||
Long tsEnd = values.getAsLong(Events.DTEND);
|
||||
if (allDay) {
|
||||
e.setDtStart(tsStart, null);
|
||||
if (tsEnd == null) {
|
||||
Dur dur = new Dur(duration);
|
||||
java.util.Date dEnd = dur.getTime(new java.util.Date(tsStart));
|
||||
tsEnd = dEnd.getTime();
|
||||
}
|
||||
e.setDtEnd(tsEnd, null);
|
||||
|
||||
} else {
|
||||
// use the start time zone for the end time, too
|
||||
// because apps like Samsung Planner allow the user to change "the" time zone but change the start time zone only
|
||||
tzId = values.getAsString(Events.EVENT_TIMEZONE);
|
||||
e.setDtStart(tsStart, tzId);
|
||||
if (tsEnd != null)
|
||||
e.setDtEnd(tsEnd, tzId);
|
||||
else if (!StringUtils.isEmpty(duration))
|
||||
e.duration = new Duration(new Dur(duration));
|
||||
}
|
||||
|
||||
// recurrence
|
||||
try {
|
||||
String strRRule = values.getAsString(Events.RRULE);
|
||||
if (!StringUtils.isEmpty(strRRule))
|
||||
e.rrule = new RRule(strRRule);
|
||||
|
||||
String strRDate = values.getAsString(Events.RDATE);
|
||||
if (!StringUtils.isEmpty(strRDate)) {
|
||||
RDate rDate = (RDate)DateUtils.androidStringToRecurrenceSet(strRDate, RDate.class, allDay);
|
||||
e.getRdates().add(rDate);
|
||||
}
|
||||
|
||||
String strExRule = values.getAsString(Events.EXRULE);
|
||||
if (!StringUtils.isEmpty(strExRule)) {
|
||||
ExRule exRule = new ExRule();
|
||||
exRule.setValue(strExRule);
|
||||
e.exrule = exRule;
|
||||
}
|
||||
|
||||
String strExDate = values.getAsString(Events.EXDATE);
|
||||
if (!StringUtils.isEmpty(strExDate)) {
|
||||
ExDate exDate = (ExDate)DateUtils.androidStringToRecurrenceSet(strExDate, ExDate.class, allDay);
|
||||
e.getExdates().add(exDate);
|
||||
}
|
||||
} catch (ParseException ex) {
|
||||
Log.w(TAG, "Couldn't parse recurrence rules, ignoring", ex);
|
||||
} catch (IllegalArgumentException ex) {
|
||||
Log.w(TAG, "Invalid recurrence rules, ignoring", ex);
|
||||
}
|
||||
|
||||
if (values.containsKey(Events.ORIGINAL_INSTANCE_TIME)) {
|
||||
// this event is an exception of a recurring event
|
||||
long originalInstanceTime = values.getAsLong(Events.ORIGINAL_INSTANCE_TIME);
|
||||
|
||||
boolean originalAllDay = false;
|
||||
if (values.containsKey(Events.ORIGINAL_ALL_DAY))
|
||||
originalAllDay = values.getAsInteger(Events.ORIGINAL_ALL_DAY) != 0;
|
||||
|
||||
Date originalDate = originalAllDay ?
|
||||
new Date(originalInstanceTime) :
|
||||
new DateTime(originalInstanceTime);
|
||||
if (originalDate instanceof DateTime)
|
||||
((DateTime)originalDate).setUtc(true);
|
||||
e.recurrenceId = new RecurrenceId(originalDate);
|
||||
}
|
||||
|
||||
// status
|
||||
if (values.containsKey(Events.STATUS))
|
||||
switch (values.getAsInteger(Events.STATUS)) {
|
||||
case Events.STATUS_CONFIRMED:
|
||||
e.status = Status.VEVENT_CONFIRMED;
|
||||
break;
|
||||
case Events.STATUS_TENTATIVE:
|
||||
e.status = Status.VEVENT_TENTATIVE;
|
||||
break;
|
||||
case Events.STATUS_CANCELED:
|
||||
e.status = Status.VEVENT_CANCELLED;
|
||||
}
|
||||
|
||||
// availability
|
||||
e.opaque = values.getAsInteger(Events.AVAILABILITY) != Events.AVAILABILITY_FREE;
|
||||
|
||||
// set ORGANIZER if there's attendee data
|
||||
if (values.getAsInteger(Events.HAS_ATTENDEE_DATA) != 0)
|
||||
try {
|
||||
e.organizer = new Organizer(new URI("mailto", values.getAsString(Events.ORGANIZER), null));
|
||||
} catch (URISyntaxException ex) {
|
||||
Log.e(TAG, "Error when creating ORGANIZER mailto URI, ignoring", ex);
|
||||
}
|
||||
|
||||
// classification
|
||||
switch (values.getAsInteger(Events.ACCESS_LEVEL)) {
|
||||
case Events.ACCESS_CONFIDENTIAL:
|
||||
case Events.ACCESS_PRIVATE:
|
||||
e.forPublic = false;
|
||||
break;
|
||||
case Events.ACCESS_PUBLIC:
|
||||
e.forPublic = true;
|
||||
}
|
||||
}
|
||||
|
||||
void populateExceptions(Event e) throws RemoteException {
|
||||
@Cleanup Cursor c = providerClient.query(syncAdapterURI(Events.CONTENT_URI),
|
||||
new String[]{Events._ID, entryColumnRemoteName()},
|
||||
Events.ORIGINAL_ID + "=?", new String[]{ String.valueOf(e.getLocalID()) }, null);
|
||||
while (c != null && c.moveToNext()) {
|
||||
long exceptionId = c.getLong(0);
|
||||
String exceptionRemoteName = c.getString(1);
|
||||
try {
|
||||
Event exception = new Event(exceptionId, exceptionRemoteName, null);
|
||||
populate(exception);
|
||||
e.getExceptions().add(exception);
|
||||
} catch (LocalStorageException ex) {
|
||||
Log.e(TAG, "Couldn't find exception details, ignoring");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void populateAttendee(Event event, ContentValues values) {
|
||||
try {
|
||||
final Attendee attendee;
|
||||
final String
|
||||
email = values.getAsString(Attendees.ATTENDEE_EMAIL),
|
||||
idNS = values.getAsString(Attendees.ATTENDEE_ID_NAMESPACE),
|
||||
id = values.getAsString(Attendees.ATTENDEE_IDENTITY);
|
||||
if (idNS != null || id != null) {
|
||||
// attendee identified by namespace and ID
|
||||
attendee = new Attendee(new URI(idNS, id, null));
|
||||
if (email != null)
|
||||
attendee.getParameters().add(new iCalendar.Email(email));
|
||||
} else
|
||||
// attendee identified by email address
|
||||
attendee = new Attendee(new URI("mailto", email, null));
|
||||
final ParameterList params = attendee.getParameters();
|
||||
|
||||
String cn = values.getAsString(Attendees.ATTENDEE_NAME);
|
||||
if (cn != null)
|
||||
params.add(new Cn(cn));
|
||||
|
||||
// type
|
||||
int type = values.getAsInteger(Attendees.ATTENDEE_TYPE);
|
||||
params.add((type == Attendees.TYPE_RESOURCE) ? CuType.RESOURCE : CuType.INDIVIDUAL);
|
||||
|
||||
// role
|
||||
int relationship = values.getAsInteger(Attendees.ATTENDEE_RELATIONSHIP);
|
||||
switch (relationship) {
|
||||
case Attendees.RELATIONSHIP_ORGANIZER:
|
||||
case Attendees.RELATIONSHIP_ATTENDEE:
|
||||
case Attendees.RELATIONSHIP_PERFORMER:
|
||||
case Attendees.RELATIONSHIP_SPEAKER:
|
||||
params.add((type == Attendees.TYPE_REQUIRED) ? Role.REQ_PARTICIPANT : Role.OPT_PARTICIPANT);
|
||||
params.add(new Rsvp(true));
|
||||
break;
|
||||
case Attendees.RELATIONSHIP_NONE:
|
||||
params.add(Role.NON_PARTICIPANT);
|
||||
}
|
||||
|
||||
// status
|
||||
switch (values.getAsInteger(Attendees.ATTENDEE_STATUS)) {
|
||||
case Attendees.ATTENDEE_STATUS_INVITED:
|
||||
params.add(PartStat.NEEDS_ACTION);
|
||||
break;
|
||||
case Attendees.ATTENDEE_STATUS_ACCEPTED:
|
||||
params.add(PartStat.ACCEPTED);
|
||||
break;
|
||||
case Attendees.ATTENDEE_STATUS_DECLINED:
|
||||
params.add(PartStat.DECLINED);
|
||||
break;
|
||||
case Attendees.ATTENDEE_STATUS_TENTATIVE:
|
||||
params.add(PartStat.TENTATIVE);
|
||||
break;
|
||||
}
|
||||
|
||||
event.getAttendees().add(attendee);
|
||||
} catch (URISyntaxException ex) {
|
||||
Log.e(TAG, "Couldn't parse attendee information, ignoring", ex);
|
||||
}
|
||||
}
|
||||
|
||||
void populateReminder(Event event, ContentValues row) {
|
||||
VAlarm alarm = new VAlarm(new Dur(0, 0, -row.getAsInteger(Reminders.MINUTES), 0));
|
||||
|
||||
PropertyList props = alarm.getProperties();
|
||||
props.add(Action.DISPLAY);
|
||||
props.add(new Description(event.summary));
|
||||
event.getAlarms().add(alarm);
|
||||
}
|
||||
|
||||
|
||||
/* content builder methods */
|
||||
|
||||
@Override
|
||||
protected Builder buildEntry(Builder builder, Resource resource, boolean update) {
|
||||
final Event event = (Event)resource;
|
||||
|
||||
builder .withValue(Events.CALENDAR_ID, id)
|
||||
.withValue(Events.ALL_DAY, event.isAllDay() ? 1 : 0)
|
||||
.withValue(Events.DTSTART, event.getDtStartInMillis())
|
||||
.withValue(Events.EVENT_TIMEZONE, event.getDtStartTzID())
|
||||
.withValue(Events.HAS_ALARM, event.getAlarms().isEmpty() ? 0 : 1)
|
||||
.withValue(Events.HAS_ATTENDEE_DATA, event.getAttendees().isEmpty() ? 0 : 1)
|
||||
.withValue(Events.GUESTS_CAN_INVITE_OTHERS, 1)
|
||||
.withValue(Events.GUESTS_CAN_MODIFY, 1)
|
||||
.withValue(Events.GUESTS_CAN_SEE_GUESTS, 1);
|
||||
|
||||
if (event.recurrenceId == null) {
|
||||
// this event is a "master event" (not an exception)
|
||||
builder .withValue(entryColumnRemoteName(), event.getName())
|
||||
.withValue(entryColumnETag(), event.getETag())
|
||||
.withValue(entryColumnUID(), event.getUid());
|
||||
} else {
|
||||
// event is an exception
|
||||
builder.withValue(Events.ORIGINAL_SYNC_ID, event.getName());
|
||||
// ORIGINAL_INSTANCE_TIME and ORIGINAL_ALL_DAY is set in buildExceptions.
|
||||
// It's not possible to use only the RECURRENCE-ID to calculate
|
||||
// ORIGINAL_INSTANCE_TIME and ORIGINAL_ALL_DAY because iCloud sends DATE-TIME
|
||||
// RECURRENCE-IDs even if the original event is an all-day event.
|
||||
}
|
||||
|
||||
boolean recurring = false;
|
||||
if (event.rrule != null) {
|
||||
recurring = true;
|
||||
builder.withValue(Events.RRULE, event.rrule.getValue());
|
||||
}
|
||||
if (!event.getRdates().isEmpty()) {
|
||||
recurring = true;
|
||||
try {
|
||||
builder.withValue(Events.RDATE, DateUtils.recurrenceSetsToAndroidString(event.getRdates(), event.isAllDay()));
|
||||
} catch (ParseException e) {
|
||||
Log.e(TAG, "Couldn't parse RDate(s)", e);
|
||||
}
|
||||
}
|
||||
if (event.exrule != null)
|
||||
builder.withValue(Events.EXRULE, event.exrule.getValue());
|
||||
if (!event.getExceptions().isEmpty())
|
||||
try {
|
||||
builder.withValue(Events.EXDATE, DateUtils.recurrenceSetsToAndroidString(event.getExdates(), event.isAllDay()));
|
||||
} catch (ParseException e) {
|
||||
Log.e(TAG, "Couldn't parse ExDate(s)", e);
|
||||
}
|
||||
|
||||
// set either DTEND for single-time events or DURATION for recurring events
|
||||
// because that's the way Android likes it (see docs)
|
||||
if (recurring) {
|
||||
// calculate DURATION from start and end date
|
||||
Duration duration = new Duration(event.dtStart.getDate(), event.dtEnd.getDate());
|
||||
builder.withValue(Events.DURATION, duration.getValue());
|
||||
} else
|
||||
builder .withValue(Events.DTEND, event.getDtEndInMillis())
|
||||
.withValue(Events.EVENT_END_TIMEZONE, event.getDtEndTzID());
|
||||
|
||||
if (event.summary != null)
|
||||
builder.withValue(Events.TITLE, event.summary);
|
||||
if (event.location != null)
|
||||
builder.withValue(Events.EVENT_LOCATION, event.location);
|
||||
if (event.description != null)
|
||||
builder.withValue(Events.DESCRIPTION, event.description);
|
||||
|
||||
if (event.organizer != null) {
|
||||
final URI uri = event.organizer.getCalAddress();
|
||||
String email = null;
|
||||
if (uri != null && "mailto".equalsIgnoreCase(uri.getScheme()))
|
||||
email = uri.getSchemeSpecificPart();
|
||||
else {
|
||||
iCalendar.Email emailParam = (iCalendar.Email)event.organizer.getParameter(iCalendar.Email.PARAMETER_NAME);
|
||||
if (emailParam != null)
|
||||
email = emailParam.getValue();
|
||||
}
|
||||
if (email != null)
|
||||
builder.withValue(Events.ORGANIZER, email);
|
||||
else
|
||||
Log.w(TAG, "Got ORGANIZER without email address which is not supported by Android, ignoring");
|
||||
}
|
||||
|
||||
if (event.status!= null) {
|
||||
int statusCode = Events.STATUS_TENTATIVE;
|
||||
if (event.status == Status.VEVENT_CONFIRMED)
|
||||
statusCode = Events.STATUS_CONFIRMED;
|
||||
else if (event.status == Status.VEVENT_CANCELLED)
|
||||
statusCode = Events.STATUS_CANCELED;
|
||||
builder.withValue(Events.STATUS, statusCode);
|
||||
}
|
||||
|
||||
builder.withValue(Events.AVAILABILITY, event.opaque ? Events.AVAILABILITY_BUSY : Events.AVAILABILITY_FREE);
|
||||
|
||||
if (event.forPublic != null)
|
||||
builder.withValue(Events.ACCESS_LEVEL, event.forPublic ? Events.ACCESS_PUBLIC : Events.ACCESS_PRIVATE);
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected void addDataRows(Resource resource, long localID, int backrefIdx) {
|
||||
final Event event = (Event)resource;
|
||||
|
||||
// add attendees
|
||||
for (Attendee attendee : event.getAttendees())
|
||||
pendingOperations.add(buildAttendee(newDataInsertBuilder(Attendees.CONTENT_URI, Attendees.EVENT_ID, localID, backrefIdx), attendee).build());
|
||||
// add reminders
|
||||
for (VAlarm alarm : event.getAlarms())
|
||||
pendingOperations.add(buildReminder(newDataInsertBuilder(Reminders.CONTENT_URI, Reminders.EVENT_ID, localID, backrefIdx), alarm).build());
|
||||
// add exceptions
|
||||
for (Event exception : event.getExceptions()) {
|
||||
final int backrefIdxEx = pendingOperations.size(); // save exception ID as backref value
|
||||
pendingOperations.add(buildException(newDataInsertBuilder(Events.CONTENT_URI, Events.ORIGINAL_ID, localID, backrefIdx), event, exception).build());
|
||||
addDataRows(exception, -1, backrefIdxEx); // build attendees and reminders for exception
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void removeDataRows(Resource resource) {
|
||||
final Event event = (Event)resource;
|
||||
|
||||
// delete exceptions
|
||||
pendingOperations.add(ContentProviderOperation.newDelete(syncAdapterURI(Events.CONTENT_URI))
|
||||
.withSelection(Events.ORIGINAL_ID + "=?", new String[] { String.valueOf(event.getLocalID())}).build());
|
||||
// delete attendees
|
||||
pendingOperations.add(ContentProviderOperation.newDelete(syncAdapterURI(Attendees.CONTENT_URI))
|
||||
.withSelection(Attendees.EVENT_ID + "=?", new String[]{String.valueOf(event.getLocalID())}).build());
|
||||
// delete reminders
|
||||
pendingOperations.add(ContentProviderOperation.newDelete(syncAdapterURI(Reminders.CONTENT_URI))
|
||||
.withSelection(Reminders.EVENT_ID + "=?", new String[]{String.valueOf(event.getLocalID())}).build());
|
||||
}
|
||||
|
||||
|
||||
protected Builder buildException(Builder builder, Event master, Event exception) {
|
||||
buildEntry(builder, exception, false);
|
||||
|
||||
final boolean originalAllDay = master.isAllDay();
|
||||
|
||||
Date date = exception.recurrenceId.getDate();
|
||||
if (originalAllDay && date instanceof DateTime) { // correct VALUE=DATE-TIME RECURRENCE-IDs to VALUE=DATE
|
||||
final DateFormat dateFormatDate = new SimpleDateFormat("yyyyMMdd");
|
||||
final String dateString = dateFormatDate.format(exception.recurrenceId.getDate());
|
||||
try {
|
||||
date = new Date(dateString);
|
||||
} catch (ParseException e) {
|
||||
Log.e(TAG, "Couldn't parse DATE part of DATE-TIME RECURRENCE-ID", e);
|
||||
}
|
||||
}
|
||||
|
||||
builder.withValue(Events.ORIGINAL_INSTANCE_TIME, date.getTime());
|
||||
builder.withValue(Events.ORIGINAL_ALL_DAY, originalAllDay ? 1 : 0);
|
||||
return builder;
|
||||
}
|
||||
|
||||
@SuppressLint("InlinedApi")
|
||||
protected Builder buildAttendee(Builder builder, Attendee attendee) {
|
||||
final URI member = attendee.getCalAddress();
|
||||
if ("mailto".equalsIgnoreCase(member.getScheme()))
|
||||
// attendee identified by email
|
||||
builder = builder.withValue(Attendees.ATTENDEE_EMAIL, member.getSchemeSpecificPart());
|
||||
else {
|
||||
// attendee identified by other URI
|
||||
builder = builder
|
||||
.withValue(Attendees.ATTENDEE_ID_NAMESPACE, member.getScheme())
|
||||
.withValue(Attendees.ATTENDEE_IDENTITY, member.getSchemeSpecificPart());
|
||||
iCalendar.Email email = (iCalendar.Email)attendee.getParameter(iCalendar.Email.PARAMETER_NAME);
|
||||
if (email != null)
|
||||
builder = builder.withValue(Attendees.ATTENDEE_EMAIL, email.getValue());
|
||||
}
|
||||
|
||||
final Cn cn = (Cn)attendee.getParameter(Parameter.CN);
|
||||
if (cn != null)
|
||||
builder.withValue(Attendees.ATTENDEE_NAME, cn.getValue());
|
||||
|
||||
int type = Attendees.TYPE_NONE;
|
||||
|
||||
CuType cutype = (CuType)attendee.getParameter(Parameter.CUTYPE);
|
||||
if (cutype == CuType.RESOURCE || cutype == CuType.ROOM)
|
||||
// "attendee" is a (physical) resource
|
||||
type = Attendees.TYPE_RESOURCE;
|
||||
else {
|
||||
// attendee is not a (physical) resource
|
||||
Role role = (Role)attendee.getParameter(Parameter.ROLE);
|
||||
int relationship;
|
||||
if (role == Role.CHAIR)
|
||||
relationship = Attendees.RELATIONSHIP_ORGANIZER;
|
||||
else {
|
||||
relationship = Attendees.RELATIONSHIP_ATTENDEE;
|
||||
if (role == Role.OPT_PARTICIPANT)
|
||||
type = Attendees.TYPE_OPTIONAL;
|
||||
else if (role == Role.REQ_PARTICIPANT)
|
||||
type = Attendees.TYPE_REQUIRED;
|
||||
}
|
||||
builder.withValue(Attendees.ATTENDEE_RELATIONSHIP, relationship);
|
||||
}
|
||||
|
||||
int status = Attendees.ATTENDEE_STATUS_NONE;
|
||||
PartStat partStat = (PartStat)attendee.getParameter(Parameter.PARTSTAT);
|
||||
if (partStat == null || partStat == PartStat.NEEDS_ACTION)
|
||||
status = Attendees.ATTENDEE_STATUS_INVITED;
|
||||
else if (partStat == PartStat.ACCEPTED)
|
||||
status = Attendees.ATTENDEE_STATUS_ACCEPTED;
|
||||
else if (partStat == PartStat.DECLINED)
|
||||
status = Attendees.ATTENDEE_STATUS_DECLINED;
|
||||
else if (partStat == PartStat.TENTATIVE)
|
||||
status = Attendees.ATTENDEE_STATUS_TENTATIVE;
|
||||
|
||||
return builder
|
||||
.withValue(Attendees.ATTENDEE_TYPE, type)
|
||||
.withValue(Attendees.ATTENDEE_STATUS, status);
|
||||
}
|
||||
|
||||
protected Builder buildReminder(Builder builder, VAlarm alarm) {
|
||||
int minutes = 0;
|
||||
|
||||
if (alarm.getTrigger() != null) {
|
||||
Dur duration = alarm.getTrigger().getDuration();
|
||||
if (duration != null) {
|
||||
// negative value in TRIGGER means positive value in Reminders.MINUTES and vice versa
|
||||
minutes = -(((duration.getWeeks() * 7 + duration.getDays()) * 24 + duration.getHours()) * 60 + duration.getMinutes());
|
||||
if (duration.isNegative())
|
||||
minutes *= -1;
|
||||
}
|
||||
}
|
||||
|
||||
Log.d(TAG, "Adding alarm " + minutes + " minutes before");
|
||||
return builder
|
||||
.withValue(Reminders.METHOD, Reminders.METHOD_ALERT)
|
||||
.withValue(Reminders.MINUTES, minutes);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* private helper methods */
|
||||
|
||||
protected static Uri calendarsURI(Account account) {
|
||||
return Calendars.CONTENT_URI.buildUpon()
|
||||
.appendQueryParameter(Calendars.ACCOUNT_NAME, account.name)
|
||||
.appendQueryParameter(Calendars.ACCOUNT_TYPE, account.type)
|
||||
.appendQueryParameter(CalendarContract.CALLER_IS_SYNCADAPTER, "true")
|
||||
.build();
|
||||
}
|
||||
|
||||
protected Uri calendarsURI() {
|
||||
return calendarsURI(account);
|
||||
}
|
||||
|
||||
}
|
@ -1,430 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2013 – 2015 Ricki Hirner (bitfire web engineering).
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the GNU Public License v3.0
|
||||
* which accompanies this distribution, and is available at
|
||||
* http://www.gnu.org/licenses/gpl.html
|
||||
*/
|
||||
package at.bitfire.davdroid.resource;
|
||||
|
||||
import android.accounts.Account;
|
||||
import android.content.ContentProviderClient;
|
||||
import android.content.ContentProviderOperation;
|
||||
import android.content.ContentProviderOperation.Builder;
|
||||
import android.content.ContentProviderResult;
|
||||
import android.content.ContentUris;
|
||||
import android.content.ContentValues;
|
||||
import android.content.OperationApplicationException;
|
||||
import android.database.Cursor;
|
||||
import android.database.DatabaseUtils;
|
||||
import android.net.Uri;
|
||||
import android.os.RemoteException;
|
||||
import android.provider.CalendarContract;
|
||||
import android.util.Log;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
import at.bitfire.davdroid.webdav.WebDavResource;
|
||||
import lombok.Cleanup;
|
||||
|
||||
/**
|
||||
* Represents a locally-stored synchronizable collection (for instance, the
|
||||
* address book or a calendar). Manages a CTag that stores the last known
|
||||
* remote CTag (the remote CTag changes whenever something in the remote collection changes).
|
||||
*
|
||||
* @param <T> Subtype of Resource that can be stored in the collection
|
||||
*/
|
||||
public abstract class LocalCollection<T extends Resource> {
|
||||
private static final String TAG = "davdroid.Collection";
|
||||
|
||||
final protected Account account;
|
||||
final protected ContentProviderClient providerClient;
|
||||
final protected ArrayList<ContentProviderOperation> pendingOperations = new ArrayList<>();
|
||||
|
||||
|
||||
// database fields
|
||||
|
||||
/** base Uri of the collection's entries (for instance, Events.CONTENT_URI);
|
||||
* apply syncAdapterURI() before returning a value */
|
||||
abstract protected Uri entriesURI();
|
||||
|
||||
/** column name of the type of the account the entry belongs to */
|
||||
abstract protected String entryColumnAccountType();
|
||||
/** column name of the name of the account the entry belongs to */
|
||||
abstract protected String entryColumnAccountName();
|
||||
|
||||
/** column name of the collection ID the entry belongs to */
|
||||
abstract protected String entryColumnParentID();
|
||||
/** column name of an entry's ID */
|
||||
abstract protected String entryColumnID();
|
||||
/** column name of an entry's file name on the WebDAV server */
|
||||
abstract protected String entryColumnRemoteName();
|
||||
/** column name of an entry's last ETag on the WebDAV server; null if entry hasn't been uploaded yet */
|
||||
abstract protected String entryColumnETag();
|
||||
|
||||
/** column name of an entry's "dirty" flag (managed by content provider) */
|
||||
abstract protected String entryColumnDirty();
|
||||
/** column name of an entry's "deleted" flag (managed by content provider) */
|
||||
abstract protected String entryColumnDeleted();
|
||||
|
||||
/** column name of an entry's UID */
|
||||
abstract protected String entryColumnUID();
|
||||
|
||||
|
||||
/** SQL filter expression */
|
||||
String sqlFilter;
|
||||
|
||||
|
||||
LocalCollection(Account account, ContentProviderClient providerClient) {
|
||||
this.account = account;
|
||||
this.providerClient = providerClient;
|
||||
}
|
||||
|
||||
|
||||
// collection operations
|
||||
|
||||
/** gets the ID if the collection (for instance, ID of the Android calendar) */
|
||||
abstract public long getId();
|
||||
/** sets local stored CTag */
|
||||
abstract public void setCTag(String cTag) throws LocalStorageException;
|
||||
/** gets the CTag of the collection */
|
||||
abstract public String getCTag() throws LocalStorageException;
|
||||
/** update locally stored collection properties (e.g. display name and color) from a WebDavResource */
|
||||
abstract public void updateMetaData(WebDavResource.Properties properties) throws LocalStorageException;
|
||||
|
||||
|
||||
// content provider (= database) querying
|
||||
|
||||
/**
|
||||
* Finds new resources (resources which haven't been uploaded yet).
|
||||
* New resources are 1) dirty, and 2) don't have an ETag yet.
|
||||
* Only records matching sqlFilter will be returned.
|
||||
*
|
||||
* @return IDs of new resources
|
||||
* @throws LocalStorageException when the content provider couldn't be queried
|
||||
*/
|
||||
public long[] findNew() throws LocalStorageException {
|
||||
String where = entryColumnDirty() + "=1 AND " + entryColumnETag() + " IS NULL";
|
||||
if (entryColumnParentID() != null)
|
||||
where += " AND " + entryColumnParentID() + "=" + String.valueOf(getId());
|
||||
if (sqlFilter != null)
|
||||
where += " AND (" + sqlFilter + ")";
|
||||
try {
|
||||
@Cleanup Cursor cursor = providerClient.query(entriesURI(),
|
||||
new String[] { entryColumnID() },
|
||||
where, null, null);
|
||||
if (cursor == null)
|
||||
throw new LocalStorageException("Couldn't query new records");
|
||||
|
||||
long[] fresh = new long[cursor.getCount()];
|
||||
for (int idx = 0; cursor.moveToNext(); idx++) {
|
||||
long id = cursor.getLong(0);
|
||||
|
||||
// new record: we have to generate UID + remote file name for uploading
|
||||
T resource = findById(id, false);
|
||||
resource.initialize();
|
||||
// write generated UID + remote file name into database
|
||||
ContentValues values = new ContentValues(2);
|
||||
values.put(entryColumnUID(), resource.getUid());
|
||||
values.put(entryColumnRemoteName(), resource.getName());
|
||||
providerClient.update(ContentUris.withAppendedId(entriesURI(), id), values, null, null);
|
||||
|
||||
fresh[idx] = id;
|
||||
}
|
||||
return fresh;
|
||||
} catch(RemoteException ex) {
|
||||
throw new LocalStorageException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds updated resources (resources which have already been uploaded, but have changed locally).
|
||||
* Updated resources are 1) dirty, and 2) already have an ETag. Only records matching sqlFilter
|
||||
* will be returned.
|
||||
*
|
||||
* @return IDs of updated resources
|
||||
* @throws LocalStorageException when the content provider couldn't be queried
|
||||
*/
|
||||
public long[] findUpdated() throws LocalStorageException {
|
||||
String where = entryColumnDirty() + "=1 AND " + entryColumnETag() + " IS NOT NULL";
|
||||
if (entryColumnParentID() != null)
|
||||
where += " AND " + entryColumnParentID() + "=" + String.valueOf(getId());
|
||||
if (sqlFilter != null)
|
||||
where += " AND (" + sqlFilter + ")";
|
||||
try {
|
||||
@Cleanup Cursor cursor = providerClient.query(entriesURI(),
|
||||
new String[] { entryColumnID(), entryColumnRemoteName(), entryColumnETag() },
|
||||
where, null, null);
|
||||
if (cursor == null)
|
||||
throw new LocalStorageException("Couldn't query updated records");
|
||||
|
||||
long[] updated = new long[cursor.getCount()];
|
||||
for (int idx = 0; cursor.moveToNext(); idx++)
|
||||
updated[idx] = cursor.getLong(0);
|
||||
return updated;
|
||||
} catch(RemoteException ex) {
|
||||
throw new LocalStorageException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds deleted resources (resources which have been marked for deletion).
|
||||
* Deleted resources have the "deleted" flag set.
|
||||
* Only records matching sqlFilter will be returned.
|
||||
*
|
||||
* @return IDs of deleted resources
|
||||
* @throws LocalStorageException when the content provider couldn't be queried
|
||||
*/
|
||||
public long[] findDeleted() throws LocalStorageException {
|
||||
String where = entryColumnDeleted() + "=1";
|
||||
if (entryColumnParentID() != null)
|
||||
where += " AND " + entryColumnParentID() + "=" + String.valueOf(getId());
|
||||
if (sqlFilter != null)
|
||||
where += " AND (" + sqlFilter + ")";
|
||||
try {
|
||||
@Cleanup Cursor cursor = providerClient.query(entriesURI(),
|
||||
new String[] { entryColumnID(), entryColumnRemoteName(), entryColumnETag() },
|
||||
where, null, null);
|
||||
if (cursor == null)
|
||||
throw new LocalStorageException("Couldn't query dirty records");
|
||||
|
||||
long deleted[] = new long[cursor.getCount()];
|
||||
for (int idx = 0; cursor.moveToNext(); idx++)
|
||||
deleted[idx] = cursor.getLong(0);
|
||||
return deleted;
|
||||
} catch(RemoteException ex) {
|
||||
throw new LocalStorageException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a specific resource by ID. Only records matching sqlFilter are taken into account.
|
||||
* @param localID ID of the resource
|
||||
* @param populate true: populates all data fields (for instance, contact or event details);
|
||||
* false: only remote file name and ETag are populated
|
||||
* @return resource with either ID/remote file/name/ETag or all fields populated
|
||||
* @throws RecordNotFoundException when the resource couldn't be found
|
||||
* @throws LocalStorageException when the content provider couldn't be queried
|
||||
*/
|
||||
public T findById(long localID, boolean populate) throws LocalStorageException {
|
||||
try {
|
||||
@Cleanup Cursor cursor = providerClient.query(ContentUris.withAppendedId(entriesURI(), localID),
|
||||
new String[] { entryColumnRemoteName(), entryColumnETag() }, sqlFilter, null, null);
|
||||
if (cursor != null && cursor.moveToNext()) {
|
||||
T resource = newResource(localID, cursor.getString(0), cursor.getString(1));
|
||||
if (populate)
|
||||
populate(resource);
|
||||
return resource;
|
||||
} else
|
||||
throw new RecordNotFoundException();
|
||||
} catch(RemoteException ex) {
|
||||
throw new LocalStorageException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a specific resource by remote file name. Only records matching sqlFilter are taken into account.
|
||||
* @param remoteName remote file name of the resource
|
||||
* @param populate true: populates all data fields (for instance, contact or event details);
|
||||
* false: only remote file name and ETag are populated
|
||||
* @return resource with either ID/remote file/name/ETag or all fields populated
|
||||
* @throws RecordNotFoundException when the resource couldn't be found
|
||||
* @throws LocalStorageException when the content provider couldn't be queried
|
||||
*/
|
||||
public T findByRemoteName(String remoteName, boolean populate) throws LocalStorageException {
|
||||
String where = entryColumnRemoteName() + "=?";
|
||||
if (sqlFilter != null)
|
||||
where += " AND (" + sqlFilter + ")";
|
||||
try {
|
||||
@Cleanup Cursor cursor = providerClient.query(entriesURI(),
|
||||
new String[] { entryColumnID(), entryColumnRemoteName(), entryColumnETag() },
|
||||
where, new String[] { remoteName }, null);
|
||||
if (cursor != null && cursor.moveToNext()) {
|
||||
T resource = newResource(cursor.getLong(0), cursor.getString(1), cursor.getString(2));
|
||||
if (populate)
|
||||
populate(resource);
|
||||
return resource;
|
||||
} else
|
||||
throw new RecordNotFoundException();
|
||||
} catch(RemoteException ex) {
|
||||
throw new LocalStorageException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
/** populates all data fields from the content provider */
|
||||
public abstract void populate(Resource record) throws LocalStorageException;
|
||||
|
||||
|
||||
// create/update/delete
|
||||
|
||||
/**
|
||||
* Creates a new resource object in memory. No content provider operations involved.
|
||||
* @param localID the ID of the resource
|
||||
* @param resourceName the (remote) file name of the resource
|
||||
* @param eTag ETag of the resource
|
||||
* @return the new resource object */
|
||||
abstract public T newResource(long localID, String resourceName, String eTag);
|
||||
|
||||
/** Adds the resource (including all data) to the local collection.
|
||||
* @param resource Resource to be added
|
||||
*/
|
||||
public void add(Resource resource) throws LocalStorageException {
|
||||
int idx = pendingOperations.size();
|
||||
pendingOperations.add(
|
||||
buildEntry(ContentProviderOperation.newInsert(entriesURI()), resource, false)
|
||||
.withYieldAllowed(true)
|
||||
.build());
|
||||
|
||||
addDataRows(resource, -1, idx);
|
||||
commit();
|
||||
}
|
||||
|
||||
/** Updates an existing resource in the local collection. The resource will be found by
|
||||
* the remote file name and all data will be updated. */
|
||||
public void updateByRemoteName(Resource remoteResource) throws LocalStorageException {
|
||||
T localResource = findByRemoteName(remoteResource.getName(), false);
|
||||
pendingOperations.add(
|
||||
buildEntry(ContentProviderOperation.newUpdate(ContentUris.withAppendedId(entriesURI(), localResource.getLocalID())), remoteResource, true)
|
||||
.withValue(entryColumnETag(), remoteResource.getETag())
|
||||
.withYieldAllowed(true)
|
||||
.build());
|
||||
|
||||
removeDataRows(localResource);
|
||||
addDataRows(remoteResource, localResource.getLocalID(), -1);
|
||||
commit();
|
||||
}
|
||||
|
||||
/** Enqueues deleting a resource from the local collection. Requires commit() to be effective! */
|
||||
public void delete(Resource resource) {
|
||||
pendingOperations.add(ContentProviderOperation
|
||||
.newDelete(ContentUris.withAppendedId(entriesURI(), resource.getLocalID()))
|
||||
.withYieldAllowed(true)
|
||||
.build());
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes all resources except the give ones from the local collection.
|
||||
* @param remoteResources resources with these remote file names will be kept
|
||||
* @return number of deleted resources
|
||||
*/
|
||||
public int deleteAllExceptRemoteNames(Resource[] remoteResources) throws LocalStorageException
|
||||
{
|
||||
final String where;
|
||||
|
||||
if (remoteResources.length != 0) {
|
||||
// delete all except certain entries
|
||||
final List<String> sqlFileNames = new LinkedList<>();
|
||||
for (final Resource res : remoteResources)
|
||||
sqlFileNames.add(DatabaseUtils.sqlEscapeString(res.getName()));
|
||||
where = entryColumnRemoteName() + " NOT IN (" + StringUtils.join(sqlFileNames, ",") + ')';
|
||||
} else
|
||||
// delete all entries
|
||||
where = entryColumnRemoteName() + " IS NOT NULL";
|
||||
|
||||
try {
|
||||
if (entryColumnParentID() != null)
|
||||
// entries have a parent collection (for instance, events which have a calendar)
|
||||
return providerClient.delete(
|
||||
entriesURI(),
|
||||
entryColumnParentID() + "=? AND (" + where + ')', // restrict deletion to parent collection
|
||||
new String[] { String.valueOf(getId()) }
|
||||
);
|
||||
else
|
||||
// entries don't have a parent collection (contacts are stored directly and not within an address book)
|
||||
return providerClient.delete(entriesURI(), where, null);
|
||||
} catch (RemoteException e) {
|
||||
throw new LocalStorageException("Couldn't delete local resources", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/** Updates the locally-known ETag of a resource. */
|
||||
public void updateETag(Resource res, String eTag) throws LocalStorageException {
|
||||
Log.d(TAG, "Setting ETag of local resource " + res.getName() + " to " + eTag);
|
||||
|
||||
ContentValues values = new ContentValues(1);
|
||||
values.put(entryColumnETag(), eTag);
|
||||
try {
|
||||
providerClient.update(ContentUris.withAppendedId(entriesURI(), res.getLocalID()), values, null, new String[] {});
|
||||
} catch (RemoteException e) {
|
||||
throw new LocalStorageException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/** Enqueues removing the dirty flag from a locally-stored resource. Requires commit() to be effective! */
|
||||
public void clearDirty(Resource resource) {
|
||||
pendingOperations.add(ContentProviderOperation
|
||||
.newUpdate(ContentUris.withAppendedId(entriesURI(), resource.getLocalID()))
|
||||
.withValue(entryColumnDirty(), 0)
|
||||
.build());
|
||||
}
|
||||
|
||||
/** Commits enqueued operations to the content provider (for batch operations). */
|
||||
public int commit() throws LocalStorageException {
|
||||
int affected = 0;
|
||||
if (!pendingOperations.isEmpty())
|
||||
try {
|
||||
Log.d(TAG, "Committing " + pendingOperations.size() + " operations ...");
|
||||
ContentProviderResult[] results = providerClient.applyBatch(pendingOperations);
|
||||
for (ContentProviderResult result : results)
|
||||
if (result != null) // will have either .uri or .count set
|
||||
if (result.count != null)
|
||||
affected += result.count;
|
||||
else if (result.uri != null)
|
||||
affected = 1;
|
||||
Log.d(TAG, "... " + affected + " record(s) affected");
|
||||
pendingOperations.clear();
|
||||
} catch(IllegalArgumentException|OperationApplicationException|RemoteException ex) {
|
||||
throw new LocalStorageException(ex);
|
||||
}
|
||||
return affected;
|
||||
}
|
||||
|
||||
|
||||
// helpers
|
||||
|
||||
protected void queueOperation(Builder builder) {
|
||||
if (builder != null)
|
||||
pendingOperations.add(builder.build());
|
||||
}
|
||||
|
||||
/** Appends account type, name and CALLER_IS_SYNCADAPTER to an Uri. */
|
||||
protected Uri syncAdapterURI(Uri baseURI) {
|
||||
return baseURI.buildUpon()
|
||||
.appendQueryParameter(entryColumnAccountType(), account.type)
|
||||
.appendQueryParameter(entryColumnAccountName(), account.name)
|
||||
.appendQueryParameter(CalendarContract.CALLER_IS_SYNCADAPTER, "true")
|
||||
.build();
|
||||
}
|
||||
|
||||
protected Builder newDataInsertBuilder(Uri dataUri, String refFieldName, long raw_ref_id, int backrefIdx) {
|
||||
Builder builder = ContentProviderOperation.newInsert(syncAdapterURI(dataUri));
|
||||
if (backrefIdx != -1)
|
||||
return builder.withValueBackReference(refFieldName, backrefIdx);
|
||||
else
|
||||
return builder.withValue(refFieldName, raw_ref_id);
|
||||
}
|
||||
|
||||
|
||||
// content builders
|
||||
|
||||
/**
|
||||
* Builds the main entry (for instance, a ContactsContract.RawContacts row) from a resource.
|
||||
* The entry is built for insertion to the location identified by entriesURI().
|
||||
*
|
||||
* @param builder Builder to be extended by all resource data that can be stored without extra data rows.
|
||||
* @param resource Event, task or note resource whose contents shall be inserted/updated
|
||||
* @param update false when the entry is built the first time (when creating the row), true if it's an update
|
||||
*/
|
||||
protected abstract Builder buildEntry(Builder builder, Resource resource, boolean update);
|
||||
|
||||
/** Enqueues adding extra data rows of the resource to the local collection. */
|
||||
protected abstract void addDataRows(Resource resource, long localID, int backrefIdx);
|
||||
|
||||
/** Enqueues removing all extra data rows of the resource from the local collection. */
|
||||
protected abstract void removeDataRows(Resource resource);
|
||||
}
|
@ -1,31 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2013 – 2015 Ricki Hirner (bitfire web engineering).
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the GNU Public License v3.0
|
||||
* which accompanies this distribution, and is available at
|
||||
* http://www.gnu.org/licenses/gpl.html
|
||||
*/
|
||||
package at.bitfire.davdroid.resource;
|
||||
|
||||
public class LocalStorageException extends Exception {
|
||||
private static final long serialVersionUID = -7787658815291629529L;
|
||||
|
||||
private static final String detailMessage = "Couldn't access local content provider";
|
||||
|
||||
|
||||
public LocalStorageException(String detailMessage, Throwable throwable) {
|
||||
super(detailMessage, throwable);
|
||||
}
|
||||
|
||||
public LocalStorageException(String detailMessage) {
|
||||
super(detailMessage);
|
||||
}
|
||||
|
||||
public LocalStorageException(Throwable throwable) {
|
||||
super(detailMessage, throwable);
|
||||
}
|
||||
|
||||
public LocalStorageException() {
|
||||
super(detailMessage);
|
||||
}
|
||||
}
|
@ -1,395 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2013 – 2015 Ricki Hirner (bitfire web engineering).
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the GNU Public License v3.0
|
||||
* which accompanies this distribution, and is available at
|
||||
* http://www.gnu.org/licenses/gpl.html
|
||||
*/
|
||||
|
||||
package at.bitfire.davdroid.resource;
|
||||
|
||||
import android.accounts.Account;
|
||||
import android.content.ContentProviderClient;
|
||||
import android.content.ContentProviderOperation;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.ContentUris;
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.RemoteException;
|
||||
import android.util.Log;
|
||||
|
||||
import net.fortuna.ical4j.model.Date;
|
||||
import net.fortuna.ical4j.model.DateTime;
|
||||
import net.fortuna.ical4j.model.Dur;
|
||||
import net.fortuna.ical4j.model.TimeZone;
|
||||
import net.fortuna.ical4j.model.property.Clazz;
|
||||
import net.fortuna.ical4j.model.property.Completed;
|
||||
import net.fortuna.ical4j.model.property.DtStart;
|
||||
import net.fortuna.ical4j.model.property.Due;
|
||||
import net.fortuna.ical4j.model.property.Duration;
|
||||
import net.fortuna.ical4j.model.property.Status;
|
||||
import net.fortuna.ical4j.util.TimeZones;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.dmfs.provider.tasks.TaskContract;
|
||||
|
||||
import java.util.LinkedList;
|
||||
|
||||
import at.bitfire.davdroid.DavUtils;
|
||||
import at.bitfire.davdroid.DateUtils;
|
||||
import at.bitfire.davdroid.webdav.WebDavResource;
|
||||
import lombok.Cleanup;
|
||||
import lombok.Getter;
|
||||
|
||||
public class LocalTaskList extends LocalCollection<Task> {
|
||||
private static final String TAG = "davdroid.LocalTaskList";
|
||||
|
||||
@Getter protected String url;
|
||||
@Getter protected long id;
|
||||
|
||||
public static final String TASKS_AUTHORITY = "org.dmfs.tasks";
|
||||
|
||||
protected static final String COLLECTION_COLUMN_CTAG = TaskContract.TaskLists.SYNC1;
|
||||
|
||||
@Override protected Uri entriesURI() { return syncAdapterURI(TaskContract.Tasks.getContentUri(TASKS_AUTHORITY)); }
|
||||
@Override protected String entryColumnAccountType() { return TaskContract.Tasks.ACCOUNT_TYPE; }
|
||||
@Override protected String entryColumnAccountName() { return TaskContract.Tasks.ACCOUNT_NAME; }
|
||||
@Override protected String entryColumnParentID() { return TaskContract.Tasks.LIST_ID; }
|
||||
@Override protected String entryColumnID() { return TaskContract.Tasks._ID; }
|
||||
@Override protected String entryColumnRemoteName() { return TaskContract.Tasks._SYNC_ID; }
|
||||
@Override protected String entryColumnETag() { return TaskContract.Tasks.SYNC1; }
|
||||
@Override protected String entryColumnDirty() { return TaskContract.Tasks._DIRTY; }
|
||||
@Override protected String entryColumnDeleted() { return TaskContract.Tasks._DELETED; }
|
||||
@Override protected String entryColumnUID() { return TaskContract.Tasks.SYNC2; }
|
||||
|
||||
|
||||
public static Uri create(Account account, ContentResolver resolver, ServerInfo.ResourceInfo info) throws LocalStorageException {
|
||||
@Cleanup("release") final ContentProviderClient client = resolver.acquireContentProviderClient(TASKS_AUTHORITY);
|
||||
if (client == null)
|
||||
throw new LocalStorageException("No tasks provider found");
|
||||
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(TaskContract.TaskLists.ACCOUNT_NAME, account.name);
|
||||
values.put(TaskContract.TaskLists.ACCOUNT_TYPE, account.type);
|
||||
values.put(TaskContract.TaskLists._SYNC_ID, info.getURL());
|
||||
values.put(TaskContract.TaskLists.LIST_NAME, info.getTitle());
|
||||
values.put(TaskContract.TaskLists.LIST_COLOR, info.getColor() != null ? info.getColor() : DavUtils.calendarGreen);
|
||||
values.put(TaskContract.TaskLists.OWNER, account.name);
|
||||
values.put(TaskContract.TaskLists.ACCESS_LEVEL, 0);
|
||||
values.put(TaskContract.TaskLists.SYNC_ENABLED, 1);
|
||||
values.put(TaskContract.TaskLists.VISIBLE, 1);
|
||||
|
||||
Log.i(TAG, "Inserting task list: " + values.toString());
|
||||
try {
|
||||
return client.insert(taskListsURI(account), values);
|
||||
} catch (RemoteException e) {
|
||||
throw new LocalStorageException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static LocalTaskList[] findAll(Account account, ContentProviderClient providerClient) throws RemoteException {
|
||||
@Cleanup Cursor cursor = providerClient.query(taskListsURI(account),
|
||||
new String[]{TaskContract.TaskLists._ID, TaskContract.TaskLists._SYNC_ID},
|
||||
null, null, null);
|
||||
|
||||
LinkedList<LocalTaskList> taskList = new LinkedList<>();
|
||||
while (cursor != null && cursor.moveToNext())
|
||||
taskList.add(new LocalTaskList(account, providerClient, cursor.getInt(0), cursor.getString(1)));
|
||||
return taskList.toArray(new LocalTaskList[taskList.size()]);
|
||||
}
|
||||
|
||||
public LocalTaskList(Account account, ContentProviderClient providerClient, long id, String url) {
|
||||
super(account, providerClient);
|
||||
this.id = id;
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String getCTag() throws LocalStorageException {
|
||||
try {
|
||||
@Cleanup Cursor c = providerClient.query(ContentUris.withAppendedId(taskListsURI(account), id),
|
||||
new String[] { COLLECTION_COLUMN_CTAG }, null, null, null);
|
||||
if (c != null && c.moveToFirst())
|
||||
return c.getString(0);
|
||||
else
|
||||
throw new LocalStorageException("Couldn't query task list CTag");
|
||||
} catch(RemoteException e) {
|
||||
throw new LocalStorageException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setCTag(String cTag) throws LocalStorageException {
|
||||
ContentValues values = new ContentValues(1);
|
||||
values.put(COLLECTION_COLUMN_CTAG, cTag);
|
||||
try {
|
||||
providerClient.update(ContentUris.withAppendedId(taskListsURI(account), id), values, null, null);
|
||||
} catch(RemoteException e) {
|
||||
throw new LocalStorageException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateMetaData(WebDavResource.Properties properties) throws LocalStorageException {
|
||||
ContentValues values = new ContentValues();
|
||||
|
||||
final String displayName = properties.getDisplayName();
|
||||
if (displayName != null)
|
||||
values.put(TaskContract.TaskLists.LIST_NAME, displayName);
|
||||
|
||||
final Integer color = properties.getColor();
|
||||
if (color != null)
|
||||
values.put(TaskContract.TaskLists.LIST_COLOR, color);
|
||||
|
||||
try {
|
||||
if (values.size() > 0)
|
||||
providerClient.update(ContentUris.withAppendedId(taskListsURI(account), id), values, null, null);
|
||||
} catch(RemoteException e) {
|
||||
throw new LocalStorageException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Task newResource(long localID, String resourceName, String eTag) {
|
||||
return new Task(localID, resourceName, eTag);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void populate(Resource record) throws LocalStorageException {
|
||||
try {
|
||||
@Cleanup final Cursor cursor = providerClient.query(entriesURI(),
|
||||
new String[] {
|
||||
/* 0 */ entryColumnUID(), TaskContract.Tasks.TITLE, TaskContract.Tasks.LOCATION, TaskContract.Tasks.DESCRIPTION, TaskContract.Tasks.URL,
|
||||
/* 5 */ TaskContract.Tasks.CLASSIFICATION, TaskContract.Tasks.STATUS, TaskContract.Tasks.PERCENT_COMPLETE,
|
||||
/* 8 */ TaskContract.Tasks.TZ, TaskContract.Tasks.DTSTART, TaskContract.Tasks.IS_ALLDAY,
|
||||
/* 11 */ TaskContract.Tasks.DUE, TaskContract.Tasks.DURATION, TaskContract.Tasks.COMPLETED,
|
||||
/* 14 */ TaskContract.Tasks.CREATED, TaskContract.Tasks.LAST_MODIFIED, TaskContract.Tasks.PRIORITY
|
||||
}, entryColumnID() + "=?", new String[]{ String.valueOf(record.getLocalID()) }, null);
|
||||
|
||||
Task task = (Task)record;
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
task.setUid(cursor.getString(0));
|
||||
|
||||
if (!cursor.isNull(14))
|
||||
task.setCreatedAt(new DateTime(cursor.getLong(14)));
|
||||
if (!cursor.isNull(15))
|
||||
task.setLastModified(new DateTime(cursor.getLong(15)));
|
||||
|
||||
if (!StringUtils.isEmpty(cursor.getString(1)))
|
||||
task.setSummary(cursor.getString(1));
|
||||
|
||||
if (!StringUtils.isEmpty(cursor.getString(2)))
|
||||
task.setLocation(cursor.getString(2));
|
||||
|
||||
if (!StringUtils.isEmpty(cursor.getString(3)))
|
||||
task.setDescription(cursor.getString(3));
|
||||
|
||||
if (!StringUtils.isEmpty(cursor.getString(4)))
|
||||
task.setUrl(cursor.getString(4));
|
||||
|
||||
if (!cursor.isNull(16))
|
||||
task.setPriority(cursor.getInt(16));
|
||||
|
||||
if (!cursor.isNull(5))
|
||||
switch (cursor.getInt(5)) {
|
||||
case TaskContract.Tasks.CLASSIFICATION_PUBLIC:
|
||||
task.setClassification(Clazz.PUBLIC);
|
||||
break;
|
||||
case TaskContract.Tasks.CLASSIFICATION_CONFIDENTIAL:
|
||||
task.setClassification(Clazz.CONFIDENTIAL);
|
||||
break;
|
||||
default:
|
||||
task.setClassification(Clazz.PRIVATE);
|
||||
}
|
||||
|
||||
if (!cursor.isNull(6))
|
||||
switch (cursor.getInt(6)) {
|
||||
case TaskContract.Tasks.STATUS_IN_PROCESS:
|
||||
task.setStatus(Status.VTODO_IN_PROCESS);
|
||||
break;
|
||||
case TaskContract.Tasks.STATUS_COMPLETED:
|
||||
task.setStatus(Status.VTODO_COMPLETED);
|
||||
break;
|
||||
case TaskContract.Tasks.STATUS_CANCELLED:
|
||||
task.setStatus(Status.VTODO_CANCELLED);
|
||||
break;
|
||||
default:
|
||||
task.setStatus(Status.VTODO_NEEDS_ACTION);
|
||||
}
|
||||
if (!cursor.isNull(7))
|
||||
task.setPercentComplete(cursor.getInt(7));
|
||||
|
||||
TimeZone tz = null;
|
||||
if (!cursor.isNull(8))
|
||||
tz = DateUtils.tzRegistry.getTimeZone(cursor.getString(8));
|
||||
|
||||
if (!cursor.isNull(9) && !cursor.isNull(10)) {
|
||||
long ts = cursor.getLong(9);
|
||||
boolean allDay = cursor.getInt(10) != 0;
|
||||
|
||||
Date dt;
|
||||
if (allDay)
|
||||
dt = new Date(ts);
|
||||
else {
|
||||
dt = new DateTime(ts);
|
||||
if (tz != null)
|
||||
((DateTime)dt).setTimeZone(tz);
|
||||
}
|
||||
task.setDtStart(new DtStart(dt));
|
||||
}
|
||||
|
||||
if (!cursor.isNull(11)) {
|
||||
DateTime dt = new DateTime(cursor.getLong(11));
|
||||
if (tz != null)
|
||||
dt.setTimeZone(tz);
|
||||
task.setDue(new Due(dt));
|
||||
}
|
||||
|
||||
if (!cursor.isNull(12))
|
||||
task.setDuration(new Duration(new Dur(cursor.getString(12))));
|
||||
|
||||
if (!cursor.isNull(13))
|
||||
task.setCompletedAt(new Completed(new DateTime(cursor.getLong(13))));
|
||||
}
|
||||
|
||||
} catch (RemoteException e) {
|
||||
throw new LocalStorageException("Couldn't process locally stored task", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ContentProviderOperation.Builder buildEntry(ContentProviderOperation.Builder builder, Resource resource, boolean update) {
|
||||
final Task task = (Task)resource;
|
||||
|
||||
if (!update)
|
||||
builder .withValue(entryColumnParentID(), id)
|
||||
.withValue(entryColumnRemoteName(), task.getName())
|
||||
.withValue(entryColumnDirty(), 0); // _DIRTY is INTEGER DEFAULT 1 in org.dmfs.provider.tasks
|
||||
|
||||
builder.withValue(entryColumnUID(), task.getUid())
|
||||
.withValue(entryColumnETag(), task.getETag())
|
||||
.withValue(TaskContract.Tasks.TITLE, task.getSummary())
|
||||
.withValue(TaskContract.Tasks.LOCATION, task.getLocation())
|
||||
.withValue(TaskContract.Tasks.DESCRIPTION, task.getDescription())
|
||||
.withValue(TaskContract.Tasks.URL, task.getUrl())
|
||||
.withValue(TaskContract.Tasks.PRIORITY, task.getPriority());
|
||||
|
||||
if (task.getCreatedAt() != null)
|
||||
builder.withValue(TaskContract.Tasks.CREATED, task.getCreatedAt().getTime());
|
||||
if (task.getLastModified() != null)
|
||||
builder.withValue(TaskContract.Tasks.LAST_MODIFIED, task.getLastModified().getTime());
|
||||
|
||||
if (task.getClassification() != null) {
|
||||
int classCode = TaskContract.Tasks.CLASSIFICATION_PRIVATE;
|
||||
if (task.getClassification() == Clazz.PUBLIC)
|
||||
classCode = TaskContract.Tasks.CLASSIFICATION_PUBLIC;
|
||||
else if (task.getClassification() == Clazz.CONFIDENTIAL)
|
||||
classCode = TaskContract.Tasks.CLASSIFICATION_CONFIDENTIAL;
|
||||
builder = builder.withValue(TaskContract.Tasks.CLASSIFICATION, classCode);
|
||||
}
|
||||
|
||||
int statusCode = TaskContract.Tasks.STATUS_DEFAULT;
|
||||
if (task.getStatus() != null) {
|
||||
if (task.getStatus() == Status.VTODO_NEEDS_ACTION)
|
||||
statusCode = TaskContract.Tasks.STATUS_NEEDS_ACTION;
|
||||
else if (task.getStatus() == Status.VTODO_IN_PROCESS)
|
||||
statusCode = TaskContract.Tasks.STATUS_IN_PROCESS;
|
||||
else if (task.getStatus() == Status.VTODO_COMPLETED)
|
||||
statusCode = TaskContract.Tasks.STATUS_COMPLETED;
|
||||
else if (task.getStatus() == Status.VTODO_CANCELLED)
|
||||
statusCode = TaskContract.Tasks.STATUS_CANCELLED;
|
||||
}
|
||||
builder .withValue(TaskContract.Tasks.STATUS, statusCode)
|
||||
.withValue(TaskContract.Tasks.PERCENT_COMPLETE, task.getPercentComplete());
|
||||
|
||||
TimeZone tz = null;
|
||||
|
||||
if (task.getDtStart() != null) {
|
||||
Date start = task.getDtStart().getDate();
|
||||
boolean allDay;
|
||||
if (start instanceof DateTime) {
|
||||
allDay = false;
|
||||
tz = ((DateTime)start).getTimeZone();
|
||||
} else
|
||||
allDay = true;
|
||||
long ts = start.getTime();
|
||||
builder .withValue(TaskContract.Tasks.DTSTART, ts)
|
||||
.withValue(TaskContract.Tasks.IS_ALLDAY, allDay ? 1 : 0);
|
||||
}
|
||||
|
||||
if (task.getDue() != null) {
|
||||
Due due = task.getDue();
|
||||
builder.withValue(TaskContract.Tasks.DUE, due.getDate().getTime());
|
||||
if (tz == null)
|
||||
tz = due.getTimeZone();
|
||||
|
||||
} else if (task.getDuration() != null)
|
||||
builder.withValue(TaskContract.Tasks.DURATION, task.getDuration().getValue());
|
||||
|
||||
if (task.getCompletedAt() != null) {
|
||||
Date completed = task.getCompletedAt().getDate();
|
||||
boolean allDay;
|
||||
if (completed instanceof DateTime) {
|
||||
allDay = false;
|
||||
if (tz == null)
|
||||
tz = ((DateTime)completed).getTimeZone();
|
||||
} else {
|
||||
task.getCompletedAt().setUtc(true);
|
||||
allDay = true;
|
||||
}
|
||||
long ts = completed.getTime();
|
||||
builder .withValue(TaskContract.Tasks.COMPLETED, ts)
|
||||
.withValue(TaskContract.Tasks.COMPLETED_IS_ALLDAY, allDay ? 1 : 0);
|
||||
}
|
||||
|
||||
// TZ *must* be provided when DTSTART or DUE is set
|
||||
if ((task.getDtStart() != null || task.getDue() != null) && tz == null)
|
||||
tz = DateUtils.tzRegistry.getTimeZone(TimeZones.GMT_ID);
|
||||
if (tz != null)
|
||||
builder.withValue(TaskContract.Tasks.TZ, DateUtils.findAndroidTimezoneID(tz.getID()));
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void addDataRows(Resource resource, long localID, int backrefIdx) {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void removeDataRows(Resource resource) {
|
||||
}
|
||||
|
||||
|
||||
// helpers
|
||||
|
||||
public static boolean isAvailable(Context context) {
|
||||
try {
|
||||
@Cleanup("release") ContentProviderClient client = context.getContentResolver().acquireContentProviderClient(TASKS_AUTHORITY);
|
||||
return client != null;
|
||||
} catch (SecurityException e) {
|
||||
Log.e(TAG, "DAVdroid is not allowed to access tasks", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Uri syncAdapterURI(Uri baseURI) {
|
||||
return baseURI.buildUpon()
|
||||
.appendQueryParameter(entryColumnAccountType(), account.type)
|
||||
.appendQueryParameter(entryColumnAccountName(), account.name)
|
||||
.appendQueryParameter(TaskContract.CALLER_IS_SYNCADAPTER, "true")
|
||||
.build();
|
||||
}
|
||||
|
||||
protected static Uri taskListsURI(Account account) {
|
||||
return TaskContract.TaskLists.getContentUri(TASKS_AUTHORITY).buildUpon()
|
||||
.appendQueryParameter(TaskContract.TaskLists.ACCOUNT_TYPE, account.type)
|
||||
.appendQueryParameter(TaskContract.TaskLists.ACCOUNT_NAME, account.name)
|
||||
.appendQueryParameter(TaskContract.CALLER_IS_SYNCADAPTER, "true")
|
||||
.build();
|
||||
}
|
||||
}
|
@ -1,24 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2013 – 2015 Ricki Hirner (bitfire web engineering).
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the GNU Public License v3.0
|
||||
* which accompanies this distribution, and is available at
|
||||
* http://www.gnu.org/licenses/gpl.html
|
||||
*/
|
||||
package at.bitfire.davdroid.resource;
|
||||
|
||||
/**
|
||||
* Thrown when a local record (for instance, Contact with ID 12345) should be read
|
||||
* but could not be found.
|
||||
*/
|
||||
public class RecordNotFoundException extends LocalStorageException {
|
||||
private static final long serialVersionUID = 4961024282198632578L;
|
||||
|
||||
private static final String detailMessage = "Record not found in local content provider";
|
||||
|
||||
|
||||
RecordNotFoundException() {
|
||||
super(detailMessage);
|
||||
}
|
||||
|
||||
}
|
@ -1,65 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2013 – 2015 Ricki Hirner (bitfire web engineering).
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the GNU Public License v3.0
|
||||
* which accompanies this distribution, and is available at
|
||||
* http://www.gnu.org/licenses/gpl.html
|
||||
*/
|
||||
package at.bitfire.davdroid.resource;
|
||||
|
||||
import org.apache.http.entity.ContentType;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.nio.charset.Charset;
|
||||
|
||||
import at.bitfire.davdroid.webdav.DavException;
|
||||
import at.bitfire.davdroid.webdav.HttpException;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.ToString;
|
||||
|
||||
/**
|
||||
* Represents a resource that can be contained in a LocalCollection or RemoteCollection
|
||||
* for synchronization by WebDAV.
|
||||
*/
|
||||
@ToString
|
||||
public abstract class Resource {
|
||||
@Getter @Setter protected String name, ETag;
|
||||
@Getter @Setter protected String uid;
|
||||
@Getter protected long localID;
|
||||
|
||||
public Resource(String name, String ETag) {
|
||||
this.name = name;
|
||||
this.ETag = ETag;
|
||||
}
|
||||
|
||||
public Resource(long localID, String name, String ETag) {
|
||||
this(name, ETag);
|
||||
this.localID = localID;
|
||||
}
|
||||
|
||||
/** initializes UID and remote file name (required for first upload) */
|
||||
public abstract void initialize();
|
||||
|
||||
/** fills the resource data from an input stream (for instance, .vcf file for Contact)
|
||||
* @param entity entity to parse
|
||||
* @param downloader will be used to fetch additional resources like contact images
|
||||
**/
|
||||
public abstract void parseEntity(InputStream entity, Charset charset, AssetDownloader downloader) throws IOException, InvalidResourceException;
|
||||
|
||||
|
||||
/* returns the MIME type that toEntity() will produce */
|
||||
public abstract ContentType getContentType();
|
||||
|
||||
/** writes the resource data to an output stream (for instance, .vcf file for Contact) */
|
||||
public abstract ByteArrayOutputStream toEntity() throws IOException;
|
||||
|
||||
|
||||
public interface AssetDownloader {
|
||||
byte[] download(URI url) throws URISyntaxException, IOException, HttpException, DavException;
|
||||
}
|
||||
}
|
@ -1,223 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2013 – 2015 Ricki Hirner (bitfire web engineering).
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the GNU Public License v3.0
|
||||
* which accompanies this distribution, and is available at
|
||||
* http://www.gnu.org/licenses/gpl.html
|
||||
*/
|
||||
|
||||
package at.bitfire.davdroid.resource;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import net.fortuna.ical4j.data.CalendarOutputter;
|
||||
import net.fortuna.ical4j.data.ParserException;
|
||||
import net.fortuna.ical4j.model.Component;
|
||||
import net.fortuna.ical4j.model.ComponentList;
|
||||
import net.fortuna.ical4j.model.DateTime;
|
||||
import net.fortuna.ical4j.model.PropertyList;
|
||||
import net.fortuna.ical4j.model.TimeZone;
|
||||
import net.fortuna.ical4j.model.ValidationException;
|
||||
import net.fortuna.ical4j.model.component.VToDo;
|
||||
import net.fortuna.ical4j.model.property.Clazz;
|
||||
import net.fortuna.ical4j.model.property.Completed;
|
||||
import net.fortuna.ical4j.model.property.Created;
|
||||
import net.fortuna.ical4j.model.property.Description;
|
||||
import net.fortuna.ical4j.model.property.DtStart;
|
||||
import net.fortuna.ical4j.model.property.Due;
|
||||
import net.fortuna.ical4j.model.property.Duration;
|
||||
import net.fortuna.ical4j.model.property.LastModified;
|
||||
import net.fortuna.ical4j.model.property.Location;
|
||||
import net.fortuna.ical4j.model.property.PercentComplete;
|
||||
import net.fortuna.ical4j.model.property.Priority;
|
||||
import net.fortuna.ical4j.model.property.Status;
|
||||
import net.fortuna.ical4j.model.property.Summary;
|
||||
import net.fortuna.ical4j.model.property.Uid;
|
||||
import net.fortuna.ical4j.model.property.Url;
|
||||
import net.fortuna.ical4j.model.property.Version;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import at.bitfire.davdroid.Constants;
|
||||
import lombok.Cleanup;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
public class Task extends iCalendar {
|
||||
private final static String TAG = "davdroid.Task";
|
||||
|
||||
@Getter @Setter DateTime createdAt;
|
||||
@Getter @Setter DateTime lastModified;
|
||||
|
||||
@Getter @Setter String summary, location, description, url;
|
||||
@Getter @Setter int priority;
|
||||
@Getter @Setter Clazz classification;
|
||||
@Getter @Setter Status status;
|
||||
|
||||
@Getter @Setter DtStart dtStart;
|
||||
@Getter @Setter Due due;
|
||||
@Getter @Setter Duration duration;
|
||||
@Getter @Setter Completed completedAt;
|
||||
@Getter @Setter Integer percentComplete;
|
||||
|
||||
|
||||
public Task(String name, String ETag) {
|
||||
super(name, ETag);
|
||||
}
|
||||
|
||||
public Task(long localId, String name, String ETag)
|
||||
{
|
||||
super(localId, name, ETag);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void parseEntity(InputStream entity, Charset charset, AssetDownloader downloader) throws IOException, InvalidResourceException {
|
||||
final net.fortuna.ical4j.model.Calendar ical;
|
||||
try {
|
||||
if (charset != null) {
|
||||
@Cleanup InputStreamReader reader = new InputStreamReader(entity, charset);
|
||||
ical = calendarBuilder.build(reader);
|
||||
} else
|
||||
ical = calendarBuilder.build(entity);
|
||||
|
||||
if (ical == null)
|
||||
throw new InvalidResourceException("No iCalendar found");
|
||||
} catch (ParserException e) {
|
||||
throw new InvalidResourceException(e);
|
||||
}
|
||||
|
||||
ComponentList notes = ical.getComponents(Component.VTODO);
|
||||
if (notes == null || notes.isEmpty())
|
||||
throw new InvalidResourceException("No VTODO found");
|
||||
VToDo todo = (VToDo)notes.get(0);
|
||||
|
||||
if (todo.getUid() != null)
|
||||
uid = todo.getUid().getValue();
|
||||
else {
|
||||
Log.w(TAG, "Received VTODO without UID, generating new one");
|
||||
generateUID();
|
||||
}
|
||||
|
||||
if (todo.getCreated() != null)
|
||||
createdAt = todo.getCreated().getDateTime();
|
||||
if (todo.getLastModified() != null)
|
||||
lastModified = todo.getLastModified().getDateTime();
|
||||
|
||||
if (todo.getSummary() != null)
|
||||
summary = todo.getSummary().getValue();
|
||||
if (todo.getLocation() != null)
|
||||
location = todo.getLocation().getValue();
|
||||
if (todo.getDescription() != null)
|
||||
description = todo.getDescription().getValue();
|
||||
if (todo.getUrl() != null)
|
||||
url = todo.getUrl().getValue();
|
||||
|
||||
priority = (todo.getPriority() != null) ? todo.getPriority().getLevel() : 0;
|
||||
if (todo.getClassification() != null)
|
||||
classification = todo.getClassification();
|
||||
if (todo.getStatus() != null)
|
||||
status = todo.getStatus();
|
||||
|
||||
if (todo.getDue() != null) {
|
||||
due = todo.getDue();
|
||||
validateTimeZone(due);
|
||||
}
|
||||
if (todo.getDuration() != null)
|
||||
duration = todo.getDuration();
|
||||
if (todo.getStartDate() != null) {
|
||||
dtStart = todo.getStartDate();
|
||||
validateTimeZone(dtStart);
|
||||
}
|
||||
if (todo.getDateCompleted() != null) {
|
||||
completedAt = todo.getDateCompleted();
|
||||
validateTimeZone(completedAt);
|
||||
}
|
||||
if (todo.getPercentComplete() != null)
|
||||
percentComplete = todo.getPercentComplete().getPercentage();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public ByteArrayOutputStream toEntity() throws IOException {
|
||||
final net.fortuna.ical4j.model.Calendar ical = new net.fortuna.ical4j.model.Calendar();
|
||||
ical.getProperties().add(Version.VERSION_2_0);
|
||||
ical.getProperties().add(Constants.ICAL_PRODID);
|
||||
|
||||
final VToDo todo = new VToDo();
|
||||
ical.getComponents().add(todo);
|
||||
final PropertyList props = todo.getProperties();
|
||||
|
||||
if (uid != null)
|
||||
props.add(new Uid(uid));
|
||||
|
||||
if (createdAt != null)
|
||||
props.add(new Created(createdAt));
|
||||
if (lastModified != null)
|
||||
props.add(new LastModified(lastModified));
|
||||
|
||||
if (summary != null)
|
||||
props.add(new Summary(summary));
|
||||
if (location != null)
|
||||
props.add(new Location(location));
|
||||
if (description != null)
|
||||
props.add(new Description(description));
|
||||
if (url != null)
|
||||
try {
|
||||
props.add(new Url(new URI(url)));
|
||||
} catch (URISyntaxException e) {
|
||||
Log.e(TAG, "Ignoring invalid task URL: " + url, e);
|
||||
}
|
||||
if (priority != 0)
|
||||
props.add(new Priority(priority));
|
||||
if (classification != null)
|
||||
props.add(classification);
|
||||
if (status != null)
|
||||
props.add(status);
|
||||
|
||||
// remember used time zones
|
||||
Set<TimeZone> usedTimeZones = new HashSet<>();
|
||||
|
||||
if (due != null) {
|
||||
props.add(due);
|
||||
if (due.getTimeZone() != null)
|
||||
usedTimeZones.add(due.getTimeZone());
|
||||
}
|
||||
if (duration != null)
|
||||
props.add(duration);
|
||||
if (dtStart != null) {
|
||||
props.add(dtStart);
|
||||
if (dtStart.getTimeZone() != null)
|
||||
usedTimeZones.add(dtStart.getTimeZone());
|
||||
}
|
||||
if (completedAt != null) {
|
||||
props.add(completedAt);
|
||||
if (completedAt.getTimeZone() != null)
|
||||
usedTimeZones.add(completedAt.getTimeZone());
|
||||
}
|
||||
if (percentComplete != null)
|
||||
props.add(new PercentComplete(percentComplete));
|
||||
|
||||
// add VTIMEZONE components
|
||||
for (TimeZone timeZone : usedTimeZones)
|
||||
ical.getComponents().add(timeZone.getVTimeZone());
|
||||
|
||||
CalendarOutputter output = new CalendarOutputter(false);
|
||||
ByteArrayOutputStream os = new ByteArrayOutputStream();
|
||||
try {
|
||||
output.output(ical, os);
|
||||
} catch (ValidationException e) {
|
||||
Log.e(TAG, "Generated invalid iCalendar");
|
||||
}
|
||||
return os;
|
||||
}
|
||||
|
||||
}
|
@ -1,226 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2013 – 2015 Ricki Hirner (bitfire web engineering).
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the GNU Public License v3.0
|
||||
* which accompanies this distribution, and is available at
|
||||
* http://www.gnu.org/licenses/gpl.html
|
||||
*/
|
||||
package at.bitfire.davdroid.resource;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.apache.http.entity.ContentType;
|
||||
import org.apache.http.impl.client.CloseableHttpClient;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
import at.bitfire.davdroid.URIUtils;
|
||||
import at.bitfire.davdroid.webdav.DavException;
|
||||
import at.bitfire.davdroid.webdav.DavMultiget;
|
||||
import at.bitfire.davdroid.webdav.DavNoContentException;
|
||||
import at.bitfire.davdroid.webdav.HttpException;
|
||||
import at.bitfire.davdroid.webdav.HttpPropfind;
|
||||
import at.bitfire.davdroid.webdav.WebDavResource;
|
||||
import at.bitfire.davdroid.webdav.WebDavResource.PutMode;
|
||||
import ezvcard.io.text.VCardParseException;
|
||||
import lombok.Cleanup;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* Represents a remotely stored synchronizable collection (collection as in
|
||||
* WebDAV terminology).
|
||||
*
|
||||
* @param <T> Subtype of Resource that can be stored in the collection
|
||||
*/
|
||||
public abstract class WebDavCollection<T extends Resource> {
|
||||
private static final String TAG = "davdroid.resource";
|
||||
|
||||
URI baseURI;
|
||||
@Getter WebDavResource collection;
|
||||
|
||||
abstract protected String memberAcceptedMimeTypes();
|
||||
abstract protected DavMultiget.Type multiGetType();
|
||||
|
||||
abstract protected T newResourceSkeleton(String name, String ETag);
|
||||
|
||||
public WebDavCollection(CloseableHttpClient httpClient, String baseURL, String user, String password, boolean preemptiveAuth) throws URISyntaxException {
|
||||
baseURI = URIUtils.parseURI(baseURL, false);
|
||||
collection = new WebDavResource(httpClient, baseURI, user, password, preemptiveAuth);
|
||||
}
|
||||
|
||||
|
||||
/* collection operations */
|
||||
|
||||
public void getProperties() throws URISyntaxException, IOException, HttpException, DavException {
|
||||
collection.propfind(HttpPropfind.Mode.COLLECTION_PROPERTIES);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns a body for the REPORT request that queries all members of the collection
|
||||
* that should be considered. Allows collections to implement remote filters.
|
||||
* @return body for REPORT request or null if PROPFIND shall be used
|
||||
*/
|
||||
public String getMemberETagsQuery() {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the ETags of the resources in a collection. If davQuery is set, it's required to be a
|
||||
* complete addressbook-query or calendar-query XML and will cause getMemberETags() to use REPORT
|
||||
* instead of PROPFIND.
|
||||
* @return array of Resources where only the resource names and ETags are set
|
||||
*/
|
||||
public Resource[] getMemberETags() throws URISyntaxException, IOException, DavException, HttpException {
|
||||
String query = getMemberETagsQuery();
|
||||
if (query != null)
|
||||
collection.report(query);
|
||||
else
|
||||
collection.propfind(HttpPropfind.Mode.MEMBERS_ETAG);
|
||||
|
||||
List<T> resources = new LinkedList<>();
|
||||
if (collection.getMembers() != null)
|
||||
for (WebDavResource member : collection.getMembers())
|
||||
resources.add(newResourceSkeleton(member.getName(), member.getProperties().getETag()));
|
||||
|
||||
return resources.toArray(new Resource[resources.size()]);
|
||||
}
|
||||
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public Resource[] multiGet(Resource[] resources) throws URISyntaxException, IOException, DavException, HttpException {
|
||||
try {
|
||||
if (resources.length == 1)
|
||||
return new Resource[] { get(resources[0]) };
|
||||
|
||||
Log.i(TAG, "Multi-getting " + resources.length + " remote resource(s)");
|
||||
|
||||
LinkedList<String> names = new LinkedList<>();
|
||||
for (Resource resource : resources)
|
||||
names.add(resource.getName());
|
||||
|
||||
LinkedList<T> foundResources = new LinkedList<>();
|
||||
collection.multiGet(multiGetType(), names.toArray(new String[names.size()]));
|
||||
if (collection.getMembers() == null)
|
||||
throw new DavNoContentException();
|
||||
|
||||
for (WebDavResource member : collection.getMembers()) {
|
||||
T resource = newResourceSkeleton(member.getName(), member.getProperties().getETag());
|
||||
try {
|
||||
if (member.getContent() != null) {
|
||||
@Cleanup InputStream is = new ByteArrayInputStream(member.getContent());
|
||||
resource.parseEntity(is, null, getDownloader());
|
||||
foundResources.add(resource);
|
||||
} else
|
||||
Log.e(TAG, "Ignoring entity without content");
|
||||
} catch (InvalidResourceException e) {
|
||||
Log.e(TAG, "Ignoring unparseable entity in multi-response", e);
|
||||
}
|
||||
}
|
||||
|
||||
return foundResources.toArray(new Resource[foundResources.size()]);
|
||||
} catch (InvalidResourceException e) {
|
||||
Log.e(TAG, "Couldn't parse entity from GET", e);
|
||||
}
|
||||
|
||||
return new Resource[0];
|
||||
}
|
||||
|
||||
|
||||
/* internal member operations */
|
||||
|
||||
public Resource get(Resource resource) throws URISyntaxException, IOException, HttpException, DavException, InvalidResourceException {
|
||||
WebDavResource member = new WebDavResource(collection, resource.getName());
|
||||
|
||||
member.get(memberAcceptedMimeTypes());
|
||||
|
||||
final byte[] data = member.getContent();
|
||||
if (data == null)
|
||||
throw new DavNoContentException();
|
||||
|
||||
@Cleanup InputStream is = new ByteArrayInputStream(data);
|
||||
try {
|
||||
Charset charset = null;
|
||||
ContentType mime = member.getProperties().getContentType();
|
||||
if (mime != null)
|
||||
charset = mime.getCharset();
|
||||
resource.parseEntity(is, charset, getDownloader());
|
||||
} catch (VCardParseException e) {
|
||||
throw new InvalidResourceException(e);
|
||||
}
|
||||
return resource;
|
||||
}
|
||||
|
||||
// returns ETag of the created resource, if returned by server
|
||||
public String add(Resource res) throws URISyntaxException, IOException, HttpException {
|
||||
WebDavResource member = new WebDavResource(collection, res.getName(), res.getETag());
|
||||
member.getProperties().setContentType(res.getContentType());
|
||||
|
||||
@Cleanup ByteArrayOutputStream os = res.toEntity();
|
||||
String eTag = member.put(os.toByteArray(), PutMode.ADD_DONT_OVERWRITE);
|
||||
|
||||
// after a successful upload, the collection has implicitly changed, too
|
||||
collection.getProperties().invalidateCTag();
|
||||
|
||||
return eTag;
|
||||
}
|
||||
|
||||
public void delete(Resource res) throws URISyntaxException, IOException, HttpException {
|
||||
WebDavResource member = new WebDavResource(collection, res.getName(), res.getETag());
|
||||
member.delete();
|
||||
|
||||
collection.getProperties().invalidateCTag();
|
||||
}
|
||||
|
||||
// returns ETag of the updated resource, if returned by server
|
||||
public String update(Resource res) throws URISyntaxException, IOException, HttpException {
|
||||
WebDavResource member = new WebDavResource(collection, res.getName(), res.getETag());
|
||||
member.getProperties().setContentType(res.getContentType());
|
||||
|
||||
@Cleanup ByteArrayOutputStream os = res.toEntity();
|
||||
String eTag = member.put(os.toByteArray(), PutMode.UPDATE_DONT_OVERWRITE);
|
||||
|
||||
// after a successful upload, the collection has implicitely changed, too
|
||||
collection.getProperties().invalidateCTag();
|
||||
|
||||
return eTag;
|
||||
}
|
||||
|
||||
|
||||
// helpers
|
||||
|
||||
Resource.AssetDownloader getDownloader() {
|
||||
return new Resource.AssetDownloader() {
|
||||
@Override
|
||||
public byte[] download(URI uri) throws URISyntaxException, IOException, HttpException, DavException {
|
||||
if (!uri.isAbsolute())
|
||||
throw new URISyntaxException(uri.toString(), "URI referenced from entity must be absolute");
|
||||
|
||||
// send credentials when asset has same URI authority as baseURI
|
||||
// we have to construct these helper URIs because
|
||||
// "https://server:443" is NOT equal to "https://server" otherwise
|
||||
URI baseUriAuthority = new URI(baseURI.getScheme(), null, baseURI.getHost(), baseURI.getPort(), null, null, null),
|
||||
assetUriAuthority = new URI(uri.getScheme(), null, uri.getHost(), baseURI.getPort(), null, null, null);
|
||||
|
||||
if (baseUriAuthority.equals(assetUriAuthority)) {
|
||||
Log.d(TAG, "Asset is on same server, sending credentials");
|
||||
WebDavResource file = new WebDavResource(collection, uri);
|
||||
file.get("image/*");
|
||||
return file.getContent();
|
||||
|
||||
} else
|
||||
// resource is on an external server, don't send credentials
|
||||
return IOUtils.toByteArray(uri);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
@ -1,169 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2013 – 2015 Ricki Hirner (bitfire web engineering).
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the GNU Public License v3.0
|
||||
* which accompanies this distribution, and is available at
|
||||
* http://www.gnu.org/licenses/gpl.html
|
||||
*/
|
||||
|
||||
package at.bitfire.davdroid.resource;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import net.fortuna.ical4j.data.CalendarBuilder;
|
||||
import net.fortuna.ical4j.data.CalendarParserFactory;
|
||||
import net.fortuna.ical4j.data.ParserException;
|
||||
import net.fortuna.ical4j.model.DateTime;
|
||||
import net.fortuna.ical4j.model.Parameter;
|
||||
import net.fortuna.ical4j.model.ParameterFactory;
|
||||
import net.fortuna.ical4j.model.ParameterFactoryImpl;
|
||||
import net.fortuna.ical4j.model.ParameterFactoryRegistry;
|
||||
import net.fortuna.ical4j.model.PropertyFactoryRegistry;
|
||||
import net.fortuna.ical4j.model.component.VTimeZone;
|
||||
import net.fortuna.ical4j.model.property.DateProperty;
|
||||
import net.fortuna.ical4j.util.CompatibilityHints;
|
||||
import net.fortuna.ical4j.util.SimpleHostInfo;
|
||||
import net.fortuna.ical4j.util.Strings;
|
||||
import net.fortuna.ical4j.util.UidGenerator;
|
||||
|
||||
import org.apache.commons.codec.CharEncoding;
|
||||
import org.apache.http.entity.ContentType;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.StringReader;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.TimeZone;
|
||||
|
||||
import at.bitfire.davdroid.DateUtils;
|
||||
import at.bitfire.davdroid.syncadapter.DavSyncAdapter;
|
||||
import lombok.Getter;
|
||||
import lombok.NonNull;
|
||||
|
||||
public abstract class iCalendar extends Resource {
|
||||
private static final String TAG = "DAVdroid.iCal";
|
||||
|
||||
// static ical4j initialization
|
||||
static {
|
||||
CompatibilityHints.setHintEnabled(CompatibilityHints.KEY_RELAXED_UNFOLDING, true);
|
||||
CompatibilityHints.setHintEnabled(CompatibilityHints.KEY_RELAXED_PARSING, true);
|
||||
CompatibilityHints.setHintEnabled(CompatibilityHints.KEY_OUTLOOK_COMPATIBILITY, true);
|
||||
}
|
||||
|
||||
public static final ContentType MIME_ICALENDAR = ContentType.create("text/calendar", CharEncoding.UTF_8);
|
||||
|
||||
|
||||
public iCalendar(long localID, String name, String ETag) {
|
||||
super(localID, name, ETag);
|
||||
}
|
||||
|
||||
public iCalendar(String name, String ETag) {
|
||||
super(name, ETag);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
generateUID();
|
||||
name = uid + ".ics";
|
||||
}
|
||||
|
||||
protected void generateUID() {
|
||||
UidGenerator generator = new UidGenerator(new SimpleHostInfo(DavSyncAdapter.getAndroidID()), String.valueOf(android.os.Process.myPid()));
|
||||
uid = generator.generateUid().getValue().replace("@", "_");
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public ContentType getContentType() {
|
||||
return MIME_ICALENDAR;
|
||||
}
|
||||
|
||||
|
||||
// time zone helpers
|
||||
|
||||
protected static boolean isDateTime(DateProperty date) {
|
||||
return date.getDate() instanceof DateTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that a given DateProperty has a time zone with an ID that is available in Android.
|
||||
* @param date DateProperty to validate. Values which are not DATE-TIME will be ignored.
|
||||
*/
|
||||
protected static void validateTimeZone(DateProperty date) {
|
||||
if (isDateTime(date)) {
|
||||
final TimeZone tz = date.getTimeZone();
|
||||
if (tz == null)
|
||||
return;
|
||||
final String tzID = tz.getID();
|
||||
if (tzID == null)
|
||||
return;
|
||||
|
||||
String deviceTzID = DateUtils.findAndroidTimezoneID(tzID);
|
||||
if (!tzID.equals(deviceTzID))
|
||||
date.setTimeZone(DateUtils.tzRegistry.getTimeZone(deviceTzID));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a string with a timezone definition and returns the time zone ID.
|
||||
* @param timezoneDef time zone definition (VCALENDAR with VTIMEZONE component)
|
||||
* @return time zone id (TZID) if VTIMEZONE contains a TZID,
|
||||
* null otherwise
|
||||
*/
|
||||
public static String TimezoneDefToTzId(@NonNull String timezoneDef) {
|
||||
try {
|
||||
CalendarBuilder builder = new CalendarBuilder();
|
||||
net.fortuna.ical4j.model.Calendar cal = builder.build(new StringReader(timezoneDef));
|
||||
VTimeZone timezone = (VTimeZone)cal.getComponent(VTimeZone.VTIMEZONE);
|
||||
if (timezone != null && timezone.getTimeZoneId() != null)
|
||||
return timezone.getTimeZoneId().getValue();
|
||||
} catch (IOException|ParserException e) {
|
||||
Log.e(TAG, "Can't understand time zone definition", e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
// ical4j helpers and extensions
|
||||
|
||||
private static final ParameterFactoryRegistry parameterFactoryRegistry = new ParameterFactoryRegistry();
|
||||
static {
|
||||
parameterFactoryRegistry.register(Email.PARAMETER_NAME, Email.FACTORY);
|
||||
}
|
||||
protected static final CalendarBuilder calendarBuilder = new CalendarBuilder(
|
||||
CalendarParserFactory.getInstance().createParser(),
|
||||
new PropertyFactoryRegistry(), parameterFactoryRegistry, DateUtils.tzRegistry);
|
||||
|
||||
public static class Email extends Parameter {
|
||||
/* EMAIL property for ATTENDEE properties, as used by iCloud:
|
||||
ATTENDEE;EMAIL=bla@domain.tld;/path/to/principal
|
||||
*/
|
||||
public static final ParameterFactory FACTORY = new Factory();
|
||||
|
||||
public static final String PARAMETER_NAME = "EMAIL";
|
||||
@Getter private String value;
|
||||
|
||||
protected Email() {
|
||||
super(PARAMETER_NAME, ParameterFactoryImpl.getInstance());
|
||||
}
|
||||
|
||||
public Email(String aValue) {
|
||||
super(PARAMETER_NAME, ParameterFactoryImpl.getInstance());
|
||||
value = Strings.unquote(aValue);
|
||||
}
|
||||
|
||||
public static class Factory implements ParameterFactory {
|
||||
@Override
|
||||
public Parameter createParameter(String value) throws URISyntaxException {
|
||||
return new Email(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(String name) {
|
||||
return PARAMETER_NAME.equalsIgnoreCase(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -9,21 +9,13 @@ package at.bitfire.davdroid.syncadapter;
|
||||
|
||||
import android.accounts.Account;
|
||||
import android.app.Service;
|
||||
import android.content.AbstractThreadedSyncAdapter;
|
||||
import android.content.ContentProviderClient;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SyncResult;
|
||||
import android.os.Bundle;
|
||||
import android.os.IBinder;
|
||||
import android.os.RemoteException;
|
||||
import android.util.Log;
|
||||
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import at.bitfire.davdroid.resource.CalDavCalendar;
|
||||
import at.bitfire.davdroid.resource.LocalCalendar;
|
||||
import at.bitfire.davdroid.resource.LocalCollection;
|
||||
import at.bitfire.davdroid.resource.WebDavCollection;
|
||||
|
||||
public class CalendarsSyncAdapterService extends Service {
|
||||
private static SyncAdapter syncAdapter;
|
||||
@ -36,7 +28,6 @@ public class CalendarsSyncAdapterService extends Service {
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
syncAdapter.close();
|
||||
syncAdapter = null;
|
||||
}
|
||||
|
||||
@ -46,35 +37,16 @@ public class CalendarsSyncAdapterService extends Service {
|
||||
}
|
||||
|
||||
|
||||
private static class SyncAdapter extends DavSyncAdapter {
|
||||
private final static String TAG = "davdroid.CalendarsSync";
|
||||
private static class SyncAdapter extends AbstractThreadedSyncAdapter {
|
||||
|
||||
private SyncAdapter(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Map<LocalCollection<?>, WebDavCollection<?>> getSyncPairs(Account account, ContentProviderClient provider) {
|
||||
AccountSettings settings = new AccountSettings(getContext(), account);
|
||||
String userName = settings.getUserName(),
|
||||
password = settings.getPassword();
|
||||
boolean preemptive = settings.getPreemptiveAuth();
|
||||
public SyncAdapter(Context context) {
|
||||
super(context, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPerformSync(Account account, Bundle extras, String authority, ContentProviderClient provider, SyncResult syncResult) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
Map<LocalCollection<?>, WebDavCollection<?>> map = new HashMap<>();
|
||||
|
||||
for (LocalCalendar calendar : LocalCalendar.findAll(account, provider)) {
|
||||
WebDavCollection<?> dav = new CalDavCalendar(httpClient, calendar.getUrl(), userName, password, preemptive);
|
||||
map.put(calendar, dav);
|
||||
}
|
||||
return map;
|
||||
} catch (RemoteException ex) {
|
||||
Log.e(TAG, "Couldn't find local calendars", ex);
|
||||
} catch (URISyntaxException ex) {
|
||||
Log.e(TAG, "Couldn't build calendar URI", ex);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -8,70 +8,20 @@
|
||||
package at.bitfire.davdroid.syncadapter;
|
||||
|
||||
import android.accounts.Account;
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.app.Service;
|
||||
import android.content.AbstractThreadedSyncAdapter;
|
||||
import android.content.ContentProviderClient;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SyncResult;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.IBinder;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import com.squareup.okhttp.HttpUrl;
|
||||
import com.squareup.okhttp.MediaType;
|
||||
import com.squareup.okhttp.Request;
|
||||
import com.squareup.okhttp.RequestBody;
|
||||
import com.squareup.okhttp.Response;
|
||||
import com.squareup.okhttp.ResponseBody;
|
||||
|
||||
import org.apache.commons.io.Charsets;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
import at.bitfire.dav4android.DavAddressBook;
|
||||
import at.bitfire.dav4android.DavResource;
|
||||
import at.bitfire.dav4android.exception.DavException;
|
||||
import at.bitfire.dav4android.exception.HttpException;
|
||||
import at.bitfire.dav4android.exception.PreconditionFailedException;
|
||||
import at.bitfire.dav4android.property.AddressData;
|
||||
import at.bitfire.dav4android.property.GetCTag;
|
||||
import at.bitfire.dav4android.property.GetContentType;
|
||||
import at.bitfire.dav4android.property.GetETag;
|
||||
import at.bitfire.dav4android.property.SupportedAddressData;
|
||||
import at.bitfire.davdroid.ArrayUtils;
|
||||
import at.bitfire.davdroid.Constants;
|
||||
import at.bitfire.davdroid.HttpClient;
|
||||
import at.bitfire.davdroid.R;
|
||||
import at.bitfire.davdroid.resource.LocalAddressBook;
|
||||
import at.bitfire.davdroid.resource.LocalContact;
|
||||
import at.bitfire.davdroid.ui.DebugInfoActivity;
|
||||
import at.bitfire.vcard4android.Contact;
|
||||
import at.bitfire.vcard4android.ContactsStorageException;
|
||||
import ezvcard.VCardVersion;
|
||||
import ezvcard.util.IOUtils;
|
||||
import lombok.Cleanup;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
public class ContactsSyncAdapterService extends Service {
|
||||
private static ContactsSyncAdapter syncAdapter;
|
||||
|
||||
protected static final int MAX_MULTIGET = 10;
|
||||
|
||||
|
||||
@Override
|
||||
@ -92,354 +42,21 @@ public class ContactsSyncAdapterService extends Service {
|
||||
|
||||
|
||||
private static class ContactsSyncAdapter extends AbstractThreadedSyncAdapter {
|
||||
private static final int
|
||||
NOTIFICATION_ERROR = 1,
|
||||
|
||||
SYNC_PHASE_QUERY_CAPABILITIES = 0,
|
||||
SYNC_PHASE_PROCESS_LOCALLY_DELETED = 1,
|
||||
SYNC_PHASE_PREPARE_LOCALLY_CREATED = 2,
|
||||
SYNC_PHASE_UPLOAD_DIRTY = 3,
|
||||
SYNC_PHASE_CHECK_STATE = 4,
|
||||
SYNC_PHASE_LIST_LOCAL = 5,
|
||||
SYNC_PHASE_LIST_REMOTE = 6,
|
||||
SYNC_PHASE_COMPARE_ENTRIES = 7,
|
||||
SYNC_PHASE_DOWNLOAD_REMOTE = 8,
|
||||
SYNC_PHASE_SAVE_STATE = 9;
|
||||
|
||||
public ContactsSyncAdapter(Context context) {
|
||||
super(context, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPerformSync(Account account, Bundle extras, String authority, ContentProviderClient provider, SyncResult syncResult) {
|
||||
Constants.log.info("Starting sync for authority " + authority);
|
||||
Constants.log.info("Starting contacts sync (" + authority + ")");
|
||||
|
||||
AccountSettings settings = new AccountSettings(getContext(), account);
|
||||
HttpClient httpClient = new HttpClient(getContext(), settings.getUserName(), settings.getPassword(), settings.getPreemptiveAuth());
|
||||
|
||||
HttpUrl addressBookURL = HttpUrl.parse(settings.getAddressBookURL());
|
||||
DavAddressBook dav = new DavAddressBook(httpClient, addressBookURL);
|
||||
|
||||
// dismiss previous error notifications
|
||||
NotificationManager notificationManager = (NotificationManager)getContext().getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
notificationManager.cancel(account.name, NOTIFICATION_ERROR);
|
||||
|
||||
// prepare local address book
|
||||
LocalAddressBook addressBook = new LocalAddressBook(account, provider);
|
||||
|
||||
int syncPhase = SYNC_PHASE_QUERY_CAPABILITIES;
|
||||
try {
|
||||
// prepare remote address book
|
||||
boolean hasVCard4 = false;
|
||||
dav.propfind(0, SupportedAddressData.NAME, GetCTag.NAME);
|
||||
SupportedAddressData supportedAddressData = (SupportedAddressData)dav.properties.get(SupportedAddressData.NAME);
|
||||
if (supportedAddressData != null)
|
||||
for (MediaType type : supportedAddressData.types)
|
||||
if ("text/vcard; version=4.0".equalsIgnoreCase(type.toString()))
|
||||
hasVCard4 = true;
|
||||
Constants.log.info("Server advertises VCard/4 support: " + hasVCard4);
|
||||
|
||||
syncPhase = SYNC_PHASE_PROCESS_LOCALLY_DELETED;
|
||||
// Remove locally deleted contacts from server (if they have a name, i.e. if they were uploaded before),
|
||||
// but only if they don't have changed on the server. Then finally remove them from the local address book.
|
||||
LocalContact[] localList = addressBook.getDeleted();
|
||||
for (LocalContact local : localList) {
|
||||
final String fileName = local.getFileName();
|
||||
if (!TextUtils.isEmpty(fileName)) {
|
||||
Constants.log.info(fileName + " has been deleted locally -> deleting from server");
|
||||
try {
|
||||
new DavResource(httpClient, addressBookURL.newBuilder().addPathSegment(fileName).build())
|
||||
.delete(local.eTag);
|
||||
} catch(IOException|HttpException e) {
|
||||
Constants.log.warn("Couldn't delete " + fileName + " from server");
|
||||
}
|
||||
} else
|
||||
Constants.log.info("Removing local contact #" + local.getId() + " which has been deleted locally and was never uploaded");
|
||||
local.delete();
|
||||
syncResult.stats.numDeletes++;
|
||||
}
|
||||
|
||||
syncPhase = SYNC_PHASE_PREPARE_LOCALLY_CREATED;
|
||||
// assign file names and UIDs to new contacts so that we can use the file name as an index
|
||||
localList = addressBook.getWithoutFileName();
|
||||
for (LocalContact local : localList) {
|
||||
String uuid = UUID.randomUUID().toString();
|
||||
Constants.log.info("Found local contact #" + local.getId() + " without file name; assigning name UID/name " + uuid + "[.vcf]");
|
||||
local.updateFileNameAndUID(uuid);
|
||||
}
|
||||
|
||||
syncPhase = SYNC_PHASE_UPLOAD_DIRTY;
|
||||
// upload dirty contacts
|
||||
localList = addressBook.getDirty();
|
||||
for (LocalContact local : localList) {
|
||||
final String fileName = local.getFileName();
|
||||
|
||||
DavResource remote = new DavResource(httpClient, addressBookURL.newBuilder().addPathSegment(fileName).build());
|
||||
|
||||
RequestBody vCard = RequestBody.create(
|
||||
hasVCard4 ? DavAddressBook.MIME_VCARD4 : DavAddressBook.MIME_VCARD3_UTF8,
|
||||
local.getContact().toStream(hasVCard4 ? VCardVersion.V4_0 : VCardVersion.V3_0).toByteArray()
|
||||
);
|
||||
|
||||
try {
|
||||
if (local.eTag == null) {
|
||||
Constants.log.info("Uploading new contact " + fileName);
|
||||
remote.put(vCard, null, true);
|
||||
// TODO handle 30x
|
||||
} else {
|
||||
Constants.log.info("Uploading locally modified contact " + fileName);
|
||||
remote.put(vCard, local.eTag, false);
|
||||
// TODO handle 30x
|
||||
}
|
||||
|
||||
} catch(PreconditionFailedException e) {
|
||||
Constants.log.info("Contact has been modified on the server before upload, ignoring", e);
|
||||
}
|
||||
|
||||
String eTag = null;
|
||||
GetETag newETag = (GetETag)remote.properties.get(GetETag.NAME);
|
||||
if (newETag != null) {
|
||||
eTag = newETag.eTag;
|
||||
Constants.log.debug("Received new ETag=" + eTag + " after uploading");
|
||||
} else
|
||||
Constants.log.debug("Didn't receive new ETag after uploading, setting to null");
|
||||
|
||||
local.clearDirty(eTag);
|
||||
}
|
||||
|
||||
syncPhase = SYNC_PHASE_CHECK_STATE;
|
||||
// check CTag (ignore on manual sync)
|
||||
String currentCTag = null;
|
||||
GetCTag getCTag = (GetCTag) dav.properties.get(GetCTag.NAME);
|
||||
if (getCTag != null)
|
||||
currentCTag = getCTag.cTag;
|
||||
|
||||
String localCTag = null;
|
||||
if (extras.containsKey(ContentResolver.SYNC_EXTRAS_MANUAL))
|
||||
Constants.log.info("Manual sync, ignoring CTag");
|
||||
else
|
||||
localCTag = addressBook.getCTag();
|
||||
|
||||
if (currentCTag != null && currentCTag.equals(localCTag)) {
|
||||
Constants.log.info("Remote address book didn't change (CTag=" + currentCTag + "), no need to list VCards");
|
||||
|
||||
} else /* remote CTag has changed */ {
|
||||
syncPhase = SYNC_PHASE_LIST_LOCAL;
|
||||
// fetch list of local contacts and build hash table to index file name
|
||||
localList = addressBook.getAll();
|
||||
Map<String, LocalContact> localContacts = new HashMap<>(localList.length);
|
||||
for (LocalContact contact : localList) {
|
||||
Constants.log.debug("Found local contact: " + contact.getFileName());
|
||||
localContacts.put(contact.getFileName(), contact);
|
||||
}
|
||||
|
||||
syncPhase = SYNC_PHASE_LIST_REMOTE;
|
||||
// fetch list of remote VCards and build hash table to index file name
|
||||
Constants.log.info("Listing remote VCards");
|
||||
dav.queryMemberETags();
|
||||
Map<String, DavResource> remoteContacts = new HashMap<>(dav.members.size());
|
||||
for (DavResource vCard : dav.members) {
|
||||
String fileName = vCard.fileName();
|
||||
Constants.log.debug("Found remote VCard: " + fileName);
|
||||
remoteContacts.put(fileName, vCard);
|
||||
}
|
||||
|
||||
syncPhase = SYNC_PHASE_COMPARE_ENTRIES;
|
||||
/* check which contacts
|
||||
1. are not present anymore remotely -> delete immediately on local side
|
||||
2. updated remotely -> add to downloadNames
|
||||
3. added remotely -> add to downloadNames
|
||||
*/
|
||||
Set<DavResource> toDownload = new HashSet<>();
|
||||
for (String localName : localContacts.keySet()) {
|
||||
DavResource remote = remoteContacts.get(localName);
|
||||
if (remote == null) {
|
||||
Constants.log.info(localName + " is not on server anymore, deleting");
|
||||
localContacts.get(localName).delete();
|
||||
syncResult.stats.numDeletes++;
|
||||
} else {
|
||||
// contact is still on server, check whether it has been updated remotely
|
||||
GetETag getETag = (GetETag)remote.properties.get(GetETag.NAME);
|
||||
if (getETag == null || getETag.eTag == null)
|
||||
throw new DavException("Server didn't provide ETag");
|
||||
String localETag = localContacts.get(localName).eTag,
|
||||
remoteETag = getETag.eTag;
|
||||
if (remoteETag.equals(localETag))
|
||||
syncResult.stats.numSkippedEntries++;
|
||||
else {
|
||||
Constants.log.info(localName + " has been changed on server (current ETag=" + remoteETag + ", last known ETag=" + localETag + ")");
|
||||
toDownload.add(remote);
|
||||
}
|
||||
|
||||
// remote entry has been seen, remove from list
|
||||
remoteContacts.remove(localName);
|
||||
}
|
||||
}
|
||||
|
||||
// add all unseen (= remotely added) remote contacts
|
||||
if (!remoteContacts.isEmpty()) {
|
||||
Constants.log.info("New VCards have been found on the server: " + TextUtils.join(", ", remoteContacts.keySet()));
|
||||
toDownload.addAll(remoteContacts.values());
|
||||
}
|
||||
|
||||
syncPhase = SYNC_PHASE_DOWNLOAD_REMOTE;
|
||||
Constants.log.info("Downloading " + toDownload.size() + " contacts (" + MAX_MULTIGET + " at once)");
|
||||
|
||||
// prepare downloader which may be used to download external resource like contact photos
|
||||
Contact.Downloader downloader = new ResourceDownloader(httpClient, addressBookURL);
|
||||
|
||||
// download new/updated VCards from server
|
||||
for (DavResource[] bunch : ArrayUtils.partition(toDownload.toArray(new DavResource[toDownload.size()]), MAX_MULTIGET)) {
|
||||
Constants.log.info("Downloading " + TextUtils.join(" + ", bunch));
|
||||
|
||||
if (bunch.length == 1) {
|
||||
// only one contact, use GET
|
||||
DavResource remote = bunch[0];
|
||||
String fileName = remote.fileName();
|
||||
|
||||
ResponseBody body = remote.get("text/vcard;q=0.5, text/vcard;charset=utf-8;q=0.8, text/vcard;version=4.0");
|
||||
String eTag = ((GetETag)remote.properties.get(GetETag.NAME)).eTag;
|
||||
|
||||
@Cleanup InputStream stream = body.byteStream();
|
||||
processVCard(syncResult, addressBook, localContacts, remote.fileName(), eTag, stream, body.contentType().charset(Charsets.UTF_8), downloader);
|
||||
|
||||
} else {
|
||||
// multiple contacts, use multi-get
|
||||
List<HttpUrl> urls = new LinkedList<>();
|
||||
for (DavResource remote : bunch)
|
||||
urls.add(remote.location);
|
||||
dav.multiget(urls.toArray(new HttpUrl[urls.size()]), hasVCard4);
|
||||
|
||||
// process multiget results
|
||||
for (DavResource remote : dav.members) {
|
||||
String eTag = null;
|
||||
GetETag getETag = (GetETag)remote.properties.get(GetETag.NAME);
|
||||
if (getETag != null)
|
||||
eTag = getETag.eTag;
|
||||
else
|
||||
throw new DavException("Received multi-get response without ETag");
|
||||
|
||||
Charset charset = Charsets.UTF_8;
|
||||
GetContentType getContentType = (GetContentType)remote.properties.get(GetContentType.NAME);
|
||||
if (getContentType != null && getContentType.type != null) {
|
||||
MediaType type = MediaType.parse(getContentType.type);
|
||||
if (type != null)
|
||||
charset = type.charset(Charsets.UTF_8);
|
||||
}
|
||||
|
||||
AddressData addressData = (AddressData)remote.properties.get(AddressData.NAME);
|
||||
if (addressData == null || addressData.vCard == null)
|
||||
throw new DavException("Received multi-get response without address data");
|
||||
|
||||
@Cleanup InputStream stream = new ByteArrayInputStream(addressData.vCard.getBytes());
|
||||
processVCard(syncResult, addressBook, localContacts, remote.fileName(), eTag, stream, charset, downloader);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
syncPhase = SYNC_PHASE_SAVE_STATE;
|
||||
/* Save sync state (CTag). It doesn't matter if it has changed during the sync process
|
||||
(for instance, because another client has uploaded changes), because this will simply
|
||||
cause all remote entries to be listed at the next sync. */
|
||||
Constants.log.info("Saving sync state: CTag=" + currentCTag);
|
||||
addressBook.setCTag(currentCTag);
|
||||
}
|
||||
|
||||
} catch (IOException e) {
|
||||
Constants.log.error("I/O exception during sync, trying again later", e);
|
||||
syncResult.stats.numIoExceptions++;
|
||||
|
||||
} catch(HttpException e) {
|
||||
Constants.log.error("HTTP Exception during sync", e);
|
||||
syncResult.stats.numParseExceptions++;
|
||||
|
||||
Intent detailsIntent = new Intent(getContext(), DebugInfoActivity.class);
|
||||
detailsIntent.putExtra(DebugInfoActivity.KEY_EXCEPTION, e);
|
||||
detailsIntent.putExtra(DebugInfoActivity.KEY_ACCOUNT, account);
|
||||
detailsIntent.putExtra(DebugInfoActivity.KEY_PHASE, syncPhase);
|
||||
|
||||
Notification.Builder builder = new Notification.Builder(getContext());
|
||||
Notification notification = null;
|
||||
builder .setSmallIcon(R.drawable.ic_launcher)
|
||||
.setContentTitle(getContext().getString(R.string.sync_error_title, account.name))
|
||||
.setContentIntent(PendingIntent.getActivity(getContext(), 0, detailsIntent, PendingIntent.FLAG_UPDATE_CURRENT));
|
||||
|
||||
String[] phases = getContext().getResources().getStringArray(R.array.sync_error_phases);
|
||||
if (phases.length > syncPhase)
|
||||
builder.setContentText(getContext().getString(R.string.sync_error_http, phases[syncPhase]));
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 16) {
|
||||
if (Build.VERSION.SDK_INT >= 21)
|
||||
builder.setCategory(Notification.CATEGORY_ERROR);
|
||||
notification = builder.build();
|
||||
} else {
|
||||
notification = builder.getNotification();
|
||||
}
|
||||
notificationManager.notify(account.name, NOTIFICATION_ERROR, notification);
|
||||
|
||||
} catch(DavException e) {
|
||||
;
|
||||
} catch(ContactsStorageException e) {
|
||||
syncResult.databaseError = true;
|
||||
}
|
||||
ContactsSyncManager syncManager = new ContactsSyncManager(getContext(), account, extras, provider, syncResult);
|
||||
syncManager.performSync();
|
||||
|
||||
Constants.log.info("Sync complete for authority " + authority);
|
||||
}
|
||||
|
||||
|
||||
private void processVCard(SyncResult syncResult, LocalAddressBook addressBook, Map<String, LocalContact>localContacts, String fileName, String eTag, InputStream stream, Charset charset, Contact.Downloader downloader) throws IOException, ContactsStorageException {
|
||||
Contact contacts[] = Contact.fromStream(stream, charset, downloader);
|
||||
if (contacts.length == 1) {
|
||||
Contact newData = contacts[0];
|
||||
|
||||
// delete local contact, if it exists
|
||||
LocalContact localContact = localContacts.get(fileName);
|
||||
if (localContact != null) {
|
||||
Constants.log.info("Updating " + fileName + " in local address book");
|
||||
localContact.eTag = eTag;
|
||||
localContact.update(newData);
|
||||
syncResult.stats.numUpdates++;
|
||||
} else {
|
||||
Constants.log.info("Adding " + fileName + " to local address book");
|
||||
localContact = new LocalContact(addressBook, newData, fileName, eTag);
|
||||
localContact.add();
|
||||
syncResult.stats.numInserts++;
|
||||
}
|
||||
} else
|
||||
Constants.log.error("Received VCard with not exactly one VCARD, ignoring " + fileName);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@RequiredArgsConstructor
|
||||
static class ResourceDownloader implements Contact.Downloader {
|
||||
final HttpClient httpClient;
|
||||
final HttpUrl baseUrl;
|
||||
|
||||
@Override
|
||||
public byte[] download(String url, String accepts) {
|
||||
HttpUrl httpUrl = HttpUrl.parse(url);
|
||||
HttpClient resourceClient = new HttpClient(httpClient, httpUrl.host());
|
||||
try {
|
||||
Response response = resourceClient.newCall(new Request.Builder()
|
||||
.get()
|
||||
.url(httpUrl)
|
||||
.build()).execute();
|
||||
|
||||
ResponseBody body = response.body();
|
||||
if (body != null) {
|
||||
@Cleanup InputStream stream = body.byteStream();
|
||||
if (response.isSuccessful() && stream != null) {
|
||||
return IOUtils.toByteArray(stream);
|
||||
} else
|
||||
Constants.log.error("Couldn't download external resource");
|
||||
}
|
||||
} catch(IOException e) {
|
||||
Constants.log.error("Couldn't download external resource", e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,380 @@
|
||||
/*
|
||||
* Copyright © 2013 – 2015 Ricki Hirner (bitfire web engineering).
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the GNU Public License v3.0
|
||||
* which accompanies this distribution, and is available at
|
||||
* http://www.gnu.org/licenses/gpl.html
|
||||
*/
|
||||
|
||||
package at.bitfire.davdroid.syncadapter;
|
||||
|
||||
import android.accounts.Account;
|
||||
import android.content.ContentProviderClient;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.content.SyncResult;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import com.google.common.base.Charsets;
|
||||
import com.squareup.okhttp.HttpUrl;
|
||||
import com.squareup.okhttp.MediaType;
|
||||
import com.squareup.okhttp.Request;
|
||||
import com.squareup.okhttp.RequestBody;
|
||||
import com.squareup.okhttp.Response;
|
||||
import com.squareup.okhttp.ResponseBody;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
import at.bitfire.dav4android.DavAddressBook;
|
||||
import at.bitfire.dav4android.DavResource;
|
||||
import at.bitfire.dav4android.exception.DavException;
|
||||
import at.bitfire.dav4android.exception.HttpException;
|
||||
import at.bitfire.dav4android.exception.PreconditionFailedException;
|
||||
import at.bitfire.dav4android.property.AddressData;
|
||||
import at.bitfire.dav4android.property.GetCTag;
|
||||
import at.bitfire.dav4android.property.GetContentType;
|
||||
import at.bitfire.dav4android.property.GetETag;
|
||||
import at.bitfire.dav4android.property.SupportedAddressData;
|
||||
import at.bitfire.davdroid.ArrayUtils;
|
||||
import at.bitfire.davdroid.Constants;
|
||||
import at.bitfire.davdroid.HttpClient;
|
||||
import at.bitfire.davdroid.resource.LocalAddressBook;
|
||||
import at.bitfire.davdroid.resource.LocalContact;
|
||||
import at.bitfire.vcard4android.Contact;
|
||||
import at.bitfire.vcard4android.ContactsStorageException;
|
||||
import ezvcard.VCardVersion;
|
||||
import ezvcard.util.IOUtils;
|
||||
import lombok.Cleanup;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
public class ContactsSyncManager extends SyncManager {
|
||||
protected static final int
|
||||
MAX_MULTIGET = 10,
|
||||
NOTIFICATION_ID = 1;
|
||||
|
||||
protected HttpUrl addressBookURL;
|
||||
protected DavAddressBook davCollection;
|
||||
protected boolean hasVCard4;
|
||||
|
||||
protected LocalAddressBook addressBook;
|
||||
String currentCTag;
|
||||
|
||||
Map<String, LocalContact> localContacts;
|
||||
Map<String, DavResource> remoteContacts;
|
||||
Set<DavResource> toDownload;
|
||||
|
||||
|
||||
public ContactsSyncManager(Context context, Account account, Bundle extras, ContentProviderClient provider, SyncResult result) {
|
||||
super(NOTIFICATION_ID, context, account, extras, provider, result);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected void prepare() {
|
||||
addressBookURL = HttpUrl.parse(settings.getAddressBookURL());
|
||||
davCollection = new DavAddressBook(httpClient, addressBookURL);
|
||||
|
||||
// prepare local address book
|
||||
addressBook = new LocalAddressBook(account, provider);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void queryCapabilities() throws DavException, IOException, HttpException {
|
||||
// prepare remote address book
|
||||
hasVCard4 = false;
|
||||
davCollection.propfind(0, SupportedAddressData.NAME, GetCTag.NAME);
|
||||
SupportedAddressData supportedAddressData = (SupportedAddressData) davCollection.properties.get(SupportedAddressData.NAME);
|
||||
if (supportedAddressData != null)
|
||||
for (MediaType type : supportedAddressData.types)
|
||||
if ("text/vcard; version=4.0".equalsIgnoreCase(type.toString()))
|
||||
hasVCard4 = true;
|
||||
Constants.log.info("Server advertises VCard/4 support: " + hasVCard4);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void processLocallyDeleted() throws ContactsStorageException {
|
||||
// Remove locally deleted contacts from server (if they have a name, i.e. if they were uploaded before),
|
||||
// but only if they don't have changed on the server. Then finally remove them from the local address book.
|
||||
LocalContact[] localList = addressBook.getDeleted();
|
||||
for (LocalContact local : localList) {
|
||||
final String fileName = local.getFileName();
|
||||
if (!TextUtils.isEmpty(fileName)) {
|
||||
Constants.log.info(fileName + " has been deleted locally -> deleting from server");
|
||||
try {
|
||||
new DavResource(httpClient, addressBookURL.newBuilder().addPathSegment(fileName).build())
|
||||
.delete(local.eTag);
|
||||
} catch (IOException | HttpException e) {
|
||||
Constants.log.warn("Couldn't delete " + fileName + " from server");
|
||||
}
|
||||
} else
|
||||
Constants.log.info("Removing local contact #" + local.getId() + " which has been deleted locally and was never uploaded");
|
||||
local.delete();
|
||||
syncResult.stats.numDeletes++;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void processLocallyCreated() throws ContactsStorageException {
|
||||
// assign file names and UIDs to new contacts so that we can use the file name as an index
|
||||
for (LocalContact local : addressBook.getWithoutFileName()) {
|
||||
String uuid = UUID.randomUUID().toString();
|
||||
Constants.log.info("Found local contact #" + local.getId() + " without file name; assigning name UID/name " + uuid + "[.vcf]");
|
||||
local.updateFileNameAndUID(uuid);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void uploadDirty() throws ContactsStorageException, IOException, HttpException {
|
||||
// upload dirty contacts
|
||||
for (LocalContact local : addressBook.getDirty()) {
|
||||
final String fileName = local.getFileName();
|
||||
|
||||
DavResource remote = new DavResource(httpClient, addressBookURL.newBuilder().addPathSegment(fileName).build());
|
||||
|
||||
RequestBody vCard = RequestBody.create(
|
||||
hasVCard4 ? DavAddressBook.MIME_VCARD4 : DavAddressBook.MIME_VCARD3_UTF8,
|
||||
local.getContact().toStream(hasVCard4 ? VCardVersion.V4_0 : VCardVersion.V3_0).toByteArray()
|
||||
);
|
||||
|
||||
try {
|
||||
if (local.eTag == null) {
|
||||
Constants.log.info("Uploading new contact " + fileName);
|
||||
remote.put(vCard, null, true);
|
||||
// TODO handle 30x
|
||||
} else {
|
||||
Constants.log.info("Uploading locally modified contact " + fileName);
|
||||
remote.put(vCard, local.eTag, false);
|
||||
// TODO handle 30x
|
||||
}
|
||||
|
||||
} catch (PreconditionFailedException e) {
|
||||
Constants.log.info("Contact has been modified on the server before upload, ignoring", e);
|
||||
}
|
||||
|
||||
String eTag = null;
|
||||
GetETag newETag = (GetETag) remote.properties.get(GetETag.NAME);
|
||||
if (newETag != null) {
|
||||
eTag = newETag.eTag;
|
||||
Constants.log.debug("Received new ETag=" + eTag + " after uploading");
|
||||
} else
|
||||
Constants.log.debug("Didn't receive new ETag after uploading, setting to null");
|
||||
|
||||
local.clearDirty(eTag);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean checkSyncState() throws ContactsStorageException {
|
||||
// check CTag (ignore on manual sync)
|
||||
currentCTag = null;
|
||||
GetCTag getCTag = (GetCTag) davCollection.properties.get(GetCTag.NAME);
|
||||
if (getCTag != null)
|
||||
currentCTag = getCTag.cTag;
|
||||
|
||||
String localCTag = null;
|
||||
if (extras.containsKey(ContentResolver.SYNC_EXTRAS_MANUAL))
|
||||
Constants.log.info("Manual sync, ignoring CTag");
|
||||
else
|
||||
localCTag = addressBook.getCTag();
|
||||
|
||||
if (currentCTag != null && currentCTag.equals(localCTag)) {
|
||||
Constants.log.info("Remote address book didn't change (CTag=" + currentCTag + "), no need to list VCards");
|
||||
return false;
|
||||
} else
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void listLocal() throws ContactsStorageException {
|
||||
// fetch list of local contacts and build hash table to index file name
|
||||
LocalContact[] localList = addressBook.getAll();
|
||||
localContacts = new HashMap<>(localList.length);
|
||||
for (LocalContact contact : localList) {
|
||||
Constants.log.debug("Found local contact: " + contact.getFileName());
|
||||
localContacts.put(contact.getFileName(), contact);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void listRemote() throws IOException, HttpException, DavException, ContactsStorageException {
|
||||
// fetch list of remote VCards and build hash table to index file name
|
||||
Constants.log.info("Listing remote VCards");
|
||||
davCollection.queryMemberETags();
|
||||
remoteContacts = new HashMap<>(davCollection.members.size());
|
||||
for (DavResource vCard : davCollection.members) {
|
||||
String fileName = vCard.fileName();
|
||||
Constants.log.debug("Found remote VCard: " + fileName);
|
||||
remoteContacts.put(fileName, vCard);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void compareEntries() throws IOException, HttpException, DavException, ContactsStorageException {
|
||||
/* check which contacts
|
||||
1. are not present anymore remotely -> delete immediately on local side
|
||||
2. updated remotely -> add to downloadNames
|
||||
3. added remotely -> add to downloadNames
|
||||
*/
|
||||
toDownload = new HashSet<>();
|
||||
for (String localName : localContacts.keySet()) {
|
||||
DavResource remote = remoteContacts.get(localName);
|
||||
if (remote == null) {
|
||||
Constants.log.info(localName + " is not on server anymore, deleting");
|
||||
localContacts.get(localName).delete();
|
||||
syncResult.stats.numDeletes++;
|
||||
} else {
|
||||
// contact is still on server, check whether it has been updated remotely
|
||||
GetETag getETag = (GetETag) remote.properties.get(GetETag.NAME);
|
||||
if (getETag == null || getETag.eTag == null)
|
||||
throw new DavException("Server didn't provide ETag");
|
||||
String localETag = localContacts.get(localName).eTag,
|
||||
remoteETag = getETag.eTag;
|
||||
if (remoteETag.equals(localETag))
|
||||
syncResult.stats.numSkippedEntries++;
|
||||
else {
|
||||
Constants.log.info(localName + " has been changed on server (current ETag=" + remoteETag + ", last known ETag=" + localETag + ")");
|
||||
toDownload.add(remote);
|
||||
}
|
||||
|
||||
// remote entry has been seen, remove from list
|
||||
remoteContacts.remove(localName);
|
||||
}
|
||||
}
|
||||
|
||||
// add all unseen (= remotely added) remote contacts
|
||||
if (!remoteContacts.isEmpty()) {
|
||||
Constants.log.info("New VCards have been found on the server: " + TextUtils.join(", ", remoteContacts.keySet()));
|
||||
toDownload.addAll(remoteContacts.values());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void downloadRemote() throws IOException, HttpException, DavException, ContactsStorageException {
|
||||
Constants.log.info("Downloading " + toDownload.size() + " contacts (" + MAX_MULTIGET + " at once)");
|
||||
|
||||
// prepare downloader which may be used to download external resource like contact photos
|
||||
Contact.Downloader downloader = new ResourceDownloader(httpClient, addressBookURL);
|
||||
|
||||
// download new/updated VCards from server
|
||||
for (DavResource[] bunch : ArrayUtils.partition(toDownload.toArray(new DavResource[toDownload.size()]), MAX_MULTIGET)) {
|
||||
Constants.log.info("Downloading " + TextUtils.join(" + ", bunch));
|
||||
|
||||
if (bunch.length == 1) {
|
||||
// only one contact, use GET
|
||||
DavResource remote = bunch[0];
|
||||
|
||||
ResponseBody body = remote.get("text/vcard;q=0.5, text/vcard;charset=utf-8;q=0.8, text/vcard;version=4.0");
|
||||
String eTag = ((GetETag) remote.properties.get(GetETag.NAME)).eTag;
|
||||
|
||||
@Cleanup InputStream stream = body.byteStream();
|
||||
processVCard(syncResult, addressBook, localContacts, remote.fileName(), eTag, stream, body.contentType().charset(Charsets.UTF_8), downloader);
|
||||
|
||||
} else {
|
||||
// multiple contacts, use multi-get
|
||||
List<HttpUrl> urls = new LinkedList<>();
|
||||
for (DavResource remote : bunch)
|
||||
urls.add(remote.location);
|
||||
davCollection.multiget(urls.toArray(new HttpUrl[urls.size()]), hasVCard4);
|
||||
|
||||
// process multiget results
|
||||
for (DavResource remote : davCollection.members) {
|
||||
String eTag;
|
||||
GetETag getETag = (GetETag) remote.properties.get(GetETag.NAME);
|
||||
if (getETag != null)
|
||||
eTag = getETag.eTag;
|
||||
else
|
||||
throw new DavException("Received multi-get response without ETag");
|
||||
|
||||
Charset charset = Charsets.UTF_8;
|
||||
GetContentType getContentType = (GetContentType) remote.properties.get(GetContentType.NAME);
|
||||
if (getContentType != null && getContentType.type != null) {
|
||||
MediaType type = MediaType.parse(getContentType.type);
|
||||
if (type != null)
|
||||
charset = type.charset(Charsets.UTF_8);
|
||||
}
|
||||
|
||||
AddressData addressData = (AddressData) remote.properties.get(AddressData.NAME);
|
||||
if (addressData == null || addressData.vCard == null)
|
||||
throw new DavException("Received multi-get response without address data");
|
||||
|
||||
@Cleanup InputStream stream = new ByteArrayInputStream(addressData.vCard.getBytes());
|
||||
processVCard(syncResult, addressBook, localContacts, remote.fileName(), eTag, stream, charset, downloader);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void saveSyncState() throws ContactsStorageException {
|
||||
/* Save sync state (CTag). It doesn't matter if it has changed during the sync process
|
||||
(for instance, because another client has uploaded changes), because this will simply
|
||||
cause all remote entries to be listed at the next sync. */
|
||||
Constants.log.info("Saving sync state: CTag=" + currentCTag);
|
||||
addressBook.setCTag(currentCTag);
|
||||
}
|
||||
|
||||
private void processVCard(SyncResult syncResult, LocalAddressBook addressBook, Map<String, LocalContact>localContacts, String fileName, String eTag, InputStream stream, Charset charset, Contact.Downloader downloader) throws IOException, ContactsStorageException {
|
||||
Contact contacts[] = Contact.fromStream(stream, charset, downloader);
|
||||
if (contacts.length == 1) {
|
||||
Contact newData = contacts[0];
|
||||
|
||||
// delete local contact, if it exists
|
||||
LocalContact localContact = localContacts.get(fileName);
|
||||
if (localContact != null) {
|
||||
Constants.log.info("Updating " + fileName + " in local address book");
|
||||
localContact.eTag = eTag;
|
||||
localContact.update(newData);
|
||||
syncResult.stats.numUpdates++;
|
||||
} else {
|
||||
Constants.log.info("Adding " + fileName + " to local address book");
|
||||
localContact = new LocalContact(addressBook, newData, fileName, eTag);
|
||||
localContact.add();
|
||||
syncResult.stats.numInserts++;
|
||||
}
|
||||
} else
|
||||
Constants.log.error("Received VCard with not exactly one VCARD, ignoring " + fileName);
|
||||
}
|
||||
|
||||
|
||||
@RequiredArgsConstructor
|
||||
static class ResourceDownloader implements Contact.Downloader {
|
||||
final HttpClient httpClient;
|
||||
final HttpUrl baseUrl;
|
||||
|
||||
@Override
|
||||
public byte[] download(String url, String accepts) {
|
||||
HttpUrl httpUrl = HttpUrl.parse(url);
|
||||
HttpClient resourceClient = new HttpClient(httpClient, httpUrl.host());
|
||||
try {
|
||||
Response response = resourceClient.newCall(new Request.Builder()
|
||||
.get()
|
||||
.url(httpUrl)
|
||||
.build()).execute();
|
||||
|
||||
ResponseBody body = response.body();
|
||||
if (body != null) {
|
||||
@Cleanup InputStream stream = body.byteStream();
|
||||
if (response.isSuccessful() && stream != null) {
|
||||
return IOUtils.toByteArray(stream);
|
||||
} else
|
||||
Constants.log.error("Couldn't download external resource");
|
||||
}
|
||||
} catch(IOException e) {
|
||||
Constants.log.error("Couldn't download external resource", e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,204 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2013 – 2015 Ricki Hirner (bitfire web engineering).
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the GNU Public License v3.0
|
||||
* which accompanies this distribution, and is available at
|
||||
* http://www.gnu.org/licenses/gpl.html
|
||||
*/
|
||||
package at.bitfire.davdroid.syncadapter;
|
||||
|
||||
import android.accounts.Account;
|
||||
import android.annotation.TargetApi;
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.AbstractThreadedSyncAdapter;
|
||||
import android.content.ContentProviderClient;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SyncResult;
|
||||
import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.provider.Settings;
|
||||
import android.util.Log;
|
||||
|
||||
import org.apache.commons.lang3.exception.ExceptionUtils;
|
||||
import org.apache.http.HttpStatus;
|
||||
import org.apache.http.impl.client.CloseableHttpClient;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.locks.ReentrantReadWriteLock;
|
||||
|
||||
import javax.net.ssl.SSLException;
|
||||
|
||||
import at.bitfire.davdroid.Constants;
|
||||
import at.bitfire.davdroid.R;
|
||||
import at.bitfire.davdroid.resource.LocalCollection;
|
||||
import at.bitfire.davdroid.resource.LocalStorageException;
|
||||
import at.bitfire.davdroid.resource.WebDavCollection;
|
||||
import at.bitfire.davdroid.ui.settings.AccountActivity;
|
||||
import at.bitfire.davdroid.webdav.DavException;
|
||||
import at.bitfire.davdroid.webdav.DavHttpClient;
|
||||
import at.bitfire.davdroid.webdav.HttpException;
|
||||
import lombok.Getter;
|
||||
|
||||
public abstract class DavSyncAdapter extends AbstractThreadedSyncAdapter implements Closeable {
|
||||
private final static String TAG = "davdroid.DavSyncAdapter";
|
||||
|
||||
@Getter private static String androidID;
|
||||
|
||||
final protected Context context;
|
||||
|
||||
/* 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);
|
||||
}
|
||||
|
||||
this.context = 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<?>, WebDavCollection<?>> getSyncPairs(Account account, ContentProviderClient provider);
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
|
||||
@Override
|
||||
public void onPerformSync(Account account, Bundle extras, String authority, ContentProviderClient provider, SyncResult syncResult) {
|
||||
Log.i(TAG, "Performing sync for authority " + authority);
|
||||
|
||||
/* Set class loader for iCal4j ResourceLoader – this is required because the various
|
||||
* sync adapters (contacts, events, tasks) share the same :sync process (see AndroidManifest */
|
||||
Thread.currentThread().setContextClassLoader(getContext().getClassLoader());
|
||||
|
||||
// create httpClient, if necessary
|
||||
httpClientLock.writeLock().lock();
|
||||
if (httpClient == null) {
|
||||
Log.d(TAG, "Creating new DavHttpClient");
|
||||
httpClient = DavHttpClient.create();
|
||||
}
|
||||
|
||||
// prevent httpClient shutdown until we're ready by holding a read lock
|
||||
// acquiring read lock before releasing write lock will downgrade the write lock to a read lock
|
||||
httpClientLock.readLock().lock();
|
||||
httpClientLock.writeLock().unlock();
|
||||
|
||||
Exception exceptionToShow = null; // exception to show notification for
|
||||
Intent exceptionIntent = null; // what shall happen when clicking on the exception notification
|
||||
try {
|
||||
// get local <-> remote collection pairs
|
||||
Map<LocalCollection<?>, WebDavCollection<?>> syncCollections = getSyncPairs(account, provider);
|
||||
if (syncCollections == null)
|
||||
Log.i(TAG, "Nothing to synchronize");
|
||||
else
|
||||
try {
|
||||
for (Map.Entry<LocalCollection<?>, WebDavCollection<?>> entry : syncCollections.entrySet())
|
||||
new SyncManager(entry.getKey(), entry.getValue()).synchronize(extras.containsKey(ContentResolver.SYNC_EXTRAS_MANUAL), syncResult);
|
||||
|
||||
} catch (DavException ex) {
|
||||
syncResult.stats.numParseExceptions++;
|
||||
Log.e(TAG, "Invalid DAV response", ex);
|
||||
exceptionToShow = ex;
|
||||
|
||||
} catch (HttpException ex) {
|
||||
if (ex.getCode() == HttpStatus.SC_UNAUTHORIZED) {
|
||||
Log.e(TAG, "HTTP Unauthorized " + ex.getCode(), ex);
|
||||
syncResult.stats.numAuthExceptions++; // hard error
|
||||
|
||||
exceptionToShow = ex;
|
||||
exceptionIntent = new Intent(context, AccountActivity.class);
|
||||
exceptionIntent.putExtra(AccountActivity.EXTRA_ACCOUNT, account);
|
||||
} else if (ex.isClientError()) {
|
||||
Log.e(TAG, "Hard HTTP error " + ex.getCode(), ex);
|
||||
syncResult.stats.numParseExceptions++; // hard error
|
||||
exceptionToShow = ex;
|
||||
} else {
|
||||
Log.w(TAG, "Soft HTTP error " + ex.getCode() + " (Android will try again later)", ex);
|
||||
syncResult.stats.numIoExceptions++; // soft error
|
||||
}
|
||||
} catch (LocalStorageException ex) {
|
||||
syncResult.databaseError = true; // hard error
|
||||
Log.e(TAG, "Local storage (content provider) exception", ex);
|
||||
exceptionToShow = ex;
|
||||
} catch (IOException ex) {
|
||||
syncResult.stats.numIoExceptions++; // soft error
|
||||
Log.e(TAG, "I/O error (Android will try again later)", ex);
|
||||
if (ex instanceof SSLException) // always notify on SSL/TLS errors
|
||||
exceptionToShow = ex;
|
||||
} catch (URISyntaxException ex) {
|
||||
syncResult.stats.numParseExceptions++; // hard error
|
||||
Log.e(TAG, "Invalid URI (file name) syntax", ex);
|
||||
exceptionToShow = ex;
|
||||
}
|
||||
} finally {
|
||||
// allow httpClient shutdown
|
||||
httpClientLock.readLock().unlock();
|
||||
}
|
||||
|
||||
// show sync errors as notification
|
||||
if (exceptionToShow != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
|
||||
if (exceptionIntent == null)
|
||||
exceptionIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(Constants.WEB_URL_VIEW_LOGS));
|
||||
|
||||
PendingIntent contentIntent = PendingIntent.getActivity(context, 0, exceptionIntent, 0);
|
||||
Notification.Builder builder = new Notification.Builder(context)
|
||||
.setSmallIcon(R.drawable.ic_launcher)
|
||||
.setPriority(Notification.PRIORITY_LOW)
|
||||
.setOnlyAlertOnce(true)
|
||||
.setWhen(System.currentTimeMillis())
|
||||
.setContentTitle(context.getString(R.string.sync_error_title))
|
||||
.setContentText(exceptionToShow.getLocalizedMessage())
|
||||
.setContentInfo(account.name)
|
||||
.setStyle(new Notification.BigTextStyle().bigText(account.name + ":\n" + ExceptionUtils.getStackTrace(exceptionToShow)))
|
||||
.setContentIntent(contentIntent);
|
||||
|
||||
NotificationManager notificationManager = (NotificationManager)context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
notificationManager.notify(account.name.hashCode(), builder.build());
|
||||
}
|
||||
|
||||
Log.i(TAG, "Sync complete for " + authority);
|
||||
}
|
||||
|
||||
}
|
@ -7,210 +7,171 @@
|
||||
*/
|
||||
package at.bitfire.davdroid.syncadapter;
|
||||
|
||||
import android.accounts.Account;
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.ContentProviderClient;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SyncResult;
|
||||
import android.util.Log;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
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.Resource;
|
||||
import at.bitfire.davdroid.resource.WebDavCollection;
|
||||
import at.bitfire.davdroid.webdav.ConflictException;
|
||||
import at.bitfire.davdroid.webdav.DavException;
|
||||
import at.bitfire.davdroid.webdav.ForbiddenException;
|
||||
import at.bitfire.davdroid.webdav.HttpException;
|
||||
import at.bitfire.davdroid.webdav.NotFoundException;
|
||||
import at.bitfire.davdroid.webdav.PreconditionFailedException;
|
||||
import at.bitfire.davdroid.webdav.WebDavResource;
|
||||
import at.bitfire.dav4android.exception.DavException;
|
||||
import at.bitfire.dav4android.exception.HttpException;
|
||||
import at.bitfire.davdroid.Constants;
|
||||
import at.bitfire.davdroid.HttpClient;
|
||||
import at.bitfire.davdroid.R;
|
||||
import at.bitfire.davdroid.ui.DebugInfoActivity;
|
||||
import at.bitfire.vcard4android.ContactsStorageException;
|
||||
|
||||
public class SyncManager {
|
||||
private static final String TAG = "davdroid.SyncManager";
|
||||
|
||||
private static final int MAX_MULTIGET_RESOURCES = 35;
|
||||
|
||||
final protected LocalCollection<? extends Resource> local;
|
||||
final protected WebDavCollection<? extends Resource> remote;
|
||||
|
||||
|
||||
public SyncManager(LocalCollection<? extends Resource> local, WebDavCollection<? extends Resource> remote) {
|
||||
this.local = local;
|
||||
this.remote = remote;
|
||||
}
|
||||
abstract public class SyncManager {
|
||||
|
||||
|
||||
public void synchronize(boolean manualSync, SyncResult syncResult) throws URISyntaxException, LocalStorageException, IOException, HttpException, DavException {
|
||||
// PHASE 1: fetch collection properties
|
||||
remote.getProperties();
|
||||
final WebDavResource.Properties collectionProperties = remote.getCollection().getProperties();
|
||||
local.updateMetaData(collectionProperties);
|
||||
protected final int SYNC_PHASE_PREPARE = 0,
|
||||
SYNC_PHASE_QUERY_CAPABILITIES = 1,
|
||||
SYNC_PHASE_PROCESS_LOCALLY_DELETED = 2,
|
||||
SYNC_PHASE_PREPARE_LOCALLY_CREATED = 3,
|
||||
SYNC_PHASE_UPLOAD_DIRTY = 4,
|
||||
SYNC_PHASE_CHECK_SYNC_STATE = 5,
|
||||
SYNC_PHASE_LIST_LOCAL = 6,
|
||||
SYNC_PHASE_LIST_REMOTE = 7,
|
||||
SYNC_PHASE_COMPARE_ENTRIES = 8,
|
||||
SYNC_PHASE_DOWNLOAD_REMOTE = 9,
|
||||
SYNC_PHASE_SAVE_SYNC_STATE = 10;
|
||||
|
||||
// PHASE 2: push local changes to server
|
||||
int deletedRemotely = pushDeleted(),
|
||||
addedRemotely = pushNew(),
|
||||
updatedRemotely = pushDirty();
|
||||
final NotificationManager notificationManager;
|
||||
final int notificationId;
|
||||
|
||||
// PHASE 3A: check if there's a reason to do a sync with remote (= forced sync or remote CTag changed)
|
||||
boolean syncMembers = (deletedRemotely + addedRemotely + updatedRemotely) > 0;
|
||||
if (manualSync) {
|
||||
Log.i(TAG, "Full synchronization forced");
|
||||
syncMembers = true;
|
||||
}
|
||||
if (!syncMembers) {
|
||||
final String
|
||||
currentCTag = collectionProperties.getCTag(),
|
||||
lastCTag = local.getCTag();
|
||||
Log.d(TAG, "Last local CTag = " + lastCTag + "; current remote CTag = " + currentCTag);
|
||||
if (currentCTag == null || !currentCTag.equals(lastCTag))
|
||||
syncMembers = true;
|
||||
}
|
||||
|
||||
if (!syncMembers) {
|
||||
Log.i(TAG, "No local changes and CTags match, no need to sync");
|
||||
return;
|
||||
}
|
||||
|
||||
// PHASE 3B: detect details of remote changes
|
||||
Log.i(TAG, "Fetching remote resource list");
|
||||
Set<Resource> remotelyAdded = new HashSet<>(),
|
||||
remotelyUpdated = new HashSet<>();
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
final Context context;
|
||||
final Account account;
|
||||
final Bundle extras;
|
||||
final ContentProviderClient provider;
|
||||
final SyncResult syncResult;
|
||||
|
||||
// PHASE 4: pull remote changes from server
|
||||
syncResult.stats.numInserts = pullNew(remotelyAdded.toArray(new Resource[remotelyAdded.size()]));
|
||||
syncResult.stats.numUpdates = pullChanged(remotelyUpdated.toArray(new Resource[remotelyUpdated.size()]));
|
||||
final AccountSettings settings;
|
||||
final HttpClient httpClient;
|
||||
|
||||
Log.i(TAG, "Removing entries that are not present remotely anymore (retaining " + remoteResources.length + " entries)");
|
||||
syncResult.stats.numDeletes = local.deleteAllExceptRemoteNames(remoteResources);
|
||||
public SyncManager(int notificationId, Context context, Account account, Bundle extras, ContentProviderClient provider, SyncResult syncResult) {
|
||||
this.context = context;
|
||||
this.account = account;
|
||||
this.extras = extras;
|
||||
this.provider = provider;
|
||||
this.syncResult = syncResult;
|
||||
|
||||
syncResult.stats.numEntries = syncResult.stats.numInserts + syncResult.stats.numUpdates + syncResult.stats.numDeletes;
|
||||
// get account settings and generate httpClient
|
||||
settings = new AccountSettings(context, account);
|
||||
httpClient = new HttpClient(context, settings.getUserName(), settings.getPassword(), settings.getPreemptiveAuth());
|
||||
|
||||
// update collection CTag
|
||||
Log.i(TAG, "Sync complete, fetching new CTag");
|
||||
local.setCTag(collectionProperties.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|ConflictException e) {
|
||||
Log.i(TAG, "Locally-deleted resource has been changed on the server in the meanwhile");
|
||||
}
|
||||
// dismiss previous error notifications
|
||||
notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
notificationManager.cancel(account.name, this.notificationId = notificationId);
|
||||
}
|
||||
|
||||
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 (ConflictException|PreconditionFailedException e) {
|
||||
Log.i(TAG, "Didn't overwrite existing resource with other content");
|
||||
} 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 (ForbiddenException e) {
|
||||
Log.w(TAG, "Server has rejected local changes, server wins", e);
|
||||
} catch (ConflictException|PreconditionFailedException e) {
|
||||
Log.i(TAG, "Locally changed resource has been changed on the server in the meanwhile", e);
|
||||
} 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);
|
||||
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);
|
||||
count++;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
public void performSync() {
|
||||
int syncPhase = SYNC_PHASE_PREPARE;
|
||||
try {
|
||||
prepare();
|
||||
|
||||
syncPhase = SYNC_PHASE_QUERY_CAPABILITIES;
|
||||
queryCapabilities();
|
||||
|
||||
syncPhase = SYNC_PHASE_PROCESS_LOCALLY_DELETED;
|
||||
processLocallyDeleted();
|
||||
|
||||
syncPhase = SYNC_PHASE_PREPARE_LOCALLY_CREATED;
|
||||
processLocallyCreated();
|
||||
|
||||
syncPhase = SYNC_PHASE_UPLOAD_DIRTY;
|
||||
uploadDirty();
|
||||
|
||||
syncPhase = SYNC_PHASE_CHECK_SYNC_STATE;
|
||||
if (checkSyncState()) {
|
||||
syncPhase = SYNC_PHASE_LIST_LOCAL;
|
||||
listLocal();
|
||||
|
||||
syncPhase = SYNC_PHASE_LIST_REMOTE;
|
||||
listRemote();
|
||||
|
||||
syncPhase = SYNC_PHASE_COMPARE_ENTRIES;
|
||||
compareEntries();
|
||||
|
||||
syncPhase = SYNC_PHASE_DOWNLOAD_REMOTE;
|
||||
downloadRemote();
|
||||
|
||||
syncPhase = SYNC_PHASE_SAVE_SYNC_STATE;
|
||||
saveSyncState();
|
||||
} else
|
||||
Constants.log.info("Remote collection didn't change, skipping remote sync");
|
||||
|
||||
} catch (IOException e) {
|
||||
Constants.log.error("I/O exception during sync, trying again later", e);
|
||||
syncResult.stats.numIoExceptions++;
|
||||
|
||||
} catch(HttpException e) {
|
||||
Constants.log.error("HTTP Exception during sync", e);
|
||||
syncResult.stats.numParseExceptions++;
|
||||
|
||||
Intent detailsIntent = new Intent(context, DebugInfoActivity.class);
|
||||
detailsIntent.putExtra(DebugInfoActivity.KEY_EXCEPTION, e);
|
||||
detailsIntent.putExtra(DebugInfoActivity.KEY_ACCOUNT, account);
|
||||
detailsIntent.putExtra(DebugInfoActivity.KEY_PHASE, syncPhase);
|
||||
|
||||
Notification.Builder builder = new Notification.Builder(context);
|
||||
Notification notification;
|
||||
builder .setSmallIcon(R.drawable.ic_launcher)
|
||||
.setContentTitle(context.getString(R.string.sync_error_title, account.name))
|
||||
.setContentIntent(PendingIntent.getActivity(context, 0, detailsIntent, PendingIntent.FLAG_UPDATE_CURRENT));
|
||||
|
||||
String[] phases = context.getResources().getStringArray(R.array.sync_error_phases);
|
||||
if (phases.length > syncPhase)
|
||||
builder.setContentText(context.getString(R.string.sync_error_http, phases[syncPhase]));
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 16) {
|
||||
if (Build.VERSION.SDK_INT >= 21)
|
||||
builder.setCategory(Notification.CATEGORY_ERROR);
|
||||
notification = builder.build();
|
||||
} else {
|
||||
notification = builder.getNotification();
|
||||
}
|
||||
notificationManager.notify(account.name, notificationId, notification);
|
||||
|
||||
} catch(DavException e) {
|
||||
// TODO
|
||||
} catch(ContactsStorageException e) {
|
||||
syncResult.databaseError = true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
abstract protected void prepare();
|
||||
|
||||
abstract protected void queryCapabilities() throws IOException, HttpException, DavException, ContactsStorageException;
|
||||
|
||||
abstract protected void processLocallyDeleted() throws IOException, HttpException, DavException, ContactsStorageException;
|
||||
|
||||
abstract protected void processLocallyCreated() throws IOException, HttpException, DavException, ContactsStorageException;
|
||||
|
||||
abstract protected void uploadDirty() throws IOException, HttpException, DavException, ContactsStorageException;
|
||||
|
||||
/**
|
||||
* Checks the current sync state (e.g. CTag) and whether synchronization from remote is required.
|
||||
* @return true if the remote collection has changed, i.e. synchronization from remote is required
|
||||
* false if the remote collection hasn't changed
|
||||
*/
|
||||
abstract protected boolean checkSyncState() throws IOException, HttpException, DavException, ContactsStorageException;
|
||||
|
||||
abstract protected void listLocal() throws IOException, HttpException, DavException, ContactsStorageException;
|
||||
|
||||
abstract protected void listRemote() throws IOException, HttpException, DavException, ContactsStorageException;
|
||||
|
||||
abstract protected void compareEntries() throws IOException, HttpException, DavException, ContactsStorageException;
|
||||
|
||||
abstract protected void downloadRemote() throws IOException, HttpException, DavException, ContactsStorageException;
|
||||
|
||||
abstract protected void saveSyncState() throws IOException, HttpException, DavException, ContactsStorageException;
|
||||
|
||||
}
|
||||
|
@ -9,21 +9,13 @@ package at.bitfire.davdroid.syncadapter;
|
||||
|
||||
import android.accounts.Account;
|
||||
import android.app.Service;
|
||||
import android.content.AbstractThreadedSyncAdapter;
|
||||
import android.content.ContentProviderClient;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SyncResult;
|
||||
import android.os.Bundle;
|
||||
import android.os.IBinder;
|
||||
import android.os.RemoteException;
|
||||
import android.util.Log;
|
||||
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import at.bitfire.davdroid.resource.CalDavTaskList;
|
||||
import at.bitfire.davdroid.resource.LocalCollection;
|
||||
import at.bitfire.davdroid.resource.LocalTaskList;
|
||||
import at.bitfire.davdroid.resource.WebDavCollection;
|
||||
|
||||
public class TasksSyncAdapterService extends Service {
|
||||
private static SyncAdapter syncAdapter;
|
||||
@ -36,7 +28,6 @@ public class TasksSyncAdapterService extends Service {
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
syncAdapter.close();
|
||||
syncAdapter = null;
|
||||
}
|
||||
|
||||
@ -46,35 +37,15 @@ public class TasksSyncAdapterService extends Service {
|
||||
}
|
||||
|
||||
|
||||
private static class SyncAdapter extends DavSyncAdapter {
|
||||
private final static String TAG = "davdroid.TasksSync";
|
||||
private static class SyncAdapter extends AbstractThreadedSyncAdapter {
|
||||
public SyncAdapter(Context context) {
|
||||
super(context, false);
|
||||
}
|
||||
|
||||
private SyncAdapter(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Map<LocalCollection<?>, WebDavCollection<?>> getSyncPairs(Account account, ContentProviderClient provider) {
|
||||
AccountSettings settings = new AccountSettings(getContext(), account);
|
||||
String userName = settings.getUserName(),
|
||||
password = settings.getPassword();
|
||||
boolean preemptive = settings.getPreemptiveAuth();
|
||||
@Override
|
||||
public void onPerformSync(Account account, Bundle extras, String authority, ContentProviderClient provider, SyncResult syncResult) {
|
||||
|
||||
try {
|
||||
Map<LocalCollection<?>, WebDavCollection<?>> map = new HashMap<>();
|
||||
}
|
||||
}
|
||||
|
||||
for (LocalTaskList calendar : LocalTaskList.findAll(account, provider)) {
|
||||
WebDavCollection<?> dav = new CalDavTaskList(httpClient, calendar.getUrl(), userName, password, preemptive);
|
||||
map.put(calendar, dav);
|
||||
}
|
||||
return map;
|
||||
} catch (RemoteException ex) {
|
||||
Log.e(TAG, "Couldn't find local task lists", ex);
|
||||
} catch (URISyntaxException ex) {
|
||||
Log.e(TAG, "Couldn't build task list URI", ex);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -20,7 +20,6 @@ import android.provider.CalendarContract;
|
||||
import android.provider.ContactsContract;
|
||||
|
||||
import at.bitfire.davdroid.R;
|
||||
import at.bitfire.davdroid.resource.LocalTaskList;
|
||||
import at.bitfire.davdroid.syncadapter.AccountSettings;
|
||||
import ezvcard.VCardVersion;
|
||||
|
||||
@ -120,7 +119,7 @@ public class AccountFragment extends PreferenceFragment {
|
||||
prefSyncCalendars.setSummary(R.string.settings_sync_summary_not_available);
|
||||
}
|
||||
|
||||
final ListPreference prefSyncTasks = (ListPreference)findPreference("sync_interval_tasks");
|
||||
/*final ListPreference prefSyncTasks = (ListPreference)findPreference("sync_interval_tasks");
|
||||
final Long syncIntervalTasks = settings.getSyncInterval(LocalTaskList.TASKS_AUTHORITY);
|
||||
if (syncIntervalTasks != null) {
|
||||
prefSyncTasks.setValue(syncIntervalTasks.toString());
|
||||
@ -139,7 +138,7 @@ public class AccountFragment extends PreferenceFragment {
|
||||
} else {
|
||||
prefSyncTasks.setEnabled(false);
|
||||
prefSyncTasks.setSummary(R.string.settings_sync_summary_not_available);
|
||||
}
|
||||
}*/
|
||||
|
||||
// category: address book
|
||||
final CheckBoxPreference prefVCard4 = (CheckBoxPreference) findPreference("vcard4_support");
|
||||
|
@ -14,7 +14,6 @@ import android.content.ContentProviderClient;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.ContentValues;
|
||||
import android.os.Bundle;
|
||||
import android.provider.CalendarContract;
|
||||
import android.provider.ContactsContract;
|
||||
import android.text.Editable;
|
||||
import android.text.TextWatcher;
|
||||
@ -34,9 +33,6 @@ import java.util.List;
|
||||
import at.bitfire.davdroid.Constants;
|
||||
import at.bitfire.davdroid.R;
|
||||
import at.bitfire.davdroid.resource.LocalAddressBook;
|
||||
import at.bitfire.davdroid.resource.LocalCalendar;
|
||||
import at.bitfire.davdroid.resource.LocalStorageException;
|
||||
import at.bitfire.davdroid.resource.LocalTaskList;
|
||||
import at.bitfire.davdroid.resource.ServerInfo;
|
||||
import at.bitfire.davdroid.syncadapter.AccountSettings;
|
||||
import at.bitfire.vcard4android.ContactsStorageException;
|
||||
@ -99,7 +95,7 @@ public class AccountDetailsFragment extends Fragment implements TextWatcher {
|
||||
if (accountManager.addAccountExplicitly(account, serverInfo.getPassword(), userData)) {
|
||||
addSync(account, ContactsContract.AUTHORITY, serverInfo.getAddressBooks(), new AddSyncCallback() {
|
||||
@Override
|
||||
public void createLocalCollection(Account account, ServerInfo.ResourceInfo resource) throws LocalStorageException, ContactsStorageException {
|
||||
public void createLocalCollection(Account account, ServerInfo.ResourceInfo resource) throws ContactsStorageException {
|
||||
@Cleanup("release") ContentProviderClient provider = getActivity().getContentResolver().acquireContentProviderClient(ContactsContract.AUTHORITY);
|
||||
LocalAddressBook addressBook = new LocalAddressBook(account, provider);
|
||||
ContentValues settings = new ContentValues(2);
|
||||
@ -109,10 +105,10 @@ public class AccountDetailsFragment extends Fragment implements TextWatcher {
|
||||
}
|
||||
});
|
||||
|
||||
addSync(account, CalendarContract.AUTHORITY, serverInfo.getCalendars(), new AddSyncCallback() {
|
||||
/*addSync(account, CalendarContract.AUTHORITY, serverInfo.getCalendars(), new AddSyncCallback() {
|
||||
@Override
|
||||
public void createLocalCollection(Account account, ServerInfo.ResourceInfo calendar) throws LocalStorageException {
|
||||
LocalCalendar.create(account, getActivity().getContentResolver(), calendar);
|
||||
LocalCalendar.create(account, getActivity().getContentResolver(), calendar);
|
||||
}
|
||||
});
|
||||
|
||||
@ -121,7 +117,7 @@ public class AccountDetailsFragment extends Fragment implements TextWatcher {
|
||||
public void createLocalCollection(Account account, ServerInfo.ResourceInfo todoList) throws LocalStorageException {
|
||||
LocalTaskList.create(account, getActivity().getContentResolver(), todoList);
|
||||
}
|
||||
});
|
||||
});*/
|
||||
|
||||
getActivity().finish();
|
||||
} else
|
||||
@ -129,7 +125,7 @@ public class AccountDetailsFragment extends Fragment implements TextWatcher {
|
||||
}
|
||||
|
||||
protected interface AddSyncCallback {
|
||||
void createLocalCollection(Account account, ServerInfo.ResourceInfo resource) throws LocalStorageException, ContactsStorageException;
|
||||
void createLocalCollection(Account account, ServerInfo.ResourceInfo resource) throws ContactsStorageException;
|
||||
}
|
||||
|
||||
protected void addSync(Account account, String authority, List<ServerInfo.ResourceInfo> resourceList, AddSyncCallback callback) {
|
||||
@ -140,7 +136,7 @@ public class AccountDetailsFragment extends Fragment implements TextWatcher {
|
||||
if (callback != null)
|
||||
try {
|
||||
callback.createLocalCollection(account, resource);
|
||||
} catch(LocalStorageException|ContactsStorageException e) {
|
||||
} catch(ContactsStorageException e) {
|
||||
Log.e(TAG, "Couldn't add sync collection", e);
|
||||
Toast.makeText(getActivity(), "Couldn't set up synchronization for " + authority, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
|
@ -12,6 +12,7 @@ import android.app.Fragment;
|
||||
import android.app.FragmentTransaction;
|
||||
import android.os.Bundle;
|
||||
import android.text.Editable;
|
||||
import android.text.TextUtils;
|
||||
import android.text.TextWatcher;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
@ -26,8 +27,6 @@ import android.widget.EditText;
|
||||
import android.widget.Spinner;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
|
||||
@ -128,7 +127,7 @@ public class LoginURLFragment extends Fragment implements TextWatcher {
|
||||
|
||||
// check host name
|
||||
try {
|
||||
if (!StringUtils.isBlank(getBaseURI().getHost()))
|
||||
if (!TextUtils.isEmpty(getBaseURI().getHost()))
|
||||
urlOk = true;
|
||||
} catch (Exception e) {
|
||||
}
|
||||
|
@ -20,18 +20,14 @@ import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.apache.commons.lang3.exception.ExceptionUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.security.cert.CertPathValidatorException;
|
||||
|
||||
import at.bitfire.dav4android.exception.DavException;
|
||||
import at.bitfire.dav4android.exception.HttpException;
|
||||
import at.bitfire.davdroid.R;
|
||||
import at.bitfire.davdroid.resource.DavResourceFinder;
|
||||
import at.bitfire.davdroid.resource.LocalTaskList;
|
||||
import at.bitfire.davdroid.resource.ServerInfo;
|
||||
|
||||
public class QueryServerDialogFragment extends DialogFragment implements LoaderCallbacks<ServerInfo> {
|
||||
@ -72,7 +68,7 @@ public class QueryServerDialogFragment extends DialogFragment implements LoaderC
|
||||
((AddAccountActivity)getActivity()).serverInfo = serverInfo;
|
||||
|
||||
Fragment nextFragment;
|
||||
if (!serverInfo.getTaskLists().isEmpty() && !LocalTaskList.isAvailable(getActivity()))
|
||||
if (!serverInfo.getTaskLists().isEmpty() /*&& !LocalTaskList.isAvailable(getActivity())*/)
|
||||
nextFragment = new InstallAppsFragment();
|
||||
else
|
||||
nextFragment = new SelectCollectionsFragment();
|
||||
@ -120,8 +116,8 @@ public class QueryServerDialogFragment extends DialogFragment implements LoaderC
|
||||
// general message
|
||||
serverInfo.setErrorMessage(getContext().getString(R.string.exception_io, e.getLocalizedMessage()));
|
||||
// overwrite by more specific message, if possible
|
||||
if (ExceptionUtils.indexOfType(e, CertPathValidatorException.class) != -1)
|
||||
serverInfo.setErrorMessage(getContext().getString(R.string.exception_cert_path_validation, e.getMessage()));
|
||||
/*if (ExceptionUtils.indexOfType(e, CertPathValidatorException.class) != -1)
|
||||
serverInfo.setErrorMessage(getContext().getString(R.string.exception_cert_path_validation, e.getMessage()));*/
|
||||
} catch (HttpException e) {
|
||||
Log.e(TAG, "HTTP error while querying server info", e);
|
||||
serverInfo.setErrorMessage(getContext().getString(R.string.exception_http, e.getLocalizedMessage()));
|
||||
|
@ -18,7 +18,6 @@ import android.widget.CheckedTextView;
|
||||
import android.widget.ListAdapter;
|
||||
|
||||
import at.bitfire.davdroid.R;
|
||||
import at.bitfire.davdroid.resource.LocalTaskList;
|
||||
import at.bitfire.davdroid.resource.ServerInfo;
|
||||
import lombok.Getter;
|
||||
|
||||
@ -172,7 +171,7 @@ public class SelectCollectionsAdapter extends BaseAdapter implements ListAdapter
|
||||
}
|
||||
|
||||
// disable task list selection if there's no local task provider
|
||||
if (viewType == TYPE_TASK_LISTS_ROW && !LocalTaskList.isAvailable(context)) {
|
||||
if (viewType == TYPE_TASK_LISTS_ROW /*&& !LocalTaskList.isAvailable(context)*/) {
|
||||
final CheckedTextView check = (CheckedTextView)v;
|
||||
check.setEnabled(false);
|
||||
check.setOnClickListener(new View.OnClickListener() {
|
||||
|
@ -1,17 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2013 – 2015 Ricki Hirner (bitfire web engineering).
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the GNU Public License v3.0
|
||||
* which accompanies this distribution, and is available at
|
||||
* http://www.gnu.org/licenses/gpl.html
|
||||
*/
|
||||
|
||||
package at.bitfire.davdroid.webdav;
|
||||
|
||||
import org.apache.http.HttpStatus;
|
||||
|
||||
public class ConflictException extends HttpException {
|
||||
public ConflictException(String reason) {
|
||||
super(HttpStatus.SC_CONFLICT, reason);
|
||||
}
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2013 – 2015 Ricki Hirner (bitfire web engineering).
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the GNU Public License v3.0
|
||||
* which accompanies this distribution, and is available at
|
||||
* http://www.gnu.org/licenses/gpl.html
|
||||
*/
|
||||
package at.bitfire.davdroid.webdav;
|
||||
|
||||
import 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 © 2013 – 2015 Ricki Hirner (bitfire web engineering).
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the GNU Public License v3.0
|
||||
* which accompanies this distribution, and is available at
|
||||
* http://www.gnu.org/licenses/gpl.html
|
||||
*/
|
||||
package at.bitfire.davdroid.webdav;
|
||||
|
||||
import org.simpleframework.xml.Namespace;
|
||||
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,31 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2013 – 2015 Ricki Hirner (bitfire web engineering).
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the GNU Public License v3.0
|
||||
* which accompanies this distribution, and is available at
|
||||
* http://www.gnu.org/licenses/gpl.html
|
||||
*/
|
||||
|
||||
package at.bitfire.davdroid.webdav;
|
||||
|
||||
import org.simpleframework.xml.Element;
|
||||
import org.simpleframework.xml.Namespace;
|
||||
import org.simpleframework.xml.NamespaceList;
|
||||
import org.simpleframework.xml.Root;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
@Root(name="calendar-query")
|
||||
@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 DavCalendarQuery {
|
||||
@Element
|
||||
@Getter @Setter DavProp prop;
|
||||
|
||||
@Element
|
||||
@Getter @Setter DavFilter filter;
|
||||
}
|
@ -1,29 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2013 – 2015 Ricki Hirner (bitfire web engineering).
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the GNU Public License v3.0
|
||||
* which accompanies this distribution, and is available at
|
||||
* http://www.gnu.org/licenses/gpl.html
|
||||
*/
|
||||
|
||||
package at.bitfire.davdroid.webdav;
|
||||
|
||||
import org.simpleframework.xml.Attribute;
|
||||
import org.simpleframework.xml.Element;
|
||||
import org.simpleframework.xml.Namespace;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
@Namespace(prefix="C",reference="urn:ietf:params:xml:ns:caldav")
|
||||
public class DavCompFilter {
|
||||
public DavCompFilter(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
@Attribute(required=false)
|
||||
final String name;
|
||||
|
||||
@Element(required=false,name="comp-filter")
|
||||
@Getter @Setter DavCompFilter compFilter;
|
||||
}
|
@ -1,25 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2013 – 2015 Ricki Hirner (bitfire web engineering).
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the GNU Public License v3.0
|
||||
* which accompanies this distribution, and is available at
|
||||
* http://www.gnu.org/licenses/gpl.html
|
||||
*/
|
||||
package at.bitfire.davdroid.webdav;
|
||||
|
||||
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,21 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2013 – 2015 Ricki Hirner (bitfire web engineering).
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the GNU Public License v3.0
|
||||
* which accompanies this distribution, and is available at
|
||||
* http://www.gnu.org/licenses/gpl.html
|
||||
*/
|
||||
|
||||
package at.bitfire.davdroid.webdav;
|
||||
|
||||
import org.simpleframework.xml.Element;
|
||||
import org.simpleframework.xml.Namespace;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
@Namespace(prefix="C",reference="urn:ietf:params:xml:ns:caldav")
|
||||
public class DavFilter {
|
||||
@Element(required=false,name="comp-filter")
|
||||
@Getter @Setter DavCompFilter compFilter;
|
||||
}
|
@ -1,26 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2013 – 2015 Ricki Hirner (bitfire web engineering).
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the GNU Public License v3.0
|
||||
* which accompanies this distribution, and is available at
|
||||
* http://www.gnu.org/licenses/gpl.html
|
||||
*/
|
||||
package at.bitfire.davdroid.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,71 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2013 – 2015 Ricki Hirner (bitfire web engineering).
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the GNU Public License v3.0
|
||||
* which accompanies this distribution, and is available at
|
||||
* http://www.gnu.org/licenses/gpl.html
|
||||
*/
|
||||
package at.bitfire.davdroid.webdav;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.util.Log;
|
||||
|
||||
import org.apache.http.client.config.RequestConfig;
|
||||
import org.apache.http.config.Registry;
|
||||
import org.apache.http.config.RegistryBuilder;
|
||||
import org.apache.http.conn.socket.ConnectionSocketFactory;
|
||||
import org.apache.http.conn.socket.PlainConnectionSocketFactory;
|
||||
import org.apache.http.impl.client.CloseableHttpClient;
|
||||
import org.apache.http.impl.client.HttpClientBuilder;
|
||||
import org.apache.http.impl.client.HttpClients;
|
||||
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
|
||||
|
||||
import at.bitfire.davdroid.BuildConfig;
|
||||
import at.bitfire.davdroid.Constants;
|
||||
|
||||
|
||||
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.getSocketFactory())
|
||||
.build();
|
||||
|
||||
// use request defaults from AndroidHttpClient
|
||||
defaultRqConfig = RequestConfig.copy(RequestConfig.DEFAULT)
|
||||
.setConnectTimeout(20*1000)
|
||||
.setSocketTimeout(45*1000)
|
||||
.setStaleConnectionCheckEnabled(false)
|
||||
.build();
|
||||
}
|
||||
|
||||
|
||||
@SuppressLint("LogTagMismatch")
|
||||
public static CloseableHttpClient create() {
|
||||
PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(socketFactoryRegistry);
|
||||
// limits per DavHttpClient (= per DavSyncAdapter extends AbstractThreadedSyncAdapter)
|
||||
connectionManager.setMaxTotal(3); // max. 3 connections in total
|
||||
connectionManager.setDefaultMaxPerRoute(2); // max. 2 connections per host
|
||||
|
||||
HttpClientBuilder builder = HttpClients.custom()
|
||||
.useSystemProperties()
|
||||
.setConnectionManager(connectionManager)
|
||||
.setDefaultRequestConfig(defaultRqConfig)
|
||||
.setRetryHandler(DavHttpRequestRetryHandler.INSTANCE)
|
||||
.setRedirectStrategy(DavRedirectStrategy.INSTANCE)
|
||||
.setUserAgent("DAVdroid/" + BuildConfig.VERSION_NAME);
|
||||
|
||||
if (Log.isLoggable("Wire", Log.DEBUG)) {
|
||||
Log.i(TAG, "Wire logging active, disabling HTTP compression");
|
||||
builder = builder.disableContentCompression();
|
||||
}
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
}
|
@ -1,34 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2013 – 2015 Ricki Hirner (bitfire web engineering).
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the GNU Public License v3.0
|
||||
* which accompanies this distribution, and is available at
|
||||
* http://www.gnu.org/licenses/gpl.html
|
||||
*/
|
||||
package at.bitfire.davdroid.webdav;
|
||||
|
||||
import org.apache.commons.lang3.ArrayUtils;
|
||||
import org.apache.http.HttpRequest;
|
||||
import org.apache.http.impl.client.DefaultHttpRequestRetryHandlerHC4;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
public class DavHttpRequestRetryHandler extends DefaultHttpRequestRetryHandlerHC4 {
|
||||
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 © 2013 – 2015 Ricki Hirner (bitfire web engineering).
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the GNU Public License v3.0
|
||||
* which accompanies this distribution, and is available at
|
||||
* http://www.gnu.org/licenses/gpl.html
|
||||
*/
|
||||
package at.bitfire.davdroid.webdav;
|
||||
|
||||
public class DavIncapableException extends DavException {
|
||||
private static final long serialVersionUID = -7199786680939975667L;
|
||||
|
||||
/* used to indicate that the server doesn't support DAV */
|
||||
|
||||
public DavIncapableException(String msg) {
|
||||
super(msg);
|
||||
}
|
||||
}
|
@ -1,57 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2013 – 2015 Ricki Hirner (bitfire web engineering).
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the GNU Public License v3.0
|
||||
* which accompanies this distribution, and is available at
|
||||
* http://www.gnu.org/licenses/gpl.html
|
||||
*/
|
||||
package at.bitfire.davdroid.webdav;
|
||||
|
||||
import org.simpleframework.xml.Element;
|
||||
import org.simpleframework.xml.ElementList;
|
||||
import org.simpleframework.xml.Order;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
@Order(elements={"prop","href"})
|
||||
public class DavMultiget {
|
||||
public enum Type {
|
||||
ADDRESS_BOOK,
|
||||
ADDRESS_BOOK_V4,
|
||||
CALENDAR
|
||||
}
|
||||
|
||||
@Element
|
||||
DavProp prop;
|
||||
|
||||
@ElementList(inline=true)
|
||||
ArrayList<DavHref> hrefs;
|
||||
|
||||
|
||||
public static DavMultiget newRequest(Type type, String names[]) {
|
||||
DavMultiget multiget = (type == Type.CALENDAR) ? new DavCalendarMultiget() : new DavAddressbookMultiget();
|
||||
|
||||
multiget.prop = new DavProp();
|
||||
multiget.prop.getetag = new DavProp.GetETag();
|
||||
|
||||
switch (type) {
|
||||
case ADDRESS_BOOK:
|
||||
multiget.prop.addressData = new DavProp.AddressData();
|
||||
break;
|
||||
case ADDRESS_BOOK_V4:
|
||||
DavProp.AddressData addressData = new DavProp.AddressData();
|
||||
addressData.setContentType("text/vcard");
|
||||
addressData.setVersion("4.0");
|
||||
multiget.prop.addressData = addressData;
|
||||
break;
|
||||
case CALENDAR:
|
||||
multiget.prop.calendarData = new DavProp.CalendarData();
|
||||
}
|
||||
|
||||
multiget.hrefs = new ArrayList<>(names.length);
|
||||
for (String name : names)
|
||||
multiget.hrefs.add(new DavHref(name));
|
||||
|
||||
return multiget;
|
||||
}
|
||||
}
|
@ -1,22 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2013 – 2015 Ricki Hirner (bitfire web engineering).
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the GNU Public License v3.0
|
||||
* which accompanies this distribution, and is available at
|
||||
* http://www.gnu.org/licenses/gpl.html
|
||||
*/
|
||||
package at.bitfire.davdroid.webdav;
|
||||
|
||||
import org.simpleframework.xml.ElementList;
|
||||
import org.simpleframework.xml.Namespace;
|
||||
import org.simpleframework.xml.Root;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@Namespace(reference="DAV:")
|
||||
@Root(strict=false)
|
||||
public class DavMultistatus {
|
||||
@ElementList(inline=true,entry="response",required=false)
|
||||
ArrayList<DavResponse> response;
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2013 – 2015 Ricki Hirner (bitfire web engineering).
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the GNU Public License v3.0
|
||||
* which accompanies this distribution, and is available at
|
||||
* http://www.gnu.org/licenses/gpl.html
|
||||
*/
|
||||
package at.bitfire.davdroid.webdav;
|
||||
|
||||
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 © 2013 – 2015 Ricki Hirner (bitfire web engineering).
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the GNU Public License v3.0
|
||||
* which accompanies this distribution, and is available at
|
||||
* http://www.gnu.org/licenses/gpl.html
|
||||
*/
|
||||
package at.bitfire.davdroid.webdav;
|
||||
|
||||
public class DavNoMultiStatusException extends DavException {
|
||||
private static final long serialVersionUID = -3600405724694229828L;
|
||||
|
||||
private final static String message = "207 Multi-Status expected but not received";
|
||||
|
||||
public DavNoMultiStatusException() {
|
||||
super(message);
|
||||
}
|
||||
}
|
@ -1,218 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2013 – 2015 Ricki Hirner (bitfire web engineering).
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the GNU Public License v3.0
|
||||
* which accompanies this distribution, and is available at
|
||||
* http://www.gnu.org/licenses/gpl.html
|
||||
*/
|
||||
package at.bitfire.davdroid.webdav;
|
||||
|
||||
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;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
@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)
|
||||
@Setter 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")
|
||||
ArrayList<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")
|
||||
ArrayList<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")
|
||||
ArrayList<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 {
|
||||
@Attribute(name="content-type", required=false)
|
||||
@Getter @Setter String contentType;
|
||||
|
||||
@Attribute(required=false)
|
||||
@Getter @Setter String version;
|
||||
|
||||
@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 © 2013 – 2015 Ricki Hirner (bitfire web engineering).
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the GNU Public License v3.0
|
||||
* which accompanies this distribution, and is available at
|
||||
* http://www.gnu.org/licenses/gpl.html
|
||||
*/
|
||||
package at.bitfire.davdroid.webdav;
|
||||
|
||||
import org.simpleframework.xml.Element;
|
||||
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 © 2013 – 2015 Ricki Hirner (bitfire web engineering).
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the GNU Public License v3.0
|
||||
* which accompanies this distribution, and is available at
|
||||
* http://www.gnu.org/licenses/gpl.html
|
||||
*/
|
||||
package at.bitfire.davdroid.webdav;
|
||||
|
||||
import org.simpleframework.xml.Element;
|
||||
import org.simpleframework.xml.Root;
|
||||
|
||||
@Root(strict=false,name="propstat")
|
||||
public class DavPropstat {
|
||||
@Element
|
||||
DavProp prop;
|
||||
|
||||
@Element
|
||||
String status;
|
||||
}
|
@ -1,108 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2013 – 2015 Ricki Hirner (bitfire web engineering).
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the GNU Public License v3.0
|
||||
* which accompanies this distribution, and is available at
|
||||
* http://www.gnu.org/licenses/gpl.html
|
||||
*/
|
||||
package at.bitfire.davdroid.webdav;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import org.apache.http.Header;
|
||||
import org.apache.http.HttpHost;
|
||||
import org.apache.http.HttpRequest;
|
||||
import org.apache.http.HttpResponse;
|
||||
import org.apache.http.ProtocolException;
|
||||
import org.apache.http.RequestLine;
|
||||
import org.apache.http.client.RedirectStrategy;
|
||||
import org.apache.http.client.methods.HttpUriRequest;
|
||||
import org.apache.http.client.methods.RequestBuilder;
|
||||
import org.apache.http.client.protocol.HttpClientContext;
|
||||
import org.apache.http.protocol.HttpContext;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
|
||||
import at.bitfire.davdroid.URIUtils;
|
||||
|
||||
/**
|
||||
* Custom Redirect Strategy that handles 30x for CalDAV/CardDAV-specific requests correctly
|
||||
*/
|
||||
public class DavRedirectStrategy implements RedirectStrategy {
|
||||
private final static String TAG = "davdroid.DavRedirectStrategy";
|
||||
public 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 URI 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 = URIUtils.parseURI(locationHdr.getValue(), false);
|
||||
|
||||
// some servers don't return absolute URLs as required by RFC 2616
|
||||
if (!location.isAbsolute()) {
|
||||
Log.w(TAG, "Received invalid redirection to relative URL, repairing");
|
||||
URI originalURI = URIUtils.parseURI(request.getRequestLine().getUri(), false);
|
||||
if (!originalURI.isAbsolute()) {
|
||||
final HttpHost target = HttpClientContext.adapt(context).getTargetHost();
|
||||
if (target != null)
|
||||
originalURI = org.apache.http.client.utils.URIUtilsHC4.rewriteURI(originalURI, target);
|
||||
else
|
||||
return null;
|
||||
}
|
||||
return originalURI.resolve(location);
|
||||
}
|
||||
return location;
|
||||
} catch (URISyntaxException e) {
|
||||
Log.e(TAG, "Received redirection from/to invalid URI, ignoring", e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
@ -1,26 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2013 – 2015 Ricki Hirner (bitfire web engineering).
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the GNU Public License v3.0
|
||||
* which accompanies this distribution, and is available at
|
||||
* http://www.gnu.org/licenses/gpl.html
|
||||
*/
|
||||
package at.bitfire.davdroid.webdav;
|
||||
|
||||
import org.simpleframework.xml.Element;
|
||||
import org.simpleframework.xml.ElementList;
|
||||
import org.simpleframework.xml.Root;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
@Root(strict=false)
|
||||
public class DavResponse {
|
||||
@Element
|
||||
DavHref href;
|
||||
|
||||
@Element(required=false)
|
||||
String status;
|
||||
|
||||
@ElementList(inline=true,required=false)
|
||||
ArrayList<DavPropstat> propstat;
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2013 – 2015 Ricki Hirner (bitfire web engineering).
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the GNU Public License v3.0
|
||||
* which accompanies this distribution, and is available at
|
||||
* http://www.gnu.org/licenses/gpl.html
|
||||
*/
|
||||
|
||||
package at.bitfire.davdroid.webdav;
|
||||
|
||||
import org.apache.http.HttpStatus;
|
||||
|
||||
public class ForbiddenException extends HttpException {
|
||||
private static final long serialVersionUID = 102282229174086113L;
|
||||
|
||||
public ForbiddenException(String reason) {
|
||||
super(HttpStatus.SC_FORBIDDEN, reason);
|
||||
}
|
||||
}
|
@ -1,26 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2013 – 2015 Ricki Hirner (bitfire web engineering).
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the GNU Public License v3.0
|
||||
* which accompanies this distribution, and is available at
|
||||
* http://www.gnu.org/licenses/gpl.html
|
||||
*/
|
||||
package at.bitfire.davdroid.webdav;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
public class HttpException extends org.apache.http.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,105 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2013 – 2015 Ricki Hirner (bitfire web engineering).
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the GNU Public License v3.0
|
||||
* which accompanies this distribution, and is available at
|
||||
* http://www.gnu.org/licenses/gpl.html
|
||||
*/
|
||||
package at.bitfire.davdroid.webdav;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import org.apache.http.client.methods.HttpEntityEnclosingRequestBaseHC4;
|
||||
import org.apache.http.entity.StringEntity;
|
||||
import org.simpleframework.xml.Serializer;
|
||||
import org.simpleframework.xml.core.Persister;
|
||||
|
||||
import java.io.StringWriter;
|
||||
import java.net.URI;
|
||||
import java.util.ArrayList;
|
||||
|
||||
public class HttpPropfind extends HttpEntityEnclosingRequestBaseHC4 {
|
||||
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_PROPERTIES,
|
||||
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 ArrayList<>();
|
||||
propfind.prop.addressbookDescription = new DavProp.AddressbookDescription();
|
||||
break;
|
||||
case CALDAV_COLLECTIONS:
|
||||
depth = 1;
|
||||
propfind.prop.displayname = new DavProp.DisplayName();
|
||||
propfind.prop.resourcetype = new DavProp.ResourceType();
|
||||
propfind.prop.currentUserPrivilegeSet = new ArrayList<>();
|
||||
propfind.prop.calendarDescription = new DavProp.CalendarDescription();
|
||||
propfind.prop.calendarColor = new DavProp.CalendarColor();
|
||||
propfind.prop.calendarTimezone = new DavProp.CalendarTimezone();
|
||||
propfind.prop.supportedCalendarComponentSet = new ArrayList<>();
|
||||
break;
|
||||
case COLLECTION_PROPERTIES:
|
||||
propfind.prop.getctag = new DavProp.GetCTag();
|
||||
propfind.prop.resourcetype = new DavProp.ResourceType();
|
||||
propfind.prop.displayname = new DavProp.DisplayName();
|
||||
propfind.prop.calendarColor = new DavProp.CalendarColor();
|
||||
propfind.prop.supportedAddressData = new ArrayList<>();
|
||||
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,46 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2013 – 2015 Ricki Hirner (bitfire web engineering).
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the GNU Public License v3.0
|
||||
* which accompanies this distribution, and is available at
|
||||
* http://www.gnu.org/licenses/gpl.html
|
||||
*/
|
||||
package at.bitfire.davdroid.webdav;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import org.apache.http.client.methods.HttpEntityEnclosingRequestBaseHC4;
|
||||
import org.apache.http.entity.StringEntity;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URI;
|
||||
|
||||
public class HttpReport extends HttpEntityEnclosingRequestBaseHC4 {
|
||||
private static final String TAG = "davdroid.HttpEntityEncloseRequestBase";
|
||||
|
||||
public final static String METHOD_NAME = "REPORT";
|
||||
|
||||
|
||||
HttpReport(URI uri) {
|
||||
setURI(uri);
|
||||
}
|
||||
|
||||
HttpReport(URI uri, String entity) {
|
||||
this(uri);
|
||||
|
||||
setHeader("Content-Type", "text/xml; charset=UTF-8");
|
||||
setHeader("Accept", "text/xml");
|
||||
setHeader("Depth", "1");
|
||||
|
||||
try {
|
||||
setEntity(new StringEntity(entity, "UTF-8"));
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
Log.wtf(TAG, "String entity doesn't support UTF-8");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMethod() {
|
||||
return METHOD_NAME;
|
||||
}
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2013 – 2015 Ricki Hirner (bitfire web engineering).
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the GNU Public License v3.0
|
||||
* which accompanies this distribution, and is available at
|
||||
* http://www.gnu.org/licenses/gpl.html
|
||||
*/
|
||||
package at.bitfire.davdroid.webdav;
|
||||
|
||||
import 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 © 2013 – 2015 Ricki Hirner (bitfire web engineering).
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the GNU Public License v3.0
|
||||
* which accompanies this distribution, and is available at
|
||||
* http://www.gnu.org/licenses/gpl.html
|
||||
*/
|
||||
package at.bitfire.davdroid.webdav;
|
||||
|
||||
import org.apache.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 © 2013 – 2015 Ricki Hirner (bitfire web engineering).
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the GNU Public License v3.0
|
||||
* which accompanies this distribution, and is available at
|
||||
* http://www.gnu.org/licenses/gpl.html
|
||||
*/
|
||||
package at.bitfire.davdroid.webdav;
|
||||
|
||||
import org.apache.http.HttpStatus;
|
||||
|
||||
public class PreconditionFailedException extends HttpException {
|
||||
private static final long serialVersionUID = 102282229174086113L;
|
||||
|
||||
public PreconditionFailedException(String reason) {
|
||||
super(HttpStatus.SC_PRECONDITION_FAILED, reason);
|
||||
}
|
||||
}
|
@ -1,108 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2013 – 2015 Ricki Hirner (bitfire web engineering).
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the GNU Public License v3.0
|
||||
* which accompanies this distribution, and is available at
|
||||
* http://www.gnu.org/licenses/gpl.html
|
||||
*/
|
||||
|
||||
package at.bitfire.davdroid.webdav;
|
||||
|
||||
import android.os.Build;
|
||||
import android.util.Log;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.http.conn.ssl.BrowserCompatHostnameVerifierHC4;
|
||||
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
|
||||
import org.apache.http.conn.ssl.X509HostnameVerifier;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
import javax.net.ssl.SSLSocket;
|
||||
import javax.net.ssl.SSLSocketFactory;
|
||||
|
||||
import lombok.Cleanup;
|
||||
|
||||
public class TlsSniSocketFactory extends SSLConnectionSocketFactory {
|
||||
private static final String TAG = "davdroid.TLS_SNI";
|
||||
|
||||
public static TlsSniSocketFactory getSocketFactory() {
|
||||
return new TlsSniSocketFactory(
|
||||
(SSLSocketFactory) SSLSocketFactory.getDefault(),
|
||||
new BrowserCompatHostnameVerifierHC4() // use BrowserCompatHostnameVerifier to allow IP addresses in the Common Name
|
||||
);
|
||||
}
|
||||
|
||||
// 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
|
||||
static String protocols[] = null, cipherSuites[] = null;
|
||||
static {
|
||||
try {
|
||||
@Cleanup SSLSocket socket = (SSLSocket)SSLSocketFactory.getDefault().createSocket();
|
||||
|
||||
/* set reasonable protocol versions */
|
||||
// - enable all supported protocols (enables TLSv1.1 and TLSv1.2 on Android <5.0)
|
||||
// - remove all SSL versions (especially SSLv3) because they're insecure now
|
||||
List<String> protocols = new LinkedList<>();
|
||||
for (String protocol : socket.getSupportedProtocols())
|
||||
if (!protocol.toUpperCase().contains("SSL"))
|
||||
protocols.add(protocol);
|
||||
Log.v(TAG, "Setting allowed TLS protocols: " + StringUtils.join(protocols, ", "));
|
||||
TlsSniSocketFactory.protocols = protocols.toArray(new String[protocols.size()]);
|
||||
|
||||
/* set up reasonable cipher suites */
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
||||
// choose known secure cipher suites
|
||||
List<String> allowedCiphers = Arrays.asList(
|
||||
// TLS 1.2
|
||||
"TLS_RSA_WITH_AES_256_GCM_SHA384",
|
||||
"TLS_RSA_WITH_AES_128_GCM_SHA256",
|
||||
"TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256",
|
||||
"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
|
||||
"TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
|
||||
"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256",
|
||||
"TLS_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(socket.getSupportedCipherSuites());
|
||||
Log.v(TAG, "Available cipher suites: " + StringUtils.join(availableCiphers, ", "));
|
||||
Log.v(TAG, "Cipher suites enabled by default: " + StringUtils.join(socket.getEnabledCipherSuites(), ", "));
|
||||
|
||||
// take all allowed ciphers that are available and put them into preferredCiphers
|
||||
HashSet<String> preferredCiphers = new HashSet<>(allowedCiphers);
|
||||
preferredCiphers.retainAll(availableCiphers);
|
||||
|
||||
/* For maximum security, preferredCiphers should *replace* enabled ciphers (thus disabling
|
||||
* ciphers which are enabled by default, but have become unsecure), but I guess for
|
||||
* the security level of DAVdroid and maximum compatibility, disabling of insecure
|
||||
* ciphers should be a server-side task */
|
||||
|
||||
// add preferred ciphers to enabled ciphers
|
||||
HashSet<String> enabledCiphers = preferredCiphers;
|
||||
enabledCiphers.addAll(new HashSet<>(Arrays.asList(socket.getEnabledCipherSuites())));
|
||||
|
||||
Log.v(TAG, "Enabling (only) those TLS ciphers: " + StringUtils.join(enabledCiphers, ", "));
|
||||
TlsSniSocketFactory.cipherSuites = enabledCiphers.toArray(new String[enabledCiphers.size()]);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
}
|
||||
}
|
||||
|
||||
public TlsSniSocketFactory(SSLSocketFactory socketfactory, X509HostnameVerifier hostnameVerifier) {
|
||||
super(socketfactory, protocols, cipherSuites, hostnameVerifier);
|
||||
}
|
||||
|
||||
}
|
@ -1,574 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2013 – 2015 Ricki Hirner (bitfire web engineering).
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the GNU Public License v3.0
|
||||
* which accompanies this distribution, and is available at
|
||||
* http://www.gnu.org/licenses/gpl.html
|
||||
*/
|
||||
package at.bitfire.davdroid.webdav;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.http.Header;
|
||||
import org.apache.http.HttpEntity;
|
||||
import org.apache.http.HttpHost;
|
||||
import org.apache.http.HttpResponse;
|
||||
import org.apache.http.HttpStatus;
|
||||
import org.apache.http.StatusLine;
|
||||
import org.apache.http.auth.AuthScope;
|
||||
import org.apache.http.auth.UsernamePasswordCredentials;
|
||||
import org.apache.http.client.AuthCache;
|
||||
import org.apache.http.client.methods.CloseableHttpResponse;
|
||||
import org.apache.http.client.methods.HttpDeleteHC4;
|
||||
import org.apache.http.client.methods.HttpGetHC4;
|
||||
import org.apache.http.client.methods.HttpOptionsHC4;
|
||||
import org.apache.http.client.methods.HttpPutHC4;
|
||||
import org.apache.http.client.protocol.HttpClientContext;
|
||||
import org.apache.http.entity.ByteArrayEntityHC4;
|
||||
import org.apache.http.entity.ContentType;
|
||||
import org.apache.http.impl.auth.BasicSchemeHC4;
|
||||
import org.apache.http.impl.client.BasicAuthCache;
|
||||
import org.apache.http.impl.client.BasicCredentialsProviderHC4;
|
||||
import org.apache.http.impl.client.CloseableHttpClient;
|
||||
import org.apache.http.message.BasicLineParserHC4;
|
||||
import org.apache.http.util.EntityUtilsHC4;
|
||||
import org.simpleframework.xml.Serializer;
|
||||
import org.simpleframework.xml.core.Persister;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.StringWriter;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import at.bitfire.davdroid.DavUtils;
|
||||
import at.bitfire.davdroid.URIUtils;
|
||||
import at.bitfire.davdroid.resource.iCalendar;
|
||||
import at.bitfire.davdroid.webdav.DavProp.Comp;
|
||||
import ezvcard.VCardVersion;
|
||||
import lombok.Cleanup;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.ToString;
|
||||
|
||||
|
||||
/**
|
||||
* 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 PutMode {
|
||||
ADD_DONT_OVERWRITE,
|
||||
UPDATE_DONT_OVERWRITE
|
||||
}
|
||||
|
||||
// location of this resource
|
||||
@Getter protected URI location;
|
||||
|
||||
// DAV capabilities (DAV: header) and allowed DAV methods (set for OPTIONS request)
|
||||
protected Set<String> capabilities = new HashSet<>(),
|
||||
methods = new HashSet<>();
|
||||
|
||||
// list of members (only for collections)
|
||||
@Getter Properties properties = new Properties();
|
||||
@Getter protected List<WebDavResource> members;
|
||||
|
||||
// content (available after GET)
|
||||
@Getter protected byte[] content;
|
||||
|
||||
protected CloseableHttpClient httpClient;
|
||||
protected HttpClientContext context;
|
||||
|
||||
|
||||
public WebDavResource(CloseableHttpClient httpClient, URI baseURI) {
|
||||
this.httpClient = httpClient;
|
||||
location = baseURI;
|
||||
|
||||
context = HttpClientContext.create();
|
||||
context.setCredentialsProvider(new BasicCredentialsProviderHC4());
|
||||
}
|
||||
|
||||
public WebDavResource(CloseableHttpClient httpClient, URI baseURI, String username, String password, boolean preemptive) {
|
||||
this(httpClient, baseURI);
|
||||
|
||||
context.getCredentialsProvider().setCredentials(
|
||||
AuthScope.ANY,
|
||||
new UsernamePasswordCredentials(username, password)
|
||||
);
|
||||
|
||||
if (preemptive) {
|
||||
HttpHost host = new HttpHost(baseURI.getHost(), baseURI.getPort(), baseURI.getScheme());
|
||||
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 BasicSchemeHC4());
|
||||
context.setAuthCache(authCache);
|
||||
}
|
||||
}
|
||||
|
||||
public WebDavResource(WebDavResource parent) { // copy constructor: based on existing WebDavResource, reuse settings
|
||||
// reuse httpClient, context and location (no deep copy)
|
||||
httpClient = parent.httpClient;
|
||||
context = parent.context;
|
||||
location = parent.location;
|
||||
}
|
||||
|
||||
public WebDavResource(WebDavResource parent, URI url) {
|
||||
this(parent);
|
||||
location = parent.location.resolve(url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a WebDavResource representing a member of the parent collection.
|
||||
* @param parent Parent collection
|
||||
* @param member File name of the member, unescaped. This may contain ":" without leading "./"!
|
||||
* To create a new collection with a relative path that may not be a member, use the
|
||||
* WebDavResource(WebDavResource parent, URI url) constructor.
|
||||
* @throws URISyntaxException
|
||||
*/
|
||||
public WebDavResource(WebDavResource parent, String member) throws URISyntaxException {
|
||||
this(parent);
|
||||
location = parent.location.resolve(new URI(null, null, "./" + member, null));
|
||||
}
|
||||
|
||||
public WebDavResource(WebDavResource parent, String member, String eTag) throws URISyntaxException {
|
||||
this(parent, member);
|
||||
properties.eTag = eTag;
|
||||
}
|
||||
|
||||
|
||||
/* feature detection */
|
||||
|
||||
public void options() throws URISyntaxException, IOException, HttpException {
|
||||
HttpOptionsHC4 options = new HttpOptionsHC4(location);
|
||||
|
||||
@Cleanup CloseableHttpResponse response = httpClient.execute(options, context);
|
||||
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(", ?")));
|
||||
}
|
||||
|
||||
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];
|
||||
}
|
||||
|
||||
|
||||
/* collection operations */
|
||||
|
||||
public void propfind(HttpPropfind.Mode mode) throws URISyntaxException, IOException, DavException, HttpException {
|
||||
@Cleanup 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, 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();
|
||||
|
||||
checkResponse(response); // will also handle Content-Location
|
||||
processMultiStatus(response);
|
||||
}
|
||||
|
||||
public void multiGet(DavMultiget.Type type, String[] names) throws URISyntaxException, IOException, DavException, HttpException {
|
||||
@Cleanup 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<>();
|
||||
for (String name : names)
|
||||
// name may contain "%" which have to be encoded → use non-quoting URI constructor and getRawPath()
|
||||
// name may also contain ":", so prepend "./" because even the non-quoting URI constructor parses after constructing
|
||||
// DAVdroid ensures that collections always have a trailing slash, so "./" won't go down in directory hierarchy
|
||||
hrefs.add(location.resolve(new URI(null, null, "./" + name, null)).getRawPath());
|
||||
DavMultiget multiget = DavMultiget.newRequest(type, hrefs.toArray(new String[hrefs.size()]));
|
||||
|
||||
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, 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();
|
||||
|
||||
checkResponse(response); // will also handle Content-Location
|
||||
processMultiStatus(response);
|
||||
}
|
||||
|
||||
public void report(String query) throws IOException, HttpException, DavException {
|
||||
HttpReport report = new HttpReport(location, query);
|
||||
report.setHeader("Depth", "1");
|
||||
|
||||
@Cleanup CloseableHttpResponse response = httpClient.execute(report, context);
|
||||
if (response == null)
|
||||
throw new DavNoContentException();
|
||||
|
||||
checkResponse(response);
|
||||
processMultiStatus(response);
|
||||
}
|
||||
|
||||
|
||||
/* resource operations */
|
||||
|
||||
public void get(String acceptedMimeTypes) throws URISyntaxException, IOException, HttpException, DavException {
|
||||
HttpGetHC4 get = new HttpGetHC4(location);
|
||||
get.addHeader("Accept", acceptedMimeTypes);
|
||||
|
||||
@Cleanup CloseableHttpResponse response = httpClient.execute(get, context);
|
||||
checkResponse(response);
|
||||
|
||||
HttpEntity entity = response.getEntity();
|
||||
if (entity == null)
|
||||
throw new DavNoContentException();
|
||||
|
||||
properties.contentType = ContentType.get(entity);
|
||||
content = EntityUtilsHC4.toByteArray(entity);
|
||||
}
|
||||
|
||||
// returns the ETag of the created/updated resource, if available (null otherwise)
|
||||
public String put(byte[] data, PutMode mode) throws URISyntaxException, IOException, HttpException {
|
||||
HttpPutHC4 put = new HttpPutHC4(location);
|
||||
put.setEntity(new ByteArrayEntityHC4(data));
|
||||
|
||||
switch (mode) {
|
||||
case ADD_DONT_OVERWRITE:
|
||||
put.addHeader("If-None-Match", "*");
|
||||
break;
|
||||
case UPDATE_DONT_OVERWRITE:
|
||||
put.addHeader("If-Match", (properties.eTag != null) ? properties.eTag : "*");
|
||||
break;
|
||||
}
|
||||
|
||||
if (properties.contentType != null)
|
||||
put.addHeader("Content-Type", properties.contentType.toString());
|
||||
|
||||
@Cleanup CloseableHttpResponse response = httpClient.execute(put, context);
|
||||
checkResponse(response);
|
||||
|
||||
Header eTag = response.getLastHeader("ETag");
|
||||
if (eTag != null)
|
||||
return eTag.getValue();
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public void delete() throws URISyntaxException, IOException, HttpException {
|
||||
HttpDeleteHC4 delete = new HttpDeleteHC4(location);
|
||||
|
||||
if (properties.eTag != null)
|
||||
delete.addHeader("If-Match", properties.eTag);
|
||||
|
||||
@Cleanup CloseableHttpResponse response = httpClient.execute(delete, context);
|
||||
checkResponse(response);
|
||||
}
|
||||
|
||||
|
||||
/* 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) {
|
||||
// Content-Location was set, update location correspondingly
|
||||
location = location.resolve(contentLocationHdr.getValue());
|
||||
Log.d(TAG, "Set Content-Location to " + location);
|
||||
}
|
||||
}
|
||||
|
||||
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_FORBIDDEN:
|
||||
throw new ForbiddenException(reason);
|
||||
case HttpStatus.SC_NOT_FOUND:
|
||||
throw new NotFoundException(reason);
|
||||
case HttpStatus.SC_CONFLICT:
|
||||
throw new ConflictException(reason);
|
||||
case HttpStatus.SC_PRECONDITION_FAILED:
|
||||
throw new PreconditionFailedException(reason);
|
||||
default:
|
||||
throw new HttpException(code, reason);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a 207 Multi-status response as defined in RFC 4918 "13. Multi-Status Response"
|
||||
*/
|
||||
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();
|
||||
|
||||
properties.contentType = ContentType.get(entity);
|
||||
@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
|
||||
return;
|
||||
|
||||
// member list will be built from response
|
||||
List<WebDavResource> members = new LinkedList<>();
|
||||
|
||||
// iterate through all resources (either ourselves or member)
|
||||
for (DavResponse singleResponse : multiStatus.response) {
|
||||
URI href;
|
||||
try {
|
||||
href = location.resolve(URIUtils.parseURI(singleResponse.href.href, false));
|
||||
} catch(Exception 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
|
||||
Properties properties = new Properties();
|
||||
byte[] data = null;
|
||||
|
||||
// in <response>, either <status> or <propstat> must be present
|
||||
if (singleResponse.status != null) { // method 1 (status of resource as a whole)
|
||||
StatusLine status = BasicLineParserHC4.parseStatusLine(singleResponse.status, new BasicLineParserHC4());
|
||||
checkResponse(status);
|
||||
|
||||
} else for (DavPropstat singlePropstat : singleResponse.propstat) { // method 2 (propstat)
|
||||
StatusLine status = BasicLineParserHC4.parseStatusLine(singlePropstat.status, new BasicLineParserHC4());
|
||||
|
||||
// ignore information about missing properties etc.
|
||||
if (status.getStatusCode()/100 != 1 && status.getStatusCode()/100 != 2)
|
||||
continue;
|
||||
|
||||
DavProp prop = singlePropstat.prop;
|
||||
properties.process(prop);
|
||||
|
||||
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 (properties.isCollection) // ensure trailing slashs for collections
|
||||
href = URIUtils.ensureTrailingSlash(href);
|
||||
|
||||
if (location.equals(href) || URIUtils.ensureTrailingSlash(location).equals(href)) { // about ourselves
|
||||
this.properties = properties;
|
||||
this.content = data;
|
||||
|
||||
} else { // about a member
|
||||
WebDavResource member = new WebDavResource(this, href);
|
||||
member.properties = properties;
|
||||
member.content = data;
|
||||
|
||||
members.add(member);
|
||||
}
|
||||
}
|
||||
|
||||
this.members = members;
|
||||
}
|
||||
|
||||
|
||||
public static class Properties {
|
||||
// DAV properties
|
||||
protected String
|
||||
currentUserPrincipal,
|
||||
addressBookHomeset,
|
||||
calendarHomeset,
|
||||
color;
|
||||
|
||||
@Getter protected String
|
||||
displayName,
|
||||
description,
|
||||
timeZone,
|
||||
eTag,
|
||||
cTag;
|
||||
|
||||
@Getter @Setter protected ContentType contentType;
|
||||
|
||||
@Getter protected boolean
|
||||
readOnly,
|
||||
isCollection,
|
||||
isCalendar,
|
||||
isAddressBook;
|
||||
|
||||
@Getter protected List<String> supportedComponents;
|
||||
@Getter protected VCardVersion supportedVCardVersion;
|
||||
|
||||
// fill from DavProp
|
||||
|
||||
protected void process(DavProp prop) {
|
||||
if (prop.currentUserPrincipal != null && prop.currentUserPrincipal.getHref() != null)
|
||||
currentUserPrincipal = 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))
|
||||
readOnly = true;
|
||||
}
|
||||
|
||||
if (prop.addressbookHomeSet != null && prop.addressbookHomeSet.getHref() != null)
|
||||
addressBookHomeset = URIUtils.ensureTrailingSlash(prop.addressbookHomeSet.getHref().href);
|
||||
|
||||
if (prop.calendarHomeSet != null && prop.calendarHomeSet.getHref() != null)
|
||||
calendarHomeset = URIUtils.ensureTrailingSlash(prop.calendarHomeSet.getHref().href);
|
||||
|
||||
if (prop.displayname != null)
|
||||
displayName = prop.displayname.getDisplayName();
|
||||
|
||||
if (prop.resourcetype != null) {
|
||||
if (prop.resourcetype.getCollection() != null)
|
||||
isCollection = true;
|
||||
if (prop.resourcetype.getAddressbook() != null) { // CardDAV collection properties
|
||||
isAddressBook = true;
|
||||
|
||||
if (prop.addressbookDescription != null)
|
||||
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()))
|
||||
supportedVCardVersion = VCardVersion.V4_0;
|
||||
}
|
||||
if (prop.resourcetype.getCalendar() != null) { // CalDAV collection propertioes
|
||||
isCalendar = true;
|
||||
|
||||
if (prop.calendarDescription != null)
|
||||
description = prop.calendarDescription.getDescription();
|
||||
|
||||
if (prop.calendarColor != null)
|
||||
color = prop.calendarColor.getColor();
|
||||
|
||||
if (prop.calendarTimezone != null)
|
||||
timeZone = prop.calendarTimezone.getTimezone();
|
||||
|
||||
if (prop.supportedCalendarComponentSet != null) {
|
||||
supportedComponents = new LinkedList<>();
|
||||
for (Comp component : prop.supportedCalendarComponentSet)
|
||||
supportedComponents.add(component.getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (prop.getctag != null)
|
||||
cTag = prop.getctag.getCTag();
|
||||
|
||||
if (prop.getetag != null)
|
||||
eTag = prop.getetag.getETag();
|
||||
}
|
||||
|
||||
// getters / setters
|
||||
|
||||
public Integer getColor() {
|
||||
return color != null ? DavUtils.CalDAVtoARGBColor(color) : null;
|
||||
}
|
||||
|
||||
public URI getCurrentUserPrincipal() throws URISyntaxException {
|
||||
return currentUserPrincipal != null ? URIUtils.parseURI(currentUserPrincipal, false) : null;
|
||||
}
|
||||
|
||||
public URI getAddressbookHomeSet() throws URISyntaxException {
|
||||
return addressBookHomeset != null ? URIUtils.parseURI(addressBookHomeset, false) : null;
|
||||
}
|
||||
|
||||
public URI getCalendarHomeSet() throws URISyntaxException {
|
||||
return calendarHomeset != null ? URIUtils.parseURI(calendarHomeset, false) : null;
|
||||
}
|
||||
|
||||
public String getTimeZone() {
|
||||
return timeZone != null ? iCalendar.TimezoneDefToTzId(timeZone) : null;
|
||||
}
|
||||
|
||||
public void invalidateCTag() {
|
||||
cTag = null;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -180,15 +180,16 @@
|
||||
<string name="sync_error_title">Synchronisierung von %s fehlgeschlagen</string>
|
||||
<string name="sync_error_http">HTTP-Fehler beim %1$s</string>
|
||||
<string-array name="sync_error_phases">
|
||||
<item>Vorbereiten der Synchronisierung</item>
|
||||
<item>Abfragen der Server-Fähigkeiten</item>
|
||||
<item>Verarbeiten lokal gelöschter Einträge</item>
|
||||
<item>Vorbereiten neuer lokaler Einträge</item>
|
||||
<item>Hochladen neuer/geänderter lokaler Einträge</item>
|
||||
<item>Abfragen des Sync.-Zustands</item>
|
||||
<item>Abfragen des Synchronisierungs-Zustands</item>
|
||||
<item>Auflisten lokaler Einträge</item>
|
||||
<item>Auflisten der Server-Einträge</item>
|
||||
<item>Herunterladen von Server-Einträgen</item>
|
||||
<item>Speichern des Sync.-Zustands</item>
|
||||
<item>Speichern des Synchronisierungs-Zustands</item>
|
||||
</string-array>
|
||||
|
||||
</resources>
|
@ -195,6 +195,7 @@
|
||||
<string name="sync_error_title">Synchronization of %s failed</string>
|
||||
<string name="sync_error_http">HTTP error while %1$s</string>
|
||||
<string-array name="sync_error_phases">
|
||||
<item>preparing synchronization</item>
|
||||
<item>querying capabilities</item>
|
||||
<item>processing locally deleted entries</item>
|
||||
<item>preparing locally created entries</item>
|
||||
|
Loading…
Reference in New Issue
Block a user