From 1c461e9d132cf5c804d2166cc7e42202555c30ec Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Mon, 3 Aug 2015 12:16:40 +0200 Subject: [PATCH] Refactoring * WebDavResource: properties in separate subclass * improve time zone handling * always provide task list color --- .../at/bitfire/davdroid/DateUtilsTest.java | 4 - .../davdroid/resource/ContactTest.java | 2 +- .../bitfire/davdroid/resource/EventTest.java | 84 ++--- .../davdroid/resource/LocalCalendarTest.java | 11 - .../davdroid/resource/iCalendarTest.java | 95 ++++++ .../syncadapter/DavResourceFinderTest.java | 1 - .../davdroid/webdav/WebDavResourceTest.java | 37 +- .../robohydra/plugins/dav/index.js | 34 ++ .../java/at/bitfire/davdroid/DAVUtils.java | 5 +- .../java/at/bitfire/davdroid/DateUtils.java | 32 +- .../davdroid/resource/DavResourceFinder.java | 47 +-- .../at/bitfire/davdroid/resource/Event.java | 32 +- .../davdroid/resource/LocalAddressBook.java | 6 +- .../davdroid/resource/LocalCalendar.java | 17 +- .../davdroid/resource/LocalCollection.java | 4 +- .../davdroid/resource/LocalTaskList.java | 12 +- .../bitfire/davdroid/resource/ServerInfo.java | 5 +- .../at/bitfire/davdroid/resource/Task.java | 3 - .../davdroid/resource/WebDavCollection.java | 14 +- .../bitfire/davdroid/resource/iCalendar.java | 72 ++-- .../davdroid/syncadapter/SyncManager.java | 10 +- .../ui/setup/InstallAppsFragment.java | 1 - .../davdroid/webdav/WebDavResource.java | 322 ++++++++---------- 23 files changed, 451 insertions(+), 399 deletions(-) create mode 100644 app/src/androidTest/java/at/bitfire/davdroid/resource/iCalendarTest.java diff --git a/app/src/androidTest/java/at/bitfire/davdroid/DateUtilsTest.java b/app/src/androidTest/java/at/bitfire/davdroid/DateUtilsTest.java index b78fcedd..e6c2de99 100644 --- a/app/src/androidTest/java/at/bitfire/davdroid/DateUtilsTest.java +++ b/app/src/androidTest/java/at/bitfire/davdroid/DateUtilsTest.java @@ -10,13 +10,9 @@ package at.bitfire.davdroid; import junit.framework.TestCase; -import net.fortuna.ical4j.model.Date; import net.fortuna.ical4j.model.DateList; 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 net.fortuna.ical4j.model.property.ExDate; import net.fortuna.ical4j.model.property.RDate; diff --git a/app/src/androidTest/java/at/bitfire/davdroid/resource/ContactTest.java b/app/src/androidTest/java/at/bitfire/davdroid/resource/ContactTest.java index 2a1ba153..d97b5e60 100644 --- a/app/src/androidTest/java/at/bitfire/davdroid/resource/ContactTest.java +++ b/app/src/androidTest/java/at/bitfire/davdroid/resource/ContactTest.java @@ -36,7 +36,7 @@ public class ContactTest extends InstrumentationTestCase { Contact c = new Contact("test.vcf", null); // should generate VCard 3.0 by default - assertEquals("text/vcard", c.getMimeType()); + assertEquals("text/vcard;charset=UTF-8", c.getMimeType()); assertTrue(new String(c.toEntity().toByteArray()).contains("VERSION:3.0")); // now let's generate VCard 4.0 diff --git a/app/src/androidTest/java/at/bitfire/davdroid/resource/EventTest.java b/app/src/androidTest/java/at/bitfire/davdroid/resource/EventTest.java index 68d184fa..bfccbc84 100644 --- a/app/src/androidTest/java/at/bitfire/davdroid/resource/EventTest.java +++ b/app/src/androidTest/java/at/bitfire/davdroid/resource/EventTest.java @@ -9,16 +9,23 @@ package at.bitfire.davdroid.resource; import android.content.res.AssetManager; import android.test.InstrumentationTestCase; -import android.text.format.Time; import net.fortuna.ical4j.data.ParserException; +import net.fortuna.ical4j.model.Date; +import net.fortuna.ical4j.model.DateTime; +import net.fortuna.ical4j.model.TimeZone; +import net.fortuna.ical4j.model.property.DtStart; +import net.fortuna.ical4j.util.TimeZones; import java.io.IOException; import java.io.InputStream; +import at.bitfire.davdroid.DateUtils; import lombok.Cleanup; public class EventTest extends InstrumentationTestCase { + protected final TimeZone tzVienna = DateUtils.tzRegistry.getTimeZone("Europe/Vienna"); + AssetManager assetMgr; Event eOnThatDay, eAllDay1Day, eAllDay10Days, eAllDay0Sec; @@ -33,6 +40,21 @@ public class EventTest extends InstrumentationTestCase { } + public void testGetTzID() throws Exception { + // DATE (without time) + assertEquals(TimeZones.UTC_ID, Event.getTzId(new DtStart(new Date("20150101")))); + + // DATE-TIME without time zone (floating time): should be UTC (because net.fortuna.ical4j.timezone.date.floating=false) + assertEquals(TimeZones.UTC_ID, Event.getTzId(new DtStart(new DateTime("20150101T000000")))); + + // DATE-TIME without time zone (UTC) + assertEquals(TimeZones.UTC_ID, Event.getTzId(new DtStart(new DateTime(1438607288000L)))); + + // DATE-TIME with time zone + assertEquals(tzVienna.getID(), Event.getTzId(new DtStart(new DateTime("20150101T000000", tzVienna)))); + } + + public void testRecurringWithException() throws Exception { Event event = parseCalendar("recurring-with-exception1.ics"); assertTrue(event.isAllDay()); @@ -55,75 +77,31 @@ public class EventTest extends InstrumentationTestCase { public void testStartEndTimesAllDay() throws IOException, ParserException { // event with start date only assertEquals(868838400000L, eOnThatDay.getDtStartInMillis()); - assertEquals(Time.TIMEZONE_UTC, eOnThatDay.getDtStartTzID()); + assertEquals(TimeZones.UTC_ID, eOnThatDay.getDtStartTzID()); // DTEND missing in VEVENT, must have been set to DTSTART+1 day assertEquals(868838400000L + 86400000, eOnThatDay.getDtEndInMillis()); - assertEquals(Time.TIMEZONE_UTC, eOnThatDay.getDtEndTzID()); + assertEquals(TimeZones.UTC_ID, eOnThatDay.getDtEndTzID()); // event with start+end date for all-day event (one day) assertEquals(868838400000L, eAllDay1Day.getDtStartInMillis()); - assertEquals(Time.TIMEZONE_UTC, eAllDay1Day.getDtStartTzID()); + assertEquals(TimeZones.UTC_ID, eAllDay1Day.getDtStartTzID()); assertEquals(868838400000L + 86400000, eAllDay1Day.getDtEndInMillis()); - assertEquals(Time.TIMEZONE_UTC, eAllDay1Day.getDtEndTzID()); + assertEquals(TimeZones.UTC_ID, eAllDay1Day.getDtEndTzID()); // event with start+end date for all-day event (ten days) assertEquals(868838400000L, eAllDay10Days.getDtStartInMillis()); - assertEquals(Time.TIMEZONE_UTC, eAllDay10Days.getDtStartTzID()); + assertEquals(TimeZones.UTC_ID, eAllDay10Days.getDtStartTzID()); assertEquals(868838400000L + 10*86400000, eAllDay10Days.getDtEndInMillis()); - assertEquals(Time.TIMEZONE_UTC, eAllDay10Days.getDtEndTzID()); + assertEquals(TimeZones.UTC_ID, eAllDay10Days.getDtEndTzID()); // event with start+end date on some day (invalid 0 sec-event) assertEquals(868838400000L, eAllDay0Sec.getDtStartInMillis()); - assertEquals(Time.TIMEZONE_UTC, eAllDay0Sec.getDtStartTzID()); + assertEquals(TimeZones.UTC_ID, eAllDay0Sec.getDtStartTzID()); // DTEND invalid in VEVENT, must have been set to DTSTART+1 day assertEquals(868838400000L + 86400000, eAllDay0Sec.getDtEndInMillis()); - assertEquals(Time.TIMEZONE_UTC, eAllDay0Sec.getDtEndTzID()); + assertEquals(TimeZones.UTC_ID, eAllDay0Sec.getDtEndTzID()); } - public void testTimezoneDefToTzId() { - // test valid definition - final String VTIMEZONE_SAMPLE = // taken from RFC 4791, 5.2.2. CALDAV:calendar-timezone Property - "BEGIN:VCALENDAR\n" + - "PRODID:-//Example Corp.//CalDAV Client//EN\n" + - "VERSION:2.0\n" + - "BEGIN:VTIMEZONE\n" + - "TZID:US-Eastern\n" + - "LAST-MODIFIED:19870101T000000Z\n" + - "BEGIN:STANDARD\n" + - "DTSTART:19671029T020000\n" + - "RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10\n" + - "TZOFFSETFROM:-0400\n" + - "TZOFFSETTO:-0500\n" + - "TZNAME:Eastern Standard Time (US & Canada)\n" + - "END:STANDARD\n" + - "BEGIN:DAYLIGHT\n" + - "DTSTART:19870405T020000\n" + - "RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4\n" + - "TZOFFSETFROM:-0500\n" + - "TZOFFSETTO:-0400\n" + - "TZNAME:Eastern Daylight Time (US & Canada)\n" + - "END:DAYLIGHT\n" + - "END:VTIMEZONE\n" + - "END:VCALENDAR"; - assertEquals("US-Eastern", Event.TimezoneDefToTzId(VTIMEZONE_SAMPLE)); - - // test null value - try { - Event.TimezoneDefToTzId(null); - fail(); - } catch(IllegalArgumentException e) { - assert(true); - } - - // test invalid time zone - try { - Event.TimezoneDefToTzId("/* invalid content */"); - fail(); - } catch(IllegalArgumentException e) { - assert(true); - } - } - public void testUnfolding() throws IOException, InvalidResourceException { Event e = parseCalendar("two-line-description-without-crlf.ics"); assertEquals("http://www.tgbornheim.de/index.php?sessionid=&page=&id=&sportcentergroup=&day=6", e.getDescription()); diff --git a/app/src/androidTest/java/at/bitfire/davdroid/resource/LocalCalendarTest.java b/app/src/androidTest/java/at/bitfire/davdroid/resource/LocalCalendarTest.java index 4cfa857b..aef23aa7 100644 --- a/app/src/androidTest/java/at/bitfire/davdroid/resource/LocalCalendarTest.java +++ b/app/src/androidTest/java/at/bitfire/davdroid/resource/LocalCalendarTest.java @@ -25,25 +25,14 @@ import android.test.InstrumentationTestCase; import android.util.Log; import net.fortuna.ical4j.model.Date; -import net.fortuna.ical4j.model.DateList; import net.fortuna.ical4j.model.Dur; import net.fortuna.ical4j.model.TimeZone; -import net.fortuna.ical4j.model.TimeZoneRegistry; -import net.fortuna.ical4j.model.TimeZoneRegistryFactory; -import net.fortuna.ical4j.model.ValidationException; import net.fortuna.ical4j.model.component.VAlarm; -import net.fortuna.ical4j.model.parameter.Value; -import net.fortuna.ical4j.model.property.DateListProperty; import net.fortuna.ical4j.model.property.DtEnd; import net.fortuna.ical4j.model.property.DtStart; -import net.fortuna.ical4j.model.property.ExDate; -import net.fortuna.ical4j.model.property.RDate; -import net.fortuna.ical4j.util.Dates; import java.text.ParseException; -import java.util.ArrayList; import java.util.Calendar; -import java.util.List; import at.bitfire.davdroid.DateUtils; import lombok.Cleanup; diff --git a/app/src/androidTest/java/at/bitfire/davdroid/resource/iCalendarTest.java b/app/src/androidTest/java/at/bitfire/davdroid/resource/iCalendarTest.java new file mode 100644 index 00000000..b7685c80 --- /dev/null +++ b/app/src/androidTest/java/at/bitfire/davdroid/resource/iCalendarTest.java @@ -0,0 +1,95 @@ +/* + * 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 junit.framework.TestCase; + +import net.fortuna.ical4j.data.CalendarBuilder; +import net.fortuna.ical4j.model.Date; +import net.fortuna.ical4j.model.TimeZone; +import net.fortuna.ical4j.model.component.VTimeZone; +import net.fortuna.ical4j.model.property.DtStart; + +import java.io.StringReader; + +import at.bitfire.davdroid.DateUtils; + +public class iCalendarTest extends TestCase { + protected final TimeZone tzVienna = DateUtils.tzRegistry.getTimeZone("Europe/Vienna"); + + public void testTimezoneDefToTzId() { + // test valid definition + assertEquals("US-Eastern", Event.TimezoneDefToTzId("BEGIN:VCALENDAR\n" + + "PRODID:-//Example Corp.//CalDAV Client//EN\n" + + "VERSION:2.0\n" + + "BEGIN:VTIMEZONE\n" + + "TZID:US-Eastern\n" + + "LAST-MODIFIED:19870101T000000Z\n" + + "BEGIN:STANDARD\n" + + "DTSTART:19671029T020000\n" + + "RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10\n" + + "TZOFFSETFROM:-0400\n" + + "TZOFFSETTO:-0500\n" + + "TZNAME:Eastern Standard Time (US & Canada)\n" + + "END:STANDARD\n" + + "BEGIN:DAYLIGHT\n" + + "DTSTART:19870405T020000\n" + + "RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4\n" + + "TZOFFSETFROM:-0500\n" + + "TZOFFSETTO:-0400\n" + + "TZNAME:Eastern Daylight Time (US & Canada)\n" + + "END:DAYLIGHT\n" + + "END:VTIMEZONE\n" + + "END:VCALENDAR")); + + // test invalid time zone + assertNull(iCalendar.TimezoneDefToTzId("/* invalid content */")); + + // test time zone without TZID + assertNull(iCalendar.TimezoneDefToTzId("BEGIN:VCALENDAR\n" + + "PRODID:-//Inverse inc./SOGo 2.2.10//EN\n" + + "VERSION:2.0\n" + + "END:VCALENDAR")); + } + + public void testValidateTimeZone() throws Exception { + assertNotNull(tzVienna); + + // date (no time zone) should be ignored + DtStart date = new DtStart(new Date("20150101")); + iCalendar.validateTimeZone(date); + assertNull(date.getTimeZone()); + + // date-time (Europe/Vienna) should be unchanged + DtStart dtStart = new DtStart("20150101", tzVienna); + iCalendar.validateTimeZone(dtStart); + assertEquals(tzVienna, dtStart.getTimeZone()); + + // time zone that is not available on Android systems should be changed to system default + CalendarBuilder builder = new CalendarBuilder(); + net.fortuna.ical4j.model.Calendar cal = builder.build(new StringReader("BEGIN:VCALENDAR\n" + + "BEGIN:VTIMEZONE\n" + + "TZID:CustomTime\n" + + "BEGIN:STANDARD\n" + + "TZOFFSETFROM:-0400\n" + + "TZOFFSETTO:-0500\n" + + "DTSTART:19600101T000000\n" + + "END:STANDARD\n" + + "END:VTIMEZONE\n" + + "END:VCALENDAR")); + final TimeZone tzCustom = new TimeZone((VTimeZone)cal.getComponent(VTimeZone.VTIMEZONE)); + dtStart = new DtStart("20150101T000000", tzCustom); + iCalendar.validateTimeZone(dtStart); + + final TimeZone tzDefault = DateUtils.tzRegistry.getTimeZone(java.util.TimeZone.getDefault().getID()); + assertNotNull(tzDefault); + assertEquals(tzDefault.getID(), dtStart.getTimeZone().getID()); + } + +} diff --git a/app/src/androidTest/java/at/bitfire/davdroid/syncadapter/DavResourceFinderTest.java b/app/src/androidTest/java/at/bitfire/davdroid/syncadapter/DavResourceFinderTest.java index e6e64dbc..72b42a75 100644 --- a/app/src/androidTest/java/at/bitfire/davdroid/syncadapter/DavResourceFinderTest.java +++ b/app/src/androidTest/java/at/bitfire/davdroid/syncadapter/DavResourceFinderTest.java @@ -18,7 +18,6 @@ import at.bitfire.davdroid.TestConstants; import at.bitfire.davdroid.resource.DavResourceFinder; import at.bitfire.davdroid.resource.ServerInfo; import at.bitfire.davdroid.resource.ServerInfo.ResourceInfo; -import ezvcard.VCardVersion; public class DavResourceFinderTest extends InstrumentationTestCase { diff --git a/app/src/androidTest/java/at/bitfire/davdroid/webdav/WebDavResourceTest.java b/app/src/androidTest/java/at/bitfire/davdroid/webdav/WebDavResourceTest.java index b4288ad3..7e033a96 100644 --- a/app/src/androidTest/java/at/bitfire/davdroid/webdav/WebDavResourceTest.java +++ b/app/src/androidTest/java/at/bitfire/davdroid/webdav/WebDavResourceTest.java @@ -73,7 +73,7 @@ public class WebDavResourceTest extends InstrumentationTestCase { public void testPropfindCurrentUserPrincipal() throws Exception { davCollection.propfind(HttpPropfind.Mode.CURRENT_USER_PRINCIPAL); - assertEquals(new URI("/dav/principals/users/test"), davCollection.getCurrentUserPrincipal()); + assertEquals(new URI("/dav/principals/users/test"), davCollection.getProperties().getCurrentUserPrincipal()); WebDavResource simpleFile = new WebDavResource(davAssets, "test.random"); try { @@ -82,14 +82,14 @@ public class WebDavResourceTest extends InstrumentationTestCase { } catch(DavException ex) { } - assertNull(simpleFile.getCurrentUserPrincipal()); + assertNull(simpleFile.getProperties().getCurrentUserPrincipal()); } public void testPropfindHomeSets() throws Exception { WebDavResource dav = new WebDavResource(davCollection, "principals/users/test"); dav.propfind(HttpPropfind.Mode.HOME_SETS); - assertEquals(new URI("/dav/addressbooks/test/"), dav.getAddressbookHomeSet()); - assertEquals(new URI("/dav/calendars/test/"), dav.getCalendarHomeSet()); + assertEquals(new URI("/dav/addressbooks/test/"), dav.getProperties().getAddressbookHomeSet()); + assertEquals(new URI("/dav/calendars/test/"), dav.getProperties().getCalendarHomeSet()); } public void testPropfindAddressBooks() throws Exception { @@ -103,34 +103,45 @@ public class WebDavResourceTest extends InstrumentationTestCase { WebDavResource ab = dav.getMembers().get(0); assertEquals(TestConstants.roboHydra.resolve("/dav/addressbooks/test/useless-member"), ab.getLocation()); assertEquals("useless-member", ab.getName()); - assertFalse(ab.isAddressBook()); + assertFalse(ab.getProperties().isAddressBook()); // the second one is an address book (referenced by relative URI) ab = dav.getMembers().get(1); assertEquals(TestConstants.roboHydra.resolve("/dav/addressbooks/test/default.vcf/"), ab.getLocation()); assertEquals("default.vcf", ab.getName()); - assertTrue(ab.isAddressBook()); + assertTrue(ab.getProperties().isAddressBook()); // the third one is an address book (referenced by an absolute URI) ab = dav.getMembers().get(2); assertEquals(new URI("https://my.server/absolute:uri/my-address-book/"), ab.getLocation()); assertEquals("my-address-book", ab.getName()); - assertTrue(ab.isAddressBook()); + assertTrue(ab.getProperties().isAddressBook()); } public void testPropfindCalendars() throws Exception { WebDavResource dav = new WebDavResource(davCollection, "calendars/test"); dav.propfind(Mode.CALDAV_COLLECTIONS); assertEquals(3, dav.getMembers().size()); - assertEquals("0xFF00FF", dav.getMembers().get(2).getColor()); + assertEquals(new Integer(0xFFFF00FF), dav.getMembers().get(2).getProperties().getColor()); for (WebDavResource member : dav.getMembers()) { if (member.getName().contains(".ics")) - assertTrue(member.isCalendar()); + assertTrue(member.getProperties().isCalendar()); else - assertFalse(member.isCalendar()); - assertFalse(member.isAddressBook()); + assertFalse(member.getProperties().isCalendar()); + assertFalse(member.getProperties().isAddressBook()); } } + + public void testPropfindCollectionProperties() throws Exception { + WebDavResource dav = new WebDavResource(davCollection, "propfind-collection-properties"); + dav.propfind(Mode.COLLECTION_PROPERTIES); + assertTrue(dav.members.isEmpty()); + assertTrue(dav.properties.isCollection); + assertTrue(dav.properties.isAddressBook); + assertNull(dav.properties.displayName); + assertNull(dav.properties.color); + assertEquals(VCardVersion.V4_0, dav.properties.supportedVCardVersion); + } public void testPropfindTrailingSlashes() throws Exception { final String principalOK = "/principals/ok"; @@ -145,7 +156,7 @@ public class WebDavResourceTest extends InstrumentationTestCase { for (String path : requestPaths) { WebDavResource davSlash = new WebDavResource(davCollection, new URI(path)); davSlash.propfind(Mode.CARDDAV_COLLECTIONS); - assertEquals(new URI(principalOK), davSlash.getCurrentUserPrincipal()); + assertEquals(new URI(principalOK), davSlash.getProperties().getCurrentUserPrincipal()); } } @@ -190,7 +201,7 @@ public class WebDavResourceTest extends InstrumentationTestCase { WebDavResource davAddressBook = new WebDavResource(davCollection, "addressbooks/default.vcf/"); davAddressBook.multiGet(DavMultiget.Type.ADDRESS_BOOK, new String[] { "1.vcf", "2:3@my%40pc.vcf" }); // queried address book has a name - assertEquals("My Book", davAddressBook.getDisplayName()); + assertEquals("My Book", davAddressBook.getProperties().getDisplayName()); // there are two contacts assertEquals(2, davAddressBook.getMembers().size()); // contact file names should be unescaped (yes, it's really named ...%40pc... to check double-encoding) diff --git a/app/src/androidTest/robohydra/plugins/dav/index.js b/app/src/androidTest/robohydra/plugins/dav/index.js index b89b0176..f27756c6 100644 --- a/app/src/androidTest/robohydra/plugins/dav/index.js +++ b/app/src/androidTest/robohydra/plugins/dav/index.js @@ -81,6 +81,40 @@ exports.getBodyParts = function(conf) { } } }), + new RoboHydraHeadDAV({ + path: "/dav/propfind-collection-properties", + handler: function(req,res,next) { + if (req.method == "PROPFIND") { + res.statusCode = 207; + res.write('\\ + \ + \ + /dav/propfind-collection-properties \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + HTTP/1.1 200 OK\ + \ + \ + \ + \ + 0xFF00FF\ + \ + HTTP/1.1 404 Not Found\ + \ + \ + \ + '); + } + } + }), /* principal URL */ new RoboHydraHeadDAV({ diff --git a/app/src/main/java/at/bitfire/davdroid/DAVUtils.java b/app/src/main/java/at/bitfire/davdroid/DAVUtils.java index 70b6cc16..96884123 100644 --- a/app/src/main/java/at/bitfire/davdroid/DAVUtils.java +++ b/app/src/main/java/at/bitfire/davdroid/DAVUtils.java @@ -16,8 +16,11 @@ import java.util.regex.Pattern; public class DAVUtils { private static final String TAG = "davdroid.DAVutils"; + public static final int calendarGreen = 0xFFC3EA6E; + + public static int CalDAVtoARGBColor(String davColor) { - int color = 0xFFC3EA6E; // fallback: "DAVdroid green" + int color = calendarGreen; // fallback: "DAVdroid green" if (davColor != null) { Pattern p = Pattern.compile("#?(\\p{XDigit}{6})(\\p{XDigit}{2})?"); Matcher m = p.matcher(davColor); diff --git a/app/src/main/java/at/bitfire/davdroid/DateUtils.java b/app/src/main/java/at/bitfire/davdroid/DateUtils.java index e102f464..941ae207 100644 --- a/app/src/main/java/at/bitfire/davdroid/DateUtils.java +++ b/app/src/main/java/at/bitfire/davdroid/DateUtils.java @@ -8,33 +8,25 @@ package at.bitfire.davdroid; -import android.text.format.Time; 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.DefaultTimeZoneRegistryFactory; 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 net.fortuna.ical4j.model.property.ExDate; -import net.fortuna.ical4j.model.property.RDate; -import net.fortuna.ical4j.util.TimeZones; import org.apache.commons.lang3.StringUtils; -import java.lang.reflect.InvocationTargetException; import java.text.DateFormat; import java.text.ParseException; import java.text.SimpleDateFormat; -import java.util.GregorianCalendar; import java.util.LinkedList; import java.util.List; import java.util.SimpleTimeZone; -import java.util.StringTokenizer; public class DateUtils { private final static String TAG = "davdroid.DateUtils"; @@ -49,35 +41,35 @@ public class DateUtils { // time zones - public static String findAndroidTimezoneID(String tzID) { - String localTZ = null; + 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(tzID)) { - localTZ = availableTZ; + if (availableTZ.equalsIgnoreCase(tz)) { + deviceTZ = availableTZ; break; } // if that doesn't work, try to find something else that matches - if (localTZ == null) { + if (deviceTZ == null) { Log.w(TAG, "Coulnd't find time zone with matching identifiers, trying to guess"); for (String availableTZ : availableTZs) - if (StringUtils.indexOfIgnoreCase(tzID, availableTZ) != -1) { - localTZ = availableTZ; + if (StringUtils.indexOfIgnoreCase(tz, availableTZ) != -1) { + deviceTZ = availableTZ; break; } } // if that doesn't work, use UTC as fallback - if (localTZ == null) { - Log.e(TAG, "Couldn't identify time zone, using UTC as fallback"); - localTZ = Time.TIMEZONE_UTC; + 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; } - Log.d(TAG, "Assuming time zone " + localTZ + " for " + tzID); - return localTZ; + return deviceTZ; } diff --git a/app/src/main/java/at/bitfire/davdroid/resource/DavResourceFinder.java b/app/src/main/java/at/bitfire/davdroid/resource/DavResourceFinder.java index f46a2e48..c966a352 100644 --- a/app/src/main/java/at/bitfire/davdroid/resource/DavResourceFinder.java +++ b/app/src/main/java/at/bitfire/davdroid/resource/DavResourceFinder.java @@ -33,7 +33,6 @@ import at.bitfire.davdroid.webdav.DavIncapableException; import at.bitfire.davdroid.webdav.HttpPropfind.Mode; import at.bitfire.davdroid.webdav.NotAuthorizedException; import at.bitfire.davdroid.webdav.WebDavResource; -import ezvcard.VCardVersion; public class DavResourceFinder implements Closeable { private final static String TAG = "davdroid.ResourceFinder"; @@ -62,7 +61,7 @@ public class DavResourceFinder implements Closeable { URI uriAddressBookHomeSet = null; try { principal.propfind(Mode.HOME_SETS); - uriAddressBookHomeSet = principal.getAddressbookHomeSet(); + uriAddressBookHomeSet = principal.getProperties().getAddressbookHomeSet(); } catch (Exception e) { Log.i(TAG, "Couldn't find address-book home set", e); } @@ -80,19 +79,21 @@ public class DavResourceFinder implements Closeable { possibleAddressBooks.addAll(homeSetAddressBooks.getMembers()); List addressBooks = new LinkedList<>(); - for (WebDavResource resource : possibleAddressBooks) - if (resource.isAddressBook()) { + for (WebDavResource resource : possibleAddressBooks) { + final WebDavResource.Properties properties = resource.getProperties(); + if (properties.isAddressBook()) { Log.i(TAG, "Found address book: " + resource.getLocation().getPath()); ServerInfo.ResourceInfo info = new ServerInfo.ResourceInfo( - ServerInfo.ResourceInfo.Type.ADDRESS_BOOK, - resource.isReadOnly(), - resource.getLocation().toString(), - resource.getDisplayName(), - resource.getDescription(), resource.getColor() + ServerInfo.ResourceInfo.Type.ADDRESS_BOOK, + properties.isReadOnly(), + resource.getLocation().toString(), + properties.getDisplayName(), + properties.getDescription(), properties.getColor() ); addressBooks.add(info); } + } serverInfo.setAddressBooks(addressBooks); } else Log.w(TAG, "Found address-book home set, but it doesn't advertise CardDAV support"); @@ -104,7 +105,7 @@ public class DavResourceFinder implements Closeable { URI uriCalendarHomeSet = null; try { principal.propfind(Mode.HOME_SETS); - uriCalendarHomeSet = principal.getCalendarHomeSet(); + uriCalendarHomeSet = principal.getProperties().getCalendarHomeSet(); } catch(Exception e) { Log.i(TAG, "Couldn't find calendar home set", e); } @@ -124,27 +125,28 @@ public class DavResourceFinder implements Closeable { List calendars = new LinkedList<>(), todoLists = new LinkedList<>(); - for (WebDavResource resource : possibleCalendars) - if (resource.isCalendar()) { + for (WebDavResource resource : possibleCalendars) { + final WebDavResource.Properties properties = resource.getProperties(); + if (properties.isCalendar()) { Log.i(TAG, "Found calendar: " + resource.getLocation().getPath()); ServerInfo.ResourceInfo info = new ServerInfo.ResourceInfo( ServerInfo.ResourceInfo.Type.CALENDAR, - resource.isReadOnly(), + properties.isReadOnly(), resource.getLocation().toString(), - resource.getDisplayName(), - resource.getDescription(), resource.getColor() + properties.getDisplayName(), + properties.getDescription(), properties.getColor() ); - info.setTimezone(resource.getTimezone()); + info.setTimezone(properties.getTimeZone()); boolean isCalendar = false, isTodoList = false; - if (resource.getSupportedComponents() == null) { + if (properties.getSupportedComponents() == null) { // no info about supported components, assuming all components are supported isCalendar = true; isTodoList = true; } else { // CALDAV:supported-calendar-component-set available - for (String supportedComponent : resource.getSupportedComponents()) + for (String supportedComponent : properties.getSupportedComponents()) if ("VEVENT".equalsIgnoreCase(supportedComponent)) isCalendar = true; else if ("VTODO".equalsIgnoreCase(supportedComponent)) @@ -162,6 +164,7 @@ public class DavResourceFinder implements Closeable { if (isTodoList) todoLists.add(new ServerInfo.ResourceInfo(info)); } + } serverInfo.setCalendars(calendars); serverInfo.setTodoLists(todoLists); @@ -271,8 +274,8 @@ public class DavResourceFinder implements Closeable { try { WebDavResource wellKnown = new WebDavResource(base, "/.well-known/" + serviceName); wellKnown.propfind(Mode.CURRENT_USER_PRINCIPAL); - if (wellKnown.getCurrentUserPrincipal() != null) { - URI principal = wellKnown.getCurrentUserPrincipal(); + if (wellKnown.getProperties().getCurrentUserPrincipal() != null) { + URI principal = wellKnown.getProperties().getCurrentUserPrincipal(); Log.i(TAG, "Principal URL found from Well-Known URI: " + principal); return new WebDavResource(wellKnown, principal); } @@ -293,8 +296,8 @@ public class DavResourceFinder implements Closeable { Log.d(TAG, "Well-known service detection failed, trying initial context path " + initialURL); try { base.propfind(Mode.CURRENT_USER_PRINCIPAL); - if (base.getCurrentUserPrincipal() != null) { - URI principal = base.getCurrentUserPrincipal(); + if (base.getProperties().getCurrentUserPrincipal() != null) { + URI principal = base.getProperties().getCurrentUserPrincipal(); Log.i(TAG, "Principal URL found from initial context path: " + principal); return new WebDavResource(base, principal); } diff --git a/app/src/main/java/at/bitfire/davdroid/resource/Event.java b/app/src/main/java/at/bitfire/davdroid/resource/Event.java index 0fe5fa9a..c243cd0b 100644 --- a/app/src/main/java/at/bitfire/davdroid/resource/Event.java +++ b/app/src/main/java/at/bitfire/davdroid/resource/Event.java @@ -17,15 +17,11 @@ import net.fortuna.ical4j.model.Component; import net.fortuna.ical4j.model.ComponentList; import net.fortuna.ical4j.model.Date; import net.fortuna.ical4j.model.DateTime; -import net.fortuna.ical4j.model.DefaultTimeZoneRegistryFactory; import net.fortuna.ical4j.model.Property; import net.fortuna.ical4j.model.PropertyList; -import net.fortuna.ical4j.model.TimeZoneRegistry; import net.fortuna.ical4j.model.ValidationException; import net.fortuna.ical4j.model.component.VAlarm; import net.fortuna.ical4j.model.component.VEvent; -import net.fortuna.ical4j.model.component.VTimeZone; -import net.fortuna.ical4j.model.parameter.Value; import net.fortuna.ical4j.model.property.Attendee; import net.fortuna.ical4j.model.property.Clazz; import net.fortuna.ical4j.model.property.DateProperty; @@ -46,14 +42,11 @@ 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.CompatibilityHints; -import net.fortuna.ical4j.util.SimpleHostInfo; -import net.fortuna.ical4j.util.UidGenerator; +import net.fortuna.ical4j.util.TimeZones; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; -import java.io.StringReader; import java.util.Calendar; import java.util.HashSet; import java.util.LinkedList; @@ -63,7 +56,6 @@ import java.util.TimeZone; import at.bitfire.davdroid.Constants; import at.bitfire.davdroid.DateUtils; -import at.bitfire.davdroid.syncadapter.DavSyncAdapter; import lombok.Getter; import lombok.NonNull; import lombok.Setter; @@ -156,15 +148,13 @@ public class Event extends iCalendar { if ((dtStart = event.getStartDate()) == null || (dtEnd = event.getEndDate()) == null) throw new InvalidResourceException("Invalid start time/end time/duration"); - if (hasTime(dtStart)) { - validateTimeZone(dtStart); - validateTimeZone(dtEnd); - } + validateTimeZone(dtStart); + validateTimeZone(dtEnd); // all-day events and "events on that day": // * related UNIX times must be in UTC // * must have a duration (set to one day if missing) - if (!hasTime(dtStart) && !dtEnd.getDate().after(dtStart.getDate())) { + 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()); @@ -304,8 +294,20 @@ public class Event extends iCalendar { // 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 !hasTime(dtStart); + return !isDateTime(dtStart); } public long getDtStartInMillis() { diff --git a/app/src/main/java/at/bitfire/davdroid/resource/LocalAddressBook.java b/app/src/main/java/at/bitfire/davdroid/resource/LocalAddressBook.java index c51abe75..d7d8bb62 100644 --- a/app/src/main/java/at/bitfire/davdroid/resource/LocalAddressBook.java +++ b/app/src/main/java/at/bitfire/davdroid/resource/LocalAddressBook.java @@ -17,7 +17,6 @@ import android.content.Entity; import android.content.EntityIterator; import android.content.res.AssetFileDescriptor; import android.database.Cursor; -import android.database.DatabaseUtils; import android.net.Uri; import android.os.RemoteException; import android.provider.ContactsContract; @@ -48,7 +47,6 @@ import java.io.InputStream; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Date; -import java.util.HashMap; import java.util.Iterator; import java.util.LinkedList; import java.util.List; @@ -127,9 +125,9 @@ public class LocalAddressBook extends LocalCollection { } @Override - public void updateMetaData(WebDavResource resource) + public void updateMetaData(WebDavResource.Properties properties) { - final VCardVersion vCardVersion = resource.getVCardVersion(); + final VCardVersion vCardVersion = properties.getSupportedVCardVersion(); accountSettings.setAddressBookVCardVersion(vCardVersion != null ? vCardVersion : VCardVersion.V3_0); } diff --git a/app/src/main/java/at/bitfire/davdroid/resource/LocalCalendar.java b/app/src/main/java/at/bitfire/davdroid/resource/LocalCalendar.java index e64ffa39..c4e16b63 100644 --- a/app/src/main/java/at/bitfire/davdroid/resource/LocalCalendar.java +++ b/app/src/main/java/at/bitfire/davdroid/resource/LocalCalendar.java @@ -22,7 +22,6 @@ import android.database.Cursor; import android.database.DatabaseUtils; import android.net.Uri; import android.os.Build; -import android.os.Bundle; import android.os.RemoteException; import android.provider.CalendarContract; import android.provider.CalendarContract.Attendees; @@ -42,10 +41,8 @@ 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.Value; import net.fortuna.ical4j.model.property.Action; import net.fortuna.ical4j.model.property.Attendee; -import net.fortuna.ical4j.model.property.DateListProperty; import net.fortuna.ical4j.model.property.Description; import net.fortuna.ical4j.model.property.Duration; import net.fortuna.ical4j.model.property.ExDate; @@ -61,10 +58,8 @@ import org.apache.commons.lang3.StringUtils; import java.net.URI; import java.net.URISyntaxException; import java.text.ParseException; -import java.util.HashMap; import java.util.LinkedList; import java.util.List; -import java.util.StringTokenizer; import at.bitfire.davdroid.DAVUtils; import at.bitfire.davdroid.DateUtils; @@ -119,7 +114,7 @@ public class LocalCalendar extends LocalCollection { 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, DAVUtils.CalDAVtoARGBColor(info.getColor())); + 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); @@ -139,7 +134,7 @@ public class LocalCalendar extends LocalCollection { } if (info.getTimezone() != null) - values.put(Calendars.CALENDAR_TIME_ZONE, info.getTimezone()); + values.put(Calendars.CALENDAR_TIME_ZONE, DateUtils.findAndroidTimezoneID(info.getTimezone())); Log.i(TAG, "Inserting calendar: " + values.toString()); try { @@ -196,16 +191,16 @@ public class LocalCalendar extends LocalCollection { } @Override - public void updateMetaData(WebDavResource resource) throws LocalStorageException { + public void updateMetaData(WebDavResource.Properties properties) throws LocalStorageException { ContentValues values = new ContentValues(); - final String displayName = resource.getDisplayName(); + final String displayName = properties.getDisplayName(); if (displayName != null) values.put(Calendars.CALENDAR_DISPLAY_NAME, displayName); - final String color = resource.getColor(); + final Integer color = properties.getColor(); if (color != null) - values.put(Calendars.CALENDAR_COLOR, DAVUtils.CalDAVtoARGBColor(color)); + values.put(Calendars.CALENDAR_COLOR, color); try { if (values.size() > 0) diff --git a/app/src/main/java/at/bitfire/davdroid/resource/LocalCollection.java b/app/src/main/java/at/bitfire/davdroid/resource/LocalCollection.java index 0583010b..af70cb23 100644 --- a/app/src/main/java/at/bitfire/davdroid/resource/LocalCollection.java +++ b/app/src/main/java/at/bitfire/davdroid/resource/LocalCollection.java @@ -18,7 +18,6 @@ import android.content.OperationApplicationException; import android.database.Cursor; import android.database.DatabaseUtils; import android.net.Uri; -import android.os.Bundle; import android.os.RemoteException; import android.provider.CalendarContract; import android.util.Log; @@ -26,7 +25,6 @@ import android.util.Log; import org.apache.commons.lang3.StringUtils; import java.util.ArrayList; -import java.util.HashMap; import java.util.LinkedList; import java.util.List; @@ -96,7 +94,7 @@ public abstract class LocalCollection { /** 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 resource) throws LocalStorageException; + abstract public void updateMetaData(WebDavResource.Properties properties) throws LocalStorageException; // content provider (= database) querying diff --git a/app/src/main/java/at/bitfire/davdroid/resource/LocalTaskList.java b/app/src/main/java/at/bitfire/davdroid/resource/LocalTaskList.java index 6c2250a1..3b6d30cf 100644 --- a/app/src/main/java/at/bitfire/davdroid/resource/LocalTaskList.java +++ b/app/src/main/java/at/bitfire/davdroid/resource/LocalTaskList.java @@ -18,7 +18,6 @@ import android.content.Context; import android.database.Cursor; import android.net.Uri; import android.os.RemoteException; -import android.provider.CalendarContract; import android.util.Log; import net.fortuna.ical4j.model.Date; @@ -36,7 +35,6 @@ import net.fortuna.ical4j.util.TimeZones; import org.apache.commons.lang3.StringUtils; import org.dmfs.provider.tasks.TaskContract; -import java.util.HashMap; import java.util.LinkedList; import at.bitfire.davdroid.DAVUtils; @@ -77,7 +75,7 @@ public class LocalTaskList extends LocalCollection { 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, DAVUtils.CalDAVtoARGBColor(info.getColor())); + 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); @@ -135,16 +133,16 @@ public class LocalTaskList extends LocalCollection { } @Override - public void updateMetaData(WebDavResource resource) throws LocalStorageException { + public void updateMetaData(WebDavResource.Properties properties) throws LocalStorageException { ContentValues values = new ContentValues(); - final String displayName = resource.getDisplayName(); + final String displayName = properties.getDisplayName(); if (displayName != null) values.put(TaskContract.TaskLists.LIST_NAME, displayName); - final String color = resource.getColor(); + final Integer color = properties.getColor(); if (color != null) - values.put(TaskContract.TaskLists.LIST_COLOR, DAVUtils.CalDAVtoARGBColor(color)); + values.put(TaskContract.TaskLists.LIST_COLOR, color); try { if (values.size() > 0) diff --git a/app/src/main/java/at/bitfire/davdroid/resource/ServerInfo.java b/app/src/main/java/at/bitfire/davdroid/resource/ServerInfo.java index a0f11e2c..7ad0547e 100644 --- a/app/src/main/java/at/bitfire/davdroid/resource/ServerInfo.java +++ b/app/src/main/java/at/bitfire/davdroid/resource/ServerInfo.java @@ -13,7 +13,6 @@ import java.net.URI; import java.util.LinkedList; import java.util.List; -import ezvcard.VCardVersion; import lombok.Data; import lombok.RequiredArgsConstructor; @@ -56,8 +55,8 @@ public class ServerInfo implements Serializable { final String URL, // absolute URL of resource title, - description, - color; + description; + final Integer color; String timezone; diff --git a/app/src/main/java/at/bitfire/davdroid/resource/Task.java b/app/src/main/java/at/bitfire/davdroid/resource/Task.java index 044a4bd9..2c587c19 100644 --- a/app/src/main/java/at/bitfire/davdroid/resource/Task.java +++ b/app/src/main/java/at/bitfire/davdroid/resource/Task.java @@ -35,8 +35,6 @@ 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 net.fortuna.ical4j.util.SimpleHostInfo; -import net.fortuna.ical4j.util.UidGenerator; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -45,7 +43,6 @@ import java.net.URI; import java.net.URISyntaxException; import at.bitfire.davdroid.Constants; -import at.bitfire.davdroid.syncadapter.DavSyncAdapter; import lombok.Getter; import lombok.Setter; diff --git a/app/src/main/java/at/bitfire/davdroid/resource/WebDavCollection.java b/app/src/main/java/at/bitfire/davdroid/resource/WebDavCollection.java index f22298f5..d3a9c650 100644 --- a/app/src/main/java/at/bitfire/davdroid/resource/WebDavCollection.java +++ b/app/src/main/java/at/bitfire/davdroid/resource/WebDavCollection.java @@ -88,7 +88,7 @@ public abstract class WebDavCollection { List resources = new LinkedList<>(); if (collection.getMembers() != null) for (WebDavResource member : collection.getMembers()) - resources.add(newResourceSkeleton(member.getName(), member.getETag())); + resources.add(newResourceSkeleton(member.getName(), member.getProperties().getETag())); return resources.toArray(new Resource[resources.size()]); @@ -113,7 +113,7 @@ public abstract class WebDavCollection { throw new DavNoContentException(); for (WebDavResource member : collection.getMembers()) { - T resource = newResourceSkeleton(member.getName(), member.getETag()); + T resource = newResourceSkeleton(member.getName(), member.getProperties().getETag()); try { if (member.getContent() != null) { @Cleanup InputStream is = new ByteArrayInputStream(member.getContent()); @@ -158,13 +158,13 @@ public abstract class WebDavCollection { // 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.setContentType(res.getMimeType()); + member.getProperties().setContentType(res.getMimeType()); @Cleanup ByteArrayOutputStream os = res.toEntity(); String eTag = member.put(os.toByteArray(), PutMode.ADD_DONT_OVERWRITE); // after a successful upload, the collection has implicitely changed, too - collection.invalidateCTag(); + collection.getProperties().invalidateCTag(); return eTag; } @@ -173,19 +173,19 @@ public abstract class WebDavCollection { WebDavResource member = new WebDavResource(collection, res.getName(), res.getETag()); member.delete(); - collection.invalidateCTag(); + 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.setContentType(res.getMimeType()); + member.getProperties().setContentType(res.getMimeType()); @Cleanup ByteArrayOutputStream os = res.toEntity(); String eTag = member.put(os.toByteArray(), PutMode.UPDATE_DONT_OVERWRITE); // after a successful upload, the collection has implicitely changed, too - collection.invalidateCTag(); + collection.getProperties().invalidateCTag(); return eTag; } diff --git a/app/src/main/java/at/bitfire/davdroid/resource/iCalendar.java b/app/src/main/java/at/bitfire/davdroid/resource/iCalendar.java index 262babb1..1176a5fb 100644 --- a/app/src/main/java/at/bitfire/davdroid/resource/iCalendar.java +++ b/app/src/main/java/at/bitfire/davdroid/resource/iCalendar.java @@ -8,29 +8,24 @@ package at.bitfire.davdroid.resource; -import android.text.format.Time; import android.util.Log; import net.fortuna.ical4j.data.CalendarBuilder; +import net.fortuna.ical4j.data.ParserException; import net.fortuna.ical4j.model.DateTime; -import net.fortuna.ical4j.model.DefaultTimeZoneRegistryFactory; -import net.fortuna.ical4j.model.TimeZoneRegistry; import net.fortuna.ical4j.model.component.VTimeZone; -import net.fortuna.ical4j.model.parameter.Value; import net.fortuna.ical4j.model.property.DateProperty; -import net.fortuna.ical4j.model.property.DtStart; import net.fortuna.ical4j.util.CompatibilityHints; import net.fortuna.ical4j.util.SimpleHostInfo; import net.fortuna.ical4j.util.UidGenerator; -import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.io.InputStream; import java.io.StringReader; +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 { static private final String TAG = "DAVdroid.iCal"; @@ -72,47 +67,46 @@ public abstract class iCalendar extends Resource { // time zone helpers - protected static boolean hasTime(DateProperty date) { + protected static boolean isDateTime(DateProperty date) { return date.getDate() instanceof DateTime; } - protected static String getTzId(DateProperty date) { - if (date.isUtc() || !hasTime(date)) - return Time.TIMEZONE_UTC; - else if (date.getTimeZone() != null) - return date.getTimeZone().getID(); - else if (date.getParameter(Value.TZID) != null) - return date.getParameter(Value.TZID).getValue(); - - // fallback - return Time.TIMEZONE_UTC; - } - - /* guess matching Android timezone ID */ + /** + * 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 (date.isUtc() || !hasTime(date)) - return; + if (isDateTime(date)) { + final TimeZone tz = date.getTimeZone(); + if (tz == null) + return; + final String tzID = tz.getID(); + if (tzID == null) + return; - String tzID = getTzId(date); - if (tzID == null) - return; - - String localTZ = DateUtils.findAndroidTimezoneID(tzID); - date.setTimeZone(DateUtils.tzRegistry.getTimeZone(localTZ)); + String deviceTzID = DateUtils.findAndroidTimezoneID(tzID); + if (!tzID.equals(deviceTzID)) + date.setTimeZone(DateUtils.tzRegistry.getTimeZone(deviceTzID)); + } } - public static String TimezoneDefToTzId(String timezoneDef) throws IllegalArgumentException { + /** + * 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 { - if (timezoneDef != null) { - CalendarBuilder builder = new CalendarBuilder(); - net.fortuna.ical4j.model.Calendar cal = builder.build(new StringReader(timezoneDef)); - VTimeZone timezone = (VTimeZone)cal.getComponent(VTimeZone.VTIMEZONE); + 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 (Exception ex) { - Log.w(TAG, "Can't understand time zone definition, ignoring", ex); + } catch (IOException|ParserException e) { + Log.e(TAG, "Can't understand time zone definition", e); } - throw new IllegalArgumentException(); + return null; } } 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 24720ef7..be0193a4 100644 --- a/app/src/main/java/at/bitfire/davdroid/syncadapter/SyncManager.java +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/SyncManager.java @@ -19,8 +19,8 @@ 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.WebDavCollection; 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.HttpException; @@ -46,8 +46,8 @@ 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 collectionResource = remote.getCollection(); - local.updateMetaData(collectionResource); + final WebDavResource.Properties collectionProperties = remote.getCollection().getProperties(); + local.updateMetaData(collectionProperties); // PHASE 2: push local changes to server int deletedRemotely = pushDeleted(), @@ -62,7 +62,7 @@ public class SyncManager { } if (!syncMembers) { final String - currentCTag = collectionResource.getCTag(), + currentCTag = collectionProperties.getCTag(), lastCTag = local.getCTag(); Log.d(TAG, "Last local CTag = " + lastCTag + "; current remote CTag = " + currentCTag); if (currentCTag == null || !currentCTag.equals(lastCTag)) @@ -101,7 +101,7 @@ public class SyncManager { // update collection CTag Log.i(TAG, "Sync complete, fetching new CTag"); - local.setCTag(collectionResource.getCTag()); + local.setCTag(collectionProperties.getCTag()); } diff --git a/app/src/main/java/at/bitfire/davdroid/ui/setup/InstallAppsFragment.java b/app/src/main/java/at/bitfire/davdroid/ui/setup/InstallAppsFragment.java index d584e9a0..423fc353 100644 --- a/app/src/main/java/at/bitfire/davdroid/ui/setup/InstallAppsFragment.java +++ b/app/src/main/java/at/bitfire/davdroid/ui/setup/InstallAppsFragment.java @@ -9,7 +9,6 @@ package at.bitfire.davdroid.ui.setup; import android.app.Fragment; -import android.app.FragmentManager; import android.os.Bundle; import android.text.Html; import android.text.method.LinkMovementMethod; diff --git a/app/src/main/java/at/bitfire/davdroid/webdav/WebDavResource.java b/app/src/main/java/at/bitfire/davdroid/webdav/WebDavResource.java index bf3b2a17..99240e26 100644 --- a/app/src/main/java/at/bitfire/davdroid/webdav/WebDavResource.java +++ b/app/src/main/java/at/bitfire/davdroid/webdav/WebDavResource.java @@ -41,18 +41,19 @@ import java.io.StringWriter; import java.net.URI; import java.net.URISyntaxException; import java.util.Arrays; -import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Set; +import at.bitfire.davdroid.DAVUtils; import at.bitfire.davdroid.URIUtils; -import at.bitfire.davdroid.resource.Event; +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; @@ -64,15 +65,6 @@ import lombok.ToString; public class WebDavResource { private static final String TAG = "davdroid.WebDavResource"; - public enum Property { - CURRENT_USER_PRINCIPAL, // resource detection - ADDRESSBOOK_HOMESET, CALENDAR_HOMESET, - CONTENT_TYPE, READ_ONLY, // WebDAV (common) - DISPLAY_NAME, DESCRIPTION, ETAG, - IS_COLLECTION, CTAG, // collections - IS_CALENDAR, COLOR, TIMEZONE, // CalDAV - IS_ADDRESSBOOK, VCARD_VERSION // CardDAV - } public enum PutMode { ADD_DONT_OVERWRITE, UPDATE_DONT_OVERWRITE @@ -85,11 +77,8 @@ public class WebDavResource { protected Set capabilities = new HashSet<>(), methods = new HashSet<>(); - // DAV properties - protected HashMap properties = new HashMap<>(); - @Getter protected List supportedComponents; - // list of members (only for collections) + @Getter Properties properties = new Properties(); @Getter protected List members; // content (available after GET) @@ -151,9 +140,9 @@ public class WebDavResource { location = parent.location.resolve(new URI(null, null, "./" + member, null)); } - public WebDavResource(WebDavResource parent, String member, String ETag) throws URISyntaxException { + public WebDavResource(WebDavResource parent, String member, String eTag) throws URISyntaxException { this(parent, member); - properties.put(Property.ETAG, ETag); + properties.eTag = eTag; } @@ -191,76 +180,6 @@ public class WebDavResource { } - /* property methods */ - - public URI getCurrentUserPrincipal() throws URISyntaxException { - String principal = properties.get(Property.CURRENT_USER_PRINCIPAL); - return principal != null ? URIUtils.parseURI(principal, false) : null; - } - - public URI getAddressbookHomeSet() throws URISyntaxException { - String homeset = properties.get(Property.ADDRESSBOOK_HOMESET); - return homeset != null ? URIUtils.parseURI(homeset, false) : null; - } - - public URI getCalendarHomeSet() throws URISyntaxException { - String homeset = properties.get(Property.CALENDAR_HOMESET); - return homeset != null ? URIUtils.parseURI(homeset, false) : null; - } - - public String getContentType() { - return properties.get(Property.CONTENT_TYPE); - } - - public void setContentType(String mimeType) { - properties.put(Property.CONTENT_TYPE, mimeType); - } - - public boolean isReadOnly() { - return properties.containsKey(Property.READ_ONLY); - } - - public String getDisplayName() { - return properties.get(Property.DISPLAY_NAME); - } - - public String getDescription() { - return properties.get(Property.DESCRIPTION); - } - - public String getCTag() { - return properties.get(Property.CTAG); - } - public void invalidateCTag() { - properties.remove(Property.CTAG); - } - - public String getETag() { - return properties.get(Property.ETAG); - } - - public boolean isCalendar() { - return properties.containsKey(Property.IS_CALENDAR); - } - - public String getColor() { - return properties.get(Property.COLOR); - } - - public String getTimezone() { - return properties.get(Property.TIMEZONE); - } - - public boolean isAddressBook() { - return properties.containsKey(Property.IS_ADDRESSBOOK); - } - - public VCardVersion getVCardVersion() { - String versionStr = properties.get(Property.VCARD_VERSION); - return (versionStr != null) ? VCardVersion.valueOfByStr(versionStr) : null; - } - - /* collection operations */ public void propfind(HttpPropfind.Mode mode) throws URISyntaxException, IOException, DavException, HttpException { @@ -372,12 +291,12 @@ public class WebDavResource { put.addHeader("If-None-Match", "*"); break; case UPDATE_DONT_OVERWRITE: - put.addHeader("If-Match", (getETag() != null) ? getETag() : "*"); + put.addHeader("If-Match", (properties.eTag != null) ? properties.eTag : "*"); break; } - if (getContentType() != null) - put.addHeader("Content-Type", getContentType()); + if (properties.contentType != null) + put.addHeader("Content-Type", properties.contentType); @Cleanup CloseableHttpResponse response = httpClient.execute(put, context); checkResponse(response); @@ -392,8 +311,8 @@ public class WebDavResource { public void delete() throws URISyntaxException, IOException, HttpException { HttpDeleteHC4 delete = new HttpDeleteHC4(location); - if (getETag() != null) - delete.addHeader("If-Match", getETag()); + if (properties.eTag != null) + delete.addHeader("If-Match", properties.eTag); @Cleanup CloseableHttpResponse response = httpClient.execute(delete, context); checkResponse(response); @@ -473,8 +392,7 @@ public class WebDavResource { Log.d(TAG, "Processing multi-status element: " + href); // process known properties - HashMap properties = new HashMap<>(); - List supportedComponents = null; + Properties properties = new Properties(); byte[] data = null; // in , either or must be present @@ -488,84 +406,9 @@ public class WebDavResource { // ignore information about missing properties etc. if (status.getStatusCode()/100 != 1 && status.getStatusCode()/100 != 2) continue; + DavProp prop = singlePropstat.prop; - - if (prop.currentUserPrincipal != null && prop.currentUserPrincipal.getHref() != null) - properties.put(Property.CURRENT_USER_PRINCIPAL, prop.currentUserPrincipal.getHref().href); - - if (prop.currentUserPrivilegeSet != null) { - // privilege info available - boolean mayAll = false, - mayBind = false, - mayUnbind = false, - mayWrite = false, - mayWriteContent = false; - for (DavProp.Privilege privilege : prop.currentUserPrivilegeSet) { - if (privilege.getAll() != null) mayAll = true; - if (privilege.getBind() != null) mayBind = true; - if (privilege.getUnbind() != null) mayUnbind = true; - if (privilege.getWrite() != null) mayWrite = true; - if (privilege.getWriteContent() != null) mayWriteContent = true; - } - if (!mayAll && !mayWrite && !(mayWriteContent && mayBind && mayUnbind)) - properties.put(Property.READ_ONLY, "1"); - } - - if (prop.addressbookHomeSet != null && prop.addressbookHomeSet.getHref() != null) - properties.put(Property.ADDRESSBOOK_HOMESET, URIUtils.ensureTrailingSlash(prop.addressbookHomeSet.getHref().href)); - - if (prop.calendarHomeSet != null && prop.calendarHomeSet.getHref() != null) - properties.put(Property.CALENDAR_HOMESET, URIUtils.ensureTrailingSlash(prop.calendarHomeSet.getHref().href)); - - if (prop.displayname != null) - properties.put(Property.DISPLAY_NAME, prop.displayname.getDisplayName()); - - if (prop.resourcetype != null) { - if (prop.resourcetype.getCollection() != null) { - properties.put(Property.IS_COLLECTION, "1"); - // is a collection, ensure trailing slash - href = URIUtils.ensureTrailingSlash(href); - } - if (prop.resourcetype.getAddressbook() != null) { // CardDAV collection properties - properties.put(Property.IS_ADDRESSBOOK, "1"); - - if (prop.addressbookDescription != null) - properties.put(Property.DESCRIPTION, prop.addressbookDescription.getDescription()); - if (prop.supportedAddressData != null) - for (DavProp.AddressDataType dataType : prop.supportedAddressData) - if ("text/vcard".equalsIgnoreCase(dataType.getContentType())) - // ignore "3.0" as it MUST be supported anyway - if ("4.0".equals(dataType.getVersion())) - properties.put(Property.VCARD_VERSION, VCardVersion.V4_0.getVersion()); - } - if (prop.resourcetype.getCalendar() != null) { // CalDAV collection propertioes - properties.put(Property.IS_CALENDAR, "1"); - - if (prop.calendarDescription != null) - properties.put(Property.DESCRIPTION, prop.calendarDescription.getDescription()); - - if (prop.calendarColor != null) - properties.put(Property.COLOR, prop.calendarColor.getColor()); - - if (prop.calendarTimezone != null) - try { - properties.put(Property.TIMEZONE, Event.TimezoneDefToTzId(prop.calendarTimezone.getTimezone())); - } catch(IllegalArgumentException e) { - } - - if (prop.supportedCalendarComponentSet != null) { - supportedComponents = new LinkedList<>(); - for (Comp component : prop.supportedCalendarComponentSet) - supportedComponents.add(component.getName()); - } - } - } - - if (prop.getctag != null) - properties.put(Property.CTAG, prop.getctag.getCTag()); - - if (prop.getetag != null) - properties.put(Property.ETAG, prop.getetag.getETag()); + properties.process(prop); if (prop.calendarData != null && prop.calendarData.ical != null) data = prop.calendarData.ical.getBytes(); @@ -574,16 +417,16 @@ public class WebDavResource { } // 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.putAll(properties); - if (supportedComponents != null) - this.supportedComponents = supportedComponents; + this.properties = properties; this.content = data; } else { // about a member WebDavResource member = new WebDavResource(this, href); member.properties = properties; - member.supportedComponents = supportedComponents; member.content = data; members.add(member); @@ -593,4 +436,133 @@ public class WebDavResource { 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 String 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; + } + + } }