From d348f54deb4ad6a5f348d96675ba09fa81e8e5bf Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Wed, 14 Oct 2015 13:38:18 +0200 Subject: [PATCH] Remove legacy calendar/task/WebDAV code --- app/build.gradle | 22 +- .../java/at/bitfire/davdroid/Constants.java | 4 +- .../java/at/bitfire/davdroid/DateUtils.java | 166 ---- .../java/at/bitfire/davdroid/URIUtils.java | 80 -- .../davdroid/resource/CalDavCalendar.java | 78 -- .../davdroid/resource/CalDavTaskList.java | 80 -- .../at/bitfire/davdroid/resource/Event.java | 353 -------- .../davdroid/resource/LocalCalendar.java | 786 ------------------ .../davdroid/resource/LocalCollection.java | 430 ---------- .../resource/LocalStorageException.java | 31 - .../davdroid/resource/LocalTaskList.java | 395 --------- .../resource/RecordNotFoundException.java | 24 - .../bitfire/davdroid/resource/Resource.java | 65 -- .../at/bitfire/davdroid/resource/Task.java | 223 ----- .../davdroid/resource/WebDavCollection.java | 226 ----- .../bitfire/davdroid/resource/iCalendar.java | 169 ---- .../CalendarsSyncAdapterService.java | 54 +- .../ContactsSyncAdapterService.java | 389 +-------- .../syncadapter/ContactsSyncManager.java | 380 +++++++++ .../davdroid/syncadapter/DavSyncAdapter.java | 204 ----- .../davdroid/syncadapter/SyncManager.java | 345 ++++---- .../syncadapter/TasksSyncAdapterService.java | 51 +- .../davdroid/ui/settings/AccountFragment.java | 5 +- .../ui/setup/AccountDetailsFragment.java | 16 +- .../davdroid/ui/setup/LoginURLFragment.java | 5 +- .../ui/setup/QueryServerDialogFragment.java | 10 +- .../ui/setup/SelectCollectionsAdapter.java | 3 +- .../davdroid/webdav/ConflictException.java | 17 - .../webdav/DavAddressbookMultiget.java | 21 - .../davdroid/webdav/DavCalendarMultiget.java | 21 - .../davdroid/webdav/DavCalendarQuery.java | 31 - .../davdroid/webdav/DavCompFilter.java | 29 - .../bitfire/davdroid/webdav/DavException.java | 25 - .../at/bitfire/davdroid/webdav/DavFilter.java | 21 - .../at/bitfire/davdroid/webdav/DavHref.java | 26 - .../davdroid/webdav/DavHttpClient.java | 71 -- .../webdav/DavHttpRequestRetryHandler.java | 34 - .../webdav/DavIncapableException.java | 18 - .../bitfire/davdroid/webdav/DavMultiget.java | 57 -- .../davdroid/webdav/DavMultistatus.java | 22 - .../webdav/DavNoContentException.java | 18 - .../webdav/DavNoMultiStatusException.java | 18 - .../at/bitfire/davdroid/webdav/DavProp.java | 218 ----- .../bitfire/davdroid/webdav/DavPropfind.java | 19 - .../bitfire/davdroid/webdav/DavPropstat.java | 20 - .../davdroid/webdav/DavRedirectStrategy.java | 108 --- .../bitfire/davdroid/webdav/DavResponse.java | 26 - .../davdroid/webdav/ForbiddenException.java | 19 - .../davdroid/webdav/HttpException.java | 26 - .../bitfire/davdroid/webdav/HttpPropfind.java | 105 --- .../bitfire/davdroid/webdav/HttpReport.java | 46 - .../webdav/NotAuthorizedException.java | 19 - .../davdroid/webdav/NotFoundException.java | 18 - .../webdav/PreconditionFailedException.java | 18 - .../davdroid/webdav/TlsSniSocketFactory.java | 108 --- .../davdroid/webdav/WebDavResource.java | 574 ------------- app/src/main/res/values-de/strings.xml | 5 +- app/src/main/res/values/strings.xml | 1 + 58 files changed, 581 insertions(+), 5772 deletions(-) delete mode 100644 app/src/main/java/at/bitfire/davdroid/DateUtils.java delete mode 100644 app/src/main/java/at/bitfire/davdroid/URIUtils.java delete mode 100644 app/src/main/java/at/bitfire/davdroid/resource/CalDavCalendar.java delete mode 100644 app/src/main/java/at/bitfire/davdroid/resource/CalDavTaskList.java delete mode 100644 app/src/main/java/at/bitfire/davdroid/resource/Event.java delete mode 100644 app/src/main/java/at/bitfire/davdroid/resource/LocalCalendar.java delete mode 100644 app/src/main/java/at/bitfire/davdroid/resource/LocalCollection.java delete mode 100644 app/src/main/java/at/bitfire/davdroid/resource/LocalStorageException.java delete mode 100644 app/src/main/java/at/bitfire/davdroid/resource/LocalTaskList.java delete mode 100644 app/src/main/java/at/bitfire/davdroid/resource/RecordNotFoundException.java delete mode 100644 app/src/main/java/at/bitfire/davdroid/resource/Resource.java delete mode 100644 app/src/main/java/at/bitfire/davdroid/resource/Task.java delete mode 100644 app/src/main/java/at/bitfire/davdroid/resource/WebDavCollection.java delete mode 100644 app/src/main/java/at/bitfire/davdroid/resource/iCalendar.java create mode 100644 app/src/main/java/at/bitfire/davdroid/syncadapter/ContactsSyncManager.java delete mode 100644 app/src/main/java/at/bitfire/davdroid/syncadapter/DavSyncAdapter.java delete mode 100644 app/src/main/java/at/bitfire/davdroid/webdav/ConflictException.java delete mode 100644 app/src/main/java/at/bitfire/davdroid/webdav/DavAddressbookMultiget.java delete mode 100644 app/src/main/java/at/bitfire/davdroid/webdav/DavCalendarMultiget.java delete mode 100644 app/src/main/java/at/bitfire/davdroid/webdav/DavCalendarQuery.java delete mode 100644 app/src/main/java/at/bitfire/davdroid/webdav/DavCompFilter.java delete mode 100644 app/src/main/java/at/bitfire/davdroid/webdav/DavException.java delete mode 100644 app/src/main/java/at/bitfire/davdroid/webdav/DavFilter.java delete mode 100644 app/src/main/java/at/bitfire/davdroid/webdav/DavHref.java delete mode 100644 app/src/main/java/at/bitfire/davdroid/webdav/DavHttpClient.java delete mode 100644 app/src/main/java/at/bitfire/davdroid/webdav/DavHttpRequestRetryHandler.java delete mode 100644 app/src/main/java/at/bitfire/davdroid/webdav/DavIncapableException.java delete mode 100644 app/src/main/java/at/bitfire/davdroid/webdav/DavMultiget.java delete mode 100644 app/src/main/java/at/bitfire/davdroid/webdav/DavMultistatus.java delete mode 100644 app/src/main/java/at/bitfire/davdroid/webdav/DavNoContentException.java delete mode 100644 app/src/main/java/at/bitfire/davdroid/webdav/DavNoMultiStatusException.java delete mode 100644 app/src/main/java/at/bitfire/davdroid/webdav/DavProp.java delete mode 100644 app/src/main/java/at/bitfire/davdroid/webdav/DavPropfind.java delete mode 100644 app/src/main/java/at/bitfire/davdroid/webdav/DavPropstat.java delete mode 100644 app/src/main/java/at/bitfire/davdroid/webdav/DavRedirectStrategy.java delete mode 100644 app/src/main/java/at/bitfire/davdroid/webdav/DavResponse.java delete mode 100644 app/src/main/java/at/bitfire/davdroid/webdav/ForbiddenException.java delete mode 100644 app/src/main/java/at/bitfire/davdroid/webdav/HttpException.java delete mode 100644 app/src/main/java/at/bitfire/davdroid/webdav/HttpPropfind.java delete mode 100644 app/src/main/java/at/bitfire/davdroid/webdav/HttpReport.java delete mode 100644 app/src/main/java/at/bitfire/davdroid/webdav/NotAuthorizedException.java delete mode 100644 app/src/main/java/at/bitfire/davdroid/webdav/NotFoundException.java delete mode 100644 app/src/main/java/at/bitfire/davdroid/webdav/PreconditionFailedException.java delete mode 100644 app/src/main/java/at/bitfire/davdroid/webdav/TlsSniSocketFactory.java delete mode 100644 app/src/main/java/at/bitfire/davdroid/webdav/WebDavResource.java diff --git a/app/build.gradle b/app/build.gradle index d93134a0..9132d7c8 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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') diff --git a/app/src/main/java/at/bitfire/davdroid/Constants.java b/app/src/main/java/at/bitfire/davdroid/Constants.java index 5131d6fc..77dc2ec0 100644 --- a/app/src/main/java/at/bitfire/davdroid/Constants.java +++ b/app/src/main/java/at/bitfire/davdroid/Constants.java @@ -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"); } diff --git a/app/src/main/java/at/bitfire/davdroid/DateUtils.java b/app/src/main/java/at/bitfire/davdroid/DateUtils.java deleted file mode 100644 index c48002fc..00000000 --- a/app/src/main/java/at/bitfire/davdroid/DateUtils.java +++ /dev/null @@ -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 dates, boolean allDay) throws ParseException { - List 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 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 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; - } - -} diff --git a/app/src/main/java/at/bitfire/davdroid/URIUtils.java b/app/src/main/java/at/bitfire/davdroid/URIUtils.java deleted file mode 100644 index 8a6337ed..00000000 --- a/app/src/main/java/at/bitfire/davdroid/URIUtils.java +++ /dev/null @@ -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; - } - -} diff --git a/app/src/main/java/at/bitfire/davdroid/resource/CalDavCalendar.java b/app/src/main/java/at/bitfire/davdroid/resource/CalDavCalendar.java deleted file mode 100644 index f5ecc00a..00000000 --- a/app/src/main/java/at/bitfire/davdroid/resource/CalDavCalendar.java +++ /dev/null @@ -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 { - 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(); - } -} diff --git a/app/src/main/java/at/bitfire/davdroid/resource/CalDavTaskList.java b/app/src/main/java/at/bitfire/davdroid/resource/CalDavTaskList.java deleted file mode 100644 index 5a6ccab3..00000000 --- a/app/src/main/java/at/bitfire/davdroid/resource/CalDavTaskList.java +++ /dev/null @@ -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 { - 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(); - } - -} diff --git a/app/src/main/java/at/bitfire/davdroid/resource/Event.java b/app/src/main/java/at/bitfire/davdroid/resource/Event.java deleted file mode 100644 index b982973c..00000000 --- a/app/src/main/java/at/bitfire/davdroid/resource/Event.java +++ /dev/null @@ -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 rdates = new LinkedList<>(); - protected RRule rrule; - @Getter private List exdates = new LinkedList<>(); - protected ExRule exrule; - @Getter private List exceptions = new LinkedList<>(); - - protected Boolean forPublic; - protected Status status; - - protected boolean opaque; - - protected Organizer organizer; - @Getter private List attendees = new LinkedList<>(); - - @Getter private List 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)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)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)(List)event.getProperties(Property.RDATE)) - rdates.add(rdate); - exrule = (ExRule)event.getProperty(Property.EXRULE); - for (ExDate exdate : (List)(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)(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 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); - } - } - -} diff --git a/app/src/main/java/at/bitfire/davdroid/resource/LocalCalendar.java b/app/src/main/java/at/bitfire/davdroid/resource/LocalCalendar.java deleted file mode 100644 index 046c6ecc..00000000 --- a/app/src/main/java/at/bitfire/davdroid/resource/LocalCalendar.java +++ /dev/null @@ -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 { - 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 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 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 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); - } - -} diff --git a/app/src/main/java/at/bitfire/davdroid/resource/LocalCollection.java b/app/src/main/java/at/bitfire/davdroid/resource/LocalCollection.java deleted file mode 100644 index 26cfe82f..00000000 --- a/app/src/main/java/at/bitfire/davdroid/resource/LocalCollection.java +++ /dev/null @@ -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 Subtype of Resource that can be stored in the collection - */ -public abstract class LocalCollection { - private static final String TAG = "davdroid.Collection"; - - final protected Account account; - final protected ContentProviderClient providerClient; - final protected ArrayList 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 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); -} diff --git a/app/src/main/java/at/bitfire/davdroid/resource/LocalStorageException.java b/app/src/main/java/at/bitfire/davdroid/resource/LocalStorageException.java deleted file mode 100644 index f3516367..00000000 --- a/app/src/main/java/at/bitfire/davdroid/resource/LocalStorageException.java +++ /dev/null @@ -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); - } -} diff --git a/app/src/main/java/at/bitfire/davdroid/resource/LocalTaskList.java b/app/src/main/java/at/bitfire/davdroid/resource/LocalTaskList.java deleted file mode 100644 index 4bc7df74..00000000 --- a/app/src/main/java/at/bitfire/davdroid/resource/LocalTaskList.java +++ /dev/null @@ -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 { - 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 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(); - } -} diff --git a/app/src/main/java/at/bitfire/davdroid/resource/RecordNotFoundException.java b/app/src/main/java/at/bitfire/davdroid/resource/RecordNotFoundException.java deleted file mode 100644 index b2e173c2..00000000 --- a/app/src/main/java/at/bitfire/davdroid/resource/RecordNotFoundException.java +++ /dev/null @@ -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); - } - -} diff --git a/app/src/main/java/at/bitfire/davdroid/resource/Resource.java b/app/src/main/java/at/bitfire/davdroid/resource/Resource.java deleted file mode 100644 index b46cb087..00000000 --- a/app/src/main/java/at/bitfire/davdroid/resource/Resource.java +++ /dev/null @@ -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; - } -} diff --git a/app/src/main/java/at/bitfire/davdroid/resource/Task.java b/app/src/main/java/at/bitfire/davdroid/resource/Task.java deleted file mode 100644 index 2c1bba2e..00000000 --- a/app/src/main/java/at/bitfire/davdroid/resource/Task.java +++ /dev/null @@ -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 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; - } - -} diff --git a/app/src/main/java/at/bitfire/davdroid/resource/WebDavCollection.java b/app/src/main/java/at/bitfire/davdroid/resource/WebDavCollection.java deleted file mode 100644 index bcb761fb..00000000 --- a/app/src/main/java/at/bitfire/davdroid/resource/WebDavCollection.java +++ /dev/null @@ -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 Subtype of Resource that can be stored in the collection - */ -public abstract class WebDavCollection { - 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 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 names = new LinkedList<>(); - for (Resource resource : resources) - names.add(resource.getName()); - - LinkedList 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); - } - }; - } -} diff --git a/app/src/main/java/at/bitfire/davdroid/resource/iCalendar.java b/app/src/main/java/at/bitfire/davdroid/resource/iCalendar.java deleted file mode 100644 index 8d8ec766..00000000 --- a/app/src/main/java/at/bitfire/davdroid/resource/iCalendar.java +++ /dev/null @@ -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); - } - } - } - - -} diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/CalendarsSyncAdapterService.java b/app/src/main/java/at/bitfire/davdroid/syncadapter/CalendarsSyncAdapterService.java index 5f7e03bd..baaf3d52 100644 --- a/app/src/main/java/at/bitfire/davdroid/syncadapter/CalendarsSyncAdapterService.java +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/CalendarsSyncAdapterService.java @@ -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, 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, 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; - } - } } diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/ContactsSyncAdapterService.java b/app/src/main/java/at/bitfire/davdroid/syncadapter/ContactsSyncAdapterService.java index 26389ae1..a82904a5 100644 --- a/app/src/main/java/at/bitfire/davdroid/syncadapter/ContactsSyncAdapterService.java +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/ContactsSyncAdapterService.java @@ -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 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 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 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 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, MaplocalContacts, 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; - } - } - } diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/ContactsSyncManager.java b/app/src/main/java/at/bitfire/davdroid/syncadapter/ContactsSyncManager.java new file mode 100644 index 00000000..02c3e29a --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/ContactsSyncManager.java @@ -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 localContacts; + Map remoteContacts; + Set 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 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, MaplocalContacts, 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; + } + } + +} diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/DavSyncAdapter.java b/app/src/main/java/at/bitfire/davdroid/syncadapter/DavSyncAdapter.java deleted file mode 100644 index d124c8bf..00000000 --- a/app/src/main/java/at/bitfire/davdroid/syncadapter/DavSyncAdapter.java +++ /dev/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() { - @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, 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, WebDavCollection> syncCollections = getSyncPairs(account, provider); - if (syncCollections == null) - Log.i(TAG, "Nothing to synchronize"); - else - try { - for (Map.Entry, 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); - } - -} diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/SyncManager.java b/app/src/main/java/at/bitfire/davdroid/syncadapter/SyncManager.java index b4ca1aa1..22abccbe 100644 --- a/app/src/main/java/at/bitfire/davdroid/syncadapter/SyncManager.java +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/SyncManager.java @@ -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 local; - final protected WebDavCollection remote; - - - public SyncManager(LocalCollection local, WebDavCollection 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 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; } diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/TasksSyncAdapterService.java b/app/src/main/java/at/bitfire/davdroid/syncadapter/TasksSyncAdapterService.java index 5bc4152d..751dcba8 100644 --- a/app/src/main/java/at/bitfire/davdroid/syncadapter/TasksSyncAdapterService.java +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/TasksSyncAdapterService.java @@ -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, 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, 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; - } - } } diff --git a/app/src/main/java/at/bitfire/davdroid/ui/settings/AccountFragment.java b/app/src/main/java/at/bitfire/davdroid/ui/settings/AccountFragment.java index 017c87a8..e332e6fe 100644 --- a/app/src/main/java/at/bitfire/davdroid/ui/settings/AccountFragment.java +++ b/app/src/main/java/at/bitfire/davdroid/ui/settings/AccountFragment.java @@ -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"); diff --git a/app/src/main/java/at/bitfire/davdroid/ui/setup/AccountDetailsFragment.java b/app/src/main/java/at/bitfire/davdroid/ui/setup/AccountDetailsFragment.java index fa46f0cf..b068db4c 100644 --- a/app/src/main/java/at/bitfire/davdroid/ui/setup/AccountDetailsFragment.java +++ b/app/src/main/java/at/bitfire/davdroid/ui/setup/AccountDetailsFragment.java @@ -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 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(); } diff --git a/app/src/main/java/at/bitfire/davdroid/ui/setup/LoginURLFragment.java b/app/src/main/java/at/bitfire/davdroid/ui/setup/LoginURLFragment.java index 547a6c9b..b0fd599a 100644 --- a/app/src/main/java/at/bitfire/davdroid/ui/setup/LoginURLFragment.java +++ b/app/src/main/java/at/bitfire/davdroid/ui/setup/LoginURLFragment.java @@ -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) { } diff --git a/app/src/main/java/at/bitfire/davdroid/ui/setup/QueryServerDialogFragment.java b/app/src/main/java/at/bitfire/davdroid/ui/setup/QueryServerDialogFragment.java index 02d7654e..d06e9e5d 100644 --- a/app/src/main/java/at/bitfire/davdroid/ui/setup/QueryServerDialogFragment.java +++ b/app/src/main/java/at/bitfire/davdroid/ui/setup/QueryServerDialogFragment.java @@ -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 { @@ -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())); diff --git a/app/src/main/java/at/bitfire/davdroid/ui/setup/SelectCollectionsAdapter.java b/app/src/main/java/at/bitfire/davdroid/ui/setup/SelectCollectionsAdapter.java index 15998028..992e8239 100644 --- a/app/src/main/java/at/bitfire/davdroid/ui/setup/SelectCollectionsAdapter.java +++ b/app/src/main/java/at/bitfire/davdroid/ui/setup/SelectCollectionsAdapter.java @@ -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() { diff --git a/app/src/main/java/at/bitfire/davdroid/webdav/ConflictException.java b/app/src/main/java/at/bitfire/davdroid/webdav/ConflictException.java deleted file mode 100644 index 2ce29336..00000000 --- a/app/src/main/java/at/bitfire/davdroid/webdav/ConflictException.java +++ /dev/null @@ -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); - } -} diff --git a/app/src/main/java/at/bitfire/davdroid/webdav/DavAddressbookMultiget.java b/app/src/main/java/at/bitfire/davdroid/webdav/DavAddressbookMultiget.java deleted file mode 100644 index fdb1b44b..00000000 --- a/app/src/main/java/at/bitfire/davdroid/webdav/DavAddressbookMultiget.java +++ /dev/null @@ -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 { -} diff --git a/app/src/main/java/at/bitfire/davdroid/webdav/DavCalendarMultiget.java b/app/src/main/java/at/bitfire/davdroid/webdav/DavCalendarMultiget.java deleted file mode 100644 index 9ea95b51..00000000 --- a/app/src/main/java/at/bitfire/davdroid/webdav/DavCalendarMultiget.java +++ /dev/null @@ -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 { -} diff --git a/app/src/main/java/at/bitfire/davdroid/webdav/DavCalendarQuery.java b/app/src/main/java/at/bitfire/davdroid/webdav/DavCalendarQuery.java deleted file mode 100644 index 2c0ff88e..00000000 --- a/app/src/main/java/at/bitfire/davdroid/webdav/DavCalendarQuery.java +++ /dev/null @@ -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; -} diff --git a/app/src/main/java/at/bitfire/davdroid/webdav/DavCompFilter.java b/app/src/main/java/at/bitfire/davdroid/webdav/DavCompFilter.java deleted file mode 100644 index a9b66628..00000000 --- a/app/src/main/java/at/bitfire/davdroid/webdav/DavCompFilter.java +++ /dev/null @@ -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; -} diff --git a/app/src/main/java/at/bitfire/davdroid/webdav/DavException.java b/app/src/main/java/at/bitfire/davdroid/webdav/DavException.java deleted file mode 100644 index 4ad0efb7..00000000 --- a/app/src/main/java/at/bitfire/davdroid/webdav/DavException.java +++ /dev/null @@ -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); - } -} diff --git a/app/src/main/java/at/bitfire/davdroid/webdav/DavFilter.java b/app/src/main/java/at/bitfire/davdroid/webdav/DavFilter.java deleted file mode 100644 index a9dd7f38..00000000 --- a/app/src/main/java/at/bitfire/davdroid/webdav/DavFilter.java +++ /dev/null @@ -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; -} diff --git a/app/src/main/java/at/bitfire/davdroid/webdav/DavHref.java b/app/src/main/java/at/bitfire/davdroid/webdav/DavHref.java deleted file mode 100644 index bebe47eb..00000000 --- a/app/src/main/java/at/bitfire/davdroid/webdav/DavHref.java +++ /dev/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.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; - } -} diff --git a/app/src/main/java/at/bitfire/davdroid/webdav/DavHttpClient.java b/app/src/main/java/at/bitfire/davdroid/webdav/DavHttpClient.java deleted file mode 100644 index 26ab951c..00000000 --- a/app/src/main/java/at/bitfire/davdroid/webdav/DavHttpClient.java +++ /dev/null @@ -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 socketFactoryRegistry; - - static { - socketFactoryRegistry = RegistryBuilder. 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(); - } - -} diff --git a/app/src/main/java/at/bitfire/davdroid/webdav/DavHttpRequestRetryHandler.java b/app/src/main/java/at/bitfire/davdroid/webdav/DavHttpRequestRetryHandler.java deleted file mode 100644 index 346f1db7..00000000 --- a/app/src/main/java/at/bitfire/davdroid/webdav/DavHttpRequestRetryHandler.java +++ /dev/null @@ -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); - } -} diff --git a/app/src/main/java/at/bitfire/davdroid/webdav/DavIncapableException.java b/app/src/main/java/at/bitfire/davdroid/webdav/DavIncapableException.java deleted file mode 100644 index c3bcf995..00000000 --- a/app/src/main/java/at/bitfire/davdroid/webdav/DavIncapableException.java +++ /dev/null @@ -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); - } -} diff --git a/app/src/main/java/at/bitfire/davdroid/webdav/DavMultiget.java b/app/src/main/java/at/bitfire/davdroid/webdav/DavMultiget.java deleted file mode 100644 index 21a28e3b..00000000 --- a/app/src/main/java/at/bitfire/davdroid/webdav/DavMultiget.java +++ /dev/null @@ -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 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; - } -} diff --git a/app/src/main/java/at/bitfire/davdroid/webdav/DavMultistatus.java b/app/src/main/java/at/bitfire/davdroid/webdav/DavMultistatus.java deleted file mode 100644 index e453c2cb..00000000 --- a/app/src/main/java/at/bitfire/davdroid/webdav/DavMultistatus.java +++ /dev/null @@ -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 response; -} diff --git a/app/src/main/java/at/bitfire/davdroid/webdav/DavNoContentException.java b/app/src/main/java/at/bitfire/davdroid/webdav/DavNoContentException.java deleted file mode 100644 index bd00414f..00000000 --- a/app/src/main/java/at/bitfire/davdroid/webdav/DavNoContentException.java +++ /dev/null @@ -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); - } -} diff --git a/app/src/main/java/at/bitfire/davdroid/webdav/DavNoMultiStatusException.java b/app/src/main/java/at/bitfire/davdroid/webdav/DavNoMultiStatusException.java deleted file mode 100644 index 81db350b..00000000 --- a/app/src/main/java/at/bitfire/davdroid/webdav/DavNoMultiStatusException.java +++ /dev/null @@ -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); - } -} diff --git a/app/src/main/java/at/bitfire/davdroid/webdav/DavProp.java b/app/src/main/java/at/bitfire/davdroid/webdav/DavProp.java deleted file mode 100644 index 08eeef33..00000000 --- a/app/src/main/java/at/bitfire/davdroid/webdav/DavProp.java +++ /dev/null @@ -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 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 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 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; - } -} diff --git a/app/src/main/java/at/bitfire/davdroid/webdav/DavPropfind.java b/app/src/main/java/at/bitfire/davdroid/webdav/DavPropfind.java deleted file mode 100644 index 7942aac8..00000000 --- a/app/src/main/java/at/bitfire/davdroid/webdav/DavPropfind.java +++ /dev/null @@ -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; -} diff --git a/app/src/main/java/at/bitfire/davdroid/webdav/DavPropstat.java b/app/src/main/java/at/bitfire/davdroid/webdav/DavPropstat.java deleted file mode 100644 index 52fd4638..00000000 --- a/app/src/main/java/at/bitfire/davdroid/webdav/DavPropstat.java +++ /dev/null @@ -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; -} diff --git a/app/src/main/java/at/bitfire/davdroid/webdav/DavRedirectStrategy.java b/app/src/main/java/at/bitfire/davdroid/webdav/DavRedirectStrategy.java deleted file mode 100644 index 93430338..00000000 --- a/app/src/main/java/at/bitfire/davdroid/webdav/DavRedirectStrategy.java +++ /dev/null @@ -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; - } - -} diff --git a/app/src/main/java/at/bitfire/davdroid/webdav/DavResponse.java b/app/src/main/java/at/bitfire/davdroid/webdav/DavResponse.java deleted file mode 100644 index d172c9e9..00000000 --- a/app/src/main/java/at/bitfire/davdroid/webdav/DavResponse.java +++ /dev/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 propstat; -} diff --git a/app/src/main/java/at/bitfire/davdroid/webdav/ForbiddenException.java b/app/src/main/java/at/bitfire/davdroid/webdav/ForbiddenException.java deleted file mode 100644 index 599c12e7..00000000 --- a/app/src/main/java/at/bitfire/davdroid/webdav/ForbiddenException.java +++ /dev/null @@ -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); - } -} diff --git a/app/src/main/java/at/bitfire/davdroid/webdav/HttpException.java b/app/src/main/java/at/bitfire/davdroid/webdav/HttpException.java deleted file mode 100644 index 748ec0a0..00000000 --- a/app/src/main/java/at/bitfire/davdroid/webdav/HttpException.java +++ /dev/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 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; - } - -} diff --git a/app/src/main/java/at/bitfire/davdroid/webdav/HttpPropfind.java b/app/src/main/java/at/bitfire/davdroid/webdav/HttpPropfind.java deleted file mode 100644 index 6c7a552c..00000000 --- a/app/src/main/java/at/bitfire/davdroid/webdav/HttpPropfind.java +++ /dev/null @@ -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; - } -} diff --git a/app/src/main/java/at/bitfire/davdroid/webdav/HttpReport.java b/app/src/main/java/at/bitfire/davdroid/webdav/HttpReport.java deleted file mode 100644 index 2d9b6f12..00000000 --- a/app/src/main/java/at/bitfire/davdroid/webdav/HttpReport.java +++ /dev/null @@ -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; - } -} diff --git a/app/src/main/java/at/bitfire/davdroid/webdav/NotAuthorizedException.java b/app/src/main/java/at/bitfire/davdroid/webdav/NotAuthorizedException.java deleted file mode 100644 index e734bb1d..00000000 --- a/app/src/main/java/at/bitfire/davdroid/webdav/NotAuthorizedException.java +++ /dev/null @@ -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); - } - -} diff --git a/app/src/main/java/at/bitfire/davdroid/webdav/NotFoundException.java b/app/src/main/java/at/bitfire/davdroid/webdav/NotFoundException.java deleted file mode 100644 index 86add89e..00000000 --- a/app/src/main/java/at/bitfire/davdroid/webdav/NotFoundException.java +++ /dev/null @@ -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); - } -} diff --git a/app/src/main/java/at/bitfire/davdroid/webdav/PreconditionFailedException.java b/app/src/main/java/at/bitfire/davdroid/webdav/PreconditionFailedException.java deleted file mode 100644 index d9ee5540..00000000 --- a/app/src/main/java/at/bitfire/davdroid/webdav/PreconditionFailedException.java +++ /dev/null @@ -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); - } -} diff --git a/app/src/main/java/at/bitfire/davdroid/webdav/TlsSniSocketFactory.java b/app/src/main/java/at/bitfire/davdroid/webdav/TlsSniSocketFactory.java deleted file mode 100644 index 0eedf6f0..00000000 --- a/app/src/main/java/at/bitfire/davdroid/webdav/TlsSniSocketFactory.java +++ /dev/null @@ -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 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 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 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 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 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); - } - -} diff --git a/app/src/main/java/at/bitfire/davdroid/webdav/WebDavResource.java b/app/src/main/java/at/bitfire/davdroid/webdav/WebDavResource.java deleted file mode 100644 index b1f241e6..00000000 --- a/app/src/main/java/at/bitfire/davdroid/webdav/WebDavResource.java +++ /dev/null @@ -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 capabilities = new HashSet<>(), - methods = new HashSet<>(); - - // list of members (only for collections) - @Getter Properties properties = new Properties(); - @Getter protected List 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 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 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 , either or 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 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; - } - - } -} diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 6b0bd7e1..1252877b 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -180,15 +180,16 @@ Synchronisierung von %s fehlgeschlagen HTTP-Fehler beim %1$s + Vorbereiten der Synchronisierung Abfragen der Server-Fähigkeiten Verarbeiten lokal gelöschter Einträge Vorbereiten neuer lokaler Einträge Hochladen neuer/geänderter lokaler Einträge - Abfragen des Sync.-Zustands + Abfragen des Synchronisierungs-Zustands Auflisten lokaler Einträge Auflisten der Server-Einträge Herunterladen von Server-Einträgen - Speichern des Sync.-Zustands + Speichern des Synchronisierungs-Zustands \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 4e124ad2..75f70cea 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -195,6 +195,7 @@ Synchronization of %s failed HTTP error while %1$s + preparing synchronization querying capabilities processing locally deleted entries preparing locally created entries