Commit fa4f090f authored by Ricki Hirner's avatar Ricki Hirner 🐑

Remove legacy calendar/task/WebDAV code

parent c71dc2ba
......@@ -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");
}
/*
* 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;
}
}
/*
* 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;
}
}
/*
* 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();
}
}
/*
* 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();
}
}
/*
* 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);
}
}
/*
* 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);
}
}
/*
* 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;
}
}
/*
* 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);